chore: merged latest develop

This commit is contained in:
CheetoDa 2024-04-02 18:19:20 +05:30
commit 93bdfd3d83
294 changed files with 7583 additions and 3208 deletions

View File

@ -146,7 +146,7 @@ services:
condition: on-failure condition: on-failure
query-service: query-service:
image: signoz/query-service:0.39.0 image: signoz/query-service:0.42.0
command: command:
[ [
"-config=/root/config/prometheus.yml", "-config=/root/config/prometheus.yml",
@ -186,7 +186,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:0.39.0 image: signoz/frontend:0.42.0
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:0.88.12 image: signoz/signoz-otel-collector:0.88.17
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",
@ -237,7 +237,7 @@ services:
- query-service - query-service
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.88.12 image: signoz/signoz-schema-migrator:0.88.17
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure

View File

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

View File

@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.39.0} image: signoz/query-service:${DOCKER_TAG:-0.42.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -203,7 +203,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.39.0} image: signoz/frontend:${DOCKER_TAG:-0.42.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -215,7 +215,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.17}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -229,7 +229,7 @@ services:
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.12} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.17}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
[ [

View File

@ -74,7 +74,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() defer r.Body.Close()
requestBody, err := io.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
zap.S().Errorf("received no input in api\n", err) zap.L().Error("received no input in api", zap.Error(err))
RespondError(w, model.BadRequest(err), nil) RespondError(w, model.BadRequest(err), nil)
return return
} }
@ -82,7 +82,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
err = json.Unmarshal(requestBody, &req) err = json.Unmarshal(requestBody, &req)
if err != nil { if err != nil {
zap.S().Errorf("received invalid user registration request", zap.Error(err)) zap.L().Error("received invalid user registration request", zap.Error(err))
RespondError(w, model.BadRequest(fmt.Errorf("failed to register user")), nil) RespondError(w, model.BadRequest(fmt.Errorf("failed to register user")), nil)
return return
} }
@ -90,13 +90,13 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
// get invite object // get invite object
invite, err := baseauth.ValidateInvite(ctx, req) invite, err := baseauth.ValidateInvite(ctx, req)
if err != nil { if err != nil {
zap.S().Errorf("failed to validate invite token", err) zap.L().Error("failed to validate invite token", zap.Error(err))
RespondError(w, model.BadRequest(err), nil) RespondError(w, model.BadRequest(err), nil)
return return
} }
if invite == nil { if invite == nil {
zap.S().Errorf("failed to validate invite token: it is either empty or invalid", err) zap.L().Error("failed to validate invite token: it is either empty or invalid", zap.Error(err))
RespondError(w, model.BadRequest(basemodel.ErrSignupFailed{}), nil) RespondError(w, model.BadRequest(basemodel.ErrSignupFailed{}), nil)
return return
} }
@ -104,7 +104,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
// get auth domain from email domain // get auth domain from email domain
domain, apierr := ah.AppDao().GetDomainByEmail(ctx, invite.Email) domain, apierr := ah.AppDao().GetDomainByEmail(ctx, invite.Email)
if apierr != nil { if apierr != nil {
zap.S().Errorf("failed to get domain from email", apierr) zap.L().Error("failed to get domain from email", zap.Error(apierr))
RespondError(w, model.InternalError(basemodel.ErrSignupFailed{}), nil) RespondError(w, model.InternalError(basemodel.ErrSignupFailed{}), nil)
} }
@ -205,24 +205,24 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
ctx := context.Background() ctx := context.Background()
if !ah.CheckFeature(model.SSO) { if !ah.CheckFeature(model.SSO) {
zap.S().Errorf("[receiveGoogleAuth] sso requested but feature unavailable %s in org domain %s", model.SSO) zap.L().Error("[receiveGoogleAuth] sso requested but feature unavailable in org domain")
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently) http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
return return
} }
q := r.URL.Query() q := r.URL.Query()
if errType := q.Get("error"); errType != "" { if errType := q.Get("error"); errType != "" {
zap.S().Errorf("[receiveGoogleAuth] failed to login with google auth", q.Get("error_description")) zap.L().Error("[receiveGoogleAuth] failed to login with google auth", zap.String("error", errType), zap.String("error_description", q.Get("error_description")))
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "failed to login through SSO "), http.StatusMovedPermanently) http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "failed to login through SSO "), http.StatusMovedPermanently)
return return
} }
relayState := q.Get("state") relayState := q.Get("state")
zap.S().Debug("[receiveGoogleAuth] relay state received", zap.String("state", relayState)) zap.L().Debug("[receiveGoogleAuth] relay state received", zap.String("state", relayState))
parsedState, err := url.Parse(relayState) parsedState, err := url.Parse(relayState)
if err != nil || relayState == "" { if err != nil || relayState == "" {
zap.S().Errorf("[receiveGoogleAuth] failed to process response - invalid response from IDP", err, r) zap.L().Error("[receiveGoogleAuth] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
@ -244,14 +244,14 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
identity, err := callbackHandler.HandleCallback(r) identity, err := callbackHandler.HandleCallback(r)
if err != nil { if err != nil {
zap.S().Errorf("[receiveGoogleAuth] failed to process HandleCallback ", domain.String(), zap.Error(err)) zap.L().Error("[receiveGoogleAuth] failed to process HandleCallback ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email) nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email)
if err != nil { if err != nil {
zap.S().Errorf("[receiveGoogleAuth] failed to generate redirect URI after successful login ", domain.String(), zap.Error(err)) zap.L().Error("[receiveGoogleAuth] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
@ -266,14 +266,14 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
ctx := context.Background() ctx := context.Background()
if !ah.CheckFeature(model.SSO) { if !ah.CheckFeature(model.SSO) {
zap.S().Errorf("[receiveSAML] sso requested but feature unavailable %s in org domain %s", model.SSO) zap.L().Error("[receiveSAML] sso requested but feature unavailable in org domain")
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently) http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
return return
} }
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
zap.S().Errorf("[receiveSAML] failed to process response - invalid response from IDP", err, r) zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
@ -281,11 +281,11 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
// the relay state is sent when a login request is submitted to // the relay state is sent when a login request is submitted to
// Idp. // Idp.
relayState := r.FormValue("RelayState") relayState := r.FormValue("RelayState")
zap.S().Debug("[receiveML] relay state", zap.String("relayState", relayState)) zap.L().Debug("[receiveML] relay state", zap.String("relayState", relayState))
parsedState, err := url.Parse(relayState) parsedState, err := url.Parse(relayState)
if err != nil || relayState == "" { if err != nil || relayState == "" {
zap.S().Errorf("[receiveSAML] failed to process response - invalid response from IDP", err, r) zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
@ -302,34 +302,34 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
sp, err := domain.PrepareSamlRequest(parsedState) sp, err := domain.PrepareSamlRequest(parsedState)
if err != nil { if err != nil {
zap.S().Errorf("[receiveSAML] failed to prepare saml request for domain (%s): %v", domain.String(), err) zap.L().Error("[receiveSAML] failed to prepare saml request for domain", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
assertionInfo, err := sp.RetrieveAssertionInfo(r.FormValue("SAMLResponse")) assertionInfo, err := sp.RetrieveAssertionInfo(r.FormValue("SAMLResponse"))
if err != nil { if err != nil {
zap.S().Errorf("[receiveSAML] failed to retrieve assertion info from saml response for organization (%s): %v", domain.String(), err) zap.L().Error("[receiveSAML] failed to retrieve assertion info from saml response", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
if assertionInfo.WarningInfo.InvalidTime { if assertionInfo.WarningInfo.InvalidTime {
zap.S().Errorf("[receiveSAML] expired saml response for organization (%s): %v", domain.String(), err) zap.L().Error("[receiveSAML] expired saml response", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
email := assertionInfo.NameID email := assertionInfo.NameID
if email == "" { if email == "" {
zap.S().Errorf("[receiveSAML] invalid email in the SSO response (%s)", domain.String()) zap.L().Error("[receiveSAML] invalid email in the SSO response", zap.String("domain", domain.String()))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email) nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email)
if err != nil { if err != nil {
zap.S().Errorf("[receiveSAML] failed to generate redirect URI after successful login ", domain.String(), zap.Error(err)) zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }

View File

@ -191,7 +191,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
url := fmt.Sprintf("%s/trial?licenseKey=%s", constants.LicenseSignozIo, currentActiveLicenseKey) url := fmt.Sprintf("%s/trial?licenseKey=%s", constants.LicenseSignozIo, currentActiveLicenseKey)
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
zap.S().Error("Error while creating request for trial details", err) zap.L().Error("Error while creating request for trial details", zap.Error(err))
// If there is an error in fetching trial details, we will still return the license details // If there is an error in fetching trial details, we will still return the license details
// to avoid blocking the UI // to avoid blocking the UI
ah.Respond(w, resp) ah.Respond(w, resp)
@ -200,7 +200,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey) req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey)
trialResp, err := hClient.Do(req) trialResp, err := hClient.Do(req)
if err != nil { if err != nil {
zap.S().Error("Error while fetching trial details", err) zap.L().Error("Error while fetching trial details", zap.Error(err))
// If there is an error in fetching trial details, we will still return the license details // If there is an error in fetching trial details, we will still return the license details
// to avoid incorrectly blocking the UI // to avoid incorrectly blocking the UI
ah.Respond(w, resp) ah.Respond(w, resp)
@ -211,7 +211,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
trialRespBody, err := io.ReadAll(trialResp.Body) trialRespBody, err := io.ReadAll(trialResp.Body)
if err != nil || trialResp.StatusCode != http.StatusOK { if err != nil || trialResp.StatusCode != http.StatusOK {
zap.S().Error("Error while fetching trial details", err) zap.L().Error("Error while fetching trial details", zap.Error(err))
// If there is an error in fetching trial details, we will still return the license details // If there is an error in fetching trial details, we will still return the license details
// to avoid incorrectly blocking the UI // to avoid incorrectly blocking the UI
ah.Respond(w, resp) ah.Respond(w, resp)
@ -222,7 +222,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
var trialRespData model.SubscriptionServerResp var trialRespData model.SubscriptionServerResp
if err := json.Unmarshal(trialRespBody, &trialRespData); err != nil { if err := json.Unmarshal(trialRespBody, &trialRespData); err != nil {
zap.S().Error("Error while decoding trial details", err) zap.L().Error("Error while decoding trial details", zap.Error(err))
// If there is an error in fetching trial details, we will still return the license details // If there is an error in fetching trial details, we will still return the license details
// to avoid incorrectly blocking the UI // to avoid incorrectly blocking the UI
ah.Respond(w, resp) ah.Respond(w, resp)

View File

@ -18,14 +18,14 @@ import (
func (ah *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request) { func (ah *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request) {
if !ah.CheckFeature(basemodel.CustomMetricsFunction) { if !ah.CheckFeature(basemodel.CustomMetricsFunction) {
zap.S().Info("CustomMetricsFunction feature is not enabled in this plan") zap.L().Info("CustomMetricsFunction feature is not enabled in this plan")
ah.APIHandler.QueryRangeMetricsV2(w, r) ah.APIHandler.QueryRangeMetricsV2(w, r)
return return
} }
metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r) metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r)
if apiErrorObj != nil { if apiErrorObj != nil {
zap.S().Errorf(apiErrorObj.Err.Error()) zap.L().Error("Error in parsing metric query params", zap.Error(apiErrorObj.Err))
RespondError(w, apiErrorObj, nil) RespondError(w, apiErrorObj, nil)
return return
} }

View File

@ -43,8 +43,8 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
return return
} }
pat := model.PAT{ pat := model.PAT{
Name: req.Name, Name: req.Name,
Role: req.Role, Role: req.Role,
ExpiresAt: req.ExpiresInDays, ExpiresAt: req.ExpiresInDays,
} }
err = validatePATRequest(pat) err = validatePATRequest(pat)
@ -65,7 +65,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
pat.ExpiresAt = time.Now().Unix() + (pat.ExpiresAt * 24 * 60 * 60) pat.ExpiresAt = time.Now().Unix() + (pat.ExpiresAt * 24 * 60 * 60)
} }
zap.S().Debugf("Got Create PAT request: %+v", pat) zap.L().Info("Got Create PAT request", zap.Any("pat", pat))
var apierr basemodel.BaseApiError var apierr basemodel.BaseApiError
if pat, apierr = ah.AppDao().CreatePAT(ctx, pat); apierr != nil { if pat, apierr = ah.AppDao().CreatePAT(ctx, pat); apierr != nil {
RespondError(w, apierr, nil) RespondError(w, apierr, nil)
@ -115,7 +115,7 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
req.UpdatedByUserID = user.Id req.UpdatedByUserID = user.Id
id := mux.Vars(r)["id"] id := mux.Vars(r)["id"]
req.UpdatedAt = time.Now().Unix() req.UpdatedAt = time.Now().Unix()
zap.S().Debugf("Got Update PAT request: %+v", req) zap.L().Info("Got Update PAT request", zap.Any("pat", req))
var apierr basemodel.BaseApiError var apierr basemodel.BaseApiError
if apierr = ah.AppDao().UpdatePAT(ctx, req, id); apierr != nil { if apierr = ah.AppDao().UpdatePAT(ctx, req, id); apierr != nil {
RespondError(w, apierr, nil) RespondError(w, apierr, nil)
@ -135,7 +135,7 @@ func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
}, nil) }, nil)
return return
} }
zap.S().Infof("Get PATs for user: %+v", user.Id) zap.L().Info("Get PATs for user", zap.String("user_id", user.Id))
pats, apierr := ah.AppDao().ListPATs(ctx) pats, apierr := ah.AppDao().ListPATs(ctx)
if apierr != nil { if apierr != nil {
RespondError(w, apierr, nil) RespondError(w, apierr, nil)
@ -156,7 +156,7 @@ func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) {
return return
} }
zap.S().Debugf("Revoke PAT with id: %+v", id) zap.L().Info("Revoke PAT with id", zap.String("id", id))
if apierr := ah.AppDao().RevokePAT(ctx, id, user.Id); apierr != nil { if apierr := ah.AppDao().RevokePAT(ctx, id, user.Id); apierr != nil {
RespondError(w, apierr, nil) RespondError(w, apierr, nil)
return return

View File

@ -15,7 +15,7 @@ import (
func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) { func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) {
if !ah.CheckFeature(basemodel.SmartTraceDetail) { if !ah.CheckFeature(basemodel.SmartTraceDetail) {
zap.S().Info("SmartTraceDetail feature is not enabled in this plan") zap.L().Info("SmartTraceDetail feature is not enabled in this plan")
ah.APIHandler.SearchTraces(w, r) ah.APIHandler.SearchTraces(w, r)
return return
} }
@ -26,7 +26,7 @@ func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) {
} }
spanLimit, err := strconv.Atoi(constants.SpanLimitStr) spanLimit, err := strconv.Atoi(constants.SpanLimitStr)
if err != nil { if err != nil {
zap.S().Error("Error during strconv.Atoi() on SPAN_LIMIT env variable: ", err) zap.L().Error("Error during strconv.Atoi() on SPAN_LIMIT env variable", zap.Error(err))
return return
} }
result, err := ah.opts.DataConnector.SearchTraces(r.Context(), traceId, spanId, levelUpInt, levelDownInt, spanLimit, db.SmartTraceAlgorithm) result, err := ah.opts.DataConnector.SearchTraces(r.Context(), traceId, spanId, levelUpInt, levelDownInt, spanLimit, db.SmartTraceAlgorithm)

View File

@ -22,7 +22,7 @@ import (
func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string) ([]*basemodel.Series, string, error) { func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string) ([]*basemodel.Series, string, error) {
defer utils.Elapsed("GetMetricResult")() defer utils.Elapsed("GetMetricResult")()
zap.S().Infof("Executing metric result query: %s", query) zap.L().Info("Executing metric result query: ", zap.String("query", query))
var hash string var hash string
// If getSubTreeSpans function is used in the clickhouse query // If getSubTreeSpans function is used in the clickhouse query
@ -38,9 +38,8 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string)
} }
rows, err := r.conn.Query(ctx, query) rows, err := r.conn.Query(ctx, query)
zap.S().Debug(query)
if err != nil { if err != nil {
zap.S().Debug("Error in processing query: ", err) zap.L().Error("Error in processing query", zap.Error(err))
return nil, "", fmt.Errorf("error in processing query") return nil, "", fmt.Errorf("error in processing query")
} }
@ -117,7 +116,7 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string)
groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int()) groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int())
} }
default: default:
zap.S().Errorf("invalid var found in metric builder query result", v, colName) zap.L().Error("invalid var found in metric builder query result", zap.Any("var", v), zap.String("colName", colName))
} }
} }
sort.Strings(groupBy) sort.Strings(groupBy)
@ -140,7 +139,7 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string)
} }
// err = r.conn.Exec(ctx, "DROP TEMPORARY TABLE IF EXISTS getSubTreeSpans"+hash) // err = r.conn.Exec(ctx, "DROP TEMPORARY TABLE IF EXISTS getSubTreeSpans"+hash)
// if err != nil { // if err != nil {
// zap.S().Error("Error in dropping temporary table: ", err) // zap.L().Error("Error in dropping temporary table: ", err)
// return nil, err // return nil, err
// } // }
if hash == "" { if hash == "" {
@ -152,7 +151,7 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string)
func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, query string, hash string) (string, string, error) { func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, query string, hash string) (string, string, error) {
zap.S().Debugf("Executing getSubTreeSpans function") zap.L().Debug("Executing getSubTreeSpans function")
// str1 := `select fromUnixTimestamp64Milli(intDiv( toUnixTimestamp64Milli ( timestamp ), 100) * 100) AS interval, toFloat64(count()) as count from (select timestamp, spanId, parentSpanId, durationNano from getSubTreeSpans(select * from signoz_traces.signoz_index_v2 where serviceName='frontend' and name='/driver.DriverService/FindNearest' and traceID='00000000000000004b0a863cb5ed7681') where name='FindDriverIDs' group by interval order by interval asc;` // str1 := `select fromUnixTimestamp64Milli(intDiv( toUnixTimestamp64Milli ( timestamp ), 100) * 100) AS interval, toFloat64(count()) as count from (select timestamp, spanId, parentSpanId, durationNano from getSubTreeSpans(select * from signoz_traces.signoz_index_v2 where serviceName='frontend' and name='/driver.DriverService/FindNearest' and traceID='00000000000000004b0a863cb5ed7681') where name='FindDriverIDs' group by interval order by interval asc;`
@ -162,28 +161,28 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
err := r.conn.Exec(ctx, "DROP TABLE IF EXISTS getSubTreeSpans"+hash) err := r.conn.Exec(ctx, "DROP TABLE IF EXISTS getSubTreeSpans"+hash)
if err != nil { if err != nil {
zap.S().Error("Error in dropping temporary table: ", err) zap.L().Error("Error in dropping temporary table", zap.Error(err))
return query, hash, err return query, hash, err
} }
// Create temporary table to store the getSubTreeSpans() results // Create temporary table to store the getSubTreeSpans() results
zap.S().Debugf("Creating temporary table getSubTreeSpans%s", hash) zap.L().Debug("Creating temporary table getSubTreeSpans", zap.String("hash", hash))
err = r.conn.Exec(ctx, "CREATE TABLE IF NOT EXISTS "+"getSubTreeSpans"+hash+" (timestamp DateTime64(9) CODEC(DoubleDelta, LZ4), traceID FixedString(32) CODEC(ZSTD(1)), spanID String CODEC(ZSTD(1)), parentSpanID String CODEC(ZSTD(1)), rootSpanID String CODEC(ZSTD(1)), serviceName LowCardinality(String) CODEC(ZSTD(1)), name LowCardinality(String) CODEC(ZSTD(1)), rootName LowCardinality(String) CODEC(ZSTD(1)), durationNano UInt64 CODEC(T64, ZSTD(1)), kind Int8 CODEC(T64, ZSTD(1)), tagMap Map(LowCardinality(String), String) CODEC(ZSTD(1)), events Array(String) CODEC(ZSTD(2))) ENGINE = MergeTree() ORDER BY (timestamp)") err = r.conn.Exec(ctx, "CREATE TABLE IF NOT EXISTS "+"getSubTreeSpans"+hash+" (timestamp DateTime64(9) CODEC(DoubleDelta, LZ4), traceID FixedString(32) CODEC(ZSTD(1)), spanID String CODEC(ZSTD(1)), parentSpanID String CODEC(ZSTD(1)), rootSpanID String CODEC(ZSTD(1)), serviceName LowCardinality(String) CODEC(ZSTD(1)), name LowCardinality(String) CODEC(ZSTD(1)), rootName LowCardinality(String) CODEC(ZSTD(1)), durationNano UInt64 CODEC(T64, ZSTD(1)), kind Int8 CODEC(T64, ZSTD(1)), tagMap Map(LowCardinality(String), String) CODEC(ZSTD(1)), events Array(String) CODEC(ZSTD(2))) ENGINE = MergeTree() ORDER BY (timestamp)")
if err != nil { if err != nil {
zap.S().Error("Error in creating temporary table: ", err) zap.L().Error("Error in creating temporary table", zap.Error(err))
return query, hash, err return query, hash, err
} }
var getSpansSubQueryDBResponses []model.GetSpansSubQueryDBResponse var getSpansSubQueryDBResponses []model.GetSpansSubQueryDBResponse
getSpansSubQuery := subtreeInput getSpansSubQuery := subtreeInput
// Execute the subTree query // Execute the subTree query
zap.S().Debugf("Executing subTree query: %s", getSpansSubQuery) zap.L().Debug("Executing subTree query", zap.String("query", getSpansSubQuery))
err = r.conn.Select(ctx, &getSpansSubQueryDBResponses, getSpansSubQuery) err = r.conn.Select(ctx, &getSpansSubQueryDBResponses, getSpansSubQuery)
// zap.S().Info(getSpansSubQuery) // zap.L().Info(getSpansSubQuery)
if err != nil { if err != nil {
zap.S().Debug("Error in processing sql query: ", err) zap.L().Error("Error in processing sql query", zap.Error(err))
return query, hash, fmt.Errorf("Error in processing sql query") return query, hash, fmt.Errorf("Error in processing sql query")
} }
@ -196,16 +195,16 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
if len(getSpansSubQueryDBResponses) == 0 { if len(getSpansSubQueryDBResponses) == 0 {
return query, hash, fmt.Errorf("No spans found for the given query") return query, hash, fmt.Errorf("No spans found for the given query")
} }
zap.S().Debugf("Executing query to fetch all the spans from the same TraceID: %s", modelQuery) zap.L().Debug("Executing query to fetch all the spans from the same TraceID: ", zap.String("modelQuery", modelQuery))
err = r.conn.Select(ctx, &searchScanResponses, modelQuery, getSpansSubQueryDBResponses[0].TraceID) err = r.conn.Select(ctx, &searchScanResponses, modelQuery, getSpansSubQueryDBResponses[0].TraceID)
if err != nil { if err != nil {
zap.S().Debug("Error in processing sql query: ", err) zap.L().Error("Error in processing sql query", zap.Error(err))
return query, hash, fmt.Errorf("Error in processing sql query") return query, hash, fmt.Errorf("Error in processing sql query")
} }
// Process model to fetch the spans // Process model to fetch the spans
zap.S().Debugf("Processing model to fetch the spans") zap.L().Debug("Processing model to fetch the spans")
searchSpanResponses := []basemodel.SearchSpanResponseItem{} searchSpanResponses := []basemodel.SearchSpanResponseItem{}
for _, item := range searchScanResponses { for _, item := range searchScanResponses {
var jsonItem basemodel.SearchSpanResponseItem var jsonItem basemodel.SearchSpanResponseItem
@ -218,17 +217,17 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
} }
// Build the subtree and store all the subtree spans in temporary table getSubTreeSpans+hash // Build the subtree and store all the subtree spans in temporary table getSubTreeSpans+hash
// Use map to store pointer to the spans to avoid duplicates and save memory // Use map to store pointer to the spans to avoid duplicates and save memory
zap.S().Debugf("Building the subtree to store all the subtree spans in temporary table getSubTreeSpans%s", hash) zap.L().Debug("Building the subtree to store all the subtree spans in temporary table getSubTreeSpans", zap.String("hash", hash))
treeSearchResponse, err := getSubTreeAlgorithm(searchSpanResponses, getSpansSubQueryDBResponses) treeSearchResponse, err := getSubTreeAlgorithm(searchSpanResponses, getSpansSubQueryDBResponses)
if err != nil { if err != nil {
zap.S().Error("Error in getSubTreeAlgorithm function: ", err) zap.L().Error("Error in getSubTreeAlgorithm function", zap.Error(err))
return query, hash, err return query, hash, err
} }
zap.S().Debugf("Preparing batch to store subtree spans in temporary table getSubTreeSpans%s", hash) zap.L().Debug("Preparing batch to store subtree spans in temporary table getSubTreeSpans", zap.String("hash", hash))
statement, err := r.conn.PrepareBatch(context.Background(), fmt.Sprintf("INSERT INTO getSubTreeSpans"+hash)) statement, err := r.conn.PrepareBatch(context.Background(), fmt.Sprintf("INSERT INTO getSubTreeSpans"+hash))
if err != nil { if err != nil {
zap.S().Error("Error in preparing batch statement: ", err) zap.L().Error("Error in preparing batch statement", zap.Error(err))
return query, hash, err return query, hash, err
} }
for _, span := range treeSearchResponse { for _, span := range treeSearchResponse {
@ -251,14 +250,14 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
span.Events, span.Events,
) )
if err != nil { if err != nil {
zap.S().Debug("Error in processing sql query: ", err) zap.L().Error("Error in processing sql query", zap.Error(err))
return query, hash, err return query, hash, err
} }
} }
zap.S().Debugf("Inserting the subtree spans in temporary table getSubTreeSpans%s", hash) zap.L().Debug("Inserting the subtree spans in temporary table getSubTreeSpans", zap.String("hash", hash))
err = statement.Send() err = statement.Send()
if err != nil { if err != nil {
zap.S().Error("Error in sending statement: ", err) zap.L().Error("Error in sending statement", zap.Error(err))
return query, hash, err return query, hash, err
} }
return query, hash, nil return query, hash, nil
@ -323,7 +322,7 @@ func getSubTreeAlgorithm(payload []basemodel.SearchSpanResponseItem, getSpansSub
spans = append(spans, span) spans = append(spans, span)
} }
zap.S().Debug("Building Tree") zap.L().Debug("Building Tree")
roots, err := buildSpanTrees(&spans) roots, err := buildSpanTrees(&spans)
if err != nil { if err != nil {
return nil, err return nil, err
@ -333,7 +332,7 @@ func getSubTreeAlgorithm(payload []basemodel.SearchSpanResponseItem, getSpansSub
// For each root, get the subtree spans // For each root, get the subtree spans
for _, getSpansSubQueryDBResponse := range getSpansSubQueryDBResponses { for _, getSpansSubQueryDBResponse := range getSpansSubQueryDBResponses {
targetSpan := &model.SpanForTraceDetails{} targetSpan := &model.SpanForTraceDetails{}
// zap.S().Debug("Building tree for span id: " + getSpansSubQueryDBResponse.SpanID + " " + strconv.Itoa(i+1) + " of " + strconv.Itoa(len(getSpansSubQueryDBResponses))) // zap.L().Debug("Building tree for span id: " + getSpansSubQueryDBResponse.SpanID + " " + strconv.Itoa(i+1) + " of " + strconv.Itoa(len(getSpansSubQueryDBResponses)))
// Search target span object in the tree // Search target span object in the tree
for _, root := range roots { for _, root := range roots {
targetSpan, err = breadthFirstSearch(root, getSpansSubQueryDBResponse.SpanID) targetSpan, err = breadthFirstSearch(root, getSpansSubQueryDBResponse.SpanID)
@ -341,7 +340,7 @@ func getSubTreeAlgorithm(payload []basemodel.SearchSpanResponseItem, getSpansSub
break break
} }
if err != nil { if err != nil {
zap.S().Error("Error during BreadthFirstSearch(): ", err) zap.L().Error("Error during BreadthFirstSearch()", zap.Error(err))
return nil, err return nil, err
} }
} }

View File

@ -49,7 +49,7 @@ func SmartTraceAlgorithm(payload []basemodel.SearchSpanResponseItem, targetSpanI
break break
} }
if err != nil { if err != nil {
zap.S().Error("Error during BreadthFirstSearch(): ", err) zap.L().Error("Error during BreadthFirstSearch()", zap.Error(err))
return nil, err return nil, err
} }
} }
@ -186,7 +186,7 @@ func buildSpanTrees(spansPtr *[]*model.SpanForTraceDetails) ([]*model.SpanForTra
// If the parent span is not found, add current span to list of roots // If the parent span is not found, add current span to list of roots
if parent == nil { if parent == nil {
// zap.S().Debug("Parent Span not found parent_id: ", span.ParentID) // zap.L().Debug("Parent Span not found parent_id: ", span.ParentID)
roots = append(roots, span) roots = append(roots, span)
span.ParentID = "" span.ParentID = ""
continue continue

View File

@ -134,7 +134,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
var reader interfaces.DataConnector var reader interfaces.DataConnector
storage := os.Getenv("STORAGE") storage := os.Getenv("STORAGE")
if storage == "clickhouse" { if storage == "clickhouse" {
zap.S().Info("Using ClickHouse as datastore ...") zap.L().Info("Using ClickHouse as datastore ...")
qb := db.NewDataConnector( qb := db.NewDataConnector(
localDB, localDB,
serverOptions.PromConfigPath, serverOptions.PromConfigPath,
@ -419,30 +419,33 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
signozMetricsUsed := false signozMetricsUsed := false
signozLogsUsed := false signozLogsUsed := false
dataSources := []string{} signozTracesUsed := false
if postData != nil { if postData != nil {
if postData.CompositeQuery != nil { if postData.CompositeQuery != nil {
data["queryType"] = postData.CompositeQuery.QueryType data["queryType"] = postData.CompositeQuery.QueryType
data["panelType"] = postData.CompositeQuery.PanelType data["panelType"] = postData.CompositeQuery.PanelType
signozLogsUsed, signozMetricsUsed, _ = telemetry.GetInstance().CheckSigNozSignals(postData) signozLogsUsed, signozMetricsUsed, signozTracesUsed = telemetry.GetInstance().CheckSigNozSignals(postData)
} }
} }
if signozMetricsUsed || signozLogsUsed { if signozMetricsUsed || signozLogsUsed || signozTracesUsed {
if signozMetricsUsed { if signozMetricsUsed {
dataSources = append(dataSources, "metrics")
telemetry.GetInstance().AddActiveMetricsUser() telemetry.GetInstance().AddActiveMetricsUser()
} }
if signozLogsUsed { if signozLogsUsed {
dataSources = append(dataSources, "logs")
telemetry.GetInstance().AddActiveLogsUser() telemetry.GetInstance().AddActiveLogsUser()
} }
data["dataSources"] = dataSources if signozTracesUsed {
telemetry.GetInstance().AddActiveTracesUser()
}
data["metricsUsed"] = signozMetricsUsed
data["logsUsed"] = signozLogsUsed
data["tracesUsed"] = signozTracesUsed
userEmail, err := baseauth.GetEmailFromJwt(r.Context()) userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil { if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_V3, data, userEmail, true) telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail, true, false)
} }
} }
return data, true return data, true
@ -485,7 +488,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
if _, ok := telemetry.EnabledPaths()[path]; ok { if _, ok := telemetry.EnabledPaths()[path]; ok {
userEmail, err := baseauth.GetEmailFromJwt(r.Context()) userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil { if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail) telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail, true, false)
} }
} }
@ -522,7 +525,7 @@ func (s *Server) initListeners() error {
return err return err
} }
zap.S().Info(fmt.Sprintf("Query server started listening on %s...", s.serverOptions.HTTPHostPort)) zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.serverOptions.HTTPHostPort))
// listen on private port to support internal services // listen on private port to support internal services
privateHostPort := s.serverOptions.PrivateHostPort privateHostPort := s.serverOptions.PrivateHostPort
@ -535,7 +538,7 @@ func (s *Server) initListeners() error {
if err != nil { if err != nil {
return err return err
} }
zap.S().Info(fmt.Sprintf("Query server started listening on private port %s...", s.serverOptions.PrivateHostPort)) zap.L().Info(fmt.Sprintf("Query server started listening on private port %s...", s.serverOptions.PrivateHostPort))
return nil return nil
} }
@ -547,7 +550,7 @@ func (s *Server) Start() error {
if !s.serverOptions.DisableRules { if !s.serverOptions.DisableRules {
s.ruleManager.Start() s.ruleManager.Start()
} else { } else {
zap.S().Info("msg: Rules disabled as rules.disable is set to TRUE") zap.L().Info("msg: Rules disabled as rules.disable is set to TRUE")
} }
err := s.initListeners() err := s.initListeners()
@ -561,23 +564,23 @@ func (s *Server) Start() error {
} }
go func() { go func() {
zap.S().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.serverOptions.HTTPHostPort)) zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.serverOptions.HTTPHostPort))
switch err := s.httpServer.Serve(s.httpConn); err { switch err := s.httpServer.Serve(s.httpConn); err {
case nil, http.ErrServerClosed, cmux.ErrListenerClosed: case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
// normal exit, nothing to do // normal exit, nothing to do
default: default:
zap.S().Error("Could not start HTTP server", zap.Error(err)) zap.L().Error("Could not start HTTP server", zap.Error(err))
} }
s.unavailableChannel <- healthcheck.Unavailable s.unavailableChannel <- healthcheck.Unavailable
}() }()
go func() { go func() {
zap.S().Info("Starting pprof server", zap.String("addr", baseconst.DebugHttpPort)) zap.L().Info("Starting pprof server", zap.String("addr", baseconst.DebugHttpPort))
err = http.ListenAndServe(baseconst.DebugHttpPort, nil) err = http.ListenAndServe(baseconst.DebugHttpPort, nil)
if err != nil { if err != nil {
zap.S().Error("Could not start pprof server", zap.Error(err)) zap.L().Error("Could not start pprof server", zap.Error(err))
} }
}() }()
@ -587,14 +590,14 @@ func (s *Server) Start() error {
} }
go func() { go func() {
zap.S().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort)) zap.L().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort))
switch err := s.privateHTTP.Serve(s.privateConn); err { switch err := s.privateHTTP.Serve(s.privateConn); err {
case nil, http.ErrServerClosed, cmux.ErrListenerClosed: case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
// normal exit, nothing to do // normal exit, nothing to do
zap.S().Info("private http server closed") zap.L().Info("private http server closed")
default: default:
zap.S().Error("Could not start private HTTP server", zap.Error(err)) zap.L().Error("Could not start private HTTP server", zap.Error(err))
} }
s.unavailableChannel <- healthcheck.Unavailable s.unavailableChannel <- healthcheck.Unavailable
@ -602,10 +605,10 @@ func (s *Server) Start() error {
}() }()
go func() { go func() {
zap.S().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint)) zap.L().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint))
err := s.opampServer.Start(baseconst.OpAmpWsEndpoint) err := s.opampServer.Start(baseconst.OpAmpWsEndpoint)
if err != nil { if err != nil {
zap.S().Info("opamp ws server failed to start", err) zap.L().Error("opamp ws server failed to start", zap.Error(err))
s.unavailableChannel <- healthcheck.Unavailable s.unavailableChannel <- healthcheck.Unavailable
} }
}() }()
@ -681,7 +684,7 @@ func makeRulesManager(
return nil, fmt.Errorf("rule manager error: %v", err) return nil, fmt.Errorf("rule manager error: %v", err)
} }
zap.S().Info("rules manager is ready") zap.L().Info("rules manager is ready")
return manager, nil return manager, nil
} }

