Compare commits
37 Commits
v0.55.0-cl
...
v0.56.0-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b7455ac4c | ||
|
|
5a0a7c2c60 | ||
|
|
794d6fc0ca | ||
|
|
4c95df44d5 | ||
|
|
717545e14c | ||
|
|
e4d1452f5f | ||
|
|
88ace79a64 | ||
|
|
9b42326f80 | ||
|
|
44a3469b9b | ||
|
|
ef4b70f67b | ||
|
|
7a125e31ec | ||
|
|
6e3141a4ce | ||
|
|
fc8391c5aa | ||
|
|
55f653d92e | ||
|
|
35f8e133a9 | ||
|
|
58d6487f77 | ||
|
|
708158f50f | ||
|
|
0feab5aa93 | ||
|
|
b49ed913c7 | ||
|
|
419d2da363 | ||
|
|
df2844ea74 | ||
|
|
5e5f0f167f | ||
|
|
a6b05f0a3d | ||
|
|
f69aaa2cfb | ||
|
|
3866f89d3e | ||
|
|
f9ac41b865 | ||
|
|
c5b5bfe540 | ||
|
|
f3c01a5155 | ||
|
|
033b64a62a | ||
|
|
4aabfe7cf5 | ||
|
|
0218f701b2 | ||
|
|
f6d3f95768 | ||
|
|
cb1cd3555b | ||
|
|
ced72f86a4 | ||
|
|
54d5666b92 | ||
|
|
4edc6dbeae | ||
|
|
e203276678 |
@@ -38,7 +38,7 @@ Also, have a look at these [good first issues label](https://github.com/SigNoz/s
|
||||
## 1.1 For Creating Issue(s)
|
||||
Before making any significant changes and before filing a new issue, please check [existing open](https://github.com/SigNoz/signoz/issues?q=is%3Aopen+is%3Aissue), or [recently closed](https://github.com/SigNoz/signoz/issues?q=is%3Aissue+is%3Aclosed) issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can.
|
||||
|
||||
**Issue Types** - [Bug Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=) | [Feature Request](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=) | [Performance Issue Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=performance-issue-report.md&title=) | [Report a Security Vulnerability](https://github.com/SigNoz/signoz/security/policy)
|
||||
**Issue Types** - [Bug Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=) | [Feature Request](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=) | [Performance Issue Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=performance-issue-report.md&title=) | [Request Dashboard](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=dashboard-template&projects=&template=request_dashboard.md&title=%5BDashboard+Request%5D+) | [Report a Security Vulnerability](https://github.com/SigNoz/signoz/security/policy)
|
||||
|
||||
#### Details like these are incredibly useful:
|
||||
|
||||
@@ -57,7 +57,7 @@ Before making any significant changes and before filing a new issue, please chec
|
||||
Discussing your proposed changes ahead of time will make the contribution
|
||||
process smooth for everyone 🙌.
|
||||
|
||||
**[`^top^`](#)**
|
||||
**[`^top^`](#contributing-guidelines)**
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -98,13 +98,14 @@ GitHub provides additional document on [forking a repository](https://help.githu
|
||||
stability and quality of the component.
|
||||
|
||||
|
||||
You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [SLACK](https://signoz.io/slack).
|
||||
You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [slack community](https://signoz.io/slack).
|
||||
|
||||
### Pointers:
|
||||
- If you find any **bugs** → please create an [**issue.**](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=)
|
||||
- If you find anything **missing** in documentation → you can create an issue with the label **`documentation`**.
|
||||
- If you want to build any **new feature** → please create an [issue with the label **`enhancement`**.](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=)
|
||||
- If you want to **discuss** something about the product, start a new [**discussion**.](https://github.com/SigNoz/signoz/discussions)
|
||||
- If you want to request a new **dashboard template** → please create an issue [here](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=dashboard-template&projects=&template=request_dashboard.md&title=%5BDashboard+Request%5D+).
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -118,7 +119,7 @@ e.g. If you are submitting a fix for an issue in frontend, the PR name should be
|
||||
|
||||
- Feel free to ping us on [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) or [`#contributing-frontend`](https://signoz-community.slack.com/archives/C027134DM8B) on our slack community if you need any help on this :)
|
||||
|
||||
**[`^top^`](#)**
|
||||
**[`^top^`](#contributing-guidelines)**
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -128,14 +129,13 @@ e.g. If you are submitting a fix for an issue in frontend, the PR name should be
|
||||
|
||||
- [**Frontend**](#3-develop-frontend-) (Written in Typescript, React)
|
||||
- [**Backend**](#4-contribute-to-backend-query-service-) (Query Service, written in Go)
|
||||
- [**Dashboard Templates**](#6-contribute-to-dashboards-) (JSON dashboard templates built with SigNoz)
|
||||
|
||||
Depending upon your area of expertise & interest, you can choose one or more to contribute. Below are detailed instructions to contribute in each area.
|
||||
|
||||
**Please note:** If you want to work on an issue, please ask the maintainers to assign the issue to you before starting work on it. This would help us understand who is working on an issue and prevent duplicate work. 🙏🏻
|
||||
**Please note:** If you want to work on an issue, please add a brief description of your solution on the issue before starting work on it.
|
||||
|
||||
⚠️ If you just raise a PR, without the corresponding issue being assigned to you - it may not be accepted.
|
||||
|
||||
**[`^top^`](#)**
|
||||
**[`^top^`](#contributing-guidelines)**
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -189,7 +189,7 @@ Also, have a look at [Frontend README.md](https://github.com/SigNoz/signoz/blob/
|
||||
### Important Notes:
|
||||
The Maintainers / Contributors who will change Line Numbers of `Frontend` & `Query-Section`, please update line numbers in [`/.scripts/commentLinesForSetup.sh`](https://github.com/SigNoz/signoz/blob/develop/.scripts/commentLinesForSetup.sh)
|
||||
|
||||
**[`^top^`](#)**
|
||||
**[`^top^`](#contributing-guidelines)**
|
||||
|
||||
## 3.2 Contribute to Frontend without installing SigNoz backend
|
||||
|
||||
@@ -210,7 +210,7 @@ Please ping us in the [`#contributing`](https://signoz-community.slack.com/archi
|
||||
|
||||
**Frontend should now be accessible at** [`http://localhost:3301/services`](http://localhost:3301/services)
|
||||
|
||||
**[`^top^`](#)**
|
||||
**[`^top^`](#contributing-guidelines)**
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -310,7 +310,7 @@ Click the button below. A workspace with all required environments will be creat
|
||||
|
||||
> To use it on your forked repo, edit the 'Open in Gitpod' button URL to `https://gitpod.io/#https://github.com/<your-github-username>/signoz` -->
|
||||
|
||||
**[`^top^`](#)**
|
||||
**[`^top^`](#contributing-guidelines)**
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -366,7 +366,7 @@ curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-
|
||||
| HOTROD_NAMESPACE=sample-application bash
|
||||
```
|
||||
|
||||
**[`^top^`](#)**
|
||||
**[`^top^`](#contributing-guidelines)**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ const (
|
||||
SeasonalityWeekly Seasonality = "weekly"
|
||||
)
|
||||
|
||||
func (s Seasonality) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
var (
|
||||
oneWeekOffset = 24 * 7 * time.Hour.Milliseconds()
|
||||
oneDayOffset = 24 * time.Hour.Milliseconds()
|
||||
|
||||
@@ -67,6 +67,7 @@ func (p *BaseSeasonalProvider) getQueryParams(req *GetAnomaliesRequest) *anomaly
|
||||
}
|
||||
|
||||
func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQueryParams) (*anomalyQueryResults, error) {
|
||||
zap.L().Info("fetching results for current period", zap.Any("currentPeriodQuery", params.CurrentPeriodQuery))
|
||||
currentPeriodResults, _, err := p.querierV2.QueryRange(ctx, params.CurrentPeriodQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -77,6 +78,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQu
|
||||
return nil, err
|
||||
}
|
||||
|
||||
zap.L().Info("fetching results for past period", zap.Any("pastPeriodQuery", params.PastPeriodQuery))
|
||||
pastPeriodResults, _, err := p.querierV2.QueryRange(ctx, params.PastPeriodQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -87,6 +89,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQu
|
||||
return nil, err
|
||||
}
|
||||
|
||||
zap.L().Info("fetching results for current season", zap.Any("currentSeasonQuery", params.CurrentSeasonQuery))
|
||||
currentSeasonResults, _, err := p.querierV2.QueryRange(ctx, params.CurrentSeasonQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -97,6 +100,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQu
|
||||
return nil, err
|
||||
}
|
||||
|
||||
zap.L().Info("fetching results for past season", zap.Any("pastSeasonQuery", params.PastSeasonQuery))
|
||||
pastSeasonResults, _, err := p.querierV2.QueryRange(ctx, params.PastSeasonQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -107,6 +111,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQu
|
||||
return nil, err
|
||||
}
|
||||
|
||||
zap.L().Info("fetching results for past 2 season", zap.Any("past2SeasonQuery", params.Past2SeasonQuery))
|
||||
past2SeasonResults, _, err := p.querierV2.QueryRange(ctx, params.Past2SeasonQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -117,6 +122,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQu
|
||||
return nil, err
|
||||
}
|
||||
|
||||
zap.L().Info("fetching results for past 3 season", zap.Any("past3SeasonQuery", params.Past3SeasonQuery))
|
||||
past3SeasonResults, _, err := p.querierV2.QueryRange(ctx, params.Past3SeasonQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -184,7 +190,7 @@ func (p *BaseSeasonalProvider) getMovingAvg(series *v3.Series, movingAvgWindowSi
|
||||
return 0
|
||||
}
|
||||
if startIdx >= len(series.Points)-movingAvgWindowSize {
|
||||
startIdx = len(series.Points) - movingAvgWindowSize
|
||||
startIdx = int(math.Max(0, float64(len(series.Points)-movingAvgWindowSize)))
|
||||
}
|
||||
var sum float64
|
||||
points := series.Points[startIdx:]
|
||||
@@ -250,7 +256,7 @@ func (p *BaseSeasonalProvider) getPredictedSeries(
|
||||
// moving avg of the previous period series + z score threshold * std dev of the series
|
||||
// moving avg of the previous period series - z score threshold * std dev of the series
|
||||
func (p *BaseSeasonalProvider) getBounds(
|
||||
series, prevSeries, _, _, _, _ *v3.Series,
|
||||
series, predictedSeries *v3.Series,
|
||||
zScoreThreshold float64,
|
||||
) (*v3.Series, *v3.Series) {
|
||||
upperBoundSeries := &v3.Series{
|
||||
@@ -266,8 +272,8 @@ func (p *BaseSeasonalProvider) getBounds(
|
||||
}
|
||||
|
||||
for idx, curr := range series.Points {
|
||||
upperBound := p.getMovingAvg(prevSeries, movingAvgWindowSize, idx) + zScoreThreshold*p.getStdDev(series)
|
||||
lowerBound := p.getMovingAvg(prevSeries, movingAvgWindowSize, idx) - zScoreThreshold*p.getStdDev(series)
|
||||
upperBound := p.getMovingAvg(predictedSeries, movingAvgWindowSize, idx) + zScoreThreshold*p.getStdDev(series)
|
||||
lowerBound := p.getMovingAvg(predictedSeries, movingAvgWindowSize, idx) - zScoreThreshold*p.getStdDev(series)
|
||||
upperBoundSeries.Points = append(upperBoundSeries.Points, v3.Point{
|
||||
Timestamp: curr.Timestamp,
|
||||
Value: upperBound,
|
||||
@@ -431,11 +437,7 @@ func (p *BaseSeasonalProvider) getAnomalies(ctx context.Context, req *GetAnomali
|
||||
|
||||
upperBoundSeries, lowerBoundSeries := p.getBounds(
|
||||
series,
|
||||
pastPeriodSeries,
|
||||
currentSeasonSeries,
|
||||
pastSeasonSeries,
|
||||
past2SeasonSeries,
|
||||
past3SeasonSeries,
|
||||
predictedSeries,
|
||||
zScoreThreshold,
|
||||
)
|
||||
result.UpperBoundSeries = append(result.UpperBoundSeries, upperBoundSeries)
|
||||
|
||||
@@ -177,6 +177,8 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
am.ViewAccess(ah.listLicensesV2)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
|
||||
|
||||
// Gateway
|
||||
router.PathPrefix(gateway.RoutePrefix).HandlerFunc(am.AdminAccess(ah.ServeGatewayHTTP))
|
||||
|
||||
|
||||
119
ee/query-service/app/api/queryrange.go
Normal file
119
ee/query-service/app/api/queryrange.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.signoz.io/signoz/ee/query-service/anomaly"
|
||||
baseapp "go.signoz.io/signoz/pkg/query-service/app"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
bodyBytes, _ := io.ReadAll(r.Body)
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
queryRangeParams, apiErrorObj := baseapp.ParseQueryRangeParams(r)
|
||||
|
||||
if apiErrorObj != nil {
|
||||
zap.L().Error("error parsing metric query range params", zap.Error(apiErrorObj.Err))
|
||||
RespondError(w, apiErrorObj, nil)
|
||||
return
|
||||
}
|
||||
queryRangeParams.Version = "v4"
|
||||
|
||||
// add temporality for each metric
|
||||
temporalityErr := aH.PopulateTemporality(r.Context(), queryRangeParams)
|
||||
if temporalityErr != nil {
|
||||
zap.L().Error("Error while adding temporality for metrics", zap.Error(temporalityErr))
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
anomalyQueryExists := false
|
||||
anomalyQuery := &v3.BuilderQuery{}
|
||||
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
||||
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
|
||||
for _, fn := range query.Functions {
|
||||
if fn.Name == v3.FunctionNameAnomaly {
|
||||
anomalyQueryExists = true
|
||||
anomalyQuery = query
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if anomalyQueryExists {
|
||||
// ensure all queries have metric data source, and there should be only one anomaly query
|
||||
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
|
||||
if query.DataSource != v3.DataSourceMetrics {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("all queries must have metric data source")}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// get the threshold, and seasonality from the anomaly query
|
||||
var seasonality anomaly.Seasonality
|
||||
for _, fn := range anomalyQuery.Functions {
|
||||
if fn.Name == v3.FunctionNameAnomaly {
|
||||
seasonalityStr, ok := fn.NamedArgs["seasonality"].(string)
|
||||
if !ok {
|
||||
seasonalityStr = "daily"
|
||||
}
|
||||
if seasonalityStr == "weekly" {
|
||||
seasonality = anomaly.SeasonalityWeekly
|
||||
} else if seasonalityStr == "daily" {
|
||||
seasonality = anomaly.SeasonalityDaily
|
||||
} else {
|
||||
seasonality = anomaly.SeasonalityHourly
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
var provider anomaly.Provider
|
||||
switch seasonality {
|
||||
case anomaly.SeasonalityWeekly:
|
||||
provider = anomaly.NewWeeklyProvider(
|
||||
anomaly.WithCache[*anomaly.WeeklyProvider](aH.opts.Cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.WeeklyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.WeeklyProvider](aH.opts.DataConnector),
|
||||
anomaly.WithFeatureLookup[*anomaly.WeeklyProvider](aH.opts.FeatureFlags),
|
||||
)
|
||||
case anomaly.SeasonalityDaily:
|
||||
provider = anomaly.NewDailyProvider(
|
||||
anomaly.WithCache[*anomaly.DailyProvider](aH.opts.Cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.DailyProvider](aH.opts.DataConnector),
|
||||
anomaly.WithFeatureLookup[*anomaly.DailyProvider](aH.opts.FeatureFlags),
|
||||
)
|
||||
case anomaly.SeasonalityHourly:
|
||||
provider = anomaly.NewHourlyProvider(
|
||||
anomaly.WithCache[*anomaly.HourlyProvider](aH.opts.Cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.HourlyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.HourlyProvider](aH.opts.DataConnector),
|
||||
anomaly.WithFeatureLookup[*anomaly.HourlyProvider](aH.opts.FeatureFlags),
|
||||
)
|
||||
}
|
||||
anomalies, err := provider.GetAnomalies(r.Context(), &anomaly.GetAnomaliesRequest{Params: queryRangeParams})
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
uniqueResults := make(map[string]*v3.Result)
|
||||
for _, anomaly := range anomalies.Results {
|
||||
uniqueResults[anomaly.QueryName] = anomaly
|
||||
uniqueResults[anomaly.QueryName].IsAnomaly = true
|
||||
}
|
||||
aH.Respond(w, uniqueResults)
|
||||
} else {
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
aH.QueryRangeV4(w, r)
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,14 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var c cache.Cache
|
||||
if serverOptions.CacheConfigPath != "" {
|
||||
cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c = cache.NewCache(cacheOpts)
|
||||
}
|
||||
|
||||
<-readerReady
|
||||
rm, err := makeRulesManager(serverOptions.PromConfigPath,
|
||||
@@ -177,6 +185,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
serverOptions.RuleRepoURL,
|
||||
localDB,
|
||||
reader,
|
||||
c,
|
||||
serverOptions.DisableRules,
|
||||
lm,
|
||||
serverOptions.UseLogsNewSchema,
|
||||
@@ -237,15 +246,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
telemetry.GetInstance().SetReader(reader)
|
||||
telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey)
|
||||
|
||||
var c cache.Cache
|
||||
if serverOptions.CacheConfigPath != "" {
|
||||
cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c = cache.NewCache(cacheOpts)
|
||||
}
|
||||
|
||||
fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
|
||||
|
||||
if err != nil {
|
||||
@@ -732,6 +732,7 @@ func makeRulesManager(
|
||||
ruleRepoURL string,
|
||||
db *sqlx.DB,
|
||||
ch baseint.Reader,
|
||||
cache cache.Cache,
|
||||
disableRules bool,
|
||||
fm baseint.FeatureLookup,
|
||||
useLogsNewSchema bool) (*baserules.Manager, error) {
|
||||
@@ -760,6 +761,7 @@ func makeRulesManager(
|
||||
DisableRules: disableRules,
|
||||
FeatureFlags: fm,
|
||||
Reader: ch,
|
||||
Cache: cache,
|
||||
EvalDelay: baseconst.GetEvalDelay(),
|
||||
|
||||
PrepareTaskFunc: rules.PrepareTaskFunc,
|
||||
|
||||
@@ -13,7 +13,6 @@ const Onboarding = "ONBOARDING"
|
||||
const ChatSupport = "CHAT_SUPPORT"
|
||||
const Gateway = "GATEWAY"
|
||||
const PremiumSupport = "PREMIUM_SUPPORT"
|
||||
const QueryBuilderSearchV2 = "QUERY_BUILDER_SEARCH_V2"
|
||||
|
||||
var BasicPlan = basemodel.FeatureSet{
|
||||
basemodel.Feature{
|
||||
@@ -129,7 +128,7 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: QueryBuilderSearchV2,
|
||||
Name: basemodel.AnomalyDetection,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
@@ -244,8 +243,8 @@ var ProPlan = basemodel.FeatureSet{
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: QueryBuilderSearchV2,
|
||||
Active: false,
|
||||
Name: basemodel.AnomalyDetection,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
@@ -373,8 +372,8 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: QueryBuilderSearchV2,
|
||||
Active: false,
|
||||
Name: basemodel.AnomalyDetection,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
|
||||
393
ee/query-service/rules/anomaly.go
Normal file
393
ee/query-service/rules/anomaly.go
Normal file
@@ -0,0 +1,393 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"go.signoz.io/signoz/ee/query-service/anomaly"
|
||||
"go.signoz.io/signoz/pkg/query-service/cache"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
|
||||
querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
|
||||
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/utils/labels"
|
||||
"go.signoz.io/signoz/pkg/query-service/utils/times"
|
||||
"go.signoz.io/signoz/pkg/query-service/utils/timestamp"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/formatter"
|
||||
|
||||
baserules "go.signoz.io/signoz/pkg/query-service/rules"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
RuleTypeAnomaly = "anomaly_rule"
|
||||
)
|
||||
|
||||
type AnomalyRule struct {
|
||||
*baserules.BaseRule
|
||||
|
||||
mtx sync.Mutex
|
||||
|
||||
reader interfaces.Reader
|
||||
|
||||
// querierV2 is used for alerts created after the introduction of new metrics query builder
|
||||
querierV2 interfaces.Querier
|
||||
|
||||
provider anomaly.Provider
|
||||
|
||||
seasonality anomaly.Seasonality
|
||||
}
|
||||
|
||||
func NewAnomalyRule(
|
||||
id string,
|
||||
p *baserules.PostableRule,
|
||||
featureFlags interfaces.FeatureLookup,
|
||||
reader interfaces.Reader,
|
||||
cache cache.Cache,
|
||||
opts ...baserules.RuleOption,
|
||||
) (*AnomalyRule, error) {
|
||||
|
||||
zap.L().Info("creating new AnomalyRule", zap.String("id", id), zap.Any("opts", opts))
|
||||
|
||||
baseRule, err := baserules.NewBaseRule(id, p, reader, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := AnomalyRule{
|
||||
BaseRule: baseRule,
|
||||
}
|
||||
|
||||
switch strings.ToLower(p.RuleCondition.Seasonality) {
|
||||
case "hourly":
|
||||
t.seasonality = anomaly.SeasonalityHourly
|
||||
case "daily":
|
||||
t.seasonality = anomaly.SeasonalityDaily
|
||||
case "weekly":
|
||||
t.seasonality = anomaly.SeasonalityWeekly
|
||||
default:
|
||||
t.seasonality = anomaly.SeasonalityDaily
|
||||
}
|
||||
|
||||
zap.L().Info("using seasonality", zap.String("seasonality", t.seasonality.String()))
|
||||
|
||||
querierOptsV2 := querierV2.QuerierOptions{
|
||||
Reader: reader,
|
||||
Cache: cache,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
FeatureLookup: featureFlags,
|
||||
}
|
||||
|
||||
t.querierV2 = querierV2.NewQuerier(querierOptsV2)
|
||||
t.reader = reader
|
||||
if t.seasonality == anomaly.SeasonalityHourly {
|
||||
t.provider = anomaly.NewHourlyProvider(
|
||||
anomaly.WithCache[*anomaly.HourlyProvider](cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.HourlyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.HourlyProvider](reader),
|
||||
anomaly.WithFeatureLookup[*anomaly.HourlyProvider](featureFlags),
|
||||
)
|
||||
} else if t.seasonality == anomaly.SeasonalityDaily {
|
||||
t.provider = anomaly.NewDailyProvider(
|
||||
anomaly.WithCache[*anomaly.DailyProvider](cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.DailyProvider](reader),
|
||||
anomaly.WithFeatureLookup[*anomaly.DailyProvider](featureFlags),
|
||||
)
|
||||
} else if t.seasonality == anomaly.SeasonalityWeekly {
|
||||
t.provider = anomaly.NewWeeklyProvider(
|
||||
anomaly.WithCache[*anomaly.WeeklyProvider](cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.WeeklyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.WeeklyProvider](reader),
|
||||
anomaly.WithFeatureLookup[*anomaly.WeeklyProvider](featureFlags),
|
||||
)
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) Type() baserules.RuleType {
|
||||
return RuleTypeAnomaly
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) prepareQueryRange(ts time.Time) (*v3.QueryRangeParamsV3, error) {
|
||||
|
||||
zap.L().Info("prepareQueryRange", zap.Int64("ts", ts.UnixMilli()), zap.Int64("evalWindow", r.EvalWindow().Milliseconds()), zap.Int64("evalDelay", r.EvalDelay().Milliseconds()))
|
||||
|
||||
start := ts.Add(-time.Duration(r.EvalWindow())).UnixMilli()
|
||||
end := ts.UnixMilli()
|
||||
|
||||
if r.EvalDelay() > 0 {
|
||||
start = start - int64(r.EvalDelay().Milliseconds())
|
||||
end = end - int64(r.EvalDelay().Milliseconds())
|
||||
}
|
||||
// round to minute otherwise we could potentially miss data
|
||||
start = start - (start % (60 * 1000))
|
||||
end = end - (end % (60 * 1000))
|
||||
|
||||
compositeQuery := r.Condition().CompositeQuery
|
||||
|
||||
if compositeQuery.PanelType != v3.PanelTypeGraph {
|
||||
compositeQuery.PanelType = v3.PanelTypeGraph
|
||||
}
|
||||
|
||||
// default mode
|
||||
return &v3.QueryRangeParamsV3{
|
||||
Start: start,
|
||||
End: end,
|
||||
Step: int64(math.Max(float64(common.MinAllowedStepInterval(start, end)), 60)),
|
||||
CompositeQuery: compositeQuery,
|
||||
Variables: make(map[string]interface{}, 0),
|
||||
NoCache: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) GetSelectedQuery() string {
|
||||
return r.Condition().GetSelectedQueryName()
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (baserules.Vector, error) {
|
||||
|
||||
params, err := r.prepareQueryRange(ts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = r.PopulateTemporality(ctx, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal error while setting temporality")
|
||||
}
|
||||
|
||||
anomalies, err := r.provider.GetAnomalies(ctx, &anomaly.GetAnomaliesRequest{
|
||||
Params: params,
|
||||
Seasonality: r.seasonality,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var queryResult *v3.Result
|
||||
for _, result := range anomalies.Results {
|
||||
if result.QueryName == r.GetSelectedQuery() {
|
||||
queryResult = result
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var resultVector baserules.Vector
|
||||
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
zap.L().Info("anomaly scores", zap.String("scores", string(scoresJSON)))
|
||||
|
||||
for _, series := range queryResult.AnomalyScores {
|
||||
smpl, shouldAlert := r.ShouldAlert(*series)
|
||||
if shouldAlert {
|
||||
resultVector = append(resultVector, smpl)
|
||||
}
|
||||
}
|
||||
return resultVector, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, error) {
|
||||
|
||||
prevState := r.State()
|
||||
|
||||
valueFormatter := formatter.FromUnit(r.Unit())
|
||||
res, err := r.buildAndRunQuery(ctx, ts)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
resultFPs := map[uint64]struct{}{}
|
||||
var alerts = make(map[uint64]*baserules.Alert, len(res))
|
||||
|
||||
for _, smpl := range res {
|
||||
l := make(map[string]string, len(smpl.Metric))
|
||||
for _, lbl := range smpl.Metric {
|
||||
l[lbl.Name] = lbl.Value
|
||||
}
|
||||
|
||||
value := valueFormatter.Format(smpl.V, r.Unit())
|
||||
threshold := valueFormatter.Format(r.TargetVal(), r.Unit())
|
||||
zap.L().Debug("Alert template data for rule", zap.String("name", r.Name()), zap.String("formatter", valueFormatter.Name()), zap.String("value", value), zap.String("threshold", threshold))
|
||||
|
||||
tmplData := baserules.AlertTemplateData(l, value, threshold)
|
||||
// Inject some convenience variables that are easier to remember for users
|
||||
// who are not used to Go's templating system.
|
||||
defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"
|
||||
|
||||
// utility function to apply go template on labels and annotations
|
||||
expand := func(text string) string {
|
||||
|
||||
tmpl := baserules.NewTemplateExpander(
|
||||
ctx,
|
||||
defs+text,
|
||||
"__alert_"+r.Name(),
|
||||
tmplData,
|
||||
times.Time(timestamp.FromTime(ts)),
|
||||
nil,
|
||||
)
|
||||
result, err := tmpl.Expand()
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("<error expanding template: %s>", err)
|
||||
zap.L().Error("Expanding alert template failed", zap.Error(err), zap.Any("data", tmplData))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
lb := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel)
|
||||
resultLabels := labels.NewBuilder(smpl.MetricOrig).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel).Labels()
|
||||
|
||||
for name, value := range r.Labels().Map() {
|
||||
lb.Set(name, expand(value))
|
||||
}
|
||||
|
||||
lb.Set(labels.AlertNameLabel, r.Name())
|
||||
lb.Set(labels.AlertRuleIdLabel, r.ID())
|
||||
lb.Set(labels.RuleSourceLabel, r.GeneratorURL())
|
||||
|
||||
annotations := make(labels.Labels, 0, len(r.Annotations().Map()))
|
||||
for name, value := range r.Annotations().Map() {
|
||||
annotations = append(annotations, labels.Label{Name: common.NormalizeLabelName(name), Value: expand(value)})
|
||||
}
|
||||
if smpl.IsMissing {
|
||||
lb.Set(labels.AlertNameLabel, "[No data] "+r.Name())
|
||||
}
|
||||
|
||||
lbs := lb.Labels()
|
||||
h := lbs.Hash()
|
||||
resultFPs[h] = struct{}{}
|
||||
|
||||
if _, ok := alerts[h]; ok {
|
||||
zap.L().Error("the alert query returns duplicate records", zap.String("ruleid", r.ID()), zap.Any("alert", alerts[h]))
|
||||
err = fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alerts[h] = &baserules.Alert{
|
||||
Labels: lbs,
|
||||
QueryResultLables: resultLabels,
|
||||
Annotations: annotations,
|
||||
ActiveAt: ts,
|
||||
State: model.StatePending,
|
||||
Value: smpl.V,
|
||||
GeneratorURL: r.GeneratorURL(),
|
||||
Receivers: r.PreferredChannels(),
|
||||
Missing: smpl.IsMissing,
|
||||
}
|
||||
}
|
||||
|
||||
zap.L().Info("number of alerts found", zap.String("name", r.Name()), zap.Int("count", len(alerts)))
|
||||
|
||||
// alerts[h] is ready, add or update active list now
|
||||
for h, a := range alerts {
|
||||
// Check whether we already have alerting state for the identifying label set.
|
||||
// Update the last value and annotations if so, create a new alert entry otherwise.
|
||||
if alert, ok := r.Active[h]; ok && alert.State != model.StateInactive {
|
||||
|
||||
alert.Value = a.Value
|
||||
alert.Annotations = a.Annotations
|
||||
alert.Receivers = r.PreferredChannels()
|
||||
continue
|
||||
}
|
||||
|
||||
r.Active[h] = a
|
||||
}
|
||||
|
||||
itemsToAdd := []model.RuleStateHistory{}
|
||||
|
||||
// Check if any pending alerts should be removed or fire now. Write out alert timeseries.
|
||||
for fp, a := range r.Active {
|
||||
labelsJSON, err := json.Marshal(a.QueryResultLables)
|
||||
if err != nil {
|
||||
zap.L().Error("error marshaling labels", zap.Error(err), zap.Any("labels", a.Labels))
|
||||
}
|
||||
if _, ok := resultFPs[fp]; !ok {
|
||||
// If the alert was previously firing, keep it around for a given
|
||||
// retention time so it is reported as resolved to the AlertManager.
|
||||
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > baserules.ResolvedRetention) {
|
||||
delete(r.Active, fp)
|
||||
}
|
||||
if a.State != model.StateInactive {
|
||||
a.State = model.StateInactive
|
||||
a.ResolvedAt = ts
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: model.StateInactive,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if a.State == model.StatePending && ts.Sub(a.ActiveAt) >= r.HoldDuration() {
|
||||
a.State = model.StateFiring
|
||||
a.FiredAt = ts
|
||||
state := model.StateFiring
|
||||
if a.Missing {
|
||||
state = model.StateNoData
|
||||
}
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
currentState := r.State()
|
||||
|
||||
overallStateChanged := currentState != prevState
|
||||
for idx, item := range itemsToAdd {
|
||||
item.OverallStateChanged = overallStateChanged
|
||||
item.OverallState = currentState
|
||||
itemsToAdd[idx] = item
|
||||
}
|
||||
|
||||
r.RecordRuleStateHistory(ctx, prevState, currentState, itemsToAdd)
|
||||
|
||||
return len(r.Active), nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) String() string {
|
||||
|
||||
ar := baserules.PostableRule{
|
||||
AlertName: r.Name(),
|
||||
RuleCondition: r.Condition(),
|
||||
EvalWindow: baserules.Duration(r.EvalWindow()),
|
||||
Labels: r.Labels().Map(),
|
||||
Annotations: r.Annotations().Map(),
|
||||
PreferredChannels: r.PreferredChannels(),
|
||||
}
|
||||
|
||||
byt, err := yaml.Marshal(ar)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error marshaling alerting rule: %s", err.Error())
|
||||
}
|
||||
|
||||
return string(byt)
|
||||
}
|
||||
@@ -53,6 +53,25 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
// create promql rule task for evalution
|
||||
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||
|
||||
} else if opts.Rule.RuleType == baserules.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
ar, err := NewAnomalyRule(
|
||||
ruleId,
|
||||
opts.Rule,
|
||||
opts.FF,
|
||||
opts.Reader,
|
||||
opts.Cache,
|
||||
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
||||
)
|
||||
if err != nil {
|
||||
return task, err
|
||||
}
|
||||
|
||||
rules = append(rules, ar)
|
||||
|
||||
// create anomaly rule task for evalution
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported rule type. Supported types: %s, %s", baserules.RuleTypeProm, baserules.RuleTypeThreshold)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2048_2251)">
|
||||
<path opacity="0.9" d="M8.02226 15.9866C3.56539 15.9866 -6.10352e-05 12.4896 -6.10352e-05 8.11832C-6.10352e-05 3.79075 3.56539 0.25 8.02226 0.25H13.0584C14.7075 0.25 15.9999 1.56139 15.9999 3.13506V8.11832C15.9999 12.4896 12.4345 15.9866 8.02226 15.9866Z" fill="#F25733"/>
|
||||
<path d="M7.95919 4.71207C4.63025 4.71207 2.75514 7.46868 2.67693 7.58603C2.48413 7.87508 2.48413 8.24888 2.67707 8.53816C2.75514 8.65528 4.63025 11.4119 7.95919 11.4119C11.2881 11.4119 13.1633 8.65528 13.2414 8.53792C13.4342 8.24888 13.4342 7.87508 13.2413 7.58582C13.1632 7.46868 11.2881 4.71207 7.95919 4.71207ZM3.13771 8.23088C3.06925 8.12832 3.06925 7.99571 3.13771 7.89307C3.20059 7.79867 4.53564 5.83764 6.92256 5.36723C5.84092 5.78476 5.07127 6.83485 5.07127 8.062C5.07127 9.28912 5.84092 10.3392 6.92256 10.7567C4.53564 10.2863 3.20059 8.32528 3.13771 8.23088ZM6.62838 8.062C6.62838 8.21488 6.50443 8.3388 6.35151 8.3388C6.19859 8.3388 6.07465 8.21488 6.07465 8.062C6.07465 7.02287 6.92003 6.17748 7.95916 6.17748C8.11207 6.17748 8.23599 6.30141 8.23599 6.45434C8.23599 6.60727 8.11207 6.73119 7.95916 6.73119C7.22535 6.73119 6.62838 7.32815 6.62838 8.062ZM7.95919 8.73504C7.58803 8.73504 7.2861 8.43312 7.2861 8.062C7.2861 7.69085 7.58803 7.3889 7.95919 7.3889C8.33039 7.3889 8.63231 7.69083 8.63231 8.062C8.63231 8.43312 8.33039 8.73504 7.95919 8.73504ZM12.7806 8.23088C12.7178 8.32528 11.3827 10.2863 8.99583 10.7567C10.0775 10.3392 10.8471 9.28912 10.8471 8.062C10.8471 6.83487 10.0775 5.78477 8.99583 5.36724C11.3827 5.83768 12.7178 7.7987 12.7806 7.89307C12.8491 7.99571 12.8491 8.12832 12.7806 8.23088Z" fill="#F9F2F9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2048_2251">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="none"><rect width="100" height="100" fill="url(#a)" rx="20"/><g fill="#fff" fill-rule="evenodd" clip-rule="evenodd" filter="url(#b)"><path d="M11 49.941v-.003l.002-.005.003-.014.007-.035a8.37 8.37 0 0 1 .105-.42c.073-.263.184-.624.348-1.072.328-.896.866-2.135 1.73-3.617 1.732-2.97 4.753-6.883 9.95-10.955 5.223-4.092 10.295-6.293 14.08-7.471a35.328 35.328 0 0 1 4.585-1.114 23.628 23.628 0 0 1 1.687-.223 9.17 9.17 0 0 1 .108-.009l.034-.002h.011l.007-.001s.002 0 .133 2.217c.13 2.218.132 2.218.132 2.218h-.002l-.053.004a19.098 19.098 0 0 0-1.326.178c-.809.136-1.937.37-3.302.763l-.127.043c-2.745.94-6.666 2.775-11.249 6.362-4.572 3.577-7.142 6.95-8.563 9.393-.711 1.222-1.137 2.215-1.383 2.889a9.995 9.995 0 0 0-.29.933c.008.037.022.095.044.173.046.166.123.423.246.76.246.674.672 1.667 1.383 2.89 1.421 2.441 3.991 5.815 8.563 9.392 4.584 3.587 8.504 5.423 11.25 6.362l.126.043c1.365.393 2.493.627 3.302.763a19.098 19.098 0 0 0 1.326.178l.053.004h.002s-.002 0-.133 2.218C43.66 75 43.657 75 43.657 75h-.007l-.011-.001-.034-.002a9.17 9.17 0 0 1-.478-.046 23.628 23.628 0 0 1-1.317-.186 35.328 35.328 0 0 1-4.584-1.114c-3.786-1.178-8.858-3.38-14.081-7.471-5.197-4.072-8.218-7.985-9.95-10.955-.864-1.482-1.402-2.72-1.73-3.617-.164-.448-.275-.81-.348-1.072a8.37 8.37 0 0 1-.105-.42l-.007-.035-.003-.014-.002-.005v-.121Zm78 0v-.003l-.002-.005-.002-.014-.008-.035a8.532 8.532 0 0 0-.105-.42 14.049 14.049 0 0 0-.348-1.072c-.328-.896-.866-2.135-1.73-3.617-1.732-2.97-4.753-6.883-9.95-10.955-5.223-4.092-10.295-6.293-14.08-7.471a35.328 35.328 0 0 0-4.585-1.114 23.628 23.628 0 0 0-1.687-.223 9.17 9.17 0 0 0-.108-.009l-.034-.002h-.011L56.343 25s-.002 0-.133 2.217c-.13 2.218-.132 2.218-.132 2.218h.002l.053.004a19.098 19.098 0 0 1 1.326.178c.809.136 1.937.37 3.302.763l.127.043c2.745.94 6.666 2.775 11.249 6.362 4.572 3.577 7.141 6.95 8.563 9.393.711 1.222 1.137 2.215 1.383 2.889a9.995 9.995 0 0 1 .29.933 9.995 9.995 0 0 1-.29.934c-.246.673-.672 1.666-1.383 2.888-1.422 2.442-3.991 5.816-8.563 9.393-4.584 3.587-8.504 5.423-11.25 6.362l-.126.043a30.108 30.108 0 0 1-3.302.763 19.098 19.098 0 0 1-1.326.178l-.053.004h-.002s.002 0 .133 2.218C56.34 75 56.343 75 56.343 75h.007l.011-.001.034-.002a9.17 9.17 0 0 0 .478-.046c.314-.034.758-.092 1.317-.186a35.328 35.328 0 0 0 4.584-1.114c3.786-1.178 8.858-3.38 14.081-7.471 5.197-4.072 8.218-7.985 9.95-10.955.864-1.482 1.402-2.72 1.73-3.617.164-.448.275-.81.348-1.072a8.532 8.532 0 0 0 .105-.42l.008-.035.002-.014.001-.005.001-.003v-.118Z"/><path d="M68.342 49.998c0 9.846-7.924 17.827-17.7 17.827-9.775 0-17.7-7.981-17.7-17.827 0-9.846 7.925-17.827 17.7-17.827 9.776 0 17.7 7.981 17.7 17.827ZM46.218 39.97s-2.127 2.508-2.766 4.457c-.412 1.257-.553 3.343-.553 3.343h-5.531s0-1.672 1.106-4.457c1.106-2.786 2.212-3.343 2.212-3.343h5.532Zm-2.766 15.6c.639 1.949 2.766 4.457 2.766 4.457h-5.532s-1.106-.557-2.212-3.343c-1.106-2.785-1.106-4.457-1.106-4.457h5.53s.142 2.086.554 3.343Z"/></g><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="matrix(40.99997 42 -42 40.99997 50 50)" gradientUnits="userSpaceOnUse"><stop offset=".33" stop-color="#F76526"/><stop offset="1" stop-color="#F43030"/></radialGradient><filter id="b" width="90" height="62" x="5" y="23" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="4"/><feGaussianBlur stdDeviation="3"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0.368384 0 0 0 0 0.0623777 0 0 0 0 0.0623777 0 0 0 0.25 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_3909_18731"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_3909_18731" result="shape"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="4"/><feGaussianBlur stdDeviation="3"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/><feBlend in2="shape" result="effect2_innerShadow_3909_18731"/></filter></defs></svg>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 13 KiB |
@@ -53,6 +53,7 @@
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_last": "last",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_last": "last",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_last": "last",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_last": "last",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 10 KiB |
@@ -12,6 +12,7 @@ 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 { NotificationProvider } from 'hooks/useNotifications';
|
||||
@@ -58,23 +59,16 @@ function App(): JSX.Element {
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const isOnboardingEnabled =
|
||||
useFeatureFlags(FeatureKeys.ONBOARDING)?.active || false;
|
||||
|
||||
const isChatSupportEnabled =
|
||||
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
|
||||
|
||||
const isPremiumSupportEnabled =
|
||||
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
||||
|
||||
const featureResponse = useGetFeatureFlag((allFlags) => {
|
||||
const isOnboardingEnabled =
|
||||
allFlags.find((flag) => flag.name === FeatureKeys.ONBOARDING)?.active ||
|
||||
false;
|
||||
|
||||
const isChatSupportEnabled =
|
||||
allFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)?.active ||
|
||||
false;
|
||||
|
||||
const isPremiumSupportEnabled =
|
||||
allFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)?.active ||
|
||||
false;
|
||||
|
||||
const showAddCreditCardModal =
|
||||
!isPremiumSupportEnabled &&
|
||||
!licenseData?.payload?.trialConvertedToSubscription;
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_FEATURE_FLAG_RESPONSE,
|
||||
payload: {
|
||||
@@ -90,16 +84,6 @@ function App(): JSX.Element {
|
||||
|
||||
setRoutes(newRoutes);
|
||||
}
|
||||
|
||||
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.Intercom('boot', {
|
||||
app_id: process.env.INTERCOM_APP_ID,
|
||||
email: user?.email || '',
|
||||
name: user?.name || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const isOnBasicPlan =
|
||||
@@ -201,6 +185,26 @@ function App(): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
const showAddCreditCardModal =
|
||||
!isPremiumSupportEnabled &&
|
||||
!licenseData?.payload?.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,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && user?.email && user?.userId && user?.name) {
|
||||
try {
|
||||
|
||||
@@ -12,6 +12,20 @@ beforeAll(() => {
|
||||
matchMedia();
|
||||
});
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('react-dnd', () => ({
|
||||
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
|
||||
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
|
||||
|
||||
@@ -2,6 +2,14 @@ export const VIEW_TYPES = {
|
||||
OVERVIEW: 'OVERVIEW',
|
||||
JSON: 'JSON',
|
||||
CONTEXT: 'CONTEXT',
|
||||
INFRAMETRICS: 'INFRAMETRICS',
|
||||
} as const;
|
||||
|
||||
export type VIEWS = typeof VIEW_TYPES[keyof typeof VIEW_TYPES];
|
||||
|
||||
export const RESOURCE_KEYS = {
|
||||
CLUSTER_NAME: 'k8s.cluster.name',
|
||||
POD_NAME: 'k8s.pod.name',
|
||||
NODE_NAME: 'k8s.node.name',
|
||||
HOST_NAME: 'host.name',
|
||||
} as const;
|
||||
|
||||
@@ -9,6 +9,7 @@ import cx from 'classnames';
|
||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
||||
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
|
||||
import JSONView from 'container/LogDetailedView/JsonView';
|
||||
import Overview from 'container/LogDetailedView/Overview';
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import {
|
||||
BarChart2,
|
||||
Braces,
|
||||
Copy,
|
||||
Filter,
|
||||
@@ -36,7 +38,7 @@ import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||
import { LogDetailProps } from './LogDetail.interfaces';
|
||||
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
|
||||
|
||||
@@ -192,6 +194,17 @@ function LogDetail({
|
||||
Context
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.INFRAMETRICS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<BarChart2 size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
{selectedView === VIEW_TYPES.JSON && (
|
||||
@@ -246,6 +259,15 @@ function LogDetail({
|
||||
isEdit={isEdit}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.INFRAMETRICS && (
|
||||
<InfraMetrics
|
||||
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
|
||||
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
|
||||
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
|
||||
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
|
||||
logLineTimestamp={log.timestamp.toString()}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,26 +22,21 @@
|
||||
}
|
||||
|
||||
&.INFO {
|
||||
background-color: var(--bg-slate-400);
|
||||
background-color: var(--bg-robin-500);
|
||||
}
|
||||
|
||||
&.WARNING,
|
||||
&.WARN {
|
||||
background-color: var(--bg-amber-500);
|
||||
}
|
||||
|
||||
&.ERROR {
|
||||
background-color: var(--bg-cherry-500);
|
||||
}
|
||||
|
||||
&.TRACE {
|
||||
background-color: var(--bg-robin-300);
|
||||
background-color: var(--bg-forest-400);
|
||||
}
|
||||
|
||||
&.DEBUG {
|
||||
background-color: var(--bg-forest-500);
|
||||
background-color: var(--bg-aqua-500);
|
||||
}
|
||||
|
||||
&.FATAL {
|
||||
background-color: var(--bg-sakura-500);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ function WelcomeLeftContainer({
|
||||
<Container>
|
||||
<LeftContainer direction="vertical">
|
||||
<Space align="center">
|
||||
<Logo src="signoz-signup.svg" alt="logo" />
|
||||
<Logo src="/Logos/signoz-brand-logo.svg" alt="logo" />
|
||||
<Title style={{ fontSize: '46px', margin: 0 }}>SigNoz</Title>
|
||||
</Space>
|
||||
<Typography>{t('monitor_signup')}</Typography>
|
||||
|
||||
@@ -6,7 +6,6 @@ export const AUTH0_REDIRECT_PATH = '/redirect';
|
||||
|
||||
export const DEFAULT_AUTH0_APP_REDIRECTION_PATH = ROUTES.APPLICATION;
|
||||
|
||||
export const IS_SIDEBAR_COLLAPSED = 'isSideBarCollapsed';
|
||||
export const INVITE_MEMBERS_HASH = '#invite-team-members';
|
||||
|
||||
export const SIGNOZ_UPGRADE_PLAN_URL =
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
|
||||
const userOS = getUserOperatingSystem();
|
||||
export const GlobalShortcuts = {
|
||||
SidebarCollapse: '\\+meta',
|
||||
NavigateToServices: 's+shift',
|
||||
NavigateToTraces: 't+shift',
|
||||
NavigateToLogs: 'l+shift',
|
||||
@@ -13,7 +9,6 @@ export const GlobalShortcuts = {
|
||||
};
|
||||
|
||||
export const GlobalShortcutsName = {
|
||||
SidebarCollapse: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+\\`,
|
||||
NavigateToServices: 'shift+s',
|
||||
NavigateToTraces: 'shift+t',
|
||||
NavigateToLogs: 'shift+l',
|
||||
@@ -24,7 +19,6 @@ export const GlobalShortcutsName = {
|
||||
};
|
||||
|
||||
export const GlobalShortcutsDescription = {
|
||||
SidebarCollapse: 'Collpase the sidebar',
|
||||
NavigateToServices: 'Navigate to Services page',
|
||||
NavigateToTraces: 'Navigate to Traces page',
|
||||
NavigateToLogs: 'Navigate to logs page',
|
||||
|
||||
@@ -16,12 +16,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.docked {
|
||||
.app-content {
|
||||
width: calc(100% - 240px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-support-gateway {
|
||||
|
||||
@@ -5,13 +5,11 @@ import './AppLayout.styles.scss';
|
||||
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Flex } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||
import getUserVersion from 'api/user/getVersion';
|
||||
import cx from 'classnames';
|
||||
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import SideNav from 'container/SideNav';
|
||||
@@ -22,22 +20,13 @@ import useLicense from 'hooks/useLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Dispatch } from 'redux';
|
||||
import { sideBarCollapse } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
@@ -59,10 +48,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const [collapsed, setCollapsed] = useState<boolean>(
|
||||
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@@ -117,14 +102,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const latestCurrentCounter = useRef(0);
|
||||
const latestVersionCounter = useRef(0);
|
||||
|
||||
const onCollapse = useCallback(() => {
|
||||
setCollapsed((collapsed) => !collapsed);
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
dispatch(sideBarCollapse(collapsed));
|
||||
}, [collapsed, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
getUserLatestVersionResponse.isFetched &&
|
||||
@@ -279,23 +256,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
}
|
||||
}, [isDarkMode]);
|
||||
|
||||
const isSideNavCollapsed = getLocalStorageKey(IS_SIDEBAR_COLLAPSED);
|
||||
|
||||
/**
|
||||
* Note: Right now we don't have a page-level method to pass the sidebar collapse state.
|
||||
* Since the use case for overriding is not widely needed, we are setting it here
|
||||
* so that the workspace locked page will have an expanded sidebar regardless of how users
|
||||
* have set it or what is stored in localStorage. This will not affect the localStorage config.
|
||||
*/
|
||||
const isWorkspaceLocked = pathname === ROUTES.WORKSPACE_LOCKED;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
className={cx(
|
||||
isDarkMode ? 'darkMode' : 'lightMode',
|
||||
isSideNavCollapsed ? 'sidebarCollapsed' : '',
|
||||
)}
|
||||
>
|
||||
<Layout className={cx(isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||
<Helmet>
|
||||
<title>{pageTitle}</title>
|
||||
</Helmet>
|
||||
@@ -321,25 +283,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
className={cx(
|
||||
'app-layout',
|
||||
isDarkMode ? 'darkMode' : 'lightMode',
|
||||
!collapsed && !renderFullScreen ? 'docked' : '',
|
||||
)}
|
||||
>
|
||||
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||
{isToDisplayLayout && !renderFullScreen && (
|
||||
<SideNav
|
||||
licenseData={licenseData}
|
||||
isFetching={isFetching}
|
||||
onCollapse={onCollapse}
|
||||
collapsed={isWorkspaceLocked ? false : collapsed}
|
||||
/>
|
||||
<SideNav licenseData={licenseData} isFetching={isFetching} />
|
||||
)}
|
||||
<div
|
||||
className={cx('app-content', collapsed ? 'collapsed' : '')}
|
||||
data-overlayscrollbars-initialize
|
||||
>
|
||||
<div className="app-content" data-overlayscrollbars-initialize>
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<LayoutContent data-overlayscrollbars-initialize>
|
||||
<OverlayScrollbar>
|
||||
|
||||
@@ -103,6 +103,7 @@ function RuleOptions({
|
||||
<Select.Option value="2">{t('option_allthetimes')}</Select.Option>
|
||||
<Select.Option value="3">{t('option_onaverage')}</Select.Option>
|
||||
<Select.Option value="4">{t('option_intotal')}</Select.Option>
|
||||
<Select.Option value="5">{t('option_last')}</Select.Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
|
||||
|
||||
@@ -370,7 +370,10 @@ function FormAlertRules({
|
||||
});
|
||||
|
||||
// invalidate rule in cache
|
||||
ruleCache.invalidateQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]);
|
||||
ruleCache.invalidateQueries([
|
||||
REACT_QUERY_KEY.ALERT_RULE_DETAILS,
|
||||
`${ruleId}`,
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
box-sizing: border-box;
|
||||
margin: 16px 0;
|
||||
border-radius: 3px;
|
||||
|
||||
.global-search {
|
||||
.ant-input-group-addon {
|
||||
border: none;
|
||||
background-color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.height-widget {
|
||||
@@ -55,3 +62,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.full-view-container {
|
||||
.graph-container {
|
||||
.global-search {
|
||||
.ant-input-group-addon {
|
||||
background-color: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import './WidgetFullView.styles.scss';
|
||||
|
||||
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { Button, Spin } from 'antd';
|
||||
import {
|
||||
LoadingOutlined,
|
||||
SearchOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input, Spin } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
@@ -172,6 +176,10 @@ function FullView({
|
||||
|
||||
const isListView = widget.panelTypes === PANEL_TYPES.LIST;
|
||||
|
||||
const isTablePanel = widget.panelTypes === PANEL_TYPES.TABLE;
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
|
||||
if (response.isLoading && widget.panelTypes !== PANEL_TYPES.LIST) {
|
||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||
}
|
||||
@@ -216,6 +224,18 @@ function FullView({
|
||||
}}
|
||||
isGraphLegendToggleAvailable={canModifyChart}
|
||||
>
|
||||
{isTablePanel && (
|
||||
<Input
|
||||
addonBefore={<SearchOutlined size={14} />}
|
||||
className="global-search"
|
||||
placeholder="Search..."
|
||||
allowClear
|
||||
key={widget.id}
|
||||
onChange={(e): void => {
|
||||
setSearchTerm(e.target.value || '');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<PanelWrapper
|
||||
queryResponse={response}
|
||||
widget={widget}
|
||||
@@ -226,6 +246,7 @@ function FullView({
|
||||
graphVisibility={graphsVisibilityStates}
|
||||
onDragSelect={onDragSelect}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</div>
|
||||
|
||||
@@ -234,6 +234,8 @@ function WidgetGraphComponent({
|
||||
});
|
||||
};
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
|
||||
const loadingState =
|
||||
(queryResponse.isLoading || queryResponse.status === 'idle') &&
|
||||
widget.panelTypes !== PANEL_TYPES.LIST;
|
||||
@@ -317,6 +319,7 @@ function WidgetGraphComponent({
|
||||
isWarning={isWarning}
|
||||
isFetchingResponse={isFetchingResponse}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
setSearchTerm={setSearchTerm}
|
||||
/>
|
||||
</div>
|
||||
{queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && (
|
||||
@@ -337,6 +340,7 @@ function WidgetGraphComponent({
|
||||
onDragSelect={onDragSelect}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
customTooltipElement={customTooltipElement}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
@@ -10,6 +10,14 @@
|
||||
font-weight: 600;
|
||||
|
||||
cursor: move;
|
||||
|
||||
.ant-input-group-addon {
|
||||
border: none;
|
||||
background-color: var(--bg-ink-500);
|
||||
}
|
||||
.search-header-icons {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-header-title {
|
||||
@@ -19,6 +27,7 @@
|
||||
.widget-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.widget-header-more-options {
|
||||
visibility: hidden;
|
||||
@@ -30,6 +39,10 @@
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.widget-header-more-options-visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.widget-header-hover {
|
||||
visibility: visible;
|
||||
}
|
||||
@@ -37,3 +50,11 @@
|
||||
.widget-api-actions {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.widget-header-container {
|
||||
.ant-input-group-addon {
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
ExclamationCircleOutlined,
|
||||
FullscreenOutlined,
|
||||
MoreOutlined,
|
||||
SearchOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
||||
import { Dropdown, Input, MenuProps, Tooltip, Typography } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -20,8 +21,9 @@ import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import history from 'lib/history';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { X } from 'lucide-react';
|
||||
import { unparse } from 'papaparse';
|
||||
import { ReactNode, useCallback, useMemo } from 'react';
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -51,6 +53,7 @@ interface IWidgetHeaderProps {
|
||||
isWarning: boolean;
|
||||
isFetchingResponse: boolean;
|
||||
tableProcessedDataRef: React.MutableRefObject<RowData[]>;
|
||||
setSearchTerm: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
function WidgetHeader({
|
||||
@@ -67,6 +70,7 @@ function WidgetHeader({
|
||||
isWarning,
|
||||
isFetchingResponse,
|
||||
tableProcessedDataRef,
|
||||
setSearchTerm,
|
||||
}: IWidgetHeaderProps): JSX.Element | null {
|
||||
const onEditHandler = useCallback((): void => {
|
||||
const widgetId = widget.id;
|
||||
@@ -187,6 +191,10 @@ function WidgetHeader({
|
||||
|
||||
const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]);
|
||||
|
||||
const [showGlobalSearch, setShowGlobalSearch] = useState(false);
|
||||
|
||||
const globalSearchAvailable = widget.panelTypes === PANEL_TYPES.TABLE;
|
||||
|
||||
const menu = useMemo(
|
||||
() => ({
|
||||
items: updatedMenuList,
|
||||
@@ -201,46 +209,80 @@ function WidgetHeader({
|
||||
|
||||
return (
|
||||
<div className="widget-header-container">
|
||||
<Typography.Text
|
||||
ellipsis
|
||||
data-testid={title}
|
||||
className="widget-header-title"
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
<div className="widget-header-actions">
|
||||
<div className="widget-api-actions">{threshold}</div>
|
||||
{isFetchingResponse && !queryResponse.isError && (
|
||||
<Spinner style={{ paddingRight: '0.25rem' }} />
|
||||
)}
|
||||
{queryResponse.isError && (
|
||||
<Tooltip
|
||||
title={errorMessage}
|
||||
placement={errorTooltipPosition}
|
||||
className="widget-api-actions"
|
||||
{showGlobalSearch ? (
|
||||
<Input
|
||||
addonBefore={<SearchOutlined size={14} />}
|
||||
placeholder="Search..."
|
||||
bordered={false}
|
||||
data-testid="widget-header-search-input"
|
||||
autoFocus
|
||||
addonAfter={
|
||||
<X
|
||||
size={14}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowGlobalSearch(false);
|
||||
}}
|
||||
className="search-header-icons"
|
||||
/>
|
||||
}
|
||||
key={widget.id}
|
||||
onChange={(e): void => {
|
||||
setSearchTerm(e.target.value || '');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Typography.Text
|
||||
ellipsis
|
||||
data-testid={title}
|
||||
className="widget-header-title"
|
||||
>
|
||||
<ExclamationCircleOutlined />
|
||||
</Tooltip>
|
||||
)}
|
||||
{title}
|
||||
</Typography.Text>
|
||||
<div className="widget-header-actions">
|
||||
<div className="widget-api-actions">{threshold}</div>
|
||||
{isFetchingResponse && !queryResponse.isError && (
|
||||
<Spinner style={{ paddingRight: '0.25rem' }} />
|
||||
)}
|
||||
{queryResponse.isError && (
|
||||
<Tooltip
|
||||
title={errorMessage}
|
||||
placement={errorTooltipPosition}
|
||||
className="widget-api-actions"
|
||||
>
|
||||
<ExclamationCircleOutlined />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isWarning && (
|
||||
<Tooltip
|
||||
title={WARNING_MESSAGE}
|
||||
placement={errorTooltipPosition}
|
||||
className="widget-api-actions"
|
||||
>
|
||||
<WarningOutlined />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
||||
<MoreOutlined
|
||||
data-testid="widget-header-options"
|
||||
className={`widget-header-more-options ${
|
||||
parentHover ? 'widget-header-hover' : ''
|
||||
}`}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{isWarning && (
|
||||
<Tooltip
|
||||
title={WARNING_MESSAGE}
|
||||
placement={errorTooltipPosition}
|
||||
className="widget-api-actions"
|
||||
>
|
||||
<WarningOutlined />
|
||||
</Tooltip>
|
||||
)}
|
||||
{globalSearchAvailable && (
|
||||
<SearchOutlined
|
||||
className="search-header-icons"
|
||||
onClick={(): void => setShowGlobalSearch(true)}
|
||||
data-testid="widget-header-search"
|
||||
/>
|
||||
)}
|
||||
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
||||
<MoreOutlined
|
||||
data-testid="widget-header-options"
|
||||
className={`widget-header-more-options ${
|
||||
parentHover ? 'widget-header-hover' : ''
|
||||
} ${globalSearchAvailable ? 'widget-header-more-options-visible' : ''}`}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,14 @@ export const Card = styled(CardComponent)<CardProps>`
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: calc(100% - 30px);
|
||||
${({ $panelType }): StyledCSS =>
|
||||
$panelType === PANEL_TYPES.TABLE
|
||||
? css`
|
||||
height: 100%;
|
||||
`
|
||||
: css`
|
||||
height: calc(100% - 30px);
|
||||
`}
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.long-text-tooltip {
|
||||
max-width: 500px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import './GridTableComponent.styles.scss';
|
||||
|
||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||
import { Space, Tooltip } from 'antd';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import { Events } from 'constants/events';
|
||||
import { QueryTable } from 'container/QueryTable';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { cloneDeep, get, isEmpty, set } from 'lodash-es';
|
||||
import { cloneDeep, get, isEmpty } from 'lodash-es';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
@@ -38,15 +41,13 @@ function GridTableComponent({
|
||||
const createDataInCorrectFormat = useCallback(
|
||||
(dataSource: RowData[]): RowData[] =>
|
||||
dataSource.map((d) => {
|
||||
const finalObject = {};
|
||||
const finalObject: Record<string, number | string> = {};
|
||||
|
||||
// we use the order of the columns here to have similar download as the user view
|
||||
// the [] access for the object is used because the titles can contain dot(.) as well
|
||||
columns.forEach((k) => {
|
||||
set(
|
||||
finalObject,
|
||||
get(k, 'title', '') as string,
|
||||
get(d, get(k, 'dataIndex', ''), 'n/a'),
|
||||
);
|
||||
finalObject[`${get(k, 'title', '')}`] =
|
||||
d[`${get(k, 'dataIndex', '')}`] || 'n/a';
|
||||
});
|
||||
return finalObject as RowData;
|
||||
}),
|
||||
@@ -86,6 +87,7 @@ function GridTableComponent({
|
||||
applyColumnUnits,
|
||||
originalDataSource,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableProcessedDataRef) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
@@ -117,7 +119,16 @@ function GridTableComponent({
|
||||
}
|
||||
>
|
||||
<Space>
|
||||
{text}
|
||||
<LineClampedText
|
||||
text={text}
|
||||
lines={3}
|
||||
tooltipProps={{
|
||||
placement: 'right',
|
||||
autoAdjustOverflow: true,
|
||||
overlayClassName: 'long-text-tooltip',
|
||||
}}
|
||||
/>
|
||||
|
||||
{hasMultipleMatches && (
|
||||
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
|
||||
<ExclamationCircleFilled className="value-graph-icon" />
|
||||
@@ -128,7 +139,19 @@ function GridTableComponent({
|
||||
);
|
||||
}
|
||||
}
|
||||
return <div>{text}</div>;
|
||||
return (
|
||||
<div>
|
||||
<LineClampedText
|
||||
text={text}
|
||||
lines={3}
|
||||
tooltipProps={{
|
||||
placement: 'right',
|
||||
autoAdjustOverflow: true,
|
||||
overlayClassName: 'long-text-tooltip',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export type GridTableComponentProps = {
|
||||
columnUnits?: ColumnUnit;
|
||||
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
|
||||
sticky?: TableProps<RowData>['sticky'];
|
||||
searchTerm?: string;
|
||||
} & Pick<LogsExplorerTableProps, 'data'> &
|
||||
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;
|
||||
|
||||
|
||||
@@ -64,9 +64,9 @@
|
||||
|
||||
.dashboard-icon {
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
margin-right: 4px;
|
||||
line-height: 20px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
@@ -75,6 +75,12 @@
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.title-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
@@ -459,17 +459,19 @@ function DashboardsList(): JSX.Element {
|
||||
placement="left"
|
||||
overlayClassName="title-toolip"
|
||||
>
|
||||
<Typography.Text data-testid={`dashboard-title-${index}`}>
|
||||
<Link to={getLink()} className="title">
|
||||
<img
|
||||
src={dashboard?.image || Base64Icons[0]}
|
||||
style={{ height: '14px', width: '14px' }}
|
||||
alt="dashboard-image"
|
||||
className="dashboard-icon"
|
||||
/>
|
||||
<Link to={getLink()} className="title-link">
|
||||
<img
|
||||
src={dashboard?.image || Base64Icons[0]}
|
||||
alt="dashboard-image"
|
||||
className="dashboard-icon"
|
||||
/>
|
||||
<Typography.Text
|
||||
data-testid={`dashboard-title-${index}`}
|
||||
className="title"
|
||||
>
|
||||
{dashboard.name}
|
||||
</Link>
|
||||
</Typography.Text>
|
||||
</Typography.Text>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.infra-metrics-container {
|
||||
.views-tabs {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.infra-metrics-card {
|
||||
margin: 1rem 0;
|
||||
height: 300px;
|
||||
padding: 10px;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.no-data-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import './InfraMetrics.styles.scss';
|
||||
|
||||
import { Empty, Radio } from 'antd';
|
||||
import { RadioChangeEvent } from 'antd/lib';
|
||||
import { History, Table } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { VIEW_TYPES } from './constants';
|
||||
import NodeMetrics from './NodeMetrics';
|
||||
import PodMetrics from './PodMetrics';
|
||||
|
||||
interface MetricsDataProps {
|
||||
podName: string;
|
||||
nodeName: string;
|
||||
hostName: string;
|
||||
clusterName: string;
|
||||
logLineTimestamp: string;
|
||||
}
|
||||
|
||||
function InfraMetrics({
|
||||
podName,
|
||||
nodeName,
|
||||
hostName,
|
||||
clusterName,
|
||||
logLineTimestamp,
|
||||
}: MetricsDataProps): JSX.Element {
|
||||
const [selectedView, setSelectedView] = useState<string>(() =>
|
||||
podName ? VIEW_TYPES.POD : VIEW_TYPES.NODE,
|
||||
);
|
||||
|
||||
const handleModeChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
};
|
||||
|
||||
if (!podName && !nodeName && !hostName) {
|
||||
return (
|
||||
<div className="empty-container">
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="No data available. Please select a valid log line containing a pod, node, or host attributes to view metrics."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="infra-metrics-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleModeChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<Radio.Button
|
||||
className={selectedView === VIEW_TYPES.NODE ? 'selected_view tab' : 'tab'}
|
||||
value={VIEW_TYPES.NODE}
|
||||
>
|
||||
<div className="view-title">
|
||||
<Table size={14} />
|
||||
Node
|
||||
</div>
|
||||
</Radio.Button>
|
||||
{podName && (
|
||||
<Radio.Button
|
||||
className={selectedView === VIEW_TYPES.POD ? 'selected_view tab' : 'tab'}
|
||||
value={VIEW_TYPES.POD}
|
||||
>
|
||||
<div className="view-title">
|
||||
<History size={14} />
|
||||
Pod
|
||||
</div>
|
||||
</Radio.Button>
|
||||
)}
|
||||
</Radio.Group>
|
||||
{/* TODO(Rahul): Make a common config driven component for this and other infra metrics components */}
|
||||
{selectedView === VIEW_TYPES.NODE && (
|
||||
<NodeMetrics
|
||||
nodeName={nodeName}
|
||||
clusterName={clusterName}
|
||||
hostName={hostName}
|
||||
logLineTimestamp={logLineTimestamp}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.POD && podName && (
|
||||
<PodMetrics
|
||||
podName={podName}
|
||||
clusterName={clusterName}
|
||||
logLineTimestamp={logLineTimestamp}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default InfraMetrics;
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Card, Col, Row, Skeleton, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
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 { useMemo, useRef } from 'react';
|
||||
import { useQueries, UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import {
|
||||
getHostQueryPayload,
|
||||
getNodeQueryPayload,
|
||||
hostWidgetInfo,
|
||||
nodeWidgetInfo,
|
||||
} from './constants';
|
||||
|
||||
function NodeMetrics({
|
||||
nodeName,
|
||||
clusterName,
|
||||
hostName,
|
||||
logLineTimestamp,
|
||||
}: {
|
||||
nodeName: string;
|
||||
clusterName: string;
|
||||
hostName: string;
|
||||
logLineTimestamp: string;
|
||||
}): JSX.Element {
|
||||
const { start, end, verticalLineTimestamp } = useMemo(() => {
|
||||
const logTimestamp = dayjs(logLineTimestamp);
|
||||
const now = dayjs();
|
||||
const startTime = logTimestamp.subtract(3, 'hour');
|
||||
|
||||
const endTime = logTimestamp.add(3, 'hour').isBefore(now)
|
||||
? logTimestamp.add(3, 'hour')
|
||||
: now;
|
||||
|
||||
return {
|
||||
start: startTime.unix(),
|
||||
end: endTime.unix(),
|
||||
verticalLineTimestamp: logTimestamp.unix(),
|
||||
};
|
||||
}, [logLineTimestamp]);
|
||||
|
||||
const queryPayloads = useMemo(() => {
|
||||
if (nodeName) {
|
||||
return getNodeQueryPayload(clusterName, nodeName, start, end);
|
||||
}
|
||||
return getHostQueryPayload(hostName, start, end);
|
||||
}, [nodeName, hostName, clusterName, start, end]);
|
||||
|
||||
const widgetInfo = nodeName ? nodeWidgetInfo : hostWidgetInfo;
|
||||
const queries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
queryKey: ['metrics', payload, ENTITY_VERSION_V4, 'NODE'],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
})),
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const dimensions = useResizeObserver(graphRef);
|
||||
|
||||
const chartData = useMemo(
|
||||
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
||||
[queries],
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
queries.map(({ data }, idx) =>
|
||||
getUPlotChartOptions({
|
||||
apiResponse: data?.payload,
|
||||
isDarkMode,
|
||||
dimensions,
|
||||
yAxisUnit: widgetInfo[idx].yAxisUnit,
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
minTimeScale: start,
|
||||
maxTimeScale: end,
|
||||
verticalLineTimestamp,
|
||||
}),
|
||||
),
|
||||
[
|
||||
queries,
|
||||
isDarkMode,
|
||||
dimensions,
|
||||
widgetInfo,
|
||||
start,
|
||||
verticalLineTimestamp,
|
||||
end,
|
||||
],
|
||||
);
|
||||
|
||||
const renderCardContent = (
|
||||
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
||||
idx: number,
|
||||
): JSX.Element => {
|
||||
if (query.isLoading) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
if (query.error) {
|
||||
const errorMessage =
|
||||
(query.error as Error)?.message || 'Something went wrong';
|
||||
return <div>{errorMessage}</div>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cx('chart-container', {
|
||||
'no-data-container':
|
||||
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||
})}
|
||||
>
|
||||
<Uplot options={options[idx]} data={chartData[idx]} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Row gutter={24}>
|
||||
{queries.map((query, idx) => (
|
||||
<Col span={12} key={widgetInfo[idx].title}>
|
||||
<Typography.Text>{widgetInfo[idx].title}</Typography.Text>
|
||||
<Card bordered className="infra-metrics-card" ref={graphRef}>
|
||||
{renderCardContent(query, idx)}
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default NodeMetrics;
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Card, Col, Row, Skeleton, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
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 { useMemo, useRef } from 'react';
|
||||
import { useQueries, UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { getPodQueryPayload, podWidgetInfo } from './constants';
|
||||
|
||||
function PodMetrics({
|
||||
podName,
|
||||
clusterName,
|
||||
logLineTimestamp,
|
||||
}: {
|
||||
podName: string;
|
||||
clusterName: string;
|
||||
logLineTimestamp: string;
|
||||
}): JSX.Element {
|
||||
const { start, end, verticalLineTimestamp } = useMemo(() => {
|
||||
const logTimestamp = dayjs(logLineTimestamp);
|
||||
const now = dayjs();
|
||||
const startTime = logTimestamp.subtract(3, 'hour');
|
||||
|
||||
const endTime = logTimestamp.add(3, 'hour').isBefore(now)
|
||||
? logTimestamp.add(3, 'hour')
|
||||
: now;
|
||||
|
||||
return {
|
||||
start: startTime.unix(),
|
||||
end: endTime.unix(),
|
||||
verticalLineTimestamp: logTimestamp.unix(),
|
||||
};
|
||||
}, [logLineTimestamp]);
|
||||
const queryPayloads = useMemo(
|
||||
() => getPodQueryPayload(clusterName, podName, start, end),
|
||||
[clusterName, end, podName, start],
|
||||
);
|
||||
const queries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
queryKey: ['metrics', payload, ENTITY_VERSION_V4, 'POD'],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
})),
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const dimensions = useResizeObserver(graphRef);
|
||||
|
||||
const chartData = useMemo(
|
||||
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
||||
[queries],
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
queries.map(({ data }, idx) =>
|
||||
getUPlotChartOptions({
|
||||
apiResponse: data?.payload,
|
||||
isDarkMode,
|
||||
dimensions,
|
||||
yAxisUnit: podWidgetInfo[idx].yAxisUnit,
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
minTimeScale: start,
|
||||
maxTimeScale: end,
|
||||
verticalLineTimestamp,
|
||||
}),
|
||||
),
|
||||
[queries, isDarkMode, dimensions, start, verticalLineTimestamp, end],
|
||||
);
|
||||
|
||||
const renderCardContent = (
|
||||
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
||||
idx: number,
|
||||
): JSX.Element => {
|
||||
if (query.isLoading) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
if (query.error) {
|
||||
const errorMessage =
|
||||
(query.error as Error)?.message || 'Something went wrong';
|
||||
return <div>{errorMessage}</div>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cx('chart-container', {
|
||||
'no-data-container':
|
||||
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||
})}
|
||||
>
|
||||
<Uplot options={options[idx]} data={chartData[idx]} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gutter={24}>
|
||||
{queries.map((query, idx) => (
|
||||
<Col span={12} key={podWidgetInfo[idx].title}>
|
||||
<Typography.Text>{podWidgetInfo[idx].title}</Typography.Text>
|
||||
<Card bordered className="infra-metrics-card" ref={graphRef}>
|
||||
{renderCardContent(query, idx)}
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default PodMetrics;
|
||||
3033
frontend/src/container/LogDetailedView/InfraMetrics/constants.ts
Normal file
3033
frontend/src/container/LogDetailedView/InfraMetrics/constants.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
import './LogsExplorerQuerySection.styles.scss';
|
||||
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import {
|
||||
initialQueriesMap,
|
||||
OPERATORS,
|
||||
@@ -9,14 +8,12 @@ import {
|
||||
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||
import {
|
||||
prepareQueryWithDefaultTimestamp,
|
||||
SELECTED_VIEWS,
|
||||
@@ -89,26 +86,15 @@ function LogExplorerQuerySection({
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const isSearchV2Enabled =
|
||||
useFeatureFlags(FeatureKeys.QUERY_BUILDER_SEARCH_V2)?.active || false;
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedView === SELECTED_VIEWS.SEARCH && (
|
||||
<div className="qb-search-view-container">
|
||||
{isSearchV2Enabled ? (
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
whereClauseConfig={filterConfigs?.filters}
|
||||
/>
|
||||
) : (
|
||||
<QueryBuilderSearch
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
whereClauseConfig={filterConfigs?.filters}
|
||||
/>
|
||||
)}
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
whereClauseConfig={filterConfigs?.filters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -9,15 +9,18 @@ export function getColorsForSeverityLabels(
|
||||
const lowerCaseLabel = label.toLowerCase();
|
||||
|
||||
if (lowerCaseLabel.includes(`{severity_text="trace"}`)) {
|
||||
return Color.BG_ROBIN_300;
|
||||
return Color.BG_FOREST_400;
|
||||
}
|
||||
|
||||
if (lowerCaseLabel.includes(`{severity_text="debug"}`)) {
|
||||
return Color.BG_FOREST_500;
|
||||
return Color.BG_AQUA_500;
|
||||
}
|
||||
|
||||
if (lowerCaseLabel.includes(`{severity_text="info"}`)) {
|
||||
return Color.BG_SLATE_400;
|
||||
if (
|
||||
lowerCaseLabel.includes(`{severity_text="info"}`) ||
|
||||
lowerCaseLabel.includes(`{severity_text=""}`)
|
||||
) {
|
||||
return Color.BG_ROBIN_500;
|
||||
}
|
||||
|
||||
if (lowerCaseLabel.includes(`{severity_text="warn"}`)) {
|
||||
|
||||
@@ -28,6 +28,20 @@ const lodsQueryServerRequest = (): void =>
|
||||
),
|
||||
);
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
// mocking the graph components in this test as this should be handled separately
|
||||
jest.mock(
|
||||
'container/TimeSeriesView/TimeSeriesView',
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
height: 40px;
|
||||
justify-content: end;
|
||||
padding: 0 8px;
|
||||
margin: 12px 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,12 +130,16 @@
|
||||
|
||||
.left-section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 45%;
|
||||
|
||||
.dashboard-img {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
color: #fff;
|
||||
font-family: Inter;
|
||||
|
||||
@@ -306,16 +306,13 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
</div>
|
||||
<section className="dashboard-details">
|
||||
<div className="left-section">
|
||||
<img src={image} alt="dashboard-img" className="dashboard-img" />
|
||||
<Tooltip title={title.length > 30 ? title : ''}>
|
||||
<Typography.Text
|
||||
className="dashboard-title"
|
||||
data-testid="dashboard-title"
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt="dashboard-img"
|
||||
style={{ width: '16px', height: '16px' }}
|
||||
/>{' '}
|
||||
{' '}
|
||||
{title}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
|
||||
@@ -43,6 +43,15 @@
|
||||
.ant-select-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rc-virtual-list-holder {
|
||||
[data-testid='option-ALL'] {
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.all-label {
|
||||
@@ -56,28 +65,25 @@
|
||||
}
|
||||
|
||||
.dropdown-value {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
|
||||
.option-text {
|
||||
max-width: 180px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.toggle-tag-label {
|
||||
padding-left: 8px;
|
||||
right: 40px;
|
||||
font-weight: normal;
|
||||
position: absolute;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-styles {
|
||||
min-width: 300px;
|
||||
max-width: 350px;
|
||||
min-width: 400px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
|
||||
@@ -62,14 +62,14 @@ interface VariableItemProps {
|
||||
const getSelectValue = (
|
||||
selectedValue: IDashboardVariable['selectedValue'],
|
||||
variableData: IDashboardVariable,
|
||||
): string | string[] => {
|
||||
): string | string[] | undefined => {
|
||||
if (Array.isArray(selectedValue)) {
|
||||
if (!variableData.multiSelect && selectedValue.length === 1) {
|
||||
return selectedValue[0]?.toString() || '';
|
||||
return selectedValue[0]?.toString();
|
||||
}
|
||||
return selectedValue.map((item) => item.toString());
|
||||
}
|
||||
return selectedValue?.toString() || '';
|
||||
return selectedValue?.toString();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
@@ -300,7 +300,7 @@ function VariableItem({
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const isChecked =
|
||||
variableData.allSelected || selectValue.includes(ALL_SELECT_VALUE);
|
||||
variableData.allSelected || selectValue?.includes(ALL_SELECT_VALUE);
|
||||
|
||||
if (isChecked) {
|
||||
handleChange([]);
|
||||
@@ -462,6 +462,7 @@ function VariableItem({
|
||||
<span>+ {omittedValues.length} </span>
|
||||
</Tooltip>
|
||||
)}
|
||||
allowClear
|
||||
>
|
||||
{enableSelectAll && (
|
||||
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
|
||||
@@ -500,11 +501,17 @@ function VariableItem({
|
||||
{...retProps(option as string)}
|
||||
onClick={(e): void => handleToggle(e as any, option as string)}
|
||||
>
|
||||
<Tooltip title={option.toString()} placement="bottomRight">
|
||||
<Typography.Text ellipsis className="option-text">
|
||||
{option.toString()}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
tooltip: {
|
||||
placement: variableData.multiSelect ? 'top' : 'right',
|
||||
autoAdjustOverflow: true,
|
||||
},
|
||||
}}
|
||||
className="option-text"
|
||||
>
|
||||
{option.toString()}
|
||||
</Typography.Text>
|
||||
|
||||
{variableData.multiSelect &&
|
||||
optionState.tag === option.toString() &&
|
||||
|
||||
@@ -16,6 +16,7 @@ function PanelWrapper({
|
||||
selectedGraph,
|
||||
tableProcessedDataRef,
|
||||
customTooltipElement,
|
||||
searchTerm,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const Component = PanelTypeVsPanelWrapper[
|
||||
selectedGraph || widget.panelTypes
|
||||
@@ -39,6 +40,7 @@ function PanelWrapper({
|
||||
selectedGraph={selectedGraph}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
customTooltipElement={customTooltipElement}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ function TablePanelWrapper({
|
||||
widget,
|
||||
queryResponse,
|
||||
tableProcessedDataRef,
|
||||
searchTerm,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const panelData =
|
||||
(queryResponse.data?.payload?.data?.result?.[0] as any)?.table || [];
|
||||
@@ -20,6 +21,7 @@ function TablePanelWrapper({
|
||||
columnUnits={widget.columnUnits}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
sticky={widget.panelTypes === PANEL_TYPES.TABLE}
|
||||
searchTerm={searchTerm}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...GRID_TABLE_CONFIG}
|
||||
/>
|
||||
|
||||
@@ -266,14 +266,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
demo-app
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
demo-app
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
4.35 s
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
4.35 s
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -284,14 +292,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
customer
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
customer
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
431 ms
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
431 ms
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -302,14 +318,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
mysql
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
mysql
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
431 ms
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
431 ms
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -320,14 +344,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
frontend
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
frontend
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
287 ms
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
287 ms
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -338,14 +370,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
driver
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
driver
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
230 ms
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
230 ms
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -356,14 +396,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
route
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
route
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
66.4 ms
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
66.4 ms
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -374,14 +422,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
redis
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
redis
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div>
|
||||
31.3 ms
|
||||
<div
|
||||
class="line-clamped-text"
|
||||
>
|
||||
31.3 ms
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -23,6 +23,7 @@ export type PanelWrapperProps = {
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
selectedGraph?: PANEL_TYPES;
|
||||
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
|
||||
searchTerm?: string;
|
||||
customTooltipElement?: HTMLDivElement;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,20 @@ import store from 'store';
|
||||
import ChangeHistory from '../index';
|
||||
import { pipelineData, pipelineDataHistory } from './testUtils';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
|
||||
@@ -9,6 +9,20 @@ import store from 'store';
|
||||
import { pipelineMockData } from '../mocks/pipeline';
|
||||
import AddNewPipeline from '../PipelineListsView/AddNewPipeline';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
export function matchMedia(): void {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
|
||||
@@ -9,6 +9,20 @@ import { pipelineMockData } from '../mocks/pipeline';
|
||||
import AddNewProcessor from '../PipelineListsView/AddNewProcessor';
|
||||
import { matchMedia } from './AddNewPipeline.test';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
matchMedia();
|
||||
});
|
||||
|
||||
@@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import i18n from 'ReactI18';
|
||||
import store from 'store';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
describe('PipelinePage container test', () => {
|
||||
it('should render DeleteAction section', () => {
|
||||
const { asFragment } = render(
|
||||
|
||||
@@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import i18n from 'ReactI18';
|
||||
import store from 'store';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
describe('PipelinePage container test', () => {
|
||||
it('should render DragAction section', () => {
|
||||
const { asFragment } = render(
|
||||
|
||||
@@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import i18n from 'ReactI18';
|
||||
import store from 'store';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
describe('PipelinePage container test', () => {
|
||||
it('should render EditAction section', () => {
|
||||
const { asFragment } = render(
|
||||
|
||||
@@ -8,6 +8,20 @@ import store from 'store';
|
||||
import { pipelineMockData } from '../mocks/pipeline';
|
||||
import PipelineActions from '../PipelineListsView/TableComponents/PipelineActions';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
describe('PipelinePage container test', () => {
|
||||
it('should render PipelineActions section', () => {
|
||||
const { asFragment } = render(
|
||||
|
||||
@@ -9,6 +9,20 @@ import { pipelineMockData } from '../mocks/pipeline';
|
||||
import PipelineExpandView from '../PipelineListsView/PipelineExpandView';
|
||||
import { matchMedia } from './AddNewPipeline.test';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
matchMedia();
|
||||
});
|
||||
|
||||
@@ -11,6 +11,20 @@ import store from 'store';
|
||||
import { pipelineApiResponseMockData } from '../mocks/pipeline';
|
||||
import PipelineListsView from '../PipelineListsView';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
const samplePipelinePreviewResponse = {
|
||||
isLoading: false,
|
||||
logs: [
|
||||
|
||||
@@ -11,6 +11,20 @@ import { v4 } from 'uuid';
|
||||
import PipelinePageLayout from '../Layouts/Pipeline';
|
||||
import { matchMedia } from './AddNewPipeline.test';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
matchMedia();
|
||||
});
|
||||
|
||||
@@ -7,6 +7,20 @@ import store from 'store';
|
||||
|
||||
import TagInput from '../components/TagInput';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
describe('Pipeline Page', () => {
|
||||
it('should render TagInput section', () => {
|
||||
const { asFragment } = render(
|
||||
|
||||
@@ -11,6 +11,20 @@ import {
|
||||
getTableColumn,
|
||||
} from '../PipelineListsView/utils';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
describe('Utils testing of Pipeline Page', () => {
|
||||
test('it should be check form field of add pipeline', () => {
|
||||
expect(pipelineFields.length).toBe(3);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryFilter';
|
||||
import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
// ** Hooks
|
||||
@@ -81,6 +82,10 @@ export const Query = memo(function Query({
|
||||
entityVersion: version,
|
||||
});
|
||||
|
||||
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
||||
pathname,
|
||||
]);
|
||||
|
||||
const handleChangeAggregateEvery = useCallback(
|
||||
(value: IBuilderQuery['stepInterval']) => {
|
||||
handleChangeQueryData('stepInterval', value);
|
||||
@@ -452,11 +457,19 @@ export const Query = memo(function Query({
|
||||
</Col>
|
||||
)}
|
||||
<Col flex="1" className="qb-search-container">
|
||||
<QueryBuilderSearch
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
whereClauseConfig={filterConfigs?.filters}
|
||||
/>
|
||||
{isLogsExplorerPage ? (
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
whereClauseConfig={filterConfigs?.filters}
|
||||
/>
|
||||
) : (
|
||||
<QueryBuilderSearch
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
whereClauseConfig={filterConfigs?.filters}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Select } from 'antd';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
// ** Constants
|
||||
@@ -34,6 +35,7 @@ export function HavingFilter({
|
||||
const [currentFormValue, setCurrentFormValue] = useState<HavingForm>(
|
||||
initialHavingValues,
|
||||
);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const { isMulti } = useTagValidation(
|
||||
currentFormValue.op,
|
||||
@@ -198,6 +200,29 @@ export function HavingFilter({
|
||||
resetChanges();
|
||||
};
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
setErrorMessage(null);
|
||||
}, []);
|
||||
|
||||
const handleBlur = useCallback((): void => {
|
||||
if (searchText) {
|
||||
const { columnName, op, value } = getHavingObject(searchText);
|
||||
const isCompleteHavingClause =
|
||||
columnName && op && value.every((v) => v !== '');
|
||||
|
||||
if (isCompleteHavingClause && isValidHavingValue(searchText)) {
|
||||
setLocalValues((prev) => {
|
||||
const updatedValues = [...prev, searchText];
|
||||
onChange(updatedValues.map(transformFromStringToHaving));
|
||||
return updatedValues;
|
||||
});
|
||||
setSearchText('');
|
||||
} else {
|
||||
setErrorMessage('Invalid HAVING clause');
|
||||
}
|
||||
}
|
||||
}, [searchText, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
parseSearchText(searchText);
|
||||
}, [searchText, parseSearchText]);
|
||||
@@ -209,28 +234,36 @@ export function HavingFilter({
|
||||
const isMetricsDataSource = query.dataSource === DataSource.METRICS;
|
||||
|
||||
return (
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
autoClearSearchValue={false}
|
||||
mode="multiple"
|
||||
onSearch={handleSearch}
|
||||
searchValue={searchText}
|
||||
tagRender={tagRender}
|
||||
value={localValues}
|
||||
data-testid="havingSelect"
|
||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
||||
style={{ width: '100%' }}
|
||||
notFoundContent={currentFormValue.value.length === 0 ? undefined : null}
|
||||
placeholder="GroupBy(operation) > 5"
|
||||
onDeselect={handleDeselect}
|
||||
onChange={handleChange}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<Select.Option key={opt.value} value={opt.value} title="havingOption">
|
||||
{opt.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
autoClearSearchValue={false}
|
||||
mode="multiple"
|
||||
onSearch={handleSearch}
|
||||
searchValue={searchText}
|
||||
tagRender={tagRender}
|
||||
value={localValues}
|
||||
data-testid="havingSelect"
|
||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
||||
style={{ width: '100%' }}
|
||||
notFoundContent={currentFormValue.value.length === 0 ? undefined : null}
|
||||
placeholder="GroupBy(operation) > 5"
|
||||
onDeselect={handleDeselect}
|
||||
onChange={handleChange}
|
||||
onSelect={handleSelect}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
status={errorMessage ? 'error' : undefined}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<Select.Option key={opt.value} value={opt.value} title="havingOption">
|
||||
{opt.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
{errorMessage && (
|
||||
<div style={{ color: Color.BG_CHERRY_500 }}>{errorMessage}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.ant-select-dropdown {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.show-all-filters {
|
||||
.content {
|
||||
.rc-virtual-list-holder {
|
||||
@@ -231,16 +235,16 @@
|
||||
}
|
||||
|
||||
&.resource {
|
||||
border: 1px solid rgba(242, 71, 105, 0.2);
|
||||
border: 1px solid #4bcff920;
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-sakura-400);
|
||||
background: rgba(245, 108, 135, 0.1);
|
||||
color: var(--bg-aqua-400);
|
||||
background: #4bcff910;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ant-tag-close-icon {
|
||||
background: rgba(245, 108, 135, 0.1);
|
||||
background: #4bcff910;
|
||||
}
|
||||
}
|
||||
&.tag {
|
||||
@@ -259,3 +263,110 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.query-builder-search-v2 {
|
||||
.content {
|
||||
.operator-for {
|
||||
.operator-for-text {
|
||||
color: var(--bg-ink-200);
|
||||
}
|
||||
|
||||
.operator-for-value {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--bg-ink-200);
|
||||
}
|
||||
}
|
||||
|
||||
.value-for {
|
||||
.value-for-text {
|
||||
color: var(--bg-ink-200);
|
||||
}
|
||||
|
||||
.value-for-value {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
.example-queries {
|
||||
cursor: default;
|
||||
.heading {
|
||||
color: var(--bg-slate-50);
|
||||
}
|
||||
|
||||
.query-container {
|
||||
.example-query {
|
||||
background: var(--bg-vanilla-200);
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.example-query:hover {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.keyboard-shortcuts {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-200);
|
||||
|
||||
.icons {
|
||||
border-top: 1.143px solid var(--bg-ink-200);
|
||||
border-right: 1.143px solid var(--bg-ink-200);
|
||||
border-bottom: 2.286px solid var(--bg-ink-200);
|
||||
border-left: 1.143px solid var(--bg-ink-200);
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.keyboard-text {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.navigate {
|
||||
border-right: 1px solid #1d212d;
|
||||
}
|
||||
|
||||
.show-all-filter-items {
|
||||
border-left: 1px solid #1d212d;
|
||||
}
|
||||
}
|
||||
|
||||
.qb-search-bar-tokenised-tags {
|
||||
.ant-tag {
|
||||
border: 1px solid var(--bg-slate-100);
|
||||
background: var(--bg-vanilla-300);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
|
||||
&.resource {
|
||||
border: 1px solid #4bcff920;
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-aqua-400);
|
||||
background: #4bcff910;
|
||||
}
|
||||
|
||||
.ant-tag-close-icon {
|
||||
background: #4bcff910;
|
||||
}
|
||||
}
|
||||
&.tag {
|
||||
border: 1px solid rgba(189, 153, 121, 0.2);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-sienna-400);
|
||||
background: rgba(189, 153, 121, 0.1);
|
||||
}
|
||||
|
||||
.ant-tag-close-icon {
|
||||
background: rgba(189, 153, 121, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,16 +286,62 @@ function QueryBuilderSearchV2(
|
||||
parsedValue = value;
|
||||
}
|
||||
if (currentState === DropdownState.ATTRIBUTE_KEY) {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
...prev,
|
||||
key: parsedValue as BaseAutocompleteData,
|
||||
op: '',
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
setSearchValue((parsedValue as BaseAutocompleteData)?.key);
|
||||
// Case - convert abc def ghi type attribute keys directly to body contains abc def ghi
|
||||
if (
|
||||
isObject(parsedValue) &&
|
||||
parsedValue?.key &&
|
||||
parsedValue?.key?.split(' ').length > 1
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: {
|
||||
key: 'body',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
id: 'body--string----true',
|
||||
},
|
||||
op: OPERATORS.CONTAINS,
|
||||
value: (parsedValue as BaseAutocompleteData)?.key,
|
||||
},
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
...prev,
|
||||
key: parsedValue as BaseAutocompleteData,
|
||||
op: '',
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
setSearchValue((parsedValue as BaseAutocompleteData)?.key);
|
||||
}
|
||||
} else if (currentState === DropdownState.OPERATOR) {
|
||||
if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) {
|
||||
if (isEmpty(value) && currentFilterItem?.key?.key) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: {
|
||||
key: 'body',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'body--string----true',
|
||||
},
|
||||
op: OPERATORS.CONTAINS,
|
||||
value: currentFilterItem?.key?.key,
|
||||
},
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
@@ -399,6 +445,7 @@ function QueryBuilderSearchV2(
|
||||
whereClauseConfig?.customKey === 'body' &&
|
||||
whereClauseConfig?.customOp === OPERATORS.CONTAINS
|
||||
) {
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
@@ -519,19 +566,20 @@ function QueryBuilderSearchV2(
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
}
|
||||
}
|
||||
if (suggestionsData?.payload?.attributes?.length === 0) {
|
||||
// again let's not auto select anything for the user
|
||||
if (tagOperator) {
|
||||
setCurrentFilterItem({
|
||||
key: {
|
||||
key: tagKey.split(' ')[0],
|
||||
key: tagKey,
|
||||
dataType: DataTypes.EMPTY,
|
||||
type: '',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '',
|
||||
op: tagOperator,
|
||||
value: '',
|
||||
});
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||
}
|
||||
} else if (
|
||||
// Case 2 - if key is defined but the search text doesn't match with the set key,
|
||||
@@ -607,13 +655,32 @@ function QueryBuilderSearchV2(
|
||||
// the useEffect takes care of setting the dropdown values correctly on change of the current state
|
||||
useEffect(() => {
|
||||
if (currentState === DropdownState.ATTRIBUTE_KEY) {
|
||||
const { tagKey } = getTagToken(searchValue);
|
||||
if (isLogsExplorerPage) {
|
||||
setDropdownOptions(
|
||||
suggestionsData?.payload?.attributes?.map((key) => ({
|
||||
// add the user typed option in the dropdown to select that and move ahead irrespective of the matches and all
|
||||
setDropdownOptions([
|
||||
...(!isEmpty(tagKey) &&
|
||||
!suggestionsData?.payload?.attributes?.some((val) =>
|
||||
isEqual(val.key, tagKey),
|
||||
)
|
||||
? [
|
||||
{
|
||||
label: tagKey,
|
||||
value: {
|
||||
key: tagKey,
|
||||
dataType: DataTypes.EMPTY,
|
||||
type: '',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(suggestionsData?.payload?.attributes?.map((key) => ({
|
||||
label: key.key,
|
||||
value: key,
|
||||
})) || [],
|
||||
);
|
||||
})) || []),
|
||||
]);
|
||||
} else {
|
||||
setDropdownOptions(
|
||||
data?.payload?.attributeKeys?.map((key) => ({
|
||||
@@ -643,12 +710,14 @@ function QueryBuilderSearchV2(
|
||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||
);
|
||||
}
|
||||
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
|
||||
setDropdownOptions(operatorOptions);
|
||||
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
|
||||
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
|
||||
label: operator,
|
||||
value: operator,
|
||||
}));
|
||||
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
|
||||
setDropdownOptions(operatorOptions);
|
||||
} else {
|
||||
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
|
||||
@@ -663,6 +732,7 @@ function QueryBuilderSearchV2(
|
||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||
);
|
||||
}
|
||||
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
|
||||
setDropdownOptions(operatorOptions);
|
||||
}
|
||||
}
|
||||
@@ -729,7 +799,8 @@ function QueryBuilderSearchV2(
|
||||
}, [tags]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(query.filters.items, tags)) {
|
||||
// convert the query and tags to same format before comparison
|
||||
if (!isEqual(getInitTags(query), tags)) {
|
||||
setTags(getInitTags(query));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -769,7 +840,7 @@ function QueryBuilderSearchV2(
|
||||
);
|
||||
|
||||
const queryTags = useMemo(
|
||||
() => tags.map((tag) => `${tag.key.key} ${tag.op} ${tag.value}`),
|
||||
() => tags.map((tag) => `${tag.key?.key} ${tag.op} ${tag.value}`),
|
||||
[tags],
|
||||
);
|
||||
|
||||
|
||||
@@ -77,14 +77,14 @@
|
||||
|
||||
&.resource {
|
||||
border-radius: 50px;
|
||||
background: rgba(245, 108, 135, 0.1) !important;
|
||||
color: var(--bg-sakura-400) !important;
|
||||
background: #4bcff910 !important;
|
||||
color: var(--bg-aqua-400) !important;
|
||||
|
||||
.dot {
|
||||
background-color: var(--bg-sakura-400);
|
||||
background-color: var(--bg-aqua-400);
|
||||
}
|
||||
.text {
|
||||
color: var(--bg-sakura-400);
|
||||
color: var(--bg-aqua-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
@@ -168,3 +168,59 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.text {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.option {
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.right-section {
|
||||
.data-type {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
.option-meta-data-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-without-tag {
|
||||
.left {
|
||||
.OPERATOR {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.VALUE {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
.data-type {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.option:hover {
|
||||
.container {
|
||||
.left-section {
|
||||
.value {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
.container-without-tag {
|
||||
.value {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,5 @@ export type QueryTableProps = Omit<
|
||||
columns?: ColumnsType<RowData>;
|
||||
dataSource?: RowData[];
|
||||
sticky?: TableProps<RowData>['sticky'];
|
||||
searchTerm?: string;
|
||||
};
|
||||
|
||||
@@ -3,8 +3,11 @@ import './QueryTable.styles.scss';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import Download from 'container/Download/Download';
|
||||
import { IServiceName } from 'container/MetricsApplication/Tabs/types';
|
||||
import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
createTableColumnsFromQuery,
|
||||
RowData,
|
||||
} from 'lib/query/createTableColumnsFromQuery';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { QueryTableProps } from './QueryTable.intefaces';
|
||||
@@ -20,6 +23,7 @@ export function QueryTable({
|
||||
columns,
|
||||
dataSource,
|
||||
sticky,
|
||||
searchTerm,
|
||||
...props
|
||||
}: QueryTableProps): JSX.Element {
|
||||
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
|
||||
@@ -55,6 +59,27 @@ export function QueryTable({
|
||||
hideOnSinglePage: true,
|
||||
};
|
||||
|
||||
const [filterTable, setFilterTable] = useState<RowData[] | null>(null);
|
||||
|
||||
const onTableSearch = useCallback(
|
||||
(value?: string): void => {
|
||||
const filterTable = newDataSource.filter((o) =>
|
||||
Object.keys(o).some((k) =>
|
||||
String(o[k])
|
||||
.toLowerCase()
|
||||
.includes(value?.toLowerCase() || ''),
|
||||
),
|
||||
);
|
||||
|
||||
setFilterTable(filterTable);
|
||||
},
|
||||
[newDataSource],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onTableSearch(searchTerm);
|
||||
}, [newDataSource, onTableSearch, searchTerm]);
|
||||
|
||||
return (
|
||||
<div className="query-table">
|
||||
{isDownloadEnabled && (
|
||||
@@ -69,7 +94,7 @@ export function QueryTable({
|
||||
<ResizeTable
|
||||
columns={tableColumns}
|
||||
tableLayout="fixed"
|
||||
dataSource={newDataSource}
|
||||
dataSource={filterTable === null ? newDataSource : filterTable}
|
||||
scroll={{ x: true }}
|
||||
pagination={paginationConfig}
|
||||
sticky={sticky}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import WidgetHeader from 'container/GridCardLayout/WidgetHeader';
|
||||
import { fireEvent, render } from 'tests/test-utils';
|
||||
|
||||
import { QueryTable } from '../QueryTable';
|
||||
import { QueryTableProps, WidgetHeaderProps } from './mocks';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: ``,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock useDashabord hook
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): any => ({
|
||||
selectedDashboard: {
|
||||
data: {
|
||||
variables: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('QueryTable -', () => {
|
||||
it('should render correctly with all the data rows', () => {
|
||||
const { container } = render(<QueryTable {...QueryTableProps} />);
|
||||
const tableRows = container.querySelectorAll('tr.ant-table-row');
|
||||
expect(tableRows.length).toBe(QueryTableProps.queryTableData.rows.length);
|
||||
});
|
||||
|
||||
it('should render correctly with searchTerm', () => {
|
||||
const { container } = render(
|
||||
<QueryTable {...QueryTableProps} searchTerm="frontend" />,
|
||||
);
|
||||
const tableRows = container.querySelectorAll('tr.ant-table-row');
|
||||
expect(tableRows.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
const setSearchTerm = jest.fn();
|
||||
describe('WidgetHeader -', () => {
|
||||
it('global search option should be working', () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<WidgetHeader {...WidgetHeaderProps} setSearchTerm={setSearchTerm} />,
|
||||
);
|
||||
expect(getByText('Table - Panel')).toBeInTheDocument();
|
||||
const searchWidget = getByTestId('widget-header-search');
|
||||
expect(searchWidget).toBeInTheDocument();
|
||||
// click and open the search input
|
||||
fireEvent.click(searchWidget);
|
||||
// check if input is opened
|
||||
const searchInput = getByTestId('widget-header-search-input');
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
|
||||
// enter search term
|
||||
fireEvent.change(searchInput, { target: { value: 'frontend' } });
|
||||
// check if search term is set
|
||||
expect(setSearchTerm).toHaveBeenCalledWith('frontend');
|
||||
expect(searchInput).toHaveValue('frontend');
|
||||
});
|
||||
|
||||
it('global search should not be present for non-table panel', () => {
|
||||
const { queryByTestId } = render(
|
||||
<WidgetHeader
|
||||
{...WidgetHeaderProps}
|
||||
widget={{ ...WidgetHeaderProps.widget, panelTypes: 'chart' }}
|
||||
/>,
|
||||
);
|
||||
expect(queryByTestId('widget-header-search')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
797
frontend/src/container/QueryTable/__test__/mocks.ts
Normal file
797
frontend/src/container/QueryTable/__test__/mocks.ts
Normal file
@@ -0,0 +1,797 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
export const QueryTableProps: any = {
|
||||
props: {
|
||||
loading: false,
|
||||
size: 'small',
|
||||
},
|
||||
queryTableData: {
|
||||
columns: [
|
||||
{
|
||||
name: 'resource_host_name',
|
||||
queryName: '',
|
||||
isValueColumn: false,
|
||||
},
|
||||
{
|
||||
name: 'service_name',
|
||||
queryName: '',
|
||||
isValueColumn: false,
|
||||
},
|
||||
{
|
||||
name: 'operation',
|
||||
queryName: '',
|
||||
isValueColumn: false,
|
||||
},
|
||||
{
|
||||
name: 'A',
|
||||
queryName: 'A',
|
||||
isValueColumn: true,
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
data: {
|
||||
A: 11.5,
|
||||
operation: 'GetDriver',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'redis',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 10.13,
|
||||
operation: 'HTTP GET',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'frontend',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 9.21,
|
||||
operation: 'HTTP GET /route',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'route',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 9.21,
|
||||
operation: 'HTTP GET: /route',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'frontend',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.92,
|
||||
operation: 'HTTP GET: /customer',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'frontend',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.92,
|
||||
operation: 'SQL SELECT',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'mysql',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.92,
|
||||
operation: 'HTTP GET /customer',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'customer',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
query: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: 'float64',
|
||||
id: 'signoz_calls_total--float64--Sum--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'signoz_calls_total',
|
||||
type: 'Sum',
|
||||
},
|
||||
aggregateOperator: 'rate',
|
||||
dataSource: 'metrics',
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'resource_host_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'resource_host_name',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'service_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'service_name',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'operation--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'operation',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: '',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'rate',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
id: '1e08128f-c6a3-42ff-8033-4e38d291cf0a',
|
||||
promql: [
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
queryType: 'builder',
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
dataIndex: 'resource_host_name',
|
||||
title: 'resource_host_name',
|
||||
width: 145,
|
||||
},
|
||||
{
|
||||
dataIndex: 'service_name',
|
||||
title: 'service_name',
|
||||
width: 145,
|
||||
},
|
||||
{
|
||||
dataIndex: 'operation',
|
||||
title: 'operation',
|
||||
width: 145,
|
||||
},
|
||||
{
|
||||
dataIndex: 'A',
|
||||
title: 'A',
|
||||
width: 145,
|
||||
},
|
||||
],
|
||||
dataSource: [
|
||||
{
|
||||
A: 11.5,
|
||||
operation: 'GetDriver',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'redis',
|
||||
},
|
||||
{
|
||||
A: 10.13,
|
||||
operation: 'HTTP GET',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'frontend',
|
||||
},
|
||||
{
|
||||
A: 9.21,
|
||||
operation: 'HTTP GET /route',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'route',
|
||||
},
|
||||
{
|
||||
A: 9.21,
|
||||
operation: 'HTTP GET: /route',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'frontend',
|
||||
},
|
||||
{
|
||||
A: 0.92,
|
||||
operation: 'HTTP GET: /customer',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'frontend',
|
||||
},
|
||||
{
|
||||
A: 0.92,
|
||||
operation: 'SQL SELECT',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'mysql',
|
||||
},
|
||||
{
|
||||
A: 0.92,
|
||||
operation: 'HTTP GET /customer',
|
||||
resource_host_name: 'test-hs-name',
|
||||
service_name: 'customer',
|
||||
},
|
||||
],
|
||||
sticky: true,
|
||||
searchTerm: '',
|
||||
};
|
||||
|
||||
export const WidgetHeaderProps: any = {
|
||||
title: 'Table - Panel',
|
||||
widget: {
|
||||
bucketCount: 30,
|
||||
bucketWidth: 0,
|
||||
columnUnits: {},
|
||||
description: '',
|
||||
fillSpans: false,
|
||||
id: 'add65f0d-7662-4024-af51-da567759235d',
|
||||
isStacked: false,
|
||||
mergeAllActiveQueries: false,
|
||||
nullZeroValues: 'zero',
|
||||
opacity: '1',
|
||||
panelTypes: 'table',
|
||||
query: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: 'float64',
|
||||
id: 'signoz_calls_total--float64--Sum--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'signoz_calls_total',
|
||||
type: 'Sum',
|
||||
},
|
||||
aggregateOperator: 'rate',
|
||||
dataSource: 'metrics',
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'resource_host_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'resource_host_name',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'service_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'service_name',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'operation--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'operation',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: '',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'rate',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
id: '1e08128f-c6a3-42ff-8033-4e38d291cf0a',
|
||||
promql: [
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
queryType: 'builder',
|
||||
},
|
||||
selectedLogFields: [
|
||||
{
|
||||
dataType: 'string',
|
||||
name: 'body',
|
||||
type: '',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
name: 'timestamp',
|
||||
type: '',
|
||||
},
|
||||
],
|
||||
selectedTracesFields: [
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'serviceName--string--tag--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'serviceName',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'name--string--tag--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'name',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: 'float64',
|
||||
id: 'durationNano--float64--tag--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'durationNano',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'httpMethod--string--tag--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'httpMethod',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'responseStatusCode--string--tag--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'responseStatusCode',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
softMax: 0,
|
||||
softMin: 0,
|
||||
stackedBarChart: false,
|
||||
thresholds: [],
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
title: 'Table - Panel',
|
||||
yAxisUnit: 'none',
|
||||
},
|
||||
parentHover: false,
|
||||
queryResponse: {
|
||||
status: 'success',
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
isIdle: false,
|
||||
data: {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'success',
|
||||
payload: {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: '',
|
||||
result: [
|
||||
{
|
||||
table: {
|
||||
columns: [
|
||||
{
|
||||
name: 'resource_host_name',
|
||||
queryName: '',
|
||||
isValueColumn: false,
|
||||
},
|
||||
{
|
||||
name: 'service_name',
|
||||
queryName: '',
|
||||
isValueColumn: false,
|
||||
},
|
||||
{
|
||||
name: 'operation',
|
||||
queryName: '',
|
||||
isValueColumn: false,
|
||||
},
|
||||
{
|
||||
name: 'A',
|
||||
queryName: 'A',
|
||||
isValueColumn: true,
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
data: {
|
||||
A: 11.67,
|
||||
operation: 'GetDriver',
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'redis',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 10.26,
|
||||
operation: 'HTTP GET',
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'frontend',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 9.33,
|
||||
operation: 'HTTP GET: /route',
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'frontend',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 9.33,
|
||||
operation: 'HTTP GET /route',
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'route',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.93,
|
||||
operation: 'FindDriverIDs',
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'redis',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.93,
|
||||
operation: 'HTTP GET: /customer',
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'frontend',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.93,
|
||||
operation: '/driver.DriverService/FindNearest',
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'driver',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.93,
|
||||
operation: '/driver.DriverService/FindNearest',
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'frontend',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.93,
|
||||
operation: 'SQL SELECT',
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'mysql',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.93,
|
||||
operation: 'HTTP GET /customer',
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'customer',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.93,
|
||||
operation: 'HTTP GET /dispatch',
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'frontend',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.21,
|
||||
operation: 'check_request limit',
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.21,
|
||||
operation: 'authenticate_check_cache',
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.21,
|
||||
operation: 'authenticate_check_db',
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.21,
|
||||
operation: 'authenticate',
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.21,
|
||||
operation: 'check cart in cache',
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.2,
|
||||
operation: 'get_cart',
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.2,
|
||||
operation: 'check cart in db',
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
A: 0.2,
|
||||
operation: 'home',
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
params: {
|
||||
start: 1726669030000,
|
||||
end: 1726670830000,
|
||||
step: 60,
|
||||
variables: {},
|
||||
formatForWeb: true,
|
||||
compositeQuery: {
|
||||
queryType: 'builder',
|
||||
panelType: 'table',
|
||||
fillGaps: false,
|
||||
builderQueries: {
|
||||
A: {
|
||||
aggregateAttribute: {
|
||||
dataType: 'float64',
|
||||
id: 'signoz_calls_total--float64--Sum--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'signoz_calls_total',
|
||||
type: 'Sum',
|
||||
},
|
||||
aggregateOperator: 'rate',
|
||||
dataSource: 'metrics',
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'resource_host_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'resource_host_name',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'service_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'service_name',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
id: 'operation--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'operation',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: '',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'rate',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataUpdatedAt: 1726670830710,
|
||||
error: null,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
errorUpdateCount: 0,
|
||||
isFetched: true,
|
||||
isFetchedAfterMount: true,
|
||||
isFetching: false,
|
||||
isRefetching: false,
|
||||
isLoadingError: false,
|
||||
isPlaceholderData: false,
|
||||
isPreviousData: false,
|
||||
isRefetchError: false,
|
||||
isStale: true,
|
||||
},
|
||||
headerMenuList: ['view', 'clone', 'delete', 'edit'],
|
||||
isWarning: false,
|
||||
isFetchingResponse: false,
|
||||
tableProcessedDataRef: {
|
||||
current: [
|
||||
{
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'redis',
|
||||
operation: 'GetDriver',
|
||||
A: 11.67,
|
||||
},
|
||||
{
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'frontend',
|
||||
operation: 'HTTP GET',
|
||||
A: 10.26,
|
||||
},
|
||||
{
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'frontend',
|
||||
operation: 'HTTP GET: /route',
|
||||
A: 9.33,
|
||||
},
|
||||
{
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'route',
|
||||
operation: 'HTTP GET /route',
|
||||
A: 9.33,
|
||||
},
|
||||
{
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'redis',
|
||||
operation: 'FindDriverIDs',
|
||||
A: 0.93,
|
||||
},
|
||||
{
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'frontend',
|
||||
operation: 'HTTP GET: /customer',
|
||||
A: 0.93,
|
||||
},
|
||||
{
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'driver',
|
||||
operation: '/driver.DriverService/FindNearest',
|
||||
A: 0.93,
|
||||
},
|
||||
{
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'frontend',
|
||||
operation: '/driver.DriverService/FindNearest',
|
||||
A: 0.93,
|
||||
},
|
||||
{
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'mysql',
|
||||
operation: 'SQL SELECT',
|
||||
A: 0.93,
|
||||
},
|
||||
{
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'customer',
|
||||
operation: 'HTTP GET /customer',
|
||||
A: 0.93,
|
||||
},
|
||||
{
|
||||
resource_host_name: '4f6ec470feea',
|
||||
service_name: 'frontend',
|
||||
operation: 'HTTP GET /dispatch',
|
||||
A: 0.93,
|
||||
},
|
||||
{
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
operation: 'check_request limit',
|
||||
A: 0.21,
|
||||
},
|
||||
{
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
operation: 'authenticate_check_cache',
|
||||
A: 0.21,
|
||||
},
|
||||
{
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
operation: 'authenticate_check_db',
|
||||
A: 0.21,
|
||||
},
|
||||
{
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
operation: 'authenticate',
|
||||
A: 0.21,
|
||||
},
|
||||
{
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
operation: 'check cart in cache',
|
||||
A: 0.21,
|
||||
},
|
||||
{
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
operation: 'get_cart',
|
||||
A: 0.2,
|
||||
},
|
||||
{
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
operation: 'check cart in db',
|
||||
A: 0.2,
|
||||
},
|
||||
{
|
||||
resource_host_name: '',
|
||||
service_name: 'demo-app',
|
||||
operation: 'home',
|
||||
A: 0.2,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -3,10 +3,6 @@
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&.docked {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
.sideNav {
|
||||
@@ -229,39 +225,6 @@
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.docked {
|
||||
flex: 0 0 240px;
|
||||
max-width: 240px;
|
||||
min-width: 240px;
|
||||
width: 240px;
|
||||
|
||||
.secondary-nav-items {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.get-started-nav-items {
|
||||
.get-started-btn {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-expand-handlers {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-item-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-item-beta {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import './SideNav.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Button } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
@@ -16,9 +16,6 @@ import history from 'lib/history';
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckSquare,
|
||||
ChevronLeftCircle,
|
||||
ChevronRightCircle,
|
||||
PanelRight,
|
||||
RocketIcon,
|
||||
UserCircle,
|
||||
} from 'lucide-react';
|
||||
@@ -55,13 +52,9 @@ interface UserManagementMenuItems {
|
||||
function SideNav({
|
||||
licenseData,
|
||||
isFetching,
|
||||
onCollapse,
|
||||
collapsed,
|
||||
}: {
|
||||
licenseData: any;
|
||||
isFetching: boolean;
|
||||
onCollapse: () => void;
|
||||
collapsed: boolean;
|
||||
}): JSX.Element {
|
||||
const [menuItems, setMenuItems] = useState(defaultMenuItems);
|
||||
|
||||
@@ -330,8 +323,6 @@ function SideNav({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
registerShortcut(GlobalShortcuts.SidebarCollapse, onCollapse);
|
||||
|
||||
registerShortcut(GlobalShortcuts.NavigateToServices, () =>
|
||||
onClickHandler(ROUTES.APPLICATION, null),
|
||||
);
|
||||
@@ -359,7 +350,6 @@ function SideNav({
|
||||
);
|
||||
|
||||
return (): void => {
|
||||
deregisterShortcut(GlobalShortcuts.SidebarCollapse);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToServices);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToTraces);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToLogs);
|
||||
@@ -368,11 +358,11 @@ function SideNav({
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToExceptions);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToMessagingQueues);
|
||||
};
|
||||
}, [deregisterShortcut, onClickHandler, onCollapse, registerShortcut]);
|
||||
}, [deregisterShortcut, onClickHandler, registerShortcut]);
|
||||
|
||||
return (
|
||||
<div className={cx('sidenav-container', !collapsed ? 'docked' : '')}>
|
||||
<div className={cx('sideNav', !collapsed ? 'docked' : '')}>
|
||||
<div className={cx('sidenav-container')}>
|
||||
<div className={cx('sideNav')}>
|
||||
<div className="brand">
|
||||
<div className="brand-company-meta">
|
||||
<div
|
||||
@@ -392,17 +382,6 @@ function SideNav({
|
||||
<div className="license tag nav-item-label">{licenseTag}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
title={collapsed ? 'Dock Sidebar' : 'Undock Sidebar'}
|
||||
placement="right"
|
||||
>
|
||||
<Button
|
||||
className="periscope-btn nav-item-label dockBtn"
|
||||
icon={<PanelRight size={16} />}
|
||||
onClick={onCollapse}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{isCloudUserVal && (
|
||||
@@ -504,14 +483,6 @@ function SideNav({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="collapse-expand-handlers" onClick={onCollapse}>
|
||||
{collapsed ? (
|
||||
<ChevronRightCircle size={18} />
|
||||
) : (
|
||||
<ChevronLeftCircle size={18} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,9 +14,8 @@ import { Pagination } from 'hooks/queryPagination';
|
||||
import useDragColumns from 'hooks/useDragColumns';
|
||||
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import history from 'lib/history';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { HTMLAttributes, memo, useCallback, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -25,7 +24,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { TracesLoading } from '../TraceLoading/TraceLoading';
|
||||
import { defaultSelectedColumns, PER_PAGE_OPTIONS } from './configs';
|
||||
import { Container, ErrorText, tableStyles } from './styles';
|
||||
import { getListColumns, getTraceLink, transformDataWithDate } from './utils';
|
||||
import { getListColumns, transformDataWithDate } from './utils';
|
||||
|
||||
interface ListViewProps {
|
||||
isFilterApplied: boolean;
|
||||
@@ -108,21 +107,6 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element {
|
||||
[queryTableData],
|
||||
);
|
||||
|
||||
const handleRow = useCallback(
|
||||
(record: RowData): HTMLAttributes<RowData> => ({
|
||||
onClick: (event): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getTraceLink(record), '_blank');
|
||||
} else {
|
||||
history.push(getTraceLink(record));
|
||||
}
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDragColumn = useCallback(
|
||||
(fromIndex: number, toIndex: number) =>
|
||||
onDragColumns(columns, fromIndex, toIndex),
|
||||
@@ -169,7 +153,6 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element {
|
||||
style={tableStyles}
|
||||
dataSource={transformedQueryTableData}
|
||||
columns={columns}
|
||||
onRow={handleRow}
|
||||
onDragColumn={handleDragColumn}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -47,11 +47,11 @@ export const getListColumns = (
|
||||
key: 'date',
|
||||
title: 'Timestamp',
|
||||
width: 145,
|
||||
render: (item): JSX.Element => {
|
||||
render: (value, item): JSX.Element => {
|
||||
const date =
|
||||
typeof item === 'string'
|
||||
? dayjs(item).format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||
: dayjs(item / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
typeof value === 'string'
|
||||
? dayjs(value).format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||
: dayjs(value / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
return (
|
||||
<BlockLink to={getTraceLink(item)}>
|
||||
<Typography.Text>{date}</Typography.Text>
|
||||
@@ -67,10 +67,10 @@ export const getListColumns = (
|
||||
dataIndex: key,
|
||||
key: `${key}-${dataType}-${type}`,
|
||||
width: 145,
|
||||
render: (value): JSX.Element => {
|
||||
render: (value, item): JSX.Element => {
|
||||
if (value === '') {
|
||||
return (
|
||||
<BlockLink to={getTraceLink(value)}>
|
||||
<BlockLink to={getTraceLink(item)}>
|
||||
<Typography data-testid={key}>N/A</Typography>
|
||||
</BlockLink>
|
||||
);
|
||||
@@ -78,7 +78,7 @@ export const getListColumns = (
|
||||
|
||||
if (key === 'httpMethod' || key === 'responseStatusCode') {
|
||||
return (
|
||||
<BlockLink to={getTraceLink(value)}>
|
||||
<BlockLink to={getTraceLink(item)}>
|
||||
<Tag data-testid={key} color="magenta">
|
||||
{value}
|
||||
</Tag>
|
||||
@@ -88,14 +88,14 @@ export const getListColumns = (
|
||||
|
||||
if (key === 'durationNano') {
|
||||
return (
|
||||
<BlockLink to={getTraceLink(value)}>
|
||||
<BlockLink to={getTraceLink(item)}>
|
||||
<Typography data-testid={key}>{getMs(value)}ms</Typography>
|
||||
</BlockLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockLink to={getTraceLink(value)}>
|
||||
<BlockLink to={getTraceLink(item)}>
|
||||
<Typography data-testid={key}>{value}</Typography>
|
||||
</BlockLink>
|
||||
);
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
height: 40px;
|
||||
justify-content: end;
|
||||
padding: 0 8px;
|
||||
margin: 12px 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ export const useGetCompositeQueryParam = (): Query | null => {
|
||||
try {
|
||||
if (!compositeQuery) return null;
|
||||
|
||||
parsedCompositeQuery = JSON.parse(decodeURIComponent(compositeQuery));
|
||||
// MDN reference - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url
|
||||
// MDN reference to support + characters using encoding - https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#preserving_plus_signs add later
|
||||
parsedCompositeQuery = JSON.parse(
|
||||
decodeURIComponent(compositeQuery.replace(/\+/g, ' ')),
|
||||
);
|
||||
} catch (e) {
|
||||
parsedCompositeQuery = null;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
/>
|
||||
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
|
||||
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
|
||||
<meta name="robots" content="noindex">
|
||||
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
||||
@@ -26,13 +26,13 @@ export async function GetMetricQueryRange(
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponse<MetricRangePayloadProps>> {
|
||||
const { legendMap, queryPayload } = prepareQueryRangePayload(props);
|
||||
|
||||
const response = await getMetricsQueryRange(
|
||||
queryPayload,
|
||||
version || 'v3',
|
||||
signal,
|
||||
headers,
|
||||
);
|
||||
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
let error = `API responded with ${response.statusCode} - ${response.error} status: ${response.message}`;
|
||||
@@ -78,7 +78,7 @@ export interface GetQueryResultsProps {
|
||||
query: Query;
|
||||
graphType: PANEL_TYPES;
|
||||
selectedTime: timePreferenceType;
|
||||
globalSelectedInterval: Time | TimeV2 | CustomTimeType;
|
||||
globalSelectedInterval?: Time | TimeV2 | CustomTimeType;
|
||||
variables?: Record<string, unknown>;
|
||||
params?: Record<string, unknown>;
|
||||
fillGaps?: boolean;
|
||||
@@ -87,4 +87,6 @@ export interface GetQueryResultsProps {
|
||||
pagination?: Pagination;
|
||||
selectColumns?: any;
|
||||
};
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export const prepareQueryRangePayload = ({
|
||||
variables = {},
|
||||
params = {},
|
||||
fillGaps = false,
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
}: GetQueryResultsProps): PrepareQueryRangePayload => {
|
||||
let legendMap: Record<string, string> = {};
|
||||
const {
|
||||
@@ -100,8 +102,8 @@ export const prepareQueryRangePayload = ({
|
||||
: undefined;
|
||||
|
||||
const queryPayload: QueryRangePayload = {
|
||||
start: parseInt(start, 10) * 1e3,
|
||||
end: endLogTimeStamp || parseInt(end, 10) * 1e3,
|
||||
start: startTime ? startTime * 1e3 : parseInt(start, 10) * 1e3,
|
||||
end: endTime ? endTime * 1e3 : endLogTimeStamp || parseInt(end, 10) * 1e3,
|
||||
step: getStep({
|
||||
start: allowSelectedIntervalForStepGen
|
||||
? start
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface GetUPlotChartOptions {
|
||||
}>
|
||||
>;
|
||||
customTooltipElement?: HTMLDivElement;
|
||||
verticalLineTimestamp?: number;
|
||||
}
|
||||
|
||||
/** the function converts series A , series B , series C to
|
||||
@@ -156,6 +157,7 @@ export const getUPlotChartOptions = ({
|
||||
hiddenGraph,
|
||||
setHiddenGraph,
|
||||
customTooltipElement,
|
||||
verticalLineTimestamp,
|
||||
}: GetUPlotChartOptions): uPlot.Options => {
|
||||
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
|
||||
|
||||
@@ -222,6 +224,29 @@ export const getUPlotChartOptions = ({
|
||||
onClick: onClickHandler,
|
||||
apiResponse,
|
||||
}),
|
||||
{
|
||||
hooks: {
|
||||
draw: [
|
||||
(u): void => {
|
||||
if (verticalLineTimestamp) {
|
||||
const { ctx } = u;
|
||||
ctx.save();
|
||||
ctx.setLineDash([4, 2]);
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.lineWidth = 1;
|
||||
const x = u.valToPos(verticalLineTimestamp, 'x', true);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, u.bbox.top);
|
||||
ctx.lineTo(x, u.bbox.top + u.bbox.height);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
ctx.restore();
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
draw: [
|
||||
|
||||
@@ -26,6 +26,21 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import LogsExplorer from '../index';
|
||||
|
||||
const queryRangeURL = 'http://localhost/api/v3/query_range';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
// mocking the graph components in this test as this should be handled separately
|
||||
jest.mock(
|
||||
'container/TimeSeriesView/TimeSeriesView',
|
||||
|
||||
@@ -10,8 +10,6 @@ import useLicense from 'hooks/useLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import {
|
||||
Book,
|
||||
Cable,
|
||||
Calendar,
|
||||
CreditCard,
|
||||
Github,
|
||||
MessageSquare,
|
||||
@@ -78,22 +76,6 @@ const supportChannels = [
|
||||
url: '',
|
||||
btnText: 'Launch chat',
|
||||
},
|
||||
{
|
||||
key: 'schedule_call',
|
||||
name: 'Schedule a call',
|
||||
icon: <Calendar />,
|
||||
title: 'Schedule a call with the founders.',
|
||||
url: 'https://calendly.com/vishal-signoz/30min',
|
||||
btnText: 'Schedule call',
|
||||
},
|
||||
{
|
||||
key: 'slack_connect',
|
||||
name: 'Slack Connect',
|
||||
icon: <Cable />,
|
||||
title: 'Get a dedicated support channel for your team.',
|
||||
url: '',
|
||||
btnText: 'Request Slack connect',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Support(): JSX.Element {
|
||||
@@ -122,20 +104,6 @@ export default function Support(): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleSlackConnectRequest = (): void => {
|
||||
const recipient = 'support@signoz.io';
|
||||
const subject = 'Slack Connect Request';
|
||||
const body = `I'd like to request a dedicated Slack Connect channel for me and my team. Users (emails) to include besides mine:`;
|
||||
|
||||
// Create the mailto link
|
||||
const mailtoLink = `mailto:${recipient}?subject=${encodeURIComponent(
|
||||
subject,
|
||||
)}&body=${encodeURIComponent(body)}`;
|
||||
|
||||
// Open the default email client
|
||||
window.location.href = mailtoLink;
|
||||
};
|
||||
|
||||
const isPremiumChatSupportEnabled =
|
||||
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
||||
|
||||
@@ -214,15 +182,11 @@ export default function Support(): JSX.Element {
|
||||
case channelsMap.documentation:
|
||||
case channelsMap.github:
|
||||
case channelsMap.slack_community:
|
||||
case channelsMap.schedule_call:
|
||||
handleChannelWithRedirects(channel.url);
|
||||
break;
|
||||
case channelsMap.chat:
|
||||
handleChat();
|
||||
break;
|
||||
case channelsMap.slack_connect:
|
||||
handleSlackConnectRequest();
|
||||
break;
|
||||
default:
|
||||
handleChannelWithRedirects('https://signoz.io/slack');
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.trace-explorer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.trace-explorer-run-query {
|
||||
display: flex;
|
||||
|
||||
@@ -32,4 +32,5 @@ function CustomerStoryCard({
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomerStoryCard;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
$light-theme: 'lightMode';
|
||||
$dark-theme: 'darkMode';
|
||||
|
||||
@keyframes gradientFlow {
|
||||
0% {
|
||||
@@ -147,6 +148,34 @@ $light-theme: 'lightMode';
|
||||
animation: gradientFlow 24s ease infinite;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
&__faq-container {
|
||||
width: 100%;
|
||||
|
||||
.ant-collapse,
|
||||
.ant-collapse-item,
|
||||
.ant-collapse-content-active {
|
||||
.#{$dark-theme} & {
|
||||
border-color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__customer-stories {
|
||||
&__left-container,
|
||||
&__right-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__left-container {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&__right-container {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact-us {
|
||||
|
||||
@@ -54,6 +54,25 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
data: licensesData,
|
||||
} = useLicense();
|
||||
|
||||
useEffect((): void => {
|
||||
logEvent('Workspace Blocked: Screen Viewed', {});
|
||||
}, []);
|
||||
|
||||
const handleContactUsClick = (): void => {
|
||||
logEvent('Workspace Blocked: Contact Us Clicked', {});
|
||||
};
|
||||
|
||||
const handleTabClick = (key: string): void => {
|
||||
logEvent('Workspace Blocked: Screen Tabs Clicked', { tabKey: key });
|
||||
};
|
||||
|
||||
const handleCollapseChange = (key: string | string[]): void => {
|
||||
const lastKey = Array.isArray(key) ? key.slice(-1)[0] : key;
|
||||
logEvent('Workspace Blocked: Screen Tab FAQ Item Clicked', {
|
||||
panelKey: lastKey,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingLicenseData) {
|
||||
const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
|
||||
@@ -135,7 +154,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
key: 'whyChooseSignoz',
|
||||
label: t('whyChooseSignoz'),
|
||||
children: (
|
||||
<Row align="middle" justify="center">
|
||||
@@ -182,13 +201,23 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
key: 'youAreInGoodCompany',
|
||||
label: t('youAreInGoodCompany'),
|
||||
children: (
|
||||
<Row gutter={[24, 16]} justify="center">
|
||||
{/* #FIXME: please suggest if there is any better way to loop in different columns to get the masonry layout */}
|
||||
<Col span={10}>{renderCustomerStories((index) => index % 2 === 0)}</Col>
|
||||
<Col span={10}>{renderCustomerStories((index) => index % 2 !== 0)}</Col>
|
||||
<Col
|
||||
span={10}
|
||||
className="workspace-locked__customer-stories__left-container"
|
||||
>
|
||||
{renderCustomerStories((index) => index % 2 === 0)}
|
||||
</Col>
|
||||
<Col
|
||||
span={10}
|
||||
className="workspace-locked__customer-stories__right-container"
|
||||
>
|
||||
{renderCustomerStories((index) => index % 2 !== 0)}
|
||||
</Col>
|
||||
{isAdmin && (
|
||||
<Col span={24}>
|
||||
<Flex justify="center">
|
||||
@@ -214,13 +243,21 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
// children: 'Our Pricing',
|
||||
// },
|
||||
{
|
||||
key: '4',
|
||||
key: 'faqs',
|
||||
label: t('faqs'),
|
||||
children: (
|
||||
<Row align="middle" justify="center">
|
||||
<Col span={18}>
|
||||
<Space size="large" direction="vertical">
|
||||
<Collapse items={faqData} defaultActiveKey={['1']} />
|
||||
<Col span={12}>
|
||||
<Space
|
||||
size="large"
|
||||
direction="vertical"
|
||||
className="workspace-locked__faq-container"
|
||||
>
|
||||
<Collapse
|
||||
items={faqData}
|
||||
defaultActiveKey={['signoz-cloud-vs-community']}
|
||||
onChange={handleCollapseChange}
|
||||
/>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -258,6 +295,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
size="middle"
|
||||
href="mailto:cloud-support@signoz.io"
|
||||
role="button"
|
||||
onClick={handleContactUsClick}
|
||||
>
|
||||
Contact Us
|
||||
</Button>
|
||||
@@ -324,7 +362,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
loading={isLoading}
|
||||
onClick={handleUpdateCreditCard}
|
||||
>
|
||||
continue my journey
|
||||
Continue my Journey
|
||||
</Button>
|
||||
</Col>
|
||||
<Col>
|
||||
@@ -340,9 +378,13 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Flex justify="center" className="workspace-locked__tabs">
|
||||
<Tabs items={tabItems} defaultActiveKey="2" />
|
||||
</Flex>
|
||||
<div className="workspace-locked__tabs">
|
||||
<Tabs
|
||||
items={tabItems}
|
||||
defaultActiveKey="youAreInGoodCompany"
|
||||
onTabClick={handleTabClick}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const enterpriseGradeValuesData = [
|
||||
|
||||
export const customerStoriesData = [
|
||||
{
|
||||
key: 'c-story-1',
|
||||
key: 'story-subomi-oluwalana',
|
||||
avatar: 'https://signoz.io/img/users/subomi-oluwalana.webp',
|
||||
personName: 'Subomi Oluwalana',
|
||||
role: 'Founder & CEO at Convoy',
|
||||
@@ -53,7 +53,7 @@ export const customerStoriesData = [
|
||||
'https://www.linkedin.com/feed/update/urn:li:activity:7212117589068591105/',
|
||||
},
|
||||
{
|
||||
key: 'c-story-2',
|
||||
key: 'story-dhruv-garg',
|
||||
avatar: 'https://signoz.io/img/users/dhruv-garg.webp',
|
||||
personName: 'Dhruv Garg',
|
||||
role: 'Tech Lead at Nudge',
|
||||
@@ -64,7 +64,7 @@ export const customerStoriesData = [
|
||||
'https://www.linkedin.com/posts/dhruv-garg79_signoz-docker-kubernetes-activity-7205163679028240384-Otlb/',
|
||||
},
|
||||
{
|
||||
key: 'c-story-3',
|
||||
key: 'story-vivek-bhakta',
|
||||
avatar: 'https://signoz.io/img/users/vivek-bhakta.webp',
|
||||
personName: 'Vivek Bhakta',
|
||||
role: 'CTO at Wombo AI',
|
||||
@@ -74,7 +74,7 @@ export const customerStoriesData = [
|
||||
link: 'https://x.com/notorious_VB/status/1701773119696904242',
|
||||
},
|
||||
{
|
||||
key: 'c-story-4',
|
||||
key: 'story-pranay-narang',
|
||||
avatar: 'https://signoz.io/img/users/pranay-narang.webp',
|
||||
personName: 'Pranay Narang',
|
||||
role: 'Engineering at Azodha',
|
||||
@@ -84,7 +84,7 @@ export const customerStoriesData = [
|
||||
link: 'https://x.com/PranayNarang/status/1676247073396752387',
|
||||
},
|
||||
{
|
||||
key: 'c-story-4',
|
||||
key: 'story-Sheheryar-Sewani',
|
||||
avatar: 'https://signoz.io/img/users/shey.webp',
|
||||
personName: 'Sheheryar Sewani',
|
||||
role: 'Seasoned Rails Dev & Founder',
|
||||
@@ -95,7 +95,7 @@ export const customerStoriesData = [
|
||||
'https://www.linkedin.com/feed/update/urn:li:activity:7181011853915926528/',
|
||||
},
|
||||
{
|
||||
key: 'c-story-5',
|
||||
key: 'story-daniel-schell',
|
||||
avatar: 'https://signoz.io/img/users/daniel.webp',
|
||||
personName: 'Daniel Schell',
|
||||
role: 'Founder & CTO at Airlockdigital',
|
||||
@@ -115,7 +115,7 @@ export const customerStoriesData = [
|
||||
link: 'https://x.com/gofrendiasgard/status/1680139003658641408',
|
||||
},
|
||||
{
|
||||
key: 'c-story-7',
|
||||
key: 'story-anselm-eickhoff',
|
||||
avatar: 'https://signoz.io/img/users/anselm.jpg',
|
||||
personName: 'Anselm Eickhoff',
|
||||
role: 'Software Architect',
|
||||
@@ -129,26 +129,26 @@ export const customerStoriesData = [
|
||||
|
||||
export const faqData = [
|
||||
{
|
||||
key: '1',
|
||||
key: 'signoz-cloud-vs-community',
|
||||
label:
|
||||
'What is the difference between SigNoz Cloud(Teams) and Community Edition?',
|
||||
children:
|
||||
'You can self-host and manage the community edition yourself. You should choose SigNoz Cloud if you don’t want to worry about managing the SigNoz cluster. There are some exclusive features like SSO & SAML support, which come with SigNoz cloud offering. Our team also offers support on the initial configuration of dashboards & alerts and advises on best practices for setting up your observability stack in the SigNoz cloud offering.',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
key: 'calc-for-metrics',
|
||||
label: 'How are number of samples calculated for metrics pricing?',
|
||||
children:
|
||||
"If a timeseries sends data every 30s, then it will generate 2 samples per min. So, if you have 10,000 time series sending data every 30s then you will be sending 20,000 samples per min to SigNoz. This will be around 864 mn samples per month and would cost 86.4 USD/month. Here's an explainer video on how metrics pricing is calculated - Link: https://vimeo.com/973012522",
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
key: 'enterprise-support-plans',
|
||||
label: 'Do you offer enterprise support plans?',
|
||||
children:
|
||||
'Yes, feel free to reach out to us on hello@signoz.io if you need a dedicated support plan or paid support for setting up your initial SigNoz setup.',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
key: 'who-should-use-enterprise-plans',
|
||||
label: 'Who should use Enterprise plans?',
|
||||
children:
|
||||
'Teams which need enterprise support or features like SSO, Audit logs, etc. may find our enterprise plans valuable.',
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import './LineClampedText.styles.scss';
|
||||
|
||||
import { Tooltip } from 'antd';
|
||||
import { Tooltip, TooltipProps } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
function LineClampedText({
|
||||
text,
|
||||
lines,
|
||||
tooltipProps,
|
||||
}: {
|
||||
text: string;
|
||||
lines?: number;
|
||||
tooltipProps?: TooltipProps;
|
||||
}): JSX.Element {
|
||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
@@ -42,11 +44,22 @@ function LineClampedText({
|
||||
</div>
|
||||
);
|
||||
|
||||
return isOverflowing ? <Tooltip title={text}>{content}</Tooltip> : content;
|
||||
return isOverflowing ? (
|
||||
<Tooltip
|
||||
title={text}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...tooltipProps}
|
||||
>
|
||||
{content}
|
||||
</Tooltip>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
LineClampedText.defaultProps = {
|
||||
lines: 1,
|
||||
tooltipProps: {},
|
||||
};
|
||||
|
||||
export default LineClampedText;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './sideBarCollapse';
|
||||
@@ -1,16 +0,0 @@
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
|
||||
export const sideBarCollapse = (
|
||||
collapseState: boolean,
|
||||
): ((dispatch: Dispatch<AppActions>) => void) => {
|
||||
setLocalStorageKey(IS_SIDEBAR_COLLAPSED, `${collapseState}`);
|
||||
return (dispatch: Dispatch<AppActions>): void => {
|
||||
dispatch({
|
||||
type: 'SIDEBAR_COLLAPSE',
|
||||
payload: collapseState,
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './app';
|
||||
export * from './global';
|
||||
export * from './metrics';
|
||||
export * from './serviceMap';
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { getInitialUserTokenRefreshToken } from 'store/utils';
|
||||
import {
|
||||
AppAction,
|
||||
LOGGED_IN,
|
||||
SIDEBAR_COLLAPSE,
|
||||
UPDATE_CONFIGS,
|
||||
UPDATE_CURRENT_ERROR,
|
||||
UPDATE_CURRENT_VERSION,
|
||||
@@ -44,7 +42,6 @@ const getInitialUser = (): User | null => {
|
||||
|
||||
const InitialValue: InitialValueTypes = {
|
||||
isLoggedIn: getLocalStorageKey(LOCALSTORAGE.IS_LOGGED_IN) === 'true',
|
||||
isSideBarCollapsed: getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
|
||||
currentVersion: '',
|
||||
latestVersion: '',
|
||||
featureResponse: {
|
||||
@@ -76,13 +73,6 @@ const appReducer = (
|
||||
};
|
||||
}
|
||||
|
||||
case SIDEBAR_COLLAPSE: {
|
||||
return {
|
||||
...state,
|
||||
isSideBarCollapsed: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
case UPDATE_FEATURE_FLAG_RESPONSE: {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -34,11 +34,6 @@ export interface LoggedInUser {
|
||||
};
|
||||
}
|
||||
|
||||
export interface SideBarCollapse {
|
||||
type: typeof SIDEBAR_COLLAPSE;
|
||||
payload: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateAppVersion {
|
||||
type: typeof UPDATE_CURRENT_VERSION;
|
||||
payload: {
|
||||
@@ -137,7 +132,6 @@ export interface UpdateFeatureFlag {
|
||||
|
||||
export type AppAction =
|
||||
| LoggedInUser
|
||||
| SideBarCollapse
|
||||
| UpdateAppVersion
|
||||
| UpdateLatestVersion
|
||||
| UpdateVersionError
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user