diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index 88453360f9..3887e223f7 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -150,7 +150,8 @@ services: command: [ "-config=/root/config/prometheus.yml", - "--use-logs-new-schema=true" + "--use-logs-new-schema=true", + "--use-trace-new-schema=true" ] # ports: # - "6060:6060" # pprof port diff --git a/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml b/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml index 8c0b30df61..1b81ea214a 100644 --- a/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml +++ b/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml @@ -110,6 +110,7 @@ exporters: clickhousetraces: datasource: tcp://clickhouse:9000/signoz_traces low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING} + use_new_schema: true clickhousemetricswrite: endpoint: tcp://clickhouse:9000/signoz_metrics resource_to_telemetry_conversion: diff --git a/deploy/docker/clickhouse-setup/docker-compose-local.yaml b/deploy/docker/clickhouse-setup/docker-compose-local.yaml index 7effc129fe..7a4222ff8c 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-local.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-local.yaml @@ -25,7 +25,8 @@ services: command: [ "-config=/root/config/prometheus.yml", - "--use-logs-new-schema=true" + "--use-logs-new-schema=true", + "--use-trace-new-schema=true" ] ports: - "6060:6060" diff --git a/deploy/docker/clickhouse-setup/docker-compose-minimal.yaml b/deploy/docker/clickhouse-setup/docker-compose-minimal.yaml index f737b7d440..37df9590d3 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-minimal.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-minimal.yaml @@ -167,7 +167,8 @@ services: command: [ "-config=/root/config/prometheus.yml", - "--use-logs-new-schema=true" + "--use-logs-new-schema=true", + "--use-trace-new-schema=true" ] # ports: # - "6060:6060" # pprof port diff --git a/deploy/docker/clickhouse-setup/docker-compose.testing.yaml b/deploy/docker/clickhouse-setup/docker-compose.testing.yaml index d90773844e..bd00cf1702 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.testing.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.testing.yaml @@ -173,7 +173,8 @@ services: [ "-config=/root/config/prometheus.yml", "-gateway-url=https://api.staging.signoz.cloud", - "--use-logs-new-schema=true" + "--use-logs-new-schema=true", + "--use-trace-new-schema=true" ] # ports: # - "6060:6060" # pprof port diff --git a/deploy/docker/clickhouse-setup/otel-collector-config.yaml b/deploy/docker/clickhouse-setup/otel-collector-config.yaml index cba7756d8e..b73acdea11 100644 --- a/deploy/docker/clickhouse-setup/otel-collector-config.yaml +++ b/deploy/docker/clickhouse-setup/otel-collector-config.yaml @@ -119,6 +119,7 @@ exporters: clickhousetraces: datasource: tcp://clickhouse:9000/signoz_traces low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING} + use_new_schema: true clickhousemetricswrite: endpoint: tcp://clickhouse:9000/signoz_metrics resource_to_telemetry_conversion: diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index 5d7d6d2ffa..181186d323 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -41,7 +41,6 @@ type APIHandlerOptions struct { FluxInterval time.Duration UseLogsNewSchema bool UseTraceNewSchema bool - UseLicensesV3 bool } type APIHandler struct { @@ -68,7 +67,6 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) { FluxInterval: opts.FluxInterval, UseLogsNewSchema: opts.UseLogsNewSchema, UseTraceNewSchema: opts.UseTraceNewSchema, - UseLicensesV3: opts.UseLicensesV3, }) if err != nil { diff --git a/ee/query-service/app/api/license.go b/ee/query-service/app/api/license.go index 7138e29f80..7a098d4e63 100644 --- a/ee/query-service/app/api/license.go +++ b/ee/query-service/app/api/license.go @@ -84,13 +84,6 @@ func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) { } func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) { - if ah.UseLicensesV3 { - // if the licenses v3 is toggled on then do not apply license in v2 and run the validator! - // TODO: remove after migration to v3 and deprecation from zeus - zap.L().Info("early return from apply license v2 call") - render.Success(w, http.StatusOK, nil) - return - } var l model.License if err := json.NewDecoder(r.Body).Decode(&l); err != nil { @@ -102,7 +95,7 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) { RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil) return } - license, apiError := ah.LM().Activate(r.Context(), l.Key) + license, apiError := ah.LM().ActivateV3(r.Context(), l.Key) if apiError != nil { RespondError(w, apiError, nil) return @@ -265,24 +258,12 @@ func convertLicenseV3ToLicenseV2(licenses []*model.LicenseV3) []model.License { } func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) { - - var licenses []model.License - - if ah.UseLicensesV3 { - licensesV3, err := ah.LM().GetLicensesV3(r.Context()) - if err != nil { - RespondError(w, err, nil) - return - } - licenses = convertLicenseV3ToLicenseV2(licensesV3) - } else { - _licenses, apiError := ah.LM().GetLicenses(r.Context()) - if apiError != nil { - RespondError(w, apiError, nil) - return - } - licenses = _licenses + licensesV3, apierr := ah.LM().GetLicensesV3(r.Context()) + if apierr != nil { + RespondError(w, apierr, nil) + return } + licenses := convertLicenseV3ToLicenseV2(licensesV3) resp := model.Licenses{ TrialStart: -1, diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index a970a34b0e..938b72b5a3 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -78,7 +78,6 @@ type ServerOptions struct { GatewayUrl string UseLogsNewSchema bool UseTraceNewSchema bool - UseLicensesV3 bool } // Server runs HTTP api service @@ -135,7 +134,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { } // initiate license manager - lm, err := licensepkg.StartManager("sqlite", localDB, serverOptions.UseLicensesV3) + lm, err := licensepkg.StartManager("sqlite", localDB) if err != nil { return nil, err } @@ -274,7 +273,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { Gateway: gatewayProxy, UseLogsNewSchema: serverOptions.UseLogsNewSchema, UseTraceNewSchema: serverOptions.UseTraceNewSchema, - UseLicensesV3: serverOptions.UseLicensesV3, } apiHandler, err := api.NewAPIHandler(apiOpts) diff --git a/ee/query-service/integrations/signozio/response.go b/ee/query-service/integrations/signozio/response.go index f0b0132d1b..891ea77da1 100644 --- a/ee/query-service/integrations/signozio/response.go +++ b/ee/query-service/integrations/signozio/response.go @@ -2,18 +2,6 @@ package signozio type status string -type ActivationResult struct { - Status status `json:"status"` - Data *ActivationResponse `json:"data,omitempty"` - ErrorType string `json:"errorType,omitempty"` - Error string `json:"error,omitempty"` -} - -type ActivationResponse struct { - ActivationId string `json:"ActivationId"` - PlanDetails string `json:"PlanDetails"` -} - type ValidateLicenseResponse struct { Status status `json:"status"` Data map[string]interface{} `json:"data"` diff --git a/ee/query-service/integrations/signozio/signozio.go b/ee/query-service/integrations/signozio/signozio.go index 6c0b937c80..a3a5cad414 100644 --- a/ee/query-service/integrations/signozio/signozio.go +++ b/ee/query-service/integrations/signozio/signozio.go @@ -10,7 +10,6 @@ import ( "time" "github.com/pkg/errors" - "go.uber.org/zap" "go.signoz.io/signoz/ee/query-service/constants" "go.signoz.io/signoz/ee/query-service/model" @@ -39,86 +38,6 @@ func init() { C = New() } -// ActivateLicense sends key to license.signoz.io and gets activation data -func ActivateLicense(key, siteId string) (*ActivationResponse, *model.ApiError) { - licenseReq := map[string]string{ - "key": key, - "siteId": siteId, - } - - reqString, _ := json.Marshal(licenseReq) - httpResponse, err := http.Post(C.Prefix+"/licenses/activate", APPLICATION_JSON, bytes.NewBuffer(reqString)) - - if err != nil { - 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")) - } - - httpBody, err := io.ReadAll(httpResponse.Body) - if err != nil { - 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")) - } - - defer httpResponse.Body.Close() - - // read api request result - result := ActivationResult{} - err = json.Unmarshal(httpBody, &result) - if err != nil { - 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")) - } - - switch httpResponse.StatusCode { - case 200, 201: - return result.Data, nil - case 400, 401: - return nil, model.BadRequest(fmt.Errorf(fmt.Sprintf("failed to activate: %s", result.Error))) - default: - return nil, model.InternalError(fmt.Errorf(fmt.Sprintf("failed to activate: %s", result.Error))) - } - -} - -// ValidateLicense validates the license key -func ValidateLicense(activationId string) (*ActivationResponse, *model.ApiError) { - validReq := map[string]string{ - "activationId": activationId, - } - - reqString, _ := json.Marshal(validReq) - response, err := http.Post(C.Prefix+"/licenses/validate", APPLICATION_JSON, bytes.NewBuffer(reqString)) - - if err != nil { - return nil, model.BadRequest(errors.Wrap(err, "unable to connect with license.signoz.io, please check your network connection")) - } - - body, err := io.ReadAll(response.Body) - if err != nil { - return nil, model.BadRequest(errors.Wrap(err, "failed to read validation response from license.signoz.io")) - } - - defer response.Body.Close() - - switch response.StatusCode { - case 200, 201: - a := ActivationResult{} - err = json.Unmarshal(body, &a) - if err != nil { - return nil, model.BadRequest(errors.Wrap(err, "failed to marshal license validation response")) - } - return a.Data, nil - case 400, 401: - return nil, model.BadRequest(errors.Wrap(fmt.Errorf(string(body)), - "bad request error received from license.signoz.io")) - default: - return nil, model.InternalError(errors.Wrap(fmt.Errorf(string(body)), - "internal error received from license.signoz.io")) - } - -} - func ValidateLicenseV3(licenseKey string) (*model.LicenseV3, *model.ApiError) { // Creating an HTTP client with a timeout for better control diff --git a/ee/query-service/license/db.go b/ee/query-service/license/db.go index 4bd34e232c..1dba4053d7 100644 --- a/ee/query-service/license/db.go +++ b/ee/query-service/license/db.go @@ -18,15 +18,13 @@ import ( // Repo is license repo. stores license keys in a secured DB type Repo struct { - db *sqlx.DB - useLicensesV3 bool + db *sqlx.DB } // NewLicenseRepo initiates a new license repo -func NewLicenseRepo(db *sqlx.DB, useLicensesV3 bool) Repo { +func NewLicenseRepo(db *sqlx.DB) Repo { return Repo{ - db: db, - useLicensesV3: useLicensesV3, + db: db, } } @@ -112,26 +110,16 @@ func (r *Repo) GetActiveLicenseV2(ctx context.Context) (*model.License, *basemod // GetActiveLicense fetches the latest active license from DB. // If the license is not present, expect a nil license and a nil error in the output. func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) { - if r.useLicensesV3 { - zap.L().Info("Using licenses v3 for GetActiveLicense") - activeLicenseV3, err := r.GetActiveLicenseV3(ctx) - if err != nil { - return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err)) - } - - if activeLicenseV3 == nil { - return nil, nil - } - activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3) - return activeLicenseV2, nil - + activeLicenseV3, err := r.GetActiveLicenseV3(ctx) + if err != nil { + return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err)) } - active, err := r.GetActiveLicenseV2(ctx) - if err != nil { - return nil, err + if activeLicenseV3 == nil { + return nil, nil } - return active, nil + activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3) + return activeLicenseV2, nil } func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) { diff --git a/ee/query-service/license/manager.go b/ee/query-service/license/manager.go index 0a4370de3f..c036a01ab5 100644 --- a/ee/query-service/license/manager.go +++ b/ee/query-service/license/manager.go @@ -51,12 +51,12 @@ type Manager struct { activeFeatures basemodel.FeatureSet } -func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...basemodel.Feature) (*Manager, error) { +func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) { if LM != nil { return LM, nil } - repo := NewLicenseRepo(db, useLicensesV3) + repo := NewLicenseRepo(db) err := repo.InitDB(dbType) if err != nil { @@ -67,32 +67,7 @@ func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...ba repo: &repo, } - if useLicensesV3 { - // get active license from the db - active, err := m.repo.GetActiveLicenseV2(context.Background()) - if err != nil { - return m, err - } - - // if we have an active license then need to fetch the complete details - if active != nil { - // fetch the new license structure from control plane - licenseV3, apiError := validate.ValidateLicenseV3(active.Key) - if apiError != nil { - return m, apiError - } - - // insert the licenseV3 in sqlite db - apiError = m.repo.InsertLicenseV3(context.Background(), licenseV3) - // if the license already exists move ahead. - if apiError != nil && apiError.Typ != model.ErrorConflict { - return m, apiError - } - zap.L().Info("Successfully inserted license from v2 to v3 table") - } - } - - if err := m.start(useLicensesV3, features...); err != nil { + if err := m.start(features...); err != nil { return m, err } LM = m @@ -100,16 +75,8 @@ func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...ba } // start loads active license in memory and initiates validator -func (lm *Manager) start(useLicensesV3 bool, features ...basemodel.Feature) error { - - var err error - if useLicensesV3 { - err = lm.LoadActiveLicenseV3(features...) - } else { - err = lm.LoadActiveLicense(features...) - } - - return err +func (lm *Manager) start(features ...basemodel.Feature) error { + return lm.LoadActiveLicenseV3(features...) } func (lm *Manager) Stop() { @@ -117,31 +84,6 @@ func (lm *Manager) Stop() { <-lm.terminated } -func (lm *Manager) SetActive(l *model.License, features ...basemodel.Feature) { - lm.mutex.Lock() - defer lm.mutex.Unlock() - - if l == nil { - return - } - - lm.activeLicense = l - lm.activeFeatures = append(l.FeatureSet, features...) - // set default features - setDefaultFeatures(lm) - - err := lm.InitFeatures(lm.activeFeatures) - if err != nil { - zap.L().Panic("Couldn't activate features", zap.Error(err)) - } - if !lm.validatorRunning { - // we want to make sure only one validator runs, - // we already have lock() so good to go - lm.validatorRunning = true - go lm.Validator(context.Background()) - } - -} func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) { lm.mutex.Lock() defer lm.mutex.Unlock() @@ -172,29 +114,6 @@ func setDefaultFeatures(lm *Manager) { lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...) } -// LoadActiveLicense loads the most recent active license -func (lm *Manager) LoadActiveLicense(features ...basemodel.Feature) error { - active, err := lm.repo.GetActiveLicense(context.Background()) - if err != nil { - return err - } - if active != nil { - lm.SetActive(active, features...) - } else { - 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 - lm.activeFeatures = model.BasicPlan - setDefaultFeatures(lm) - err := lm.InitFeatures(lm.activeFeatures) - if err != nil { - zap.L().Error("Couldn't initialize features", zap.Error(err)) - return err - } - } - - return nil -} - func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error { active, err := lm.repo.GetActiveLicenseV3(context.Background()) if err != nil { @@ -265,31 +184,6 @@ func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.License return response, nil } -// Validator validates license after an epoch of time -func (lm *Manager) Validator(ctx context.Context) { - zap.L().Info("Validator started!") - defer close(lm.terminated) - tick := time.NewTicker(validationFrequency) - defer tick.Stop() - - lm.Validate(ctx) - - for { - select { - case <-lm.done: - return - default: - select { - case <-lm.done: - return - case <-tick.C: - lm.Validate(ctx) - } - } - - } -} - // Validator validates license after an epoch of time func (lm *Manager) ValidatorV3(ctx context.Context) { zap.L().Info("ValidatorV3 started!") @@ -315,73 +209,6 @@ func (lm *Manager) ValidatorV3(ctx context.Context) { } } -// Validate validates the current active license -func (lm *Manager) Validate(ctx context.Context) (reterr error) { - zap.L().Info("License validation started") - if lm.activeLicense == nil { - return nil - } - - defer func() { - lm.mutex.Lock() - - lm.lastValidated = time.Now().Unix() - if reterr != nil { - zap.L().Error("License validation completed with error", zap.Error(reterr)) - atomic.AddUint64(&lm.failedAttempts, 1) - telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED, - map[string]interface{}{"err": reterr.Error()}, "", true, false) - } else { - zap.L().Info("License validation completed with no errors") - } - - lm.mutex.Unlock() - }() - - response, apiError := validate.ValidateLicense(lm.activeLicense.ActivationId) - if apiError != nil { - zap.L().Error("failed to validate license", zap.Error(apiError.Err)) - return apiError.Err - } - - if response.PlanDetails == lm.activeLicense.PlanDetails { - // license plan hasnt changed, nothing to do - return nil - } - - if response.PlanDetails != "" { - - // copy and replace the active license record - l := model.License{ - Key: lm.activeLicense.Key, - CreatedAt: lm.activeLicense.CreatedAt, - PlanDetails: response.PlanDetails, - ValidationMessage: lm.activeLicense.ValidationMessage, - ActivationId: lm.activeLicense.ActivationId, - } - - if err := l.ParsePlan(); err != nil { - zap.L().Error("failed to parse updated license", zap.Error(err)) - return err - } - - // updated plan is parsable, check if plan has changed - if lm.activeLicense.PlanDetails != response.PlanDetails { - err := lm.repo.UpdatePlanDetails(ctx, lm.activeLicense.Key, response.PlanDetails) - if err != nil { - // unexpected db write issue but we can let the user continue - // and wait for update to work in next cycle. - zap.L().Error("failed to validate license", zap.Error(err)) - } - } - - // activate the update license plan - lm.SetActive(&l) - } - - return nil -} - func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError { license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key) @@ -429,50 +256,6 @@ func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) { return nil } -// Activate activates a license key with signoz server -func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *model.License, errResponse *model.ApiError) { - defer func() { - if errResponse != nil { - userEmail, err := auth.GetEmailFromJwt(ctx) - if err == nil { - telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED, - map[string]interface{}{"err": errResponse.Err.Error()}, userEmail, true, false) - } - } - }() - - response, apiError := validate.ActivateLicense(key, "") - if apiError != nil { - zap.L().Error("failed to activate license", zap.Error(apiError.Err)) - return nil, apiError - } - - l := &model.License{ - Key: key, - ActivationId: response.ActivationId, - PlanDetails: response.PlanDetails, - } - - // parse validity and features from the plan details - err := l.ParsePlan() - - if err != nil { - zap.L().Error("failed to activate license", zap.Error(err)) - return nil, model.InternalError(err) - } - - // store the license before activating it - err = lm.repo.InsertLicense(ctx, l) - if err != nil { - zap.L().Error("failed to activate license", zap.Error(err)) - return nil, model.InternalError(err) - } - - // license is valid, activate it - lm.SetActive(l) - return l, nil -} - func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) { defer func() { if errResponse != nil { diff --git a/ee/query-service/main.go b/ee/query-service/main.go index 23824bd636..a93a034f87 100644 --- a/ee/query-service/main.go +++ b/ee/query-service/main.go @@ -95,7 +95,6 @@ func main() { var useLogsNewSchema bool var useTraceNewSchema bool - var useLicensesV3 bool var cacheConfigPath, fluxInterval string var enableQueryServiceLogOTLPExport bool var preferSpanMetrics bool @@ -104,10 +103,10 @@ func main() { var maxOpenConns int var dialTimeout time.Duration var gatewayUrl string + var useLicensesV3 bool flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs") flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces") - flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses") flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)") flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)") flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)") @@ -121,6 +120,7 @@ func main() { flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)") flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')") flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)") + flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses") flag.Parse() @@ -148,7 +148,6 @@ func main() { GatewayUrl: gatewayUrl, UseLogsNewSchema: useLogsNewSchema, UseTraceNewSchema: useTraceNewSchema, - UseLicensesV3: useLicensesV3, } // Read the jwt secret key diff --git a/frontend/src/components/CustomTimePicker/CustomTimePicker.styles.scss b/frontend/src/components/CustomTimePicker/CustomTimePicker.styles.scss index 14f80a9b93..022a193761 100644 --- a/frontend/src/components/CustomTimePicker/CustomTimePicker.styles.scss +++ b/frontend/src/components/CustomTimePicker/CustomTimePicker.styles.scss @@ -40,7 +40,7 @@ &.custom-time { input:not(:focus) { - min-width: 240px; + min-width: 280px; } } @@ -119,3 +119,69 @@ color: var(--bg-slate-400) !important; } } + +.date-time-popover__footer { + border-top: 1px solid var(--bg-ink-200); + padding: 8px 14px; + .timezone-container { + &, + .timezone { + font-family: Inter; + font-size: 12px; + line-height: 16px; + letter-spacing: -0.06px; + } + display: flex; + align-items: center; + color: var(--bg-vanilla-400); + gap: 6px; + .timezone { + display: flex; + align-items: center; + gap: 4px; + border-radius: 2px; + background: rgba(171, 189, 255, 0.04); + cursor: pointer; + padding: 0px 4px; + color: var(--bg-vanilla-100); + border: none; + } + } +} +.timezone-badge { + display: flex; + align-items: center; + justify-content: center; + padding: 0 4px; + border-radius: 2px; + background: rgba(171, 189, 255, 0.04); + color: var(--bg-vanilla-100); + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: -0.06px; + cursor: pointer; +} + +.lightMode { + .date-time-popover__footer { + border-color: var(--bg-vanilla-400); + } + .timezone-container { + color: var(--bg-ink-400); + &__clock-icon { + stroke: var(--bg-ink-400); + } + .timezone { + color: var(--bg-ink-100); + background: rgb(179 179 179 / 15%); + &__icon { + stroke: var(--bg-ink-100); + } + } + } + .timezone-badge { + color: var(--bg-ink-100); + background: rgb(179 179 179 / 15%); + } +} diff --git a/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx b/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx index 47ee89c880..6064b64a09 100644 --- a/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx +++ b/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx @@ -15,11 +15,14 @@ import { isValidTimeFormat } from 'lib/getMinMax'; import { defaultTo, isFunction, noop } from 'lodash-es'; import debounce from 'lodash-es/debounce'; import { CheckCircle, ChevronDown, Clock } from 'lucide-react'; +import { useTimezone } from 'providers/Timezone'; import { ChangeEvent, Dispatch, SetStateAction, + useCallback, useEffect, + useMemo, useState, } from 'react'; import { useLocation } from 'react-router-dom'; @@ -28,6 +31,8 @@ import { popupContainer } from 'utils/selectPopupContainer'; import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent'; const maxAllowedMinTimeInMonths = 6; +type ViewType = 'datetime' | 'timezone'; +const DEFAULT_VIEW: ViewType = 'datetime'; interface CustomTimePickerProps { onSelect: (value: string) => void; @@ -81,11 +86,42 @@ function CustomTimePicker({ const location = useLocation(); const [isInputFocused, setIsInputFocused] = useState(false); + const [activeView, setActiveView] = useState(DEFAULT_VIEW); + + const { timezone, browserTimezone } = useTimezone(); + const activeTimezoneOffset = timezone.offset; + const isTimezoneOverridden = useMemo( + () => timezone.offset !== browserTimezone.offset, + [timezone, browserTimezone], + ); + + const handleViewChange = useCallback( + (newView: 'timezone' | 'datetime'): void => { + if (activeView !== newView) { + setActiveView(newView); + } + setOpen(true); + }, + [activeView, setOpen], + ); + + const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false); + const getSelectedTimeRangeLabel = ( selectedTime: string, selectedTimeValue: string, ): string => { if (selectedTime === 'custom') { + // Convert the date range string to 12-hour format + const dates = selectedTimeValue.split(' - '); + if (dates.length === 2) { + const startDate = dayjs(dates[0], 'DD/MM/YYYY HH:mm'); + const endDate = dayjs(dates[1], 'DD/MM/YYYY HH:mm'); + + return `${startDate.format('DD/MM/YYYY hh:mm A')} - ${endDate.format( + 'DD/MM/YYYY hh:mm A', + )}`; + } return selectedTimeValue; } @@ -131,6 +167,7 @@ function CustomTimePicker({ setOpen(newOpen); if (!newOpen) { setCustomDTPickerVisible?.(false); + setActiveView('datetime'); } }; @@ -244,6 +281,7 @@ function CustomTimePicker({ const handleFocus = (): void => { setIsInputFocused(true); + setActiveView('datetime'); }; const handleBlur = (): void => { @@ -280,6 +318,10 @@ function CustomTimePicker({ handleGoLive={defaultTo(handleGoLive, noop)} options={items} selectedTime={selectedTime} + activeView={activeView} + setActiveView={setActiveView} + setIsOpenedFromFooter={setIsOpenedFromFooter} + isOpenedFromFooter={isOpenedFromFooter} /> ) : ( content @@ -316,12 +358,24 @@ function CustomTimePicker({ ) } suffix={ - { - setOpen(!open); - }} - /> + <> + {!!isTimezoneOverridden && activeTimezoneOffset && ( +
{ + e.stopPropagation(); + handleViewChange('timezone'); + setIsOpenedFromFooter(false); + }} + > + {activeTimezoneOffset} +
+ )} + handleViewChange('datetime')} + /> + } /> diff --git a/frontend/src/components/CustomTimePicker/CustomTimePickerPopoverContent.tsx b/frontend/src/components/CustomTimePicker/CustomTimePickerPopoverContent.tsx index 4a41bec4f5..a42bb6b478 100644 --- a/frontend/src/components/CustomTimePicker/CustomTimePickerPopoverContent.tsx +++ b/frontend/src/components/CustomTimePicker/CustomTimePickerPopoverContent.tsx @@ -1,5 +1,6 @@ import './CustomTimePicker.styles.scss'; +import { Color } from '@signozhq/design-tokens'; import { Button } from 'antd'; import cx from 'classnames'; import ROUTES from 'constants/routes'; @@ -9,10 +10,13 @@ import { Option, RelativeDurationSuggestionOptions, } from 'container/TopNav/DateTimeSelectionV2/config'; +import { Clock, PenLine } from 'lucide-react'; +import { useTimezone } from 'providers/Timezone'; import { Dispatch, SetStateAction, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import RangePickerModal from './RangePickerModal'; +import TimezonePicker from './TimezonePicker'; interface CustomTimePickerPopoverContentProps { options: any[]; @@ -26,8 +30,13 @@ interface CustomTimePickerPopoverContentProps { onSelectHandler: (label: string, value: string) => void; handleGoLive: () => void; selectedTime: string; + activeView: 'datetime' | 'timezone'; + setActiveView: Dispatch>; + isOpenedFromFooter: boolean; + setIsOpenedFromFooter: Dispatch>; } +// eslint-disable-next-line sonarjs/cognitive-complexity function CustomTimePickerPopoverContent({ options, setIsOpen, @@ -37,12 +46,18 @@ function CustomTimePickerPopoverContent({ onSelectHandler, handleGoLive, selectedTime, + activeView, + setActiveView, + isOpenedFromFooter, + setIsOpenedFromFooter, }: CustomTimePickerPopoverContentProps): JSX.Element { const { pathname } = useLocation(); const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [ pathname, ]); + const { timezone } = useTimezone(); + const activeTimezoneOffset = timezone.offset; function getTimeChips(options: Option[]): JSX.Element { return ( @@ -63,55 +78,99 @@ function CustomTimePickerPopoverContent({ ); } + const handleTimezoneHintClick = (): void => { + setActiveView('timezone'); + setIsOpenedFromFooter(true); + }; + + if (activeView === 'timezone') { + return ( +
+ +
+ ); + } + return ( -
-
- {isLogsExplorerPage && ( - - )} - {options.map((option) => ( - - ))} + <> +
+
+ {isLogsExplorerPage && ( + + )} + {options.map((option) => ( + + ))} +
+
+ {selectedTime === 'custom' || customDateTimeVisible ? ( + + ) : ( +
+
RELATIVE TIMES
+
{getTimeChips(RelativeDurationSuggestionOptions)}
+
+ )} +
-
- {selectedTime === 'custom' || customDateTimeVisible ? ( - +
+ - ) : ( -
-
RELATIVE TIMES
-
{getTimeChips(RelativeDurationSuggestionOptions)}
-
- )} + Current timezone +
+ +
-
+ ); } diff --git a/frontend/src/components/CustomTimePicker/RangePickerModal.tsx b/frontend/src/components/CustomTimePicker/RangePickerModal.tsx index 24ba0e2b01..862d63e922 100644 --- a/frontend/src/components/CustomTimePicker/RangePickerModal.tsx +++ b/frontend/src/components/CustomTimePicker/RangePickerModal.tsx @@ -3,7 +3,8 @@ import './RangePickerModal.styles.scss'; import { DatePicker } from 'antd'; import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal'; import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config'; -import dayjs, { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; +import { useTimezone } from 'providers/Timezone'; import { Dispatch, SetStateAction } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; @@ -31,7 +32,10 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element { (state) => state.globalTime, ); - const disabledDate = (current: Dayjs): boolean => { + // Using any type here because antd's DatePicker expects its own internal Dayjs type + // which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc). + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + const disabledDate = (current: any): boolean => { const currentDay = dayjs(current); return currentDay.isAfter(dayjs()); }; @@ -49,16 +53,22 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element { } onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER); }; + + const { timezone } = useTimezone(); return (
diff --git a/frontend/src/components/CustomTimePicker/TimezonePicker.styles.scss b/frontend/src/components/CustomTimePicker/TimezonePicker.styles.scss new file mode 100644 index 0000000000..50ee1502ba --- /dev/null +++ b/frontend/src/components/CustomTimePicker/TimezonePicker.styles.scss @@ -0,0 +1,166 @@ +// Variables +$font-family: 'Inter'; +$item-spacing: 8px; + +:root { + --border-color: var(--bg-slate-400); +} + +.lightMode { + --border-color: var(--bg-vanilla-400); +} + +// Mixins +@mixin text-style-base { + font-family: $font-family; + font-style: normal; + font-weight: 400; +} + +@mixin flex-center { + display: flex; + align-items: center; +} + +.timezone-picker { + width: 532px; + color: var(--bg-vanilla-400); + font-family: $font-family; + + &__search { + @include flex-center; + justify-content: space-between; + padding: 12px 14px; + border-bottom: 1px solid var(--border-color); + } + + &__input-container { + @include flex-center; + gap: 6px; + width: -webkit-fill-available; + } + + &__input { + @include text-style-base; + width: 100%; + background: transparent; + border: none; + outline: none; + color: var(--bg-vanilla-100); + font-size: 14px; + line-height: 20px; + letter-spacing: -0.07px; + padding: 0; + &.ant-input:focus { + box-shadow: none; + } + + &::placeholder { + color: var(--bg-vanilla-400); + } + } + + &__esc-key { + @include text-style-base; + font-size: 8px; + color: var(--bg-vanilla-400); + letter-spacing: -0.04px; + border-radius: 2.286px; + border: 1.143px solid var(--bg-ink-200); + border-bottom-width: 2.286px; + background: var(--bg-ink-400); + padding: 0 1px; + } + + &__list { + max-height: 310px; + overflow-y: auto; + } + + &__item { + @include flex-center; + justify-content: space-between; + padding: 7.5px 6px 7.5px $item-spacing; + margin: 4px $item-spacing; + cursor: pointer; + background: transparent; + border: none; + width: -webkit-fill-available; + color: var(--bg-vanilla-400); + font-family: $font-family; + + &:hover, + &.selected { + border-radius: 2px; + background: rgba(171, 189, 255, 0.04); + color: var(--bg-vanilla-100); + } + + &.has-divider { + position: relative; + &::after { + content: ''; + position: absolute; + bottom: -2px; + left: -$item-spacing; + right: -$item-spacing; + border-bottom: 1px solid var(--border-color); + } + } + } + + &__name { + @include text-style-base; + font-size: 14px; + line-height: 20px; + letter-spacing: -0.07px; + } + + &__offset { + color: var(--bg-vanilla-100); + font-size: 12px; + line-height: 16px; + letter-spacing: -0.06px; + } +} + +.timezone-name-wrapper { + @include flex-center; + gap: 6px; + + &__selected-icon { + height: 15px; + width: 15px; + } +} + +.lightMode { + .timezone-picker { + &__search { + .search-icon { + stroke: var(--bg-ink-400); + } + } + &__input { + color: var(--bg-ink-100); + } + &__esc-key { + background-color: var(--bg-vanilla-100); + border-color: var(--bg-vanilla-400); + color: var(--bg-ink-400); + } + &__item { + color: var(--bg-ink-400); + } + &__offset { + color: var(--bg-ink-100); + } + } + .timezone-name-wrapper { + &__selected-icon { + .check-icon { + stroke: var(--bg-ink-100); + } + } + } +} diff --git a/frontend/src/components/CustomTimePicker/TimezonePicker.tsx b/frontend/src/components/CustomTimePicker/TimezonePicker.tsx new file mode 100644 index 0000000000..2f4da45837 --- /dev/null +++ b/frontend/src/components/CustomTimePicker/TimezonePicker.tsx @@ -0,0 +1,201 @@ +import './TimezonePicker.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Input } from 'antd'; +import cx from 'classnames'; +import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts'; +import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; +import { Check, Search } from 'lucide-react'; +import { useTimezone } from 'providers/Timezone'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from 'react'; + +import { Timezone, TIMEZONE_DATA } from './timezoneUtils'; + +interface SearchBarProps { + value: string; + onChange: (value: string) => void; + setIsOpen: Dispatch>; + setActiveView: Dispatch>; + isOpenedFromFooter: boolean; +} + +interface TimezoneItemProps { + timezone: Timezone; + isSelected?: boolean; + onClick?: () => void; +} + +const ICON_SIZE = 14; + +function SearchBar({ + value, + onChange, + setIsOpen, + setActiveView, + isOpenedFromFooter = false, +}: SearchBarProps): JSX.Element { + const handleKeyDown = useCallback( + (e: React.KeyboardEvent): void => { + if (e.key === 'Escape') { + if (isOpenedFromFooter) { + setActiveView('datetime'); + } else { + setIsOpen(false); + } + } + }, + [setActiveView, setIsOpen, isOpenedFromFooter], + ); + + return ( +
+
+ + onChange(e.target.value)} + onKeyDown={handleKeyDown} + tabIndex={0} + autoFocus + /> +
+ esc +
+ ); +} + +function TimezoneItem({ + timezone, + isSelected = false, + onClick, +}: TimezoneItemProps): JSX.Element { + return ( + + ); +} + +TimezoneItem.defaultProps = { + isSelected: false, + onClick: undefined, +}; + +interface TimezonePickerProps { + setActiveView: Dispatch>; + setIsOpen: Dispatch>; + isOpenedFromFooter: boolean; +} + +function TimezonePicker({ + setActiveView, + setIsOpen, + isOpenedFromFooter, +}: TimezonePickerProps): JSX.Element { + console.log({ isOpenedFromFooter }); + const [searchTerm, setSearchTerm] = useState(''); + const { timezone, updateTimezone } = useTimezone(); + const [selectedTimezone, setSelectedTimezone] = useState( + timezone.name ?? TIMEZONE_DATA[0].name, + ); + + const getFilteredTimezones = useCallback((searchTerm: string): Timezone[] => { + const normalizedSearch = searchTerm.toLowerCase(); + return TIMEZONE_DATA.filter( + (tz) => + tz.name.toLowerCase().includes(normalizedSearch) || + tz.offset.toLowerCase().includes(normalizedSearch) || + tz.searchIndex.toLowerCase().includes(normalizedSearch), + ); + }, []); + + const handleCloseTimezonePicker = useCallback(() => { + if (isOpenedFromFooter) { + setActiveView('datetime'); + } else { + setIsOpen(false); + } + }, [isOpenedFromFooter, setActiveView, setIsOpen]); + + const handleTimezoneSelect = useCallback( + (timezone: Timezone) => { + setSelectedTimezone(timezone.name); + updateTimezone(timezone); + handleCloseTimezonePicker(); + setIsOpen(false); + }, + [handleCloseTimezonePicker, setIsOpen, updateTimezone], + ); + + // Register keyboard shortcuts + const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys(); + + useEffect(() => { + registerShortcut( + TimezonePickerShortcuts.CloseTimezonePicker, + handleCloseTimezonePicker, + ); + + return (): void => { + deregisterShortcut(TimezonePickerShortcuts.CloseTimezonePicker); + }; + }, [deregisterShortcut, handleCloseTimezonePicker, registerShortcut]); + + return ( +
+ +
+ {getFilteredTimezones(searchTerm).map((timezone) => ( + handleTimezoneSelect(timezone)} + /> + ))} +
+
+ ); +} + +export default TimezonePicker; diff --git a/frontend/src/components/CustomTimePicker/timezoneUtils.ts b/frontend/src/components/CustomTimePicker/timezoneUtils.ts new file mode 100644 index 0000000000..92da405ba4 --- /dev/null +++ b/frontend/src/components/CustomTimePicker/timezoneUtils.ts @@ -0,0 +1,152 @@ +import dayjs from 'dayjs'; +import timezone from 'dayjs/plugin/timezone'; +import utc from 'dayjs/plugin/utc'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export interface Timezone { + name: string; + value: string; + offset: string; + searchIndex: string; + hasDivider?: boolean; +} + +const TIMEZONE_TYPES = { + BROWSER: 'BROWSER', + UTC: 'UTC', + STANDARD: 'STANDARD', +} as const; + +type TimezoneType = typeof TIMEZONE_TYPES[keyof typeof TIMEZONE_TYPES]; + +export const UTC_TIMEZONE: Timezone = { + name: 'Coordinated Universal Time — UTC, GMT', + value: 'UTC', + offset: 'UTC', + searchIndex: 'UTC', + hasDivider: true, +}; + +const normalizeTimezoneName = (timezone: string): string => { + // https://github.com/tc39/proposal-temporal/issues/1076 + if (timezone === 'Asia/Calcutta') { + return 'Asia/Kolkata'; + } + return timezone; +}; + +const formatOffset = (offsetMinutes: number): string => { + if (offsetMinutes === 0) return 'UTC'; + + const hours = Math.floor(Math.abs(offsetMinutes) / 60); + const minutes = Math.abs(offsetMinutes) % 60; + const sign = offsetMinutes > 0 ? '+' : '-'; + + return `UTC ${sign} ${hours}${ + minutes ? `:${minutes.toString().padStart(2, '0')}` : ':00' + }`; +}; + +const createTimezoneEntry = ( + name: string, + offsetMinutes: number, + type: TimezoneType = TIMEZONE_TYPES.STANDARD, + hasDivider = false, +): Timezone => { + const offset = formatOffset(offsetMinutes); + let value = name; + let displayName = name; + + switch (type) { + case TIMEZONE_TYPES.BROWSER: + displayName = `Browser time — ${name}`; + value = name; + break; + case TIMEZONE_TYPES.UTC: + displayName = 'Coordinated Universal Time — UTC, GMT'; + value = 'UTC'; + break; + case TIMEZONE_TYPES.STANDARD: + displayName = name; + value = name; + break; + default: + console.error(`Invalid timezone type: ${type}`); + } + + return { + name: displayName, + value, + offset, + searchIndex: offset.replace(/ /g, ''), + ...(hasDivider && { hasDivider }), + }; +}; + +const getOffsetByTimezone = (timezone: string): number => { + const dayjsTimezone = dayjs().tz(timezone); + return dayjsTimezone.utcOffset(); +}; + +export const getBrowserTimezone = (): Timezone => { + const browserTz = dayjs.tz.guess(); + const normalizedTz = normalizeTimezoneName(browserTz); + const browserOffset = getOffsetByTimezone(normalizedTz); + return createTimezoneEntry( + normalizedTz, + browserOffset, + TIMEZONE_TYPES.BROWSER, + ); +}; + +const filterAndSortTimezones = ( + allTimezones: string[], + browserTzName?: string, + includeEtcTimezones = false, +): Timezone[] => + allTimezones + .filter((tz) => { + const isNotBrowserTz = tz !== browserTzName; + const isNotEtcTz = includeEtcTimezones || !tz.startsWith('Etc/'); + return isNotBrowserTz && isNotEtcTz; + }) + .sort((a, b) => a.localeCompare(b)) + .map((tz) => { + const normalizedTz = normalizeTimezoneName(tz); + const offset = getOffsetByTimezone(normalizedTz); + return createTimezoneEntry(normalizedTz, offset); + }); + +const generateTimezoneData = (includeEtcTimezones = false): Timezone[] => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const allTimezones = (Intl as any).supportedValuesOf('timeZone'); + const timezones: Timezone[] = []; + + // Add browser timezone + const browserTzObject = getBrowserTimezone(); + timezones.push(browserTzObject); + + // Add UTC timezone with divider + timezones.push(UTC_TIMEZONE); + + timezones.push( + ...filterAndSortTimezones( + allTimezones, + browserTzObject.value, + includeEtcTimezones, + ), + ); + + return timezones; +}; + +export const getTimezoneObjectByTimezoneString = ( + timezone: string, +): Timezone => { + const utcOffset = getOffsetByTimezone(timezone); + return createTimezoneEntry(timezone, utcOffset); +}; + +export const TIMEZONE_DATA = generateTimezoneData(); diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx index 0065f6b33c..6358ebfe7a 100644 --- a/frontend/src/components/Graph/index.tsx +++ b/frontend/src/components/Graph/index.tsx @@ -1,4 +1,5 @@ import { + _adapters, BarController, BarElement, CategoryScale, @@ -18,8 +19,10 @@ import { } from 'chart.js'; import annotationPlugin from 'chartjs-plugin-annotation'; import { generateGridTitle } from 'container/GridPanelSwitch/utils'; +import dayjs from 'dayjs'; import { useIsDarkMode } from 'hooks/useDarkMode'; import isEqual from 'lodash-es/isEqual'; +import { useTimezone } from 'providers/Timezone'; import { forwardRef, memo, @@ -62,6 +65,17 @@ Chart.register( Tooltip.positioners.custom = TooltipPositionHandler; +// Map of Chart.js time formats to dayjs format strings +const formatMap = { + 'HH:mm:ss': 'HH:mm:ss', + 'HH:mm': 'HH:mm', + 'MM/DD HH:mm': 'MM/DD HH:mm', + 'MM/dd HH:mm': 'MM/DD HH:mm', + 'MM/DD': 'MM/DD', + 'YY-MM': 'YY-MM', + YY: 'YY', +}; + const Graph = forwardRef( ( { @@ -80,11 +94,13 @@ const Graph = forwardRef( dragSelectColor, }, ref, + // eslint-disable-next-line sonarjs/cognitive-complexity ): JSX.Element => { const nearestDatasetIndex = useRef(null); const chartRef = useRef(null); const isDarkMode = useIsDarkMode(); const gridTitle = useMemo(() => generateGridTitle(title), [title]); + const { timezone } = useTimezone(); const currentTheme = isDarkMode ? 'dark' : 'light'; const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data @@ -112,6 +128,22 @@ const Graph = forwardRef( return 'rgba(231,233,237,0.8)'; }, [currentTheme]); + // Override Chart.js date adapter to use dayjs with timezone support + useEffect(() => { + _adapters._date.override({ + format(time: number | Date, fmt: string) { + const dayjsTime = dayjs(time).tz(timezone.value); + const format = formatMap[fmt as keyof typeof formatMap]; + if (!format) { + console.warn(`Missing datetime format for ${fmt}`); + return dayjsTime.format('YYYY-MM-DD HH:mm:ss'); // fallback format + } + + return dayjsTime.format(format); + }, + }); + }, [timezone]); + const buildChart = useCallback(() => { if (lineChartRef.current !== undefined) { lineChartRef.current.destroy(); @@ -132,6 +164,7 @@ const Graph = forwardRef( isStacked, onClickHandler, data, + timezone, ); const chartHasData = hasData(data); @@ -166,6 +199,7 @@ const Graph = forwardRef( isStacked, onClickHandler, data, + timezone, name, type, ]); diff --git a/frontend/src/components/Graph/utils.ts b/frontend/src/components/Graph/utils.ts index f002d1402f..603f387566 100644 --- a/frontend/src/components/Graph/utils.ts +++ b/frontend/src/components/Graph/utils.ts @@ -1,5 +1,6 @@ import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js'; import * as chartjsAdapter from 'chartjs-adapter-date-fns'; +import { Timezone } from 'components/CustomTimePicker/timezoneUtils'; import dayjs from 'dayjs'; import { MutableRefObject } from 'react'; @@ -50,6 +51,7 @@ export const getGraphOptions = ( isStacked: boolean | undefined, onClickHandler: GraphOnClickHandler | undefined, data: ChartData, + timezone: Timezone, // eslint-disable-next-line sonarjs/cognitive-complexity ): CustomChartOptions => ({ animation: { @@ -97,7 +99,7 @@ export const getGraphOptions = ( callbacks: { title(context): string | string[] { const date = dayjs(context[0].parsed.x); - return date.format('MMM DD, YYYY, HH:mm:ss'); + return date.tz(timezone.value).format('MMM DD, YYYY, HH:mm:ss'); }, label(context): string | string[] { let label = context.dataset.label || ''; diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index 8d5c0118cd..1fdd7414dd 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -8,13 +8,13 @@ import LogDetail from 'components/LogDetail'; import { VIEW_TYPES } from 'components/LogDetail/constants'; import { unescapeString } from 'container/LogDetailedView/utils'; import { FontSize } from 'container/OptionsMenu/types'; -import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useIsDarkMode } from 'hooks/useDarkMode'; // utils import { FlatLogData } from 'lib/logs/flatLogData'; +import { useTimezone } from 'providers/Timezone'; import { useCallback, useMemo, useState } from 'react'; // interfaces import { IField } from 'types/api/logs/fields'; @@ -174,12 +174,20 @@ function ListLogView({ [selectedFields], ); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + const timestampValue = useMemo( () => typeof flattenLogData.timestamp === 'string' - ? dayjs(flattenLogData.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS') - : dayjs(flattenLogData.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'), - [flattenLogData.timestamp], + ? formatTimezoneAdjustedTimestamp( + flattenLogData.timestamp, + 'YYYY-MM-DD HH:mm:ss.SSS', + ) + : formatTimezoneAdjustedTimestamp( + flattenLogData.timestamp / 1e6, + 'YYYY-MM-DD HH:mm:ss.SSS', + ), + [flattenLogData.timestamp, formatTimezoneAdjustedTimestamp], ); const logType = getLogIndicatorType(logData); diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index 46e6a63aba..222931ee6d 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -6,7 +6,6 @@ import LogDetail from 'components/LogDetail'; import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants'; import { unescapeString } from 'container/LogDetailedView/utils'; import LogsExplorerContext from 'container/LogsExplorerContext'; -import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; @@ -14,6 +13,7 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { FlatLogData } from 'lib/logs/flatLogData'; import { isEmpty, isNumber, isUndefined } from 'lodash-es'; +import { useTimezone } from 'providers/Timezone'; import { KeyboardEvent, MouseEvent, @@ -89,16 +89,24 @@ function RawLogView({ attributesText += ' | '; } + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + const text = useMemo(() => { const date = typeof data.timestamp === 'string' - ? dayjs(data.timestamp) - : dayjs(data.timestamp / 1e6); - - return `${date.format('YYYY-MM-DD HH:mm:ss.SSS')} | ${attributesText} ${ - data.body - }`; - }, [data.timestamp, data.body, attributesText]); + ? formatTimezoneAdjustedTimestamp(data.timestamp, 'YYYY-MM-DD HH:mm:ss.SSS') + : formatTimezoneAdjustedTimestamp( + data.timestamp / 1e6, + 'YYYY-MM-DD HH:mm:ss.SSS', + ); + + return `${date} | ${attributesText} ${data.body}`; + }, [ + data.timestamp, + data.body, + attributesText, + formatTimezoneAdjustedTimestamp, + ]); const handleClickExpand = useCallback(() => { if (activeContextLog || isReadOnly) return; diff --git a/frontend/src/components/Logs/TableView/useTableView.tsx b/frontend/src/components/Logs/TableView/useTableView.tsx index 662686e67d..890b9e0500 100644 --- a/frontend/src/components/Logs/TableView/useTableView.tsx +++ b/frontend/src/components/Logs/TableView/useTableView.tsx @@ -5,10 +5,10 @@ import { Typography } from 'antd'; import { ColumnsType } from 'antd/es/table'; import cx from 'classnames'; import { unescapeString } from 'container/LogDetailedView/utils'; -import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { FlatLogData } from 'lib/logs/flatLogData'; +import { useTimezone } from 'providers/Timezone'; import { useMemo } from 'react'; import { FORBID_DOM_PURIFY_TAGS } from 'utils/app'; @@ -44,6 +44,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { logs, ]); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + const columns: ColumnsType> = useMemo(() => { const fieldColumns: ColumnsType> = fields .filter((e) => e.name !== 'id') @@ -81,8 +83,11 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { render: (field, item): ColumnTypeRender> => { const date = typeof field === 'string' - ? dayjs(field).format('YYYY-MM-DD HH:mm:ss.SSS') - : dayjs(field / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'); + ? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS') + : formatTimezoneAdjustedTimestamp( + field / 1e6, + 'YYYY-MM-DD HH:mm:ss.SSS', + ); return { children: (
@@ -125,7 +130,15 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { }, ...(appendTo === 'end' ? fieldColumns : []), ]; - }, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]); + }, [ + fields, + isListViewPanel, + appendTo, + isDarkMode, + linesPerRow, + fontSize, + formatTimezoneAdjustedTimestamp, + ]); return { columns, dataSource: flattenLogData }; }; diff --git a/frontend/src/components/ResizeTable/TableComponent/Time.tsx b/frontend/src/components/ResizeTable/TableComponent/Time.tsx index e579dbdfda..062dafcc2e 100644 --- a/frontend/src/components/ResizeTable/TableComponent/Time.tsx +++ b/frontend/src/components/ResizeTable/TableComponent/Time.tsx @@ -1,11 +1,13 @@ import { Typography } from 'antd'; -import convertDateToAmAndPm from 'lib/convertDateToAmAndPm'; -import getFormattedDate from 'lib/getFormatedDate'; +import { useTimezone } from 'providers/Timezone'; function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element { + const { formatTimezoneAdjustedTimestamp } = useTimezone(); const time = new Date(CreatedOrUpdateTime); - const date = getFormattedDate(time); - const timeString = `${date} ${convertDateToAmAndPm(time)}`; + const timeString = formatTimezoneAdjustedTimestamp( + time, + 'MM/DD/YYYY hh:mm:ss A (UTC Z)', + ); return {timeString}; } diff --git a/frontend/src/constants/localStorage.ts b/frontend/src/constants/localStorage.ts index 4e6859a2dd..5284fc92ad 100644 --- a/frontend/src/constants/localStorage.ts +++ b/frontend/src/constants/localStorage.ts @@ -21,4 +21,5 @@ export enum LOCALSTORAGE { THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1', LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS', SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS', + PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE', } diff --git a/frontend/src/constants/shortcuts/TimezonePickerShortcuts.ts b/frontend/src/constants/shortcuts/TimezonePickerShortcuts.ts new file mode 100644 index 0000000000..b7627e6e3d --- /dev/null +++ b/frontend/src/constants/shortcuts/TimezonePickerShortcuts.ts @@ -0,0 +1,3 @@ +export const TimezonePickerShortcuts = { + CloseTimezonePicker: 'escape', +}; diff --git a/frontend/src/container/AlertHistory/Timeline/Graph/Graph.tsx b/frontend/src/container/AlertHistory/Timeline/Graph/Graph.tsx index b3eecda24c..6a09298fd3 100644 --- a/frontend/src/container/AlertHistory/Timeline/Graph/Graph.tsx +++ b/frontend/src/container/AlertHistory/Timeline/Graph/Graph.tsx @@ -7,6 +7,7 @@ import useUrlQuery from 'hooks/useUrlQuery'; import history from 'lib/history'; import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin'; import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin'; +import { useTimezone } from 'providers/Timezone'; import { useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { UpdateTimeInterval } from 'store/actions'; @@ -48,6 +49,7 @@ function HorizontalTimelineGraph({ const urlQuery = useUrlQuery(); const dispatch = useDispatch(); + const { timezone } = useTimezone(); const options: uPlot.Options = useMemo( () => ({ @@ -116,8 +118,18 @@ function HorizontalTimelineGraph({ }), ] : [], + + tzDate: (timestamp: number): Date => + uPlot.tzDate(new Date(timestamp * 1e3), timezone.value), }), - [width, isDarkMode, transformedData.length, urlQuery, dispatch], + [ + width, + isDarkMode, + transformedData.length, + urlQuery, + dispatch, + timezone.value, + ], ); return ; } diff --git a/frontend/src/container/AlertHistory/Timeline/Table/Table.tsx b/frontend/src/container/AlertHistory/Timeline/Table/Table.tsx index dffcf3fb7a..d910950547 100644 --- a/frontend/src/container/AlertHistory/Timeline/Table/Table.tsx +++ b/frontend/src/container/AlertHistory/Timeline/Table/Table.tsx @@ -7,6 +7,7 @@ import { useGetAlertRuleDetailsTimelineTable, useTimelineTable, } from 'pages/AlertDetails/hooks'; +import { useTimezone } from 'providers/Timezone'; import { HTMLAttributes, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def'; @@ -41,6 +42,8 @@ function TimelineTable(): JSX.Element { const { t } = useTranslation('common'); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + if (isError || !isValidRuleId || !ruleId) { return
{t('something_went_wrong')}
; } @@ -64,6 +67,7 @@ function TimelineTable(): JSX.Element { filters, labels: labels ?? {}, setFilters, + formatTimezoneAdjustedTimestamp, })} onRow={handleRowClick} dataSource={timelineData} diff --git a/frontend/src/container/AlertHistory/Timeline/Table/useTimelineTable.tsx b/frontend/src/container/AlertHistory/Timeline/Table/useTimelineTable.tsx index 5c67caa984..adccbedfa3 100644 --- a/frontend/src/container/AlertHistory/Timeline/Table/useTimelineTable.tsx +++ b/frontend/src/container/AlertHistory/Timeline/Table/useTimelineTable.tsx @@ -8,6 +8,7 @@ import ClientSideQBSearch, { import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover'; import { transformKeyValuesToAttributeValuesMap } from 'container/QueryBuilder/filters/utils'; import { useIsDarkMode } from 'hooks/useDarkMode'; +import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter'; import { Search } from 'lucide-react'; import AlertLabels, { AlertLabelsProps, @@ -16,7 +17,6 @@ import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState'; import { useMemo } from 'react'; import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def'; import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; -import { formatEpochTimestamp } from 'utils/timeUtils'; const transformLabelsToQbKeys = ( labels: AlertRuleTimelineTableResponse['labels'], @@ -74,10 +74,15 @@ export const timelineTableColumns = ({ filters, labels, setFilters, + formatTimezoneAdjustedTimestamp, }: { filters: TagFilter; labels: AlertLabelsProps['labels']; setFilters: (filters: TagFilter) => void; + formatTimezoneAdjustedTimestamp: ( + input: TimestampInput, + format?: string, + ) => string; }): ColumnsType => [ { title: 'STATE', @@ -106,7 +111,9 @@ export const timelineTableColumns = ({ dataIndex: 'unixMilli', width: 200, render: (value): JSX.Element => ( -
{formatEpochTimestamp(value)}
+
+ {formatTimezoneAdjustedTimestamp(value, 'MMM D, YYYY ⎯ HH:mm:ss')} +
), }, { diff --git a/frontend/src/container/AllError/index.tsx b/frontend/src/container/AllError/index.tsx index 0dd46c0a64..ce238c76b5 100644 --- a/frontend/src/container/AllError/index.tsx +++ b/frontend/src/container/AllError/index.tsx @@ -17,14 +17,15 @@ import getAll from 'api/errors/getAll'; import getErrorCounts from 'api/errors/getErrorCounts'; import { ResizeTable } from 'components/ResizeTable'; import ROUTES from 'constants/routes'; -import dayjs from 'dayjs'; import { useNotifications } from 'hooks/useNotifications'; import useResourceAttribute from 'hooks/useResourceAttribute'; import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; +import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter'; import useUrlQuery from 'hooks/useUrlQuery'; import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; import { isUndefined } from 'lodash-es'; +import { useTimezone } from 'providers/Timezone'; import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useQueries } from 'react-query'; @@ -155,8 +156,16 @@ function AllErrors(): JSX.Element { } }, [data?.error, data?.payload, t, notifications]); - const getDateValue = (value: string): JSX.Element => ( - {dayjs(value).format('DD/MM/YYYY HH:mm:ss A')} + const getDateValue = ( + value: string, + formatTimezoneAdjustedTimestamp: ( + input: TimestampInput, + format?: string, + ) => string, + ): JSX.Element => ( + + {formatTimezoneAdjustedTimestamp(value, 'DD/MM/YYYY hh:mm:ss A')} + ); const filterIcon = useCallback(() => , []); @@ -283,6 +292,8 @@ function AllErrors(): JSX.Element { [filterIcon, filterDropdownWrapper], ); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + const columns: ColumnsType = [ { title: 'Exception Type', @@ -342,7 +353,8 @@ function AllErrors(): JSX.Element { dataIndex: 'lastSeen', width: 80, key: 'lastSeen', - render: getDateValue, + render: (value): JSX.Element => + getDateValue(value, formatTimezoneAdjustedTimestamp), sorter: true, defaultSortOrder: getDefaultOrder( getUpdatedParams, @@ -355,7 +367,8 @@ function AllErrors(): JSX.Element { dataIndex: 'firstSeen', width: 80, key: 'firstSeen', - render: getDateValue, + render: (value): JSX.Element => + getDateValue(value, formatTimezoneAdjustedTimestamp), sorter: true, defaultSortOrder: getDefaultOrder( getUpdatedParams, diff --git a/frontend/src/container/AnomalyAlertEvaluationView/AnomalyAlertEvaluationView.tsx b/frontend/src/container/AnomalyAlertEvaluationView/AnomalyAlertEvaluationView.tsx index 88eff524c6..90ecdbed0f 100644 --- a/frontend/src/container/AnomalyAlertEvaluationView/AnomalyAlertEvaluationView.tsx +++ b/frontend/src/container/AnomalyAlertEvaluationView/AnomalyAlertEvaluationView.tsx @@ -10,6 +10,7 @@ import getAxes from 'lib/uPlotLib/utils/getAxes'; import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData'; import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale'; import { LineChart } from 'lucide-react'; +import { useTimezone } from 'providers/Timezone'; import { useEffect, useRef, useState } from 'react'; import uPlot from 'uplot'; @@ -148,10 +149,12 @@ function AnomalyAlertEvaluationView({ ] : []; + const { timezone } = useTimezone(); + const options = { width: dimensions.width, height: dimensions.height - 36, - plugins: [bandsPlugin, tooltipPlugin(isDarkMode)], + plugins: [bandsPlugin, tooltipPlugin(isDarkMode, timezone.value)], focus: { alpha: 0.3, }, @@ -256,6 +259,8 @@ function AnomalyAlertEvaluationView({ show: true, }, axes: getAxes(isDarkMode, yAxisUnit), + tzDate: (timestamp: number): Date => + uPlot.tzDate(new Date(timestamp * 1e3), timezone.value), }; const handleSearch = (searchText: string): void => { diff --git a/frontend/src/container/AnomalyAlertEvaluationView/tooltipPlugin.ts b/frontend/src/container/AnomalyAlertEvaluationView/tooltipPlugin.ts index 6d32dbee35..c98515e371 100644 --- a/frontend/src/container/AnomalyAlertEvaluationView/tooltipPlugin.ts +++ b/frontend/src/container/AnomalyAlertEvaluationView/tooltipPlugin.ts @@ -1,8 +1,10 @@ import { themeColors } from 'constants/theme'; +import dayjs from 'dayjs'; import { generateColor } from 'lib/uPlotLib/utils/generateColor'; const tooltipPlugin = ( isDarkMode: boolean, + timezone: string, ): { hooks: { init: (u: any) => void } } => { let tooltip: HTMLDivElement; const tooltipLeftOffset = 10; @@ -17,7 +19,7 @@ const tooltipPlugin = ( return value.toFixed(3); } if (value instanceof Date) { - return value.toLocaleString(); + return dayjs(value).tz(timezone).format('MM/DD/YYYY, h:mm:ss A'); } if (value == null) { return 'N/A'; diff --git a/frontend/src/container/ErrorDetails/index.tsx b/frontend/src/container/ErrorDetails/index.tsx index c6b0d5fa22..7dbc57e4a5 100644 --- a/frontend/src/container/ErrorDetails/index.tsx +++ b/frontend/src/container/ErrorDetails/index.tsx @@ -6,12 +6,12 @@ import getNextPrevId from 'api/errors/getNextPrevId'; import Editor from 'components/Editor'; import { ResizeTable } from 'components/ResizeTable'; import { getNanoSeconds } from 'container/AllError/utils'; -import dayjs from 'dayjs'; import { useNotifications } from 'hooks/useNotifications'; import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; import { isUndefined } from 'lodash-es'; import { urlKey } from 'pages/ErrorDetails/utils'; +import { useTimezone } from 'providers/Timezone'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from 'react-query'; @@ -103,8 +103,6 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element { } }; - const timeStamp = dayjs(errorDetail.timestamp); - const data: { key: string; value: string }[] = Object.keys(errorDetail) .filter((e) => !keyToExclude.includes(e)) .map((key) => ({ @@ -136,6 +134,8 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + return ( <> {errorDetail.exceptionType} @@ -145,7 +145,12 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
Event {errorDetail.errorId} - {timeStamp.format('MMM DD YYYY hh:mm:ss A')} + + {formatTimezoneAdjustedTimestamp( + errorDetail.timestamp, + 'DD/MM/YYYY hh:mm:ss A (UTC Z)', + )} +
diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index af6550dedf..54a25e6565 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -25,6 +25,7 @@ import getTimeString from 'lib/getTimeString'; import history from 'lib/history'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; +import { useTimezone } from 'providers/Timezone'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; @@ -35,6 +36,7 @@ import { AlertDef } from 'types/api/alerts/def'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; import { GlobalReducer } from 'types/reducer/globalTime'; +import uPlot from 'uplot'; import { getGraphType } from 'utils/getGraphType'; import { getSortedSeriesData } from 'utils/getSortedSeriesData'; import { getTimeRange } from 'utils/getTimeRange'; @@ -201,6 +203,8 @@ function ChartPreview({ [dispatch, location.pathname, urlQuery], ); + const { timezone } = useTimezone(); + const options = useMemo( () => getUPlotChartOptions({ @@ -236,6 +240,9 @@ function ChartPreview({ softMax: null, softMin: null, panelType: graphType, + tzDate: (timestamp: number) => + uPlot.tzDate(new Date(timestamp * 1e3), timezone.value), + timezone: timezone.value, }), [ yAxisUnit, @@ -250,6 +257,7 @@ function ChartPreview({ optionName, alertDef?.condition.targetUnit, graphType, + timezone.value, ], ); diff --git a/frontend/src/container/GantChart/Span/index.tsx b/frontend/src/container/GantChart/Span/index.tsx index 23fc000ac3..03b2b805e4 100644 --- a/frontend/src/container/GantChart/Span/index.tsx +++ b/frontend/src/container/GantChart/Span/index.tsx @@ -4,6 +4,7 @@ import { Popover, Typography } from 'antd'; import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils'; import dayjs from 'dayjs'; import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useTimezone } from 'providers/Timezone'; import { useEffect } from 'react'; import { toFixed } from 'utils/toFixed'; @@ -32,13 +33,17 @@ function Span(props: SpanLengthProps): JSX.Element { const isDarkMode = useIsDarkMode(); const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount); + const { timezone } = useTimezone(); + useEffect(() => { document.documentElement.scrollTop = document.documentElement.clientHeight; document.documentElement.scrollLeft = document.documentElement.clientWidth; }, []); const getContent = (): JSX.Element => { - const timeStamp = dayjs(startTime).format('h:mm:ss:SSS A'); + const timeStamp = dayjs(startTime) + .tz(timezone.value) + .format('h:mm:ss:SSS A (UTC Z)'); const startTimeInMs = startTime - globalStart; return (
diff --git a/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx index b6e744ccaf..aceb1c477a 100644 --- a/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx +++ b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx @@ -31,7 +31,7 @@ import { AxiosError } from 'axios'; import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import Tags from 'components/Tags/Tags'; import { SOMETHING_WENT_WRONG } from 'constants/api'; -import dayjs, { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys'; import useDebouncedFn from 'hooks/useDebouncedFunction'; import { useNotifications } from 'hooks/useNotifications'; @@ -51,6 +51,7 @@ import { Trash2, X, } from 'lucide-react'; +import { useTimezone } from 'providers/Timezone'; import { ChangeEvent, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; @@ -70,7 +71,10 @@ const { Option } = Select; const BYTES = 1073741824; -export const disabledDate = (current: Dayjs): boolean => +// Using any type here because antd's DatePicker expects its own internal Dayjs type +// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc). +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export const disabledDate = (current: any): boolean => // Disable all dates before today current && current < dayjs().endOf('day'); @@ -393,8 +397,11 @@ function MultiIngestionSettings(): JSX.Element { const gbToBytes = (gb: number): number => Math.round(gb * 1024 ** 3); - const getFormattedTime = (date: string): string => - dayjs(date).format('MMM DD,YYYY, hh:mm a'); + const getFormattedTime = ( + date: string, + formatTimezoneAdjustedTimestamp: (date: string, format: string) => string, + ): string => + formatTimezoneAdjustedTimestamp(date, 'MMM DD,YYYY, hh:mm a (UTC Z)'); const showDeleteLimitModal = ( APIKey: IngestionKeyProps, @@ -544,17 +551,27 @@ function MultiIngestionSettings(): JSX.Element { } }; + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + const columns: AntDTableProps['columns'] = [ { title: 'Ingestion Key', key: 'ingestion-key', // eslint-disable-next-line sonarjs/cognitive-complexity render: (APIKey: IngestionKeyProps): JSX.Element => { - const createdOn = getFormattedTime(APIKey.created_at); + const createdOn = getFormattedTime( + APIKey.created_at, + formatTimezoneAdjustedTimestamp, + ); const formattedDateAndTime = - APIKey && APIKey?.expires_at && getFormattedTime(APIKey?.expires_at); + APIKey && + APIKey?.expires_at && + getFormattedTime(APIKey?.expires_at, formatTimezoneAdjustedTimestamp); - const updatedOn = getFormattedTime(APIKey?.updated_at); + const updatedOn = getFormattedTime( + APIKey?.updated_at, + formatTimezoneAdjustedTimestamp, + ); const limits: { [key: string]: LimitProps } = {}; diff --git a/frontend/src/container/Licenses/ListLicenses.tsx b/frontend/src/container/Licenses/ListLicenses.tsx index 02d3abbb65..b166e2d659 100644 --- a/frontend/src/container/Licenses/ListLicenses.tsx +++ b/frontend/src/container/Licenses/ListLicenses.tsx @@ -1,8 +1,20 @@ +import { Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { ResizeTable } from 'components/ResizeTable'; +import { useTimezone } from 'providers/Timezone'; import { useTranslation } from 'react-i18next'; import { License } from 'types/api/licenses/def'; +function ValidityColumn({ value }: { value: string }): JSX.Element { + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + + return ( + + {formatTimezoneAdjustedTimestamp(value, 'YYYY-MM-DD HH:mm:ss (UTC Z)')} + + ); +} + function ListLicenses({ licenses }: ListLicensesProps): JSX.Element { const { t } = useTranslation(['licenses']); @@ -23,12 +35,14 @@ function ListLicenses({ licenses }: ListLicensesProps): JSX.Element { title: t('column_valid_from'), dataIndex: 'ValidFrom', key: 'valid from', + render: (value: string): JSX.Element => ValidityColumn({ value }), width: 80, }, { title: t('column_valid_until'), dataIndex: 'ValidUntil', key: 'valid until', + render: (value: string): JSX.Element => ValidityColumn({ value }), width: 80, }, ]; diff --git a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss index 21d4a5e20d..08e62dd101 100644 --- a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss +++ b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss @@ -867,7 +867,7 @@ .configure-metadata-root { .ant-modal-content { - width: 400px; + width: 500px; flex-shrink: 0; border-radius: 4px; border: 1px solid var(--Slate-500, #161922); @@ -1039,7 +1039,6 @@ display: flex; justify-content: space-between; align-items: center; - width: 336px; padding: 0px 0px 0px 14.634px; .left { diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index 0a5b3b5130..902421052c 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -27,6 +27,8 @@ import { AxiosError } from 'axios'; import cx from 'classnames'; import { ENTITY_VERSION_V4 } from 'constants/app'; import ROUTES from 'constants/routes'; +import { sanitizeDashboardData } from 'container/NewDashboard/DashboardDescription'; +import { downloadObjectAsJson } from 'container/NewDashboard/DashboardDescription/utils'; import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils'; import dayjs from 'dayjs'; import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; @@ -44,6 +46,7 @@ import { EllipsisVertical, Expand, ExternalLink, + FileJson, Github, HdmiPort, LayoutGrid, @@ -57,6 +60,7 @@ import { // see more: https://github.com/lucide-icons/lucide/issues/94 import { handleContactSupport } from 'pages/Integrations/utils'; import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { useTimezone } from 'providers/Timezone'; import { ChangeEvent, Key, @@ -66,12 +70,18 @@ import { useRef, useState, } from 'react'; +import { Layout } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { generatePath, Link } from 'react-router-dom'; import { useCopyToClipboard } from 'react-use'; import { AppState } from 'store/reducers'; -import { Dashboard } from 'types/api/dashboard/getAll'; +import { + Dashboard, + IDashboardVariable, + WidgetRow, + Widgets, +} from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; import { isCloudUser } from 'utils/app'; @@ -260,6 +270,11 @@ function DashboardsList(): JSX.Element { isLocked: !!e.isLocked || false, lastUpdatedBy: e.updated_by, image: e.data.image || Base64Icons[0], + variables: e.data.variables, + widgets: e.data.widgets, + layout: e.data.layout, + panelMap: e.data.panelMap, + version: e.data.version, refetchDashboardList, })) || []; @@ -343,31 +358,13 @@ function DashboardsList(): JSX.Element { } }, [state.error, state.value, t, notifications]); - function getFormattedTime(dashboard: Dashboard, option: string): string { - const timeOptions: Intl.DateTimeFormatOptions = { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - }; - const formattedTime = new Date(get(dashboard, option, '')).toLocaleTimeString( - 'en-US', - timeOptions, - ); - - const dateOptions: Intl.DateTimeFormatOptions = { - month: 'short', - day: 'numeric', - year: 'numeric', - }; + const { formatTimezoneAdjustedTimestamp } = useTimezone(); - const formattedDate = new Date(get(dashboard, option, '')).toLocaleDateString( - 'en-US', - dateOptions, + function getFormattedTime(dashboard: Dashboard, option: string): string { + return formatTimezoneAdjustedTimestamp( + get(dashboard, option, ''), + 'MMM D, YYYY ⎯ hh:mm:ss A (UTC Z)', ); - - // Combine time and date - return `${formattedDate} ⎯ ${formattedTime}`; } const onLastUpdated = (time: string): string => { @@ -410,31 +407,11 @@ function DashboardsList(): JSX.Element { title: 'Dashboards', key: 'dashboard', render: (dashboard: Data, _, index): JSX.Element => { - const timeOptions: Intl.DateTimeFormatOptions = { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - }; - const formattedTime = new Date(dashboard.createdAt).toLocaleTimeString( - 'en-US', - timeOptions, - ); - - const dateOptions: Intl.DateTimeFormatOptions = { - month: 'short', - day: 'numeric', - year: 'numeric', - }; - - const formattedDate = new Date(dashboard.createdAt).toLocaleDateString( - 'en-US', - dateOptions, + const formattedDateAndTime = formatTimezoneAdjustedTimestamp( + dashboard.createdAt, + 'MMM D, YYYY ⎯ hh:mm:ss A (UTC Z)', ); - // Combine time and date - const formattedDateAndTime = `${formattedDate} ⎯ ${formattedTime}`; - const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`; const onClickHandler = (event: React.MouseEvent): void => { @@ -450,6 +427,15 @@ function DashboardsList(): JSX.Element { }); }; + const handleJsonExport = (event: React.MouseEvent): void => { + event.stopPropagation(); + event.preventDefault(); + downloadObjectAsJson( + sanitizeDashboardData({ ...dashboard, title: dashboard.name }), + dashboard.name, + ); + }; + return (
@@ -523,6 +509,14 @@ function DashboardsList(): JSX.Element { > Copy Link +
{ e.stopPropagation(); e.preventDefault(); @@ -1105,6 +1100,11 @@ export interface Data { isLocked: boolean; id: string; image?: string; + widgets?: Array; + layout?: Layout[]; + panelMap?: Record; + variables: Record; + version?: string; } export default DashboardsList; diff --git a/frontend/src/container/LogDetailedView/InfraMetrics/NodeMetrics.tsx b/frontend/src/container/LogDetailedView/InfraMetrics/NodeMetrics.tsx index 3c935c8b89..8c46d572fd 100644 --- a/frontend/src/container/LogDetailedView/InfraMetrics/NodeMetrics.tsx +++ b/frontend/src/container/LogDetailedView/InfraMetrics/NodeMetrics.tsx @@ -8,10 +8,12 @@ import { useResizeObserver } from 'hooks/useDimensions'; import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; +import { useTimezone } from 'providers/Timezone'; import { useMemo, useRef } from 'react'; import { useQueries, UseQueryResult } from 'react-query'; import { SuccessResponse } from 'types/api'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import uPlot from 'uplot'; import { getHostQueryPayload, @@ -73,6 +75,8 @@ function NodeMetrics({ [queries], ); + const { timezone } = useTimezone(); + const options = useMemo( () => queries.map(({ data }, idx) => @@ -86,6 +90,9 @@ function NodeMetrics({ minTimeScale: start, maxTimeScale: end, verticalLineTimestamp, + tzDate: (timestamp: number) => + uPlot.tzDate(new Date(timestamp * 1e3), timezone.value), + timezone: timezone.value, }), ), [ @@ -96,6 +103,7 @@ function NodeMetrics({ start, verticalLineTimestamp, end, + timezone.value, ], ); diff --git a/frontend/src/container/LogDetailedView/InfraMetrics/PodMetrics.tsx b/frontend/src/container/LogDetailedView/InfraMetrics/PodMetrics.tsx index 99391d65e0..bb6ff0b654 100644 --- a/frontend/src/container/LogDetailedView/InfraMetrics/PodMetrics.tsx +++ b/frontend/src/container/LogDetailedView/InfraMetrics/PodMetrics.tsx @@ -8,10 +8,12 @@ import { useResizeObserver } from 'hooks/useDimensions'; import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; +import { useTimezone } from 'providers/Timezone'; import { useMemo, useRef } from 'react'; import { useQueries, UseQueryResult } from 'react-query'; import { SuccessResponse } from 'types/api'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import uPlot from 'uplot'; import { getPodQueryPayload, podWidgetInfo } from './constants'; @@ -60,6 +62,7 @@ function PodMetrics({ () => queries.map(({ data }) => getUPlotChartData(data?.payload)), [queries], ); + const { timezone } = useTimezone(); const options = useMemo( () => @@ -74,9 +77,20 @@ function PodMetrics({ minTimeScale: start, maxTimeScale: end, verticalLineTimestamp, + tzDate: (timestamp: number) => + uPlot.tzDate(new Date(timestamp * 1e3), timezone.value), + timezone: timezone.value, }), ), - [queries, isDarkMode, dimensions, start, verticalLineTimestamp, end], + [ + queries, + isDarkMode, + dimensions, + start, + end, + verticalLineTimestamp, + timezone.value, + ], ); const renderCardContent = ( diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx index 57ceea5072..501c99740d 100644 --- a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx @@ -11,7 +11,8 @@ import ROUTES from 'constants/routes'; import dompurify from 'dompurify'; import { isEmpty } from 'lodash-es'; import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react'; -import { useMemo, useState } from 'react'; +import { useTimezone } from 'providers/Timezone'; +import React, { useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { FORBID_DOM_PURIFY_TAGS } from 'utils/app'; @@ -68,6 +69,8 @@ export function TableViewActions( const [isOpen, setIsOpen] = useState(false); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + if (record.field === 'body') { const parsedBody = recursiveParseJSON(fieldData.value); if (!isEmpty(parsedBody)) { @@ -100,33 +103,44 @@ export function TableViewActions( ); } - return ( -
- {record.field === 'body' ? ( - - - - ) : ( - - - {removeEscapeCharacters(fieldData.value)} + let cleanTimestamp: string; + if (record.field === 'timestamp') { + cleanTimestamp = fieldData.value.replace(/^["']|["']$/g, ''); + } + + const renderFieldContent = (): JSX.Element => { + const commonStyles: React.CSSProperties = { + color: Color.BG_SIENNA_400, + whiteSpace: 'pre-wrap', + tabSize: 4, + }; + + switch (record.field) { + case 'body': + return ; + + case 'timestamp': + return ( + + {formatTimezoneAdjustedTimestamp( + cleanTimestamp, + 'MM/DD/YYYY, HH:mm:ss.SSS (UTC Z)', + )} - - )} + ); + + default: + return ( + {removeEscapeCharacters(fieldData.value)} + ); + } + }; + return ( +
+ + {renderFieldContent()} + {!isListViewPanel && ( diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 5ce5dbe2be..8d6b74a025 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -50,6 +50,7 @@ import { } from 'lodash-es'; import { Sliders } from 'lucide-react'; import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; +import { useTimezone } from 'providers/Timezone'; import { memo, MutableRefObject, @@ -669,13 +670,19 @@ function LogsExplorerViews({ setIsLoadingQueries, ]); + const { timezone } = useTimezone(); + const flattenLogData = useMemo( () => logs.map((log) => { const timestamp = typeof log.timestamp === 'string' - ? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS') - : dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'); + ? dayjs(log.timestamp) + .tz(timezone.value) + .format('YYYY-MM-DD HH:mm:ss.SSS') + : dayjs(log.timestamp / 1e6) + .tz(timezone.value) + .format('YYYY-MM-DD HH:mm:ss.SSS'); return FlatLogData({ timestamp, @@ -683,7 +690,7 @@ function LogsExplorerViews({ ...omit(log, 'timestamp', 'body'), }); }), - [logs], + [logs, timezone.value], ); return ( diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx index 6271ff793e..d708684824 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx @@ -7,6 +7,7 @@ import { rest } from 'msw'; import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; import { QueryBuilderProvider } from 'providers/QueryBuilder'; import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; +import TimezoneProvider from 'providers/Timezone'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; @@ -91,17 +92,19 @@ const renderer = (): RenderResult => - - {}} - listQueryKeyRef={{ current: {} }} - chartQueryKeyRef={{ current: {} }} - /> - + + + {}} + listQueryKeyRef={{ current: {} }} + chartQueryKeyRef={{ current: {} }} + /> + + diff --git a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx index a7598b5c76..d8131b5403 100644 --- a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx +++ b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx @@ -15,6 +15,7 @@ import { useLogsData } from 'hooks/useLogsData'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import { FlatLogData } from 'lib/logs/flatLogData'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { useTimezone } from 'providers/Timezone'; import { Dispatch, HTMLAttributes, @@ -76,7 +77,12 @@ function LogsPanelComponent({ }); }; - const columns = getLogPanelColumnsList(widget.selectedLogFields); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + + const columns = getLogPanelColumnsList( + widget.selectedLogFields, + formatTimezoneAdjustedTimestamp, + ); const dataLength = queryResponse.data?.payload?.data?.newResult?.data?.result[0]?.list?.length; diff --git a/frontend/src/container/LogsPanelTable/utils.tsx b/frontend/src/container/LogsPanelTable/utils.tsx index a95442b7cc..954526bde1 100644 --- a/frontend/src/container/LogsPanelTable/utils.tsx +++ b/frontend/src/container/LogsPanelTable/utils.tsx @@ -1,6 +1,7 @@ import { ColumnsType } from 'antd/es/table'; import { Typography } from 'antd/lib'; import { OPERATORS } from 'constants/queryBuilder'; +import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter'; // import Typography from 'antd/es/typography/Typography'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { ReactNode } from 'react'; @@ -13,18 +14,31 @@ import { v4 as uuid } from 'uuid'; export const getLogPanelColumnsList = ( selectedLogFields: Widgets['selectedLogFields'], + formatTimezoneAdjustedTimestamp: ( + input: TimestampInput, + format?: string, + ) => string, ): ColumnsType => { const initialColumns: ColumnsType = []; const columns: ColumnsType = selectedLogFields?.map((field: IField) => { const { name } = field; + return { title: name, dataIndex: name, key: name, width: name === 'body' ? 350 : 100, render: (value: ReactNode): JSX.Element => { + if (name === 'timestamp') { + return ( + + {formatTimezoneAdjustedTimestamp(value as string)} + + ); + } + if (name === 'body') { return ( diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index 6decf72b39..24e47041d7 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -185,6 +185,7 @@ function Application(): JSX.Element { panelTypes: PANEL_TYPES.TIME_SERIES, yAxisUnit: '%', id: SERVICE_CHART_ID.errorPercentage, + fillSpans: true, }), [servicename, tagFilterItems, topLevelOperationsRoute], ); @@ -222,12 +223,11 @@ function Application(): JSX.Element { apmToTraceQuery: Query, isViewLogsClicked?: boolean, ): (() => void) => (): void => { - const currentTime = timestamp; - const endTime = timestamp + stepInterval; - console.log(endTime, stepInterval); + const endTime = timestamp; + const startTime = timestamp - stepInterval; const urlParams = new URLSearchParams(search); - urlParams.set(QueryParams.startTime, currentTime.toString()); + urlParams.set(QueryParams.startTime, startTime.toString()); urlParams.set(QueryParams.endTime, endTime.toString()); urlParams.delete(QueryParams.relativeTime); const avialableParams = routeConfig[ROUTES.TRACE]; diff --git a/frontend/src/container/MetricsApplication/Tabs/util.ts b/frontend/src/container/MetricsApplication/Tabs/util.ts index e6d58831a0..e1f8164b09 100644 --- a/frontend/src/container/MetricsApplication/Tabs/util.ts +++ b/frontend/src/container/MetricsApplication/Tabs/util.ts @@ -65,11 +65,11 @@ export function onViewTracePopupClick({ stepInterval, }: OnViewTracePopupClickProps): VoidFunction { return (): void => { - const currentTime = timestamp; - const endTime = timestamp + (stepInterval || 60); + const endTime = timestamp; + const startTime = timestamp - (stepInterval || 60); const urlParams = new URLSearchParams(window.location.search); - urlParams.set(QueryParams.startTime, currentTime.toString()); + urlParams.set(QueryParams.startTime, startTime.toString()); urlParams.set(QueryParams.endTime, endTime.toString()); urlParams.delete(QueryParams.relativeTime); const avialableParams = routeConfig[ROUTES.TRACE]; diff --git a/frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.styles.scss b/frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.styles.scss new file mode 100644 index 0000000000..d7b9813c50 --- /dev/null +++ b/frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.styles.scss @@ -0,0 +1,96 @@ +.timezone-adaption { + padding: 16px; + background: var(--bg-ink-400); + border: 1px solid var(--bg-ink-500); + border-radius: 4px; + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + + &__title { + color: var(--bg-vanilla-300); + font-size: 14px; + font-weight: 500; + margin: 0; + } + + &__description { + color: var(--bg-vanilla-400); + font-size: 14px; + line-height: 20px; + margin: 0 0 12px 0; + } + + &__note { + display: flex; + align-items: center; + justify-content: space-between; + padding: 7.5px 12px; + background: rgba(78, 116, 248, 0.1); + border: 1px solid rgba(78, 116, 248, 0.1); + border-radius: 4px; + } + + &__bullet { + color: var(--bg-robin-400); + font-size: 16px; + line-height: 20px; + } + + &__note-text-container { + display: flex; + align-items: center; + gap: 10px; + } + + &__note-text { + display: flex; + align-items: center; + gap: 4px; + color: var(--bg-robin-400); + font-size: 14px; + line-height: 20px; + } + &__note-text-overridden { + display: flex; + align-items: center; + padding: 0 2px; + background: rgba(171, 189, 255, 0.04); + border-radius: 2px; + font-size: 12px; + line-height: 16px; + color: var(--bg-vanilla-100); + } + &__clear-override { + display: flex; + align-items: center; + gap: 6px; + background: transparent; + border: none; + padding: 0; + color: var(--bg-robin-300); + font-size: 12px; + line-height: 16px; /* 133.333% */ + letter-spacing: 0.12px; + cursor: pointer; + } +} + +.lightMode { + .timezone-adaption { + background-color: var(--bg-vanilla-100); + border-color: var(--bg-vanilla-300); + + &__title { + color: var(--text-ink-300); + } + + &__description { + color: var(--text-ink-400); + } + } +} diff --git a/frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.tsx b/frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.tsx new file mode 100644 index 0000000000..a716d4196d --- /dev/null +++ b/frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.tsx @@ -0,0 +1,82 @@ +import './TimezoneAdaptation.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Switch } from 'antd'; +import { Delete } from 'lucide-react'; +import { useTimezone } from 'providers/Timezone'; +import { useMemo } from 'react'; + +function TimezoneAdaptation(): JSX.Element { + const { + timezone, + browserTimezone, + updateTimezone, + isAdaptationEnabled, + setIsAdaptationEnabled, + } = useTimezone(); + + const isTimezoneOverridden = useMemo( + () => timezone.offset !== browserTimezone.offset, + [timezone, browserTimezone], + ); + + const getSwitchStyles = (): React.CSSProperties => ({ + backgroundColor: + isAdaptationEnabled && isTimezoneOverridden ? Color.BG_AMBER_400 : undefined, + }); + + const handleOverrideClear = (): void => { + updateTimezone(browserTimezone); + }; + + return ( +
+
+

Adapt to my timezone

+ +
+ +

+ Adapt the timestamps shown in the SigNoz console to my active timezone. +

+ +
+
+ + + {isTimezoneOverridden ? ( + <> + Your current timezone is overridden to + + {timezone.offset} + + + ) : ( + <> + You can override the timezone adaption for any view with the time + picker. + + )} + +
+ + {!!isTimezoneOverridden && ( + + )} +
+
+ ); +} + +export default TimezoneAdaptation; diff --git a/frontend/src/container/MySettings/index.tsx b/frontend/src/container/MySettings/index.tsx index de6c12eb53..59abd0cf73 100644 --- a/frontend/src/container/MySettings/index.tsx +++ b/frontend/src/container/MySettings/index.tsx @@ -7,6 +7,7 @@ import { LogOut, Moon, Sun } from 'lucide-react'; import { useState } from 'react'; import Password from './Password'; +import TimezoneAdaptation from './TimezoneAdaptation/TimezoneAdaptation'; import UserInfo from './UserInfo'; function MySettings(): JSX.Element { @@ -78,6 +79,8 @@ function MySettings(): JSX.Element {
+ +