View File

@ -17,25 +17,25 @@ import (
func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) { func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) {
patToken := r.Header.Get("SIGNOZ-API-KEY") patToken := r.Header.Get("SIGNOZ-API-KEY")
if len(patToken) > 0 { if len(patToken) > 0 {
zap.S().Debugf("Received a non-zero length PAT token") zap.L().Debug("Received a non-zero length PAT token")
ctx := context.Background() ctx := context.Background()
dao := apiHandler.AppDao() dao := apiHandler.AppDao()
pat, err := dao.GetPAT(ctx, patToken) pat, err := dao.GetPAT(ctx, patToken)
if err == nil && pat != nil { if err == nil && pat != nil {
zap.S().Debugf("Found valid PAT: %+v", pat) zap.L().Debug("Found valid PAT: ", zap.Any("pat", pat))
if pat.ExpiresAt < time.Now().Unix() && pat.ExpiresAt != 0 { if pat.ExpiresAt < time.Now().Unix() && pat.ExpiresAt != 0 {
zap.S().Debugf("PAT has expired: %+v", pat) zap.L().Info("PAT has expired: ", zap.Any("pat", pat))
return nil, fmt.Errorf("PAT has expired") return nil, fmt.Errorf("PAT has expired")
} }
group, apiErr := dao.GetGroupByName(ctx, pat.Role) group, apiErr := dao.GetGroupByName(ctx, pat.Role)
if apiErr != nil { if apiErr != nil {
zap.S().Debugf("Error while getting group for PAT: %+v", apiErr) zap.L().Error("Error while getting group for PAT: ", zap.Any("apiErr", apiErr))
return nil, apiErr return nil, apiErr
} }
user, err := dao.GetUser(ctx, pat.UserID) user, err := dao.GetUser(ctx, pat.UserID)
if err != nil { if err != nil {
zap.S().Debugf("Error while getting user for PAT: %+v", err) zap.L().Error("Error while getting user for PAT: ", zap.Error(err))
return nil, err return nil, err
} }
telemetry.GetInstance().SetPatTokenUser() telemetry.GetInstance().SetPatTokenUser()
@ -48,7 +48,7 @@ func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel
}, nil }, nil
} }
if err != nil { if err != nil {
zap.S().Debugf("Error while getting user for PAT: %+v", err) zap.L().Error("Error while getting user for PAT: ", zap.Error(err))
return nil, err return nil, err
} }
} }

View File

@ -22,19 +22,19 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
domain, apierr := m.GetDomainByEmail(ctx, email) domain, apierr := m.GetDomainByEmail(ctx, email)
if apierr != nil { if apierr != nil {
zap.S().Errorf("failed to get domain from email", apierr) zap.L().Error("failed to get domain from email", zap.Error(apierr))
return nil, model.InternalErrorStr("failed to get domain from email") return nil, model.InternalErrorStr("failed to get domain from email")
} }
hash, err := baseauth.PasswordHash(utils.GeneratePassowrd()) hash, err := baseauth.PasswordHash(utils.GeneratePassowrd())
if err != nil { if err != nil {
zap.S().Errorf("failed to generate password hash when registering a user via SSO redirect", zap.Error(err)) zap.L().Error("failed to generate password hash when registering a user via SSO redirect", zap.Error(err))
return nil, model.InternalErrorStr("failed to generate password hash") return nil, model.InternalErrorStr("failed to generate password hash")
} }
group, apiErr := m.GetGroupByName(ctx, baseconst.ViewerGroup) group, apiErr := m.GetGroupByName(ctx, baseconst.ViewerGroup)
if apiErr != nil { if apiErr != nil {
zap.S().Debugf("GetGroupByName failed, err: %v\n", apiErr.Err) zap.L().Error("GetGroupByName failed", zap.Error(apiErr))
return nil, apiErr return nil, apiErr
} }
@ -51,7 +51,7 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
user, apiErr = m.CreateUser(ctx, user, false) user, apiErr = m.CreateUser(ctx, user, false)
if apiErr != nil { if apiErr != nil {
zap.S().Debugf("CreateUser failed, err: %v\n", apiErr.Err) zap.L().Error("CreateUser failed", zap.Error(apiErr))
return nil, apiErr return nil, apiErr
} }
@ -65,7 +65,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
userPayload, apierr := m.GetUserByEmail(ctx, email) userPayload, apierr := m.GetUserByEmail(ctx, email)
if !apierr.IsNil() { if !apierr.IsNil() {
zap.S().Errorf(" failed to get user with email received from auth provider", apierr.Error()) zap.L().Error("failed to get user with email received from auth provider", zap.String("error", apierr.Error()))
return "", model.BadRequestStr("invalid user email received from the auth provider") return "", model.BadRequestStr("invalid user email received from the auth provider")
} }
@ -75,7 +75,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
newUser, apiErr := m.createUserForSAMLRequest(ctx, email) newUser, apiErr := m.createUserForSAMLRequest(ctx, email)
user = newUser user = newUser
if apiErr != nil { if apiErr != nil {
zap.S().Errorf("failed to create user with email received from auth provider: %v", apierr.Error()) zap.L().Error("failed to create user with email received from auth provider", zap.Error(apiErr))
return "", apiErr return "", apiErr
} }
} else { } else {
@ -84,7 +84,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
tokenStore, err := baseauth.GenerateJWTForUser(user) tokenStore, err := baseauth.GenerateJWTForUser(user)
if err != nil { if err != nil {
zap.S().Errorf("failed to generate token for SSO login user", err) zap.L().Error("failed to generate token for SSO login user", zap.Error(err))
return "", model.InternalErrorStr("failed to generate token for the user") return "", model.InternalErrorStr("failed to generate token for the user")
} }
@ -143,8 +143,8 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
// do nothing, just skip sso // do nothing, just skip sso
ssoAvailable = false ssoAvailable = false
default: default:
zap.S().Errorf("feature check failed", zap.String("featureKey", model.SSO), zap.Error(err)) zap.L().Error("feature check failed", zap.String("featureKey", model.SSO), zap.Error(err))
return resp, model.BadRequest(err) return resp, model.BadRequestStr(err.Error())
} }
} }
@ -160,7 +160,7 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
if len(emailComponents) > 0 { if len(emailComponents) > 0 {
emailDomain = emailComponents[1] emailDomain = emailComponents[1]
} }
zap.S().Errorf("failed to get org domain from email", zap.String("emailDomain", emailDomain), apierr.ToError()) zap.L().Error("failed to get org domain from email", zap.String("emailDomain", emailDomain), zap.Error(apierr.ToError()))
return resp, apierr return resp, apierr
} }
@ -176,7 +176,7 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
escapedUrl, _ := url.QueryUnescape(sourceUrl) escapedUrl, _ := url.QueryUnescape(sourceUrl)
siteUrl, err := url.Parse(escapedUrl) siteUrl, err := url.Parse(escapedUrl)
if err != nil { if err != nil {
zap.S().Errorf("failed to parse referer", err) zap.L().Error("failed to parse referer", zap.Error(err))
return resp, model.InternalError(fmt.Errorf("failed to generate login request")) return resp, model.InternalError(fmt.Errorf("failed to generate login request"))
} }
@ -185,7 +185,7 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
resp.SsoUrl, err = orgDomain.BuildSsoUrl(siteUrl) resp.SsoUrl, err = orgDomain.BuildSsoUrl(siteUrl)
if err != nil { if err != nil {
zap.S().Errorf("failed to prepare saml request for domain", zap.String("domain", orgDomain.Name), err) zap.L().Error("failed to prepare saml request for domain", zap.String("domain", orgDomain.Name), zap.Error(err))
return resp, model.InternalError(err) return resp, model.InternalError(err)
} }

View File

@ -48,13 +48,13 @@ func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url
if domainIdStr != "" { if domainIdStr != "" {
domainId, err := uuid.Parse(domainIdStr) domainId, err := uuid.Parse(domainIdStr)
if err != nil { if err != nil {
zap.S().Errorf("failed to parse domainId from relay state", err) zap.L().Error("failed to parse domainId from relay state", zap.Error(err))
return nil, fmt.Errorf("failed to parse domainId from IdP response") return nil, fmt.Errorf("failed to parse domainId from IdP response")
} }
domain, err = m.GetDomain(ctx, domainId) domain, err = m.GetDomain(ctx, domainId)
if (err != nil) || domain == nil { if (err != nil) || domain == nil {
zap.S().Errorf("failed to find domain from domainId received in IdP response", err.Error()) zap.L().Error("failed to find domain from domainId received in IdP response", zap.Error(err))
return nil, fmt.Errorf("invalid credentials") return nil, fmt.Errorf("invalid credentials")
} }
} }
@ -64,7 +64,7 @@ func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url
domainFromDB, err := m.GetDomainByName(ctx, domainNameStr) domainFromDB, err := m.GetDomainByName(ctx, domainNameStr)
domain = domainFromDB domain = domainFromDB
if (err != nil) || domain == nil { if (err != nil) || domain == nil {
zap.S().Errorf("failed to find domain from domainName received in IdP response", err.Error()) zap.L().Error("failed to find domain from domainName received in IdP response", zap.Error(err))
return nil, fmt.Errorf("invalid credentials") return nil, fmt.Errorf("invalid credentials")
} }
} }
@ -132,7 +132,7 @@ func (m *modelDao) ListDomains(ctx context.Context, orgId string) ([]model.OrgDo
for _, s := range stored { for _, s := range stored {
domain := model.OrgDomain{Id: s.Id, Name: s.Name, OrgId: s.OrgId} domain := model.OrgDomain{Id: s.Id, Name: s.Name, OrgId: s.OrgId}
if err := domain.LoadConfig(s.Data); err != nil { if err := domain.LoadConfig(s.Data); err != nil {
zap.S().Errorf("ListDomains() failed", zap.Error(err)) zap.L().Error("ListDomains() failed", zap.Error(err))
} }
domains = append(domains, domain) domains = append(domains, domain)
} }
@ -153,7 +153,7 @@ func (m *modelDao) CreateDomain(ctx context.Context, domain *model.OrgDomain) ba
configJson, err := json.Marshal(domain) configJson, err := json.Marshal(domain)
if err != nil { if err != nil {
zap.S().Errorf("failed to unmarshal domain config", zap.Error(err)) zap.L().Error("failed to unmarshal domain config", zap.Error(err))
return model.InternalError(fmt.Errorf("domain creation failed")) return model.InternalError(fmt.Errorf("domain creation failed"))
} }
@ -167,7 +167,7 @@ func (m *modelDao) CreateDomain(ctx context.Context, domain *model.OrgDomain) ba
time.Now().Unix()) time.Now().Unix())
if err != nil { if err != nil {
zap.S().Errorf("failed to insert domain in db", zap.Error(err)) zap.L().Error("failed to insert domain in db", zap.Error(err))
return model.InternalError(fmt.Errorf("domain creation failed")) return model.InternalError(fmt.Errorf("domain creation failed"))
} }
@ -178,13 +178,13 @@ func (m *modelDao) CreateDomain(ctx context.Context, domain *model.OrgDomain) ba
func (m *modelDao) UpdateDomain(ctx context.Context, domain *model.OrgDomain) basemodel.BaseApiError { func (m *modelDao) UpdateDomain(ctx context.Context, domain *model.OrgDomain) basemodel.BaseApiError {
if domain.Id == uuid.Nil { if domain.Id == uuid.Nil {
zap.S().Errorf("domain update failed", zap.Error(fmt.Errorf("OrgDomain.Id is null"))) zap.L().Error("domain update failed", zap.Error(fmt.Errorf("OrgDomain.Id is null")))
return model.InternalError(fmt.Errorf("domain update failed")) return model.InternalError(fmt.Errorf("domain update failed"))
} }
configJson, err := json.Marshal(domain) configJson, err := json.Marshal(domain)
if err != nil { if err != nil {
zap.S().Errorf("domain update failed", zap.Error(err)) zap.L().Error("domain update failed", zap.Error(err))
return model.InternalError(fmt.Errorf("domain update failed")) return model.InternalError(fmt.Errorf("domain update failed"))
} }
@ -195,7 +195,7 @@ func (m *modelDao) UpdateDomain(ctx context.Context, domain *model.OrgDomain) ba
domain.Id) domain.Id)
if err != nil { if err != nil {
zap.S().Errorf("domain update failed", zap.Error(err)) zap.L().Error("domain update failed", zap.Error(err))
return model.InternalError(fmt.Errorf("domain update failed")) return model.InternalError(fmt.Errorf("domain update failed"))
} }
@ -206,7 +206,7 @@ func (m *modelDao) UpdateDomain(ctx context.Context, domain *model.OrgDomain) ba
func (m *modelDao) DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError { func (m *modelDao) DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError {
if id == uuid.Nil { if id == uuid.Nil {
zap.S().Errorf("domain delete failed", zap.Error(fmt.Errorf("OrgDomain.Id is null"))) zap.L().Error("domain delete failed", zap.Error(fmt.Errorf("OrgDomain.Id is null")))
return model.InternalError(fmt.Errorf("domain delete failed")) return model.InternalError(fmt.Errorf("domain delete failed"))
} }
@ -215,7 +215,7 @@ func (m *modelDao) DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.Bas
id) id)
if err != nil { if err != nil {
zap.S().Errorf("domain delete failed", zap.Error(err)) zap.L().Error("domain delete failed", zap.Error(err))
return model.InternalError(fmt.Errorf("domain delete failed")) return model.InternalError(fmt.Errorf("domain delete failed"))
} }

View File

