Compare commits

..

35 Commits

Author SHA1 Message Date
Vikrant Gupta
6eeab69ccc Merge branch 'develop' into cleanup-app-routes 2024-12-11 08:24:22 +05:30
vikrantgupta25
566a594b60 chore: update test-utils 2024-12-10 20:14:01 +05:30
vikrantgupta25
7c47c77501 fix: signup for subsequent users 2024-12-10 17:18:13 +05:30
vikrantgupta25
c526068bb2 Merge remote-tracking branch 'origin' into cleanup-app-routes 2024-12-10 17:10:20 +05:30
vikrantgupta25
06de904101 fix: better handling and route to something_went_wrong in case of qs down 2024-12-10 16:56:06 +05:30
vikrantgupta25
59a48a080c fix: indefinite loading on manage licenses screen 2024-12-10 16:35:08 +05:30
vikrantgupta25
dadee0189c chore: cleanup the console logs 2024-12-10 16:04:56 +05:30
vikrantgupta25
8b279a06c1 feat: update the signup API to accept isAnonymous and hasOptedUpdates 2024-12-10 09:41:44 +05:30
vikrantgupta25
e42e7133b1 fix: billing test cases 2024-12-09 18:51:29 +05:30
vikrantgupta25
d277279189 fix: pipeline actions test cases 2024-12-09 15:50:04 +05:30
vikrantgupta25
27230a7935 Merge remote-tracking branch 'origin' into cleanup-app-routes 2024-12-09 14:36:55 +05:30
vikrantgupta25
c424434f35 Merge remote-tracking branch 'origin' into cleanup-app-routes 2024-12-08 00:24:34 +05:30
vikrantgupta25
a3ffdae449 fix: test cases 2024-12-08 00:24:13 +05:30
vikrantgupta25
9aba4fdf9d feat: delete snapshot 2024-12-07 20:16:11 +05:30
vikrantgupta25
814a1e6513 feat: fix some tests and corrected the redux mock 2024-12-07 20:14:20 +05:30
vikrantgupta25
06bddd57dc chore: fix more tests 2024-12-07 16:15:26 +05:30
vikrantgupta25
a651970de3 chore: fix more tests 2024-12-07 15:42:59 +05:30
vikrantgupta25
54de01af07 feat: should fix a lot of tests 2024-12-07 13:40:28 +05:30
vikrantgupta25
3dc695a5ae feat: add app provider to the test-utils 2024-12-06 19:34:09 +05:30
vikrantgupta25
b1d919c38f feat: fix all the files except signup 2024-12-06 18:51:33 +05:30
vikrantgupta25
38f03124e5 feat: some more corrections 2024-12-06 17:11:01 +05:30
vikrantgupta25
bee1fbaac3 fix: remove redux 2024-12-06 17:02:19 +05:30
vikrantgupta25
63f188e48e fix: entire application too much re-rendering 2024-12-06 15:42:22 +05:30
vikrantgupta25
c29cd45c24 feat: some refactoring for private routes 2024-12-06 14:31:41 +05:30
vikrantgupta25
6ce5cf3290 feat: parallel processing for sync calls for faster bootup of application 2024-12-06 13:55:00 +05:30
vikrantgupta25
32906ba561 fix: minor small re-render issue 2024-12-06 01:11:23 +05:30
vikrantgupta25
56ef96b88e feat: cleanup and syncronize side nav item 2024-12-06 01:06:58 +05:30
vikrantgupta25
a8b130863e Merge remote-tracking branch 'origin' into cleanup-app-routes 2024-12-06 00:18:06 +05:30
vikrantgupta25
83f4b190fa feat: cleanup the app layout file 2024-12-06 00:15:42 +05:30
vikrantgupta25
e1b6dbb431 feat: handle login and logout 2024-12-05 23:44:19 +05:30
vikrantgupta25
43ec8e16fe feat: cleanup the private routes 2024-12-05 19:00:35 +05:30
vikrantgupta25
9f14088e8f feat: syncronize the approutes file 2024-12-05 16:57:17 +05:30
vikrantgupta25
2f37f0e068 feat: remove redux from app state and private route 2024-12-05 16:11:24 +05:30
vikrantgupta25
b1bc70de1f feat: added global event listeners for after_login event 2024-12-05 14:04:49 +05:30
vikrantgupta25
d383beef8d feat: setup the app context to fetch users,licenses and feature flags 2024-12-05 12:41:07 +05:30
522 changed files with 3083 additions and 5671 deletions

View File

