From 1b152c19ec1e3b8fe1842bcaeb2d3bee134464db Mon Sep 17 00:00:00 2001 From: Prashant Shahi Date: Thu, 13 Oct 2022 16:10:36 +0530 Subject: [PATCH 01/21] =?UTF-8?q?ci(e2e):=20=F0=9F=91=B7=20enable=20DEV=5F?= =?UTF-8?q?BUILD=20flag=20for=20query-service=20(#1636)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Prashant Shahi --- .github/workflows/e2e-k3s.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/e2e-k3s.yaml b/.github/workflows/e2e-k3s.yaml index 6b14a9c975..da3db01917 100644 --- a/.github/workflows/e2e-k3s.yaml +++ b/.github/workflows/e2e-k3s.yaml @@ -16,6 +16,8 @@ jobs: uses: actions/checkout@v2 - name: Build query-service image + env: + DEV_BUILD: 1 run: make build-ee-query-service-amd64 - name: Build frontend image From 46050a217c1e2ceb831aedd77a9fd980d0d3a0b2 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Wed, 26 Oct 2022 12:53:47 +0530 Subject: [PATCH 02/21] feat: all trace now open in new tab (#1662) --- frontend/src/container/Trace/TraceTable/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/container/Trace/TraceTable/index.tsx b/frontend/src/container/Trace/TraceTable/index.tsx index 96fece7ba4..c532ff2683 100644 --- a/frontend/src/container/Trace/TraceTable/index.tsx +++ b/frontend/src/container/Trace/TraceTable/index.tsx @@ -3,7 +3,6 @@ import Table, { ColumnsType } from 'antd/lib/table'; import ROUTES from 'constants/routes'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; -import history from 'lib/history'; import omit from 'lodash-es/omit'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -195,7 +194,7 @@ function TraceTable(): JSX.Element { onClick: (event): void => { event.preventDefault(); event.stopPropagation(); - history.push(getLink(record)); + window.open(getLink(record)); }, })} pagination={{ From 36315fcf9c3d740a44bb4f791a52e8f4e2361e11 Mon Sep 17 00:00:00 2001 From: Pang <84171958+katepangLiu@users.noreply.github.com> Date: Thu, 3 Nov 2022 08:39:15 +0800 Subject: [PATCH 03/21] fix README.zh-cn.md readable (#1647) Co-authored-by: Pranay Prateek --- README.zh-cn.md | 63 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/README.zh-cn.md b/README.zh-cn.md index d584d10f4e..3658eeb520 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -13,14 +13,19 @@ ## -SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNoz使用分布式跟踪来增加软件技术栈的可见性。 +SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNoz使用分布式追踪来增加软件技术栈的可见性。 -👉 你能看到一些性能矩阵,服务、外部api调用、每个终端(endpoint)的p99延迟和错误率。 +👉 你能看到一些性能指标,服务、外部api调用、每个终端(endpoint)的p99延迟和错误率。 -👉 通过准确的跟踪来确定是什么引起了问题,并且可以看到每个独立请求的帧图(framegraph),这样你就能找到根本原因。 +👉 通过准确的追踪来确定是什么引起了问题,并且可以看到每个独立请求的帧图(framegraph),这样你就能找到根本原因。 +👉 聚合trace数据来获得业务相关指标。 -![SigNoz Feature](https://signoz-public.s3.us-east-2.amazonaws.com/signoz_hero_github.png) +![screenzy-1644432902955](https://user-images.githubusercontent.com/504541/153270713-1b2156e6-ec03-42de-975b-3c02b8ec1836.png) +
+![screenzy-1644432986784](https://user-images.githubusercontent.com/504541/153270725-0efb73b3-06ed-4207-bf13-9b7e2e17c4b8.png) +
+![screenzy-1647005040573](https://user-images.githubusercontent.com/504541/157875938-a3d57904-ea6d-4278-b929-bd1408d7f94c.png)

@@ -36,12 +41,12 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo ## 功能: -- 应用总览矩阵(matrix),如RPS, 50/90/99百分比延迟率,错误率 +- 应用概览指标(metrics),如RPS, p50/p90/p99延迟率分位值,错误率等。 - 应用中最慢的终端(endpoint) -- 查看准确的网络请求跟踪来分析下游服务问题、慢数据库查询问题 及调用第三方服务如支付网关的问题 -- 通过服务名称、操作、延迟、错误、标签来过滤跟踪 -- 对过滤后的跟踪数据做矩阵聚合。比如,获得过滤条件`customer_type: gold` or `deployment_version: v2` or `external_call: paypal`的错误率和p99延迟 -- 整合的矩阵和跟踪用户界面。不需要像从Prometheus切换到Jaeger才能调试问题 +- 查看特定请求的trace数据来分析下游服务问题、慢数据库查询问题 及调用第三方服务如支付网关的问题 +- 通过服务名称、操作、延迟、错误、标签来过滤traces。 +- 聚合trace数据(events/spans)来得到业务相关指标。比如,你可以通过过滤条件`customer_type: gold` or `deployment_version: v2` or `external_call: paypal` 来获取指定业务的错误率和p99延迟 +- 为metrics和trace提供统一的UI。排查问题不需要在Prometheus和Jaeger之间切换。

@@ -53,7 +58,7 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo 我们想做一个自服务的开源版本的工具,类似于DataDog和NewRelic,用于那些对客户数据流入第三方有隐私和安全担忧的厂商。 -开源也让你对配置、采样和上线率有完整的控制,你可以在SigNoz基础上构建模块来满足特定的商业需求。 +开源也让你对配置、采样和正常运行时间有完整的控制,你可以在SigNoz基础上构建模块来满足特定的商业需求。 ### 语言支持 @@ -71,8 +76,8 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo ## 入门 - - + + ### 使用Docker部署 请按照[这里](https://signoz.io/docs/deployment/docker/)列出的步骤使用Docker来安装 @@ -80,35 +85,34 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo 如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/deployment/troubleshooting)会对你有帮助。

 

- - + + ### 使用Helm在Kubernetes上部署 请跟着[这里](https://signoz.io/docs/deployment/helm_chart)的步骤使用helm charts安装 -

-## Comparisons to Familiar Tools +## 与其他方案的比较 ### SigNoz vs Prometheus -如果你只是需要矩阵,那Prometheus是不错的,但如果你要无缝的在矩阵和跟踪之间切换,那目前把Prometheus & Jaeger串起来的体验并不好。 +如果你只是需要监控指标(metrics),那Prometheus是不错的,但如果你要无缝的在metrics和traces之间切换,那目前把Prometheus & Jaeger串起来的体验并不好。 -我们的目标是在矩阵和跟踪之间提供整合的UI - 类似于Datadog这样的Saas厂提供的方案,能够对跟踪进行过滤和聚合,这是目前Jaeger缺失的功能。 +我们的目标是为metrics和traces提供统一的UI - 类似于Datadog这样的Saas厂提供的方案。并且能够对trace进行过滤和聚合,这是目前Jaeger缺失的功能。

 

### SigNoz vs Jaeger -Jaeger只做分布式跟踪,SigNoz则是做了矩阵和跟踪两块,我们在计划中也有日志管理功能。 +Jaeger只做分布式追踪(distributed tracing),SigNoz则支持metrics,traces,logs ,即可视化的三大支柱。 并且SigNoz有一些Jaeger没有的高级功能: -- Jaegar UI无法在跟踪或过滤的跟踪基础上展示矩阵。 -- Jaeger不能在过滤的跟踪上进行聚合操作。例如,拥有tag为customer_type='premium'的所有请求的p99延迟,在SigNoz里这很容易实现。 +- Jaegar UI无法在traces或过滤的traces上展示metrics。 +- Jaeger不能对过滤的traces做聚合操作。例如,拥有tag为customer_type='premium'的所有请求的p99延迟。而这个功能在SigNoz这儿是很容易实现。

@@ -121,6 +125,23 @@ Jaeger只做分布式跟踪,SigNoz则是做了矩阵和跟踪两块,我们 还不清楚怎么开始? 只需在[slack社区](https://signoz.io/slack)的`#contributing`频道里ping我们。 +### Project maintainers + +#### Backend + +- [Ankit Nayan](https://github.com/ankitnayan) +- [Nityananda Gohain](https://github.com/nityanandagohain) +- [Srikanth Chekuri](https://github.com/srikanthccv) +- [Vishal Sharma](https://github.com/makeavish) + +#### Frontend + +- [Palash Gupta](https://github.com/palashgdev) + +#### DevOps + +- [Prashant Shahi](https://github.com/prashant-shahi) +

From 674883cd18a3f3e1d9e924d56d85777ff4da5bf5 Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Wed, 9 Nov 2022 08:30:00 +0530 Subject: [PATCH 04/21] Feature flagging (#1674) * feat: introduce feature flagging via env variables * refactor: enable sorting by default for users --- ee/query-service/license/manager.go | 24 ++++++++++++--- pkg/query-service/app/http_handler.go | 17 ++++++++++- pkg/query-service/app/parser.go | 16 +++++++++- pkg/query-service/app/server.go | 12 ++++++-- pkg/query-service/constants/constants.go | 30 ++++++++++++++++++ pkg/query-service/featureManager/manager.go | 34 +++++++++++++++++++++ 6 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 pkg/query-service/featureManager/manager.go diff --git a/ee/query-service/license/manager.go b/ee/query-service/license/manager.go index 306fa5a8d1..a3e9ba0771 100644 --- a/ee/query-service/license/manager.go +++ b/ee/query-service/license/manager.go @@ -10,6 +10,8 @@ import ( "sync" + baseconstants "go.signoz.io/signoz/pkg/query-service/constants" + validate "go.signoz.io/signoz/ee/query-service/integrations/signozio" "go.signoz.io/signoz/ee/query-service/model" basemodel "go.signoz.io/signoz/pkg/query-service/model" @@ -92,6 +94,8 @@ func (lm *Manager) SetActive(l *model.License) { lm.activeLicense = l lm.activeFeatures = l.FeatureSet + // set default features + setDefaultFeatures(lm) if !lm.validatorRunning { // we want to make sure only one validator runs, // we already have lock() so good to go @@ -101,7 +105,13 @@ func (lm *Manager) SetActive(l *model.License) { } -// LoadActiveLicense loads the most recent active licenseex +func setDefaultFeatures(lm *Manager) { + for k, v := range baseconstants.DEFAULT_FEATURE_SET { + lm.activeFeatures[k] = v + } +} + +// LoadActiveLicense loads the most recent active license func (lm *Manager) LoadActiveLicense() error { var err error active, err := lm.repo.GetActiveLicense(context.Background()) @@ -111,7 +121,10 @@ func (lm *Manager) LoadActiveLicense() error { if active != nil { lm.SetActive(active) } else { - zap.S().Info("No active license found.") + zap.S().Info("No active license found, defaulting to basic plan") + // if no active license is found, we default to basic(free) plan with all default features + lm.activeFeatures = basemodel.BasicPlan + setDefaultFeatures(lm) } return nil @@ -278,8 +291,11 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m // CheckFeature will be internally used by backend routines // for feature gating func (lm *Manager) CheckFeature(featureKey string) error { - if _, ok := lm.activeFeatures[featureKey]; ok { - return nil + if value, ok := lm.activeFeatures[featureKey]; ok { + if value { + return nil + } + return basemodel.ErrFeatureUnavailable{Key: featureKey} } return basemodel.ErrFeatureUnavailable{Key: featureKey} } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 1d648f4651..473d253585 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -358,6 +358,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) { router.HandleFunc("/api/v1/settings/ttl", ViewAccess(aH.getTTL)).Methods(http.MethodGet) router.HandleFunc("/api/v1/version", OpenAccess(aH.getVersion)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/featureFlags", OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet) router.HandleFunc("/api/v1/getSpanFilters", ViewAccess(aH.getSpanFilters)).Methods(http.MethodPost) router.HandleFunc("/api/v1/getTagFilters", ViewAccess(aH.getTagFilters)).Methods(http.MethodPost) @@ -1422,7 +1423,7 @@ func (aH *APIHandler) getSpanFilters(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) getFilteredSpans(w http.ResponseWriter, r *http.Request) { - query, err := parseFilteredSpansRequest(r) + query, err := parseFilteredSpansRequest(r, aH) if aH.HandleError(w, err, http.StatusBadRequest) { return } @@ -1533,6 +1534,20 @@ func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) { aH.WriteJSON(w, r, map[string]string{"version": version, "ee": "N"}) } +func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { + featureSet := aH.FF().GetFeatureFlags() + aH.Respond(w, featureSet) +} + +func (aH *APIHandler) FF() interfaces.FeatureLookup { + return aH.featureFlags +} + +func (aH *APIHandler) CheckFeature(f string) bool { + err := aH.FF().CheckFeature(f) + return err == nil +} + // inviteUser is used to invite a user. It is used by an admin api. func (aH *APIHandler) inviteUser(w http.ResponseWriter, r *http.Request) { req, err := parseInviteRequest(r) diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go index 2fed317973..bfd4042d22 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -255,7 +255,7 @@ func parseSpanFilterRequestBody(r *http.Request) (*model.SpanFilterParams, error return postData, nil } -func parseFilteredSpansRequest(r *http.Request) (*model.GetFilteredSpansParams, error) { +func parseFilteredSpansRequest(r *http.Request, aH *APIHandler) (*model.GetFilteredSpansParams, error) { var postData *model.GetFilteredSpansParams err := json.NewDecoder(r.Body).Decode(&postData) @@ -277,6 +277,20 @@ func parseFilteredSpansRequest(r *http.Request) (*model.GetFilteredSpansParams, postData.Limit = 10 } + if len(postData.Order) != 0 { + if postData.Order != constants.Ascending && postData.Order != constants.Descending { + return nil, errors.New("order param is not in correct format") + } + if postData.OrderParam != constants.Duration && postData.OrderParam != constants.Timestamp { + return nil, errors.New("order param is not in correct format") + } + if postData.OrderParam == constants.Duration && !aH.CheckFeature(constants.DurationSort) { + return nil, model.ErrFeatureUnavailable{Key: constants.DurationSort} + } else if postData.OrderParam == constants.Timestamp && !aH.CheckFeature(constants.TimestampSort) { + return nil, model.ErrFeatureUnavailable{Key: constants.TimestampSort} + } + } + return postData, nil } diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 2b46ae8fed..cbb8a807fa 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -19,6 +19,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/app/dashboards" "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/dao" + "go.signoz.io/signoz/pkg/query-service/featureManager" "go.signoz.io/signoz/pkg/query-service/healthcheck" am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager" "go.signoz.io/signoz/pkg/query-service/interfaces" @@ -77,6 +78,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { } localDB.SetMaxOpenConns(10) + + // initiate feature manager + fm := featureManager.StartManager() + readerReady := make(chan bool) var reader interfaces.Reader @@ -98,9 +103,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { telemetry.GetInstance().SetReader(reader) apiHandler, err := NewAPIHandler(APIHandlerOpts{ - Reader: reader, - AppDao: dao.DB(), - RuleManager: rm, + Reader: reader, + AppDao: dao.DB(), + RuleManager: rm, + FeatureFlags: fm, }) if err != nil { return nil, err diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index 6e3af77a5e..46b3d4651f 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -28,6 +28,9 @@ const TraceTTL = "traces" const MetricsTTL = "metrics" const LogsTTL = "logs" +const DurationSort = "DurationSort" +const TimestampSort = "TimestampSort" + func GetAlertManagerApiPrefix() string { if os.Getenv("ALERTMANAGER_API_PREFIX") != "" { return os.Getenv("ALERTMANAGER_API_PREFIX") @@ -40,6 +43,33 @@ var AmChannelApiPath = GetOrDefaultEnv("ALERTMANAGER_API_CHANNEL_PATH", "v1/rout var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db") +var DurationSortFeature = GetOrDefaultEnv("DURATION_SORT_FEATURE", "true") + +var TimestampSortFeature = GetOrDefaultEnv("TIMESTAMP_SORT_FEATURE", "true") + +func IsDurationSortFeatureEnabled() bool { + isDurationSortFeatureEnabledStr := DurationSortFeature + isDurationSortFeatureEnabledBool, err := strconv.ParseBool(isDurationSortFeatureEnabledStr) + if err != nil { + return false + } + return isDurationSortFeatureEnabledBool +} + +func IsTimestampSortFeatureEnabled() bool { + isTimestampSortFeatureEnabledStr := TimestampSortFeature + isTimestampSortFeatureEnabledBool, err := strconv.ParseBool(isTimestampSortFeatureEnabledStr) + if err != nil { + return false + } + return isTimestampSortFeatureEnabledBool +} + +var DEFAULT_FEATURE_SET = model.FeatureSet{ + DurationSort: IsDurationSortFeatureEnabled(), + TimestampSort: IsTimestampSortFeatureEnabled(), +} + const ( TraceID = "traceID" ServiceName = "serviceName" diff --git a/pkg/query-service/featureManager/manager.go b/pkg/query-service/featureManager/manager.go new file mode 100644 index 0000000000..1c8c953982 --- /dev/null +++ b/pkg/query-service/featureManager/manager.go @@ -0,0 +1,34 @@ +package featureManager + +import ( + "go.signoz.io/signoz/pkg/query-service/constants" + "go.signoz.io/signoz/pkg/query-service/model" +) + +type FeatureManager struct { + activeFeatures model.FeatureSet +} + +func StartManager() *FeatureManager { + fM := &FeatureManager{ + activeFeatures: constants.DEFAULT_FEATURE_SET, + } + return fM +} + +// CheckFeature will be internally used by backend routines +// for feature gating +func (fm *FeatureManager) CheckFeature(featureKey string) error { + if value, ok := fm.activeFeatures[featureKey]; ok { + if value { + return nil + } + return model.ErrFeatureUnavailable{Key: featureKey} + } + return model.ErrFeatureUnavailable{Key: featureKey} +} + +// GetFeatureFlags returns current active features +func (fm *FeatureManager) GetFeatureFlags() model.FeatureSet { + return fm.activeFeatures +} From 9735a6e5ce28bf7ffcc30d15b7b23931924a778e Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 10 Nov 2022 16:49:54 +0530 Subject: [PATCH 05/21] feat: add ability to import Grafana dashboards (#1700) * feat: add ability to import Grafana dashboards * chore: remove unnecessary file * chore: more 9XX support * chore: some more hacks * chore: update deps * chore: arrange equal spaced widgets instead of inheriting from grafana --- frontend/public/locales/en-GB/dashboard.json | 1 + frontend/public/locales/en/dashboard.json | 1 + frontend/src/api/dashboard/create.ts | 3 +- .../ListOfDashboard/ImportJSON/index.tsx | 3 + .../src/container/ListOfDashboard/index.tsx | 19 +- frontend/src/types/api/dashboard/create.ts | 3 +- go.mod | 1 + go.sum | 2 + pkg/query-service/app/dashboards/model.go | 199 ++++++++++++++ pkg/query-service/app/http_handler.go | 35 +++ pkg/query-service/model/dashboards.go | 252 ++++++++++++++++++ 11 files changed, 514 insertions(+), 5 deletions(-) create mode 100644 pkg/query-service/model/dashboards.go diff --git a/frontend/public/locales/en-GB/dashboard.json b/frontend/public/locales/en-GB/dashboard.json index 7f21149511..b643f4727d 100644 --- a/frontend/public/locales/en-GB/dashboard.json +++ b/frontend/public/locales/en-GB/dashboard.json @@ -1,6 +1,7 @@ { "create_dashboard": "Create Dashboard", "import_json": "Import JSON", + "import_grafana_json": "Import Grafana JSON", "copy_to_clipboard": "Copy To ClipBoard", "download_json": "Download JSON", "view_json": "View JSON", diff --git a/frontend/public/locales/en/dashboard.json b/frontend/public/locales/en/dashboard.json index 7f21149511..b643f4727d 100644 --- a/frontend/public/locales/en/dashboard.json +++ b/frontend/public/locales/en/dashboard.json @@ -1,6 +1,7 @@ { "create_dashboard": "Create Dashboard", "import_json": "Import JSON", + "import_grafana_json": "Import Grafana JSON", "copy_to_clipboard": "Copy To ClipBoard", "download_json": "Download JSON", "view_json": "View JSON", diff --git a/frontend/src/api/dashboard/create.ts b/frontend/src/api/dashboard/create.ts index ab2ace2144..3796eb685e 100644 --- a/frontend/src/api/dashboard/create.ts +++ b/frontend/src/api/dashboard/create.ts @@ -7,8 +7,9 @@ import { PayloadProps, Props } from 'types/api/dashboard/create'; const create = async ( props: Props, ): Promise | ErrorResponse> => { + const url = props.uploadedGrafana ? '/dashboards/grafana' : '/dashboards'; try { - const response = await axios.post('/dashboards', { + const response = await axios.post(url, { ...props, }); diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx index e1107fd9f2..60f429a744 100644 --- a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx +++ b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx @@ -26,6 +26,7 @@ import { EditorContainer, FooterContainer } from './styles'; function ImportJSON({ isImportJSONModalVisible, + uploadedGrafana, onModalHandler, }: ImportJSONProps): JSX.Element { const [jsonData, setJsonData] = useState>(); @@ -89,6 +90,7 @@ function ImportJSON({ const response = await createDashboard({ ...parsedWidgets, + uploadedGrafana, }); if (response.statusCode === 200) { @@ -186,6 +188,7 @@ function ImportJSON({ interface ImportJSONProps { isImportJSONModalVisible: boolean; onModalHandler: VoidFunction; + uploadedGrafana: boolean; } export default ImportJSON; diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index cb375b9742..a2d1b0c37a 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -57,6 +57,7 @@ function ListOfAllDashboard(): JSX.Element { isImportJSONModalVisible, setIsImportJSONModalVisible, ] = useState(false); + const [uploadedGrafana, setUploadedGrafana] = useState(false); const [filteredDashboards, setFilteredDashboards] = useState(); @@ -137,6 +138,7 @@ function ListOfAllDashboard(): JSX.Element { title: t('new_dashboard_title', { ns: 'dashboard', }), + uploadedGrafana: false, }); if (response.statusCode === 200) { @@ -182,8 +184,9 @@ function ListOfAllDashboard(): JSX.Element { newDashboardState.loading, ]); - const onModalHandler = (): void => { + const onModalHandler = (uploadedGrafana: boolean): void => { setIsImportJSONModalVisible((state) => !state); + setUploadedGrafana(uploadedGrafana); }; const menu = useMemo( @@ -198,9 +201,18 @@ function ListOfAllDashboard(): JSX.Element { {t('create_dashboard')} )} - + onModalHandler(false)} + key={t('import_json').toString()} + > {t('import_json')} + onModalHandler(true)} + key={t('import_grafana_json').toString()} + > + {t('import_grafana_json')} + ), [createNewDashboard, loading, onNewDashboardHandler, t], @@ -256,7 +268,8 @@ function ListOfAllDashboard(): JSX.Element { onModalHandler(false)} /> Date: Thu, 10 Nov 2022 18:24:20 +0530 Subject: [PATCH 06/21] fix: parser updated to support escaped quotes in search (#1704) Co-authored-by: Palash Gupta --- pkg/query-service/app/logs/parser.go | 2 +- pkg/query-service/app/logs/parser_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/query-service/app/logs/parser.go b/pkg/query-service/app/logs/parser.go index 1ccb0dbc54..a1ab021e33 100644 --- a/pkg/query-service/app/logs/parser.go +++ b/pkg/query-service/app/logs/parser.go @@ -36,7 +36,7 @@ const ( DESC = "desc" ) -var tokenRegex, _ = regexp.Compile(`(?i)(and( )*?|or( )*?)?(([\w.-]+ (in|nin) \([^(]+\))|([\w.]+ (gt|lt|gte|lte) (')?[\S]+(')?)|([\w.]+ (contains|ncontains)) '[^']+')`) +var tokenRegex, _ = regexp.Compile(`(?i)(and( )*?|or( )*?)?(([\w.-]+ (in|nin) \([^(]+\))|([\w.]+ (gt|lt|gte|lte) (')?[\S]+(')?)|([\w.]+ (contains|ncontains)) [^\\]?'(.*?[^\\])')`) var operatorRegex, _ = regexp.Compile(`(?i)(?: )(in|nin|gt|lt|gte|lte|contains|ncontains)(?: )`) func ParseLogFilterParams(r *http.Request) (*model.LogsFilterParams, error) { diff --git a/pkg/query-service/app/logs/parser_test.go b/pkg/query-service/app/logs/parser_test.go index 439e323739..f4b75f41e0 100644 --- a/pkg/query-service/app/logs/parser_test.go +++ b/pkg/query-service/app/logs/parser_test.go @@ -29,8 +29,8 @@ var correctQueriesTest = []struct { }, { `contains search with a different attributes`, - `resource contains 'Hello, "World"'`, - []string{`resource ILIKE '%Hello, "World"%' `}, + `resource contains 'Hello, "World" and user\'s'`, + []string{`resource ILIKE '%Hello, "World" and user\'s%' `}, }, { `more than one continas`, From 65af8c1b98d85469da6fdb40584df24457c9dbb4 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Thu, 10 Nov 2022 20:48:40 +0530 Subject: [PATCH 07/21] 801 dropdown is added in the dashboard page (#1669) * chore: update the import from constant rather than static string * chore: removed redundant div * feat: added auto refresh component * refactor: top nav is refactored --- frontend/src/constants/app.ts | 2 + .../container/TopNav/AutoRefresh/config.ts | 63 ++++++++++ .../container/TopNav/AutoRefresh/index.tsx | 108 ++++++++++++++++++ .../container/TopNav/AutoRefresh/styles.ts | 9 ++ .../TopNav/DateTimeSelection/Refresh.tsx | 8 +- .../TopNav/DateTimeSelection/config.ts | 16 +++ .../TopNav/DateTimeSelection/index.tsx | 23 +++- .../TopNav/DateTimeSelection/styles.ts | 12 +- frontend/src/container/TopNav/index.tsx | 52 ++++----- frontend/src/store/actions/global.ts | 3 +- 10 files changed, 250 insertions(+), 46 deletions(-) create mode 100644 frontend/src/container/TopNav/AutoRefresh/config.ts create mode 100644 frontend/src/container/TopNav/AutoRefresh/index.tsx create mode 100644 frontend/src/container/TopNav/AutoRefresh/styles.ts diff --git a/frontend/src/constants/app.ts b/frontend/src/constants/app.ts index 68bfe983db..8529db4e4d 100644 --- a/frontend/src/constants/app.ts +++ b/frontend/src/constants/app.ts @@ -11,3 +11,5 @@ export const INVITE_MEMBERS_HASH = '#invite-team-members'; export const SIGNOZ_UPGRADE_PLAN_URL = 'https://upgrade.signoz.io/upgrade-from-app'; + +export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval'; diff --git a/frontend/src/container/TopNav/AutoRefresh/config.ts b/frontend/src/container/TopNav/AutoRefresh/config.ts new file mode 100644 index 0000000000..240b7d1f25 --- /dev/null +++ b/frontend/src/container/TopNav/AutoRefresh/config.ts @@ -0,0 +1,63 @@ +export const options: IOptions[] = [ + { + label: '', + key: 'off', + value: 0, + }, + { + label: '5s', + key: '5s', + value: 5000, + }, + { + label: '10s', + key: '10s', + value: 10000, + }, + { + label: '30s', + key: '30s', + value: 30000, + }, + { + label: '1m', + key: '1m', + value: 60000, + }, + { + label: '5m', + key: '5m', + value: 300000, + }, + { + label: '10m', + key: '10m', + value: 600000, + }, + { + label: '30m', + key: '30m', + value: 1800000, + }, + { + label: '1h', + key: '1h', + value: 3600000, + }, + { + label: '2h', + key: '2h', + value: 7200000, + }, + { + label: '1d', + key: '1d', + value: 86400000, + }, +]; + +export interface IOptions { + label: string; + key: string; + value: number; +} diff --git a/frontend/src/container/TopNav/AutoRefresh/index.tsx b/frontend/src/container/TopNav/AutoRefresh/index.tsx new file mode 100644 index 0000000000..f81875bcc3 --- /dev/null +++ b/frontend/src/container/TopNav/AutoRefresh/index.tsx @@ -0,0 +1,108 @@ +import { Select } from 'antd'; +import get from 'api/browser/localstorage/get'; +import set from 'api/browser/localstorage/set'; +import { DASHBOARD_TIME_IN_DURATION } from 'constants/app'; +import dayjs from 'dayjs'; +import useUrlQuery from 'hooks/useUrlQuery'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import { useInterval } from 'react-use'; +import { Dispatch } from 'redux'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import { options } from './config'; +import { SelectContainer } from './styles'; + +function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { + const { minTime: initialMinTime, selectedTime } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + const { pathname } = useLocation(); + + const params = useUrlQuery(); + + const localStorageData = JSON.parse(get(DASHBOARD_TIME_IN_DURATION) || '{}'); + + const localStorageValue = useMemo(() => localStorageData[pathname], [ + pathname, + localStorageData, + ]); + + const dispatch = useDispatch>(); + const [selectedOption, setSelectedOption] = useState( + localStorageValue || options[0].key, + ); + + useEffect(() => { + setSelectedOption(localStorageValue || options[0].key); + }, [localStorageValue, params]); + + const getOption = useMemo( + () => options.find((option) => option.key === selectedOption), + [selectedOption], + ); + + useInterval(() => { + const selectedValue = getOption?.value; + + if (disabled) { + return; + } + + if (selectedOption !== 'off' && selectedValue) { + const min = initialMinTime / 1000000; + + dispatch({ + type: UPDATE_TIME_INTERVAL, + payload: { + maxTime: dayjs().valueOf() * 1000000, + minTime: dayjs(min).subtract(selectedValue, 'second').valueOf() * 1000000, + selectedTime, + }, + }); + } + }, getOption?.value || 0); + + const onChangeHandler = useCallback( + (value: unknown) => { + if (typeof value === 'string') { + setSelectedOption(value); + params.set(DASHBOARD_TIME_IN_DURATION, value); + set( + DASHBOARD_TIME_IN_DURATION, + JSON.stringify({ ...localStorageData, [pathname]: value }), + ); + } + }, + [params, pathname, localStorageData], + ); + + return ( + + {options.map((option) => ( + + {option.label} + + ))} + + ); +} + +interface AutoRefreshProps { + disabled?: boolean; +} + +AutoRefresh.defaultProps = { + disabled: false, +}; + +export default AutoRefresh; diff --git a/frontend/src/container/TopNav/AutoRefresh/styles.ts b/frontend/src/container/TopNav/AutoRefresh/styles.ts new file mode 100644 index 0000000000..6e8380703f --- /dev/null +++ b/frontend/src/container/TopNav/AutoRefresh/styles.ts @@ -0,0 +1,9 @@ +import { Select } from 'antd'; +import styled from 'styled-components'; + +export const SelectContainer = styled(Select)` + &&& { + width: 100%; + min-width: 4rem; + } +`; diff --git a/frontend/src/container/TopNav/DateTimeSelection/Refresh.tsx b/frontend/src/container/TopNav/DateTimeSelection/Refresh.tsx index 36b1163dda..f4597e5727 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/Refresh.tsx +++ b/frontend/src/container/TopNav/DateTimeSelection/Refresh.tsx @@ -2,7 +2,10 @@ import React, { useEffect, useState } from 'react'; import { RefreshTextContainer, Typography } from './styles'; -function RefreshText({ onLastRefreshHandler }: RefreshTextProps): JSX.Element { +function RefreshText({ + onLastRefreshHandler, + refreshButtonHidden, +}: RefreshTextProps): JSX.Element { const [refreshText, setRefreshText] = useState(''); // this is to update the refresh text @@ -19,7 +22,7 @@ function RefreshText({ onLastRefreshHandler }: RefreshTextProps): JSX.Element { }, [onLastRefreshHandler, refreshText]); return ( - + {refreshText} ); @@ -27,6 +30,7 @@ function RefreshText({ onLastRefreshHandler }: RefreshTextProps): JSX.Element { interface RefreshTextProps { onLastRefreshHandler: () => string; + refreshButtonHidden: boolean; } export default RefreshText; diff --git a/frontend/src/container/TopNav/DateTimeSelection/config.ts b/frontend/src/container/TopNav/DateTimeSelection/config.ts index 1654def09f..d327476e08 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/config.ts +++ b/frontend/src/container/TopNav/DateTimeSelection/config.ts @@ -67,3 +67,19 @@ export const getOptions = (routes: string): Option[] => { } return Options; }; + +export const routesToSkip = [ + ROUTES.SETTINGS, + ROUTES.LIST_ALL_ALERT, + ROUTES.TRACE_DETAIL, + ROUTES.ALL_CHANNELS, + ROUTES.USAGE_EXPLORER, + ROUTES.INSTRUMENTATION, + ROUTES.VERSION, + ROUTES.ALL_DASHBOARD, + ROUTES.ORG_SETTINGS, + ROUTES.ERROR_DETAIL, + ROUTES.ALERTS_NEW, + ROUTES.EDIT_ALERTS, + ROUTES.LIST_ALL_ALERT, +]; diff --git a/frontend/src/container/TopNav/DateTimeSelection/index.tsx b/frontend/src/container/TopNav/DateTimeSelection/index.tsx index c170a7cef3..9d62c36a8d 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/index.tsx +++ b/frontend/src/container/TopNav/DateTimeSelection/index.tsx @@ -1,3 +1,4 @@ +import { SyncOutlined } from '@ant-design/icons'; import { Button, Select as DefaultSelect } from 'antd'; import getLocalStorageKey from 'api/browser/localstorage/get'; import setLocalStorageKey from 'api/browser/localstorage/set'; @@ -14,10 +15,11 @@ import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import { GlobalReducer } from 'types/reducer/globalTime'; +import AutoRefresh from '../AutoRefresh'; import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal'; import { getDefaultOption, getOptions, Time } from './config'; import RefreshText from './Refresh'; -import { Container, Form, FormItem } from './styles'; +import { Form, FormItem } from './styles'; const { Option } = DefaultSelect; @@ -240,6 +242,8 @@ function DateTimeSelection({ setStartTime(dayjs(preStartTime)); setEndTime(dayjs(preEndTime)); + setRefreshButtonHidden(updatedTime === 'custom'); + updateTimeInterval(updatedTime, [preStartTime, preEndTime]); }, [ location.pathname, @@ -253,7 +257,7 @@ function DateTimeSelection({ ]); return ( - + <>
- {!checkRouteExists(history.location.pathname) && ( + {!isRouteToSkip && ( diff --git a/frontend/src/store/actions/global.ts b/frontend/src/store/actions/global.ts index c2b88919da..0e7b2e172f 100644 --- a/frontend/src/store/actions/global.ts +++ b/frontend/src/store/actions/global.ts @@ -2,6 +2,7 @@ import { Time } from 'container/TopNav/DateTimeSelection/config'; import GetMinMax from 'lib/getMinMax'; import { Dispatch } from 'redux'; import AppActions from 'types/actions'; +import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime'; export const UpdateTimeInterval = ( interval: Time, @@ -11,7 +12,7 @@ export const UpdateTimeInterval = ( const { maxTime, minTime } = GetMinMax(interval, dateTimeRange); dispatch({ - type: 'UPDATE_TIME_INTERVAL', + type: UPDATE_TIME_INTERVAL, payload: { maxTime, minTime, From 0480197914c3329af48276e08f2d5ec5bb9253c8 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Sat, 12 Nov 2022 11:37:52 +0530 Subject: [PATCH 08/21] fix Logs contains issue (#1708) * chore: logs is updated * chore: contains is updated --- frontend/src/container/Logs/index.tsx | 15 +++----- .../QueryBuilder/QueryBuilder.tsx | 25 ++++--------- .../SearchFields/QueryBuilder/utils.ts | 37 +++++++++++++++++++ .../LogsSearchFilter/SearchFields/utils.ts | 9 ++++- frontend/src/container/LogsTable/index.tsx | 8 ++-- 5 files changed, 61 insertions(+), 33 deletions(-) create mode 100644 frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/utils.ts diff --git a/frontend/src/container/Logs/index.tsx b/frontend/src/container/Logs/index.tsx index b2cd1eb3a0..9139d7c9e2 100644 --- a/frontend/src/container/Logs/index.tsx +++ b/frontend/src/container/Logs/index.tsx @@ -6,24 +6,17 @@ import LogsAggregate from 'container/LogsAggregate'; import LogsFilters from 'container/LogsFilters'; import SearchFilter from 'container/LogsSearchFilter'; import LogsTable from 'container/LogsTable'; -import React, { memo, useEffect, useMemo } from 'react'; +import useUrlQuery from 'hooks/useUrlQuery'; +import React, { memo, useEffect } from 'react'; import { connect, useDispatch } from 'react-redux'; -import { useLocation } from 'react-router-dom'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import { GetLogsFields } from 'store/actions/logs/getFields'; import AppActions from 'types/actions'; import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs'; -interface LogsProps { - getLogsFields: VoidFunction; -} function Logs({ getLogsFields }: LogsProps): JSX.Element { - const { search } = useLocation(); - - const urlQuery = useMemo(() => { - return new URLSearchParams(search); - }, [search]); + const urlQuery = useUrlQuery(); const dispatch = useDispatch(); @@ -58,6 +51,8 @@ function Logs({ getLogsFields }: LogsProps): JSX.Element { ); } +type LogsProps = DispatchProps; + interface DispatchProps { getLogsFields: () => (dispatch: Dispatch) => void; } diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx index 1014fa7945..0723d2378e 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx @@ -22,10 +22,11 @@ import { v4 } from 'uuid'; import FieldKey from '../FieldKey'; import { QueryConditionContainer, QueryFieldContainer } from '../styles'; import { createParsedQueryStructure } from '../utils'; +import { hashCode, parseQuery } from './utils'; const { Option } = Select; interface QueryFieldProps { - query: { value: string | string[]; type: string }[]; + query: Query; queryIndex: number; onUpdate: (query: unknown, queryIndex: number) => void; onDelete: (queryIndex: number) => void; @@ -49,12 +50,12 @@ function QueryField({ } return ''; }; + const fieldType = useMemo(() => getFieldType(query[0].value as string), [ query, ]); const handleChange = (qIdx: number, value: string): void => { query[qIdx].value = value || ''; - if (qIdx === 1) { if (Object.values(QueryOperatorsMultiVal).includes(value)) { if (!Array.isArray(query[2].value)) { @@ -166,17 +167,8 @@ function QueryConditionField({ ); } -const hashCode = (s: string): string => { - if (!s) { - return '0'; - } - return `${Math.abs( - s.split('').reduce((a, b) => { - a = (a << 5) - a + b.charCodeAt(0); - return a & a; - }, 0), - )}`; -}; + +export type Query = { value: string | string[]; type: string }[]; function QueryBuilder({ updateParsedQuery, @@ -201,12 +193,9 @@ function QueryBuilder({ } }, [parsedQuery]); - const handleUpdate = ( - query: { value: string | string[]; type: string }[], - queryIndex: number, - ): void => { + const handleUpdate = (query: Query, queryIndex: number): void => { const updatedParsedQuery = generatedQueryStructure; - updatedParsedQuery[queryIndex] = query as never; + updatedParsedQuery[queryIndex] = parseQuery(query) as never; const flatParsedQuery = flatten(updatedParsedQuery).filter((q) => q.value); keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery)); diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/utils.ts b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/utils.ts new file mode 100644 index 0000000000..2641d8af35 --- /dev/null +++ b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/utils.ts @@ -0,0 +1,37 @@ +import _set from 'lodash-es/set'; + +import { Query } from './QueryBuilder'; + +export const parseQuery = (queries: Query): Query => { + if (Array.isArray(queries)) { + const isContainsPresent = queries.find((e) => e.value === 'CONTAINS'); + if (isContainsPresent) { + // find the index of VALUE to update + const valueIndex = queries.findIndex((e) => e.type === 'QUERY_VALUE'); + if (valueIndex > -1) { + // update the value to wrap with "" + _set( + queries, + [valueIndex, 'value'], + `'${queries[valueIndex].value || ''}'`, + ); + } + return queries; + } + } + return queries; +}; + +export const hashCode = (s: string): string => { + if (!s) { + return '0'; + } + return `${Math.abs( + s.split('').reduce((a, b) => { + // eslint-disable-next-line no-bitwise, no-param-reassign + a = (a << 5) - a + b.charCodeAt(0); + // eslint-disable-next-line no-bitwise + return a & a; + }, 0), + )}`; +}; diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/utils.ts b/frontend/src/container/LogsSearchFilter/SearchFields/utils.ts index 059392f36d..ae091dc061 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/utils.ts +++ b/frontend/src/container/LogsSearchFilter/SearchFields/utils.ts @@ -2,9 +2,14 @@ // @ts-ignore // @ts-nocheck -import { QueryTypes } from 'lib/logql/tokens'; +import { QueryTypes, QueryOperatorsSingleVal } from 'lib/logql/tokens'; -export const queryKOVPair = () => [ +export interface QueryFields { + type: keyof typeof QueryTypes; + value: string; +} + +export const queryKOVPair = (): QueryFields[] => [ { type: QueryTypes.QUERY_KEY, value: null, diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index 974e77f7fe..7997fac91f 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -15,9 +15,6 @@ import { ILogsReducer } from 'types/reducer/logs'; import { Container, Heading } from './styles'; -interface LogsTableProps { - getLogs: (props: Parameters[0]) => ReturnType; -} function LogsTable({ getLogs }: LogsTableProps): JSX.Element { const { searchFilter: { queryString }, @@ -51,6 +48,7 @@ function LogsTable({ getLogs }: LogsTableProps): JSX.Element { if (isLoading) { return ; } + return ( @@ -86,4 +84,8 @@ const mapDispatchToProps = ( getLogs: bindActionCreators(getLogs, dispatch), }); +interface LogsTableProps { + getLogs: (props: Parameters[0]) => ReturnType; +} + export default connect(null, mapDispatchToProps)(memo(LogsTable)); From 73706d872fbb1ec0803d7d6097d47f3badd16184 Mon Sep 17 00:00:00 2001 From: Ankit Nayan Date: Sat, 12 Nov 2022 17:19:34 +0530 Subject: [PATCH 09/21] Update telemetry.go --- pkg/query-service/telemetry/telemetry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/query-service/telemetry/telemetry.go b/pkg/query-service/telemetry/telemetry.go index 793c02b8ab..43f1652093 100644 --- a/pkg/query-service/telemetry/telemetry.go +++ b/pkg/query-service/telemetry/telemetry.go @@ -44,7 +44,7 @@ const HEART_BEAT_DURATION = 6 * time.Hour // const HEART_BEAT_DURATION = 10 * time.Second const RATE_LIMIT_CHECK_DURATION = 1 * time.Minute -const RATE_LIMIT_VALUE = 60 +const RATE_LIMIT_VALUE = 10 // const RATE_LIMIT_CHECK_DURATION = 20 * time.Second // const RATE_LIMIT_VALUE = 5 From a50d7f227c92deac51ce29f6291df878670022f8 Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Mon, 14 Nov 2022 14:29:13 +0530 Subject: [PATCH 10/21] Feat: dynamic tooltip (#1705) * feat: integrate config service with query service * feat: add tooltip checkpoint * feat: add support for dark and light mode icons Co-authored-by: Palash Gupta --- .../api/dynamicConfigs/getDynamicConfigs.ts | 24 ++++++ frontend/src/container/AppLayout/index.tsx | 32 ++++++++ .../ConfigDropdown/Config/ErrorLink.tsx | 33 ++++++++ .../container/ConfigDropdown/Config/Link.tsx | 23 ++++++ .../container/ConfigDropdown/Config/index.tsx | 51 +++++++++++++ .../src/container/ConfigDropdown/index.tsx | 67 +++++++++++++++++ frontend/src/container/Header/index.tsx | 60 ++++++--------- frontend/src/store/reducers/app.ts | 9 +++ frontend/src/types/actions/app.ts | 10 ++- .../api/dynamicConfigs/getDynamicConfigs.ts | 14 ++++ frontend/src/types/reducer/app.ts | 2 + pkg/query-service/app/http_handler.go | 12 +++ pkg/query-service/constants/constants.go | 2 + .../integrations/signozio/dynamic_config.go | 75 +++++++++++++++++++ .../integrations/signozio/response.go | 54 +++++++++++++ 15 files changed, 429 insertions(+), 39 deletions(-) create mode 100644 frontend/src/api/dynamicConfigs/getDynamicConfigs.ts create mode 100644 frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx create mode 100644 frontend/src/container/ConfigDropdown/Config/Link.tsx create mode 100644 frontend/src/container/ConfigDropdown/Config/index.tsx create mode 100644 frontend/src/container/ConfigDropdown/index.tsx create mode 100644 frontend/src/types/api/dynamicConfigs/getDynamicConfigs.ts create mode 100644 pkg/query-service/integrations/signozio/dynamic_config.go create mode 100644 pkg/query-service/integrations/signozio/response.go diff --git a/frontend/src/api/dynamicConfigs/getDynamicConfigs.ts b/frontend/src/api/dynamicConfigs/getDynamicConfigs.ts new file mode 100644 index 0000000000..149c113119 --- /dev/null +++ b/frontend/src/api/dynamicConfigs/getDynamicConfigs.ts @@ -0,0 +1,24 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps } from 'types/api/dynamicConfigs/getDynamicConfigs'; + +const getDynamicConfigs = async (): Promise< + SuccessResponse | ErrorResponse +> => { + try { + const response = await axios.get(`/configs`); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getDynamicConfigs; diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 9d3dbb88f7..3ff1cfe6c4 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -1,4 +1,5 @@ import { notification } from 'antd'; +import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs'; import getFeaturesFlags from 'api/features/getFeatureFlags'; import getUserLatestVersion from 'api/user/getLatestVersion'; import getUserVersion from 'api/user/getVersion'; @@ -14,6 +15,7 @@ import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import { + UPDATE_CONFIGS, UPDATE_CURRENT_ERROR, UPDATE_CURRENT_VERSION, UPDATE_FEATURE_FLAGS, @@ -33,6 +35,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { getUserVersionResponse, getUserLatestVersionResponse, getFeaturesResponse, + getDynamicConfigsResponse, ] = useQueries([ { queryFn: getUserVersion, @@ -48,6 +51,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element { queryFn: getFeaturesFlags, queryKey: 'getFeatureFlags', }, + { + queryFn: getDynamicConfigs, + queryKey: 'getDynamicConfigs', + }, ]); useEffect(() => { @@ -65,11 +72,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element { if (getFeaturesResponse.status === 'idle') { getFeaturesResponse.refetch(); } + if (getDynamicConfigsResponse.status === 'idle') { + getDynamicConfigsResponse.refetch(); + } }, [ getFeaturesResponse, getUserLatestVersionResponse, getUserVersionResponse, isLoggedIn, + getDynamicConfigsResponse, ]); const { children } = props; @@ -78,6 +89,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const latestCurrentCounter = useRef(0); const latestVersionCounter = useRef(0); + const latestConfigCounter = useRef(0); useEffect(() => { if ( @@ -170,6 +182,23 @@ function AppLayout(props: AppLayoutProps): JSX.Element { }, }); } + + if ( + getDynamicConfigsResponse.isFetched && + getDynamicConfigsResponse.isSuccess && + getDynamicConfigsResponse.data && + getDynamicConfigsResponse.data.payload && + latestConfigCounter.current === 0 + ) { + latestConfigCounter.current = 1; + + dispatch({ + type: UPDATE_CONFIGS, + payload: { + configs: getDynamicConfigsResponse.data.payload, + }, + }); + } }, [ dispatch, isLoggedIn, @@ -187,6 +216,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element { getFeaturesResponse.isFetched, getFeaturesResponse.isSuccess, getFeaturesResponse.data, + getDynamicConfigsResponse.data, + getDynamicConfigsResponse.isFetched, + getDynamicConfigsResponse.isSuccess, ]); const isToDisplayLayout = isLoggedIn; diff --git a/frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx b/frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx new file mode 100644 index 0000000000..84ac44e60e --- /dev/null +++ b/frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx @@ -0,0 +1,33 @@ +import React, { PureComponent } from 'react'; + +interface State { + hasError: boolean; +} + +interface Props { + children: JSX.Element; +} + +class ErrorLink extends PureComponent { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(): State { + return { hasError: true }; + } + + render(): JSX.Element { + const { children } = this.props; + const { hasError } = this.state; + + if (hasError) { + return
; + } + + return children; + } +} + +export default ErrorLink; diff --git a/frontend/src/container/ConfigDropdown/Config/Link.tsx b/frontend/src/container/ConfigDropdown/Config/Link.tsx new file mode 100644 index 0000000000..2cc39b7779 --- /dev/null +++ b/frontend/src/container/ConfigDropdown/Config/Link.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +function LinkContainer({ children, href }: LinkContainerProps): JSX.Element { + const isInternalLink = href.startsWith('/'); + + if (isInternalLink) { + return {children}; + } + + return ( + + {children} + + ); +} + +interface LinkContainerProps { + children: React.ReactNode; + href: string; +} + +export default LinkContainer; diff --git a/frontend/src/container/ConfigDropdown/Config/index.tsx b/frontend/src/container/ConfigDropdown/Config/index.tsx new file mode 100644 index 0000000000..956ec5aa00 --- /dev/null +++ b/frontend/src/container/ConfigDropdown/Config/index.tsx @@ -0,0 +1,51 @@ +import { Menu, Space } from 'antd'; +import Spinner from 'components/Spinner'; +import React, { Suspense, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs'; +import AppReducer from 'types/reducer/app'; + +import ErrorLink from './ErrorLink'; +import LinkContainer from './Link'; + +function HelpToolTip({ config }: HelpToolTipProps): JSX.Element { + const sortedConfig = useMemo( + () => config.components.sort((a, b) => a.position - b.position), + [config.components], + ); + + const { isDarkMode } = useSelector((state) => state.app); + + return ( + + {sortedConfig.map((item) => { + const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`; + + const Component = React.lazy( + () => import(`@ant-design/icons/es/icons/${iconName}.js`), + ); + return ( + + }> + + + + + {item.text} + + + + + + ); + })} + + ); +} + +interface HelpToolTipProps { + config: ConfigProps; +} + +export default HelpToolTip; diff --git a/frontend/src/container/ConfigDropdown/index.tsx b/frontend/src/container/ConfigDropdown/index.tsx new file mode 100644 index 0000000000..65993fc629 --- /dev/null +++ b/frontend/src/container/ConfigDropdown/index.tsx @@ -0,0 +1,67 @@ +import { + CaretDownFilled, + CaretUpFilled, + QuestionCircleFilled, + QuestionCircleOutlined, +} from '@ant-design/icons'; +import { Dropdown, Menu, Space } from 'antd'; +import React, { useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import AppReducer from 'types/reducer/app'; + +import HelpToolTip from './Config'; + +function DynamicConfigDropdown({ + frontendId, +}: DynamicConfigDropdownProps): JSX.Element { + const { configs, isDarkMode } = useSelector( + (state) => state.app, + ); + const [isHelpDropDownOpen, setIsHelpDropDownOpen] = useState(false); + + const config = useMemo( + () => + Object.values(configs).find( + (config) => config.frontendPositionId === frontendId, + ), + [frontendId, configs], + ); + + const onToggleHandler = (): void => { + setIsHelpDropDownOpen(!isHelpDropDownOpen); + }; + + if (!config) { + return
; + } + + const Icon = isDarkMode ? QuestionCircleOutlined : QuestionCircleFilled; + const DropDownIcon = isHelpDropDownOpen ? CaretUpFilled : CaretDownFilled; + + return ( + + + + } + visible={isHelpDropDownOpen} + > + + + + + + ); +} + +interface DynamicConfigDropdownProps { + frontendId: string; +} + +export default DynamicConfigDropdown; diff --git a/frontend/src/container/Header/index.tsx b/frontend/src/container/Header/index.tsx index d6e4f79ae5..4f8bbca048 100644 --- a/frontend/src/container/Header/index.tsx +++ b/frontend/src/container/Header/index.tsx @@ -14,8 +14,9 @@ import { } from 'antd'; import { Logout } from 'api/utils'; import ROUTES from 'constants/routes'; +import Config from 'container/ConfigDropdown'; import setTheme, { AppMode } from 'lib/theme/setTheme'; -import React, { useCallback, useState } from 'react'; +import React, { Dispatch, SetStateAction, useCallback, useState } from 'react'; import { connect, useSelector } from 'react-redux'; import { NavLink } from 'react-router-dom'; import { bindActionCreators } from 'redux'; @@ -34,7 +35,8 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element { const { isDarkMode, user, currentVersion } = useSelector( (state) => state.app, ); - const [isUserDropDownOpen, setIsUserDropDownOpen] = useState(); + + const [isUserDropDownOpen, setIsUserDropDownOpen] = useState(false); const onToggleThemeHandler = useCallback(() => { const preMode: AppMode = isDarkMode ? 'lightMode' : 'darkMode'; @@ -57,22 +59,21 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element { }; }, [toggleDarkMode, isDarkMode]); - const onArrowClickHandler: VoidFunction = () => { - setIsUserDropDownOpen((state) => !state); - }; - - const onClickLogoutHandler = (): void => { - Logout(); - }; + const onToggleHandler = useCallback( + (functionToExecute: Dispatch>) => (): void => { + functionToExecute((state) => !state); + }, + [], + ); const menu = ( - + - + @@ -80,11 +81,11 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element { tabIndex={0} onKeyDown={(e): void => { if (e.key === 'Enter' || e.key === 'Space') { - onClickLogoutHandler(); + Logout(); } }} role="button" - onClick={onClickLogoutHandler} + onClick={Logout} > Logout
@@ -94,23 +95,18 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element { ); return ( - + - + SigNoz SigNoz - + + + + {user?.name[0]} - {!isUserDropDownOpen ? ( - - ) : ( - - )} + {!isUserDropDownOpen ? : } diff --git a/frontend/src/store/reducers/app.ts b/frontend/src/store/reducers/app.ts index 3e18a4c957..6fbc48049d 100644 --- a/frontend/src/store/reducers/app.ts +++ b/frontend/src/store/reducers/app.ts @@ -8,6 +8,7 @@ import { LOGGED_IN, SIDEBAR_COLLAPSE, SWITCH_DARK_MODE, + UPDATE_CONFIGS, UPDATE_CURRENT_ERROR, UPDATE_CURRENT_VERSION, UPDATE_FEATURE_FLAGS, @@ -56,6 +57,7 @@ const InitialValue: InitialValueTypes = { isUserFetchingError: false, org: null, role: null, + configs: {}, }; const appReducer = ( @@ -210,6 +212,13 @@ const appReducer = ( }; } + case UPDATE_CONFIGS: { + return { + ...state, + configs: action.payload.configs, + }; + } + default: return state; } diff --git a/frontend/src/types/actions/app.ts b/frontend/src/types/actions/app.ts index 65264f5ca3..a2a4b90f39 100644 --- a/frontend/src/types/actions/app.ts +++ b/frontend/src/types/actions/app.ts @@ -23,6 +23,7 @@ export const UPDATE_USER = 'UPDATE_USER'; export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME'; export const UPDATE_ORG = 'UPDATE_ORG'; export const UPDATE_FEATURE_FLAGS = 'UPDATE_FEATURE_FLAGS'; +export const UPDATE_CONFIGS = 'UPDATE_CONFIGS'; export interface SwitchDarkMode { type: typeof SWITCH_DARK_MODE; @@ -115,6 +116,12 @@ export interface UpdateOrg { org: AppReducer['org']; }; } +export interface UpdateConfigs { + type: typeof UPDATE_CONFIGS; + payload: { + configs: AppReducer['configs']; + }; +} export type AppAction = | SwitchDarkMode @@ -129,4 +136,5 @@ export type AppAction = | UpdateUser | UpdateOrgName | UpdateOrg - | UpdateFeatureFlags; + | UpdateFeatureFlags + | UpdateConfigs; diff --git a/frontend/src/types/api/dynamicConfigs/getDynamicConfigs.ts b/frontend/src/types/api/dynamicConfigs/getDynamicConfigs.ts new file mode 100644 index 0000000000..69e55e008d --- /dev/null +++ b/frontend/src/types/api/dynamicConfigs/getDynamicConfigs.ts @@ -0,0 +1,14 @@ +export interface ConfigProps { + enabled: boolean; + frontendPositionId: string; + components: Array<{ + href: string; + darkIcon: string; + lightIcon: string; + position: 1; + text: string; + }>; +} +export interface PayloadProps { + [key: string]: ConfigProps; +} diff --git a/frontend/src/types/reducer/app.ts b/frontend/src/types/reducer/app.ts index 5c10f31a83..d95fd3a77b 100644 --- a/frontend/src/types/reducer/app.ts +++ b/frontend/src/types/reducer/app.ts @@ -1,3 +1,4 @@ +import { PayloadProps as ConfigPayload } from 'types/api/dynamicConfigs/getDynamicConfigs'; import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags'; import { PayloadProps as OrgPayload } from 'types/api/user/getOrganization'; import { PayloadProps as UserPayload } from 'types/api/user/getUser'; @@ -26,4 +27,5 @@ export default interface AppReducer { role: ROLES | null; org: OrgPayload | null; featureFlags: null | FeatureFlagPayload; + configs: ConfigPayload; } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 93c9eff646..18d2743924 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -27,6 +27,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/dao" am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager" + signozio "go.signoz.io/signoz/pkg/query-service/integrations/signozio" "go.signoz.io/signoz/pkg/query-service/interfaces" "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/rules" @@ -360,6 +361,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) { router.HandleFunc("/api/v1/version", OpenAccess(aH.getVersion)).Methods(http.MethodGet) router.HandleFunc("/api/v1/featureFlags", OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/configs", OpenAccess(aH.getConfigs)).Methods(http.MethodGet) router.HandleFunc("/api/v1/getSpanFilters", ViewAccess(aH.getSpanFilters)).Methods(http.MethodPost) router.HandleFunc("/api/v1/getTagFilters", ViewAccess(aH.getTagFilters)).Methods(http.MethodPost) @@ -1583,6 +1585,16 @@ func (aH *APIHandler) CheckFeature(f string) bool { return err == nil } +func (aH *APIHandler) getConfigs(w http.ResponseWriter, r *http.Request) { + + configs, err := signozio.FetchDynamicConfigs() + if err != nil { + aH.HandleError(w, err, http.StatusInternalServerError) + return + } + aH.Respond(w, configs) +} + // inviteUser is used to invite a user. It is used by an admin api. func (aH *APIHandler) inviteUser(w http.ResponseWriter, r *http.Request) { req, err := parseInviteRequest(r) diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index 46b3d4651f..d376b068e9 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -13,6 +13,8 @@ const ( DebugHttpPort = "0.0.0.0:6060" // Address to serve http (pprof) ) +var ConfigSignozIo = "https://config.signoz.io/api/v1" + var DEFAULT_TELEMETRY_ANONYMOUS = false func IsTelemetryEnabled() bool { diff --git a/pkg/query-service/integrations/signozio/dynamic_config.go b/pkg/query-service/integrations/signozio/dynamic_config.go new file mode 100644 index 0000000000..42827b73d5 --- /dev/null +++ b/pkg/query-service/integrations/signozio/dynamic_config.go @@ -0,0 +1,75 @@ +package signozio + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "time" + + "go.signoz.io/signoz/ee/query-service/model" + "go.signoz.io/signoz/pkg/query-service/constants" +) + +var C *Client + +const ( + POST = "POST" + APPLICATION_JSON = "application/json" +) + +type Client struct { + Prefix string +} + +func New() *Client { + return &Client{ + Prefix: constants.ConfigSignozIo, + } +} + +func init() { + C = New() +} + +// FetchDynamicConfigs fetches configs from config server +func FetchDynamicConfigs() (map[string]Config, *model.ApiError) { + + client := http.Client{Timeout: 5 * time.Second} + req, err := http.NewRequest(http.MethodGet, C.Prefix+"/configs", http.NoBody) + if err != nil { + return DefaultConfig, nil + } + req.SetBasicAuth("admin", "SigNoz@adm1n") + httpResponse, err := client.Do(req) + if err != nil { + return DefaultConfig, nil + } + + defer httpResponse.Body.Close() + + if err != nil { + return DefaultConfig, nil + } + + httpBody, err := ioutil.ReadAll(httpResponse.Body) + if err != nil { + return DefaultConfig, nil + } + + // read api request result + result := ConfigResult{} + err = json.Unmarshal(httpBody, &result) + if err != nil { + return DefaultConfig, nil + } + + switch httpResponse.StatusCode { + case 200, 201: + return result.Data, nil + case 400, 401: + return DefaultConfig, nil + default: + return DefaultConfig, nil + } + +} diff --git a/pkg/query-service/integrations/signozio/response.go b/pkg/query-service/integrations/signozio/response.go new file mode 100644 index 0000000000..8440346ec4 --- /dev/null +++ b/pkg/query-service/integrations/signozio/response.go @@ -0,0 +1,54 @@ +package signozio + +type status string + +type ConfigResult struct { + Status status `json:"status"` + Data map[string]Config `json:"data,omitempty"` + ErrorType string `json:"errorType,omitempty"` + Error string `json:"error,omitempty"` +} + +type Config struct { + Enabled bool `json:"enabled"` + FrontendPositionId string `json:"frontendPositionId"` + Components []ComponentProps `json:"components"` +} + +type ComponentProps struct { + Text string `json:"text"` + Position int `json:"position"` + DarkIcon string `json:"darkIcon"` + LightIcon string `json:"lightIcon"` + Href string `json:"href"` +} + +var DefaultConfig = map[string]Config{ + "helpConfig": { + Enabled: true, + FrontendPositionId: "tooltip", + Components: []ComponentProps{ + { + Text: "How to use SigNoz in production", + Position: 1, + LightIcon: "RiseOutlined", + DarkIcon: "RiseOutlined", + Href: "https://signoz.io/docs/production-readiness", + }, + { + Text: "Create an issue in GitHub", + Position: 2, + LightIcon: "GithubFilled", + DarkIcon: "GithubOutlined", + Href: "https://github.com/SigNoz/signoz/issues/new/choose", + }, + { + Text: "Read the docs", + Position: 3, + LightIcon: "FileTextFilled", + DarkIcon: "FileTextOutlined", + Href: "https://signoz.io/docs", + }, + }, + }, +} From 2e124da36664919b5fdfaaceb56ccb2780327681 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Mon, 14 Nov 2022 22:32:19 +0530 Subject: [PATCH 11/21] feat: refresh interval is added (#1712) * feat: refresh interval is added --- .../container/TopNav/AutoRefresh/config.ts | 2 +- .../container/TopNav/AutoRefresh/index.tsx | 106 ++++++++++++++---- .../container/TopNav/AutoRefresh/styles.ts | 12 +- .../TopNav/DateTimeSelection/index.tsx | 46 ++++---- .../TopNav/DateTimeSelection/styles.ts | 5 + 5 files changed, 120 insertions(+), 51 deletions(-) diff --git a/frontend/src/container/TopNav/AutoRefresh/config.ts b/frontend/src/container/TopNav/AutoRefresh/config.ts index 240b7d1f25..cefd0c8bf1 100644 --- a/frontend/src/container/TopNav/AutoRefresh/config.ts +++ b/frontend/src/container/TopNav/AutoRefresh/config.ts @@ -1,6 +1,6 @@ export const options: IOptions[] = [ { - label: '', + label: 'off', key: 'off', value: 0, }, diff --git a/frontend/src/container/TopNav/AutoRefresh/index.tsx b/frontend/src/container/TopNav/AutoRefresh/index.tsx index f81875bcc3..bc52e2cf86 100644 --- a/frontend/src/container/TopNav/AutoRefresh/index.tsx +++ b/frontend/src/container/TopNav/AutoRefresh/index.tsx @@ -1,9 +1,20 @@ -import { Select } from 'antd'; +import { CaretDownFilled } from '@ant-design/icons'; +import { + Checkbox, + Divider, + Popover, + Radio, + RadioChangeEvent, + Space, + Typography, +} from 'antd'; +import { CheckboxChangeEvent } from 'antd/lib/checkbox'; import get from 'api/browser/localstorage/get'; import set from 'api/browser/localstorage/set'; import { DASHBOARD_TIME_IN_DURATION } from 'constants/app'; import dayjs from 'dayjs'; import useUrlQuery from 'hooks/useUrlQuery'; +import _omit from 'lodash-es/omit'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; @@ -15,7 +26,7 @@ import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime'; import { options } from './config'; -import { SelectContainer } from './styles'; +import { ButtonContainer, Container } from './styles'; function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { const { minTime: initialMinTime, selectedTime } = useSelector< @@ -24,8 +35,6 @@ function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { >((state) => state.globalTime); const { pathname } = useLocation(); - const params = useUrlQuery(); - const localStorageData = JSON.parse(get(DASHBOARD_TIME_IN_DURATION) || '{}'); const localStorageValue = useMemo(() => localStorageData[pathname], [ @@ -33,6 +42,16 @@ function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { localStorageData, ]); + const [isAutoRefreshEnabled, setIsAutoRefreshfreshEnabled] = useState( + Boolean(localStorageValue), + ); + + useEffect(() => { + setIsAutoRefreshfreshEnabled(Boolean(localStorageValue)); + }, [localStorageValue]); + + const params = useUrlQuery(); + const dispatch = useDispatch>(); const [selectedOption, setSelectedOption] = useState( localStorageValue || options[0].key, @@ -50,7 +69,7 @@ function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { useInterval(() => { const selectedValue = getOption?.value; - if (disabled) { + if (disabled || !isAutoRefreshEnabled) { return; } @@ -69,31 +88,70 @@ function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { }, getOption?.value || 0); const onChangeHandler = useCallback( - (value: unknown) => { - if (typeof value === 'string') { - setSelectedOption(value); - params.set(DASHBOARD_TIME_IN_DURATION, value); - set( - DASHBOARD_TIME_IN_DURATION, - JSON.stringify({ ...localStorageData, [pathname]: value }), - ); - } + (event: RadioChangeEvent) => { + const selectedValue = event.target.value; + setSelectedOption(selectedValue); + params.set(DASHBOARD_TIME_IN_DURATION, selectedValue); + set( + DASHBOARD_TIME_IN_DURATION, + JSON.stringify({ ...localStorageData, [pathname]: selectedValue }), + ); + setIsAutoRefreshfreshEnabled(true); }, [params, pathname, localStorageData], ); + const onChangeAutoRefreshHandler = useCallback( + (event: CheckboxChangeEvent) => { + const { checked } = event.target; + if (!checked) { + // remove the path from localstorage + set( + DASHBOARD_TIME_IN_DURATION, + JSON.stringify(_omit(localStorageData, pathname)), + ); + } + setIsAutoRefreshfreshEnabled(checked); + }, + [localStorageData, pathname], + ); + return ( - + + Auto Refresh + + + + + Refresh Interval + + + + {options + .filter((e) => e.label !== 'off') + .map((option) => ( + + {option.label} + + ))} + + + + } > - {options.map((option) => ( - - {option.label} - - ))} - + + + + ); } diff --git a/frontend/src/container/TopNav/AutoRefresh/styles.ts b/frontend/src/container/TopNav/AutoRefresh/styles.ts index 6e8380703f..9672a346e7 100644 --- a/frontend/src/container/TopNav/AutoRefresh/styles.ts +++ b/frontend/src/container/TopNav/AutoRefresh/styles.ts @@ -1,9 +1,13 @@ -import { Select } from 'antd'; +import { Button } from 'antd'; import styled from 'styled-components'; -export const SelectContainer = styled(Select)` +export const Container = styled.div` + min-width: 8rem; +`; + +export const ButtonContainer = styled(Button)` &&& { - width: 100%; - min-width: 4rem; + padding-left: 0.5rem; + padding-right: 0.5rem; } `; diff --git a/frontend/src/container/TopNav/DateTimeSelection/index.tsx b/frontend/src/container/TopNav/DateTimeSelection/index.tsx index 9d62c36a8d..18e8dce4c8 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/index.tsx +++ b/frontend/src/container/TopNav/DateTimeSelection/index.tsx @@ -19,7 +19,7 @@ import AutoRefresh from '../AutoRefresh'; import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal'; import { getDefaultOption, getOptions, Time } from './config'; import RefreshText from './Refresh'; -import { Form, FormItem } from './styles'; +import { Form, FormContainer, FormItem } from './styles'; const { Option } = DefaultSelect; @@ -263,29 +263,31 @@ function DateTimeSelection({ layout="inline" initialValues={{ interval: selectedTime }} > - onSelectHandler(value as Time)} - value={getInputLabel(startTime, endTime, selectedTime)} - data-testid="dropDown" - > - {options.map(({ value, label }) => ( - - ))} - + + onSelectHandler(value as Time)} + value={getInputLabel(startTime, endTime, selectedTime)} + data-testid="dropDown" + > + {options.map(({ value, label }) => ( + + ))} + -