@ -26,12 +26,12 @@ func (m *modelDao) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basem
p.Revoked, p.Revoked,
) )
if err != nil { if err != nil {
zap.S().Errorf("Failed to insert PAT in db, err: %v", zap.Error(err)) zap.L().Error("Failed to insert PAT in db, err: %v", zap.Error(err))
return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed")) return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
} }
id, err := result.LastInsertId() id, err := result.LastInsertId()
if err != nil { if err != nil {
zap.S().Errorf("Failed to get last inserted id, err: %v", zap.Error(err)) zap.L().Error("Failed to get last inserted id, err: %v", zap.Error(err))
return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed")) return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
} }
p.Id = strconv.Itoa(int(id)) p.Id = strconv.Itoa(int(id))
@ -62,7 +62,7 @@ func (m *modelDao) UpdatePAT(ctx context.Context, p model.PAT, id string) basemo
p.UpdatedByUserID, p.UpdatedByUserID,
id) id)
if err != nil { if err != nil {
zap.S().Errorf("Failed to update PAT in db, err: %v", zap.Error(err)) zap.L().Error("Failed to update PAT in db, err: %v", zap.Error(err))
return model.InternalError(fmt.Errorf("PAT update failed")) return model.InternalError(fmt.Errorf("PAT update failed"))
} }
return nil return nil
@ -74,7 +74,7 @@ func (m *modelDao) UpdatePATLastUsed(ctx context.Context, token string, lastUsed
lastUsed, lastUsed,
token) token)
if err != nil { if err != nil {
zap.S().Errorf("Failed to update PAT last used in db, err: %v", zap.Error(err)) zap.L().Error("Failed to update PAT last used in db, err: %v", zap.Error(err))
return model.InternalError(fmt.Errorf("PAT last used update failed")) return model.InternalError(fmt.Errorf("PAT last used update failed"))
} }
return nil return nil
@ -84,7 +84,7 @@ func (m *modelDao) ListPATs(ctx context.Context) ([]model.PAT, basemodel.BaseApi
pats := []model.PAT{} pats := []model.PAT{}
if err := m.DB().Select(&pats, "SELECT * FROM personal_access_tokens WHERE revoked=false ORDER by updated_at DESC;"); err != nil { if err := m.DB().Select(&pats, "SELECT * FROM personal_access_tokens WHERE revoked=false ORDER by updated_at DESC;"); err != nil {
zap.S().Errorf("Failed to fetch PATs err: %v", zap.Error(err)) zap.L().Error("Failed to fetch PATs err: %v", zap.Error(err))
return nil, model.InternalError(fmt.Errorf("failed to fetch PATs")) return nil, model.InternalError(fmt.Errorf("failed to fetch PATs"))
} }
for i := range pats { for i := range pats {
@ -129,7 +129,7 @@ func (m *modelDao) RevokePAT(ctx context.Context, id string, userID string) base
"UPDATE personal_access_tokens SET revoked=true, updated_by_user_id = $1, updated_at=$2 WHERE id=$3", "UPDATE personal_access_tokens SET revoked=true, updated_by_user_id = $1, updated_at=$2 WHERE id=$3",
userID, updatedAt, id) userID, updatedAt, id)
if err != nil { if err != nil {
zap.S().Errorf("Failed to revoke PAT in db, err: %v", zap.Error(err)) zap.L().Error("Failed to revoke PAT in db, err: %v", zap.Error(err))
return model.InternalError(fmt.Errorf("PAT revoke failed")) return model.InternalError(fmt.Errorf("PAT revoke failed"))
} }
return nil return nil

View File

@ -47,13 +47,13 @@ func ActivateLicense(key, siteId string) (*ActivationResponse, *model.ApiError)
httpResponse, err := http.Post(C.Prefix+"/licenses/activate", APPLICATION_JSON, bytes.NewBuffer(reqString)) httpResponse, err := http.Post(C.Prefix+"/licenses/activate", APPLICATION_JSON, bytes.NewBuffer(reqString))
if err != nil { if err != nil {
zap.S().Errorf("failed to connect to license.signoz.io", err) zap.L().Error("failed to connect to license.signoz.io", zap.Error(err))
return nil, model.BadRequest(fmt.Errorf("unable to connect with license.signoz.io, please check your network connection")) return nil, model.BadRequest(fmt.Errorf("unable to connect with license.signoz.io, please check your network connection"))
} }
httpBody, err := io.ReadAll(httpResponse.Body) httpBody, err := io.ReadAll(httpResponse.Body)
if err != nil { if err != nil {
zap.S().Errorf("failed to read activation response from license.signoz.io", err) zap.L().Error("failed to read activation response from license.signoz.io", zap.Error(err))
return nil, model.BadRequest(fmt.Errorf("failed to read activation response from license.signoz.io")) return nil, model.BadRequest(fmt.Errorf("failed to read activation response from license.signoz.io"))
} }
@ -63,7 +63,7 @@ func ActivateLicense(key, siteId string) (*ActivationResponse, *model.ApiError)
result := ActivationResult{} result := ActivationResult{}
err = json.Unmarshal(httpBody, &result) err = json.Unmarshal(httpBody, &result)
if err != nil { if err != nil {
zap.S().Errorf("failed to marshal activation response from license.signoz.io", err) zap.L().Error("failed to marshal activation response from license.signoz.io", zap.Error(err))
return nil, model.InternalError(errors.Wrap(err, "failed to marshal license activation response")) return nil, model.InternalError(errors.Wrap(err, "failed to marshal license activation response"))
} }

View File

@ -97,7 +97,7 @@ func (r *Repo) InsertLicense(ctx context.Context, l *model.License) error {
l.ValidationMessage) l.ValidationMessage)
if err != nil { if err != nil {
zap.S().Errorf("error in inserting license data: ", zap.Error(err)) zap.L().Error("error in inserting license data: ", zap.Error(err))
return fmt.Errorf("failed to insert license in db: %v", err) return fmt.Errorf("failed to insert license in db: %v", err)
} }
@ -121,7 +121,7 @@ func (r *Repo) UpdatePlanDetails(ctx context.Context,
_, err := r.db.ExecContext(ctx, query, planDetails, time.Now(), key) _, err := r.db.ExecContext(ctx, query, planDetails, time.Now(), key)
if err != nil { if err != nil {
zap.S().Errorf("error in updating license: ", zap.Error(err)) zap.L().Error("error in updating license: ", zap.Error(err))
return fmt.Errorf("failed to update license in db: %v", err) return fmt.Errorf("failed to update license in db: %v", err)
} }

View File

@ -100,7 +100,7 @@ func (lm *Manager) SetActive(l *model.License) {
err := lm.InitFeatures(lm.activeFeatures) err := lm.InitFeatures(lm.activeFeatures)
if err != nil { if err != nil {
zap.S().Panicf("Couldn't activate features: %v", err) zap.L().Panic("Couldn't activate features", zap.Error(err))
} }
if !lm.validatorRunning { if !lm.validatorRunning {
// we want to make sure only one validator runs, // we want to make sure only one validator runs,
@ -125,13 +125,13 @@ func (lm *Manager) LoadActiveLicense() error {
if active != nil { if active != nil {
lm.SetActive(active) lm.SetActive(active)
} else { } else {
zap.S().Info("No active license found, defaulting to basic plan") zap.L().Info("No active license found, defaulting to basic plan")
// if no active license is found, we default to basic(free) plan with all default features // if no active license is found, we default to basic(free) plan with all default features
lm.activeFeatures = model.BasicPlan lm.activeFeatures = model.BasicPlan
setDefaultFeatures(lm) setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures) err := lm.InitFeatures(lm.activeFeatures)
if err != nil { if err != nil {
zap.S().Error("Couldn't initialize features: ", err) zap.L().Error("Couldn't initialize features", zap.Error(err))
return err return err
} }
} }
@ -191,7 +191,7 @@ func (lm *Manager) Validator(ctx context.Context) {
// Validate validates the current active license // Validate validates the current active license
func (lm *Manager) Validate(ctx context.Context) (reterr error) { func (lm *Manager) Validate(ctx context.Context) (reterr error) {
zap.S().Info("License validation started") zap.L().Info("License validation started")
if lm.activeLicense == nil { if lm.activeLicense == nil {
return nil return nil
} }
@ -201,12 +201,12 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
lm.lastValidated = time.Now().Unix() lm.lastValidated = time.Now().Unix()
if reterr != nil { if reterr != nil {
zap.S().Errorf("License validation completed with error", reterr) zap.L().Error("License validation completed with error", zap.Error(reterr))
atomic.AddUint64(&lm.failedAttempts, 1) atomic.AddUint64(&lm.failedAttempts, 1)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED, telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
map[string]interface{}{"err": reterr.Error()}, "") map[string]interface{}{"err": reterr.Error()}, "", true, false)
} else { } else {
zap.S().Info("License validation completed with no errors") zap.L().Info("License validation completed with no errors")
} }
lm.mutex.Unlock() lm.mutex.Unlock()
@ -214,7 +214,7 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
response, apiError := validate.ValidateLicense(lm.activeLicense.ActivationId) response, apiError := validate.ValidateLicense(lm.activeLicense.ActivationId)
if apiError != nil { if apiError != nil {
zap.S().Errorf("failed to validate license", apiError) zap.L().Error("failed to validate license", zap.Error(apiError.Err))
return apiError.Err return apiError.Err
} }
@ -235,7 +235,7 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
} }
if err := l.ParsePlan(); err != nil { if err := l.ParsePlan(); err != nil {
zap.S().Errorf("failed to parse updated license", zap.Error(err)) zap.L().Error("failed to parse updated license", zap.Error(err))
return err return err
} }
@ -245,7 +245,7 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
if err != nil { if err != nil {
// unexpected db write issue but we can let the user continue // unexpected db write issue but we can let the user continue
// and wait for update to work in next cycle. // and wait for update to work in next cycle.
zap.S().Errorf("failed to validate license", zap.Error(err)) zap.L().Error("failed to validate license", zap.Error(err))
} }
} }
@ -263,14 +263,14 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
userEmail, err := auth.GetEmailFromJwt(ctx) userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil { if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED, telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail) map[string]interface{}{"err": errResponse.Err.Error()}, userEmail, true, false)
} }
} }
}() }()
response, apiError := validate.ActivateLicense(key, "") response, apiError := validate.ActivateLicense(key, "")
if apiError != nil { if apiError != nil {
zap.S().Errorf("failed to activate license", zap.Error(apiError.Err)) zap.L().Error("failed to activate license", zap.Error(apiError.Err))
return nil, apiError return nil, apiError
} }
@ -284,14 +284,14 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
err := l.ParsePlan() err := l.ParsePlan()
if err != nil { if err != nil {
zap.S().Errorf("failed to activate license", zap.Error(err)) zap.L().Error("failed to activate license", zap.Error(err))
return nil, model.InternalError(err) return nil, model.InternalError(err)
} }
// store the license before activating it // store the license before activating it
err = lm.repo.InsertLicense(ctx, l) err = lm.repo.InsertLicense(ctx, l)
if err != nil { if err != nil {
zap.S().Errorf("failed to activate license", zap.Error(err)) zap.L().Error("failed to activate license", zap.Error(err))
return nil, model.InternalError(err) return nil, model.InternalError(err)
} }

View File

