diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index d8b2f8b6c3..83b0192d55 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -44,6 +44,7 @@ import ( // "github.com/pingcap/tidb-dashboard/pkg/apiserver/__APP_NAME__" // NOTE: Don't remove above comment line, it is a placeholder for code generator. + resourcemanager "github.com/pingcap/tidb-dashboard/pkg/apiserver/resource_manager" "github.com/pingcap/tidb-dashboard/pkg/apiserver/slowquery" "github.com/pingcap/tidb-dashboard/pkg/apiserver/statement" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user" @@ -139,6 +140,7 @@ var Modules = fx.Options( topsql.Module, visualplan.Module, deadlock.Module, + resourcemanager.Module, ) func (s *Service) Start(ctx context.Context) error { diff --git a/pkg/apiserver/resource_manager/module.go b/pkg/apiserver/resource_manager/module.go new file mode 100644 index 0000000000..53d29c2abc --- /dev/null +++ b/pkg/apiserver/resource_manager/module.go @@ -0,0 +1,10 @@ +// Copyright 2023 PingCAP, Inc. Licensed under Apache-2.0. + +package resourcemanager + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide(newService), + fx.Invoke(registerRouter), +) diff --git a/pkg/apiserver/resource_manager/service.go b/pkg/apiserver/resource_manager/service.go new file mode 100644 index 0000000000..8d0e78a4a4 --- /dev/null +++ b/pkg/apiserver/resource_manager/service.go @@ -0,0 +1,153 @@ +// Copyright 2023 PingCAP, Inc. Licensed under Apache-2.0. + +package resourcemanager + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/fx" + + "github.com/pingcap/tidb-dashboard/pkg/apiserver/user" + "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" + "github.com/pingcap/tidb-dashboard/pkg/tidb" + "github.com/pingcap/tidb-dashboard/util/featureflag" + "github.com/pingcap/tidb-dashboard/util/rest" +) + +type ServiceParams struct { + fx.In + TiDBClient *tidb.Client +} + +type Service struct { + FeatureResourceManager *featureflag.FeatureFlag + + params ServiceParams +} + +func newService(p ServiceParams, ff *featureflag.Registry) *Service { + return &Service{params: p, FeatureResourceManager: ff.Register("resource_manager", ">= 7.1.0")} +} + +func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { + endpoint := r.Group("/resource_manager") + endpoint.Use( + auth.MWAuthRequired(), + s.FeatureResourceManager.VersionGuard(), + utils.MWConnectTiDB(s.params.TiDBClient), + ) + { + endpoint.GET("/config", s.GetConfig) + endpoint.GET("/information", s.GetInformation) + endpoint.GET("/calibrate/hardware", s.GetCalibrateByHardware) + endpoint.GET("/calibrate/actual", s.GetCalibrateByActual) + } +} + +type GetConfigResponse struct { + Enable bool `json:"enable" gorm:"column:tidb_enable_resource_control"` +} + +// @Summary Get Resource Control enable config +// @Router /resource_manager/config [get] +// @Security JwtAuth +// @Success 200 {object} GetConfigResponse +// @Failure 401 {object} rest.ErrorResponse +// @Failure 500 {object} rest.ErrorResponse +func (s *Service) GetConfig(c *gin.Context) { + db := utils.GetTiDBConnection(c) + resp := &GetConfigResponse{} + err := db.Raw("SELECT @@GLOBAL.tidb_enable_resource_control as tidb_enable_resource_control").Find(resp).Error + if err != nil { + rest.Error(c, err) + return + } + c.JSON(http.StatusOK, resp) +} + +type ResourceInfoRowDef struct { + Name string `json:"name" gorm:"column:NAME"` + RuPerSec string `json:"ru_per_sec" gorm:"column:RU_PER_SEC"` + Priority string `json:"priority" gorm:"column:PRIORITY"` + Burstable string `json:"burstable" gorm:"column:BURSTABLE"` +} + +// @Summary Get Information of Resource Groups +// @Router /resource_manager/information [get] +// @Security JwtAuth +// @Success 200 {object} []ResourceInfoRowDef +// @Failure 401 {object} rest.ErrorResponse +// @Failure 500 {object} rest.ErrorResponse +func (s *Service) GetInformation(c *gin.Context) { + db := utils.GetTiDBConnection(c) + var cfg []ResourceInfoRowDef + err := db.Table("INFORMATION_SCHEMA.RESOURCE_GROUPS").Scan(&cfg).Error + if err != nil { + rest.Error(c, err) + return + } + c.JSON(http.StatusOK, cfg) +} + +type CalibrateResponse struct { + EstimatedCapacity int `json:"estimated_capacity" gorm:"column:QUOTA"` +} + +// @Summary Get calibrate of Resource Groups by hardware deployment +// @Router /resource_manager/calibrate/hardware [get] +// @Param workload query string true "workload" default("tpcc") +// @Security JwtAuth +// @Success 200 {object} CalibrateResponse +// @Failure 401 {object} rest.ErrorResponse +// @Failure 500 {object} rest.ErrorResponse +func (s *Service) GetCalibrateByHardware(c *gin.Context) { + w := c.Query("workload") + if w == "" { + rest.Error(c, rest.ErrBadRequest.New("workload cannot be empty")) + return + } + + db := utils.GetTiDBConnection(c) + resp := &CalibrateResponse{} + err := db.Raw(fmt.Sprintf("calibrate resource workload %s", w)).Scan(resp).Error + if err != nil { + rest.Error(c, err) + return + } + c.JSON(http.StatusOK, resp) +} + +type GetCalibrateByActualRequest struct { + StartTime int64 `json:"start_time" form:"start_time"` + EndTime int64 `json:"end_time" form:"end_time"` +} + +// @Summary Get calibrate of Resource Groups by actual workload +// @Router /resource_manager/calibrate/actual [get] +// @Param q query GetCalibrateByActualRequest true "Query" +// @Security JwtAuth +// @Success 200 {object} CalibrateResponse +// @Failure 401 {object} rest.ErrorResponse +// @Failure 500 {object} rest.ErrorResponse +func (s *Service) GetCalibrateByActual(c *gin.Context) { + var req GetCalibrateByActualRequest + if err := c.ShouldBindQuery(&req); err != nil { + rest.Error(c, rest.ErrBadRequest.NewWithNoMessage()) + return + } + + startTime := time.Unix(req.StartTime, 0).Format("2006-01-02 15:04:05") + endTime := time.Unix(req.EndTime, 0).Format("2006-01-02 15:04:05") + + db := utils.GetTiDBConnection(c) + resp := &CalibrateResponse{} + err := db.Raw(fmt.Sprintf("calibrate resource start_time '%s' end_time '%s'", startTime, endTime)).Scan(resp).Error + if err != nil { + rest.Error(c, err) + return + } + c.JSON(http.StatusOK, resp) +} diff --git a/pkg/apiserver/slowquery/service.go b/pkg/apiserver/slowquery/service.go index d9e1d7429e..d7858fa706 100644 --- a/pkg/apiserver/slowquery/service.go +++ b/pkg/apiserver/slowquery/service.go @@ -101,11 +101,13 @@ func (s *Service) getDetails(c *gin.Context) { } // generate binary plan - result.BinaryPlan, err = utils.GenerateBinaryPlanJSON(result.BinaryPlan) - if err != nil { - rest.Error(c, err) - return - } + // + // Due to a kernel bug, the binary plan may fail to parse due to + // encoding issues. Additionally, since the binary plan field is + // not a required field, we can mask this error. + // + // See: https://github.com/pingcap/tidb-dashboard/issues/1515 + result.BinaryPlan, _ = utils.GenerateBinaryPlanJSON(result.BinaryPlan) c.JSON(http.StatusOK, *result) } diff --git a/pkg/utils/topology/store.go b/pkg/utils/topology/store.go index 7bd0054a67..a78eb77f6e 100644 --- a/pkg/utils/topology/store.go +++ b/pkg/utils/topology/store.go @@ -27,7 +27,7 @@ func FetchStoreTopology(pdClient *pd.Client) ([]StoreInfo, []StoreInfo, error) { for _, store := range stores { isTiFlash := false for _, label := range store.Labels { - if label.Key == "engine" && label.Value == "tiflash" { + if label.Key == "engine" && (label.Value == "tiflash" || label.Value == "tiflash_compute") { isTiFlash = true } } diff --git a/release-version b/release-version index ad32012f1d..0789232d28 100644 --- a/release-version +++ b/release-version @@ -1,3 +1,4 @@ # This file specifies the TiDB Dashboard internal version, which will be printed in `--version` # and UI. In release branch, changing this file will result in publishing a new version and tag. -7.0.0 +2023.05.08.1 + diff --git a/ui/packages/tidb-dashboard-client/src/client/api/api/default-api.ts b/ui/packages/tidb-dashboard-client/src/client/api/api/default-api.ts index 7f6e5b340c..374c1c59d8 100644 --- a/ui/packages/tidb-dashboard-client/src/client/api/api/default-api.ts +++ b/ui/packages/tidb-dashboard-client/src/client/api/api/default-api.ts @@ -103,6 +103,12 @@ import { QueryeditorRunRequest } from '../models'; // @ts-ignore import { QueryeditorRunResponse } from '../models'; // @ts-ignore +import { ResourcemanagerCalibrateResponse } from '../models'; +// @ts-ignore +import { ResourcemanagerGetConfigResponse } from '../models'; +// @ts-ignore +import { ResourcemanagerResourceInfoRowDef } from '../models'; +// @ts-ignore import { RestErrorResponse } from '../models'; // @ts-ignore import { SlowqueryGetListRequest } from '../models'; @@ -2415,6 +2421,155 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @summary Get calibrate of Resource Groups by actual workload + * @param {number} [endTime] + * @param {number} [startTime] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + resourceManagerCalibrateActualGet: async (endTime?: number, startTime?: number, options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/resource_manager/calibrate/actual`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication JwtAuth required + await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) + + if (endTime !== undefined) { + localVarQueryParameter['end_time'] = endTime; + } + + if (startTime !== undefined) { + localVarQueryParameter['start_time'] = startTime; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Get calibrate of Resource Groups by hardware deployment + * @param {string} workload workload + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + resourceManagerCalibrateHardwareGet: async (workload: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'workload' is not null or undefined + assertParamExists('resourceManagerCalibrateHardwareGet', 'workload', workload) + const localVarPath = `/resource_manager/calibrate/hardware`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication JwtAuth required + await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) + + if (workload !== undefined) { + localVarQueryParameter['workload'] = workload; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Get Resource Control enable config + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + resourceManagerConfigGet: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/resource_manager/config`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication JwtAuth required + await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Get Information of Resource Groups + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + resourceManagerInformationGet: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/resource_manager/information`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication JwtAuth required + await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Get available field names by slowquery table columns * @summary Get available field names @@ -4473,6 +4628,49 @@ export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.queryEditorRun(request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Get calibrate of Resource Groups by actual workload + * @param {number} [endTime] + * @param {number} [startTime] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async resourceManagerCalibrateActualGet(endTime?: number, startTime?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.resourceManagerCalibrateActualGet(endTime, startTime, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @summary Get calibrate of Resource Groups by hardware deployment + * @param {string} workload workload + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async resourceManagerCalibrateHardwareGet(workload: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.resourceManagerCalibrateHardwareGet(workload, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @summary Get Resource Control enable config + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async resourceManagerConfigGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.resourceManagerConfigGet(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @summary Get Information of Resource Groups + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async resourceManagerInformationGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.resourceManagerInformationGet(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * Get available field names by slowquery table columns * @summary Get available field names @@ -5470,6 +5668,45 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa queryEditorRun(request: QueryeditorRunRequest, options?: any): AxiosPromise { return localVarFp.queryEditorRun(request, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Get calibrate of Resource Groups by actual workload + * @param {number} [endTime] + * @param {number} [startTime] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + resourceManagerCalibrateActualGet(endTime?: number, startTime?: number, options?: any): AxiosPromise { + return localVarFp.resourceManagerCalibrateActualGet(endTime, startTime, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Get calibrate of Resource Groups by hardware deployment + * @param {string} workload workload + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + resourceManagerCalibrateHardwareGet(workload: string, options?: any): AxiosPromise { + return localVarFp.resourceManagerCalibrateHardwareGet(workload, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Get Resource Control enable config + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + resourceManagerConfigGet(options?: any): AxiosPromise { + return localVarFp.resourceManagerConfigGet(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Get Information of Resource Groups + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + resourceManagerInformationGet(options?: any): AxiosPromise> { + return localVarFp.resourceManagerInformationGet(options).then((request) => request(axios, basePath)); + }, /** * Get available field names by slowquery table columns * @summary Get available field names @@ -6444,6 +6681,41 @@ export interface DefaultApiQueryEditorRunRequest { readonly request: QueryeditorRunRequest } +/** + * Request parameters for resourceManagerCalibrateActualGet operation in DefaultApi. + * @export + * @interface DefaultApiResourceManagerCalibrateActualGetRequest + */ +export interface DefaultApiResourceManagerCalibrateActualGetRequest { + /** + * + * @type {number} + * @memberof DefaultApiResourceManagerCalibrateActualGet + */ + readonly endTime?: number + + /** + * + * @type {number} + * @memberof DefaultApiResourceManagerCalibrateActualGet + */ + readonly startTime?: number +} + +/** + * Request parameters for resourceManagerCalibrateHardwareGet operation in DefaultApi. + * @export + * @interface DefaultApiResourceManagerCalibrateHardwareGetRequest + */ +export interface DefaultApiResourceManagerCalibrateHardwareGetRequest { + /** + * workload + * @type {string} + * @memberof DefaultApiResourceManagerCalibrateHardwareGet + */ + readonly workload: string +} + /** * Request parameters for slowQueryDetailGet operation in DefaultApi. * @export @@ -7741,6 +8013,52 @@ export class DefaultApi extends BaseAPI { return DefaultApiFp(this.configuration).queryEditorRun(requestParameters.request, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Get calibrate of Resource Groups by actual workload + * @param {DefaultApiResourceManagerCalibrateActualGetRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public resourceManagerCalibrateActualGet(requestParameters: DefaultApiResourceManagerCalibrateActualGetRequest = {}, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).resourceManagerCalibrateActualGet(requestParameters.endTime, requestParameters.startTime, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Get calibrate of Resource Groups by hardware deployment + * @param {DefaultApiResourceManagerCalibrateHardwareGetRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public resourceManagerCalibrateHardwareGet(requestParameters: DefaultApiResourceManagerCalibrateHardwareGetRequest, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).resourceManagerCalibrateHardwareGet(requestParameters.workload, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Get Resource Control enable config + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public resourceManagerConfigGet(options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).resourceManagerConfigGet(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Get Information of Resource Groups + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public resourceManagerInformationGet(options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).resourceManagerInformationGet(options).then((request) => request(this.axios, this.basePath)); + } + /** * Get available field names by slowquery table columns * @summary Get available field names diff --git a/ui/packages/tidb-dashboard-client/src/client/api/models/index.ts b/ui/packages/tidb-dashboard-client/src/client/api/models/index.ts index fcf95d4056..5532a4ac7a 100644 --- a/ui/packages/tidb-dashboard-client/src/client/api/models/index.ts +++ b/ui/packages/tidb-dashboard-client/src/client/api/models/index.ts @@ -59,6 +59,9 @@ export * from './profiling-task-group-model'; export * from './profiling-task-model'; export * from './queryeditor-run-request'; export * from './queryeditor-run-response'; +export * from './resourcemanager-calibrate-response'; +export * from './resourcemanager-get-config-response'; +export * from './resourcemanager-resource-info-row-def'; export * from './rest-error-response'; export * from './slowquery-get-list-request'; export * from './slowquery-model'; diff --git a/ui/packages/tidb-dashboard-client/src/client/api/models/resourcemanager-calibrate-response.ts b/ui/packages/tidb-dashboard-client/src/client/api/models/resourcemanager-calibrate-response.ts new file mode 100644 index 0000000000..6f1d9dbb68 --- /dev/null +++ b/ui/packages/tidb-dashboard-client/src/client/api/models/resourcemanager-calibrate-response.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Dashboard API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface ResourcemanagerCalibrateResponse + */ +export interface ResourcemanagerCalibrateResponse { + /** + * + * @type {number} + * @memberof ResourcemanagerCalibrateResponse + */ + 'estimated_capacity'?: number; +} + diff --git a/ui/packages/tidb-dashboard-client/src/client/api/models/resourcemanager-get-config-response.ts b/ui/packages/tidb-dashboard-client/src/client/api/models/resourcemanager-get-config-response.ts new file mode 100644 index 0000000000..1a42f85ccb --- /dev/null +++ b/ui/packages/tidb-dashboard-client/src/client/api/models/resourcemanager-get-config-response.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Dashboard API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface ResourcemanagerGetConfigResponse + */ +export interface ResourcemanagerGetConfigResponse { + /** + * + * @type {boolean} + * @memberof ResourcemanagerGetConfigResponse + */ + 'enable'?: boolean; +} + diff --git a/ui/packages/tidb-dashboard-client/src/client/api/models/resourcemanager-resource-info-row-def.ts b/ui/packages/tidb-dashboard-client/src/client/api/models/resourcemanager-resource-info-row-def.ts new file mode 100644 index 0000000000..695824b90c --- /dev/null +++ b/ui/packages/tidb-dashboard-client/src/client/api/models/resourcemanager-resource-info-row-def.ts @@ -0,0 +1,48 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Dashboard API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface ResourcemanagerResourceInfoRowDef + */ +export interface ResourcemanagerResourceInfoRowDef { + /** + * + * @type {string} + * @memberof ResourcemanagerResourceInfoRowDef + */ + 'burstable'?: string; + /** + * + * @type {string} + * @memberof ResourcemanagerResourceInfoRowDef + */ + 'name'?: string; + /** + * + * @type {string} + * @memberof ResourcemanagerResourceInfoRowDef + */ + 'priority'?: string; + /** + * + * @type {string} + * @memberof ResourcemanagerResourceInfoRowDef + */ + 'ru_per_sec'?: string; +} + diff --git a/ui/packages/tidb-dashboard-client/swagger/spec.json b/ui/packages/tidb-dashboard-client/swagger/spec.json index 6349b718c9..91469495bc 100644 --- a/ui/packages/tidb-dashboard-client/swagger/spec.json +++ b/ui/packages/tidb-dashboard-client/swagger/spec.json @@ -2142,6 +2142,151 @@ } } }, + "/resource_manager/calibrate/actual": { + "get": { + "security": [ + { + "JwtAuth": [] + } + ], + "summary": "Get calibrate of Resource Groups by actual workload", + "parameters": [ + { + "type": "integer", + "name": "end_time", + "in": "query" + }, + { + "type": "integer", + "name": "start_time", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/resourcemanager.CalibrateResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/resource_manager/calibrate/hardware": { + "get": { + "security": [ + { + "JwtAuth": [] + } + ], + "summary": "Get calibrate of Resource Groups by hardware deployment", + "parameters": [ + { + "type": "string", + "default": "\"tpcc\"", + "description": "workload", + "name": "workload", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/resourcemanager.CalibrateResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/resource_manager/config": { + "get": { + "security": [ + { + "JwtAuth": [] + } + ], + "summary": "Get Resource Control enable config", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/resourcemanager.GetConfigResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/resource_manager/information": { + "get": { + "security": [ + { + "JwtAuth": [] + } + ], + "summary": "Get Information of Resource Groups", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/resourcemanager.ResourceInfoRowDef" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, "/slow_query/available_fields": { "get": { "security": [ @@ -4653,6 +4798,39 @@ } } }, + "resourcemanager.CalibrateResponse": { + "type": "object", + "properties": { + "estimated_capacity": { + "type": "integer" + } + } + }, + "resourcemanager.GetConfigResponse": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + } + } + }, + "resourcemanager.ResourceInfoRowDef": { + "type": "object", + "properties": { + "burstable": { + "type": "string" + }, + "name": { + "type": "string" + }, + "priority": { + "type": "string" + }, + "ru_per_sec": { + "type": "string" + } + } + }, "rest.EmptyResponse": { "type": "object" }, diff --git a/ui/packages/tidb-dashboard-for-op/src/apps/Monitoring/context.ts b/ui/packages/tidb-dashboard-for-op/src/apps/Monitoring/context.ts index 2440e60304..8c6d605244 100644 --- a/ui/packages/tidb-dashboard-for-op/src/apps/Monitoring/context.ts +++ b/ui/packages/tidb-dashboard-for-op/src/apps/Monitoring/context.ts @@ -31,6 +31,8 @@ export const ctx: IMonitoringContext = { cfg: { getMetricsQueries: (pdVersion: string | undefined) => getMonitoringItems(pdVersion), - promAddrConfigurable: true + promAddrConfigurable: true, + metricsReferenceLink: + 'https://docs.pingcap.com/tidb/stable/dashboard-monitoring' } } diff --git a/ui/packages/tidb-dashboard-for-op/src/apps/Overview/context.ts b/ui/packages/tidb-dashboard-for-op/src/apps/Overview/context.ts index 68d9c9071e..7929968f5d 100644 --- a/ui/packages/tidb-dashboard-for-op/src/apps/Overview/context.ts +++ b/ui/packages/tidb-dashboard-for-op/src/apps/Overview/context.ts @@ -55,6 +55,8 @@ export const ctx: IOverviewContext = { apiPathBase: client.getBasePath(), metricsQueries: overviewMetrics, promAddrConfigurable: true, - showViewMoreMetrics: true + showViewMoreMetrics: true, + metricsReferenceLink: + 'https://docs.pingcap.com/tidb/stable/dashboard-monitoring' } } diff --git a/ui/packages/tidb-dashboard-for-op/src/apps/ResourceManager/context-impl.ts b/ui/packages/tidb-dashboard-for-op/src/apps/ResourceManager/context-impl.ts new file mode 100644 index 0000000000..ba868c43ce --- /dev/null +++ b/ui/packages/tidb-dashboard-for-op/src/apps/ResourceManager/context-impl.ts @@ -0,0 +1,63 @@ +import { + IResourceManagerDataSource, + IResourceManagerContext, + ReqConfig +} from '@pingcap/tidb-dashboard-lib' +import { AxiosPromise } from 'axios' + +import client, { + ResourcemanagerCalibrateResponse, + ResourcemanagerGetConfigResponse, + ResourcemanagerResourceInfoRowDef +} from '~/client' + +class DataSource implements IResourceManagerDataSource { + getConfig( + options?: ReqConfig + ): AxiosPromise { + return client.getInstance().resourceManagerConfigGet(options) + } + getInformation( + options?: ReqConfig + ): AxiosPromise { + return client.getInstance().resourceManagerInformationGet(options) + } + + getCalibrateByHardware( + params: { workload: string }, + options?: ReqConfig | undefined + ): AxiosPromise { + return client + .getInstance() + .resourceManagerCalibrateHardwareGet(params, options) + } + getCalibrateByActual( + params: { startTime: number; endTime: number }, + options?: ReqConfig | undefined + ): AxiosPromise { + return client + .getInstance() + .resourceManagerCalibrateActualGet(params, options) + } + + metricsQueryGet(params: { + endTimeSec?: number + query?: string + startTimeSec?: number + stepSec?: number + }) { + return client + .getInstance() + .metricsQueryGet(params, { + handleError: 'custom' + } as ReqConfig) + .then((res) => res.data) + } +} + +export const getResourceManagerContext: () => IResourceManagerContext = () => { + return { + ds: new DataSource(), + cfg: {} + } +} diff --git a/ui/packages/tidb-dashboard-for-op/src/apps/ResourceManager/index.tsx b/ui/packages/tidb-dashboard-for-op/src/apps/ResourceManager/index.tsx new file mode 100644 index 0000000000..549ae7fd5a --- /dev/null +++ b/ui/packages/tidb-dashboard-for-op/src/apps/ResourceManager/index.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import { + ResourceManagerApp, + ResourceManagerProvider +} from '@pingcap/tidb-dashboard-lib' +import { getResourceManagerContext } from './context-impl' + +export default function () { + return ( + + + + ) +} diff --git a/ui/packages/tidb-dashboard-for-op/src/apps/ResourceManager/meta.ts b/ui/packages/tidb-dashboard-for-op/src/apps/ResourceManager/meta.ts new file mode 100644 index 0000000000..0bb929ac3c --- /dev/null +++ b/ui/packages/tidb-dashboard-for-op/src/apps/ResourceManager/meta.ts @@ -0,0 +1,8 @@ +import { HddOutlined } from '@ant-design/icons' + +export default { + id: 'resource_manager', + routerPrefix: '/resource_manager', + icon: HddOutlined, + reactRoot: () => import('.') +} diff --git a/ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/main/Sider/index.tsx b/ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/main/Sider/index.tsx index d02152055f..34923e2cde 100644 --- a/ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/main/Sider/index.tsx +++ b/ui/packages/tidb-dashboard-for-op/src/dashboardApp/layout/main/Sider/index.tsx @@ -15,10 +15,16 @@ import styles from './index.module.less' import { store, useIsFeatureSupport } from '@pingcap/tidb-dashboard-lib' -function useAppMenuItem(registry, appId, title?: string, hideIcon?: boolean) { +function useAppMenuItem( + registry, + appId, + enable: boolean = true, + title?: string, + hideIcon?: boolean +) { const { t } = useTranslation() const app = registry.apps[appId] - if (!app) { + if (!enable || !app) { return null } return ( @@ -63,17 +69,11 @@ function Sider({ const whoAmI = store.useState((s) => s.whoAmI) const appInfo = store.useState((s) => s.appInfo) - const instanceProfilingMenuItem = useAppMenuItem( - registry, - 'instance_profiling', - '', - true - ) - const conprofMenuItem = useAppMenuItem(registry, 'conprof', '', true) - const profilingSubMenuItems = [instanceProfilingMenuItem] - if (useIsFeatureSupport('conprof')) { - profilingSubMenuItems.push(conprofMenuItem) - } + const supportConProf = useIsFeatureSupport('conprof') + const profilingSubMenuItems = [ + useAppMenuItem(registry, 'instance_profiling', true, '', true), + useAppMenuItem(registry, 'conprof', supportConProf, '', true) + ] const profilingSubMenu = ( ) - const topSQLSupport = useIsFeatureSupport('topsql') - const topSQLMenu = useAppMenuItem(registry, 'topsql') - + const supportTopSQL = useIsFeatureSupport('topsql') + // const supportResourceManager = useIsFeatureSupport('resource_manager') + const supportResourceManager = true const menuItems = [ useAppMenuItem(registry, 'overview'), useAppMenuItem(registry, 'cluster_info'), // topSQL + useAppMenuItem(registry, 'topsql', supportTopSQL), useAppMenuItem(registry, 'statement'), useAppMenuItem(registry, 'slow_query'), useAppMenuItem(registry, 'keyviz'), @@ -157,14 +158,12 @@ function Sider({ // useAppMenuItem(registry, 'diagnose'), useAppMenuItem(registry, 'monitoring'), useAppMenuItem(registry, 'search_logs'), + useAppMenuItem(registry, 'resource_manager', supportResourceManager), // useAppMenuItem(registry, '__APP_NAME__'), // NOTE: Don't remove above comment line, it is a placeholder for code generator debugSubMenu // conflictSubMenu ] - if (topSQLSupport) { - menuItems.splice(2, 0, topSQLMenu) - } if (appInfo?.enable_experimental) { menuItems.push(experimentalSubMenu) @@ -174,7 +173,7 @@ function Sider({ const extraMenuItems = [ useAppMenuItem(registry, 'dashboard_settings'), - useAppMenuItem(registry, 'user_profile', displayName) + useAppMenuItem(registry, 'user_profile', true, displayName) ] const transSider = useSpring({ diff --git a/ui/packages/tidb-dashboard-for-op/src/dashboardApp/main.tsx b/ui/packages/tidb-dashboard-for-op/src/dashboardApp/main.tsx index 0d071b82d9..878d1f1016 100755 --- a/ui/packages/tidb-dashboard-for-op/src/dashboardApp/main.tsx +++ b/ui/packages/tidb-dashboard-for-op/src/dashboardApp/main.tsx @@ -43,6 +43,7 @@ import AppUserProfile from '~/apps/UserProfile/meta' import AppDiagnose from '~/apps/Diagnose/meta' import AppOptimizerTrace from '~/apps/OptimizerTrace/meta' import AppDeadlock from '~/apps/Deadlock/meta' +import AppResourceManager from '~/apps/ResourceManager/meta' import LayoutMain from './layout/main' import LayoutSignIn from './layout/signin' @@ -173,6 +174,7 @@ async function webPageStart() { .register(AppDebugAPI) .register(AppOptimizerTrace) .register(AppDeadlock) + .register(AppResourceManager) try { const ok = await reloadWhoAmI() diff --git a/ui/packages/tidb-dashboard-lib/src/apps/Overview/components/Metrics.tsx b/ui/packages/tidb-dashboard-lib/src/apps/Overview/components/Metrics.tsx index add466066a..080c2371b3 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/Overview/components/Metrics.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/Overview/components/Metrics.tsx @@ -88,18 +88,19 @@ export default function Metrics() { onRefresh={handleManualRefreshClick} disabled={isSomeLoading} /> - - - telemetry.clickDocumentationIcon()} - /> - - + {ctx?.cfg.metricsReferenceLink && ( + + + telemetry.clickDocumentationIcon()} + /> + + + )} {isSomeLoading && } diff --git a/ui/packages/tidb-dashboard-lib/src/apps/Overview/context/index.ts b/ui/packages/tidb-dashboard-lib/src/apps/Overview/context/index.ts index cd57784a3e..c9c100954c 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/Overview/context/index.ts +++ b/ui/packages/tidb-dashboard-lib/src/apps/Overview/context/index.ts @@ -27,6 +27,7 @@ interface IMetricConfig { recent_seconds: number[] customAbsoluteRangePicker: boolean } + metricsReferenceLink?: string } export interface IOverviewDataSource { diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/Configuration.tsx b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/Configuration.tsx new file mode 100644 index 0000000000..11a575f1a7 --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/Configuration.tsx @@ -0,0 +1,87 @@ +import { Card, CardTable } from '@lib/components' +import React, { useMemo } from 'react' +import { useResourceManagerContext } from '../context' +import { useClientRequest } from '@lib/utils/useClientRequest' +import { Space, Switch, Typography } from 'antd' +import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' +import { ResourcemanagerResourceInfoRowDef } from '@lib/client' +import { useTranslation } from 'react-i18next' + +type ConfigurationProps = { + info: ResourcemanagerResourceInfoRowDef[] + loadingInfo: boolean +} + +export const Configuration: React.FC = ({ + info, + loadingInfo +}) => { + const ctx = useResourceManagerContext() + const { data: config, isLoading: loadingConfig } = useClientRequest( + ctx.ds.getConfig + ) + const { t } = useTranslation() + + const columns: IColumn[] = useMemo(() => { + return [ + { + name: t('resource_manager.configuration.table_fields.resource_group'), + key: 'resource_group', + minWidth: 100, + maxWidth: 200, + onRender: (row: any) => { + return {row.name} + } + }, + { + name: t('resource_manager.configuration.table_fields.ru_per_sec'), + key: 'ru_per_sec', + minWidth: 100, + maxWidth: 150, + onRender: (row: any) => { + return {row.ru_per_sec} + } + }, + { + name: t('resource_manager.configuration.table_fields.priority'), + key: 'priority', + minWidth: 100, + maxWidth: 150, + onRender: (row: any) => { + return {row.priority} + } + }, + { + name: t('resource_manager.configuration.table_fields.burstable'), + key: 'burstable', + minWidth: 100, + maxWidth: 150, + onRender: (row: any) => { + return {row.burstable} + } + } + ] + }, [t]) + + return ( + + + + {t('resource_manager.configuration.enabled')} + + + + + + + ) +} diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/EstimateCapacity.tsx b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/EstimateCapacity.tsx new file mode 100644 index 0000000000..27949b2d21 --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/EstimateCapacity.tsx @@ -0,0 +1,262 @@ +import { + Card, + CardTabs, + ErrorBar, + Pre, + TimeRangeSelector, + toTimeRangeValue +} from '@lib/components' +import { + Alert, + Col, + Row, + Select, + Space, + Statistic, + Tooltip, + Typography +} from 'antd' +import React, { useEffect, useMemo } from 'react' +import { useResourceManagerContext } from '../context' +import { useClientRequest } from '@lib/utils/useClientRequest' +import { InfoCircleOutlined } from '@ant-design/icons' +import { useResourceManagerUrlState } from '../uilts/url-state' +import { TIME_WINDOW_RECENT_SECONDS, WORKLOAD_TYPES } from '../uilts/helpers' +import { useTranslation } from 'react-i18next' + +const { Option } = Select +const { Paragraph, Text, Link } = Typography + +const CapacityWarning: React.FC<{ totalRU: number; estimatedRU: number }> = ({ + totalRU, + estimatedRU +}) => { + const { t } = useTranslation() + + if (estimatedRU > 0 && totalRU > estimatedRU) { + return ( +
+ +
+ ) + } + + return null +} + +const HardwareCalibrate: React.FC<{ totalRU: number }> = ({ totalRU }) => { + const ctx = useResourceManagerContext() + const { workload, setWorkload } = useResourceManagerUrlState() + const { data, isLoading, sendRequest, error } = useClientRequest( + (reqConfig) => ctx.ds.getCalibrateByHardware({ workload }, reqConfig) + ) + useEffect(() => { + sendRequest() + }, [workload]) + const estimatedRU = data?.estimated_capacity ?? 0 + const { t } = useTranslation() + + return ( +
+ + + + {t('resource_manager.estimate_capacity.workload_select_tooltip')} + + } + > + + + + +
+ + + + RUs/sec + + } + /> + + + + + +
+ + {error && ( +
+ {' '} + {' '} +
+ )} + + +
+ ) +} + +const WorkloadCalibrate: React.FC<{ totalRU: number }> = ({ totalRU }) => { + const ctx = useResourceManagerContext() + const { timeRange, setTimeRange } = useResourceManagerUrlState() + const { data, isLoading, sendRequest, error } = useClientRequest( + (reqConfig) => { + const [start, end] = toTimeRangeValue(timeRange) + return ctx.ds.getCalibrateByActual( + { startTime: start, endTime: end }, + reqConfig + ) + } + ) + useEffect(() => { + sendRequest() + }, [timeRange]) + const estimatedRU = data?.estimated_capacity ?? 0 + + const { t } = useTranslation() + + return ( +
+ + + + + {t( + 'resource_manager.estimate_capacity.time_window_select_tooltip' + )} + + } + > + + + + +
+ + + + RUs/sec + + } + /> + + + + + +
+ + {error && ( +
+ {' '} + {' '} +
+ )} + + +
+ ) +} + +export const EstimateCapacity: React.FC<{ totalRU: number }> = ({ + totalRU +}) => { + const { t } = useTranslation() + const tabs = useMemo(() => { + return [ + { + key: 'calibrate_by_hardware', + title: t('resource_manager.estimate_capacity.calibrate_by_hardware'), + content: () => + }, + { + key: 'calibrate_by_workload', + title: t('resource_manager.estimate_capacity.calibrate_by_workload'), + content: () => + } + ] + }, [totalRU, t]) + + return ( + + +
+ {t('resource_manager.estimate_capacity.ru_desc_line_1')} +
+ {t('resource_manager.estimate_capacity.ru_desc_line_2')} +
+
+
+ + {t( + 'resource_manager.estimate_capacity.change_resource_allocation' + )} + + + + {t( + 'resource_manager.estimate_capacity.resource_allocation_line_1' + )} + +
+ + {`ALTER RESOURCE GROUP RU_PER_SEC=<#ru> \\[BURSTALE];`} + +
+ + {t( + 'resource_manager.estimate_capacity.resource_allocation_ref' + )} + + {t( + 'resource_manager.estimate_capacity.resource_allocation_user_manual' + )} + + . + +
+
+
+
+ + +
+ ) +} diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/Metrics.tsx b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/Metrics.tsx new file mode 100644 index 0000000000..86cee1f327 --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/Metrics.tsx @@ -0,0 +1,109 @@ +import { TimeRangeSelector } from '@lib/components' +import { Space, Typography, Row, Col } from 'antd' +import React, { useCallback, useRef, useState } from 'react' +import { useMemoizedFn } from 'ahooks' +import { MetricsChart, SyncChartPointer, TimeRangeValue } from 'metrics-chart' +import { debounce } from 'lodash' +import { Card, TimeRange, ErrorBar } from '@lib/components' +import { tz } from '@lib/utils' +import { useTimeRangeValue } from '@lib/components/TimeRangeSelector/hook' +import { useResourceManagerContext } from '../context' +import { useResourceManagerUrlState } from '../uilts/url-state' +import { MetricConfig, metrics } from '../uilts/metricQueries' +import { useTranslation } from 'react-i18next' + +export const Metrics: React.FC = () => { + const { timeRange, setTimeRange } = useResourceManagerUrlState() + const [, setIsSomeLoading] = useState(false) + const { t } = useTranslation() + + return ( + +
+ +
+ + + + +
+ ) +} + +interface MetricsChartWrapperProps { + metrics: MetricConfig[] + timeRange: TimeRange + setTimeRange: (timeRange: TimeRange) => void + setIsSomeLoading: (isLoading: boolean) => void +} + +const MetricsChartWrapper: React.FC = ({ + metrics, + timeRange, + setTimeRange, + setIsSomeLoading +}) => { + const ctx = useResourceManagerContext() + const loadingCounter = useRef(0) + const [chartRange, setChartRange] = useTimeRangeValue(timeRange, setTimeRange) + + // eslint-disable-next-line + const setIsSomeLoadingDebounce = useCallback( + debounce(setIsSomeLoading, 100, { leading: true }), + [] + ) + + const handleOnBrush = (range: TimeRangeValue) => { + setChartRange(range) + } + + const onLoadingStateChange = useMemoizedFn((loading: boolean) => { + loading + ? (loadingCounter.current += 1) + : loadingCounter.current > 0 && (loadingCounter.current -= 1) + setIsSomeLoadingDebounce(loadingCounter.current > 0) + }) + + const ErrorComponent = (error: Error) => ( + + + + ) + + return ( + + {metrics.map((item) => ( + + + + {item.title} + + + + + ))} + + ) +} diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/index.ts b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/index.ts new file mode 100644 index 0000000000..fc2b8b0503 --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/components/index.ts @@ -0,0 +1,3 @@ +export * from './Configuration' +export * from './EstimateCapacity' +export * from './Metrics' diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/context/index.ts b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/context/index.ts new file mode 100644 index 0000000000..61c661543a --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/context/index.ts @@ -0,0 +1,52 @@ +import { + MetricsQueryResponse, + ResourcemanagerCalibrateResponse, + ResourcemanagerGetConfigResponse, + ResourcemanagerResourceInfoRowDef +} from '@lib/client' +import { ReqConfig } from '@lib/types' +import { AxiosPromise } from 'axios' +import { createContext, useContext } from 'react' + +export interface IResourceManagerDataSource { + getConfig(options?: ReqConfig): AxiosPromise + getInformation( + options?: ReqConfig + ): AxiosPromise + + getCalibrateByHardware( + params: { workload: string }, + options?: ReqConfig + ): AxiosPromise + getCalibrateByActual( + params: { startTime: number; endTime: number }, + options?: ReqConfig + ): AxiosPromise + + metricsQueryGet(params: { + endTimeSec?: number + query?: string + startTimeSec?: number + stepSec?: number + }): Promise +} + +export interface IResourceManagerConfig {} + +export interface IResourceManagerContext { + ds: IResourceManagerDataSource + cfg: IResourceManagerConfig +} + +export const ResourceManagerContext = + createContext(null) + +export const ResourceManagerProvider = ResourceManagerContext.Provider + +export const useResourceManagerContext = () => { + const ctx = useContext(ResourceManagerContext) + if (ctx === null) { + throw new Error('ResourceManagerContext must not be null') + } + return ctx +} diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/index.tsx b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/index.tsx new file mode 100644 index 0000000000..7100ad72e8 --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/index.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { HashRouter as Router, Routes, Route } from 'react-router-dom' + +import { Root } from '@lib/components' +import { useLocationChange } from '@lib/hooks/useLocationChange' +import { addTranslations } from '@lib/utils/i18n' + +import translations from './translations' +import { useResourceManagerContext } from './context' +import { Home } from './pages' + +addTranslations(translations) + +function AppRoutes() { + useLocationChange() + + return ( + + } /> + + ) +} + +export default function () { + useResourceManagerContext() + + return ( + + + + + + ) +} + +export * from './context' diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/pages/index.tsx b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/pages/index.tsx new file mode 100644 index 0000000000..079e9bd09e --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/pages/index.tsx @@ -0,0 +1,32 @@ +import React, { useMemo } from 'react' +import { Configuration, EstimateCapacity, Metrics } from '../components' +import { useClientRequest } from '@lib/utils/useClientRequest' +import { useResourceManagerContext } from '../context' + +export const Home: React.FC = () => { + const ctx = useResourceManagerContext() + + const { data: info, isLoading: loadingInfo } = useClientRequest( + ctx.ds.getInformation + ) + + const totalRU = useMemo(() => { + return (info ?? []) + .filter((item) => item.name !== 'default') + .reduce((acc, cur) => { + const ru = Number(cur.ru_per_sec) + if (!isNaN(ru)) { + return acc + ru + } + return acc + }, 0) + }, [info]) + + return ( +
+ + + +
+ ) +} diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/translations/en.yaml b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/translations/en.yaml new file mode 100644 index 0000000000..e45665acee --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/translations/en.yaml @@ -0,0 +1,36 @@ +resource_manager: + nav_title: Resource Manager + configuration: + title: Configuration + enabled: TiDB Resource Manager Enabled + table_fields: + resource_group: Resource Group + ru_per_sec: RUs/sec + priority: Priority + burstable: Burstable + estimate_capacity: + title: Estimate Capacity + ru_desc_line_1: Request Unit (RU) is a unified abstraction unit in TiDB for system resources, which is relavant to resource comsuption. + ru_desc_line_2: Please notice the "estimated capacity" refers to a result that is hardware specs or past statistics, and may deviate from actual capacity. + change_resource_allocation: Change the Resource Allocation + resource_allocation_line_1: 'To change the resource allocation for resource group:' + resource_allocation_ref: For detail information, please refer to + resource_allocation_user_manual: user manual + calibrate_by_hardware: Calibrate by Hardware + calibrate_by_workload: Calibrate by Workload + estimated_capacity: Estimated Capacity + total_ru: Total RU of user resource groups + exceed_warning: The total RU of all customized resource groups exceeds the "estimated capacity". The RU allocated to some resource groups could not be satisfied. + workload_select_tooltip: | + Select a workload type which is similar with your actual workload. + + - oltp_read_write: mixed read & write + - oltp_read_only: read intensive workload + - oltp_write_only: write intensive workload + - tpcc: write intensive workload + time_window_select_tooltip: | + Select the time window with classic workload in the past, with which TiDB can come a better estimation of RU capacity. + + Time window length: 10 mins ~ 24 hours + metrics: + title: Metrics diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/translations/index.ts b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/translations/index.ts new file mode 100644 index 0000000000..a8c57746f6 --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/translations/index.ts @@ -0,0 +1,4 @@ +import zh from './zh.yaml' +import en from './en.yaml' + +export default { zh, en } diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/translations/zh.yaml b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/translations/zh.yaml new file mode 100644 index 0000000000..4e975914d2 --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/translations/zh.yaml @@ -0,0 +1,36 @@ +resource_manager: + nav_title: 资源管控 + configuration: + title: 配置 + enabled: 启用 TiDB 资源管控 + table_fields: + resource_group: 资源分组 + ru_per_sec: 每秒请求单元 + priority: 优先级 + burstable: 是否可突发 + estimate_capacity: + title: 容量估算 + ru_desc_line_1: 请求单元 (RU) 是一个统一的抽象单元,用于表示 TiDB 系统资源,与资源消耗相关。 + ru_desc_line_2: 请注意,"估算容量" 是基于硬件配置或过去的统计数据,可能与实际容量有所偏差。 + change_resource_allocation: 修改资源分配 + resource_allocation_line_1: '执行以下命令修改资源分配:' + resource_allocation_ref: 想了解更多,请参考 + resource_allocation_user_manual: 用户手册 + calibrate_by_hardware: 通过硬件配置校准 + calibrate_by_workload: 通过负载校准 + estimated_capacity: 估算容量 + total_ru: 用户资源分组总请求单元 + exceed_warning: 所有自定义资源分组的总请求单元超过了 "估算容量"。部分资源分组的请求单元可能无法满足。 + workload_select_tooltip: | + 选择一个与实际工作负载相似的工作负载类型。 + + - oltp_read_write: 读写混合型工作负载 + - oltp_read_only: 读取密集型工作负载 + - oltp_write_only: 写入密集型工作负载 + - tpcc: 写入密集型工作负载 + time_window_select_tooltip: | + 选择一个过去的典型工作负载时间窗口,TiDB 会基于该时间窗口的统计数据来估算 RU 容量。 + + 时间窗口长度:10 分钟 ~ 24 小时 + metrics: + title: 监控指标 diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/uilts/helpers.ts b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/uilts/helpers.ts new file mode 100644 index 0000000000..ec3e4d4b9a --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/uilts/helpers.ts @@ -0,0 +1,23 @@ +import { TimeRange } from '@lib/components/TimeRangeSelector' + +export const TIME_WINDOW_RECENT_SECONDS = [ + 15 * 60, + 30 * 60, + 60 * 60, + 3 * 60 * 60, + 6 * 60 * 60, + 12 * 60 * 60, + 24 * 60 * 60 +] + +export const DEFAULT_TIME_WINDOW: TimeRange = { + type: 'recent', + value: TIME_WINDOW_RECENT_SECONDS[1] +} + +export const WORKLOAD_TYPES = [ + 'oltp_read_write', + 'oltp_read_only', + 'oltp_write_only', + 'tpcc' +] diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/uilts/metricQueries.ts b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/uilts/metricQueries.ts new file mode 100644 index 0000000000..df4c8577e1 --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/uilts/metricQueries.ts @@ -0,0 +1,89 @@ +import { QueryConfig, TransformNullValue } from 'metrics-chart' + +export type MetricConfig = { + title: string + queries: QueryConfig[] + unit: string + nullValue?: TransformNullValue +} + +export const metrics: MetricConfig[] = [ + { + title: 'Total RU Consumed', + queries: [ + { + promql: + 'sum(rate(resource_manager_resource_unit_read_request_unit_sum[1m])) + sum(rate(resource_manager_resource_unit_write_request_unit_sum[1m]))', + name: 'Total RU', + type: 'line' + } + ], + unit: 'short', + nullValue: TransformNullValue.AS_ZERO + }, + { + title: 'RU Consumed by Resource Groups', + queries: [ + { + promql: + 'sum(rate(resource_manager_resource_unit_read_request_unit_sum[1m])) by (name) + sum(rate(resource_manager_resource_unit_write_request_unit_sum[1m])) by (name)', + name: '{name}', + type: 'line' + } + ], + unit: 'short', + nullValue: TransformNullValue.AS_ZERO + }, + { + title: 'TiDB CPU Usage', + queries: [ + { + promql: 'rate(process_cpu_seconds_total{job="tidb"}[30s])', + name: '{instance}', + type: 'line' + }, + { + promql: 'sum(tidb_server_maxprocs)', + name: 'TiDB CPU Quota', + type: 'line' + } + ], + unit: 'percentunit', + nullValue: TransformNullValue.AS_ZERO + }, + { + title: 'TiKV CPU Usage', + queries: [ + { + promql: + 'sum(rate(tikv_thread_cpu_seconds_total[$__rate_interval])) by (instance)', + name: '{instance}', + type: 'line' + }, + { + promql: 'sum(tikv_server_cpu_cores_quota)', + name: 'TiKV CPU Quota', + type: 'line' + } + ], + unit: 'percentunit' + }, + { + title: 'TiKV IO MBps', + queries: [ + { + promql: + 'sum(rate(tikv_engine_flow_bytes{db="kv", type="wal_file_bytes"}[$__rate_interval])) by (instance) + sum(rate(tikv_engine_flow_bytes{db="raft", type="wal_file_bytes"}[$__rate_interval])) by (instance) + sum(rate(raft_engine_write_size_sum[$__rate_interval])) by (instance)', + name: '{instance}-write', + type: 'line' + }, + { + promql: + 'sum(rate(tikv_engine_flow_bytes{db="kv", type=~"bytes_read|iter_bytes_read"}[$__rate_interval])) by (instance)', + name: '{instance}-read', + type: 'line' + } + ], + unit: 'Bps' + } +] diff --git a/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/uilts/url-state.ts b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/uilts/url-state.ts new file mode 100644 index 0000000000..ca758fea16 --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/ResourceManager/uilts/url-state.ts @@ -0,0 +1,45 @@ +import useUrlState from '@ahooksjs/use-url-state' +import { + TimeRange, + toURLTimeRange, + urlToTimeRange +} from '@lib/components/TimeRangeSelector' +import { useCallback, useMemo } from 'react' +import { DEFAULT_TIME_WINDOW, WORKLOAD_TYPES } from './helpers' + +type UrlState = Partial> + +export function useResourceManagerUrlState() { + const [queryParams, setQueryParams] = useUrlState() + + const timeRange = useMemo(() => { + const { from, to } = queryParams + if (from && to) { + return urlToTimeRange({ from, to }) + } + return DEFAULT_TIME_WINDOW + }, [queryParams.from, queryParams.to]) + + const setTimeRange = useCallback( + (newTimeRange: TimeRange) => { + setQueryParams({ ...toURLTimeRange(newTimeRange) }) + }, + [setQueryParams] + ) + + const workload = queryParams.workload || WORKLOAD_TYPES[0] + const setWorkload = useCallback( + (w: string) => { + setQueryParams({ workload: w || undefined }) + }, + [setQueryParams] + ) + + return { + timeRange, + setTimeRange, + + workload, + setWorkload + } +} diff --git a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.Language.tsx b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.Language.tsx similarity index 93% rename from ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.Language.tsx rename to ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.Language.tsx index 8bd019c103..945ff031bf 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.Language.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.Language.tsx @@ -1,6 +1,6 @@ import { Form, Select } from 'antd' import React, { useCallback } from 'react' -import { DEFAULT_FORM_ITEM_STYLE } from './constants' +import { DEFAULT_FORM_ITEM_STYLE } from '../utils/helper' import { ALL_LANGUAGES } from '@lib/utils/i18n' import _ from 'lodash' import { useTranslation } from 'react-i18next' diff --git a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.PrometheusAddr.tsx b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.PrometheusAddr.tsx similarity index 97% rename from ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.PrometheusAddr.tsx rename to ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.PrometheusAddr.tsx index 3d0dac41b2..e802cbfb6d 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.PrometheusAddr.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.PrometheusAddr.tsx @@ -5,8 +5,8 @@ import { Button, Form, Input, Radio, Space, Typography } from 'antd' import React, { useContext } from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { DEFAULT_FORM_ITEM_STYLE } from './constants' -import { UserProfileContext } from './context' +import { DEFAULT_FORM_ITEM_STYLE } from '../utils/helper' +import { UserProfileContext } from '../context' export function PrometheusAddressForm() { const ctx = useContext(UserProfileContext) diff --git a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.SSO.tsx b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.SSO.tsx similarity index 98% rename from ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.SSO.tsx rename to ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.SSO.tsx index ec54bfa1dc..2e8e74c3c4 100755 --- a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.SSO.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.SSO.tsx @@ -18,8 +18,8 @@ import { import React, { useContext } from 'react' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { DEFAULT_FORM_ITEM_STYLE } from './constants' -import { UserProfileContext } from './context' +import { DEFAULT_FORM_ITEM_STYLE } from '../utils/helper' +import { UserProfileContext } from '../context' interface IUserAuthInputProps { value?: SsoSSOImpersonationModel diff --git a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.Session.tsx b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.Session.tsx similarity index 99% rename from ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.Session.tsx rename to ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.Session.tsx index c326ff8118..8d61cfbd90 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.Session.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.Session.tsx @@ -24,7 +24,7 @@ import { getValueFormat } from '@baurine/grafana-value-formats' import ReactMarkdown from 'react-markdown' import Checkbox from 'antd/lib/checkbox/Checkbox' import { store } from '@lib/utils/store' -import { UserProfileContext } from './context' +import { UserProfileContext } from '../context' const SHARE_SESSION_EXPIRY_HOURS = [ 0.25, diff --git a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.Version.tsx b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.Version.tsx similarity index 100% rename from ui/packages/tidb-dashboard-lib/src/apps/UserProfile/Form.Version.tsx rename to ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/Form.Version.tsx diff --git a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/index.ts b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/index.ts new file mode 100644 index 0000000000..8857440e25 --- /dev/null +++ b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/components/index.ts @@ -0,0 +1,5 @@ +export * from './Form.SSO' +export * from './Form.Session' +export * from './Form.PrometheusAddr' +export * from './Form.Version' +export * from './Form.Language' diff --git a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/index.tsx b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/index.tsx index 627e64f0ed..6d17c6d4fe 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/index.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/index.tsx @@ -2,11 +2,13 @@ import React, { useContext } from 'react' import { useTranslation } from 'react-i18next' import { HashRouter as Router, Route, Routes } from 'react-router-dom' import { Card, Root } from '@lib/components' -import { SSOForm } from './Form.SSO' -import { SessionForm } from './Form.Session' -import { PrometheusAddressForm } from './Form.PrometheusAddr' -import { VersionForm } from './Form.Version' -import { LanguageForm } from './Form.Language' +import { + SSOForm, + SessionForm, + PrometheusAddressForm, + VersionForm, + LanguageForm +} from './components' import { addTranslations } from '@lib/utils/i18n' import translations from './translations' diff --git a/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/constants.tsx b/ui/packages/tidb-dashboard-lib/src/apps/UserProfile/utils/helper.ts similarity index 100% rename from ui/packages/tidb-dashboard-lib/src/apps/UserProfile/constants.tsx rename to ui/packages/tidb-dashboard-lib/src/apps/UserProfile/utils/helper.ts diff --git a/ui/packages/tidb-dashboard-lib/src/apps/index.ts b/ui/packages/tidb-dashboard-lib/src/apps/index.ts index 9bb0115413..a813a7f379 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/index.ts +++ b/ui/packages/tidb-dashboard-lib/src/apps/index.ts @@ -54,3 +54,6 @@ export * from './OptimizerTrace' export { default as DeadlockApp } from './Deadlock' export * from './Deadlock' + +export { default as ResourceManagerApp } from './ResourceManager' +export * from './ResourceManager' diff --git a/ui/packages/tidb-dashboard-lib/src/client/models.ts b/ui/packages/tidb-dashboard-lib/src/client/models.ts index 88418e4626..88e1d3d7a9 100644 --- a/ui/packages/tidb-dashboard-lib/src/client/models.ts +++ b/ui/packages/tidb-dashboard-lib/src/client/models.ts @@ -1977,6 +1977,75 @@ export interface QueryeditorRunResponse { +/** + * + * @export + * @interface ResourcemanagerCalibrateResponse + */ +export interface ResourcemanagerCalibrateResponse { + /** + * + * @type {number} + * @memberof ResourcemanagerCalibrateResponse + */ + 'estimated_capacity'?: number; +} + + + + +/** + * + * @export + * @interface ResourcemanagerGetConfigResponse + */ +export interface ResourcemanagerGetConfigResponse { + /** + * + * @type {boolean} + * @memberof ResourcemanagerGetConfigResponse + */ + 'enable'?: boolean; +} + + + + +/** + * + * @export + * @interface ResourcemanagerResourceInfoRowDef + */ +export interface ResourcemanagerResourceInfoRowDef { + /** + * + * @type {string} + * @memberof ResourcemanagerResourceInfoRowDef + */ + 'burstable'?: string; + /** + * + * @type {string} + * @memberof ResourcemanagerResourceInfoRowDef + */ + 'name'?: string; + /** + * + * @type {string} + * @memberof ResourcemanagerResourceInfoRowDef + */ + 'priority'?: string; + /** + * + * @type {string} + * @memberof ResourcemanagerResourceInfoRowDef + */ + 'ru_per_sec'?: string; +} + + + + /** * * @export diff --git a/ui/packages/tidb-dashboard-lib/src/components/TimeRangeSelector/index.tsx b/ui/packages/tidb-dashboard-lib/src/components/TimeRangeSelector/index.tsx index ac8169120d..9ed7be56d7 100644 --- a/ui/packages/tidb-dashboard-lib/src/components/TimeRangeSelector/index.tsx +++ b/ui/packages/tidb-dashboard-lib/src/components/TimeRangeSelector/index.tsx @@ -76,6 +76,37 @@ export function fromTimeRangeValue(v: TimeRangeValue): AbsoluteTimeRange { } } +////////////////////// + +export type URLTimeRange = { from: string; to: string } + +export const toURLTimeRange = (timeRange: TimeRange): URLTimeRange => { + if (timeRange.type === 'recent') { + return { from: `${timeRange.value}`, to: 'now' } + } + + const timeRangeValue = toTimeRangeValue(timeRange) + return { from: `${timeRangeValue[0]}`, to: `${timeRangeValue[1]}` } +} + +export const urlToTimeRange = (urlTimeRange: URLTimeRange): TimeRange => { + if (urlTimeRange.to === 'now') { + return { type: 'recent', value: Number(urlTimeRange.from) } + } + return { + type: 'absolute', + value: [Number(urlTimeRange.from), Number(urlTimeRange.to)] + } +} + +export const urlToTimeRangeValue = ( + urlTimeRange: URLTimeRange +): TimeRangeValue => { + return toTimeRangeValue(urlToTimeRange(urlTimeRange)) +} + +////////////////////// + /** * @deprecated */