@@ -41,6 +41,7 @@ type APIHandlerOptions struct {
FluxInterval time.Duration
UseLogsNewSchema bool
UseTraceNewSchema bool
UseLicensesV3 bool
}
type APIHandler struct {
@@ -67,6 +68,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
FluxInterval: opts.FluxInterval,
UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
UseLicensesV3: opts.UseLicensesV3,
})
if err != nil {

View File

@@ -84,6 +84,13 @@ 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 {
@@ -95,7 +102,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().ActivateV3(r.Context(), l.Key)
license, apiError := ah.LM().Activate(r.Context(), l.Key)
if apiError != nil {
RespondError(w, apiError, nil)
return
@@ -258,12 +265,24 @@ func convertLicenseV3ToLicenseV2(licenses []*model.LicenseV3) []model.License {
}
func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
licensesV3, apierr := ah.LM().GetLicensesV3(r.Context())
if apierr != nil {
RespondError(w, apierr, nil)
return
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
}
licenses := convertLicenseV3ToLicenseV2(licensesV3)
resp := model.Licenses{
TrialStart: -1,

View File

@@ -78,6 +78,7 @@ type ServerOptions struct {
GatewayUrl string
UseLogsNewSchema bool
UseTraceNewSchema bool
UseLicensesV3 bool
}
// Server runs HTTP api service
@@ -134,7 +135,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// initiate license manager
lm, err := licensepkg.StartManager("sqlite", localDB)
lm, err := licensepkg.StartManager("sqlite", localDB, serverOptions.UseLicensesV3)
if err != nil {
return nil, err
}
@@ -273,6 +274,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
Gateway: gatewayProxy,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
UseLicensesV3: serverOptions.UseLicensesV3,
}
apiHandler, err := api.NewAPIHandler(apiOpts)

View File

@@ -2,6 +2,18 @@ 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"`

View File

@@ -10,6 +10,7 @@ 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"
@@ -38,6 +39,86 @@ 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

View File

@@ -18,13 +18,15 @@ import (
// Repo is license repo. stores license keys in a secured DB
type Repo struct {
db *sqlx.DB
db *sqlx.DB
useLicensesV3 bool
}
// NewLicenseRepo initiates a new license repo
func NewLicenseRepo(db *sqlx.DB) Repo {
func NewLicenseRepo(db *sqlx.DB, useLicensesV3 bool) Repo {
return Repo{
db: db,
db: db,
useLicensesV3: useLicensesV3,
}
}
@@ -110,16 +112,26 @@ 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) {
activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
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
}
if activeLicenseV3 == nil {
return nil, nil
active, err := r.GetActiveLicenseV2(ctx)
if err != nil {
return nil, err
}
activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
return activeLicenseV2, nil
return active, nil
}
func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {

View File

@@ -51,12 +51,12 @@ type Manager struct {
activeFeatures basemodel.FeatureSet
}
func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...basemodel.Feature) (*Manager, error) {
if LM != nil {
return LM, nil
}
repo := NewLicenseRepo(db)
repo := NewLicenseRepo(db, useLicensesV3)
err := repo.InitDB(dbType)
if err != nil {
@@ -67,7 +67,32 @@ func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*M
repo: &repo,
}
if err := m.start(features...); err != nil {
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 {
return m, err
}
LM = m
@@ -75,8 +100,16 @@ func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*M
}
// start loads active license in memory and initiates validator
func (lm *Manager) start(features ...basemodel.Feature) error {
return lm.LoadActiveLicenseV3(features...)
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) Stop() {
@@ -84,6 +117,31 @@ 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()
@@ -114,6 +172,29 @@ 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 {
@@ -184,6 +265,31 @@ 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!")
@@ -209,6 +315,73 @@ 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)
@@ -256,6 +429,50 @@ 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 {

View File

@@ -95,6 +95,7 @@ func main() {
var useLogsNewSchema bool
var useTraceNewSchema bool
var useLicensesV3 bool
var cacheConfigPath, fluxInterval string
var enableQueryServiceLogOTLPExport bool
var preferSpanMetrics bool
@@ -103,10 +104,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)")
@@ -120,7 +121,6 @@ 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,6 +148,7 @@ func main() {
GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
UseLicensesV3: useLicensesV3,
}
// Read the jwt secret key

View File

@@ -1,29 +1,13 @@
/* eslint-disable react-hooks/exhaustive-deps */
import getLocalStorageApi from 'api/browser/localstorage/get';
import getOrgUser from 'api/user/getOrgUser';
import loginApi from 'api/user/login';
import { Logout } from 'api/utils';
import Spinner from 'components/Spinner';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { isEmpty, isNull } from 'lodash-es';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { ReactChild, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactChild, useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { matchPath, Redirect, useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import { getInitialUserTokenRefreshToken } from 'store/utils';
import AppActions from 'types/actions';
import { UPDATE_USER_IS_FETCH } from 'types/actions/app';
import { matchPath, useLocation } from 'react-router-dom';
import { LicenseState, LicenseStatus } from 'types/api/licensesV3/getActive';
import { Organization } from 'types/api/user/getOrganization';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import { routePermission } from 'utils/permission';
@@ -32,27 +16,21 @@ import routes, {
oldNewRoutesMapping,
oldRoutes,
} from './routes';
import afterLogin from './utils';
function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const location = useLocation();
const { pathname } = location;
const [isLoading, setIsLoading] = useState<boolean>(true);
const {
org,
orgPreferences,
user,
role,
isUserFetching,
isUserFetchingError,
isLoggedIn: isLoggedInState,
isFetchingOrgPreferences,
} = useSelector<AppState, AppReducer>((state) => state.app);
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
licenses,
isFetchingLicenses,
activeLicenseV3,
isFetchingActiveLicenseV3,
} = useAppContext();
const mapRoutes = useMemo(
() =>
new Map(
@@ -65,52 +43,13 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
),
[pathname],
);
const isOnboardingComplete = useMemo(
() =>
orgPreferences?.find(
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
)?.value,
[orgPreferences],
);
const {
data: licensesData,
isFetching: isFetchingLicensesData,
} = useLicense();
const { t } = useTranslation(['common']);
const isCloudUserVal = isCloudUser();
const localStorageUserAuthToken = getInitialUserTokenRefreshToken();
const dispatch = useDispatch<Dispatch<AppActions>>();
const { notifications } = useNotifications();
const currentRoute = mapRoutes.get('current');
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
const currentRoute = mapRoutes.get('current');
const isCloudUserVal = isCloudUser();
const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
const isLocalStorageLoggedIn =
getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true';
const navigateToLoginIfNotLoggedIn = (isLoggedIn = isLoggedInState): void => {
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
if (!isLoggedIn) {
history.push(ROUTES.LOGIN, { from: pathname });
}
};
const { data: orgUsers, isLoading: isLoadingOrgUsers } = useQuery({
const { data: orgUsers, isFetching: isFetchingOrgUsers } = useQuery({
queryFn: () => {
if (orgData && orgData.id !== undefined) {
return getOrgUser({
@@ -123,7 +62,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
enabled: !isEmpty(orgData),
});
const checkFirstTimeUser = (): boolean => {
const checkFirstTimeUser = useCallback((): boolean => {
const users = orgUsers?.payload || [];
const remainingUsers = users.filter(
@@ -131,145 +70,65 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
);
return remainingUsers.length === 1;
};
}, [orgUsers?.payload]);
// Check if the onboarding should be shown based on the org users and onboarding completion status, wait for org users and preferences to load
const shouldShowOnboarding = (): boolean => {
// Only run this effect if the org users and preferences are loaded
if (!isLoadingOrgUsers && !isFetchingOrgPreferences) {
const isFirstUser = checkFirstTimeUser();
// Redirect to get started if it's not the first user or if the onboarding is complete
return isFirstUser && !isOnboardingComplete;
}
return false;
};
const handleRedirectForOrgOnboarding = (key: string): void => {
useEffect(() => {
if (
isLoggedInState &&
isCloudUserVal &&
!isFetchingOrgPreferences &&
!isLoadingOrgUsers &&
!isEmpty(orgUsers?.payload) &&
!isNull(orgPreferences)
orgPreferences &&
!isFetchingOrgUsers &&
orgUsers &&
orgUsers.payload
) {
if (key === 'ONBOARDING' && isOnboardingComplete) {
history.push(ROUTES.APPLICATION);
}
const isOnboardingComplete = orgPreferences?.find(
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
)?.value;
const isFirstTimeUser = checkFirstTimeUser();
if (isFirstTimeUser && !isOnboardingComplete) {
const isFirstUser = checkFirstTimeUser();
if (isFirstUser && !isOnboardingComplete) {
history.push(ROUTES.ONBOARDING);
}
}
if (!isCloudUserVal && key === 'ONBOARDING') {
history.push(ROUTES.APPLICATION);
}
};
const handleUserLoginIfTokenPresent = async (
key: keyof typeof ROUTES,
): Promise<void> => {
if (localStorageUserAuthToken?.refreshJwt) {
// localstorage token is present
// renew web access token
const response = await loginApi({
refreshToken: localStorageUserAuthToken?.refreshJwt,
});
if (response.statusCode === 200) {
const route = routePermission[key];
// get all resource and put it over redux
const userResponse = await afterLogin(
response.payload.userId,
response.payload.accessJwt,
response.payload.refreshJwt,
);
handleRedirectForOrgOnboarding(key);
if (
userResponse &&
route &&
route.find((e) => e === userResponse.payload.role) === undefined
) {
history.push(ROUTES.UN_AUTHORIZED);
}
} else {
Logout();
notifications.error({
message: response.error || t('something_went_wrong'),
});
}
}
};
const handlePrivateRoutes = async (
key: keyof typeof ROUTES,
): Promise<void> => {
if (
localStorageUserAuthToken &&
localStorageUserAuthToken.refreshJwt &&
isUserFetching
) {
handleUserLoginIfTokenPresent(key);
} else {
handleRedirectForOrgOnboarding(key);
navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
}
};
}, [
checkFirstTimeUser,
isCloudUserVal,
isFetchingOrgPreferences,
isFetchingOrgUsers,
orgPreferences,
orgUsers,
]);
const navigateToWorkSpaceBlocked = (route: any): void => {
const { path } = route;
if (path && path !== ROUTES.WORKSPACE_LOCKED) {
history.push(ROUTES.WORKSPACE_LOCKED);
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
}
};
useEffect(() => {
if (!isFetchingLicensesData) {
const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
if (!isFetchingLicenses) {
const currentRoute = mapRoutes.get('current');
const shouldBlockWorkspace = licenses?.workSpaceBlock;
if (shouldBlockWorkspace) {
navigateToWorkSpaceBlocked(currentRoute);
}
}
}, [isFetchingLicensesData]);
}, [isFetchingLicenses, licenses?.workSpaceBlock, mapRoutes]);
const navigateToWorkSpaceSuspended = (route: any): void => {
const { path } = route;
if (path && path !== ROUTES.WORKSPACE_SUSPENDED) {
history.push(ROUTES.WORKSPACE_SUSPENDED);
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
}
};
useEffect(() => {
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
const currentRoute = mapRoutes.get('current');
const shouldSuspendWorkspace =
activeLicenseV3.status === LicenseStatus.SUSPENDED &&
activeLicenseV3.state === LicenseState.PAYMENT_FAILED;
@@ -278,7 +137,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
navigateToWorkSpaceSuspended(currentRoute);
}
}
}, [isFetchingActiveLicenseV3, activeLicenseV3]);
}, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes]);
useEffect(() => {
if (org && org.length > 0 && org[0].id !== undefined) {
@@ -286,102 +145,44 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
}
}, [org]);
const handleRouting = (): void => {
const showOrgOnboarding = shouldShowOnboarding();
if (showOrgOnboarding && !isOnboardingComplete && isCloudUserVal) {
history.push(ROUTES.ONBOARDING);
} else {
history.push(ROUTES.APPLICATION);
}
};
useEffect(() => {
const { isPrivate } = currentRoute || {
isPrivate: false,
};
if (isLoggedInState && role && role !== 'ADMIN') {
setIsLoading(false);
}
if (!isPrivate) {
setIsLoading(false);
}
if (
!isEmpty(user) &&
!isFetchingOrgPreferences &&
!isEmpty(orgUsers?.payload) &&
!isNull(orgPreferences)
) {
setIsLoading(false);
}
}, [currentRoute, user, role, orgUsers, orgPreferences]);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
(async (): Promise<void> => {
try {
if (isOldRoute) {
const redirectUrl = oldNewRoutesMapping[pathname];
// if it is an old route navigate to the new route
if (isOldRoute) {
const redirectUrl = oldNewRoutesMapping[pathname];
const newLocation = {
...location,
pathname: redirectUrl,
};
history.replace(newLocation);
}
const newLocation = {
...location,
pathname: redirectUrl,
};
history.replace(newLocation);
}
if (currentRoute) {
const { isPrivate, key } = currentRoute;
if (isPrivate && key !== String(ROUTES.WORKSPACE_LOCKED)) {
handlePrivateRoutes(key);
} else {
// no need to fetch the user and make user fetching false
if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') {
handleRouting();
}
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
}
} else if (pathname === ROUTES.HOME_PAGE) {
// routing to application page over root page
if (isLoggedInState) {
handleRouting();
} else {
navigateToLoginIfNotLoggedIn();
// if the current route
if (currentRoute) {
const { isPrivate, key } = currentRoute;
if (isPrivate) {
if (isLoggedInState) {
const route = routePermission[key];
if (route && route.find((e) => e === user.role) === undefined) {
history.push(ROUTES.UN_AUTHORIZED);
}
} else {
// not found
navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
history.push(ROUTES.LOGIN);
}
} catch (error) {
// something went wrong
history.push(ROUTES.SOMETHING_WENT_WRONG);
} else if (isLoggedInState) {
history.push(ROUTES.APPLICATION);
} else {
// do nothing as the unauthenticated routes are LOGIN and SIGNUP and the LOGIN container takes care of routing to signup if
// setup is not completed
}
})();
}, [
dispatch,
isLoggedInState,
currentRoute,
licensesData,
orgUsers,
orgPreferences,
]);
if (isUserFetchingError) {
return <Redirect to={ROUTES.SOMETHING_WENT_WRONG} />;
}
if (isUserFetching || isLoading) {
return <Spinner tip="Loading..." />;
}
} else if (isLoggedInState) {
history.push(ROUTES.APPLICATION);
} else {
history.push(ROUTES.LOGIN);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [licenses, isLoggedInState, pathname, user, isOldRoute, currentRoute]);
// NOTE: disabling this rule as there is no need to have div
// eslint-disable-next-line react/jsx-no-useless-fragment

View File

@@ -1,8 +1,6 @@
import { ConfigProvider } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features';
@@ -11,35 +9,21 @@ import ROUTES from 'constants/routes';
import AppLayout from 'container/AppLayout';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useIsDarkMode, useThemeConfig } from 'hooks/useDarkMode';
import { THEME_MODE } from 'hooks/useDarkMode/constant';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import { useThemeConfig } from 'hooks/useDarkMode';
import { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import { NotificationProvider } from 'hooks/useNotifications';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import history from 'lib/history';
import { identity, pick, pickBy } from 'lodash-es';
import { identity, pickBy } from 'lodash-es';
import posthog from 'posthog-js';
import AlertRuleProvider from 'providers/Alert';
import { AppProvider } from 'providers/App/App';
import { useAppContext } from 'providers/App/App';
import { IUser } from 'providers/App/types';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { Route, Router, Switch } from 'react-router-dom';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { Redirect, Route, Router, Switch } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
UPDATE_FEATURE_FLAG_RESPONSE,
UPDATE_IS_FETCHING_ORG_PREFERENCES,
UPDATE_ORG_PREFERENCES,
} from 'types/actions/app';
import AppReducer, { User } from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app';
import PrivateRoute from './Private';
@@ -51,14 +35,20 @@ import defaultRoutes, {
function App(): JSX.Element {
const themeConfig = useThemeConfig();
const { data: licenseData } = useLicense();
const {
licenses,
user,
isFetchingUser,
isFetchingLicenses,
isFetchingFeatureFlags,
userFetchError,
licensesFetchError,
featureFlagsFetchError,
isLoggedIn: isLoggedInState,
featureFlags,
org,
} = useAppContext();
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
const { role, isLoggedIn: isLoggedInState, user, org } = useSelector<
AppState,
AppReducer
>((state) => state.app);
const dispatch = useDispatch<Dispatch<AppActions>>();
const { trackPageView } = useAnalytics();
@@ -66,164 +56,114 @@ function App(): JSX.Element {
const isCloudUserVal = isCloudUser();
const isDarkMode = useIsDarkMode();
const enableAnalytics = useCallback(
(user: IUser): void => {
// wait for the required data to be loaded before doing init for anything!
if (!isFetchingLicenses && licenses && org) {
const orgName =
org && Array.isArray(org) && org.length > 0 ? org[0].name : '';
const isChatSupportEnabled =
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
const { name, email, role } = user;
const isPremiumSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const identifyPayload = {
email,
name,
company_name: orgName,
role,
source: 'signoz-ui',
};
const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({
queryFn: () => getAllOrgPreferences(),
queryKey: ['getOrgPreferences'],
enabled: isLoggedInState && role === USER_ROLES.ADMIN,
});
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
const domain = extractDomain(email);
const hostNameParts = hostname.split('.');
const groupTraits = {
name: orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
};
window.analytics.identify(email, sanitizedIdentifyPayload);
window.analytics.group(domain, groupTraits);
posthog?.identify(email, {
email,
name,
orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!licenses?.trialConvertedToSubscription,
});
posthog?.group('company', domain, {
name: orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!licenses?.trialConvertedToSubscription,
});
}
},
[hostname, isFetchingLicenses, licenses, org],
);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
if (orgPreferences && !isLoadingOrgPreferences) {
dispatch({
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
payload: {
isFetchingOrgPreferences: false,
},
});
dispatch({
type: UPDATE_ORG_PREFERENCES,
payload: {
orgPreferences: orgPreferences.payload?.data || null,
},
});
}
}, [orgPreferences, dispatch, isLoadingOrgPreferences]);
useEffect(() => {
if (isLoggedInState && role !== USER_ROLES.ADMIN) {
dispatch({
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
payload: {
isFetchingOrgPreferences: false,
},
});
}
}, [isLoggedInState, role, dispatch]);
const featureResponse = useGetFeatureFlag((allFlags) => {
dispatch({
type: UPDATE_FEATURE_FLAG_RESPONSE,
payload: {
featureFlag: allFlags,
refetch: featureResponse.refetch,
},
});
const isOnboardingEnabled =
allFlags.find((flag) => flag.name === FeatureKeys.ONBOARDING)?.active ||
false;
if (!isOnboardingEnabled || !isCloudUserVal) {
const newRoutes = routes.filter(
(route) => route?.path !== ROUTES.GET_STARTED,
);
setRoutes(newRoutes);
}
});
const isOnBasicPlan =
licenseData?.payload?.licenses?.some(
(license) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
) || licenseData?.payload?.licenses === null;
const enableAnalytics = (user: User): void => {
const orgName =
org && Array.isArray(org) && org.length > 0 ? org[0].name : '';
const { name, email } = user;
const identifyPayload = {
email,
name,
company_name: orgName,
role,
source: 'signoz-ui',
};
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
const domain = extractDomain(email);
const hostNameParts = hostname.split('.');
const groupTraits = {
name: orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
};
window.analytics.identify(email, sanitizedIdentifyPayload);
window.analytics.group(domain, groupTraits);
posthog?.identify(email, {
email,
name,
orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!licenseData?.payload?.trialConvertedToSubscription,
});
posthog?.group('company', domain, {
name: orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!licenseData?.payload?.trialConvertedToSubscription,
});
};
useEffect(() => {
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
if (
isLoggedInState &&
!isFetchingLicenses &&
licenses &&
!isFetchingUser &&
user &&
user.userId &&
user.email &&
!isIdentifiedUser
!!user.email
) {
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
const isOnBasicPlan =
licenses.licenses?.some(
(license) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
) || licenses.licenses === null;
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
}
let updatedRoutes = defaultRoutes;
// if the user is a cloud user
if (isCloudUserVal || isEECloudUser()) {
// if the user is on basic plan then remove billing
if (isOnBasicPlan) {
updatedRoutes = updatedRoutes.filter(
(route) => route?.path !== ROUTES.BILLING,
);
}
// always add support route for cloud users
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
} else {
// if not a cloud user then remove billing and add list licenses route
updatedRoutes = updatedRoutes.filter(
(route) => route?.path !== ROUTES.BILLING,
);
updatedRoutes = [...updatedRoutes, LIST_LICENSES];
}
setRoutes(updatedRoutes);
}
if (
isOnBasicPlan ||
(isLoggedInState && role && role !== 'ADMIN') ||
!(isCloudUserVal || isEECloudUser())
) {
const newRoutes = routes.filter((route) => route?.path !== ROUTES.BILLING);
setRoutes(newRoutes);
}
if (isCloudUserVal || isEECloudUser()) {
const newRoutes = [...routes, SUPPORT_ROUTE];
setRoutes(newRoutes);
} else {
const newRoutes = [...routes, LIST_LICENSES];
setRoutes(newRoutes);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoggedInState, isOnBasicPlan, user]);
}, [
isLoggedInState,
user,
licenses,
isCloudUserVal,
isFetchingLicenses,
isFetchingUser,
]);
useEffect(() => {
if (pathname === ROUTES.ONBOARDING) {
@@ -237,99 +177,113 @@ function App(): JSX.Element {
}
trackPageView(pathname);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);
}, [pathname, trackPageView]);
useEffect(() => {
const showAddCreditCardModal =
!isPremiumSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
// feature flag shouldn't be loading and featureFlags or fetchError any one of this should be true indicating that req is complete
// licenses should also be present. there is no check for licenses for loading and error as that is mandatory if not present then routing
// to something went wrong which would ideally need a reload.
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
licenses
) {
let isChatSupportEnabled = false;
let isPremiumSupportEnabled = false;
if (featureFlags && featureFlags.length > 0) {
isChatSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
?.active || false;
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
window.Intercom('boot', {
app_id: process.env.INTERCOM_APP_ID,
email: user?.email || '',
name: user?.name || '',
});
isPremiumSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)
?.active || false;
}
const showAddCreditCardModal =
!isPremiumSupportEnabled && !licenses.trialConvertedToSubscription;
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
window.Intercom('boot', {
app_id: process.env.INTERCOM_APP_ID,
email: user?.email || '',
name: user?.name || '',
});
}
}
}, [
isLoggedInState,
isChatSupportEnabled,
user,
licenseData,
isPremiumSupportEnabled,
pathname,
licenses?.trialConvertedToSubscription,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
licenses,
]);
useEffect(() => {
if (user && user?.email && user?.userId && user?.name) {
try {
const isThemeAnalyticsSent = getLocalStorageApi(
LOCALSTORAGE.THEME_ANALYTICS_V1,
);
if (!isThemeAnalyticsSent) {
logEvent('Theme Analytics', {
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
user: pick(user, ['email', 'userId', 'name']),
org,
});
setLocalStorageApi(LOCALSTORAGE.THEME_ANALYTICS_V1, 'true');
}
} catch {
console.error('Failed to parse local storage theme analytics event');
}
}
if (isCloudUserVal && user && user.email) {
if (!isFetchingUser && isCloudUserVal && user && user.email) {
enableAnalytics(user);
}
}, [user, isFetchingUser, isCloudUserVal, enableAnalytics]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]);
// if the user is in logged in state
if (isLoggedInState) {
// if the setup calls are loading then return a spinner
if (isFetchingLicenses || isFetchingUser || isFetchingFeatureFlags) {
return <Spinner tip="Loading..." />;
}
useEffect(() => {
console.info('We are hiring! https://jobs.gem.com/signoz');
}, []);
// if the required calls fails then return a something went wrong error
// this needs to be on top of data missing error because if there is an error, data will never be loaded and it will
// move to indefinitive loading
if (userFetchError || licensesFetchError) {
return <Redirect to={ROUTES.SOMETHING_WENT_WRONG} />;
}
// if all of the data is not set then return a spinner, this is required because there is some gap between loading states and data setting
if (!licenses || !user.email || !featureFlags) {
return <Spinner tip="Loading..." />;
}
}
return (
<AppProvider>
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<CompatRouter>
<NotificationProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<CompatRouter>
<NotificationProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</NotificationProvider>
</CompatRouter>
</Router>
</ConfigProvider>
</AppProvider>
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</NotificationProvider>
</CompatRouter>
</Router>
</ConfigProvider>
);
}

View File

@@ -1,92 +1,28 @@
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import getUserApi from 'api/user/getUser';
import { Logout } from 'api/utils';
import { LOCALSTORAGE } from 'constants/localStorage';
import store from 'store';
import AppActions from 'types/actions';
import {
LOGGED_IN,
UPDATE_USER,
UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
UPDATE_USER_IS_FETCH,
} from 'types/actions/app';
import { SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/user/getUser';
const afterLogin = async (
const afterLogin = (
userId: string,
authToken: string,
refreshToken: string,
): Promise<SuccessResponse<PayloadProps> | undefined> => {
interceptorRejected?: boolean,
): void => {
setLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN, authToken);
setLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN, refreshToken);
setLocalStorageApi(LOCALSTORAGE.USER_ID, userId);
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
store.dispatch<AppActions>({
type: UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
payload: {
accessJwt: authToken,
refreshJwt: refreshToken,
},
});
const [getUserResponse] = await Promise.all([
getUserApi({
userId,
token: authToken,
}),
]);
if (getUserResponse.statusCode === 200 && getUserResponse.payload) {
store.dispatch<AppActions>({
type: LOGGED_IN,
payload: {
isLoggedIn: true,
},
});
const { payload } = getUserResponse;
store.dispatch<AppActions>({
type: UPDATE_USER,
payload: {
ROLE: payload.role,
email: payload.email,
name: payload.name,
orgName: payload.organization,
profilePictureURL: payload.profilePictureURL,
userId: payload.id,
orgId: payload.orgId,
userFlags: payload.flags,
},
});
const isLoggedInLocalStorage = getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN);
if (isLoggedInLocalStorage === null) {
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
}
store.dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
return getUserResponse;
if (!interceptorRejected) {
window.dispatchEvent(
new CustomEvent('AFTER_LOGIN', {
detail: {
accessJWT: authToken,
refreshJWT: refreshToken,
id: userId,
},
}),
);
}
store.dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
Logout();
return undefined;
};
export default afterLogin;

View File

@@ -7,7 +7,6 @@ import afterLogin from 'AppRoutes/utils';
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import store from 'store';
import apiV1, {
apiAlertManager,
@@ -26,10 +25,7 @@ const interceptorsResponse = (
const interceptorsRequestResponse = (
value: InternalAxiosRequestConfig,
): InternalAxiosRequestConfig => {
const token =
store.getState().app.user?.accessJwt ||
getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) ||
'';
const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || '';
if (value && value.headers) {
value.headers.Authorization = token ? `Bearer ${token}` : '';
@@ -47,41 +43,36 @@ const interceptorRejected = async (
// reject the refresh token error
if (response.status === 401 && response.config.url !== '/login') {
const response = await loginApi({
refreshToken: store.getState().app.user?.refreshJwt,
refreshToken: getLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN) || '',
});
if (response.statusCode === 200) {
const user = await afterLogin(
afterLogin(
response.payload.userId,
response.payload.accessJwt,
response.payload.refreshJwt,
true,
);
if (user) {
const reResponse = await axios(
`${value.config.baseURL}${value.config.url?.substring(1)}`,
{
method: value.config.method,
headers: {
...value.config.headers,
Authorization: `Bearer ${response.payload.accessJwt}`,
},
data: {
...JSON.parse(value.config.data || '{}'),
},
const reResponse = await axios(
`${value.config.baseURL}${value.config.url?.substring(1)}`,
{
method: value.config.method,
headers: {
...value.config.headers,
Authorization: `Bearer ${response.payload.accessJwt}`,
},
);
data: {
...JSON.parse(value.config.data || '{}'),
},
},
);
if (reResponse.status === 200) {
return await Promise.resolve(reResponse);
}
Logout();
return await Promise.reject(reResponse);
if (reResponse.status === 200) {
return await Promise.resolve(reResponse);
}
Logout();
return await Promise.reject(value);
return await Promise.reject(reResponse);
}
Logout();
}

View File

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

View File

@@ -1,28 +1,18 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/user/getUser';
const getUser = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(`/user/${props.userId}`, {
headers: {
Authorization: `bearer ${props.token}`,
},
});
const response = await axios.get(`/user/${props.userId}`);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
};
export default getUser;

View File

@@ -2,14 +2,6 @@ import deleteLocalStorageKey from 'api/browser/localstorage/remove';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import store from 'store';
import {
LOGGED_IN,
UPDATE_ORG,
UPDATE_USER,
UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
UPDATE_USER_ORG_ROLE,
} from 'types/actions/app';
export const Logout = (): void => {
deleteLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN);
@@ -19,50 +11,9 @@ export const Logout = (): void => {
deleteLocalStorageKey(LOCALSTORAGE.LOGGED_IN_USER_EMAIL);
deleteLocalStorageKey(LOCALSTORAGE.LOGGED_IN_USER_NAME);
deleteLocalStorageKey(LOCALSTORAGE.CHAT_SUPPORT);
deleteLocalStorageKey(LOCALSTORAGE.USER_ID);
store.dispatch({
type: LOGGED_IN,
payload: {
isLoggedIn: false,
},
});
store.dispatch({
type: UPDATE_USER_ORG_ROLE,
payload: {
org: null,
role: null,
},
});
store.dispatch({
type: UPDATE_USER,
payload: {
ROLE: 'VIEWER',
email: '',
name: '',
orgId: '',
orgName: '',
profilePictureURL: '',
userId: '',
userFlags: {},
},
});
store.dispatch({
type: UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
payload: {
accessJwt: '',
refreshJwt: '',
},
});
store.dispatch({
type: UPDATE_ORG,
payload: {
org: [],
},
});
window.dispatchEvent(new CustomEvent('LOGOUT'));
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore

View File

@@ -2,9 +2,9 @@ import { Button, Modal, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { CreditCard, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
@@ -20,16 +20,16 @@ export default function ChatSupportGateway(): JSX.Element {
false,
);
const { data: licenseData, isFetching } = useLicense();
const { licenses, isFetchingLicenses } = useAppContext();
useEffect(() => {
const activeValidLicense =
licenseData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
if (!isFetchingLicenses && licenses) {
const activeValidLicense =
licenses.licenses?.find((license) => license.isCurrent === true) || null;
setActiveLicense(activeValidLicense);
}, [licenseData, isFetching]);
setActiveLicense(activeValidLicense);
}
}, [licenses, isFetchingLicenses]);
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,

View File

@@ -40,7 +40,7 @@
&.custom-time {
input:not(:focus) {
min-width: 280px;
min-width: 240px;
}
}
@@ -119,69 +119,3 @@
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%);
}
}

View File

@@ -15,14 +15,11 @@ 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';
@@ -31,8 +28,6 @@ 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;
@@ -86,42 +81,11 @@ function CustomTimePicker({
const location = useLocation();
const [isInputFocused, setIsInputFocused] = useState(false);
const [activeView, setActiveView] = useState<ViewType>(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;
}
@@ -167,7 +131,6 @@ function CustomTimePicker({
setOpen(newOpen);
if (!newOpen) {
setCustomDTPickerVisible?.(false);
setActiveView('datetime');
}
};
@@ -281,7 +244,6 @@ function CustomTimePicker({
const handleFocus = (): void => {
setIsInputFocused(true);
setActiveView('datetime');
};
const handleBlur = (): void => {
@@ -318,10 +280,6 @@ function CustomTimePicker({
handleGoLive={defaultTo(handleGoLive, noop)}
options={items}
selectedTime={selectedTime}
activeView={activeView}
setActiveView={setActiveView}
setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter}
/>
) : (
content
@@ -358,24 +316,12 @@ function CustomTimePicker({
)
}
suffix={
<>
{!!isTimezoneOverridden && activeTimezoneOffset && (
<div
className="timezone-badge"
onClick={(e): void => {
e.stopPropagation();
handleViewChange('timezone');
setIsOpenedFromFooter(false);
}}
>
<span>{activeTimezoneOffset}</span>
</div>
)}
<ChevronDown
size={14}
onClick={(): void => handleViewChange('datetime')}
/>
</>
<ChevronDown
size={14}
onClick={(): void => {
setOpen(!open);
}}
/>
}
/>
</Popover>

View File

@@ -1,6 +1,5 @@
import './CustomTimePicker.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import cx from 'classnames';
import ROUTES from 'constants/routes';
@@ -10,13 +9,10 @@ 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[];
@@ -30,13 +26,8 @@ interface CustomTimePickerPopoverContentProps {
onSelectHandler: (label: string, value: string) => void;
handleGoLive: () => void;
selectedTime: string;
activeView: 'datetime' | 'timezone';
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
isOpenedFromFooter: boolean;
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function CustomTimePickerPopoverContent({
options,
setIsOpen,
@@ -46,18 +37,12 @@ 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 (
@@ -78,99 +63,55 @@ function CustomTimePickerPopoverContent({
);
}
const handleTimezoneHintClick = (): void => {
setActiveView('timezone');
setIsOpenedFromFooter(true);
};
if (activeView === 'timezone') {
return (
<div className="date-time-popover">
<TimezonePicker
setActiveView={setActiveView}
setIsOpen={setIsOpen}
isOpenedFromFooter={isOpenedFromFooter}
/>
</div>
);
}
return (
<>
<div className="date-time-popover">
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
<div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
/>
) : (
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
)}
</div>
</div>
<div className="date-time-popover__footer">
<div className="timezone-container">
<Clock
color={Color.BG_VANILLA_400}
className="timezone-container__clock-icon"
height={12}
width={12}
/>
<span className="timezone__icon">Current timezone</span>
<div></div>
<button
type="button"
className="timezone"
onClick={handleTimezoneHintClick}
<div className="date-time-popover">
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
<span>{activeTimezoneOffset}</span>
<PenLine
color={Color.BG_VANILLA_100}
className="timezone__icon"
size={10}
/>
</button>
</div>
{option.label}
</Button>
))}
</div>
</>
<div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
/>
) : (
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -3,8 +3,7 @@ import './RangePickerModal.styles.scss';
import { DatePicker } from 'antd';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { useTimezone } from 'providers/Timezone';
import dayjs, { Dayjs } from 'dayjs';
import { Dispatch, SetStateAction } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -32,10 +31,7 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
(state) => state.globalTime,
);
// 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 disabledDate = (current: Dayjs): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
@@ -53,22 +49,16 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
}
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
};
const { timezone } = useTimezone();
return (
<div className="custom-date-picker">
<RangePicker
disabledDate={disabledDate}
allowClear
showTime
format="YYYY-MM-DD hh:mm A"
onOk={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && {
defaultValue: [
dayjs(minTime / 1000000).tz(timezone.value),
dayjs(maxTime / 1000000).tz(timezone.value),
],
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
})}
/>
</div>

View File

@@ -1,166 +0,0 @@
// 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);
}
}
}
}

View File

@@ -1,201 +0,0 @@
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<SetStateAction<boolean>>;
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
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 (
<div className="timezone-picker__search">
<div className="timezone-picker__input-container">
<Search
color={Color.BG_VANILLA_400}
className="search-icon"
height={ICON_SIZE}
width={ICON_SIZE}
/>
<Input
type="text"
className="timezone-picker__input"
placeholder="Search timezones..."
value={value}
onChange={(e): void => onChange(e.target.value)}
onKeyDown={handleKeyDown}
tabIndex={0}
autoFocus
/>
</div>
<kbd className="timezone-picker__esc-key">esc</kbd>
</div>
);
}
function TimezoneItem({
timezone,
isSelected = false,
onClick,
}: TimezoneItemProps): JSX.Element {
return (
<button
type="button"
className={cx('timezone-picker__item', {
selected: isSelected,
'has-divider': timezone.hasDivider,
})}
onClick={onClick}
>
<div className="timezone-name-wrapper">
<div className="timezone-name-wrapper__selected-icon">
{isSelected && (
<Check
className="check-icon"
color={Color.BG_VANILLA_100}
height={ICON_SIZE}
width={ICON_SIZE}
/>
)}
</div>
<div className="timezone-picker__name">{timezone.name}</div>
</div>
<div className="timezone-picker__offset">{timezone.offset}</div>
</button>
);
}
TimezoneItem.defaultProps = {
isSelected: false,
onClick: undefined,
};
interface TimezonePickerProps {
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
setIsOpen: Dispatch<SetStateAction<boolean>>;
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<string>(
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 (
<div className="timezone-picker">
<SearchBar
value={searchTerm}
onChange={setSearchTerm}
setIsOpen={setIsOpen}
setActiveView={setActiveView}
isOpenedFromFooter={isOpenedFromFooter}
/>
<div className="timezone-picker__list">
{getFilteredTimezones(searchTerm).map((timezone) => (
<TimezoneItem
key={timezone.value}
timezone={timezone}
isSelected={timezone.name === selectedTimezone}
onClick={(): void => handleTimezoneSelect(timezone)}
/>
))}
</div>
</div>
);
}
export default TimezonePicker;

View File

@@ -1,152 +0,0 @@
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();

View File

@@ -1,15 +1,22 @@
import { render } from '@testing-library/react';
import { Table } from 'antd';
import { matchMedia } from 'container/PipelinePage/tests/AddNewPipeline.test';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import i18n from 'ReactI18';
import store from 'store';
import DraggableTableRow from '..';
beforeAll(() => {
matchMedia();
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
});
jest.mock('uplot', () => {
@@ -34,18 +41,14 @@ jest.mock('react-dnd', () => ({
describe('DraggableTableRow Snapshot test', () => {
it('should render DraggableTableRow', async () => {
const { asFragment } = render(
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<Table
components={{
body: {
row: DraggableTableRow,
},
}}
pagination={false}
/>
</I18nextProvider>
</Provider>,
<Table
components={{
body: {
row: DraggableTableRow,
},
}}
pagination={false}
/>,
);
expect(asFragment()).toMatchSnapshot();
});

View File

@@ -99,5 +99,3 @@ exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
</div>
</DocumentFragment>
`;
exports[`PipelinePage container test should render AddNewPipeline section 1`] = `<DocumentFragment />`;

View File

@@ -1,5 +1,4 @@
import {
_adapters,
BarController,
BarElement,
CategoryScale,
@@ -19,10 +18,8 @@ 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,
@@ -65,17 +62,6 @@ 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<ToggleGraphProps | undefined, GraphProps>(
(
{
@@ -94,13 +80,11 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
dragSelectColor,
},
ref,
// eslint-disable-next-line sonarjs/cognitive-complexity
): JSX.Element => {
const nearestDatasetIndex = useRef<null | number>(null);
const chartRef = useRef<HTMLCanvasElement>(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
@@ -128,22 +112,6 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
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();
@@ -164,7 +132,6 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
isStacked,
onClickHandler,
data,
timezone,
);
const chartHasData = hasData(data);
@@ -199,7 +166,6 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
isStacked,
onClickHandler,
data,
timezone,
name,
type,
]);

View File

@@ -1,6 +1,5 @@
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';
@@ -51,7 +50,6 @@ export const getGraphOptions = (
isStacked: boolean | undefined,
onClickHandler: GraphOnClickHandler | undefined,
data: ChartData,
timezone: Timezone,
// eslint-disable-next-line sonarjs/cognitive-complexity
): CustomChartOptions => ({
animation: {
@@ -99,7 +97,7 @@ export const getGraphOptions = (
callbacks: {
title(context): string | string[] {
const date = dayjs(context[0].parsed.x);
return date.tz(timezone.value).format('MMM DD, YYYY, HH:mm:ss');
return date.format('MMM DD, YYYY, HH:mm:ss');
},
label(context): string | string[] {
let label = context.dataset.label || '';

View File

@@ -5,18 +5,16 @@ import { Button, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useNotifications } from 'hooks/useNotifications';
import { CheckCircle2, HandPlatter } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
export default function WaitlistFragment({
entityType,
}: {
entityType: string;
}): JSX.Element {
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const { t } = useTranslation(['infraMonitoring']);
const { notifications } = useNotifications();

View File

@@ -6,12 +6,11 @@ import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { defaultTo } from 'lodash-es';
import { CreditCard, HelpCircle, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useAppContext } from 'providers/App/App';
import { useEffect, useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
@@ -39,31 +38,79 @@ function LaunchChatSupport({
onHoverText = '',
intercomMessageDisabled = false,
}: LaunchChatSupportProps): JSX.Element | null {
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
const isCloudUserVal = isCloudUser();
const { notifications } = useNotifications();
const { data: licenseData, isFetching } = useLicense();
const {
licenses,
isFetchingLicenses,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
isLoggedIn,
} = useAppContext();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
const { pathname } = useLocation();
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const showAddCreditCardModal =
!isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
const isChatSupportEnabled = useMemo(() => {
if (!isFetchingFeatureFlags && (featureFlags || featureFlagsFetchError)) {
let isChatSupportEnabled = false;
if (featureFlags && featureFlags.length > 0) {
isChatSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
?.active || false;
}
return isChatSupportEnabled;
}
return false;
}, [featureFlags, featureFlagsFetchError, isFetchingFeatureFlags]);
const showAddCreditCardModal = useMemo(() => {
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
licenses
) {
let isChatSupportEnabled = false;
let isPremiumSupportEnabled = false;
const isCloudUserVal = isCloudUser();
if (featureFlags && featureFlags.length > 0) {
isChatSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
?.active || false;
isPremiumSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)
?.active || false;
}
return (
isLoggedIn &&
!isPremiumSupportEnabled &&
isChatSupportEnabled &&
!licenses.trialConvertedToSubscription &&
isCloudUserVal
);
}
return false;
}, [
featureFlags,
featureFlagsFetchError,
isFetchingFeatureFlags,
isLoggedIn,
licenses,
]);
useEffect(() => {
const activeValidLicense =
licenseData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
}, [licenseData, isFetching]);
if (!isFetchingLicenses && licenses) {
const activeValidLicense =
licenses.licenses?.find((license) => license.isCurrent === true) || null;
setActiveLicense(activeValidLicense);
}
}, [isFetchingLicenses, licenses]);
const handleFacingIssuesClick = (): void => {
if (showAddCreditCardModal) {

View File

@@ -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,20 +174,12 @@ function ListLogView({
[selectedFields],
);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const timestampValue = useMemo(
() =>
typeof flattenLogData.timestamp === 'string'
? formatTimezoneAdjustedTimestamp(
flattenLogData.timestamp,
'YYYY-MM-DD HH:mm:ss.SSS',
)
: formatTimezoneAdjustedTimestamp(
flattenLogData.timestamp / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
),
[flattenLogData.timestamp, formatTimezoneAdjustedTimestamp],
? 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],
);
const logType = getLogIndicatorType(logData);

View File

@@ -6,6 +6,7 @@ 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';
@@ -13,7 +14,6 @@ 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,24 +89,16 @@ function RawLogView({
attributesText += ' | ';
}
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const text = useMemo(() => {
const date =
typeof data.timestamp === 'string'
? formatTimezoneAdjustedTimestamp(data.timestamp, 'YYYY-MM-DD HH:mm:ss.SSS')
: formatTimezoneAdjustedTimestamp(
data.timestamp / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
);
? dayjs(data.timestamp)
: dayjs(data.timestamp / 1e6);
return `${date} | ${attributesText} ${data.body}`;
}, [
data.timestamp,
data.body,
attributesText,
formatTimezoneAdjustedTimestamp,
]);
return `${date.format('YYYY-MM-DD HH:mm:ss.SSS')} | ${attributesText} ${
data.body
}`;
}, [data.timestamp, data.body, attributesText]);
const handleClickExpand = useCallback(() => {
if (activeContextLog || isReadOnly) return;

View File

@@ -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,8 +44,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
logs,
]);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id')
@@ -83,11 +81,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
const date =
typeof field === 'string'
? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS')
: formatTimezoneAdjustedTimestamp(
field / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
);
? dayjs(field).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(field / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return {
children: (
<div className="table-timestamp">
@@ -130,15 +125,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
},
...(appendTo === 'end' ? fieldColumns : []),
];
}, [
fields,
isListViewPanel,
appendTo,
isDarkMode,
linesPerRow,
fontSize,
formatTimezoneAdjustedTimestamp,
]);
}, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]);
return { columns, dataSource: flattenLogData };
};

View File

@@ -1,31 +1,10 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import NotFoundImage from 'assets/NotFound';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { LOGGED_IN } from 'types/actions/app';
import { defaultText } from './constant';
import { Button, Container, Text, TextContainer } from './styles';
function NotFound({ text = defaultText }: Props): JSX.Element {
const dispatch = useDispatch<Dispatch<AppActions>>();
const isLoggedIn = getLocalStorageKey(LOCALSTORAGE.IS_LOGGED_IN);
const onClickHandler = useCallback(() => {
if (isLoggedIn) {
dispatch({
type: LOGGED_IN,
payload: {
isLoggedIn: true,
},
});
}
}, [dispatch, isLoggedIn]);
return (
<Container>
<NotFoundImage />
@@ -35,7 +14,7 @@ function NotFound({ text = defaultText }: Props): JSX.Element {
<Text>Page Not Found</Text>
</TextContainer>
<Button onClick={onClickHandler} to={ROUTES.APPLICATION} tabIndex={0}>
<Button to={ROUTES.APPLICATION} tabIndex={0}>
Return To Services Page
</Button>
</Container>

View File

@@ -1,40 +1,28 @@
import { Button, Space } from 'antd';
import setFlags from 'api/user/setFlags';
import MessageTip from 'components/MessageTip';
import { useAppContext } from 'providers/App/App';
import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_USER_FLAG } from 'types/actions/app';
import { UserFlags } from 'types/api/user/setFlags';
import AppReducer from 'types/reducer/app';
import ReleaseNoteProps from '../ReleaseNoteProps';
export default function ReleaseNote0120({
release,
}: ReleaseNoteProps): JSX.Element | null {
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const dispatch = useDispatch<Dispatch<AppActions>>();
const { user, setUserFlags } = useAppContext();
const handleDontShow = useCallback(async (): Promise<void> => {
const flags: UserFlags = { ReleaseNote0120Hide: 'Y' };
try {
dispatch({
type: UPDATE_USER_FLAG,
payload: {
flags,
},
});
setUserFlags(flags);
if (!user) {
// no user is set, so escape the routine
return;
}
const response = await setFlags({ userId: user?.userId, flags });
const response = await setFlags({ userId: user.id, flags });
if (response.statusCode !== 200) {
console.log('failed to complete do not show status', response.error);
@@ -44,7 +32,7 @@ export default function ReleaseNote0120({
// the user can switch the do no show option again in the further.
console.log('unexpected error: failed to complete do not show status', e);
}
}, [dispatch, user]);
}, [setUserFlags, user]);
return (
<MessageTip

View File

@@ -1,6 +1,7 @@
import ReleaseNoteProps from 'components/ReleaseNote/ReleaseNoteProps';
import ReleaseNote0120 from 'components/ReleaseNote/Releases/ReleaseNote0120';
import ROUTES from 'constants/routes';
import { useAppContext } from 'providers/App/App';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { UserFlags } from 'types/api/user/setFlags';
@@ -44,12 +45,13 @@ const allComponentMap: ComponentMapType[] = [
// ReleaseNote prints release specific warnings and notes that
// user needs to be aware of before using the upgraded version.
function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null {
const { userFlags, currentVersion } = useSelector<AppState, AppReducer>(
const { user } = useAppContext();
const { currentVersion } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const c = allComponentMap.find((item) =>
item.match(path, currentVersion, userFlags),
item.match(path, currentVersion, user.flags),
);
if (!c) {

View File

@@ -1,13 +1,11 @@
import { Typography } from 'antd';
import { useTimezone } from 'providers/Timezone';
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
import getFormattedDate from 'lib/getFormatedDate';
function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const time = new Date(CreatedOrUpdateTime);
const timeString = formatTimezoneAdjustedTimestamp(
time,
'MM/DD/YYYY hh:mm:ss A (UTC Z)',
);
const date = getFormattedDate(time);
const timeString = `${date} ${convertDateToAmAndPm(time)}`;
return <Typography>{timeString}</Typography>;
}

View File

@@ -8,7 +8,7 @@ function TabLabel({
isDisabled,
tooltipText,
}: TabLabelProps): JSX.Element {
const currentLabel = <span>{label}</span>;
const currentLabel = <span data-testid={`${label}`}>{label}</span>;
if (isDisabled) {
return (

View File

@@ -21,5 +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',
USER_ID = 'USER_ID',
}

View File

@@ -1,3 +0,0 @@
export const TimezonePickerShortcuts = {
CloseTimezonePicker: 'escape',
};

View File

@@ -44,14 +44,12 @@ import {
View,
X,
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers';
import { APIKeyProps } from 'types/api/pat/types';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
export const showErrorNotification = (
@@ -99,7 +97,7 @@ export const getDateDifference = (
};
function APIKeys(): JSX.Element {
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const { notifications } = useNotifications();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isAddModalOpen, setIsAddModalOpen] = useState(false);

View File

@@ -7,7 +7,6 @@ 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';
@@ -49,7 +48,6 @@ function HorizontalTimelineGraph({
const urlQuery = useUrlQuery();
const dispatch = useDispatch();
const { timezone } = useTimezone();
const options: uPlot.Options = useMemo(
() => ({
@@ -118,18 +116,8 @@ function HorizontalTimelineGraph({
}),
]
: [],
tzDate: (timestamp: number): Date =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
}),
[
width,
isDarkMode,
transformedData.length,
urlQuery,
dispatch,
timezone.value,
],
[width, isDarkMode, transformedData.length, urlQuery, dispatch],
);
return <Uplot data={transformedData} options={options} />;
}

View File

@@ -7,7 +7,6 @@ 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';
@@ -42,8 +41,6 @@ function TimelineTable(): JSX.Element {
const { t } = useTranslation('common');
const { formatTimezoneAdjustedTimestamp } = useTimezone();
if (isError || !isValidRuleId || !ruleId) {
return <div>{t('something_went_wrong')}</div>;
}
@@ -67,7 +64,6 @@ function TimelineTable(): JSX.Element {
filters,
labels: labels ?? {},
setFilters,
formatTimezoneAdjustedTimestamp,
})}
onRow={handleRowClick}
dataSource={timelineData}

View File

@@ -8,7 +8,6 @@ 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,
@@ -17,6 +16,7 @@ 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,15 +74,10 @@ export const timelineTableColumns = ({
filters,
labels,
setFilters,
formatTimezoneAdjustedTimestamp,
}: {
filters: TagFilter;
labels: AlertLabelsProps['labels'];
setFilters: (filters: TagFilter) => void;
formatTimezoneAdjustedTimestamp: (
input: TimestampInput,
format?: string,
) => string;
}): ColumnsType<AlertRuleTimelineTableResponse> => [
{
title: 'STATE',
@@ -111,9 +106,7 @@ export const timelineTableColumns = ({
dataIndex: 'unixMilli',
width: 200,
render: (value): JSX.Element => (
<div className="alert-rule__created-at">
{formatTimezoneAdjustedTimestamp(value, 'MMM D, YYYY ⎯ HH:mm:ss')}
</div>
<div className="alert-rule__created-at">{formatEpochTimestamp(value)}</div>
),
},
{

View File

@@ -6,13 +6,11 @@ import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Channels, PayloadProps } from 'types/api/channels/getAll';
import AppReducer from 'types/reducer/app';
import Delete from './Delete';
@@ -20,8 +18,8 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
const { t } = useTranslation(['channels']);
const { notifications } = useNotifications();
const [channels, setChannels] = useState<Channels[]>(allChannels);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [action] = useComponentPermission(['new_alert_action'], role);
const { user } = useAppContext();
const [action] = useComponentPermission(['new_alert_action'], user.role);
const onClickEditHandler = useCallback((id: string) => {
history.replace(

View File

@@ -31,13 +31,6 @@ jest.mock('hooks/useNotifications', () => ({
})),
}));
jest.mock('hooks/useFeatureFlag', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
active: true,
})),
}));
describe('Create Alert Channel', () => {
afterEach(() => {
jest.clearAllMocks();
@@ -362,7 +355,7 @@ describe('Create Alert Channel', () => {
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
});
});
describe('Opsgenie', () => {
describe('Email', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Email} />);
});
@@ -385,7 +378,9 @@ describe('Create Alert Channel', () => {
});
it('Should check if the selected item in the type dropdown has text "msteams"', () => {
expect(screen.getByText('msteams')).toBeInTheDocument();
expect(
screen.getByText('Microsoft Teams (Supported in Paid Plans Only)'),
).toBeInTheDocument();
});
it('Should check if Webhook URL label and input are displayed properly ', () => {

View File

@@ -286,7 +286,7 @@ describe('Create Alert Channel (Normal User)', () => {
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
});
});
describe('Opsgenie', () => {
describe('Email', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Email} />);
});
@@ -314,7 +314,8 @@ describe('Create Alert Channel (Normal User)', () => {
).toBeInTheDocument();
});
it('Should check if the upgrade plan message is shown', () => {
// TODO[vikrantgupta25]: check with Shaheer
it.skip('Should check if the upgrade plan message is shown', () => {
expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument();
expect(
screen.getByText(/This feature is available for paid plans only./),
@@ -335,7 +336,7 @@ describe('Create Alert Channel (Normal User)', () => {
screen.getByRole('button', { name: 'button_return' }),
).toBeInTheDocument();
});
it('Should check if save and test buttons are disabled', () => {
it.skip('Should check if save and test buttons are disabled', () => {
expect(
screen.getByRole('button', { name: 'button_save_channel' }),
).toBeDisabled();

View File

@@ -20,13 +20,6 @@ jest.mock('hooks/useNotifications', () => ({
})),
}));
jest.mock('hooks/useFeatureFlag', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
active: true,
})),
}));
describe('Should check if the edit alert channel is properly displayed ', () => {
beforeEach(() => {
render(<EditAlertChannels initialValue={editAlertChannelInitialValue} />);

View File

@@ -9,11 +9,9 @@ import useComponentPermission from 'hooks/useComponentPermission';
import useFetch from 'hooks/useFetch';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import AlertChannelsComponent from './AlertChannels';
import { Button, ButtonContainer, RightActionContainer } from './styles';
@@ -22,10 +20,10 @@ const { Paragraph } = Typography;
function AlertChannels(): JSX.Element {
const { t } = useTranslation(['channels']);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const [addNewChannelPermission] = useComponentPermission(
['add_new_channel'],
role,
user.role,
);
const onToggleHandler = useCallback(() => {
history.push(ROUTES.CHANNELS_NEW);

View File

@@ -17,15 +17,14 @@ 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';
@@ -156,16 +155,8 @@ function AllErrors(): JSX.Element {
}
}, [data?.error, data?.payload, t, notifications]);
const getDateValue = (
value: string,
formatTimezoneAdjustedTimestamp: (
input: TimestampInput,
format?: string,
) => string,
): JSX.Element => (
<Typography>
{formatTimezoneAdjustedTimestamp(value, 'DD/MM/YYYY hh:mm:ss A')}
</Typography>
const getDateValue = (value: string): JSX.Element => (
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
);
const filterIcon = useCallback(() => <SearchOutlined />, []);
@@ -292,8 +283,6 @@ function AllErrors(): JSX.Element {
[filterIcon, filterDropdownWrapper],
);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns: ColumnsType<Exception> = [
{
title: 'Exception Type',
@@ -353,8 +342,7 @@ function AllErrors(): JSX.Element {
dataIndex: 'lastSeen',
width: 80,
key: 'lastSeen',
render: (value): JSX.Element =>
getDateValue(value, formatTimezoneAdjustedTimestamp),
render: getDateValue,
sorter: true,
defaultSortOrder: getDefaultOrder(
getUpdatedParams,
@@ -367,8 +355,7 @@ function AllErrors(): JSX.Element {
dataIndex: 'firstSeen',
width: 80,
key: 'firstSeen',
render: (value): JSX.Element =>
getDateValue(value, formatTimezoneAdjustedTimestamp),
render: getDateValue,
sorter: true,
defaultSortOrder: getDefaultOrder(
getUpdatedParams,

View File

@@ -10,7 +10,6 @@ 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';
@@ -149,12 +148,10 @@ function AnomalyAlertEvaluationView({
]
: [];
const { timezone } = useTimezone();
const options = {
width: dimensions.width,
height: dimensions.height - 36,
plugins: [bandsPlugin, tooltipPlugin(isDarkMode, timezone.value)],
plugins: [bandsPlugin, tooltipPlugin(isDarkMode)],
focus: {
alpha: 0.3,
},
@@ -259,8 +256,6 @@ 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 => {

View File

@@ -1,10 +1,8 @@
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;
@@ -19,7 +17,7 @@ const tooltipPlugin = (
return value.toFixed(3);
}
if (value instanceof Date) {
return dayjs(value).tz(timezone).format('MM/DD/YYYY, h:mm:ss A');
return value.toLocaleString();
}
if (value == null) {
return 'N/A';

View File

@@ -18,8 +18,6 @@ import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { isNull } from 'lodash-es';
@@ -29,10 +27,9 @@ import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import { useMutation, useQueries } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
UPDATE_CURRENT_ERROR,
@@ -43,7 +40,6 @@ import {
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import {
getFormattedDate,
@@ -56,11 +52,18 @@ import { getRouteKey } from './utils';
// eslint-disable-next-line sonarjs/cognitive-complexity
function AppLayout(props: AppLayoutProps): JSX.Element {
const { isLoggedIn, user, role } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const {
isLoggedIn,
user,
licenses,
isFetchingLicenses,
activeLicenseV3,
isFetchingActiveLicenseV3,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
} = useAppContext();
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
const { notifications } = useNotifications();
const [
@@ -98,23 +101,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { data: licenseData, isFetching } = useLicense();
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const isChatSupportEnabled =
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
const isCloudUserVal = isCloudUser();
const showAddCreditCardModal =
isLoggedIn &&
isChatSupportEnabled &&
isCloudUserVal &&
!isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
const { pathname } = useLocation();
const { t } = useTranslation(['titles']);
@@ -248,15 +234,16 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
useEffect(() => {
if (
!isFetching &&
licenseData?.payload?.onTrial &&
!licenseData?.payload?.trialConvertedToSubscription &&
!licenseData?.payload?.workSpaceBlock &&
getRemainingDays(licenseData?.payload.trialEnd) < 7
!isFetchingLicenses &&
licenses &&
licenses.onTrial &&
!licenses.trialConvertedToSubscription &&
!licenses.workSpaceBlock &&
getRemainingDays(licenses.trialEnd) < 7
) {
setShowTrialExpiryBanner(true);
}
}, [licenseData, isFetching]);
}, [isFetchingLicenses, licenses]);
useEffect(() => {
if (
@@ -272,11 +259,12 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
// after logging out hide the trial expiry banner
if (!isLoggedIn) {
setShowTrialExpiryBanner(false);
setShowPaymentFailedWarning(false);
}
}, [isLoggedIn]);
const handleUpgrade = (): void => {
if (role === 'ADMIN') {
if (user.role === 'ADMIN') {
history.push(ROUTES.BILLING);
}
};
@@ -327,6 +315,41 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}
}, [isDarkMode]);
const showAddCreditCardModal = useMemo(() => {
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
licenses
) {
let isChatSupportEnabled = false;
let isPremiumSupportEnabled = false;
const isCloudUserVal = isCloudUser();
if (featureFlags && featureFlags.length > 0) {
isChatSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
?.active || false;
isPremiumSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)
?.active || false;
}
return (
isLoggedIn &&
!isPremiumSupportEnabled &&
isChatSupportEnabled &&
!licenses.trialConvertedToSubscription &&
isCloudUserVal
);
}
return false;
}, [
featureFlags,
featureFlagsFetchError,
isFetchingFeatureFlags,
isLoggedIn,
licenses,
]);
return (
<Layout className={cx(isDarkMode ? 'darkMode' : 'lightMode')}>
<Helmet>
@@ -336,10 +359,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
{showTrialExpiryBanner && !showPaymentFailedWarning && (
<div className="trial-expiry-banner">
You are in free trial period. Your free trial will end on{' '}
<span>
{getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
</span>
{role === 'ADMIN' ? (
<span>{getFormattedDate(licenses?.trialEnd || Date.now())}.</span>
{user.role === 'ADMIN' ? (
<span>
{' '}
Please{' '}
@@ -362,7 +383,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)}
.
</span>
{role === 'ADMIN' ? (
{user.role === 'ADMIN' ? (
<span>
{' '}
Please{' '}
@@ -385,9 +406,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)}
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
{isToDisplayLayout && !renderFullScreen && (
<SideNav licenseData={licenseData} isFetching={isFetching} />
)}
{isToDisplayLayout && !renderFullScreen && <SideNav />}
<div className="app-content" data-overlayscrollbars-initialize>
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<LayoutContent data-overlayscrollbars-initialize>

View File

@@ -1,17 +1,14 @@
import { billingSuccessResponse } from 'mocks-server/__mockdata__/billing';
import {
licensesSuccessResponse,
notOfTrailResponse,
trialConvertedToSubscriptionResponse,
} from 'mocks-server/__mockdata__/licenses';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { act, render, screen } from 'tests/test-utils';
import { act, render, screen, waitFor } from 'tests/test-utils';
import { getFormattedDate } from 'utils/timeUtils';
import BillingContainer from './BillingContainer';
const lisenceUrl = 'http://localhost/api/v2/licenses';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
@@ -38,9 +35,7 @@ window.ResizeObserver =
describe('BillingContainer', () => {
test('Component should render', async () => {
act(() => {
render(<BillingContainer />);
});
render(<BillingContainer />);
const dataInjection = screen.getByRole('columnheader', {
name: /data ingested/i,
@@ -55,13 +50,18 @@ describe('BillingContainer', () => {
});
expect(cost).toBeInTheDocument();
const dayRemainingInBillingPeriod = await screen.findByText(
/11 days_remaining/i,
);
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
const manageBilling = screen.getByRole('button', {
name: 'manage_billing',
});
expect(manageBilling).toBeInTheDocument();
const dollar = screen.getByText(/\$0/i);
expect(dollar).toBeInTheDocument();
const dollar = screen.getByText(/\$1,278.3/i);
await waitFor(() => expect(dollar).toBeInTheDocument());
const currentBill = screen.getByText('billing');
expect(currentBill).toBeInTheDocument();
@@ -69,7 +69,9 @@ describe('BillingContainer', () => {
test('OnTrail', async () => {
act(() => {
render(<BillingContainer />);
render(<BillingContainer />, undefined, undefined, {
licenses: licensesSuccessResponse.data,
});
});
const freeTrailText = await screen.findByText('Free Trial');
@@ -100,14 +102,10 @@ describe('BillingContainer', () => {
});
test('OnTrail but trialConvertedToSubscription', async () => {
server.use(
rest.get(lisenceUrl, (req, res, ctx) =>
res(ctx.status(200), ctx.json(trialConvertedToSubscriptionResponse)),
),
);
act(() => {
render(<BillingContainer />);
render(<BillingContainer />, undefined, undefined, {
licenses: trialConvertedToSubscriptionResponse.data,
});
});
const currentBill = screen.getByText('billing');
@@ -138,12 +136,9 @@ describe('BillingContainer', () => {
});
test('Not on ontrail', async () => {
server.use(
rest.get(lisenceUrl, (req, res, ctx) =>
res(ctx.status(200), ctx.json(notOfTrailResponse)),
),
);
const { findByText } = render(<BillingContainer />);
const { findByText } = render(<BillingContainer />, undefined, undefined, {
licenses: notOfTrailResponse.data,
});
const billingPeriodText = `Your current billing period is from ${getFormattedDate(
billingSuccessResponse.data.billingPeriodStart,
@@ -168,17 +163,4 @@ describe('BillingContainer', () => {
});
expect(logRow).toBeInTheDocument();
});
test('Should render corrent day remaining in billing period', async () => {
server.use(
rest.get(lisenceUrl, (req, res, ctx) =>
res(ctx.status(200), ctx.json(notOfTrailResponse)),
),
);
render(<BillingContainer />);
const dayRemainingInBillingPeriod = await screen.findByText(
/11 days_remaining/i,
);
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
});
});

View File

@@ -24,18 +24,15 @@ import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useAxiosError from 'hooks/useAxiosError';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { isEmpty, pick } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
@@ -137,9 +134,13 @@ export default function BillingContainer(): JSX.Element {
Partial<UsageResponsePayloadProps>
>({});
const { isFetching, data: licensesData, error: licenseError } = useLicense();
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
const {
user,
org,
licenses,
isFetchingLicenses,
licensesFetchError,
} = useAppContext();
const { notifications } = useNotifications();
const handleError = useAxiosError();
@@ -181,7 +182,7 @@ export default function BillingContainer(): JSX.Element {
setData(formattedUsageData);
if (!licensesData?.payload?.onTrial) {
if (!licenses?.onTrial) {
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
setHeaderText(
@@ -195,14 +196,14 @@ export default function BillingContainer(): JSX.Element {
setApiResponse(data?.payload || {});
},
[licensesData?.payload?.onTrial],
[licenses?.onTrial],
);
const isSubscriptionPastDue =
apiResponse.subscriptionStatus === SubscriptionStatus.PastDue;
const { isLoading, isFetching: isFetchingBillingData } = useQuery(
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.id],
{
queryFn: () => getUsage(activeLicense?.key || ''),
onError: handleError,
@@ -213,25 +214,29 @@ export default function BillingContainer(): JSX.Element {
useEffect(() => {
const activeValidLicense =
licensesData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
licenses?.licenses?.find((license) => license.isCurrent === true) || null;
setActiveLicense(activeValidLicense);
if (!isFetching && licensesData?.payload?.onTrial && !licenseError) {
const remainingDays = getRemainingDays(licensesData?.payload?.trialEnd);
if (!isFetchingLicenses && licenses?.onTrial && !licensesFetchError) {
const remainingDays = getRemainingDays(licenses?.trialEnd);
setIsFreeTrial(true);
setBillAmount(0);
setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
setHeaderText(
`You are in free trial period. Your free trial will end on ${getFormattedDate(
licensesData?.payload?.trialEnd,
licenses?.trialEnd,
)}`,
);
}
}, [isFetching, licensesData?.payload, licenseError]);
}, [
licenses?.licenses,
licenses?.onTrial,
licenses?.trialEnd,
isFetchingLicenses,
licensesFetchError,
]);
const columns: ColumnsType<DataType> = [
{
@@ -313,7 +318,7 @@ export default function BillingContainer(): JSX.Element {
});
const handleBilling = useCallback(async () => {
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
if (isFreeTrial && !licenses?.trialConvertedToSubscription) {
logEvent('Billing : Upgrade Plan', {
user: pick(user, ['email', 'userId', 'name']),
org,
@@ -340,7 +345,7 @@ export default function BillingContainer(): JSX.Element {
}, [
activeLicense?.key,
isFreeTrial,
licensesData?.payload?.trialConvertedToSubscription,
licenses?.trialConvertedToSubscription,
manageCreditCard,
updateCreditCard,
]);
@@ -452,22 +457,21 @@ export default function BillingContainer(): JSX.Element {
disabled={isLoading}
onClick={handleBilling}
>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
{isFreeTrial && !licenses?.trialConvertedToSubscription
? t('upgrade_plan')
: t('manage_billing')}
</Button>
</Flex>
</Flex>
{licensesData?.payload?.onTrial &&
licensesData?.payload?.trialConvertedToSubscription && (
<Typography.Text
ellipsis
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
>
{t('card_details_recieved_and_billing_info')}
</Typography.Text>
)}
{licenses?.onTrial && licenses?.trialConvertedToSubscription && (
<Typography.Text
ellipsis
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
>
{t('card_details_recieved_and_billing_info')}
</Typography.Text>
)}
{!isLoading && !isFetchingBillingData ? (
headerText && (
@@ -510,7 +514,7 @@ export default function BillingContainer(): JSX.Element {
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
</div>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription && (
{isFreeTrial && !licenses?.trialConvertedToSubscription && (
<div className="upgrade-plan-benefits">
<Row
justify="space-between"

View File

@@ -2,7 +2,7 @@ import { Row, Tag, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import { useAppContext } from 'providers/App/App';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -13,9 +13,11 @@ import { OptionType } from './types';
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
const { t } = useTranslation(['alerts']);
const { featureFlags } = useAppContext();
const isAnomalyDetectionEnabled =
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
?.active || false;
const optionList = getOptionList(t, isAnomalyDetectionEnabled);

View File

@@ -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,6 +103,8 @@ 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) => ({
@@ -134,8 +136,6 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
return (
<>
<Typography>{errorDetail.exceptionType}</Typography>
@@ -145,12 +145,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
<EventContainer>
<div>
<Typography>Event {errorDetail.errorId}</Typography>
<Typography>
{formatTimezoneAdjustedTimestamp(
errorDetail.timestamp,
'DD/MM/YYYY hh:mm:ss A (UTC Z)',
)}
</Typography>
<Typography>{timeStamp.format('MMM DD YYYY hh:mm:ss A')}</Typography>
</div>
<div>
<Space align="end" direction="horizontal">

View File

@@ -46,6 +46,7 @@ import {
Plus,
X,
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import {
CSSProperties,
Dispatch,
@@ -56,15 +57,12 @@ import {
useRef,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { PreservedViewsTypes } from './constants';
@@ -133,7 +131,7 @@ function ExplorerOptions({
setIsSaveModalOpen(false);
};
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const handleConditionalQueryModification = useCallback((): string => {
if (
@@ -472,7 +470,7 @@ function ExplorerOptions({
}
};
const isEditDeleteSupported = allowedRoles.includes(role as string);
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
const [
isRecentlyUsedSavedViewSelected,

View File

@@ -11,11 +11,11 @@ import {
SlackChannel,
WebhookChannel,
} from 'container/CreateAlertChannels/config';
import useFeatureFlags from 'hooks/useFeatureFlag';
import { isFeatureKeys } from 'hooks/useFeatureFlag/utils';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { Dispatch, ReactElement, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { isFeatureKeys } from 'utils/app';
import EmailSettings from './Settings/Email';
import MsTeamsSettings from './Settings/MsTeams';
@@ -39,15 +39,21 @@ function FormAlertChannels({
editing = false,
}: FormAlertChannelsProps): JSX.Element {
const { t } = useTranslation('channels');
const isUserOnEEPlan = useFeatureFlags(FeatureKeys.ENTERPRISE_PLAN);
const { featureFlags } = useAppContext();
const isUserOnEEPlan =
featureFlags?.find((flag) => flag.name === FeatureKeys.ENTERPRISE_PLAN)
?.active || false;
const feature = `ALERT_CHANNEL_${type.toUpperCase()}`;
const hasFeature = useFeatureFlags(
isFeatureKeys(feature) ? feature : FeatureKeys.ALERT_CHANNEL_SLACK,
);
const featureKey = isFeatureKeys(feature)
? feature
: FeatureKeys.ALERT_CHANNEL_SLACK;
const hasFeature = featureFlags?.find((flag) => flag.name === featureKey);
const isOssFeature = useFeatureFlags(FeatureKeys.OSS);
const isOssFeature = featureFlags?.find(
(flag) => flag.name === FeatureKeys.OSS,
);
const renderSettings = (): ReactElement | null => {
if (

View File

@@ -8,13 +8,11 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import useFetch from 'hooks/useFetch';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef, Labels } from 'types/api/alerts/def';
import AppReducer from 'types/reducer/app';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -45,10 +43,10 @@ function BasicInfo({
const { t } = useTranslation('alerts');
const channels = useFetch(getChannels);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const [addNewChannelPermission] = useComponentPermission(
['add_new_channel'],
role,
user.role,
);
const [

View File

@@ -3,12 +3,10 @@ import { Select, Spin } from 'antd';
import useComponentPermission from 'hooks/useComponentPermission';
import { State } from 'hooks/useFetch';
import { useNotifications } from 'hooks/useNotifications';
import { useAppContext } from 'providers/App/App';
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { PayloadProps } from 'types/api/channels/getAll';
import AppReducer from 'types/reducer/app';
import { StyledCreateChannelOption, StyledSelect } from './styles';
@@ -49,10 +47,10 @@ function ChannelSelect({
});
}
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const [addNewChannelPermission] = useComponentPermission(
['add_new_channel'],
role,
user.role,
);
const renderOptions = (): ReactNode[] => {

View File

@@ -18,14 +18,13 @@ import {
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useTimezone } from 'providers/Timezone';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -36,7 +35,6 @@ 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';
@@ -84,6 +82,8 @@ function ChartPreview({
GlobalReducer
>((state) => state.globalTime);
const { featureFlags } = useAppContext();
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
@@ -203,8 +203,6 @@ function ChartPreview({
[dispatch, location.pathname, urlQuery],
);
const { timezone } = useTimezone();
const options = useMemo(
() =>
getUPlotChartOptions({
@@ -240,9 +238,6 @@ function ChartPreview({
softMax: null,
softMin: null,
panelType: graphType,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
}),
[
yAxisUnit,
@@ -257,7 +252,6 @@ function ChartPreview({
optionName,
alertDef?.condition.targetUnit,
graphType,
timezone.value,
],
);
@@ -270,7 +264,8 @@ function ChartPreview({
chartData && !queryResponse.isError && !queryResponse.isLoading;
const isAnomalyDetectionEnabled =
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
?.active || false;
return (
<div className="alert-chart-container" ref={graphRef}>

View File

@@ -14,12 +14,9 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { Atom, Play, Terminal } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard';
import AppReducer from 'types/reducer/app';
import ChQuerySection from './ChQuerySection';
import PromqlSection from './PromqlSection';
@@ -38,14 +35,9 @@ function QuerySection({
const { t } = useTranslation('alerts');
const [currentTab, setCurrentTab] = useState(queryCategory);
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
// TODO[vikrantgupta25] : check if this is still required ??
const handleQueryCategoryChange = (queryType: string): void => {
featureResponse.refetch().then(() => {
setQueryCategory(queryType as EQueryType);
});
setQueryCategory(queryType as EQueryType);
setCurrentTab(queryType as EQueryType);
};

View File

@@ -1,14 +1,7 @@
import './FormAlertRules.styles.scss';
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import {
Button,
FormInstance,
Modal,
SelectProps,
Tooltip,
Typography,
} from 'antd';
import { Button, FormInstance, Modal, SelectProps, Typography } from 'antd';
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import logEvent from 'api/common/logEvent';
@@ -23,10 +16,6 @@ import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import useFeatureFlag, {
MESSAGE,
useIsFeatureDisabled,
} from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
@@ -35,6 +24,7 @@ import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQu
import { isEqual } from 'lodash-es';
import { BellDot, ExternalLink } from 'lucide-react';
import Tabs2 from 'periscope/components/Tabs2';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
@@ -96,6 +86,7 @@ function FormAlertRules({
}: FormAlertRuleProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
const { featureFlags } = useAppContext();
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
@@ -476,9 +467,9 @@ function FormAlertRules({
panelType,
]);
const isAlertAvailable = useIsFeatureDisabled(
FeatureKeys.QUERY_BUILDER_ALERTS,
);
const isAlertAvailable =
!featureFlags?.find((flag) => flag.name === FeatureKeys.QUERY_BUILDER_ALERTS)
?.active || false;
const saveRule = useCallback(async () => {
if (!isFormValid()) {
@@ -766,7 +757,8 @@ function FormAlertRules({
];
const isAnomalyDetectionEnabled =
useFeatureFlag(FeatureKeys.ANOMALY_DETECTION)?.active || false;
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
?.active || false;
return (
<>
@@ -866,22 +858,20 @@ function FormAlertRules({
{renderBasicInfo()}
</div>
<ButtonContainer>
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
<ActionButton
loading={loading || false}
type="primary"
onClick={onSaveHandler}
icon={<SaveOutlined />}
disabled={
isAlertNameMissing ||
isAlertAvailableToSave ||
!isChannelConfigurationValid ||
queryStatus === 'error'
}
>
{isNewRule ? t('button_createrule') : t('button_savechanges')}
</ActionButton>
</Tooltip>
<ActionButton
loading={loading || false}
type="primary"
onClick={onSaveHandler}
icon={<SaveOutlined />}
disabled={
isAlertNameMissing ||
isAlertAvailableToSave ||
!isChannelConfigurationValid ||
queryStatus === 'error'
}
>
{isNewRule ? t('button_createrule') : t('button_savechanges')}
</ActionButton>
<ActionButton
loading={loading || false}

View File

@@ -4,7 +4,6 @@ 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';
@@ -33,17 +32,13 @@ 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)
.tz(timezone.value)
.format('h:mm:ss:SSS A (UTC Z)');
const timeStamp = dayjs(startTime).format('h:mm:ss:SSS A');
const startTimeInMs = startTime - globalStart;
return (
<div>

View File

@@ -7,12 +7,11 @@ import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import find from 'lodash-es/find';
import { useAppContext } from 'providers/App/App';
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { useInterval } from 'react-use';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IDiskType,
@@ -24,7 +23,6 @@ import {
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
} from 'types/api/settings/getRetention';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import Retention from './Retention';
@@ -68,11 +66,11 @@ function GeneralSettings({
logsTtlValuesPayload,
);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const [setRetentionPermission] = useComponentPermission(
['set_retention_period'],
role,
user.role,
);
const [

View File

@@ -2,14 +2,12 @@ import { Typography } from 'antd';
import getDisks from 'api/disks/getDisks';
import getRetentionPeriodApi from 'api/settings/getRetention';
import Spinner from 'components/Spinner';
import { useAppContext } from 'providers/App/App';
import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { TTTLType } from 'types/api/settings/common';
import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention';
import AppReducer from 'types/reducer/app';
import GeneralSettingsContainer from './GeneralSettings';
@@ -19,7 +17,7 @@ type TRetentionAPIReturn<T extends TTTLType> = Promise<
function GeneralSettings(): JSX.Element {
const { t } = useTranslation('common');
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const [
getRetentionPeriodMetricsApiResponse,

View File

@@ -6,11 +6,9 @@ import { Button, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import SettingsDrawer from 'container/NewDashboard/DashboardDescription/SettingsDrawer';
import useComponentPermission from 'hooks/useComponentPermission';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -21,7 +19,7 @@ export default function DashboardEmptyState(): JSX.Element {
handleToggleDashboardSlider,
} = useDashboard();
const { user, role } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
let permissions: ComponentTypes[] = ['add_panel'];
if (isDashboardLocked) {
@@ -31,7 +29,7 @@ export default function DashboardEmptyState(): JSX.Element {
const userRole: ROLES | null =
selectedDashboard?.created_by === user?.email
? (USER_ROLES.AUTHOR as ROLES)
: role;
: user.role;
const [addPanelPermission] = useComponentPermission(permissions, userRole);

View File

@@ -22,11 +22,8 @@ import {
useRef,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { v4 } from 'uuid';
import WidgetHeader from '../WidgetHeader';
@@ -77,10 +74,6 @@ function WidgetGraphComponent({
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
const featureResponse = useSelector<AppState, AppReducer['featureResponse']>(
(state) => state.app.featureResponse,
);
const onToggleModal = useCallback(
(func: Dispatch<SetStateAction<boolean>>) => {
func((value) => !value);
@@ -117,7 +110,6 @@ function WidgetGraphComponent({
setSelectedDashboard(updatedDashboard.payload);
}
setDeleteModal(false);
featureResponse.refetch();
},
onError: () => {
notifications.error({

View File

@@ -26,17 +26,16 @@ import {
LockKeyhole,
X,
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { sortLayout } from 'providers/Dashboard/util';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FullScreen, FullScreenHandle } from 'react-full-screen';
import { ItemCallback, Layout } from 'react-grid-layout';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -69,9 +68,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
const { widgets, variables } = data || {};
const { featureResponse, role, user } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const { user } = useAppContext();
const isDarkMode = useIsDarkMode();
@@ -111,7 +108,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
const userRole: ROLES | null =
selectedDashboard?.created_by === user?.email
? (USER_ROLES.AUTHOR as ROLES)
: role;
: user.role;
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
permissions,
@@ -120,7 +117,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
const [deleteWidget, editWidget] = useComponentPermission(
['delete_widget', 'edit_widget'],
role,
user.role,
);
useEffect(() => {
@@ -160,8 +157,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
setSelectedDashboard(updatedDashboard.payload);
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
}
featureResponse.refetch();
},
onError: () => {
notifications.error({
@@ -258,7 +253,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
form.setFieldValue('title', '');
setIsSettingsModalOpen(false);
setCurrentSelectRowId(null);
featureResponse.refetch();
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
@@ -421,7 +415,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
setIsDeleteModalOpen(false);
setCurrentSelectRowId(null);
featureResponse.refetch();
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {

View File

@@ -24,14 +24,12 @@ import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { isEmpty } from 'lodash-es';
import { CircleX, X } from 'lucide-react';
import { unparse } from 'papaparse';
import { useAppContext } from 'providers/App/App';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import AppReducer from 'types/reducer/app';
import { errorTooltipPosition, WARNING_MESSAGE } from './config';
import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants';
@@ -130,11 +128,11 @@ function WidgetHeader({
},
[keyMethodMapping],
);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const [deleteWidget, editWidget] = useComponentPermission(
['delete_widget', 'edit_widget'],
role,
user.role,
);
const actions = useMemo(

View File

@@ -1,78 +0,0 @@
import { PlusSquareOutlined } from '@ant-design/icons';
import { Avatar, Typography } from 'antd';
import { INVITE_MEMBERS_HASH } from 'constants/app';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import {
InviteMembersContainer,
OrganizationContainer,
OrganizationWrapper,
} from '../styles';
function CurrentOrganization({
onToggle,
}: CurrentOrganizationProps): JSX.Element {
const { org, role } = useSelector<AppState, AppReducer>((state) => state.app);
const [currentOrgSettings, inviteMembers] = useComponentPermission(
['current_org_settings', 'invite_members'],
role,
);
// just to make sure role and org are present in the reducer
if (!org || !role) {
return <div />;
}
const orgName = org[0].name;
return (
<>
<Typography>CURRENT ORGANIZATION</Typography>
<OrganizationContainer>
<OrganizationWrapper>
<Avatar shape="square" size="large">
{orgName}
</Avatar>
<Typography>{orgName}</Typography>
</OrganizationWrapper>
{currentOrgSettings && (
<Typography.Link
onClick={(): void => {
onToggle();
history.push(ROUTES.ORG_SETTINGS);
}}
>
Settings
</Typography.Link>
)}
</OrganizationContainer>
{inviteMembers && (
<InviteMembersContainer>
<PlusSquareOutlined />
<Typography.Link
onClick={(): void => {
onToggle();
history.push(`${ROUTES.ORG_SETTINGS}${INVITE_MEMBERS_HASH}`);
}}
>
Invite Members
</Typography.Link>
</InviteMembersContainer>
)}
</>
);
}
interface CurrentOrganizationProps {
onToggle: VoidFunction;
}
export default CurrentOrganization;

View File

@@ -1,25 +0,0 @@
.trial-expiry-banner {
padding: 8px;
background-color: #f25733;
color: white;
text-align: center;
}
.upgrade-link {
padding: 0px;
padding-right: 4px;
display: inline !important;
color: white;
text-decoration: underline;
text-decoration-color: white;
text-decoration-thickness: 2px;
text-underline-offset: 2px;
&:hover {
color: white;
text-decoration: underline;
text-decoration-color: white;
text-decoration-thickness: 2px;
text-underline-offset: 2px;
}
}

View File

@@ -1,49 +0,0 @@
import { Spin, Typography } from 'antd';
import ROUTES from 'constants/routes';
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import history from 'lib/history';
import {
FreePlanIcon,
ManageLicenseContainer,
ManageLicenseWrapper,
} from './styles';
function ManageLicense({ onToggle }: ManageLicenseProps): JSX.Element {
const { data, isLoading } = useLicense();
const onManageLicense = (): void => {
onToggle();
history.push(ROUTES.LIST_LICENSES);
};
if (isLoading || data?.payload === undefined) {
return <Spin />;
}
const isEnterprise = data?.payload?.licenses?.some(
(license) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.ENTERPRISE_PLAN,
);
return (
<>
<Typography>SIGNOZ STATUS</Typography>
<ManageLicenseContainer>
<ManageLicenseWrapper>
<FreePlanIcon />
<Typography>{!isEnterprise ? 'Free Plan' : 'Enterprise Plan'} </Typography>
</ManageLicenseWrapper>
<Typography.Link onClick={onManageLicense}>Manage Licenses</Typography.Link>
</ManageLicenseContainer>
</>
);
}
interface ManageLicenseProps {
onToggle: VoidFunction;
}
export default ManageLicense;

View File

@@ -1,19 +0,0 @@
import { MinusSquareOutlined } from '@ant-design/icons';
import styled from 'styled-components';
export const ManageLicenseContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
`;
export const ManageLicenseWrapper = styled.div`
display: flex;
gap: 0.5rem;
align-items: center;
`;
export const FreePlanIcon = styled(MinusSquareOutlined)`
background-color: hsla(0, 0%, 100%, 0.3);
`;

View File

@@ -1,50 +0,0 @@
import { Avatar, Typography } from 'antd';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { AvatarContainer, ManageAccountLink, Wrapper } from '../styles';
function SignedIn({ onToggle }: SignedInProps): JSX.Element {
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const onManageAccountClick = useCallback(() => {
onToggle();
history.push(ROUTES.MY_SETTINGS);
}, [onToggle]);
if (!user) {
return <div />;
}
const { name, email } = user;
return (
<div>
<Typography>SIGNED IN AS</Typography>
<Wrapper>
<AvatarContainer>
<Avatar shape="circle" size="large">
{name[0]}
</Avatar>
<div>
<Typography>{name}</Typography>
<Typography>{email}</Typography>
</div>
</AvatarContainer>
<ManageAccountLink onClick={onManageAccountClick}>
Manage Account
</ManageAccountLink>
</Wrapper>
</div>
);
}
interface SignedInProps {
onToggle: VoidFunction;
}
export default SignedIn;

View File

@@ -1,215 +0,0 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './Header.styles.scss';
import {
CaretDownFilled,
CaretUpFilled,
LogoutOutlined,
} from '@ant-design/icons';
import { Button, Divider, MenuProps, Space, Typography } from 'antd';
import { Logout } from 'api/utils';
import ROUTES from 'constants/routes';
import Config from 'container/ConfigDropdown';
import { useIsDarkMode, useThemeMode } from 'hooks/useDarkMode';
import useLicense, { LICENSE_PLAN_STATUS } from 'hooks/useLicense';
import history from 'lib/history';
import {
Dispatch,
KeyboardEvent,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { NavLink } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { License } from 'types/api/licenses/def';
import AppReducer from 'types/reducer/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import CurrentOrganization from './CurrentOrganization';
import ManageLicense from './ManageLicense';
import SignedIn from './SignedIn';
import {
AvatarWrapper,
Container,
Header,
IconContainer,
LogoutContainer,
NavLinkWrapper,
ToggleButton,
UserDropdown,
} from './styles';
function HeaderContainer(): JSX.Element {
const { user, role, currentVersion } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const isDarkMode = useIsDarkMode();
const { toggleTheme } = useThemeMode();
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
const [homeRoute, setHomeRoute] = useState<string>(ROUTES.APPLICATION);
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>(false);
const onToggleHandler = useCallback(
(functionToExecute: Dispatch<SetStateAction<boolean>>) => (): void => {
functionToExecute((state) => !state);
},
[],
);
const onLogoutKeyDown = useCallback((e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' || e.key === 'Space') {
Logout();
}
}, []);
const menu: MenuProps = useMemo(
() => ({
items: [
{
key: 'main-menu',
label: (
<div>
<SignedIn onToggle={onToggleHandler(setIsUserDropDownOpen)} />
<Divider />
<CurrentOrganization onToggle={onToggleHandler(setIsUserDropDownOpen)} />
<Divider />
<ManageLicense onToggle={onToggleHandler(setIsUserDropDownOpen)} />
<Divider />
<LogoutContainer>
<LogoutOutlined />
<div
tabIndex={0}
onKeyDown={onLogoutKeyDown}
role="button"
onClick={Logout}
>
<Typography.Link>Logout</Typography.Link>
</div>
</LogoutContainer>
</div>
),
},
],
}),
[onToggleHandler, onLogoutKeyDown],
);
const onClickSignozCloud = (): void => {
window.open(
'https://signoz.io/oss-to-cloud/?utm_source=product_navbar&utm_medium=frontend&utm_campaign=oss_users',
'_blank',
);
};
const { data: licenseData, isFetching, status: licenseStatus } = useLicense();
const licensesStatus: string =
licenseData?.payload?.licenses?.find((e: License) => e.isCurrent)?.status ||
'';
const isLicenseActive =
licensesStatus?.toLocaleLowerCase() ===
LICENSE_PLAN_STATUS.VALID.toLocaleLowerCase();
useEffect(() => {
if (
!isFetching &&
licenseData?.payload?.onTrial &&
!licenseData?.payload?.trialConvertedToSubscription &&
getRemainingDays(licenseData?.payload.trialEnd) < 7
) {
setShowTrialExpiryBanner(true);
}
if (!isFetching && licenseData?.payload?.workSpaceBlock) {
setHomeRoute(ROUTES.WORKSPACE_LOCKED);
}
}, [licenseData, isFetching]);
const handleUpgrade = (): void => {
if (role === 'ADMIN') {
history.push(ROUTES.BILLING);
}
};
return (
<>
{showTrialExpiryBanner && (
<div className="trial-expiry-banner">
You are in free trial period. Your free trial will end on{' '}
<span>
{getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
</span>
{role === 'ADMIN' ? (
<span>
{' '}
Please{' '}
<a className="upgrade-link" onClick={handleUpgrade}>
upgrade
</a>
to continue using SigNoz features.
</span>
) : (
'Please contact your administrator for upgrading to a paid plan.'
)}
</div>
)}
<Header>
<Container>
<NavLink to={homeRoute}>
<NavLinkWrapper>
<img src={`/signoz.svg?currentVersion=${currentVersion}`} alt="SigNoz" />
<Typography.Title
style={{ margin: 0, color: 'rgb(219, 219, 219)' }}
level={4}
>
SigNoz
</Typography.Title>
</NavLinkWrapper>
</NavLink>
<Space size="middle" align="center">
{licenseStatus === 'success' && !isLicenseActive && (
<Button onClick={onClickSignozCloud} type="primary">
Try Signoz Cloud
</Button>
)}
<Config frontendId="tooltip" />
<ToggleButton
checked={isDarkMode}
onChange={toggleTheme}
defaultChecked={isDarkMode}
checkedChildren="🌜"
unCheckedChildren="🌞"
/>
<UserDropdown
onOpenChange={onToggleHandler(setIsUserDropDownOpen)}
trigger={['click']}
menu={menu}
open={isUserDropDownOpen}
>
<Space>
<AvatarWrapper shape="circle">{user?.name[0]}</AvatarWrapper>
<IconContainer>
{!isUserDropDownOpen ? <CaretDownFilled /> : <CaretUpFilled />}
</IconContainer>
</Space>
</UserDropdown>
</Space>
</Container>
</Header>
</>
);
}
export default HeaderContainer;

View File

@@ -1,90 +0,0 @@
import { Avatar, Dropdown, Layout, Switch, Typography } from 'antd';
import styled from 'styled-components';
export const Header = styled(Layout.Header)`
background: #1f1f1f !important;
padding-left: 16px;
padding-right: 16px;
`;
export const Container = styled.div`
display: flex;
justify-content: space-between;
height: 4rem;
`;
export const AvatarContainer = styled.div`
display: flex;
gap: 1rem;
`;
export const Wrapper = styled.div`
display: flex;
justify-content: space-between;
margin-top: 1rem;
`;
export const ManageAccountLink = styled(Typography.Link)`
width: 6rem;
text-align: end;
`;
export const OrganizationWrapper = styled.div`
display: flex;
gap: 1rem;
align-items: center;
margin-top: 1rem;
`;
export const OrganizationContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
export const InviteMembersContainer = styled.div`
display: flex;
gap: 0.5rem;
align-items: center;
margin-top: 1.25rem;
`;
export const LogoutContainer = styled.div`
display: flex;
gap: 0.5rem;
align-items: center;
`;
export interface DarkModeProps {
checked?: boolean;
defaultChecked?: boolean;
}
export const ToggleButton = styled(Switch)<DarkModeProps>`
&&& {
background: ${({ checked }): string => (checked === false ? 'grey' : '')};
}
.ant-switch-inner {
font-size: 1rem !important;
}
`;
export const IconContainer = styled.div`
color: white;
`;
export const NavLinkWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 100%;
gap: 0.5rem;
`;
export const AvatarWrapper = styled(Avatar)`
background-color: rgba(255, 255, 255, 0.25);
`;
export const UserDropdown = styled(Dropdown)`
cursor: pointer;
`;

View File

@@ -3,18 +3,16 @@ import './IngestionSettings.styles.scss';
import { Skeleton, Table, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import getIngestionData from 'api/settings/getIngestionData';
import { useAppContext } from 'providers/App/App';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IngestionDataType } from 'types/api/settings/ingestion';
import AppReducer from 'types/reducer/app';
export default function IngestionSettings(): JSX.Element {
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const { data: ingestionData, isFetching } = useQuery({
queryFn: getIngestionData,
queryKey: ['getIngestionData', user?.userId],
queryKey: ['getIngestionData', user?.id],
});
const columns: ColumnsType<IngestionDataType> = [

View File

@@ -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 from 'dayjs';
import dayjs, { Dayjs } from 'dayjs';
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
@@ -51,30 +51,24 @@ import {
Trash2,
X,
} from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { useAppContext } from 'providers/App/App';
import { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers';
import { ErrorResponse } from 'types/api';
import { LimitProps } from 'types/api/ingestionKeys/limits/types';
import {
IngestionKeyProps,
PaginationProps,
} from 'types/api/ingestionKeys/types';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
const { Option } = Select;
const BYTES = 1073741824;
// 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 =>
export const disabledDate = (current: Dayjs): boolean =>
// Disable all dates before today
current && current < dayjs().endOf('day');
@@ -104,7 +98,7 @@ export const API_KEY_EXPIRY_OPTIONS: ExpiryOption[] = [
];
function MultiIngestionSettings(): JSX.Element {
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const { notifications } = useNotifications();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isDeleteLimitModalOpen, setIsDeleteLimitModalOpen] = useState(false);
@@ -397,11 +391,8 @@ function MultiIngestionSettings(): JSX.Element {
const gbToBytes = (gb: number): number => Math.round(gb * 1024 ** 3);
const getFormattedTime = (
date: string,
formatTimezoneAdjustedTimestamp: (date: string, format: string) => string,
): string =>
formatTimezoneAdjustedTimestamp(date, 'MMM DD,YYYY, hh:mm a (UTC Z)');
const getFormattedTime = (date: string): string =>
dayjs(date).format('MMM DD,YYYY, hh:mm a');
const showDeleteLimitModal = (
APIKey: IngestionKeyProps,
@@ -551,27 +542,17 @@ function MultiIngestionSettings(): JSX.Element {
}
};
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns: AntDTableProps<IngestionKeyProps>['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,
formatTimezoneAdjustedTimestamp,
);
const createdOn = getFormattedTime(APIKey.created_at);
const formattedDateAndTime =
APIKey &&
APIKey?.expires_at &&
getFormattedTime(APIKey?.expires_at, formatTimezoneAdjustedTimestamp);
APIKey && APIKey?.expires_at && getFormattedTime(APIKey?.expires_at);
const updatedOn = getFormattedTime(
APIKey?.updated_at,
formatTimezoneAdjustedTimestamp,
);
const updatedOn = getFormattedTime(APIKey?.updated_at);
const limits: { [key: string]: LimitProps } = {};

View File

@@ -3,12 +3,6 @@ import apply from 'api/licenses/apply';
import { useNotifications } from 'hooks/useNotifications';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { QueryObserverResult, RefetchOptions } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/licenses/getAll';
import AppReducer from 'types/reducer/app';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import {
@@ -24,9 +18,6 @@ function ApplyLicenseForm({
const { t } = useTranslation(['licenses']);
const [isLoading, setIsLoading] = useState(false);
const [form] = Form.useForm<FormValues>();
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const { notifications } = useNotifications();
const key = Form.useWatch('key', form);
@@ -50,7 +41,7 @@ function ApplyLicenseForm({
});
if (response.statusCode === 200) {
await Promise.all([featureResponse?.refetch(), licenseRefetch()]);
await Promise.all([licenseRefetch()]);
notifications.success({
message: 'Success',
@@ -102,11 +93,7 @@ function ApplyLicenseForm({
}
interface ApplyLicenseFormProps {
licenseRefetch: (
options?: RefetchOptions,
) => Promise<
QueryObserverResult<SuccessResponse<PayloadProps> | ErrorResponse, unknown>
>;
licenseRefetch: () => void;
}
interface FormValues {

View File

@@ -1,20 +1,8 @@
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 (
<Typography>
{formatTimezoneAdjustedTimestamp(value, 'YYYY-MM-DD HH:mm:ss (UTC Z)')}
</Typography>
);
}
function ListLicenses({ licenses }: ListLicensesProps): JSX.Element {
const { t } = useTranslation(['licenses']);
@@ -35,14 +23,12 @@ 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,
},
];

View File

@@ -1,6 +1,6 @@
import { Tabs, Typography } from 'antd';
import { Tabs } from 'antd';
import Spinner from 'components/Spinner';
import useLicense from 'hooks/useLicense';
import { useAppContext } from 'providers/App/App';
import { useTranslation } from 'react-i18next';
import ApplyLicenseForm from './ApplyLicenseForm';
@@ -8,24 +8,20 @@ import ListLicenses from './ListLicenses';
function Licenses(): JSX.Element {
const { t, ready: translationsReady } = useTranslation(['licenses']);
const { data, isError, isLoading, refetch } = useLicense();
const { licenses, licensesRefetch } = useAppContext();
if (isError || data?.error) {
return <Typography>{data?.error}</Typography>;
}
if (isLoading || data?.payload === undefined || !translationsReady) {
if (!translationsReady) {
return <Spinner tip={t('loading_licenses')} height="90vh" />;
}
const allValidLicense =
data?.payload?.licenses?.filter((license) => license.isCurrent) || [];
licenses?.licenses?.filter((license) => license.isCurrent) || [];
const tabs = [
{
label: t('tab_current_license'),
key: 'licenses',
children: <ApplyLicenseForm licenseRefetch={refetch} />,
children: <ApplyLicenseForm licenseRefetch={licensesRefetch} />,
},
{
label: t('tab_license_history'),

View File

@@ -5,14 +5,10 @@ import { Button, Divider, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app';
import AlertInfoCard from './AlertInfoCard';
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
@@ -32,36 +28,18 @@ const alertLogEvents = (
};
export function AlertsEmptyState(): JSX.Element {
const { t } = useTranslation('common');
const { role, featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const { user } = useAppContext();
const [addNewAlert] = useComponentPermission(
['add_new_alert', 'action'],
role,
user.role,
);
const { notifications: notificationsApi } = useNotifications();
const handleError = useCallback((): void => {
notificationsApi.error({
message: t('something_went_wrong'),
});
}, [notificationsApi, t]);
const [loading, setLoading] = useState(false);
const onClickNewAlertHandler = useCallback(() => {
setLoading(true);
featureResponse
.refetch()
.then(() => {
setLoading(false);
history.push(ROUTES.ALERTS_NEW);
})
.catch(handleError)
.finally(() => setLoading(false));
}, [featureResponse, handleError]);
setLoading(false);
history.push(ROUTES.ALERTS_NEW);
}, []);
return (
<div className="alert-list-container">

View File

@@ -2,11 +2,8 @@ import { NotificationInstance } from 'antd/es/notification/interface';
import deleteAlerts from 'api/alerts/delete';
import { State } from 'hooks/useFetch';
import { Dispatch, SetStateAction, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { PayloadProps as DeleteAlertPayloadProps } from 'types/api/alerts/delete';
import { GettableAlert } from 'types/api/alerts/get';
import AppReducer from 'types/reducer/app';
import { ColumnButton } from './styles';
@@ -25,10 +22,6 @@ function DeleteAlert({
payload: undefined,
});
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const defaultErrorMessage = 'Something went wrong';
const onDeleteHandler = async (id: number): Promise<void> => {
@@ -79,20 +72,7 @@ function DeleteAlert({
...state,
loading: true,
}));
featureResponse
.refetch()
.then(() => {
onDeleteHandler(id);
})
.catch(() => {
setDeleteAlertState((state) => ({
...state,
loading: false,
}));
notifications.error({
message: defaultErrorMessage,
});
});
onDeleteHandler(id);
};
return (

View File

@@ -23,14 +23,12 @@ import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { useAppContext } from 'providers/App/App';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GettableAlert } from 'types/api/alerts/get';
import AppReducer from 'types/reducer/app';
import DeleteAlert from './DeleteAlert';
import { Button, ColumnButton, SearchContainer } from './styles';
@@ -42,12 +40,11 @@ const { Search } = Input;
function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const { t } = useTranslation('common');
const { role, featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const { user } = useAppContext();
// TODO[vikrantgupta25]: check with sagar on cleanup
const [addNewAlert, action] = useComponentPermission(
['add_new_alert', 'action'],
role,
user.role,
);
const [editLoader, setEditLoader] = useState<boolean>(false);
@@ -105,38 +102,23 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
logEvent('Alert: New alert button clicked', {
number: allAlertRules?.length,
});
featureResponse
.refetch()
.then(() => {
history.push(ROUTES.ALERTS_NEW);
})
.catch(handleError);
history.push(ROUTES.ALERTS_NEW);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [featureResponse, handleError]);
}, []);
const onEditHandler = (record: GettableAlert) => (): void => {
setEditLoader(true);
featureResponse
.refetch()
.then(() => {
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
params.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(compositeQuery)),
);
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
params.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(compositeQuery)),
);
params.set(
QueryParams.panelTypes,
record.condition.compositeQuery.panelType,
);
params.set(QueryParams.panelTypes, record.condition.compositeQuery.panelType);
params.set(QueryParams.ruleId, record.id.toString());
params.set(QueryParams.ruleId, record.id.toString());
setEditLoader(false);
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
})
.catch(handleError)
.finally(() => setEditLoader(false));
setEditLoader(false);
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
};
const onCloneHandler = (

View File

@@ -867,7 +867,7 @@
.configure-metadata-root {
.ant-modal-content {
width: 500px;
width: 400px;
flex-shrink: 0;
border-radius: 4px;
border: 1px solid var(--Slate-500, #161922);
@@ -1039,6 +1039,7 @@
display: flex;
justify-content: space-between;
align-items: center;
width: 336px;
padding: 0px 0px 0px 14.634px;
.left {

View File

@@ -27,8 +27,6 @@ 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';
@@ -46,7 +44,6 @@ import {
EllipsisVertical,
Expand,
ExternalLink,
FileJson,
Github,
HdmiPort,
LayoutGrid,
@@ -59,8 +56,8 @@ import {
// #TODO: lucide will be removing brand icons like Github in future, in that case we can use simple icons
// see more: https://github.com/lucide-icons/lucide/issues/94
import { handleContactSupport } from 'pages/Integrations/utils';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useTimezone } from 'providers/Timezone';
import {
ChangeEvent,
Key,
@@ -70,19 +67,10 @@ 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,
IDashboardVariable,
WidgetRow,
Widgets,
} from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { Dashboard } from 'types/api/dashboard/getAll';
import { isCloudUser } from 'utils/app';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
@@ -105,7 +93,7 @@ function DashboardsList(): JSX.Element {
refetch: refetchDashboardList,
} = useGetAllDashboard();
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const {
listSortOrder: sortOrder,
@@ -117,7 +105,7 @@ function DashboardsList(): JSX.Element {
);
const [action, createNewDashboard] = useComponentPermission(
['action', 'create_new_dashboards'],
role,
user.role,
);
const [
@@ -270,11 +258,6 @@ 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,
})) || [];
@@ -358,13 +341,31 @@ function DashboardsList(): JSX.Element {
}
}, [state.error, state.value, t, notifications]);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
function getFormattedTime(dashboard: Dashboard, option: string): string {
return formatTimezoneAdjustedTimestamp(
get(dashboard, option, ''),
'MMM D, YYYY ⎯ hh:mm:ss A (UTC Z)',
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 formattedDate = new Date(get(dashboard, option, '')).toLocaleDateString(
'en-US',
dateOptions,
);
// Combine time and date
return `${formattedDate}${formattedTime}`;
}
const onLastUpdated = (time: string): string => {
@@ -407,11 +408,32 @@ function DashboardsList(): JSX.Element {
title: 'Dashboards',
key: 'dashboard',
render: (dashboard: Data, _, index): JSX.Element => {
const formattedDateAndTime = formatTimezoneAdjustedTimestamp(
dashboard.createdAt,
'MMM D, YYYY ⎯ hh:mm:ss A (UTC Z)',
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,
);
// Combine time and date
const formattedDateAndTime = `${formattedDate}${formattedTime}`;
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`;
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
@@ -427,15 +449,6 @@ function DashboardsList(): JSX.Element {
});
};
const handleJsonExport = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
event.preventDefault();
downloadObjectAsJson(
sanitizeDashboardData({ ...dashboard, title: dashboard.name }),
dashboard.name,
);
};
return (
<div className="dashboard-list-item" onClick={onClickHandler}>
<div className="title-with-action">
@@ -509,14 +522,6 @@ function DashboardsList(): JSX.Element {
>
Copy Link
</Button>
<Button
type="text"
className="action-btn"
icon={<FileJson size={12} />}
onClick={handleJsonExport}
>
Export JSON
</Button>
</section>
<section className="section-2">
<DeleteButton
@@ -535,7 +540,6 @@ function DashboardsList(): JSX.Element {
<EllipsisVertical
className="dashboard-action-icon"
size={14}
data-testid="dashboard-action-icon"
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
@@ -1100,11 +1104,6 @@ export interface Data {
isLocked: boolean;
id: string;
image?: string;
widgets?: Array<WidgetRow | Widgets>;
layout?: Layout[];
panelMap?: Record<string, { widgets: Layout[]; collapsed: boolean }>;
variables: Record<string, IDashboardVariable>;
version?: string;
}
export default DashboardsList;

View File

@@ -17,7 +17,6 @@ import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { MESSAGE } from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import history from 'lib/history';
@@ -40,7 +39,6 @@ function ImportJSON({
const [isCreateDashboardError, setIsCreateDashboardError] = useState<boolean>(
false,
);
const [isFeatureAlert, setIsFeatureAlert] = useState<boolean>(false);
const [dashboardCreating, setDashboardCreating] = useState<boolean>(false);
@@ -108,15 +106,6 @@ function ImportJSON({
dashboardId: response.payload?.uuid,
dashboardName: response.payload?.data?.title,
});
} else if (response.error === 'feature usage exceeded') {
setIsFeatureAlert(true);
notifications.error({
message:
response.error ||
t('something_went_wrong', {
ns: 'common',
}),
});
} else {
setIsCreateDashboardError(true);
notifications.error({
@@ -130,8 +119,6 @@ function ImportJSON({
setDashboardCreating(false);
} catch (error) {
setDashboardCreating(false);
setIsFeatureAlert(false);
setIsCreateDashboardError(true);
notifications.error({
message: error instanceof Error ? error.message : t('error_loading_json'),
@@ -149,7 +136,6 @@ function ImportJSON({
const onCancelHandler = (): void => {
setIsUploadJSONError(false);
setIsCreateDashboardError(false);
setIsFeatureAlert(false);
onModalHandler();
};
@@ -239,12 +225,6 @@ function ImportJSON({
>
{t('import_and_next')} &nbsp; <MoveRight size={14} />
</Button>
{isFeatureAlert && (
<Typography.Text type="danger">
{MESSAGE.CREATE_DASHBOARD}
</Typography.Text>
)}
</div>
</div>
}

View File

@@ -7,12 +7,10 @@ import ROUTES from 'constants/routes';
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { Data } from '../DashboardsList';
@@ -34,7 +32,7 @@ export function DeleteButton({
routeToListPage,
}: DeleteButtonProps): JSX.Element {
const [modal, contextHolder] = Modal.useModal();
const { role, user } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const isAuthor = user?.email === createdBy;
const queryClient = useQueryClient();
@@ -92,7 +90,7 @@ export function DeleteButton({
const getDeleteTooltipContent = (): string => {
if (isLocked) {
if (role === USER_ROLES.ADMIN || isAuthor) {
if (user.role === USER_ROLES.ADMIN || isAuthor) {
return t('dashboard:locked_dashboard_delete_tooltip_admin_author');
}
@@ -115,7 +113,7 @@ export function DeleteButton({
}
}}
className="delete-btn"
disabled={isLocked || (role === USER_ROLES.VIEWER && !isAuthor)}
disabled={isLocked || (user.role === USER_ROLES.VIEWER && !isAuthor)}
>
<DeleteOutlined /> Delete dashboard
</TableLinkText>

View File

@@ -8,12 +8,10 @@ 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,
@@ -75,8 +73,6 @@ function NodeMetrics({
[queries],
);
const { timezone } = useTimezone();
const options = useMemo(
() =>
queries.map(({ data }, idx) =>
@@ -90,9 +86,6 @@ function NodeMetrics({
minTimeScale: start,
maxTimeScale: end,
verticalLineTimestamp,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
}),
),
[
@@ -103,7 +96,6 @@ function NodeMetrics({
start,
verticalLineTimestamp,
end,
timezone.value,
],
);

View File

@@ -8,12 +8,10 @@ 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';
@@ -62,7 +60,6 @@ function PodMetrics({
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
[queries],
);
const { timezone } = useTimezone();
const options = useMemo(
() =>
@@ -77,20 +74,9 @@ 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,
end,
verticalLineTimestamp,
timezone.value,
],
[queries, isDarkMode, dimensions, start, verticalLineTimestamp, end],
);
const renderCardContent = (

View File

@@ -11,8 +11,7 @@ import ROUTES from 'constants/routes';
import dompurify from 'dompurify';
import { isEmpty } from 'lodash-es';
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import React, { useMemo, useState } from 'react';
import { 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';
@@ -69,8 +68,6 @@ export function TableViewActions(
const [isOpen, setIsOpen] = useState<boolean>(false);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
if (record.field === 'body') {
const parsedBody = recursiveParseJSON(fieldData.value);
if (!isEmpty(parsedBody)) {
@@ -103,44 +100,33 @@ export function TableViewActions(
);
}
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 <span style={commonStyles} dangerouslySetInnerHTML={bodyHtml} />;
case 'timestamp':
return (
<span style={commonStyles}>
{formatTimezoneAdjustedTimestamp(
cleanTimestamp,
'MM/DD/YYYY, HH:mm:ss.SSS (UTC Z)',
)}
</span>
);
default:
return (
<span style={commonStyles}>{removeEscapeCharacters(fieldData.value)}</span>
);
}
};
return (
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
{renderFieldContent()}
</CopyClipboardHOC>
{record.field === 'body' ? (
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
dangerouslySetInnerHTML={bodyHtml}
/>
</CopyClipboardHOC>
) : (
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
>
{removeEscapeCharacters(fieldData.value)}
</span>
</CopyClipboardHOC>
)}
{!isListViewPanel && (
<span className="action-btn">
<Tooltip title="Filter for value">

View File

@@ -6,13 +6,11 @@ import afterLogin from 'AppRoutes/utils';
import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { PayloadProps as PrecheckResultType } from 'types/api/user/loginPrecheck';
import AppReducer from 'types/reducer/app';
import { FormContainer, FormWrapper, Label, ParentContainer } from './styles';
@@ -37,7 +35,7 @@ function Login({
}: LoginProps): JSX.Element {
const { t } = useTranslation(['login']);
const [isLoading, setIsLoading] = useState<boolean>(false);
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const { user } = useAppContext();
const [precheckResult, setPrecheckResult] = useState<PrecheckResultType>({
sso: false,
@@ -158,20 +156,11 @@ function Login({
password,
});
if (response.statusCode === 200) {
await afterLogin(
afterLogin(
response.payload.userId,
response.payload.accessJwt,
response.payload.refreshJwt,
);
if (history?.location?.state) {
const historyState = history?.location?.state as any;
if (historyState?.from) {
history.push(historyState?.from);
} else {
history.push(ROUTES.APPLICATION);
}
}
} else {
notifications.error({
message: response.error || t('unexpected_error'),

View File

@@ -50,7 +50,6 @@ import {
} from 'lodash-es';
import { Sliders } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { useTimezone } from 'providers/Timezone';
import {
memo,
MutableRefObject,
@@ -292,6 +291,8 @@ function LogsExplorerViews({
},
);
console.log(data, isLoading, isFetching, isError, isSuccess);
const getRequestData = useCallback(
(
query: Query | null,
@@ -670,19 +671,13 @@ function LogsExplorerViews({
setIsLoadingQueries,
]);
const { timezone } = useTimezone();
const flattenLogData = useMemo(
() =>
logs.map((log) => {
const timestamp =
typeof log.timestamp === 'string'
? 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');
? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return FlatLogData({
timestamp,
@@ -690,7 +685,7 @@ function LogsExplorerViews({
...omit(log, 'timestamp', 'body'),
});
}),
[logs, timezone.value],
[logs],
);
return (

View File

@@ -1,27 +1,24 @@
import { render, RenderResult } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ROUTES from 'constants/routes';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import TimezoneProvider from 'providers/Timezone';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { VirtuosoMockContext } from 'react-virtuoso';
import i18n from 'ReactI18';
import store from 'store';
import { fireEvent, render, RenderResult } from 'tests/test-utils';
import LogsExplorerViews from '..';
import { logsQueryRangeSuccessNewFormatResponse } from './mock';
const logExplorerRoute = '/logs/logs-explorer';
const queryRangeURL = 'http://localhost/api/v3/query_range';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${ROUTES.LOGS_EXPLORER}`,
}),
}));
const lodsQueryServerRequest = (): void =>
server.use(
rest.post(queryRangeURL, (req, res, ctx) =>
@@ -87,29 +84,17 @@ beforeEach(() => {
const renderer = (): RenderResult =>
render(
<MemoryRouter initialEntries={[logExplorerRoute]}>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<MockQueryClientProvider>
<QueryBuilderProvider>
<TimezoneProvider>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</VirtuosoMockContext.Provider>
</TimezoneProvider>
</QueryBuilderProvider>
</MockQueryClientProvider>
</I18nextProvider>
</Provider>
</MemoryRouter>,
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</VirtuosoMockContext.Provider>,
);
describe('LogsExplorerViews -', () => {
@@ -118,7 +103,7 @@ describe('LogsExplorerViews -', () => {
const { queryByText, queryByTestId } = renderer();
expect(queryByTestId('periscope-btn')).toBeInTheDocument();
await userEvent.click(queryByTestId('periscope-btn') as HTMLElement);
fireEvent.click(queryByTestId('periscope-btn') as HTMLElement);
expect(document.querySelector('.menu-container')).toBeInTheDocument();
@@ -127,7 +112,7 @@ describe('LogsExplorerViews -', () => {
// switch to table view
// eslint-disable-next-line sonarjs/no-duplicate-string
await userEvent.click(queryByTestId('table-view') as HTMLElement);
fireEvent.click(queryByTestId('table-view') as HTMLElement);
expect(
queryByText(
@@ -146,7 +131,7 @@ describe('LogsExplorerViews -', () => {
const { queryByText, queryByTestId } = renderer();
// switch to table view
await userEvent.click(queryByTestId('table-view') as HTMLElement);
fireEvent.click(queryByTestId('table-view') as HTMLElement);
expect(queryByText('pending_data_placeholder')).toBeInTheDocument();
});
@@ -165,7 +150,7 @@ describe('LogsExplorerViews -', () => {
).toBeInTheDocument();
// switch to table view
await userEvent.click(queryByTestId('table-view') as HTMLElement);
fireEvent.click(queryByTestId('table-view') as HTMLElement);
expect(
queryByText('Something went wrong. Please try again or contact support.'),

View File

@@ -15,7 +15,6 @@ 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,
@@ -77,12 +76,7 @@ function LogsPanelComponent({
});
};
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns = getLogPanelColumnsList(
widget.selectedLogFields,
formatTimezoneAdjustedTimestamp,
);
const columns = getLogPanelColumnsList(widget.selectedLogFields);
const dataLength =
queryResponse.data?.payload?.data?.newResult?.data?.result[0]?.list?.length;

View File

@@ -1,7 +1,6 @@
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';
@@ -14,31 +13,18 @@ import { v4 as uuid } from 'uuid';
export const getLogPanelColumnsList = (
selectedLogFields: Widgets['selectedLogFields'],
formatTimezoneAdjustedTimestamp: (
input: TimestampInput,
format?: string,
) => string,
): ColumnsType<RowData> => {
const initialColumns: ColumnsType<RowData> = [];
const columns: ColumnsType<RowData> =
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 (
<Typography.Text>
{formatTimezoneAdjustedTimestamp(value as string)}
</Typography.Text>
);
}
if (name === 'body') {
return (
<Typography.Paragraph ellipsis={{ rows: 1 }} data-testid={name}>

View File

@@ -8,7 +8,6 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { routeConfig } from 'container/SideNav/config';
import { getQueryString } from 'container/SideNav/helper';
import useFeatureFlag from 'hooks/useFeatureFlag';
import useResourceAttribute from 'hooks/useResourceAttribute';
import {
convertRawQueriesToTraceSelectedTags,
@@ -19,6 +18,7 @@ import getStep from 'lib/getStep';
import history from 'lib/history';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { defaultTo } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
@@ -68,8 +68,10 @@ function Application(): JSX.Element {
const { queries } = useResourceAttribute();
const urlQuery = useUrlQuery();
const isSpanMetricEnabled = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS)
?.active;
const { featureFlags } = useAppContext();
const isSpanMetricEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_SPAN_METRICS)
?.active || false;
const handleSetTimeStamp = useCallback((selectTime: number) => {
setSelectedTimeStamp(selectTime);
@@ -185,7 +187,6 @@ function Application(): JSX.Element {
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.errorPercentage,
fillSpans: true,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
);
@@ -223,11 +224,12 @@ function Application(): JSX.Element {
apmToTraceQuery: Query,
isViewLogsClicked?: boolean,
): (() => void) => (): void => {
const endTime = timestamp;
const startTime = timestamp - stepInterval;
const currentTime = timestamp;
const endTime = timestamp + stepInterval;
console.log(endTime, stepInterval);
const urlParams = new URLSearchParams(search);
urlParams.set(QueryParams.startTime, startTime.toString());
urlParams.set(QueryParams.startTime, currentTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];

View File

@@ -10,10 +10,10 @@ import {
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
import useFeatureFlag from 'hooks/useFeatureFlag';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { resourceAttributesToTagFilterItems } from 'hooks/useResourceAttribute/utils';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { useAppContext } from 'providers/App/App';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { EQueryType } from 'types/common/dashboard';
@@ -40,8 +40,10 @@ function ServiceOverview({
const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);
const isSpanMetricEnable = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS)
?.active;
const { featureFlags } = useAppContext();
const isSpanMetricEnable =
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_SPAN_METRICS)
?.active || false;
const { queries } = useResourceAttribute();

View File

@@ -65,11 +65,11 @@ export function onViewTracePopupClick({
stepInterval,
}: OnViewTracePopupClickProps): VoidFunction {
return (): void => {
const endTime = timestamp;
const startTime = timestamp - (stepInterval || 60);
const currentTime = timestamp;
const endTime = timestamp + (stepInterval || 60);
const urlParams = new URLSearchParams(window.location.search);
urlParams.set(QueryParams.startTime, startTime.toString());
urlParams.set(QueryParams.startTime, currentTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];

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