@ -14,10 +14,10 @@ import (
semconv "go.opentelemetry.io/otel/semconv/v1.4.0" semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.signoz.io/signoz/ee/query-service/app" "go.signoz.io/signoz/ee/query-service/app"
"go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/constants"
baseconst "go.signoz.io/signoz/pkg/query-service/constants" baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/version" "go.signoz.io/signoz/pkg/query-service/version"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
zapotlpencoder "github.com/SigNoz/zap_otlp/zap_otlp_encoder" zapotlpencoder "github.com/SigNoz/zap_otlp/zap_otlp_encoder"
zapotlpsync "github.com/SigNoz/zap_otlp/zap_otlp_sync" zapotlpsync "github.com/SigNoz/zap_otlp/zap_otlp_sync"
@ -27,18 +27,19 @@ import (
) )
func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger { func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
config := zap.NewDevelopmentConfig() config := zap.NewProductionConfig()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop() defer stop()
config.EncoderConfig.EncodeDuration = zapcore.StringDurationEncoder config.EncoderConfig.EncodeDuration = zapcore.MillisDurationEncoder
otlpEncoder := zapotlpencoder.NewOTLPEncoder(config.EncoderConfig) config.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
consoleEncoder := zapcore.NewConsoleEncoder(config.EncoderConfig)
defaultLogLevel := zapcore.DebugLevel
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
config.EncoderConfig.TimeKey = "timestamp" config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
otlpEncoder := zapotlpencoder.NewOTLPEncoder(config.EncoderConfig)
consoleEncoder := zapcore.NewJSONEncoder(config.EncoderConfig)
defaultLogLevel := zapcore.InfoLevel
res := resource.NewWithAttributes( res := resource.NewWithAttributes(
semconv.SchemaURL, semconv.SchemaURL,
semconv.ServiceNameKey.String("query-service"), semconv.ServiceNameKey.String("query-service"),
@ -48,14 +49,15 @@ func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
zapcore.NewCore(consoleEncoder, os.Stdout, defaultLogLevel), zapcore.NewCore(consoleEncoder, os.Stdout, defaultLogLevel),
) )
if enableQueryServiceLogOTLPExport == true { if enableQueryServiceLogOTLPExport {
conn, err := grpc.DialContext(ctx, constants.OTLPTarget, grpc.WithBlock(), grpc.WithInsecure(), grpc.WithTimeout(time.Second*30)) ctx, _ := context.WithTimeout(ctx, time.Second*30)
conn, err := grpc.DialContext(ctx, baseconst.OTLPTarget, grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil { if err != nil {
log.Println("failed to connect to otlp collector to export query service logs with error:", err) log.Fatalf("failed to establish connection: %v", err)
} else { } else {
logExportBatchSizeInt, err := strconv.Atoi(baseconst.LogExportBatchSize) logExportBatchSizeInt, err := strconv.Atoi(baseconst.LogExportBatchSize)
if err != nil { if err != nil {
logExportBatchSizeInt = 1000 logExportBatchSizeInt = 512
} }
ws := zapcore.AddSync(zapotlpsync.NewOtlpSyncer(conn, zapotlpsync.Options{ ws := zapcore.AddSync(zapotlpsync.NewOtlpSyncer(conn, zapotlpsync.Options{
BatchSize: logExportBatchSizeInt, BatchSize: logExportBatchSizeInt,
@ -113,7 +115,6 @@ func main() {
zap.ReplaceGlobals(loggerMgr) zap.ReplaceGlobals(loggerMgr)
defer loggerMgr.Sync() // flushes buffer, if any defer loggerMgr.Sync() // flushes buffer, if any
logger := loggerMgr.Sugar()
version.PrintVersion() version.PrintVersion()
serverOptions := &app.ServerOptions{ serverOptions := &app.ServerOptions{
@ -137,22 +138,22 @@ func main() {
auth.JwtSecret = os.Getenv("SIGNOZ_JWT_SECRET") auth.JwtSecret = os.Getenv("SIGNOZ_JWT_SECRET")
if len(auth.JwtSecret) == 0 { if len(auth.JwtSecret) == 0 {
zap.S().Warn("No JWT secret key is specified.") zap.L().Warn("No JWT secret key is specified.")
} else { } else {
zap.S().Info("No JWT secret key set successfully.") zap.L().Info("JWT secret key set successfully.")
} }
server, err := app.NewServer(serverOptions) server, err := app.NewServer(serverOptions)
if err != nil { if err != nil {
logger.Fatal("Failed to create server", zap.Error(err)) zap.L().Fatal("Failed to create server", zap.Error(err))
} }
if err := server.Start(); err != nil { if err := server.Start(); err != nil {
logger.Fatal("Could not start servers", zap.Error(err)) zap.L().Fatal("Could not start server", zap.Error(err))
} }
if err := auth.InitAuthCache(context.Background()); err != nil { if err := auth.InitAuthCache(context.Background()); err != nil {
logger.Fatal("Failed to initialize auth cache", zap.Error(err)) zap.L().Fatal("Failed to initialize auth cache", zap.Error(err))
} }
signalsChannel := make(chan os.Signal, 1) signalsChannel := make(chan os.Signal, 1)
@ -161,9 +162,9 @@ func main() {
for { for {
select { select {
case status := <-server.HealthCheckStatus(): case status := <-server.HealthCheckStatus():
logger.Info("Received HealthCheck status: ", zap.Int("status", int(status))) zap.L().Info("Received HealthCheck status: ", zap.Int("status", int(status)))
case <-signalsChannel: case <-signalsChannel:
logger.Fatal("Received OS Interrupt Signal ... ") zap.L().Fatal("Received OS Interrupt Signal ... ")
server.Stop() server.Stop()
} }
} }

View File

@ -9,8 +9,8 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
saml2 "github.com/russellhaering/gosaml2" saml2 "github.com/russellhaering/gosaml2"
"go.signoz.io/signoz/ee/query-service/sso/saml"
"go.signoz.io/signoz/ee/query-service/sso" "go.signoz.io/signoz/ee/query-service/sso"
"go.signoz.io/signoz/ee/query-service/sso/saml"
basemodel "go.signoz.io/signoz/pkg/query-service/model" basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -24,16 +24,16 @@ const (
// OrgDomain identify org owned web domains for auth and other purposes // OrgDomain identify org owned web domains for auth and other purposes
type OrgDomain struct { type OrgDomain struct {
Id uuid.UUID `json:"id"` Id uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
OrgId string `json:"orgId"` OrgId string `json:"orgId"`
SsoEnabled bool `json:"ssoEnabled"` SsoEnabled bool `json:"ssoEnabled"`
SsoType SSOType `json:"ssoType"` SsoType SSOType `json:"ssoType"`
SamlConfig *SamlConfig `json:"samlConfig"` SamlConfig *SamlConfig `json:"samlConfig"`
GoogleAuthConfig *GoogleOAuthConfig `json:"googleAuthConfig"` GoogleAuthConfig *GoogleOAuthConfig `json:"googleAuthConfig"`
Org *basemodel.Organization Org *basemodel.Organization
} }
func (od *OrgDomain) String() string { func (od *OrgDomain) String() string {
@ -138,7 +138,6 @@ func (od *OrgDomain) PrepareSamlRequest(siteUrl *url.URL) (*saml2.SAMLServicePro
func (od *OrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) { func (od *OrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) {
fmtDomainId := strings.Replace(od.Id.String(), "-", ":", -1) fmtDomainId := strings.Replace(od.Id.String(), "-", ":", -1)
// build redirect url from window.location sent by frontend // build redirect url from window.location sent by frontend
@ -156,8 +155,7 @@ func (od *OrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) {
// auth provider to the backend (HandleCallback or HandleSSO method). // auth provider to the backend (HandleCallback or HandleSSO method).
relayState := fmt.Sprintf("%s?domainId=%s", redirectURL, fmtDomainId) relayState := fmt.Sprintf("%s?domainId=%s", redirectURL, fmtDomainId)
switch od.SsoType {
switch (od.SsoType) {
case SAML: case SAML:
sp, err := od.PrepareSamlRequest(siteUrl) sp, err := od.PrepareSamlRequest(siteUrl)
@ -176,9 +174,8 @@ func (od *OrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) {
return googleProvider.BuildAuthURL(relayState) return googleProvider.BuildAuthURL(relayState)
default: default:
zap.S().Errorf("found unsupported SSO config for the org domain", zap.String("orgDomain", od.Name)) zap.L().Error("found unsupported SSO config for the org domain", zap.String("orgDomain", od.Name))
return "", fmt.Errorf("unsupported SSO config for the domain") return "", fmt.Errorf("unsupported SSO config for the domain")
} }
} }

View File

@ -102,6 +102,6 @@ func PrepareRequest(issuer, acsUrl, audience, entity, idp, certString string) (*
IDPCertificateStore: certStore, IDPCertificateStore: certStore,
SPKeyStore: randomKeyStore, SPKeyStore: randomKeyStore,
} }
zap.S().Debugf("SAML request:", sp) zap.L().Debug("SAML request", zap.Any("sp", sp))
return sp, nil return sp, nil
} }

View File

@ -91,12 +91,12 @@ func (lm *Manager) UploadUsage() {
// check if license is present or not // check if license is present or not
license, err := lm.licenseRepo.GetActiveLicense(ctx) license, err := lm.licenseRepo.GetActiveLicense(ctx)
if err != nil { if err != nil {
zap.S().Errorf("failed to get active license: %v", zap.Error(err)) zap.L().Error("failed to get active license", zap.Error(err))
return return
} }
if license == nil { if license == nil {
// we will not start the usage reporting if license is not present. // we will not start the usage reporting if license is not present.
zap.S().Info("no license present, skipping usage reporting") zap.L().Info("no license present, skipping usage reporting")
return return
} }
@ -123,7 +123,7 @@ func (lm *Manager) UploadUsage() {
dbusages := []model.UsageDB{} dbusages := []model.UsageDB{}
err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour))) err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour)))
if err != nil && !strings.Contains(err.Error(), "doesn't exist") { if err != nil && !strings.Contains(err.Error(), "doesn't exist") {
zap.S().Errorf("failed to get usage from clickhouse: %v", zap.Error(err)) zap.L().Error("failed to get usage from clickhouse: %v", zap.Error(err))
return return
} }
for _, u := range dbusages { for _, u := range dbusages {
@ -133,16 +133,16 @@ func (lm *Manager) UploadUsage() {
} }
if len(usages) <= 0 { if len(usages) <= 0 {
zap.S().Info("no snapshots to upload, skipping.") zap.L().Info("no snapshots to upload, skipping.")
return return
} }
zap.S().Info("uploading usage data") zap.L().Info("uploading usage data")
orgName := "" orgName := ""
orgNames, orgError := lm.modelDao.GetOrgs(ctx) orgNames, orgError := lm.modelDao.GetOrgs(ctx)
if orgError != nil { if orgError != nil {
zap.S().Errorf("failed to get org data: %v", zap.Error(orgError)) zap.L().Error("failed to get org data: %v", zap.Error(orgError))
} }
if len(orgNames) == 1 { if len(orgNames) == 1 {
orgName = orgNames[0].Name orgName = orgNames[0].Name
@ -152,14 +152,14 @@ func (lm *Manager) UploadUsage() {
for _, usage := range usages { for _, usage := range usages {
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data)) usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
if err != nil { if err != nil {
zap.S().Errorf("error while decrypting usage data: %v", zap.Error(err)) zap.L().Error("error while decrypting usage data: %v", zap.Error(err))
return return
} }
usageData := model.Usage{} usageData := model.Usage{}
err = json.Unmarshal(usageDataBytes, &usageData) err = json.Unmarshal(usageDataBytes, &usageData)
if err != nil { if err != nil {
zap.S().Errorf("error while unmarshalling usage data: %v", zap.Error(err)) zap.L().Error("error while unmarshalling usage data: %v", zap.Error(err))
return return
} }
@ -184,13 +184,13 @@ func (lm *Manager) UploadUsageWithExponentalBackOff(ctx context.Context, payload
for i := 1; i <= MaxRetries; i++ { for i := 1; i <= MaxRetries; i++ {
apiErr := licenseserver.SendUsage(ctx, payload) apiErr := licenseserver.SendUsage(ctx, payload)
if apiErr != nil && i == MaxRetries { if apiErr != nil && i == MaxRetries {
zap.S().Errorf("retries stopped : %v", zap.Error(apiErr)) zap.L().Error("retries stopped : %v", zap.Error(apiErr))
// not returning error here since it is captured in the failed count // not returning error here since it is captured in the failed count
return return
} else if apiErr != nil { } else if apiErr != nil {
// sleeping for exponential backoff // sleeping for exponential backoff
sleepDuration := RetryInterval * time.Duration(i) sleepDuration := RetryInterval * time.Duration(i)
zap.S().Errorf("failed to upload snapshot retrying after %v secs : %v", sleepDuration.Seconds(), zap.Error(apiErr.Err)) zap.L().Error("failed to upload snapshot retrying after %v secs : %v", zap.Duration("sleepDuration", sleepDuration), zap.Error(apiErr.Err))
time.Sleep(sleepDuration) time.Sleep(sleepDuration)
} else { } else {
break break
@ -201,7 +201,7 @@ func (lm *Manager) UploadUsageWithExponentalBackOff(ctx context.Context, payload
func (lm *Manager) Stop() { func (lm *Manager) Stop() {
lm.scheduler.Stop() lm.scheduler.Stop()
zap.S().Debug("sending usage data before shutting down") zap.L().Info("sending usage data before shutting down")
// send usage before shutting down // send usage before shutting down
lm.UploadUsage() lm.UploadUsage()

View File

@ -121,6 +121,7 @@
"web-vitals": "^0.2.4", "web-vitals": "^0.2.4",
"webpack": "5.88.2", "webpack": "5.88.2",
"webpack-dev-server": "^4.15.1", "webpack-dev-server": "^4.15.1",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0" "xstate": "^4.31.0"
}, },
"browserslist": { "browserslist": {

View File

@ -0,0 +1 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_2022_1972)" stroke="#fff" stroke-width="1.333" stroke-linecap="round" stroke-linejoin="round"><path d="M6.667 2h.006M9.333 1.333h.007M1.333 6l13.334-3.333M8 8V4.333M11.333 8H4.667a2 2 0 00-2 2v2.667a2 2 0 002 2h6.666a2 2 0 002-2V10a2 2 0 00-2-2zM6 8v3.333M10 8v3.333M2.667 11.334h10.666"/></g><defs><clipPath id="prefix__clip0_2022_1972"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 507 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g stroke="#C0C1C3" stroke-width="1.333" stroke-linecap="round"><path 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" stroke-linejoin="round"/><path d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3M10.75 10.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"/></g></svg>

After

Width:  |  Height:  |  Size: 644 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g stroke="#C0C1C3" stroke-width="1.333" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4.667V3.333C2 2.6 2.6 2 3.333 2h1.334M11.333 2h1.334C13.4 2 14 2.6 14 3.333v1.334M14 11.334v1.333C14 13.4 13.4 14 12.667 14h-1.334M4.667 14H3.333C2.6 14 2 13.4 2 12.667v-1.333M8.667 4.667H5.333a.667.667 0 00-.666.666v2c0 .368.298.667.666.667h3.334a.667.667 0 00.666-.667v-2a.667.667 0 00-.666-.667zM10.667 8H7.333a.667.667 0 00-.666.667v2c0 .368.298.666.666.666h3.334a.667.667 0 00.666-.666v-2A.667.667 0 0010.667 8z"/></g></svg>

After

Width:  |  Height:  |  Size: 604 B

View File

@ -37,11 +37,16 @@
"text_condition1": "Send a notification when", "text_condition1": "Send a notification when",
"text_condition2": "the threshold", "text_condition2": "the threshold",
"text_condition3": "during the last", "text_condition3": "during the last",
"option_1min": "1 min",
"option_5min": "5 mins", "option_5min": "5 mins",
"option_10min": "10 mins", "option_10min": "10 mins",
"option_15min": "15 mins", "option_15min": "15 mins",
"option_30min": "30 mins",
"option_60min": "60 mins", "option_60min": "60 mins",
"option_4hours": "4 hours", "option_4hours": "4 hours",
"option_3hours": "3 hours",
"option_6hours": "6 hours",
"option_12hours": "12 hours",
"option_24hours": "24 hours", "option_24hours": "24 hours",
"field_threshold": "Alert Threshold", "field_threshold": "Alert Threshold",
"option_allthetimes": "all the times", "option_allthetimes": "all the times",
@ -112,6 +117,7 @@
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.", "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
"field_unit": "Threshold unit", "field_unit": "Threshold unit",
"text_alert_on_absent": "Send a notification if data is missing for", "text_alert_on_absent": "Send a notification if data is missing for",
"text_alert_frequency": "Run alert every",
"text_for": "minutes", "text_for": "minutes",
"selected_query_placeholder": "Select query" "selected_query_placeholder": "Select query"
} }

View File

@ -37,11 +37,16 @@
"text_condition1": "Send a notification when", "text_condition1": "Send a notification when",
"text_condition2": "the threshold", "text_condition2": "the threshold",
"text_condition3": "during the last", "text_condition3": "during the last",
"option_1min": "1 min",
"option_5min": "5 mins", "option_5min": "5 mins",
"option_10min": "10 mins", "option_10min": "10 mins",
"option_15min": "15 mins", "option_15min": "15 mins",
"option_30min": "30 mins",
"option_60min": "60 mins", "option_60min": "60 mins",
"option_3hours": "3 hours",
"option_4hours": "4 hours", "option_4hours": "4 hours",
"option_6hours": "6 hours",
"option_12hours": "12 hours",
"option_24hours": "24 hours", "option_24hours": "24 hours",
"field_threshold": "Alert Threshold", "field_threshold": "Alert Threshold",
"option_allthetimes": "all the times", "option_allthetimes": "all the times",
@ -112,6 +117,7 @@
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.", "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
"field_unit": "Threshold unit", "field_unit": "Threshold unit",
"text_alert_on_absent": "Send a notification if data is missing for", "text_alert_on_absent": "Send a notification if data is missing for",
"text_alert_frequency": "Run alert every",
"text_for": "minutes", "text_for": "minutes",
"selected_query_placeholder": "Select query" "selected_query_placeholder": "Select query"
} }

View File

@ -0,0 +1,14 @@
{
"days_remaining": "days remaining in your billing period.",
"billing": "Billing",
"manage_billing_and_costs": "Manage your billing information, invoices, and monitor costs.",
"enterprise_cloud": "Enterprise Cloud",
"enterprise": "Enterprise",
"card_details_recieved_and_billing_info": "We have received your card details, your billing will only start after the end of your free trial period.",
"upgrade_plan": "Upgrade Plan",
"manage_billing": "Manage Billing",
"upgrade_now_text": "Upgrade now to have uninterrupted access",
"billing_start_info": "Your billing will start only after the trial period",
"checkout_plans": "Check out features in paid plans",
"here": "here"
}

View File

@ -48,5 +48,5 @@
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views", "TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"DEFAULT": "Open source Observability Platform | SigNoz", "DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts", "SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS_INSTALLED": "SigNoz | Integrations" "INTEGRATIONS": "SigNoz | Integrations"
} }

View File

@ -197,11 +197,3 @@ export const InstalledIntegrations = Loadable(
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage' /* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
), ),
); );
export const IntegrationsMarketPlace = Loadable(
// eslint-disable-next-line sonarjs/no-identical-functions
() =>
import(
/* webpackChunkName: "IntegrationsMarketPlace" */ 'pages/IntegrationsModulePage'
),
);

View File

@ -15,7 +15,6 @@ import {
ErrorDetails, ErrorDetails,
IngestionSettings, IngestionSettings,
InstalledIntegrations, InstalledIntegrations,
IntegrationsMarketPlace,
LicensePage, LicensePage,
ListAllALertsPage, ListAllALertsPage,
LiveLogs, LiveLogs,
@ -338,18 +337,11 @@ const routes: AppRoutes[] = [
key: 'SHORTCUTS', key: 'SHORTCUTS',
}, },
{ {
path: ROUTES.INTEGRATIONS_INSTALLED, path: ROUTES.INTEGRATIONS,
exact: true, exact: true,
component: InstalledIntegrations, component: InstalledIntegrations,
isPrivate: true, isPrivate: true,
key: 'INTEGRATIONS_INSTALLED', key: 'INTEGRATIONS',
},
{
path: ROUTES.INTEGRATIONS_MARKETPLACE,
exact: true,
component: IntegrationsMarketPlace,
isPrivate: true,
key: 'INTEGRATIONS_MARKETPLACE',
}, },
]; ];

View File

@ -30,7 +30,8 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
statusCode, statusCode,
payload: null, payload: null,
error: errorMessage, error: errorMessage,
message: null, message: (response.data as any)?.status,
body: JSON.stringify((response.data as any).data),
}; };
} }

View File

@ -8,7 +8,7 @@ const listAllDomain = async (
props: Props, props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => { ): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try { try {
const response = await axios.get(`orgs/${props.orgId}/domains`); const response = await axios.get(`/orgs/${props.orgId}/domains`);
return { return {
statusCode: 200, statusCode: 200,

View File

@ -13,6 +13,7 @@ export interface UsageResponsePayloadProps {
billTotal: number; billTotal: number;
}; };
discount: number; discount: number;
subscriptionStatus?: string;
} }
const getUsage = async ( const getUsage = async (

View File

@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { EventSuccessPayloadProps } from 'types/api/events/types';
const logEvent = async (
eventName: string,
attributes: Record<string, unknown>,
): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/event', {
eventName,
attributes,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default logEvent;

View File

@ -24,7 +24,7 @@ export const getAggregateAttribute = async ({
const response: AxiosResponse<{ const response: AxiosResponse<{
data: IQueryAutocompleteResponse; data: IQueryAutocompleteResponse;
}> = await ApiV3Instance.get( }> = await ApiV3Instance.get(
`autocomplete/aggregate_attributes?${createQueryParams({ `/autocomplete/aggregate_attributes?${createQueryParams({
aggregateOperator, aggregateOperator,
searchText, searchText,
dataSource, dataSource,

View File

@ -25,7 +25,7 @@ export const getAggregateKeys = async ({
const response: AxiosResponse<{ const response: AxiosResponse<{
data: IQueryAutocompleteResponse; data: IQueryAutocompleteResponse;
}> = await ApiV3Instance.get( }> = await ApiV3Instance.get(
`autocomplete/attribute_keys?${createQueryParams({ `/autocomplete/attribute_keys?${createQueryParams({
aggregateOperator, aggregateOperator,
searchText, searchText,
dataSource, dataSource,

View File

@ -2,4 +2,4 @@ import axios from 'api';
import { DeleteViewPayloadProps } from 'types/api/saveViews/types'; import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
export const deleteView = (uuid: string): Promise<DeleteViewPayloadProps> => export const deleteView = (uuid: string): Promise<DeleteViewPayloadProps> =>
axios.delete(`explorer/views/${uuid}`); axios.delete(`/explorer/views/${uuid}`);

View File

@ -6,4 +6,4 @@ import { DataSource } from 'types/common/queryBuilder';
export const getAllViews = ( export const getAllViews = (
sourcepage: DataSource, sourcepage: DataSource,
): Promise<AxiosResponse<AllViewsProps>> => ): Promise<AxiosResponse<AllViewsProps>> =>
axios.get(`explorer/views?sourcePage=${sourcepage}`); axios.get(`/explorer/views?sourcePage=${sourcepage}`);

View File

@ -8,7 +8,7 @@ export const saveView = ({
viewName, viewName,
extraData, extraData,
}: SaveViewProps): Promise<AxiosResponse<SaveViewPayloadProps>> => }: SaveViewProps): Promise<AxiosResponse<SaveViewPayloadProps>> =>
axios.post('explorer/views', { axios.post('/explorer/views', {
name: viewName, name: viewName,
sourcePage, sourcePage,
compositeQuery, compositeQuery,

View File

@ -11,7 +11,7 @@ export const updateView = ({
sourcePage, sourcePage,
viewKey, viewKey,
}: UpdateViewProps): Promise<UpdateViewPayloadProps> => }: UpdateViewProps): Promise<UpdateViewPayloadProps> =>
axios.put(`explorer/views/${viewKey}`, { axios.put(`/explorer/views/${viewKey}`, {
name: viewName, name: viewName,
compositeQuery, compositeQuery,
extraData, extraData,

View File

@ -0,0 +1,23 @@
import { Color } from '@signozhq/design-tokens';
import { useIsDarkMode } from 'hooks/useDarkMode';
function ConfigureIcon(): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g
stroke={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500}
strokeWidth="1.333"
strokeLinecap="round"
>
<path
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"
strokeLinejoin="round"
/>
<path d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3M10.75 10.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" />
</g>
</svg>
);
}
export default ConfigureIcon;

View File

@ -5,13 +5,14 @@ import './CustomTimePicker.styles.scss';
import { Input, Popover, Tooltip, Typography } from 'antd'; import { Input, Popover, Tooltip, Typography } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal'; import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { Options } from 'container/TopNav/DateTimeSelection/config';
import { import {
FixedDurationSuggestionOptions, FixedDurationSuggestionOptions,
Options,
RelativeDurationSuggestionOptions, RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config'; } from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { defaultTo, noop } from 'lodash-es'; import { isValidTimeFormat } from 'lib/getMinMax';
import { defaultTo, isFunction, noop } from 'lodash-es';
import debounce from 'lodash-es/debounce'; import debounce from 'lodash-es/debounce';
import { CheckCircle, ChevronDown, Clock } from 'lucide-react'; import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
import { import {
@ -33,7 +34,14 @@ interface CustomTimePickerProps {
onError: (value: boolean) => void; onError: (value: boolean) => void;
selectedValue: string; selectedValue: string;
selectedTime: string; selectedTime: string;
onValidCustomDateChange: ([t1, t2]: any[]) => void; onValidCustomDateChange: ({
time: [t1, t2],
timeStr,
}: {
time: [dayjs.Dayjs | null, dayjs.Dayjs | null];
timeStr: string;
}) => void;
onCustomTimeStatusUpdate?: (isValid: boolean) => void;
open: boolean; open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>; setOpen: Dispatch<SetStateAction<boolean>>;
items: any[]; items: any[];
@ -53,6 +61,7 @@ function CustomTimePicker({
open, open,
setOpen, setOpen,
onValidCustomDateChange, onValidCustomDateChange,
onCustomTimeStatusUpdate,
newPopover, newPopover,
customDateTimeVisible, customDateTimeVisible,
setCustomDTPickerVisible, setCustomDTPickerVisible,
@ -85,6 +94,7 @@ function CustomTimePicker({
return Options[index].label; return Options[index].label;
} }
} }
for ( for (
let index = 0; let index = 0;
index < RelativeDurationSuggestionOptions.length; index < RelativeDurationSuggestionOptions.length;
@ -94,12 +104,17 @@ function CustomTimePicker({
return RelativeDurationSuggestionOptions[index].label; return RelativeDurationSuggestionOptions[index].label;
} }
} }
for (let index = 0; index < FixedDurationSuggestionOptions.length; index++) { for (let index = 0; index < FixedDurationSuggestionOptions.length; index++) {
if (FixedDurationSuggestionOptions[index].value === selectedTime) { if (FixedDurationSuggestionOptions[index].value === selectedTime) {
return FixedDurationSuggestionOptions[index].label; return FixedDurationSuggestionOptions[index].label;
} }
} }
if (isValidTimeFormat(selectedTime)) {
return selectedTime;
}
return ''; return '';
}; };
@ -161,13 +176,22 @@ function CustomTimePicker({
setInputStatus('error'); setInputStatus('error');
onError(true); onError(true);
setInputErrorMessage('Please enter time less than 6 months'); setInputErrorMessage('Please enter time less than 6 months');
if (isFunction(onCustomTimeStatusUpdate)) {
onCustomTimeStatusUpdate(true);
}
} else { } else {
onValidCustomDateChange([minTime, currentTime]); onValidCustomDateChange({
time: [minTime, currentTime],
timeStr: inputValue,
});
} }
} else { } else {
setInputStatus('error'); setInputStatus('error');
onError(true); onError(true);
setInputErrorMessage(null); setInputErrorMessage(null);
if (isFunction(onCustomTimeStatusUpdate)) {
onCustomTimeStatusUpdate(false);
}
} }
}, 300); }, 300);
@ -320,4 +344,5 @@ CustomTimePicker.defaultProps = {
setCustomDTPickerVisible: noop, setCustomDTPickerVisible: noop,
onCustomDateHandler: noop, onCustomDateHandler: noop,
handleGoLive: noop, handleGoLive: noop,
onCustomTimeStatusUpdate: noop,
}; };

View File

@ -37,12 +37,17 @@ const convert = new Convert();
interface LogFieldProps { interface LogFieldProps {
fieldKey: string; fieldKey: string;
fieldValue: string; fieldValue: string;
linesPerRow?: number;
} }
type LogSelectedFieldProps = LogFieldProps & type LogSelectedFieldProps = Omit<LogFieldProps, 'linesPerRow'> &
Pick<AddToQueryHOCProps, 'onAddToQuery'>; Pick<AddToQueryHOCProps, 'onAddToQuery'>;
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element { function LogGeneralField({
fieldKey,
fieldValue,
linesPerRow = 1,
}: LogFieldProps): JSX.Element {
const html = useMemo( const html = useMemo(
() => ({ () => ({
__html: convert.toHtml(dompurify.sanitize(fieldValue)), __html: convert.toHtml(dompurify.sanitize(fieldValue)),
@ -55,7 +60,11 @@ function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
<Text ellipsis type="secondary" className="log-field-key"> <Text ellipsis type="secondary" className="log-field-key">
{`${fieldKey} : `} {`${fieldKey} : `}
</Text> </Text>
<LogText dangerouslySetInnerHTML={html} className="log-value" /> <LogText
dangerouslySetInnerHTML={html}
className="log-value"
linesPerRow={linesPerRow > 1 ? linesPerRow : undefined}
/>
</TextContainer> </TextContainer>
); );
} }
@ -92,6 +101,7 @@ type ListLogViewProps = {
onSetActiveLog: (log: ILog) => void; onSetActiveLog: (log: ILog) => void;
onAddToQuery: AddToQueryHOCProps['onAddToQuery']; onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
activeLog?: ILog | null; activeLog?: ILog | null;
linesPerRow: number;
}; };
function ListLogView({ function ListLogView({
@ -100,6 +110,7 @@ function ListLogView({
onSetActiveLog, onSetActiveLog,
onAddToQuery, onAddToQuery,
activeLog, activeLog,
linesPerRow,
}: ListLogViewProps): JSX.Element { }: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]); const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@ -179,7 +190,11 @@ function ListLogView({
/> />
<div> <div>
<LogContainer> <LogContainer>
<LogGeneralField fieldKey="Log" fieldValue={flattenLogData.body} /> <LogGeneralField
fieldKey="Log"
fieldValue={flattenLogData.body}
linesPerRow={linesPerRow}
/>
{flattenLogData.stream && ( {flattenLogData.stream && (
<LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} /> <LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} />
)} )}
@ -222,4 +237,8 @@ ListLogView.defaultProps = {
activeLog: null, activeLog: null,
}; };
LogGeneralField.defaultProps = {
linesPerRow: 1,
};
export default ListLogView; export default ListLogView;

View File

@ -2,6 +2,10 @@ import { Color } from '@signozhq/design-tokens';
import { Card, Typography } from 'antd'; import { Card, Typography } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
interface LogTextProps {
linesPerRow?: number;
}
export const Container = styled(Card)<{ export const Container = styled(Card)<{
$isActiveLog: boolean; $isActiveLog: boolean;
$isDarkMode: boolean; $isDarkMode: boolean;
@ -23,7 +27,7 @@ export const Container = styled(Card)<{
export const Text = styled(Typography.Text)` export const Text = styled(Typography.Text)`
&&& { &&& {
min-width: 1.5rem; min-width: 2.5rem;
white-space: nowrap; white-space: nowrap;
} }
`; `;
@ -41,11 +45,19 @@ export const LogContainer = styled.div`
gap: 6px; gap: 6px;
`; `;
export const LogText = styled.div` export const LogText = styled.div<LogTextProps>`
display: inline-block; display: inline-block;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; ${({ linesPerRow }): string =>
linesPerRow
? `-webkit-line-clamp: ${linesPerRow};
line-clamp: ${linesPerRow};
display: -webkit-box;
-webkit-box-orient: vertical;
white-space: normal; `
: 'white-space: nowrap;'};
};
`; `;
export const SelectedLog = styled.div` export const SelectedLog = styled.div`

View File

@ -27,7 +27,7 @@
line-height: 18px; line-height: 18px;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-align: left; text-align: left;
color: var(--bg-slate-200, #52575c); color: #52575c;
} }
.menu-items { .menu-items {
@ -65,7 +65,7 @@
padding: 12px; padding: 12px;
.title { .title {
color: var(--bg-slate-200, #52575c); color: #52575c;
font-family: Inter; font-family: Inter;
font-size: 11px; font-size: 11px;
font-style: normal; font-style: normal;
@ -149,7 +149,7 @@
} }
.title { .title {
color: var(--bg-slate-200, #52575c); color: #52575c;
font-family: Inter; font-family: Inter;
font-size: 11px; font-size: 11px;
font-style: normal; font-style: normal;

View File

@ -120,38 +120,36 @@ export default function LogsFormatOptionsMenu({
{selectedItem && ( {selectedItem && (
<> <>
{selectedItem === 'raw' && ( <>
<> <div className="horizontal-line" />
<div className="horizontal-line" /> <div className="max-lines-per-row">
<div className="max-lines-per-row"> <div className="title"> max lines per row </div>
<div className="title"> max lines per row </div> <div className="raw-format max-lines-per-row-input">
<div className="raw-format max-lines-per-row-input"> <button
<button type="button"
type="button" className="periscope-btn"
className="periscope-btn" onClick={decrementMaxLinesPerRow}
onClick={decrementMaxLinesPerRow} >
> {' '}
{' '} <Minus size={12} />{' '}
<Minus size={12} />{' '} </button>
</button> <InputNumber
<InputNumber min={1}
min={1} max={10}
max={10} value={maxLinesPerRow}
value={maxLinesPerRow} onChange={handleLinesPerRowChange}
onChange={handleLinesPerRowChange} />
/> <button
<button type="button"
type="button" className="periscope-btn"
className="periscope-btn" onClick={incrementMaxLinesPerRow}
onClick={incrementMaxLinesPerRow} >
> {' '}
{' '} <Plus size={12} />{' '}
<Plus size={12} />{' '} </button>
</button>
</div>
</div> </div>
</> </div>
)} </>
<div className="selected-item-content-container active"> <div className="selected-item-content-container active">
{!addNewColumn && <div className="horizontal-line" />} {!addNewColumn && <div className="horizontal-line" />}

View File

@ -16,4 +16,6 @@ export enum LOCALSTORAGE {
CHAT_SUPPORT = 'CHAT_SUPPORT', CHAT_SUPPORT = 'CHAT_SUPPORT',
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER', IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES', DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
} }

View File

@ -27,5 +27,7 @@ export enum QueryParams {
viewName = 'viewName', viewName = 'viewName',
viewKey = 'viewKey', viewKey = 'viewKey',
expandedWidgetId = 'expandedWidgetId', expandedWidgetId = 'expandedWidgetId',
integration = 'integration',
pagination = 'pagination', pagination = 'pagination',
relativeTime = 'relativeTime',
} }

View File

@ -51,9 +51,7 @@ const ROUTES = {
TRACES_SAVE_VIEWS: '/traces/saved-views', TRACES_SAVE_VIEWS: '/traces/saved-views',
WORKSPACE_LOCKED: '/workspace-locked', WORKSPACE_LOCKED: '/workspace-locked',
SHORTCUTS: '/shortcuts', SHORTCUTS: '/shortcuts',
INTEGRATIONS_BASE: '/integrations', INTEGRATIONS: '/integrations',
INTEGRATIONS_INSTALLED: '/integrations/installed',
INTEGRATIONS_MARKETPLACE: '/integrations/marketplace',
} as const; } as const;
export default ROUTES; export default ROUTES;

View File

@ -9,9 +9,10 @@ export const DashboardShortcuts = {
export const DashboardShortcutsName = { export const DashboardShortcutsName = {
SaveChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+s`, SaveChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+s`,
DiscardChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+d`,
}; };
export const DashboardShortcutsDescription = { export const DashboardShortcutsDescription = {
SaveChanges: 'Save Changes', SaveChanges: 'Save Changes for panel',
DiscardChanges: 'Discard Changes', DiscardChanges: 'Discard Changes for panel',
}; };

View File

@ -13,5 +13,5 @@ export const QBShortcutsName = {
}; };
export const QBShortcutsDescription = { export const QBShortcutsDescription = {
StageAndRunQuery: 'Stage and Run the query', StageAndRunQuery: 'Stage and Run the current query',
}; };

View File

@ -56,14 +56,14 @@ describe('BillingContainer', () => {
expect(cost).toBeInTheDocument(); expect(cost).toBeInTheDocument();
const manageBilling = screen.getByRole('button', { const manageBilling = screen.getByRole('button', {
name: /manage billing/i, name: 'manage_billing',
}); });
expect(manageBilling).toBeInTheDocument(); expect(manageBilling).toBeInTheDocument();
const dollar = screen.getByText(/\$0/i); const dollar = screen.getByText(/\$0/i);
expect(dollar).toBeInTheDocument(); expect(dollar).toBeInTheDocument();
const currentBill = screen.getByText('Billing'); const currentBill = screen.getByText('billing');
expect(currentBill).toBeInTheDocument(); expect(currentBill).toBeInTheDocument();
}); });
@ -75,7 +75,7 @@ describe('BillingContainer', () => {
const freeTrailText = await screen.findByText('Free Trial'); const freeTrailText = await screen.findByText('Free Trial');
expect(freeTrailText).toBeInTheDocument(); expect(freeTrailText).toBeInTheDocument();
const currentBill = screen.getByText('Billing'); const currentBill = screen.getByText('billing');
expect(currentBill).toBeInTheDocument(); expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$0/i); const dollar0 = await screen.findByText(/\$0/i);
@ -85,18 +85,14 @@ describe('BillingContainer', () => {
); );
expect(onTrail).toBeInTheDocument(); expect(onTrail).toBeInTheDocument();
const numberOfDayRemaining = await screen.findByText( const numberOfDayRemaining = await screen.findByText(/1 days_remaining/i);
/1 days remaining in your billing period./i,
);
expect(numberOfDayRemaining).toBeInTheDocument(); expect(numberOfDayRemaining).toBeInTheDocument();
const upgradeButton = await screen.findAllByRole('button', { const upgradeButton = await screen.findAllByRole('button', {
name: /upgrade/i, name: /upgrade_plan/i,
}); });
expect(upgradeButton[1]).toBeInTheDocument(); expect(upgradeButton[1]).toBeInTheDocument();
expect(upgradeButton.length).toBe(2); expect(upgradeButton.length).toBe(2);
const checkPaidPlan = await screen.findByText( const checkPaidPlan = await screen.findByText(/checkout_plans/i);
/Check out features in paid plans/i,
);
expect(checkPaidPlan).toBeInTheDocument(); expect(checkPaidPlan).toBeInTheDocument();
const link = screen.getByRole('link', { name: /here/i }); const link = screen.getByRole('link', { name: /here/i });
@ -114,7 +110,7 @@ describe('BillingContainer', () => {
render(<BillingContainer />); render(<BillingContainer />);
}); });
const currentBill = screen.getByText('Billing'); const currentBill = screen.getByText('billing');
expect(currentBill).toBeInTheDocument(); expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$0/i); const dollar0 = await screen.findByText(/\$0/i);
@ -126,17 +122,17 @@ describe('BillingContainer', () => {
expect(onTrail).toBeInTheDocument(); expect(onTrail).toBeInTheDocument();
const receivedCardDetails = await screen.findByText( const receivedCardDetails = await screen.findByText(
/We have received your card details, your billing will only start after the end of your free trial period./i, /card_details_recieved_and_billing_info/i,
); );
expect(receivedCardDetails).toBeInTheDocument(); expect(receivedCardDetails).toBeInTheDocument();
const manageBillingButton = await screen.findByRole('button', { const manageBillingButton = await screen.findByRole('button', {
name: /manage billing/i, name: /manage_billing/i,
}); });
expect(manageBillingButton).toBeInTheDocument(); expect(manageBillingButton).toBeInTheDocument();
const dayRemainingInBillingPeriod = await screen.findByText( const dayRemainingInBillingPeriod = await screen.findByText(
/1 days remaining in your billing period./i, /1 days_remaining/i,
); );
expect(dayRemainingInBillingPeriod).toBeInTheDocument(); expect(dayRemainingInBillingPeriod).toBeInTheDocument();
}); });
@ -156,7 +152,7 @@ describe('BillingContainer', () => {
const billingPeriod = await findByText(billingPeriodText); const billingPeriod = await findByText(billingPeriodText);
expect(billingPeriod).toBeInTheDocument(); expect(billingPeriod).toBeInTheDocument();
const currentBill = screen.getByText('Billing'); const currentBill = screen.getByText('billing');
expect(currentBill).toBeInTheDocument(); expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$1,278.3/i); const dollar0 = await screen.findByText(/\$1,278.3/i);
@ -181,7 +177,7 @@ describe('BillingContainer', () => {
); );
render(<BillingContainer />); render(<BillingContainer />);
const dayRemainingInBillingPeriod = await screen.findByText( const dayRemainingInBillingPeriod = await screen.findByText(
/11 days remaining in your billing period./i, /11 days_remaining/i,
); );
expect(dayRemainingInBillingPeriod).toBeInTheDocument(); expect(dayRemainingInBillingPeriod).toBeInTheDocument();
}); });

View File

@ -17,7 +17,7 @@ import {
} from 'antd'; } from 'antd';
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import updateCreditCardApi from 'api/billing/checkout'; import updateCreditCardApi from 'api/billing/checkout';
import getUsage from 'api/billing/getUsage'; import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
import manageCreditCardApi from 'api/billing/manage'; import manageCreditCardApi from 'api/billing/manage';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
@ -26,8 +26,9 @@ import useAnalytics from 'hooks/analytics/useAnalytics';
import useAxiosError from 'hooks/useAxiosError'; import useAxiosError from 'hooks/useAxiosError';
import useLicense from 'hooks/useLicense'; import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { pick } from 'lodash-es'; import { isEmpty, pick } from 'lodash-es';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query'; import { useMutation, useQuery } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -49,6 +50,11 @@ interface DataType {
cost: string; cost: string;
} }
enum SubscriptionStatus {
PastDue = 'past_due',
Active = 'active',
}
const renderSkeletonInput = (): JSX.Element => ( const renderSkeletonInput = (): JSX.Element => (
<Skeleton.Input <Skeleton.Input
style={{ marginTop: '10px', height: '40px', width: '100%' }} style={{ marginTop: '10px', height: '40px', width: '100%' }}
@ -116,15 +122,19 @@ const dummyColumns: ColumnsType<DataType> = [
}, },
]; ];
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function BillingContainer(): JSX.Element { export default function BillingContainer(): JSX.Element {
const daysRemainingStr = 'days remaining in your billing period.'; const { t } = useTranslation(['billings']);
const daysRemainingStr = t('days_remaining');
const [headerText, setHeaderText] = useState(''); const [headerText, setHeaderText] = useState('');
const [billAmount, setBillAmount] = useState(0); const [billAmount, setBillAmount] = useState(0);
const [activeLicense, setActiveLicense] = useState<License | null>(null); const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [daysRemaining, setDaysRemaining] = useState(0); const [daysRemaining, setDaysRemaining] = useState(0);
const [isFreeTrial, setIsFreeTrial] = useState(false); const [isFreeTrial, setIsFreeTrial] = useState(false);
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [apiResponse, setApiResponse] = useState<any>({}); const [apiResponse, setApiResponse] = useState<
Partial<UsageResponsePayloadProps>
>({});
const { trackEvent } = useAnalytics(); const { trackEvent } = useAnalytics();
@ -139,6 +149,9 @@ export default function BillingContainer(): JSX.Element {
const processUsageData = useCallback( const processUsageData = useCallback(
(data: any): void => { (data: any): void => {
if (isEmpty(data?.payload)) {
return;
}
const { const {
details: { breakdown = [], billTotal }, details: { breakdown = [], billTotal },
billingPeriodStart, billingPeriodStart,
@ -186,6 +199,9 @@ export default function BillingContainer(): JSX.Element {
[licensesData?.payload?.onTrial], [licensesData?.payload?.onTrial],
); );
const isSubscriptionPastDue =
apiResponse.subscriptionStatus === SubscriptionStatus.PastDue;
const { isLoading, isFetching: isFetchingBillingData } = useQuery( const { isLoading, isFetching: isFetchingBillingData } = useQuery(
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId], [REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
{ {
@ -342,14 +358,27 @@ export default function BillingContainer(): JSX.Element {
[apiResponse, billAmount, isLoading, isFetchingBillingData], [apiResponse, billAmount, isLoading, isFetchingBillingData],
); );
const { Text } = Typography;
const subscriptionPastDueMessage = (): JSX.Element => (
<Typography>
{`We were not able to process payments for your account. Please update your card details `}
<Text type="danger" onClick={handleBilling} style={{ cursor: 'pointer' }}>
{t('here')}
</Text>
{` if your payment information has changed. Email us at `}
<Text type="secondary">cloud-support@signoz.io</Text>
{` otherwise. Be sure to provide this information immediately to avoid interruption to your service.`}
</Typography>
);
return ( return (
<div className="billing-container"> <div className="billing-container">
<Flex vertical style={{ marginBottom: 16 }}> <Flex vertical style={{ marginBottom: 16 }}>
<Typography.Text style={{ fontWeight: 500, fontSize: 18 }}> <Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
Billing {t('billing')}
</Typography.Text> </Typography.Text>
<Typography.Text color={Color.BG_VANILLA_400}> <Typography.Text color={Color.BG_VANILLA_400}>
Manage your billing information, invoices, and monitor costs. {t('manage_billing_and_costs')}
</Typography.Text> </Typography.Text>
</Flex> </Flex>
@ -361,7 +390,7 @@ export default function BillingContainer(): JSX.Element {
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Flex vertical> <Flex vertical>
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}> <Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
{isCloudUserVal ? 'Enterprise Cloud' : 'Enterprise'}{' '} {isCloudUserVal ? t('enterprise_cloud') : t('enterprise')}{' '}
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''} {isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
</Typography.Title> </Typography.Title>
{!isLoading && !isFetchingBillingData ? ( {!isLoading && !isFetchingBillingData ? (
@ -378,8 +407,8 @@ export default function BillingContainer(): JSX.Element {
onClick={handleBilling} onClick={handleBilling}
> >
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription {isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
? 'Upgrade Plan' ? t('upgrade_plan')
: 'Manage Billing'} : t('manage_billing')}
</Button> </Button>
</Flex> </Flex>
@ -389,21 +418,34 @@ export default function BillingContainer(): JSX.Element {
ellipsis ellipsis
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }} style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
> >
We have received your card details, your billing will only start after {t('card_details_recieved_and_billing_info')}
the end of your free trial period.
</Typography.Text> </Typography.Text>
)} )}
{!isLoading && !isFetchingBillingData ? ( {!isLoading && !isFetchingBillingData ? (
<Alert headerText && (
message={headerText} <Alert
type="info" message={headerText}
showIcon type="info"
style={{ marginTop: 12 }} showIcon
/> style={{ marginTop: 12 }}
/>
)
) : ( ) : (
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} /> <Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
)} )}
{isSubscriptionPastDue &&
(!isLoading && !isFetchingBillingData ? (
<Alert
message={subscriptionPastDueMessage()}
type="error"
showIcon
style={{ marginTop: 12 }}
/>
) : (
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
))}
</Card> </Card>
<BillingUsageGraphCallback /> <BillingUsageGraphCallback />
@ -434,16 +476,16 @@ export default function BillingContainer(): JSX.Element {
<Col span={20} className="plan-benefits"> <Col span={20} className="plan-benefits">
<Typography.Text className="plan-benefit"> <Typography.Text className="plan-benefit">
<CheckCircleOutlined /> <CheckCircleOutlined />
Upgrade now to have uninterrupted access {t('upgrade_now_text')}
</Typography.Text> </Typography.Text>
<Typography.Text className="plan-benefit"> <Typography.Text className="plan-benefit">
<CheckCircleOutlined /> <CheckCircleOutlined />
Your billing will start only after the trial period {t('Your billing will start only after the trial period')}
</Typography.Text> </Typography.Text>
<Typography.Text className="plan-benefit"> <Typography.Text className="plan-benefit">
<CheckCircleOutlined /> <CheckCircleOutlined />
<span> <span>
Check out features in paid plans &nbsp; {t('checkout_plans')} &nbsp;
<a <a
href="https://signoz.io/pricing/" href="https://signoz.io/pricing/"
style={{ style={{
@ -452,7 +494,7 @@ export default function BillingContainer(): JSX.Element {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
here {t('here')}
</a> </a>
</span> </span>
</Typography.Text> </Typography.Text>
@ -464,7 +506,7 @@ export default function BillingContainer(): JSX.Element {
loading={isLoadingBilling || isLoadingManageBilling} loading={isLoadingBilling || isLoadingManageBilling}
onClick={handleBilling} onClick={handleBilling}
> >
Upgrade Plan {t('upgrade_plan')}
</Button> </Button>
</Col> </Col>
</Row> </Row>

View File

@ -3,9 +3,7 @@ import '../../../lib/uPlotLib/uPlotLib.styles.scss';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Card, Flex, Typography } from 'antd'; import { Card, Flex, Typography } from 'antd';
import { getComponentForPanelType } from 'constants/panelTypes'; import Uplot from 'components/Uplot';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PropsTypePropsMap } from 'container/GridPanelSwitch/types';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions'; import { useResizeObserver } from 'hooks/useDimensions';
import tooltipPlugin from 'lib/uPlotLib/plugins/tooltipPlugin'; import tooltipPlugin from 'lib/uPlotLib/plugins/tooltipPlugin';
@ -14,7 +12,7 @@ import getRenderer from 'lib/uPlotLib/utils/getRenderer';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { getXAxisScale } from 'lib/uPlotLib/utils/getXAxisScale'; import { getXAxisScale } from 'lib/uPlotLib/utils/getXAxisScale';
import { getYAxisScale } from 'lib/uPlotLib/utils/getYAxisScale'; import { getYAxisScale } from 'lib/uPlotLib/utils/getYAxisScale';
import { FC, useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import uPlot from 'uplot'; import uPlot from 'uplot';
import { import {
@ -43,6 +41,21 @@ const paths = (
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip); return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
}; };
const calculateStartEndTime = (
data: any,
): { startTime: number; endTime: number } => {
const timestamps: number[] = [];
data?.details?.breakdown?.forEach((breakdown: any) => {
breakdown?.dayWiseBreakdown?.breakdown?.forEach((entry: any) => {
timestamps.push(entry?.timestamp);
});
});
const billingTime = [data?.billingPeriodStart, data?.billingPeriodEnd];
const startTime: number = Math.min(...timestamps, ...billingTime);
const endTime: number = Math.max(...timestamps, ...billingTime);
return { startTime, endTime };
};
export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element { export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
const { data, billAmount } = props; const { data, billAmount } = props;
const graphCompatibleData = useMemo( const graphCompatibleData = useMemo(
@ -54,11 +67,9 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const containerDimensions = useResizeObserver(graphRef); const containerDimensions = useResizeObserver(graphRef);
const { billingPeriodStart: startTime, billingPeriodEnd: endTime } = data; const { startTime, endTime } = useMemo(() => calculateStartEndTime(data), [
data,
const Component = getComponentForPanelType(PANEL_TYPES.BAR) as FC< ]);
PropsTypePropsMap[PANEL_TYPES]
>;
const getGraphSeries = (color: string, label: string): any => ({ const getGraphSeries = (color: string, label: string): any => ({
drawStyle: 'bars', drawStyle: 'bars',
@ -183,7 +194,7 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
</Flex> </Flex>
</Flex> </Flex>
<div ref={graphRef} style={{ height: '100%', paddingBottom: 48 }}> <div ref={graphRef} style={{ height: '100%', paddingBottom: 48 }}>
<Component data={chartData} options={optionsForChart} /> <Uplot data={chartData} options={optionsForChart} />
</div> </div>
</Card> </Card>
); );

View File

@ -0,0 +1,84 @@
.download-logs-popover {
.ant-popover-inner {
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
padding: 12px 18px 12px 14px;
.download-logs-content {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
.action-btns {
padding: 4px 0px !important;
width: 159px;
display: flex;
align-items: center;
color: var(--bg-vanilla-400);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
gap: 6px;
.ant-btn-icon {
margin-inline-end: 0px;
}
}
.action-btns:hover {
&.ant-btn-text {
background-color: rgba(171, 189, 255, 0.04) !important;
}
}
.export-heading {
color: #52575c;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
}
}
}
}
.lightMode {
.download-logs-popover {
.ant-popover-inner {
border: 1px solid var(--bg-vanilla-300);
background: linear-gradient(
139deg,
rgba(255, 255, 255, 0.8) 0%,
rgba(255, 255, 255, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
.download-logs-content {
.action-btns {
color: var(--bg-ink-400);
}
.action-btns:hover {
&.ant-btn-text {
background-color: var(--bg-vanilla-300) !important;
}
}
.export-heading {
color: var(--bg-ink-200);
}
}
}
}
}

View File

@ -0,0 +1,84 @@
import './DownloadV2.styles.scss';
import { Button, Popover, Typography } from 'antd';
import { Excel } from 'antd-table-saveas-excel';
import { FileDigit, FileDown, Sheet } from 'lucide-react';
import { unparse } from 'papaparse';
import { DownloadProps } from './DownloadV2.types';
function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
const downloadExcelFile = (): void => {
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
});
const excel = new Excel();
excel
.addSheet(fileName)
.addColumns(headers)
.addDataSource(data, {
str2Percent: true,
})
.saveAs(`${fileName}.xlsx`);
};
const downloadCsvFile = (): void => {
const csv = unparse(data);
const csvBlob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const csvUrl = URL.createObjectURL(csvBlob);
const downloadLink = document.createElement('a');
downloadLink.href = csvUrl;
downloadLink.download = `${fileName}.csv`;
downloadLink.click();
downloadLink.remove();
};
return (
<Popover
trigger={['click']}
placement="bottomRight"
rootClassName="download-logs-popover"
arrow={false}
content={
<div className="download-logs-content">
<Typography.Text className="export-heading">Export As</Typography.Text>
<Button
icon={<Sheet size={14} />}
type="text"
onClick={downloadExcelFile}
className="action-btns"
>
Excel (.xlsx)
</Button>
<Button
icon={<FileDigit size={14} />}
type="text"
onClick={downloadCsvFile}
className="action-btns"
>
CSV
</Button>
</div>
}
>
<Button
className="periscope-btn"
loading={isLoading}
icon={<FileDown size={14} />}
/>
</Popover>
);
}
Download.defaultProps = {
isLoading: undefined,
};
export default Download;

View File

@ -0,0 +1,10 @@
export type DownloadOptions = {
isDownloadEnabled: boolean;
fileName: string;
};
export type DownloadProps = {
data: Record<string, string>[];
isLoading?: boolean;
fileName: string;
};

View File

@ -0,0 +1,39 @@
import { useEffect, useState } from 'react';
import ExplorerOptions, { ExplorerOptionsProps } from './ExplorerOptions';
import { getExplorerToolBarVisibility } from './utils';
type ExplorerOptionsWrapperProps = Omit<
ExplorerOptionsProps,
'isExplorerOptionDrop'
>;
function ExplorerOptionWrapper({
disabled,
query,
isLoading,
onExport,
sourcepage,
}: ExplorerOptionsWrapperProps): JSX.Element {
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
useEffect(() => {
const toolbarVisibility = getExplorerToolBarVisibility(sourcepage);
setIsExplorerOptionHidden(!toolbarVisibility);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<ExplorerOptions
disabled={disabled}
query={query}
isLoading={isLoading}
onExport={onExport}
sourcepage={sourcepage}
isExplorerOptionHidden={isExplorerOptionHidden}
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
/>
);
}
export default ExplorerOptionWrapper;

View File

@ -3,8 +3,8 @@
} }
.explorer-update { .explorer-update {
position: fixed; position: fixed;
bottom: 16px; bottom: 24px;
left: calc(50% - 225px); left: calc(50% - 250px);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
@ -37,21 +37,24 @@
} }
} }
.explorer-options { .explorer-options {
display: flex; position: fixed;
gap: 16px; bottom: 24px;
left: calc(50% + 240px);
padding: 10px 12px; padding: 10px 12px;
border-radius: 50px; transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
border: 1px solid var(--bg-slate-400); border: 1px solid var(--bg-slate-400);
border-radius: 50px;
background: rgba(22, 24, 29, 0.6); background: rgba(22, 24, 29, 0.6);
box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25); box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
position: fixed;
bottom: 16px;
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
cursor: default;
display: flex;
gap: 16px;
z-index: 1;
.ant-select-selector { .ant-select-selector {
padding: 0 !important; padding: 0 !important;
} }
@ -236,9 +239,9 @@
.lightMode { .lightMode {
.explorer-options { .explorer-options {
background: transparent;
box-shadow: none;
border: 1px solid var(--bg-vanilla-300); border: 1px solid var(--bg-vanilla-300);
background: rgba(255, 255, 255, 0.8);
box-shadow: 4px 4px 16px 4px rgba(255, 255, 255, 0.55);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
hr { hr {

View File

@ -1,3 +1,4 @@
/* eslint-disable react/jsx-props-no-spreading */
import './ExplorerOptions.styles.scss'; import './ExplorerOptions.styles.scss';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
@ -30,8 +31,24 @@ import useErrorNotification from 'hooks/useErrorNotification';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery'; import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { Check, ConciergeBell, Disc3, Plus, X, XCircle } from 'lucide-react'; import {
import { CSSProperties, useCallback, useMemo, useRef, useState } from 'react'; Check,
ConciergeBell,
Disc3,
PanelBottomClose,
Plus,
X,
XCircle,
} from 'lucide-react';
import {
CSSProperties,
Dispatch,
SetStateAction,
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -41,11 +58,13 @@ import { DataSource } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles'; import { USER_ROLES } from 'types/roles';
import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
import { import {
DATASOURCE_VS_ROUTES, DATASOURCE_VS_ROUTES,
generateRGBAFromHex, generateRGBAFromHex,
getRandomColor, getRandomColor,
saveNewViewHandler, saveNewViewHandler,
setExplorerToolBarVisibility,
} from './utils'; } from './utils';
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR]; const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
@ -57,6 +76,8 @@ function ExplorerOptions({
onExport, onExport,
query, query,
sourcepage, sourcepage,
isExplorerOptionHidden = false,
setIsExplorerOptionHidden,
}: ExplorerOptionsProps): JSX.Element { }: ExplorerOptionsProps): JSX.Element {
const [isExport, setIsExport] = useState<boolean>(false); const [isExport, setIsExport] = useState<boolean>(false);
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false); const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
@ -257,11 +278,18 @@ function ExplorerOptions({
[isDarkMode], [isDarkMode],
); );
const hideToolbar = (): void => {
setExplorerToolBarVisibility(false, sourcepage);
if (setIsExplorerOptionHidden) {
setIsExplorerOptionHidden(true);
}
};
const isEditDeleteSupported = allowedRoles.includes(role as string); const isEditDeleteSupported = allowedRoles.includes(role as string);
return ( return (
<> <>
{isQueryUpdated && ( {isQueryUpdated && !isExplorerOptionHidden && (
<div <div
className={cx( className={cx(
isEditDeleteSupported ? '' : 'hide-update', isEditDeleteSupported ? '' : 'hide-update',
@ -289,87 +317,103 @@ function ExplorerOptions({
</Tooltip> </Tooltip>
</div> </div>
)} )}
<div {!isExplorerOptionHidden && (
className="explorer-options" <div
style={{ className="explorer-options"
background: extraData style={{
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)` background: extraData
: 'transparent', ? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
backdropFilter: 'blur(20px)', : 'transparent',
}} }}
> >
<div className="view-options"> <div className="view-options">
<Select<string, { key: string; value: string }> <Select<string, { key: string; value: string }>
showSearch showSearch
placeholder="Select a view" placeholder="Select a view"
loading={viewsIsLoading || isRefetching} loading={viewsIsLoading || isRefetching}
value={viewName || undefined} value={viewName || undefined}
onSelect={handleSelect} onSelect={handleSelect}
style={{ style={{
minWidth: 170, minWidth: 170,
}} }}
dropdownStyle={dropdownStyle} dropdownStyle={dropdownStyle}
className="views-dropdown" className="views-dropdown"
allowClear={{ allowClear={{
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />, clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
}} }}
onClear={handleClearSelect} onClear={handleClearSelect}
ref={ref} ref={ref}
>
{viewsData?.data?.data?.map((view) => {
const extraData =
view.extraData !== '' ? JSON.parse(view.extraData) : '';
let bgColor = getRandomColor();
if (extraData !== '') {
bgColor = extraData.color;
}
return (
<Select.Option key={view.uuid} value={view.name}>
<div className="render-options">
<span
className="dot"
style={{
background: bgColor,
boxShadow: `0px 0px 6px 0px ${bgColor}`,
}}
/>{' '}
{view.name}
</div>
</Select.Option>
);
})}
</Select>
<Button
shape="round"
onClick={handleSaveViewModalToggle}
className={isEditDeleteSupported ? '' : 'hidden'}
disabled={viewsIsLoading || isRefetching}
>
<Disc3 size={16} /> Save this view
</Button>
</div>
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
<Tooltip title="Create Alerts">
<Button
disabled={disabled}
shape="circle"
onClick={onCreateAlertsHandler}
> >
<ConciergeBell size={16} /> {viewsData?.data?.data?.map((view) => {
</Button> const extraData =
</Tooltip> view.extraData !== '' ? JSON.parse(view.extraData) : '';
let bgColor = getRandomColor();
if (extraData !== '') {
bgColor = extraData.color;
}
return (
<Select.Option key={view.uuid} value={view.name}>
<div className="render-options">
<span
className="dot"
style={{
background: bgColor,
boxShadow: `0px 0px 6px 0px ${bgColor}`,
}}
/>{' '}
{view.name}
</div>
</Select.Option>
);
})}
</Select>
<Tooltip title="Add to Dashboard"> <Button
<Button disabled={disabled} shape="circle" onClick={onAddToDashboard}> shape="round"
<Plus size={16} /> onClick={handleSaveViewModalToggle}
className={isEditDeleteSupported ? '' : 'hidden'}
disabled={viewsIsLoading || isRefetching}
>
<Disc3 size={16} /> Save this view
</Button> </Button>
</Tooltip> </div>
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
<Tooltip title="Create Alerts">
<Button
disabled={disabled}
shape="circle"
onClick={onCreateAlertsHandler}
>
<ConciergeBell size={16} />
</Button>
</Tooltip>
<Tooltip title="Add to Dashboard">
<Button disabled={disabled} shape="circle" onClick={onAddToDashboard}>
<Plus size={16} />
</Button>
</Tooltip>
<Tooltip title="Hide">
<Button disabled={disabled} shape="circle" onClick={hideToolbar}>
<PanelBottomClose size={16} />
</Button>
</Tooltip>
</div>
</div> </div>
</div> )}
<ExplorerOptionsHideArea
isExplorerOptionHidden={isExplorerOptionHidden}
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
sourcepage={sourcepage}
isQueryUpdated={isQueryUpdated}
handleClearSelect={handleClearSelect}
onUpdateQueryHandler={onUpdateQueryHandler}
/>
<Modal <Modal
className="save-view-modal" className="save-view-modal"
@ -427,8 +471,14 @@ export interface ExplorerOptionsProps {
query: Query | null; query: Query | null;
disabled: boolean; disabled: boolean;
sourcepage: DataSource; sourcepage: DataSource;
isExplorerOptionHidden?: boolean;
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
} }
ExplorerOptions.defaultProps = { isLoading: false }; ExplorerOptions.defaultProps = {
isLoading: false,
isExplorerOptionHidden: false,
setIsExplorerOptionHidden: undefined,
};
export default ExplorerOptions; export default ExplorerOptions;

View File

@ -0,0 +1,55 @@
.explorer-option-droppable-container {
position: fixed;
bottom: 0;
width: -webkit-fill-available;
height: 24px;
display: flex;
justify-content: center;
border-radius: 10px 10px 0px 0px;
// box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
// backdrop-filter: blur(20px);
.explorer-actions-btn {
display: flex;
gap: 8px;
margin-right: 8px;
.action-btn {
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px 10px 0px 0px;
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
height: 24px !important;
border: none;
}
}
.explorer-show-btn {
border-radius: 10px 10px 0px 0px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.40);
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
align-self: center;
padding: 8px 12px;
height: 24px !important;
.menu-bar {
border-radius: 50px;
background: var(--bg-slate-200);
height: 4px;
width: 50px;
}
}
}
.lightMode {
.explorer-option-droppable-container {
.explorer-show-btn {
background: var(--bg-vanilla-200);
}
}
}

View File

@ -0,0 +1,78 @@
/* eslint-disable no-nested-ternary */
import './ExplorerOptionsHideArea.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Tooltip } from 'antd';
import { Disc3, X } from 'lucide-react';
import { Dispatch, SetStateAction } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { setExplorerToolBarVisibility } from './utils';
interface DroppableAreaProps {
isQueryUpdated: boolean;
isExplorerOptionHidden?: boolean;
sourcepage: DataSource;
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
handleClearSelect: () => void;
onUpdateQueryHandler: () => void;
}
function ExplorerOptionsHideArea({
isQueryUpdated,
isExplorerOptionHidden,
sourcepage,
setIsExplorerOptionHidden,
handleClearSelect,
onUpdateQueryHandler,
}: DroppableAreaProps): JSX.Element {
const handleShowExplorerOption = (): void => {
if (setIsExplorerOptionHidden) {
setIsExplorerOptionHidden(false);
setExplorerToolBarVisibility(true, sourcepage);
}
};
return (
<div className="explorer-option-droppable-container">
{isExplorerOptionHidden && (
<>
{isQueryUpdated && (
<div className="explorer-actions-btn">
<Tooltip title="Clear this view">
<Button
onClick={handleClearSelect}
className="action-btn"
style={{ background: Color.BG_CHERRY_500 }}
icon={<X size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
<Tooltip title="Update this View">
<Button
onClick={onUpdateQueryHandler}
className="action-btn"
style={{ background: Color.BG_ROBIN_500 }}
icon={<Disc3 size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
</div>
)}
<Button
// style={{ alignSelf: 'center', marginRight: 'calc(10% - 20px)' }}
className="explorer-show-btn"
onClick={handleShowExplorerOption}
>
<div className="menu-bar" />
</Button>
</>
)}
</div>
);
}
ExplorerOptionsHideArea.defaultProps = {
isExplorerOptionHidden: undefined,
setIsExplorerOptionHidden: undefined,
};
export default ExplorerOptionsHideArea;

View File

@ -1,5 +1,6 @@
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { showErrorNotification } from 'components/ExplorerCard/utils'; import { showErrorNotification } from 'components/ExplorerCard/utils';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
@ -67,3 +68,54 @@ export const generateRGBAFromHex = (hex: string, opacity: number): string =>
hex.slice(3, 5), hex.slice(3, 5),
16, 16,
)}, ${parseInt(hex.slice(5, 7), 16)}, ${opacity})`; )}, ${parseInt(hex.slice(5, 7), 16)}, ${opacity})`;
export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
try {
const showExplorerToolbar = localStorage.getItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar === null) {
const parsedShowExplorerToolbar: {
[DataSource.LOGS]: boolean;
[DataSource.TRACES]: boolean;
[DataSource.METRICS]: boolean;
} = {
[DataSource.METRICS]: true,
[DataSource.TRACES]: true,
[DataSource.LOGS]: true,
};
localStorage.setItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);
return true;
}
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar || '{}');
return parsedShowExplorerToolbar[dataSource];
} catch (error) {
console.error(error);
return false;
}
};
export const setExplorerToolBarVisibility = (
value: boolean,
dataSource: string,
): void => {
try {
const showExplorerToolbar = localStorage.getItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar) {
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar);
parsedShowExplorerToolbar[dataSource] = value;
localStorage.setItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);
return;
}
} catch (error) {
console.error(error);
}
};

View File

@ -1,20 +1,30 @@
import { InfoCircleOutlined } from '@ant-design/icons'; import { InfoCircleOutlined } from '@ant-design/icons';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch'; import GridPanelSwitch from 'container/GridPanelSwitch';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories'; import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config'; import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config'; import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions'; import { useResizeObserver } from 'hooks/useDimensions';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { AlertDef } from 'types/api/alerts/def'; import { AlertDef } from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
@ -32,7 +42,7 @@ export interface ChartPreviewProps {
query: Query | null; query: Query | null;
graphType?: PANEL_TYPES; graphType?: PANEL_TYPES;
selectedTime?: timePreferenceType; selectedTime?: timePreferenceType;
selectedInterval?: Time | TimeV2; selectedInterval?: Time | TimeV2 | CustomTimeType;
headline?: JSX.Element; headline?: JSX.Element;
alertDef?: AlertDef; alertDef?: AlertDef;
userQueryKey?: string; userQueryKey?: string;
@ -46,7 +56,7 @@ function ChartPreview({
query, query,
graphType = PANEL_TYPES.TIME_SERIES, graphType = PANEL_TYPES.TIME_SERIES,
selectedTime = 'GLOBAL_TIME', selectedTime = 'GLOBAL_TIME',
selectedInterval = '5min', selectedInterval = '5m',
headline, headline,
userQueryKey, userQueryKey,
allowSelectedIntervalForStepGen = false, allowSelectedIntervalForStepGen = false,
@ -54,6 +64,7 @@ function ChartPreview({
yAxisUnit, yAxisUnit,
}: ChartPreviewProps): JSX.Element | null { }: ChartPreviewProps): JSX.Element | null {
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
const dispatch = useDispatch();
const threshold = alertDef?.condition.target || 0; const threshold = alertDef?.condition.target || 0;
const [minTimeScale, setMinTimeScale] = useState<number>(); const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>(); const [maxTimeScale, setMaxTimeScale] = useState<number>();
@ -63,6 +74,30 @@ function ChartPreview({
GlobalReducer GlobalReducer
>((state) => state.globalTime); >((state) => state.globalTime);
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
const endTime = searchParams.get(QueryParams.endTime);
if (startTime && endTime && startTime !== endTime) {
dispatch(
UpdateTimeInterval('custom', [
parseInt(getTimeString(startTime), 10),
parseInt(getTimeString(endTime), 10),
]),
);
}
};
useEffect(() => {
window.addEventListener('popstate', handleBackNavigation);
return (): void => {
window.removeEventListener('popstate', handleBackNavigation);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const canQuery = useMemo((): boolean => { const canQuery = useMemo((): boolean => {
if (!query || query == null) { if (!query || query == null) {
return false; return false;
@ -131,10 +166,34 @@ function ChartPreview({
const containerDimensions = useResizeObserver(graphRef); const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const urlQuery = useUrlQuery();
const location = useLocation();
const optionName = const optionName =
getFormatNameByOptionId(alertDef?.condition.targetUnit || '') || ''; getFormatNameByOptionId(alertDef?.condition.targetUnit || '') || '';
const onDragSelect = useCallback(
(start: number, end: number): void => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
const { maxTime, minTime } = GetMinMax('custom', [
startTimestamp,
endTimestamp,
]);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
);
const options = useMemo( const options = useMemo(
() => () =>
getUPlotChartOptions({ getUPlotChartOptions({
@ -145,6 +204,7 @@ function ChartPreview({
minTimeScale, minTimeScale,
maxTimeScale, maxTimeScale,
isDarkMode, isDarkMode,
onDragSelect,
thresholds: [ thresholds: [
{ {
index: '0', // no impact index: '0', // no impact
@ -174,6 +234,7 @@ function ChartPreview({
minTimeScale, minTimeScale,
maxTimeScale, maxTimeScale,
isDarkMode, isDarkMode,
onDragSelect,
threshold, threshold,
t, t,
optionName, optionName,

View File

@ -1,5 +1,6 @@
import { import {
Checkbox, Checkbox,
Collapse,
Form, Form,
InputNumber, InputNumber,
InputNumberProps, InputNumberProps,
@ -19,12 +20,18 @@ import {
AlertDef, AlertDef,
defaultCompareOp, defaultCompareOp,
defaultEvalWindow, defaultEvalWindow,
defaultFrequency,
defaultMatchType, defaultMatchType,
} from 'types/api/alerts/def'; } from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { popupContainer } from 'utils/selectPopupContainer'; import { popupContainer } from 'utils/selectPopupContainer';
import { FormContainer, InlineSelect, StepHeading } from './styles'; import {
FormContainer,
InlineSelect,
StepHeading,
VerticalLine,
} from './styles';
function RuleOptions({ function RuleOptions({
alertDef, alertDef,
@ -200,6 +207,35 @@ function RuleOptions({
}); });
}; };
const onChangeFrequency = (value: string | unknown): void => {
const freq = (value as string) || alertDef.frequency;
setAlertDef({
...alertDef,
frequency: freq,
});
};
const renderFrequency = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
defaultValue={defaultFrequency}
style={{ minWidth: '120px' }}
value={alertDef.frequency}
onChange={onChangeFrequency}
>
<Select.Option value="1m0s">{t('option_1min')}</Select.Option>
<Select.Option value="5m0s">{t('option_5min')}</Select.Option>
<Select.Option value="10m0s">{t('option_10min')}</Select.Option>
<Select.Option value="15m0s">{t('option_15min')}</Select.Option>
<Select.Option value="30m0s">{t('option_30min')}</Select.Option>
<Select.Option value="1h0m0s">{t('option_60min')}</Select.Option>
<Select.Option value="3h0m0s">{t('option_3hours')}</Select.Option>
<Select.Option value="6h0m0s">{t('option_6hours')}</Select.Option>
<Select.Option value="12h0m0s">{t('option_12hours')}</Select.Option>
<Select.Option value="24h0m0s">{t('option_24hours')}</Select.Option>
</InlineSelect>
);
const selectedCategory = getCategoryByOptionId(currentQuery?.unit || ''); const selectedCategory = getCategoryByOptionId(currentQuery?.unit || '');
const categorySelectOptions = getCategorySelectOptionByName( const categorySelectOptions = getCategorySelectOptionByName(
@ -238,42 +274,57 @@ function RuleOptions({
/> />
</Form.Item> </Form.Item>
</Space> </Space>
<Space direction="horizontal" align="center"> <Collapse>
<Form.Item noStyle name={['condition', 'alertOnAbsent']}> <Collapse.Panel header={t('More options')} key="1">
<Checkbox <Space direction="vertical" size="large">
checked={alertDef?.condition?.alertOnAbsent} <VerticalLine>
onChange={(e): void => { <Space direction="horizontal" align="center">
setAlertDef({ <Typography.Text>{t('text_alert_frequency')}</Typography.Text>
...alertDef, {renderFrequency()}
condition: { </Space>
...alertDef.condition, </VerticalLine>
alertOnAbsent: e.target.checked,
},
});
}}
/>
</Form.Item>
<Typography.Text>{t('text_alert_on_absent')}</Typography.Text>
<Form.Item noStyle name={['condition', 'absentFor']}> <VerticalLine>
<InputNumber <Space direction="horizontal" align="center">
min={1} <Form.Item noStyle name={['condition', 'alertOnAbsent']}>
value={alertDef?.condition?.absentFor} <Checkbox
onChange={(value): void => { checked={alertDef?.condition?.alertOnAbsent}
setAlertDef({ onChange={(e): void => {
...alertDef, setAlertDef({
condition: { ...alertDef,
...alertDef.condition, condition: {
absentFor: Number(value) || 0, ...alertDef.condition,
}, alertOnAbsent: e.target.checked,
}); },
}} });
type="number" }}
onWheel={(e): void => e.currentTarget.blur()} />
/> </Form.Item>
</Form.Item> <Typography.Text>{t('text_alert_on_absent')}</Typography.Text>
<Typography.Text>{t('text_for')}</Typography.Text>
</Space> <Form.Item noStyle name={['condition', 'absentFor']}>
<InputNumber
min={1}
value={alertDef?.condition?.absentFor}
onChange={(value): void => {
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
absentFor: Number(value) || 0,
},
});
}}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
<Typography.Text>{t('text_for')}</Typography.Text>
</Space>
</VerticalLine>
</Space>
</Collapse.Panel>
</Collapse>
</Space> </Space>
</FormContainer> </FormContainer>
</> </>

View File

@ -67,6 +67,13 @@ export const SeveritySelect = styled(Select)`
width: 25% !important; width: 25% !important;
`; `;
export const VerticalLine = styled.div`
border-left: 2px solid #e8e8e8; /* Adjust color and thickness as desired */
padding-left: 20px; /* Adjust spacing to content as needed */
margin-left: 20px; /* Adjust margin as desired */
height: 100%; /* Adjust based on your layout needs */
`;
export const InputSmall = styled(Input)` export const InputSmall = styled(Input)`
width: 40% !important; width: 40% !important;
`; `;

View File

@ -12,22 +12,30 @@ import {
// toChartInterval converts eval window to chart selection time interval // toChartInterval converts eval window to chart selection time interval
export const toChartInterval = (evalWindow: string | undefined): Time => { export const toChartInterval = (evalWindow: string | undefined): Time => {
switch (evalWindow) { switch (evalWindow) {
case '1m0s':
return '1m';
case '5m0s': case '5m0s':
return '5min'; return '5m';
case '10m0s': case '10m0s':
return '10min'; return '10m';
case '15m0s': case '15m0s':
return '15min'; return '15m';
case '30m0s': case '30m0s':
return '30min'; return '30m';
case '1h0m0s': case '1h0m0s':
return '1hr'; return '1h';
case '3h0m0s':
return '3h';
case '4h0m0s': case '4h0m0s':
return '4hr'; return '4h';
case '6h0m0s':
return '6h';
case '12h0m0s':
return '12h';
case '24h0m0s': case '24h0m0s':
return '1day'; return '1d';
default: default:
return '5min'; return '5m';
} }
}; };

View File

@ -1,62 +1,60 @@
import './WidgetFullView.styles.scss'; import './WidgetFullView.styles.scss';
import { SyncOutlined } from '@ant-design/icons'; import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';
import { Button } from 'antd'; import { Button, Spin } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import { ToggleGraphProps } from 'components/Graph/types'; import { ToggleGraphProps } from 'components/Graph/types';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown'; import TimePreference from 'components/TimePreferenceDropDown';
import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { import {
timeItems, timeItems,
timePreferance, timePreferance,
} from 'container/NewWidget/RightContainer/timeItems'; } from 'container/NewWidget/RightContainer/timeItems';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval'; import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useChartMutable } from 'hooks/useChartMutable'; import { useChartMutable } from 'hooks/useChartMutable';
import { useIsDarkMode } from 'hooks/useDarkMode'; import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import GetMinMax from 'lib/getMinMax';
import history from 'lib/history';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot'; import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData'; import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import { getLocalStorageGraphVisibilityState } from '../utils'; import { getLocalStorageGraphVisibilityState } from '../utils';
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants'; import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
import GraphManager from './GraphManager';
import { GraphContainer, TimeContainer } from './styles'; import { GraphContainer, TimeContainer } from './styles';
import { FullViewProps } from './types'; import { FullViewProps } from './types';
function FullView({ function FullView({
widget, widget,
fullViewOptions = true, fullViewOptions = true,
onClickHandler,
name,
version, version,
originalName, originalName,
yAxisUnit,
onDragSelect,
isDependedDataLoaded = false, isDependedDataLoaded = false,
onToggleModelHandler, onToggleModelHandler,
parentChartRef,
}: FullViewProps): JSX.Element { }: FullViewProps): JSX.Element {
const { selectedTime: globalSelectedTime } = useSelector< const { selectedTime: globalSelectedTime } = useSelector<
AppState, AppState,
GlobalReducer GlobalReducer
>((state) => state.globalTime); >((state) => state.globalTime);
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const location = useLocation();
const fullViewRef = useRef<HTMLDivElement>(null); const fullViewRef = useRef<HTMLDivElement>(null);
const [chartOptions, setChartOptions] = useState<uPlot.Options>();
const { selectedDashboard, isDashboardLocked } = useDashboard(); const { selectedDashboard, isDashboardLocked } = useDashboard();
const getSelectedTime = useCallback( const getSelectedTime = useCallback(
@ -74,24 +72,70 @@ function FullView({
const updatedQuery = useStepInterval(widget?.query); const updatedQuery = useStepInterval(widget?.query);
const response = useGetQueryRange( const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
{ if (widget.panelTypes !== PANEL_TYPES.LIST) {
selectedTime: selectedTime.enum, return {
graphType: selectedTime: selectedTime.enum,
widget.panelTypes === PANEL_TYPES.BAR graphType: getGraphType(widget.panelTypes),
? PANEL_TYPES.TIME_SERIES query: updatedQuery,
: widget.panelTypes, globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(selectedDashboard?.data.variables),
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
return {
query: updatedQuery, query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime, globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(selectedDashboard?.data.variables), tableParams: {
}, pagination: {
offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0,
},
},
};
});
useEffect(() => {
setRequestData((prev) => ({
...prev,
selectedTime: selectedTime.enum,
}));
}, [selectedTime]);
const response = useGetQueryRange(
requestData,
selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION, selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
{ {
queryKey: `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`, queryKey: [widget?.query, widget?.panelTypes, requestData, version],
enabled: !isDependedDataLoaded && widget.panelTypes !== PANEL_TYPES.LIST, // Internally both the list view panel has it's own query range api call, so we don't need to call it again enabled: !isDependedDataLoaded,
keepPreviousData: true,
}, },
); );
const onDragSelect = useCallback(
(start: number, end: number): void => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
const { maxTime, minTime } = GetMinMax('custom', [
startTimestamp,
endTimestamp,
]);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
);
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState< const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
boolean[] boolean[]
>(Array(response.data?.payload.data.result.length).fill(true)); >(Array(response.data?.payload.data.result.length).fill(true));
@ -118,60 +162,6 @@ function FullView({
response.data.payload.data.result = sortedSeriesData; response.data.payload.data.result = sortedSeriesData;
} }
const chartData = getUPlotChartData(response?.data?.payload, widget.fillSpans);
const isDarkMode = useIsDarkMode();
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(response);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, response]);
useEffect(() => {
if (!response.isFetching && fullViewRef.current) {
const width = fullViewRef.current?.clientWidth
? fullViewRef.current.clientWidth - 45
: 700;
const height = fullViewRef.current?.clientWidth
? fullViewRef.current.clientHeight
: 300;
const newChartOptions = getUPlotChartOptions({
id: originalName,
yAxisUnit: yAxisUnit || '',
apiResponse: response.data?.payload,
dimensions: {
height,
width,
},
isDarkMode,
onDragSelect,
graphsVisibilityStates,
setGraphsVisibilityStates,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
softMax: widget.softMax === undefined ? null : widget.softMax,
softMin: widget.softMin === undefined ? null : widget.softMin,
panelType: widget.panelTypes,
});
setChartOptions(newChartOptions);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [response.isFetching, graphsVisibilityStates, fullViewRef.current]);
useEffect(() => { useEffect(() => {
graphsVisibilityStates?.forEach((e, i) => { graphsVisibilityStates?.forEach((e, i) => {
fullViewChartRef?.current?.toggleGraph(i, e); fullViewChartRef?.current?.toggleGraph(i, e);
@ -180,7 +170,7 @@ function FullView({
const isListView = widget.panelTypes === PANEL_TYPES.LIST; const isListView = widget.panelTypes === PANEL_TYPES.LIST;
if (response.isFetching) { if (response.isLoading && widget.panelTypes !== PANEL_TYPES.LIST) {
return <Spinner height="100%" size="large" tip="Loading..." />; return <Spinner height="100%" size="large" tip="Loading..." />;
} }
@ -189,6 +179,9 @@ function FullView({
<div className="full-view-header-container"> <div className="full-view-header-container">
{fullViewOptions && ( {fullViewOptions && (
<TimeContainer $panelType={widget.panelTypes}> <TimeContainer $panelType={widget.panelTypes}>
{response.isFetching && (
<Spin spinning indicator={<LoadingOutlined spin />} />
)}
<TimePreference <TimePreference
selectedTime={selectedTime} selectedTime={selectedTime}
setSelectedTime={setSelectedTime} setSelectedTime={setSelectedTime}
@ -214,47 +207,24 @@ function FullView({
})} })}
ref={fullViewRef} ref={fullViewRef}
> >
{chartOptions && ( <GraphContainer
<GraphContainer style={{
style={{ height: isListView ? '100%' : '90%',
height: isListView ? '100%' : '90%', }}
}} isGraphLegendToggleAvailable={canModifyChart}
isGraphLegendToggleAvailable={canModifyChart} >
> <PanelWrapper
<GridPanelSwitch queryResponse={response}
panelType={widget.panelTypes} widget={widget}
data={chartData} setRequestData={setRequestData}
options={chartOptions} isFullViewMode
onClickHandler={onClickHandler} onToggleModelHandler={onToggleModelHandler}
name={name} setGraphVisibility={setGraphsVisibilityStates}
yAxisUnit={yAxisUnit} graphVisibility={graphsVisibilityStates}
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
panelData={response.data?.payload.data.newResult.data.result || []} />
query={widget.query} </GraphContainer>
ref={fullViewChartRef}
thresholds={widget.thresholds}
selectedLogFields={widget.selectedLogFields}
dataSource={widget.query.builder.queryData[0].dataSource}
selectedTracesFields={widget.selectedTracesFields}
selectedTime={selectedTime}
/>
</GraphContainer>
)}
</div> </div>
{canModifyChart && chartOptions && !isDashboardLocked && (
<GraphManager
data={chartData}
name={originalName}
options={chartOptions}
yAxisUnit={yAxisUnit}
onToggleModelHandler={onToggleModelHandler}
setGraphsVisibilityStates={setGraphsVisibilityStates}
graphsVisibilityStates={graphsVisibilityStates}
lineChartRef={fullViewChartRef}
parentChartRef={parentChartRef}
/>
)}
</div> </div>
); );
} }

View File

@ -18,6 +18,7 @@ export const NotFoundContainer = styled.div`
export const TimeContainer = styled.div<Props>` export const TimeContainer = styled.div<Props>`
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center;
${({ $panelType }): FlattenSimpleInterpolation => ${({ $panelType }): FlattenSimpleInterpolation =>
$panelType === PANEL_TYPES.TABLE $panelType === PANEL_TYPES.TABLE
? css` ? css`

View File

@ -53,10 +53,8 @@ export interface FullViewProps {
version?: string; version?: string;
originalName: string; originalName: string;
yAxisUnit?: string; yAxisUnit?: string;
onDragSelect: (start: number, end: number) => void;
isDependedDataLoaded?: boolean; isDependedDataLoaded?: boolean;
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler']; onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
parentChartRef: GraphManagerProps['lineChartRef'];
} }
export interface GraphManagerProps extends UplotProps { export interface GraphManagerProps extends UplotProps {

View File

@ -6,7 +6,7 @@ import { ToggleGraphProps } from 'components/Graph/types';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch'; import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
@ -33,23 +33,20 @@ import FullView from './FullView';
import { Modal } from './styles'; import { Modal } from './styles';
import { WidgetGraphComponentProps } from './types'; import { WidgetGraphComponentProps } from './types';
import { getLocalStorageGraphVisibilityState } from './utils'; import { getLocalStorageGraphVisibilityState } from './utils';
// import { getLocalStorageGraphVisibilityState } from './utils';
function WidgetGraphComponent({ function WidgetGraphComponent({
widget, widget,
queryResponse, queryResponse,
errorMessage, errorMessage,
name,
version, version,
threshold, threshold,
headerMenuList, headerMenuList,
isWarning, isWarning,
data, isFetchingResponse,
options, setRequestData,
graphVisibiltyState,
onClickHandler, onClickHandler,
onDragSelect, onDragSelect,
setGraphVisibility,
isFetchingResponse,
}: WidgetGraphComponentProps): JSX.Element { }: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
@ -61,12 +58,15 @@ function WidgetGraphComponent({
const isFullViewOpen = params.get(QueryParams.expandedWidgetId) === widget.id; const isFullViewOpen = params.get(QueryParams.expandedWidgetId) === widget.id;
const lineChartRef = useRef<ToggleGraphProps>(); const lineChartRef = useRef<ToggleGraphProps>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
);
const graphRef = useRef<HTMLDivElement>(null); const graphRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (!lineChartRef.current) return; if (!lineChartRef.current) return;
graphVisibiltyState.forEach((state, index) => { graphVisibility.forEach((state, index) => {
lineChartRef.current?.toggleGraph(index, state); lineChartRef.current?.toggleGraph(index, state);
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -210,7 +210,7 @@ function WidgetGraphComponent({
graphVisibilityStates: localStoredVisibilityState, graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({ } = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data.payload.data.result, apiResponse: queryResponse.data.payload.data.result,
name, name: widget.id,
}); });
setGraphVisibility(localStoredVisibilityState); setGraphVisibility(localStoredVisibilityState);
} }
@ -252,7 +252,7 @@ function WidgetGraphComponent({
onBlur={(): void => { onBlur={(): void => {
setHovered(false); setHovered(false);
}} }}
id={name} id={widget.id}
> >
<Modal <Modal
destroyOnClose destroyOnClose
@ -278,14 +278,12 @@ function WidgetGraphComponent({
className="widget-full-view" className="widget-full-view"
> >
<FullView <FullView
name={`${name}expanded`} name={`${widget.id}expanded`}
version={version} version={version}
originalName={name} originalName={widget.id}
widget={widget} widget={widget}
yAxisUnit={widget.yAxisUnit} yAxisUnit={widget.yAxisUnit}
onToggleModelHandler={onToggleModelHandler} onToggleModelHandler={onToggleModelHandler}
parentChartRef={lineChartRef}
onDragSelect={onDragSelect}
/> />
</Modal> </Modal>
@ -305,26 +303,22 @@ function WidgetGraphComponent({
isFetchingResponse={isFetchingResponse} isFetchingResponse={isFetchingResponse}
/> />
</div> </div>
{queryResponse.isLoading && <Skeleton />} {queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && (
<Skeleton />
)}
{(queryResponse.isSuccess || widget.panelTypes === PANEL_TYPES.LIST) && ( {(queryResponse.isSuccess || widget.panelTypes === PANEL_TYPES.LIST) && (
<div <div
className={cx('widget-graph-container', widget.panelTypes)} className={cx('widget-graph-container', widget.panelTypes)}
ref={graphRef} ref={graphRef}
> >
<GridPanelSwitch <PanelWrapper
panelType={widget.panelTypes} widget={widget}
data={data} queryResponse={queryResponse}
name={name} setRequestData={setRequestData}
ref={lineChartRef} setGraphVisibility={setGraphVisibility}
options={options} graphVisibility={graphVisibility}
yAxisUnit={widget.yAxisUnit}
onClickHandler={onClickHandler} onClickHandler={onClickHandler}
panelData={queryResponse.data?.payload?.data.newResult.data.result || []} onDragSelect={onDragSelect}
query={widget.query}
thresholds={widget.thresholds}
selectedLogFields={widget.selectedLogFields}
dataSource={widget.query.builder?.queryData[0]?.dataSource}
selectedTracesFields={widget.selectedTracesFields}
/> />
</div> </div>
)} )}

View File

@ -1,88 +1,57 @@
import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval'; import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { useIntersectionObserver } from 'hooks/useIntersectionObserver'; import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import GetMinMax from 'lib/getMinMax'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import getTimeString from 'lib/getTimeString'; import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import isEmpty from 'lodash-es/isEmpty'; import isEmpty from 'lodash-es/isEmpty';
import _noop from 'lodash-es/noop';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { memo, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions'; import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType } from 'utils/getGraphType'; import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData'; import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import EmptyWidget from '../EmptyWidget'; import EmptyWidget from '../EmptyWidget';
import { MenuItemKeys } from '../WidgetHeader/contants'; import { MenuItemKeys } from '../WidgetHeader/contants';
import { GridCardGraphProps } from './types'; import { GridCardGraphProps } from './types';
import { getLocalStorageGraphVisibilityState } from './utils';
import WidgetGraphComponent from './WidgetGraphComponent'; import WidgetGraphComponent from './WidgetGraphComponent';
function GridCardGraph({ function GridCardGraph({
widget, widget,
name,
onClickHandler = _noop,
headerMenuList = [MenuItemKeys.View], headerMenuList = [MenuItemKeys.View],
isQueryEnabled, isQueryEnabled,
threshold, threshold,
variables, variables,
fillSpans = false,
version, version,
onClickHandler,
onDragSelect,
}: GridCardGraphProps): JSX.Element { }: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>(); const [errorMessage, setErrorMessage] = useState<string>();
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard(); const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const urlQuery = useUrlQuery();
const location = useLocation();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState, AppState,
GlobalReducer GlobalReducer
>((state) => state.globalTime); >((state) => state.globalTime);
const onDragSelect = useCallback(
(start: number, end: number): void => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
const { maxTime, minTime } = GetMinMax('custom', [
startTimestamp,
endTimestamp,
]);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
);
const handleBackNavigation = (): void => { const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime); const startTime = searchParams.get(QueryParams.startTime);
const endTime = searchParams.get(QueryParams.endTime); const endTime = searchParams.get(QueryParams.endTime);
const relativeTime = searchParams.get(
QueryParams.relativeTime,
) as CustomTimeType;
if (startTime && endTime && startTime !== endTime) { if (relativeTime) {
dispatch(UpdateTimeInterval(relativeTime));
} else if (startTime && endTime && startTime !== endTime) {
dispatch( dispatch(
UpdateTimeInterval('custom', [ UpdateTimeInterval('custom', [
parseInt(getTimeString(startTime), 10), parseInt(getTimeString(startTime), 10),
@ -121,19 +90,39 @@ function GridCardGraph({
const isEmptyWidget = const isEmptyWidget =
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget); widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
const queryEnabledCondition = const queryEnabledCondition = isVisible && !isEmptyWidget && isQueryEnabled;
isVisible &&
!isEmptyWidget && const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
isQueryEnabled && if (widget.panelTypes !== PANEL_TYPES.LIST) {
widget.panelTypes !== PANEL_TYPES.LIST; return {
selectedTime: widget?.timePreferance,
graphType: getGraphType(widget.panelTypes),
query: updatedQuery,
globalSelectedInterval,
variables: getDashboardVariables(variables),
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval,
tableParams: {
pagination: {
offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0,
},
},
};
});
const queryResponse = useGetQueryRange( const queryResponse = useGetQueryRange(
{ {
selectedTime: widget?.timePreferance, ...requestData,
graphType: getGraphType(widget.panelTypes),
query: updatedQuery,
globalSelectedInterval,
variables: getDashboardVariables(variables), variables: getDashboardVariables(variables),
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval,
}, },
version || DEFAULT_ENTITY_VERSION, version || DEFAULT_ENTITY_VERSION,
{ {
@ -145,7 +134,18 @@ function GridCardGraph({
widget?.query, widget?.query,
widget?.panelTypes, widget?.panelTypes,
widget.timePreferance, widget.timePreferance,
requestData,
], ],
retry(failureCount, error): boolean {
if (
String(error).includes('status: error') &&
String(error).includes('i/o timeout')
) {
return false;
}
return failureCount < 2;
},
keepPreviousData: true, keepPreviousData: true,
enabled: queryEnabledCondition, enabled: queryEnabledCondition,
refetchOnMount: false, refetchOnMount: false,
@ -157,15 +157,6 @@ function GridCardGraph({
const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET; const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET;
const containerDimensions = useResizeObserver(graphRef);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) { if (queryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData( const sortedSeriesData = getSortedSeriesData(
queryResponse.data?.payload.data.result, queryResponse.data?.payload.data.result,
@ -173,89 +164,29 @@ function GridCardGraph({
queryResponse.data.payload.data.result = sortedSeriesData; queryResponse.data.payload.data.result = sortedSeriesData;
} }
const chartData = getUPlotChartData(queryResponse?.data?.payload, fillSpans);
const isDarkMode = useIsDarkMode();
const menuList = const menuList =
widget.panelTypes === PANEL_TYPES.TABLE || widget.panelTypes === PANEL_TYPES.TABLE ||
widget.panelTypes === PANEL_TYPES.LIST widget.panelTypes === PANEL_TYPES.LIST
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts) ? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
: headerMenuList; : headerMenuList;
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
);
useEffect(() => {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data?.payload.data.result || [],
name,
});
setGraphVisibility(localStoredVisibilityState);
}, [name, queryResponse.data?.payload.data.result]);
const options = useMemo(
() =>
getUPlotChartOptions({
id: widget?.id,
apiResponse: queryResponse.data?.payload,
dimensions: containerDimensions,
isDarkMode,
onDragSelect,
yAxisUnit: widget?.yAxisUnit,
onClickHandler,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
softMax: widget.softMax === undefined ? null : widget.softMax,
softMin: widget.softMin === undefined ? null : widget.softMin,
graphsVisibilityStates: graphVisibility,
setGraphsVisibilityStates: setGraphVisibility,
panelType: widget.panelTypes,
}),
[
widget?.id,
widget?.yAxisUnit,
widget.thresholds,
widget.softMax,
widget.softMin,
queryResponse.data?.payload,
containerDimensions,
isDarkMode,
onDragSelect,
onClickHandler,
minTimeScale,
maxTimeScale,
graphVisibility,
setGraphVisibility,
widget.panelTypes,
],
);
return ( return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}> <div style={{ height: '100%', width: '100%' }} ref={graphRef}>
{isEmptyLayout ? ( {isEmptyLayout ? (
<EmptyWidget /> <EmptyWidget />
) : ( ) : (
<WidgetGraphComponent <WidgetGraphComponent
data={chartData}
options={options}
widget={widget} widget={widget}
queryResponse={queryResponse} queryResponse={queryResponse}
errorMessage={errorMessage} errorMessage={errorMessage}
isWarning={false} isWarning={false}
name={name}
version={version} version={version}
onDragSelect={onDragSelect}
threshold={threshold} threshold={threshold}
headerMenuList={menuList} headerMenuList={menuList}
onClickHandler={onClickHandler}
graphVisibiltyState={graphVisibility}
setGraphVisibility={setGraphVisibility}
isFetchingResponse={queryResponse.isFetching} isFetchingResponse={queryResponse.isFetching}
setRequestData={setRequestData}
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}
/> />
)} )}
</div> </div>

View File

@ -1,9 +1,9 @@
import { ToggleGraphProps } from 'components/Graph/types'; import { ToggleGraphProps } from 'components/Graph/types';
import { UplotProps } from 'components/Uplot/Uplot'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin'; import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react'; import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query'; import { UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { SuccessResponse } from 'types/api';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot'; import uPlot from 'uplot';
@ -16,35 +16,32 @@ export interface GraphVisibilityLegendEntryProps {
legendEntry: LegendEntryProps[]; legendEntry: LegendEntryProps[];
} }
export interface WidgetGraphComponentProps extends UplotProps { export interface WidgetGraphComponentProps {
widget: Widgets; widget: Widgets;
queryResponse: UseQueryResult< queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps> | ErrorResponse SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>; >;
errorMessage: string | undefined; errorMessage: string | undefined;
name: string;
version?: string; version?: string;
onDragSelect: (start: number, end: number) => void;
onClickHandler?: OnClickPluginOpts['onClick'];
threshold?: ReactNode; threshold?: ReactNode;
headerMenuList: MenuItemKeys[]; headerMenuList: MenuItemKeys[];
isWarning: boolean; isWarning: boolean;
graphVisibiltyState: boolean[];
setGraphVisibility: Dispatch<SetStateAction<boolean[]>>;
isFetchingResponse: boolean; isFetchingResponse: boolean;
setRequestData?: Dispatch<SetStateAction<GetQueryResultsProps>>;
onClickHandler?: OnClickPluginOpts['onClick'];
onDragSelect: (start: number, end: number) => void;
} }
export interface GridCardGraphProps { export interface GridCardGraphProps {
widget: Widgets; widget: Widgets;
name: string;
onDragSelect?: (start: number, end: number) => void;
onClickHandler?: OnClickPluginOpts['onClick'];
threshold?: ReactNode; threshold?: ReactNode;
headerMenuList?: WidgetGraphComponentProps['headerMenuList']; headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
onClickHandler?: OnClickPluginOpts['onClick'];
isQueryEnabled: boolean; isQueryEnabled: boolean;
variables?: Dashboard['data']['variables']; variables?: Dashboard['data']['variables'];
fillSpans?: boolean;
version?: string; version?: string;
onDragSelect: (start: number, end: number) => void;
} }
export interface GetGraphVisibilityStateOnLegendClickProps { export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@ -3,20 +3,25 @@ import './GridCardLayout.styles.scss';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme'; import { themeColors } from 'constants/theme';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import isEqual from 'lodash-es/isEqual'; import isEqual from 'lodash-es/isEqual';
import { FullscreenIcon } from 'lucide-react'; import { FullscreenIcon } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { FullScreen, useFullScreenHandle } from 'react-full-screen'; import { FullScreen, useFullScreenHandle } from 'react-full-screen';
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
@ -45,6 +50,8 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
} = useDashboard(); } = useDashboard();
const { data } = selectedDashboard || {}; const { data } = selectedDashboard || {};
const handle = useFullScreenHandle(); const handle = useFullScreenHandle();
const { pathname } = useLocation();
const dispatch = useDispatch();
const { widgets, variables } = data || {}; const { widgets, variables } = data || {};
@ -61,6 +68,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
const updateDashboardMutation = useUpdateDashboard(); const updateDashboardMutation = useUpdateDashboard();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const urlQuery = useUrlQuery();
let permissions: ComponentTypes[] = ['save_layout', 'add_panel']; let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
@ -126,6 +134,23 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
} }
}; };
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, pathname, urlQuery],
);
useEffect(() => { useEffect(() => {
if ( if (
dashboardLayout && dashboardLayout &&
@ -200,11 +225,10 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
> >
<GridCard <GridCard
widget={currentWidget || ({ id, query: {} } as Widgets)} widget={currentWidget || ({ id, query: {} } as Widgets)}
name={currentWidget?.id || ''}
headerMenuList={widgetActions} headerMenuList={widgetActions}
variables={variables} variables={variables}
fillSpans={currentWidget?.fillSpans}
version={selectedDashboard?.data?.version} version={selectedDashboard?.data?.version}
onDragSelect={onDragSelect}
/> />
</Card> </Card>
</CardContainer> </CardContainer>

View File

@ -1,10 +1,8 @@
import { ToggleGraphProps } from 'components/Graph/types'; import { ToggleGraphProps } from 'components/Graph/types';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { getComponentForPanelType } from 'constants/panelTypes'; import { getComponentForPanelType } from 'constants/panelTypes';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config'; import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
import { FC, forwardRef, memo, useMemo } from 'react'; import { FC, forwardRef, memo, useMemo } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { GridPanelSwitchProps, PropsTypePropsMap } from './types'; import { GridPanelSwitchProps, PropsTypePropsMap } from './types';
@ -21,10 +19,7 @@ const GridPanelSwitch = forwardRef<
query, query,
options, options,
thresholds, thresholds,
selectedLogFields,
selectedTracesFields,
dataSource, dataSource,
selectedTime,
}, },
ref, ref,
): JSX.Element | null => { ): JSX.Element | null => {
@ -46,20 +41,7 @@ const GridPanelSwitch = forwardRef<
query, query,
thresholds, thresholds,
}, },
[PANEL_TYPES.LIST]: [PANEL_TYPES.LIST]: null,
dataSource === DataSource.LOGS
? {
selectedLogsFields: selectedLogFields || [],
query,
version: DEFAULT_ENTITY_VERSION, // As we don't support for Metrics, defaulting to v3
selectedTime,
}
: {
selectedTracesFields: selectedTracesFields || [],
query,
version: DEFAULT_ENTITY_VERSION, // As we don't support for Metrics, defaulting to v3
selectedTime,
},
[PANEL_TYPES.TRACE]: null, [PANEL_TYPES.TRACE]: null,
[PANEL_TYPES.BAR]: { [PANEL_TYPES.BAR]: {
data, data,
@ -70,19 +52,7 @@ const GridPanelSwitch = forwardRef<
}; };
return result; return result;
}, [ }, [data, options, ref, yAxisUnit, thresholds, panelData, query]);
data,
options,
ref,
yAxisUnit,
thresholds,
panelData,
query,
dataSource,
selectedLogFields,
selectedTime,
selectedTracesFields,
]);
const Component = getComponentForPanelType(panelType, dataSource) as FC< const Component = getComponentForPanelType(panelType, dataSource) as FC<
PropsTypePropsMap[typeof panelType] PropsTypePropsMap[typeof panelType]

View File

@ -2,9 +2,7 @@ import { StaticLineProps, ToggleGraphProps } from 'components/Graph/types';
import { UplotProps } from 'components/Uplot/Uplot'; import { UplotProps } from 'components/Uplot/Uplot';
import { GridTableComponentProps } from 'container/GridTableComponent/types'; import { GridTableComponentProps } from 'container/GridTableComponent/types';
import { GridValueComponentProps } from 'container/GridValueComponent/types'; import { GridValueComponentProps } from 'container/GridValueComponent/types';
import { LogsPanelComponentProps } from 'container/LogsPanelTable/LogsPanelComponent';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import { TracesTableComponentProps } from 'container/TracesTableComponent/TracesTableComponent';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin'; import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { ForwardedRef } from 'react'; import { ForwardedRef } from 'react';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
@ -40,7 +38,7 @@ export type PropsTypePropsMap = {
[PANEL_TYPES.VALUE]: GridValueComponentProps; [PANEL_TYPES.VALUE]: GridValueComponentProps;
[PANEL_TYPES.TABLE]: GridTableComponentProps; [PANEL_TYPES.TABLE]: GridTableComponentProps;
[PANEL_TYPES.TRACE]: null; [PANEL_TYPES.TRACE]: null;
[PANEL_TYPES.LIST]: LogsPanelComponentProps | TracesTableComponentProps; [PANEL_TYPES.LIST]: null;
[PANEL_TYPES.BAR]: UplotProps & { [PANEL_TYPES.BAR]: UplotProps & {
ref: ForwardedRef<ToggleGraphProps | undefined>; ref: ForwardedRef<ToggleGraphProps | undefined>;
}; };

View File

@ -71,6 +71,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
key={log.id} key={log.id}
logData={log} logData={log}
selectedFields={selectedFields} selectedFields={selectedFields}
linesPerRow={options.maxLines}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog} onSetActiveLog={onSetActiveLog}
/> />

View File

@ -53,6 +53,19 @@
background: rgba(171, 189, 255, 0.04); background: rgba(171, 189, 255, 0.04);
padding: 8px; padding: 8px;
.ant-collapse-extra {
display: flex;
align-items: center;
.action-btn {
display: flex;
.ant-btn {
background: rgba(113, 144, 249, 0.08);
}
}
}
} }
.ant-collapse-content { .ant-collapse-content {

View File

@ -5,12 +5,13 @@
.ant-table-row:hover { .ant-table-row:hover {
.ant-table-cell { .ant-table-cell {
.value-field { .value-field {
display: flex;
justify-content: space-between;
align-items: center;
.action-btn { .action-btn {
display: flex; display: flex;
gap: 4px; position: absolute;
top: 50%;
right: 16px;
transform: translateY(-50%);
gap: 8px;
} }
} }
} }
@ -28,6 +29,30 @@
} }
} }
.attribute-pin {
cursor: pointer;
padding: 0;
vertical-align: middle;
text-align: center;
.log-attribute-pin {
padding: 8px;
display: flex;
justify-content: center;
align-items: center;
.pin-attribute-icon {
border: none;
&.pinned svg {
fill: var(--bg-robin-500);
}
}
}
}
.value-field-container { .value-field-container {
background: rgba(22, 25, 34, 0.4); background: rgba(22, 25, 34, 0.4);
@ -70,6 +95,10 @@
.value-field-container { .value-field-container {
background: var(--bg-vanilla-300); background: var(--bg-vanilla-300);
&.attribute-pin {
background: var(--bg-vanilla-100);
}
.action-btn { .action-btn {
.filter-btn { .filter-btn {
background: var(--bg-vanilla-300); background: var(--bg-vanilla-300);

View File

@ -1,22 +1,29 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './TableView.styles.scss'; import './TableView.styles.scss';
import { LinkOutlined } from '@ant-design/icons'; import { LinkOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Button, Space, Spin, Tooltip, Tree, Typography } from 'antd'; import { Button, Space, Spin, Tooltip, Tree, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import cx from 'classnames';
import AddToQueryHOC, { import AddToQueryHOC, {
AddToQueryHOCProps, AddToQueryHOCProps,
} from 'components/Logs/AddToQueryHOC'; } from 'components/Logs/AddToQueryHOC';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC'; import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import { LOCALSTORAGE } from 'constants/localStorage';
import { OPERATORS } from 'constants/queryBuilder'; import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history'; import history from 'lib/history';
import { fieldSearchFilter } from 'lib/logs/fieldSearch'; import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes'; import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
import { isEmpty } from 'lodash-es'; import { isEmpty } from 'lodash-es';
import { ArrowDownToDot, ArrowUpFromDot } from 'lucide-react'; import { ArrowDownToDot, ArrowUpFromDot, Pin } from 'lucide-react';
import { useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { generatePath } from 'react-router-dom'; import { generatePath } from 'react-router-dom';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
@ -57,6 +64,28 @@ function TableView({
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch = useDispatch<Dispatch<AppActions>>();
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false); const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
const [isfilterOutLoading, setIsFilterOutLoading] = useState<boolean>(false); const [isfilterOutLoading, setIsFilterOutLoading] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
const [pinnedAttributes, setPinnedAttributes] = useState<
Record<string, boolean>
>({});
useEffect(() => {
const pinnedAttributesFromLocalStorage = getLocalStorageApi(
LOCALSTORAGE.PINNED_ATTRIBUTES,
);
if (pinnedAttributesFromLocalStorage) {
try {
const parsedPinnedAttributes = JSON.parse(pinnedAttributesFromLocalStorage);
setPinnedAttributes(parsedPinnedAttributes);
} catch (e) {
console.error('Error parsing pinned attributes from local storgage');
}
} else {
setPinnedAttributes({});
}
}, []);
const flattenLogData: Record<string, string> | null = useMemo( const flattenLogData: Record<string, string> | null = useMemo(
() => (logData ? flattenObject(logData) : null), () => (logData ? flattenObject(logData) : null),
@ -74,6 +103,19 @@ function TableView({
} }
}; };
const togglePinAttribute = (record: DataType): void => {
if (record) {
const newPinnedAttributes = { ...pinnedAttributes };
newPinnedAttributes[record.key] = !newPinnedAttributes[record.key];
setPinnedAttributes(newPinnedAttributes);
setLocalStorageApi(
LOCALSTORAGE.PINNED_ATTRIBUTES,
JSON.stringify(newPinnedAttributes),
);
}
};
const onClickHandler = ( const onClickHandler = (
operator: string, operator: string,
fieldKey: string, fieldKey: string,
@ -138,6 +180,37 @@ function TableView({
} }
const columns: ColumnsType<DataType> = [ const columns: ColumnsType<DataType> = [
{
title: '',
dataIndex: 'pin',
key: 'pin',
width: 5,
align: 'left',
className: 'attribute-pin value-field-container',
render: (fieldData: Record<string, string>, record): JSX.Element => {
let pinColor = isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500;
if (pinnedAttributes[record?.key]) {
pinColor = Color.BG_ROBIN_500;
}
return (
<div className="log-attribute-pin value-field">
<div
className={cx(
'pin-attribute-icon',
pinnedAttributes[record?.key] ? 'pinned' : '',
)}
onClick={(): void => {
togglePinAttribute(record);
}}
>
<Pin size={14} color={pinColor} />
</div>
</div>
);
},
},
{ {
title: 'Field', title: 'Field',
dataIndex: 'field', dataIndex: 'field',
@ -264,12 +337,34 @@ function TableView({
}, },
}, },
]; ];
function sortPinnedAttributes(
data: Record<string, string>[],
sortingObj: Record<string, boolean>,
): Record<string, string>[] {
const sortingKeys = Object.keys(sortingObj);
return data.sort((a, b) => {
const aKey = a.key;
const bKey = b.key;
const aSortIndex = sortingKeys.indexOf(aKey);
const bSortIndex = sortingKeys.indexOf(bKey);
if (sortingObj[aKey] && !sortingObj[bKey]) {
return -1;
}
if (!sortingObj[aKey] && sortingObj[bKey]) {
return 1;
}
return aSortIndex - bSortIndex;
});
}
const sortedAttributes = sortPinnedAttributes(dataSource, pinnedAttributes);
return ( return (
<ResizeTable <ResizeTable
columns={columns} columns={columns}
tableLayout="fixed" tableLayout="fixed"
dataSource={dataSource} dataSource={sortedAttributes}
pagination={false} pagination={false}
showHeader={false} showHeader={false}
className="attribute-table-container" className="attribute-table-container"

View File

@ -2,6 +2,7 @@ import Graph from 'components/Graph';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { themeColors } from 'constants/theme'; import { themeColors } from 'constants/theme';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import getChartData, { GetChartDataProps } from 'lib/getChartData'; import getChartData, { GetChartDataProps } from 'lib/getChartData';
import GetMinMax from 'lib/getMinMax'; import GetMinMax from 'lib/getMinMax';
@ -65,8 +66,13 @@ function LogsExplorerChart({
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime); const startTime = searchParams.get(QueryParams.startTime);
const endTime = searchParams.get(QueryParams.endTime); const endTime = searchParams.get(QueryParams.endTime);
const relativeTime = searchParams.get(
QueryParams.relativeTime,
) as CustomTimeType;
if (startTime && endTime && startTime !== endTime) { if (relativeTime) {
dispatch(UpdateTimeInterval(relativeTime));
} else if (startTime && endTime && startTime !== endTime) {
dispatch( dispatch(
UpdateTimeInterval('custom', [ UpdateTimeInterval('custom', [
parseInt(getTimeString(startTime), 10), parseInt(getTimeString(startTime), 10),

View File

@ -90,6 +90,7 @@ function LogsExplorerList({
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog} onSetActiveLog={onSetActiveLog}
activeLog={activeLog} activeLog={activeLog}
linesPerRow={options.maxLines}
/> />
); );
}, },

View File

@ -14,13 +14,15 @@ import {
PANEL_TYPES, PANEL_TYPES,
} from 'constants/queryBuilder'; } from 'constants/queryBuilder';
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config'; import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
import ExplorerOptions from 'container/ExplorerOptions/ExplorerOptions'; import Download from 'container/DownloadV2/DownloadV2';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import GoToTop from 'container/GoToTop'; import GoToTop from 'container/GoToTop';
import LogsExplorerChart from 'container/LogsExplorerChart'; import LogsExplorerChart from 'container/LogsExplorerChart';
import LogsExplorerList from 'container/LogsExplorerList'; import LogsExplorerList from 'container/LogsExplorerList';
import LogsExplorerTable from 'container/LogsExplorerTable'; import LogsExplorerTable from 'container/LogsExplorerTable';
import { useOptionsMenu } from 'container/OptionsMenu'; import { useOptionsMenu } from 'container/OptionsMenu';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView'; import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import dayjs from 'dayjs';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils'; import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
import { LogTimeRange } from 'hooks/logs/types'; import { LogTimeRange } from 'hooks/logs/types';
@ -33,8 +35,9 @@ import useClickOutside from 'hooks/useClickOutside';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData'; import useUrlQueryData from 'hooks/useUrlQueryData';
import { FlatLogData } from 'lib/logs/flatLogData';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData'; import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
import { defaultTo, isEmpty } from 'lodash-es'; import { defaultTo, isEmpty, omit } from 'lodash-es';
import { Sliders } from 'lucide-react'; import { Sliders } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -523,6 +526,23 @@ function LogsExplorerViews({
}, },
}); });
const flattenLogData = useMemo(
() =>
logs.map((log) => {
const timestamp =
typeof log.timestamp === 'string'
? dayjs(log.timestamp).format()
: dayjs(log.timestamp / 1e6).format();
return FlatLogData({
timestamp,
body: log.body,
...omit(log, 'timestamp', 'body'),
});
}),
[logs],
);
return ( return (
<div className="logs-explorer-views-container"> <div className="logs-explorer-views-container">
{showHistogram && ( {showHistogram && (
@ -578,6 +598,11 @@ function LogsExplorerViews({
<div className="logs-actions-container"> <div className="logs-actions-container">
{selectedPanelType === PANEL_TYPES.LIST && ( {selectedPanelType === PANEL_TYPES.LIST && (
<div className="tab-options"> <div className="tab-options">
<Download
data={flattenLogData}
isLoading={isFetching}
fileName="log_data"
/>
<div className="format-options-container" ref={menuRef}> <div className="format-options-container" ref={menuRef}>
<Button <Button
className="periscope-btn" className="periscope-btn"
@ -634,7 +659,7 @@ function LogsExplorerViews({
<GoToTop /> <GoToTop />
<ExplorerOptions <ExplorerOptionWrapper
disabled={!stagedQuery} disabled={!stagedQuery}
query={exportDefaultQuery} query={exportDefaultQuery}
isLoading={isUpdateDashboardLoading} isLoading={isUpdateDashboardLoading}

View File

@ -4,82 +4,53 @@ import { Table } from 'antd';
import LogDetail from 'components/LogDetail'; import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants'; import { VIEW_TYPES } from 'components/LogDetail/constants';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import Controls from 'container/Controls'; import Controls from 'container/Controls';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs'; import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { tableStyles } from 'container/TracesExplorer/ListView/styles'; import { tableStyles } from 'container/TracesExplorer/ListView/styles';
import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { Pagination } from 'hooks/queryPagination'; import { Pagination } from 'hooks/queryPagination';
import { useLogsData } from 'hooks/useLogsData'; import { useLogsData } from 'hooks/useLogsData';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { import {
Dispatch,
HTMLAttributes, HTMLAttributes,
SetStateAction,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useState, useState,
} from 'react'; } from 'react';
import { useSelector } from 'react-redux'; import { UseQueryResult } from 'react-query';
import { AppState } from 'store/reducers'; import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { getLogPanelColumnsList } from './utils'; import { getLogPanelColumnsList, getNextOrPreviousItems } from './utils';
function LogsPanelComponent({ function LogsPanelComponent({
selectedLogsFields, widget,
query, setRequestData,
selectedTime, queryResponse,
}: LogsPanelComponentProps): JSX.Element { }: LogsPanelComponentProps): JSX.Element {
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [pagination, setPagination] = useState<Pagination>({ const [pagination, setPagination] = useState<Pagination>({
offset: 0, offset: 0,
limit: query.builder.queryData[0].limit || 0, limit: widget.query.builder.queryData[0].limit || 0,
});
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
const updatedQuery = { ...query };
updatedQuery.builder.queryData[0].pageSize = 10;
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
tableParams: {
pagination,
},
};
}); });
useEffect(() => { useEffect(() => {
setRequestData({ setRequestData((prev) => ({
...requestData, ...prev,
globalSelectedInterval: globalSelectedTime,
tableParams: { tableParams: {
pagination, pagination,
}, },
}); }));
// eslint-disable-next-line react-hooks/exhaustive-deps }, [pagination, setRequestData]);
}, [pagination]);
const [pageSize, setPageSize] = useState<number>(10); const [pageSize, setPageSize] = useState<number>(10);
const { selectedDashboard } = useDashboard();
const handleChangePageSize = (value: number): void => { const handleChangePageSize = (value: number): void => {
setPagination({ setPagination({
@ -88,53 +59,35 @@ function LogsPanelComponent({
offset: value, offset: value,
}); });
setPageSize(value); setPageSize(value);
const newQueryData = { ...requestData.query }; setRequestData((prev) => {
newQueryData.builder.queryData[0].pageSize = value; const newQueryData = { ...prev.query };
const newRequestData = { newQueryData.builder.queryData[0].pageSize = value;
...requestData, return {
query: newQueryData, ...prev,
tableParams: { query: newQueryData,
pagination, tableParams: {
}, pagination: {
}; limit: 0,
setRequestData(newRequestData); offset: value,
},
},
};
});
}; };
const { data, isFetching, isError } = useGetQueryRange( const columns = getLogPanelColumnsList(widget.selectedLogFields);
{
...requestData,
globalSelectedInterval: globalSelectedTime,
selectedTime: selectedTime?.enum || 'GLOBAL_TIME',
variables: getDashboardVariables(selectedDashboard?.data.variables),
},
DEFAULT_ENTITY_VERSION,
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
globalSelectedTime,
maxTime,
minTime,
requestData,
pagination,
selectedDashboard?.data.variables,
],
enabled: !!requestData.query && !!selectedLogsFields?.length,
},
);
const columns = getLogPanelColumnsList(selectedLogsFields);
const dataLength = const dataLength =
data?.payload?.data?.newResult?.data?.result[0]?.list?.length; queryResponse.data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
const totalCount = useMemo(() => dataLength || 0, [dataLength]); const totalCount = useMemo(() => dataLength || 0, [dataLength]);
const [firstLog, setFirstLog] = useState<ILog>(); const [firstLog, setFirstLog] = useState<ILog>();
const [lastLog, setLastLog] = useState<ILog>(); const [lastLog, setLastLog] = useState<ILog>();
const { logs } = useLogsData({ const { logs } = useLogsData({
result: data?.payload.data.newResult.data.result, result: queryResponse.data?.payload.data.newResult.data.result,
panelType: PANEL_TYPES.LIST, panelType: PANEL_TYPES.LIST,
stagedQuery: query, stagedQuery: widget.query,
}); });
useEffect(() => { useEffect(() => {
@ -167,92 +120,86 @@ function LogsPanelComponent({
); );
const isOrderByTimeStamp = const isOrderByTimeStamp =
query.builder.queryData[0].orderBy.length > 0 && widget.query.builder.queryData[0].orderBy.length > 0 &&
query.builder.queryData[0].orderBy[0].columnName === 'timestamp'; widget.query.builder.queryData[0].orderBy[0].columnName === 'timestamp';
const handlePreviousPagination = (): void => { const handlePreviousPagination = (): void => {
if (isOrderByTimeStamp) { if (isOrderByTimeStamp) {
setRequestData({ setRequestData((prev) => ({
...requestData, ...prev,
query: { query: {
...requestData.query, ...prev.query,
builder: { builder: {
...requestData.query.builder, ...prev.query.builder,
queryData: [ queryData: [
{ {
...requestData.query.builder.queryData[0], ...prev.query.builder.queryData[0],
filters: { filters: {
...requestData.query.builder.queryData[0].filters, ...prev.query.builder.queryData[0].filters,
items: [ items: [
{ ...getNextOrPreviousItems(
id: uuid(), prev.query.builder.queryData[0].filters.items,
key: { 'PREV',
key: 'id', firstLog,
type: '', ),
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['>'],
value: firstLog?.id || '',
},
], ],
}, },
limit: 0,
offset: 0,
}, },
], ],
}, },
}, },
}); }));
return; }
if (!isOrderByTimeStamp) {
setPagination({
...pagination,
limit: 0,
offset: pagination.offset - pageSize,
});
} }
setPagination({
...pagination,
limit: 0,
offset: pagination.offset - pageSize,
});
}; };
const handleNextPagination = (): void => { const handleNextPagination = (): void => {
if (isOrderByTimeStamp) { if (isOrderByTimeStamp) {
setRequestData({ setRequestData((prev) => ({
...requestData, ...prev,
query: { query: {
...requestData.query, ...prev.query,
builder: { builder: {
...requestData.query.builder, ...prev.query.builder,
queryData: [ queryData: [
{ {
...requestData.query.builder.queryData[0], ...prev.query.builder.queryData[0],
filters: { filters: {
...requestData.query.builder.queryData[0].filters, ...prev.query.builder.queryData[0].filters,
items: [ items: [
{ ...getNextOrPreviousItems(
id: uuid(), prev.query.builder.queryData[0].filters.items,
key: { 'NEXT',
key: 'id', lastLog,
type: '', ),
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['<'],
value: lastLog?.id || '',
},
], ],
}, },
limit: 0,
offset: 0,
}, },
], ],
}, },
}, },
}); }));
return; }
if (!isOrderByTimeStamp) {
setPagination({
...pagination,
limit: 0,
offset: pagination.offset + pageSize,
});
} }
setPagination({
...pagination,
limit: 0,
offset: pagination.offset + pageSize,
});
}; };
if (isError) { if (queryResponse.isError) {
return <div>{SOMETHING_WENT_WRONG}</div>; return <div>{SOMETHING_WENT_WRONG}</div>;
} }
@ -265,19 +212,19 @@ function LogsPanelComponent({
tableLayout="fixed" tableLayout="fixed"
scroll={{ x: `calc(50vw - 10px)` }} scroll={{ x: `calc(50vw - 10px)` }}
sticky sticky
loading={isFetching} loading={queryResponse.isFetching}
style={tableStyles} style={tableStyles}
dataSource={flattenLogData} dataSource={flattenLogData}
columns={columns} columns={columns}
onRow={handleRow} onRow={handleRow}
/> />
</div> </div>
{!query.builder.queryData[0].limit && ( {!widget.query.builder.queryData[0].limit && (
<div className="controller"> <div className="controller">
<Controls <Controls
totalCount={totalCount} totalCount={totalCount}
perPageOptions={PER_PAGE_OPTIONS} perPageOptions={PER_PAGE_OPTIONS}
isLoading={isFetching} isLoading={queryResponse.isFetching}
offset={pagination.offset} offset={pagination.offset}
countPerPage={pageSize} countPerPage={pageSize}
handleNavigatePrevious={handlePreviousPagination} handleNavigatePrevious={handlePreviousPagination}
@ -301,13 +248,12 @@ function LogsPanelComponent({
} }
export type LogsPanelComponentProps = { export type LogsPanelComponentProps = {
selectedLogsFields: Widgets['selectedLogFields']; setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
query: Query; queryResponse: UseQueryResult<
selectedTime?: timePreferance; SuccessResponse<MetricRangePayloadProps, unknown>,
}; Error
>;
LogsPanelComponent.defaultProps = { widget: Widgets;
selectedTime: undefined,
}; };
export default LogsPanelComponent; export default LogsPanelComponent;

View File

@ -1,10 +1,15 @@
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import { Typography } from 'antd/lib'; import { Typography } from 'antd/lib';
import { OPERATORS } from 'constants/queryBuilder';
// import Typography from 'antd/es/typography/Typography'; // import Typography from 'antd/es/typography/Typography';
import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import { IField } from 'types/api/logs/fields'; import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
export const getLogPanelColumnsList = ( export const getLogPanelColumnsList = (
selectedLogFields: Widgets['selectedLogFields'], selectedLogFields: Widgets['selectedLogFields'],
@ -36,3 +41,49 @@ export const getLogPanelColumnsList = (
return [...initialColumns, ...columns]; return [...initialColumns, ...columns];
}; };
export const getNextOrPreviousItems = (
items: TagFilterItem[],
direction: 'NEXT' | 'PREV',
log?: ILog,
): TagFilterItem[] => {
const nextItem = {
id: uuid(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['<'],
value: log?.id || '',
};
const prevItem = {
id: uuid(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['>'],
value: log?.id || '',
};
let index = items.findIndex((item) => item.op === OPERATORS['<']);
if (index === -1) {
index = items.findIndex((item) => item.op === OPERATORS['>']);
}
if (index === -1) {
if (direction === 'NEXT') {
return [...items, nextItem];
}
return [...items, prevItem];
}
const newItems = [...items];
if (direction === 'NEXT') {
newItems[index] = nextItem;
} else {
newItems[index] = prevItem;
}
return newItems;
};

View File

@ -1,9 +1,12 @@
import { Time } from 'container/TopNav/DateTimeSelection/config'; import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config'; import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { GetMinMaxPayload } from 'lib/getMinMax'; import { GetMinMaxPayload } from 'lib/getMinMax';
export const getGlobalTime = ( export const getGlobalTime = (
selectedTime: Time | TimeV2, selectedTime: Time | TimeV2 | CustomTimeType,
globalTime: GetMinMaxPayload, globalTime: GetMinMaxPayload,
): GetMinMaxPayload | undefined => { ): GetMinMaxPayload | undefined => {
if (selectedTime === 'custom') { if (selectedTime === 'custom') {

View File

@ -74,6 +74,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
key={log.id} key={log.id}
logData={log} logData={log}
selectedFields={selected} selectedFields={selected}
linesPerRow={linesPerRow}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog} onSetActiveLog={onSetActiveLog}
/> />

View File

@ -8,6 +8,7 @@ export const getWidgetQueryBuilder = ({
title = '', title = '',
panelTypes, panelTypes,
yAxisUnit = '', yAxisUnit = '',
fillSpans = false,
id, id,
}: GetWidgetQueryBuilderProps): Widgets => ({ }: GetWidgetQueryBuilderProps): Widgets => ({
description: '', description: '',
@ -24,4 +25,5 @@ export const getWidgetQueryBuilder = ({
softMin: null, softMin: null,
selectedLogFields: [], selectedLogFields: [],
selectedTracesFields: [], selectedTracesFields: [],
fillSpans,
}); });

View File

@ -70,6 +70,7 @@ function DBCall(): JSX.Element {
panelTypes: PANEL_TYPES.TIME_SERIES, panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'reqps', yAxisUnit: 'reqps',
id: SERVICE_CHART_ID.dbCallsRPS, id: SERVICE_CHART_ID.dbCallsRPS,
fillSpans: false,
}), }),
[servicename, tagFilterItems], [servicename, tagFilterItems],
); );
@ -89,7 +90,8 @@ function DBCall(): JSX.Element {
title: GraphTitle.DATABASE_CALLS_AVG_DURATION, title: GraphTitle.DATABASE_CALLS_AVG_DURATION,
panelTypes: PANEL_TYPES.TIME_SERIES, panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ms', yAxisUnit: 'ms',
id: SERVICE_CHART_ID.dbCallsAvgDuration, id: GraphTitle.DATABASE_CALLS_AVG_DURATION,
fillSpans: true,
}), }),
[servicename, tagFilterItems], [servicename, tagFilterItems],
); );
@ -112,8 +114,6 @@ function DBCall(): JSX.Element {
<Card data-testid="database_call_rps"> <Card data-testid="database_call_rps">
<GraphContainer> <GraphContainer>
<Graph <Graph
fillSpans={false}
name="database_call_rps"
widget={databaseCallsRPSWidget} widget={databaseCallsRPSWidget}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => { onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onGraphClickHandler(setSelectedTimeStamp)( onGraphClickHandler(setSelectedTimeStamp)(
@ -147,8 +147,6 @@ function DBCall(): JSX.Element {
<Card data-testid="database_call_avg_duration"> <Card data-testid="database_call_avg_duration">
<GraphContainer> <GraphContainer>
<Graph <Graph
fillSpans
name="database_call_avg_duration"
widget={databaseCallsAverageDurationWidget} widget={databaseCallsAverageDurationWidget}
headerMenuList={MENU_ITEMS} headerMenuList={MENU_ITEMS}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => { onClickHandler={(xValue, yValue, mouseX, mouseY): void => {

View File

@ -18,7 +18,7 @@ import { useParams } from 'react-router-dom';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { GraphTitle, legend, MENU_ITEMS, SERVICE_CHART_ID } from '../constant'; import { GraphTitle, legend, MENU_ITEMS } from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory'; import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles'; import { Card, GraphContainer, Row } from '../styles';
import { Button } from './styles'; import { Button } from './styles';
@ -60,7 +60,7 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE, title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES, panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%', yAxisUnit: '%',
id: SERVICE_CHART_ID.externalCallErrorPercentage, id: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
}), }),
[servicename, tagFilterItems], [servicename, tagFilterItems],
); );
@ -86,7 +86,8 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_DURATION, title: GraphTitle.EXTERNAL_CALL_DURATION,
panelTypes: PANEL_TYPES.TIME_SERIES, panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ms', yAxisUnit: 'ms',
id: SERVICE_CHART_ID.externalCallDuration, id: GraphTitle.EXTERNAL_CALL_DURATION,
fillSpans: true,
}), }),
[servicename, tagFilterItems], [servicename, tagFilterItems],
); );
@ -108,7 +109,8 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS, title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
panelTypes: PANEL_TYPES.TIME_SERIES, panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'reqps', yAxisUnit: 'reqps',
id: SERVICE_CHART_ID.externalCallRPSByAddress, id: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
fillSpans: true,
}), }),
[servicename, tagFilterItems], [servicename, tagFilterItems],
); );
@ -130,7 +132,8 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS, title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
panelTypes: PANEL_TYPES.TIME_SERIES, panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ms', yAxisUnit: 'ms',
id: SERVICE_CHART_ID.externalCallDurationByAddress, id: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
fillSpans: true,
}), }),
[servicename, tagFilterItems], [servicename, tagFilterItems],
); );
@ -155,9 +158,7 @@ function External(): JSX.Element {
<Card data-testid="external_call_error_percentage"> <Card data-testid="external_call_error_percentage">
<GraphContainer> <GraphContainer>
<Graph <Graph
fillSpans={false}
headerMenuList={MENU_ITEMS} headerMenuList={MENU_ITEMS}
name="external_call_error_percentage"
widget={externalCallErrorWidget} widget={externalCallErrorWidget}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => { onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onGraphClickHandler(setSelectedTimeStamp)( onGraphClickHandler(setSelectedTimeStamp)(
@ -192,8 +193,6 @@ function External(): JSX.Element {
<Card data-testid="external_call_duration"> <Card data-testid="external_call_duration">
<GraphContainer> <GraphContainer>
<Graph <Graph
fillSpans
name="external_call_duration"
headerMenuList={MENU_ITEMS} headerMenuList={MENU_ITEMS}
widget={externalCallDurationWidget} widget={externalCallDurationWidget}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => { onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
@ -230,8 +229,6 @@ function External(): JSX.Element {
<Card data-testid="external_call_rps_by_address"> <Card data-testid="external_call_rps_by_address">
<GraphContainer> <GraphContainer>
<Graph <Graph
fillSpans
name="external_call_rps_by_address"
widget={externalCallRPSWidget} widget={externalCallRPSWidget}
headerMenuList={MENU_ITEMS} headerMenuList={MENU_ITEMS}
onClickHandler={(xValue, yValue, mouseX, mouseY): Promise<void> => onClickHandler={(xValue, yValue, mouseX, mouseY): Promise<void> =>
@ -267,10 +264,8 @@ function External(): JSX.Element {
<Card data-testid="external_call_duration_by_address"> <Card data-testid="external_call_duration_by_address">
<GraphContainer> <GraphContainer>
<Graph <Graph
name="external_call_duration_by_address"
widget={externalCallDurationAddressWidget} widget={externalCallDurationAddressWidget}
headerMenuList={MENU_ITEMS} headerMenuList={MENU_ITEMS}
fillSpans
onClickHandler={(xValue, yValue, mouseX, mouseY): void => { onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onGraphClickHandler(setSelectedTimeStamp)( onGraphClickHandler(setSelectedTimeStamp)(
xValue, xValue,

View File

@ -19,13 +19,10 @@ import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { defaultTo } from 'lodash-es'; import { defaultTo } from 'lodash-es';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions'; import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { GraphTitle, SERVICE_CHART_ID } from '../constant'; import { GraphTitle, SERVICE_CHART_ID } from '../constant';
@ -49,9 +46,6 @@ import {
} from './util'; } from './util';
function Application(): JSX.Element { function Application(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { servicename: encodedServiceName } = useParams<IServiceName>(); const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName); const servicename = decodeURIComponent(encodedServiceName);
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0); const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
@ -59,10 +53,6 @@ function Application(): JSX.Element {
const { queries } = useResourceAttribute(); const { queries } = useResourceAttribute();
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const selectedTags = useMemo(
() => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
[queries],
);
const isSpanMetricEnabled = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS) const isSpanMetricEnabled = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS)
?.active; ?.active;
@ -94,7 +84,7 @@ function Application(): JSX.Element {
isLoading: topLevelOperationsIsLoading, isLoading: topLevelOperationsIsLoading,
isError: topLevelOperationsIsError, isError: topLevelOperationsIsError,
} = useQuery<ServiceDataProps>({ } = useQuery<ServiceDataProps>({
queryKey: [servicename, minTime, maxTime, selectedTags], queryKey: [servicename],
queryFn: getTopLevelOperations, queryFn: getTopLevelOperations,
}); });
@ -116,49 +106,41 @@ function Application(): JSX.Element {
[servicename, topLevelOperations], [servicename, topLevelOperations],
); );
const operationPerSecWidget = useMemo( const operationPerSecWidget = getWidgetQueryBuilder({
() => query: {
getWidgetQueryBuilder({ queryType: EQueryType.QUERY_BUILDER,
query: { promql: [],
queryType: EQueryType.QUERY_BUILDER, builder: operationPerSec({
promql: [], servicename,
builder: operationPerSec({ tagFilterItems,
servicename, topLevelOperations: topLevelOperationsRoute,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.RATE_PER_OPS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ops',
id: SERVICE_CHART_ID.rps,
}), }),
[servicename, tagFilterItems, topLevelOperationsRoute], clickhouse_sql: [],
); id: uuid(),
},
title: GraphTitle.RATE_PER_OPS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ops',
id: SERVICE_CHART_ID.rps,
});
const errorPercentageWidget = useMemo( const errorPercentageWidget = getWidgetQueryBuilder({
() => query: {
getWidgetQueryBuilder({ queryType: EQueryType.QUERY_BUILDER,
query: { promql: [],
queryType: EQueryType.QUERY_BUILDER, builder: errorPercentage({
promql: [], servicename,
builder: errorPercentage({ tagFilterItems,
servicename, topLevelOperations: topLevelOperationsRoute,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.errorPercentage,
}), }),
[servicename, tagFilterItems, topLevelOperationsRoute], clickhouse_sql: [],
); id: uuid(),
},
title: GraphTitle.ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.errorPercentage,
});
const onDragSelect = useCallback( const onDragSelect = useCallback(
(start: number, end: number) => { (start: number, end: number) => {

View File

@ -89,8 +89,6 @@ function ApDexMetrics({
return ( return (
<Graph <Graph
name="apdex"
fillSpans={false}
widget={apDexMetricsWidget} widget={apDexMetricsWidget}
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('ApDex')} onClickHandler={handleGraphClick('ApDex')}

View File

@ -50,7 +50,6 @@ function ApDexTraces({
return ( return (
<Graph <Graph
name="apdex"
widget={apDexTracesWidget} widget={apDexTracesWidget}
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('ApDex')} onClickHandler={handleGraphClick('ApDex')}

View File

@ -1,3 +1,4 @@
import { Skeleton } from 'antd';
import { ENTITY_VERSION_V4 } from 'constants/app'; import { ENTITY_VERSION_V4 } from 'constants/app';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
@ -46,28 +47,24 @@ function ServiceOverview({
[isSpanMetricEnable, queries], [isSpanMetricEnable, queries],
); );
const latencyWidget = useMemo( const latencyWidget = getWidgetQueryBuilder({
() => query: {
getWidgetQueryBuilder({ queryType: EQueryType.QUERY_BUILDER,
query: { promql: [],
queryType: EQueryType.QUERY_BUILDER, builder: latency({
promql: [], servicename,
builder: latency({ tagFilterItems,
servicename, isSpanMetricEnable,
tagFilterItems, topLevelOperationsRoute,
isSpanMetricEnable,
topLevelOperationsRoute,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.LATENCY,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ns',
id: SERVICE_CHART_ID.latency,
}), }),
[servicename, isSpanMetricEnable, topLevelOperationsRoute, tagFilterItems], clickhouse_sql: [],
); id: uuid(),
},
title: GraphTitle.LATENCY,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ns',
id: SERVICE_CHART_ID.latency,
});
const isQueryEnabled = const isQueryEnabled =
!topLevelOperationsIsLoading && topLevelOperationsRoute.length > 0; !topLevelOperationsIsLoading && topLevelOperationsRoute.length > 0;
@ -88,15 +85,23 @@ function ServiceOverview({
</Button> </Button>
<Card data-testid="service_latency"> <Card data-testid="service_latency">
<GraphContainer> <GraphContainer>
<Graph {topLevelOperationsIsLoading && (
name="service_latency" <Skeleton
onDragSelect={onDragSelect} style={{
widget={latencyWidget} height: '100%',
onClickHandler={handleGraphClick('Service')} padding: '16px',
isQueryEnabled={isQueryEnabled} }}
fillSpans={false} />
version={ENTITY_VERSION_V4} )}
/> {!topLevelOperationsIsLoading && (
<Graph
onDragSelect={onDragSelect}
widget={latencyWidget}
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
/>
)}
</GraphContainer> </GraphContainer>
</Card> </Card>
</> </>

View File

@ -1,4 +1,4 @@
import { Typography } from 'antd'; import { Skeleton, Typography } from 'antd';
import axios from 'axios'; import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ENTITY_VERSION_V4 } from 'constants/app'; import { ENTITY_VERSION_V4 } from 'constants/app';
@ -27,15 +27,23 @@ function TopLevelOperation({
</Typography> </Typography>
) : ( ) : (
<GraphContainer> <GraphContainer>
<Graph {topLevelOperationsIsLoading && (
fillSpans={false} <Skeleton
name={name} style={{
widget={widget} height: '100%',
onClickHandler={handleGraphClick(opName)} padding: '16px',
onDragSelect={onDragSelect} }}
isQueryEnabled={!topLevelOperationsIsLoading} />
version={ENTITY_VERSION_V4} )}
/> {!topLevelOperationsIsLoading && (
<Graph
widget={widget}
onClickHandler={handleGraphClick(opName)}
onDragSelect={onDragSelect}
isQueryEnabled={!topLevelOperationsIsLoading}
version={ENTITY_VERSION_V4}
/>
)}
</GraphContainer> </GraphContainer>
)} )}
</Card> </Card>

View File

@ -13,7 +13,7 @@ export const Card = styled(CardComponent)`
} }
.ant-card-body { .ant-card-body {
height: calc(100% - 40px); height: 100%;
padding: 0; padding: 0;
} }
`; `;
@ -40,7 +40,7 @@ export const ColErrorContainer = styled(ColComponent)`
export const GraphContainer = styled.div` export const GraphContainer = styled.div`
min-height: calc(40vh - 40px); min-height: calc(40vh - 40px);
height: calc(100% - 40px); height: 100%;
`; `;
export const GraphTitle = styled(Typography)` export const GraphTitle = styled(Typography)`

View File

@ -10,6 +10,7 @@ export interface GetWidgetQueryBuilderProps {
panelTypes: Widgets['panelTypes']; panelTypes: Widgets['panelTypes'];
yAxisUnit?: Widgets['yAxisUnit']; yAxisUnit?: Widgets['yAxisUnit'];
id?: Widgets['id']; id?: Widgets['id'];
fillSpans?: Widgets['fillSpans'];
} }
export interface NavigateToTraceProps { export interface NavigateToTraceProps {

View File

@ -23,7 +23,12 @@ function DashboardDescription(): JSX.Element {
handleDashboardLockToggle, handleDashboardLockToggle,
} = useDashboard(); } = useDashboard();
const selectedData = selectedDashboard?.data || ({} as DashboardData); const selectedData = selectedDashboard
? {
...selectedDashboard.data,
uuid: selectedDashboard.uuid,
}
: ({} as DashboardData);
const { title = '', tags, description } = selectedData || {}; const { title = '', tags, description } = selectedData || {};

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