From 94e63aa8634f198922f7cf9468c5b29001976016 Mon Sep 17 00:00:00 2001 From: Suhaha Date: Tue, 18 May 2021 00:44:10 +0800 Subject: [PATCH 01/31] refine(ui): debugapi layout --- ui/lib/apps/DebugAPI/apilist/ApiForm.tsx | 74 ++++++++++++++--------- ui/lib/apps/DebugAPI/apilist/ApiList.tsx | 18 ++++-- ui/lib/apps/DebugAPI/translations/en.yaml | 1 + ui/lib/apps/DebugAPI/translations/zh.yaml | 1 + ui/lib/components/Card/index.module.less | 2 +- 5 files changed, 61 insertions(+), 35 deletions(-) diff --git a/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx b/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx index 68dd123992..1d706e9d19 100644 --- a/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx +++ b/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Form, Button } from 'antd' -import { DownloadOutlined } from '@ant-design/icons' +import { Form, Button, Space, Tooltip, Row, Col } from 'antd' +import { DownloadOutlined, UndoOutlined } from '@ant-design/icons' import client, { DebugapiEndpointAPIModel, DebugapiEndpointAPIParam, @@ -13,15 +13,6 @@ export interface Topology { tidb: TopologyTiDBInfo[] } -const formItemLayout = { - labelCol: { offset: 1 }, - wrapperCol: { offset: 1 }, -} - -const buttonItemLayout = { - wrapperCol: { offset: 1 }, -} - export default function ApiForm({ endpoint, topology, @@ -94,30 +85,53 @@ export default function ApiForm({ ) return ( -
- - {params.map((param) => ( - - ))} - - + + + + + + {params.map((param) => ( + + + + ))} + + + + + + - - diff --git a/ui/lib/apps/DebugAPI/translations/en.yaml b/ui/lib/apps/DebugAPI/translations/en.yaml index 2e1ba06d39..4dd9ff8927 100644 --- a/ui/lib/apps/DebugAPI/translations/en.yaml +++ b/ui/lib/apps/DebugAPI/translations/en.yaml @@ -20,58 +20,50 @@ debug_api: tidb: name: TiDB endpoints: - tidb_stats_dump: Stats Dump - tidb_stats_dump_timestamp: Stats Dump With Timestamp - tidb_stats_dump_timestamp_desc: Fetch stats dump with timestamp, and timestamp needs to be set within the GC safe point + tidb_stats_dump: Statistics Data - of a Table + tidb_stats_dump_timestamp: Statistics Data - of a Table and Timestamp + tidb_stats_dump_timestamp_desc: The timestamp needs to be set within the GC safe point tidb_config: Current TiDB Config - tidb_schema: TiDB Schema - tidb_schema_db: DB Schema - tidb_schema_db_table: Table Schema - tidb_dbtable_tableid: Table Schema By Table ID - tidb_ddl_history: DDL History - tidb_info: Info - tidb_info_all: All Info - tidb_regions_meta: Regions Meta + tidb_schema: Schema Information - All / by TableID + tidb_schema_db: Schema Information - by Database + tidb_schema_db_table: Schema Information - by Database + Table + tidb_dbtable_tableid: schema and Table Information - by TableID + tidb_ddl_history: DDL History - All + tidb_info: TiDB Server Information - Current + tidb_info_all: TiDB Server Information - All TiDB Servers + tidb_regions_meta: Region - All + tidb_region_id: Region - by RegionID + tidb_table_regions: Region - by Database + Table tidb_hot_regions: Hot Regions - tidb_region_id: Specific Region - tidb_table_regions: Table Regions - tidb_pprof_alloc: Pprof Alloc - tidb_pprof_block: Pprof Block - tidb_pprof_goroutine: Pprof Goroutine - tidb_pprof_heap: Pprof Heap - tidb_pprof_mutex: Pprof Mutex + tidb_pprof: TiDB pprof pd: name: PD endpoints: - pd_cluster: PD Cluster - pd_cluster_status: PD Cluster Status - pd_health: Health Check - pd_hot_read: Hot Read - pd_hot_write: Hot Write - pd_hot_stores: Hot Stores - pd_labels: Labels - pd_label_stores: Label List Stores - pd_members_show: Show Members - pd_leader_show: Show Leader - pd_operator_show: Show Operator - pd_regions: Regions - pd_region_id: Region ID - pd_region_key: Region Key - pd_region_scan: Region Scan - pd_region_sibling: Region Sibling - pd_region_start_key: Region Start Key - pd_regions_store: Regions Store - pd_region_top_read: Region Top Read - pd_region_top_write: Region Top Write - pd_region_top_conf_ver: Region Top ConfVer - pd_region_top_version: Region Top Version - pd_region_top_size: Region Top Size - pd_region_check: Region Check - pd_scheduler_show: Show Scheduler - pd_stores: Stores - pd_store_id: Store ID - pd_pprof_alloc: Pprof Alloc - pd_pprof_block: Pprof Block - pd_pprof_goroutine: Pprof Goroutine - pd_pprof_heap: Pprof Heap - pd_pprof_mutex: Pprof Mutex + pd_cluster: Cluster Information (pd-ctl cluster) + pd_cluster_status: Cluster Status + pd_health: Cluster Health Information (pd-ctl health) + pd_hot_read: Hot - Read (pd-ctl hot read) + pd_hot_write: Hot - Write (pd-ctl hot write) + pd_hot_stores: Hot - Stores (pd-ctl hot store) + pd_labels: All Labels (pd-ctl label) + pd_label_stores: List Stores by Label (pd-ctl label store [name] [value]) + pd_members_show: All Members Information (pd-ctl member) + pd_leader_show: Leader Information (pd-ctl member leader show) + pd_operator_show: All Operators (pd-ctl operator show) + pd_regions: Regions - All (pd-ctl region) + pd_region_id: Region - by RegionID (pd-ctl region [id]) + pd_region_key: Region - by Key Reside in (pd-ctl region key [key]) + pd_region_scan: Regions - Scan All (pd-ctl region scan) + pd_region_sibling: Regions - Sibling Regions by RegionID (pd-ctl region sibling [id]) + pd_region_start_key: Regions - All Regions Starting from a Key (pd-ctl region startkey [key]) + pd_regions_store: Regions - All Regions of a Store (pd-ctl region store [store-id]) + pd_region_top_read: Regions - Top Read Flow (pd-ctl region topread) + pd_region_top_write: Regions - Top Write Flow (pd-ctl region topread) + pd_region_top_conf_ver: Regions - Top Conf Version (pd-ctl region topconfver) + pd_region_top_version: Regions - Top Version (pd-ctl region topversion) + pd_region_top_size: Regions - Top Size (pd-ctl region topsize) + pd_region_check: Regions - Check Regions in Abnormal Conditions (region check [state]) + pd_scheduler_show: All Schedulers (pd-ctl scheduler show) + pd_stores: Stores - All (pd-ctl store) + pd_store_id: Store - by StoreID (pd-ctl store [id]) + pd_pprof: PD pprof diff --git a/ui/lib/apps/DebugAPI/translations/zh.yaml b/ui/lib/apps/DebugAPI/translations/zh.yaml index d5fd94e355..ac60bc5e52 100644 --- a/ui/lib/apps/DebugAPI/translations/zh.yaml +++ b/ui/lib/apps/DebugAPI/translations/zh.yaml @@ -7,7 +7,7 @@ debug_api: body: 本页面提供的调试接口主要面向 TiDB 开发者、提供数据库内部运行数据。请在 TiDB 技术支持的指导下使用本功能。 form: download: 下载 - reset: 重置表单 + reset: 重置 widgets: host_select_placeholder: 请选择对应的 {{endpointType}} host text: 请输入 {{param}} @@ -18,60 +18,5 @@ debug_api: table: 请从列表中选择 table 或输入完整的 table 名称 table_id: 请从列表中选择 table ID 或输入完整的 table ID tidb: - name: TiDB endpoints: - tidb_stats_dump: Stats Dump - tidb_stats_dump_timestamp: Stats Dump With Timestamp - tidb_stats_dump_timestamp_desc: 获取特定时刻的 stats dump,该时刻应该在当前时间与 GC 时间节点之间 - tidb_config: Current TiDB Config - tidb_schema: TiDB Schema - tidb_schema_db: DB Schema - tidb_schema_db_table: Table Schema - tidb_dbtable_tableid: Table Schema By Table ID - tidb_ddl_history: DDL History - tidb_info: Info - tidb_info_all: All Info - tidb_regions_meta: Regions Meta - tidb_hot_regions: Hot Regions - tidb_region_id: Specific Region - tidb_table_regions: Table Regions - tidb_pprof_alloc: Pprof Alloc - tidb_pprof_block: Pprof Block - tidb_pprof_goroutine: Pprof Goroutine - tidb_pprof_heap: Pprof Heap - tidb_pprof_mutex: Pprof Mutex - pd: - name: PD - endpoints: - pd_cluster: Cluster - pd_cluster_status: Cluster Status - pd_health: Health Check - pd_hot_read: Hot Read - pd_hot_write: Hot Write - pd_hot_stores: Hot Stores - pd_labels: Labels - pd_label_stores: Label List Stores - pd_members_show: Show Members - pd_leader_show: Show Leader - pd_operator_show: Show Operator - pd_regions: Regions - pd_region_id: Region ID - pd_region_key: Region Key - pd_region_scan: Region Scan - pd_region_sibling: Region Sibling - pd_region_start_key: Region Start Key - pd_regions_store: Regions Store - pd_region_top_read: Region Top Read - pd_region_top_write: Region Top Write - pd_region_top_conf_ver: Region Top ConfVer - pd_region_top_version: Region Top Version - pd_region_top_size: Region Top Size - pd_region_check: Region Check - pd_scheduler_show: Show Scheduler - pd_stores: Stores - pd_store_id: Store ID - pd_pprof_alloc: Pprof Alloc - pd_pprof_block: Pprof Block - pd_pprof_goroutine: Pprof Goroutine - pd_pprof_heap: Pprof Heap - pd_pprof_mutex: Pprof Mutex + tidb_stats_dump_timestamp_desc: 时间戳应当在 GC Safe Point 以后 From e93902dbd78ab2c61d1793505f7e0dea98a5d226 Mon Sep 17 00:00:00 2001 From: Suhaha Date: Thu, 27 May 2021 03:53:23 +0800 Subject: [PATCH 21/31] feat(debugapi): add constant param model --- .../debugapi/endpoint/endpoint_def.go | 26 +++++++++-- .../debugapi/endpoint/param_models.go | 23 +++++++--- ui/lib/apps/DebugAPI/apilist/ApiForm.tsx | 34 +++++++------- ui/lib/apps/DebugAPI/apilist/ApiList.tsx | 10 +--- .../DebugAPI/apilist/widgets/Constant.tsx | 11 +++++ ui/lib/apps/DebugAPI/apilist/widgets/Int.tsx | 5 +- ui/lib/apps/DebugAPI/apilist/widgets/Text.tsx | 6 ++- .../apps/DebugAPI/apilist/widgets/index.tsx | 46 +++++++++++++++++-- 8 files changed, 119 insertions(+), 42 deletions(-) create mode 100644 ui/lib/apps/DebugAPI/apilist/widgets/Constant.tsx diff --git a/pkg/apiserver/debugapi/endpoint/endpoint_def.go b/pkg/apiserver/debugapi/endpoint/endpoint_def.go index 6b738bcebb..f855088a6d 100644 --- a/pkg/apiserver/debugapi/endpoint/endpoint_def.go +++ b/pkg/apiserver/debugapi/endpoint/endpoint_def.go @@ -210,12 +210,21 @@ var tidbHotRegions = APIModel{ var tidbPprof = APIModel{ ID: "tidb_pprof", Component: model.NodeKindTiDB, - Path: "/debug/pprof/{kind}?debug=1", + Path: "/debug/pprof/{kind}", Method: MethodGet, PathParams: []APIParam{ pprofKindsParam, }, - // TODO: We should allow user to specify `seconds` parameter. + QueryParams: []APIParam{ + { + Name: "debug", + Model: CreateAPIParamModelConstant("1"), + }, + { + Name: "seconds", + Model: APIParamModelInt, + }, + }, } // pd endpoints @@ -596,12 +605,21 @@ var pdStoreID = APIModel{ var pdPprof = APIModel{ ID: "pd_pprof", Component: model.NodeKindPD, - Path: "/debug/pprof/{kind}?debug=1", + Path: "/debug/pprof/{kind}", Method: MethodGet, PathParams: []APIParam{ pprofKindsParam, }, - // TODO: We should allow user to specify `seconds` parameter. + QueryParams: []APIParam{ + { + Name: "debug", + Model: CreateAPIParamModelConstant("1"), + }, + { + Name: "seconds", + Model: APIParamModelInt, + }, + }, } var APIListDef = []APIModel{ diff --git a/pkg/apiserver/debugapi/endpoint/param_models.go b/pkg/apiserver/debugapi/endpoint/param_models.go index eb2e54df50..834966925c 100644 --- a/pkg/apiserver/debugapi/endpoint/param_models.go +++ b/pkg/apiserver/debugapi/endpoint/param_models.go @@ -22,11 +22,11 @@ import ( "github.com/thoas/go-funk" ) -var APIParamModelText APIParamModel = APIParamModel{ +var APIParamModelText = APIParamModel{ Type: "text", } -var APIParamModelMultiTags APIParamModel = APIParamModel{ +var APIParamModelMultiTags = APIParamModel{ Type: "tags", Transformer: func(ctx *Context) error { vals := strings.Split(ctx.Value(), ",") @@ -38,7 +38,7 @@ var APIParamModelMultiTags APIParamModel = APIParamModel{ }, } -var APIParamModelInt APIParamModel = APIParamModel{ +var APIParamModelInt = APIParamModel{ Type: "int", Transformer: func(ctx *Context) error { if _, err := strconv.Atoi(ctx.Value()); err != nil { @@ -66,14 +66,25 @@ func CreateAPIParamModelEnum(items []EnumItem) APIParamModel { } } -var APIParamModelDB APIParamModel = APIParamModel{ +func CreateAPIParamModelConstant(constVal string) APIParamModel { + return APIParamModel{ + Type: "constant", + Data: constVal, + Transformer: func(ctx *Context) error { + ctx.SetValue(constVal) + return nil + }, + } +} + +var APIParamModelDB = APIParamModel{ Type: "db", } -var APIParamModelTable APIParamModel = APIParamModel{ +var APIParamModelTable = APIParamModel{ Type: "table", } -var APIParamModelTableID APIParamModel = APIParamModel{ +var APIParamModelTableID = APIParamModel{ Type: "table_id", } diff --git a/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx b/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx index 7f673b65f4..00677b2d83 100644 --- a/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx +++ b/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx @@ -11,7 +11,7 @@ import client, { TopologyStoreInfo, TopologyTiDBInfo, } from '@lib/client' -import { ApiFormWidgetConfig, paramWidgets, paramModelWidgets } from './widgets' +import { ApiFormWidgetConfig, createFormWidget } from './widgets' import { isJSONContentType, download as downloadFile } from './file' export interface Topology { @@ -104,16 +104,19 @@ export default function ApiForm({ - {params.map((param) => ( - - - - ))} + {params + // hide constant param model widget + .filter((param) => param.model?.type !== 'constant') + .map((param) => ( + + + + ))} @@ -147,19 +150,14 @@ function FormItemCol(props: React.HTMLAttributes) { } function ApiFormItem(widgetConfig: ApiFormWidgetConfig) { - const { param, endpoint } = widgetConfig - let widget = - paramWidgets[`${endpoint.id}/${param.name!}`] || - paramModelWidgets[param.model?.type!] || - paramModelWidgets.text - + const { param } = widgetConfig return ( - {widget(widgetConfig)} + {createFormWidget(widgetConfig)} ) } diff --git a/ui/lib/apps/DebugAPI/apilist/ApiList.tsx b/ui/lib/apps/DebugAPI/apilist/ApiList.tsx index f9efd75cfd..7d9f9d5153 100644 --- a/ui/lib/apps/DebugAPI/apilist/ApiList.tsx +++ b/ui/lib/apps/DebugAPI/apilist/ApiList.tsx @@ -11,6 +11,7 @@ import client, { EndpointAPIModel } from '@lib/client' import style from './ApiList.module.less' import ApiForm, { Topology } from './ApiForm' +import { buildQueryString } from './widgets' const useFilterEndpoints = (endpoints?: EndpointAPIModel[]) => { const [keywords, setKeywords] = useState('') @@ -189,14 +190,7 @@ function CustomHeader({ // e.g. http://{tidb_ip}/stats/dump/{db}/{table}?queryName={queryName} function Schema({ endpoint }: { endpoint: EndpointAPIModel }) { - const query = - endpoint.query_params?.reduce((prev, { name }, i) => { - if (i === 0) { - prev += '?' - } - prev += `${name}={${name}}` - return prev - }, '') || '' + const query = buildQueryString(endpoint.query_params ?? []) return (

{`http://{${endpoint.component}_host}${endpoint.path}${query}`} diff --git a/ui/lib/apps/DebugAPI/apilist/widgets/Constant.tsx b/ui/lib/apps/DebugAPI/apilist/widgets/Constant.tsx new file mode 100644 index 0000000000..2266ae134d --- /dev/null +++ b/ui/lib/apps/DebugAPI/apilist/widgets/Constant.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +import type { ApiFormWidget, QueryBuilder } from './index' + +export const ConstantWidget: ApiFormWidget = ({ param }) => { + return

{param.model?.data}

+} + +export const ConstantQueryBuilder: QueryBuilder = (p) => { + return `${p.name}=${p.model?.data}` +} diff --git a/ui/lib/apps/DebugAPI/apilist/widgets/Int.tsx b/ui/lib/apps/DebugAPI/apilist/widgets/Int.tsx index 0e5549ab52..ad595a2947 100644 --- a/ui/lib/apps/DebugAPI/apilist/widgets/Int.tsx +++ b/ui/lib/apps/DebugAPI/apilist/widgets/Int.tsx @@ -4,10 +4,13 @@ import { useTranslation } from 'react-i18next' import type { ApiFormWidget } from './index' -export const IntWidget: ApiFormWidget = ({ param }) => { +export const IntWidget: ApiFormWidget = ({ param, onChange, value }) => { const { t } = useTranslation() return ( onChange!(v ? String(v) : (undefined as any))} placeholder={t(`debug_api.widgets.int`, { param: param.name })} /> ) diff --git a/ui/lib/apps/DebugAPI/apilist/widgets/Text.tsx b/ui/lib/apps/DebugAPI/apilist/widgets/Text.tsx index 587ef964b0..c27715566c 100644 --- a/ui/lib/apps/DebugAPI/apilist/widgets/Text.tsx +++ b/ui/lib/apps/DebugAPI/apilist/widgets/Text.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Input } from 'antd' import { useTranslation } from 'react-i18next' -import type { ApiFormWidget } from './index' +import type { ApiFormWidget, QueryBuilder } from './index' export const TextWidget: ApiFormWidget = ({ param }) => { const { t } = useTranslation() @@ -10,3 +10,7 @@ export const TextWidget: ApiFormWidget = ({ param }) => { ) } + +export const TextQueryBuilder: QueryBuilder = (p) => { + return `${p.name}={${p.name}}` +} diff --git a/ui/lib/apps/DebugAPI/apilist/widgets/index.tsx b/ui/lib/apps/DebugAPI/apilist/widgets/index.tsx index bf8dfe6cdd..dd3156af8b 100644 --- a/ui/lib/apps/DebugAPI/apilist/widgets/index.tsx +++ b/ui/lib/apps/DebugAPI/apilist/widgets/index.tsx @@ -3,10 +3,11 @@ import type { FormInstance } from 'antd/es/form/Form' import { EndpointAPIModel, EndpointAPIParam } from '@lib/client' import type { Topology } from '../ApiForm' -import { TextWidget } from './Text' +import { TextWidget, TextQueryBuilder } from './Text' import { TagsWidget } from './Tags' import { IntWidget } from './Int' import { EnumWidget } from './Enum' +import { ConstantWidget, ConstantQueryBuilder } from './Constant' import { HostSelectWidget } from './Host' import { DatabaseWidget } from './Database' import { TableWidget } from './Table' @@ -35,17 +36,54 @@ const createJSXElementWrapper = (WidgetDef: ApiFormWidget) => ( config: ApiFormWidgetConfig ) => -export const paramModelWidgets: Widgets = { +const paramModelWidgets: Widgets = { host: HostSelectWidget, text: TextWidget, tags: createJSXElementWrapper(TagsWidget), - int: IntWidget, + int: createJSXElementWrapper(IntWidget), enum: EnumWidget, + constant: ConstantWidget, db: createJSXElementWrapper(DatabaseWidget), table: createJSXElementWrapper(TableWidget), table_id: createJSXElementWrapper(TableIDWidget), } -export const paramWidgets: Widgets = { +const paramWidgets: Widgets = { 'pd_stores/state': createJSXElementWrapper(StoresStateWidget), } + +export const createFormWidget = (config: ApiFormWidgetConfig) => { + const { param, endpoint } = config + const widget = + paramWidgets[`${endpoint.id}/${param.name!}`] || + paramModelWidgets[param.model?.type!] || + paramModelWidgets.text + return widget(config) +} + +// query string + +export interface QueryBuilder { + (p: EndpointAPIParam): string +} + +const queryBuilders: { [type: string]: QueryBuilder } = { + text: TextQueryBuilder, + constant: ConstantQueryBuilder, +} + +export const buildQueryString = (params: EndpointAPIParam[]) => { + const query = params.reduce((prev, param, i) => { + if (i === 0) { + prev += '?' + } else { + prev += '&' + } + + const builder = queryBuilders[param.model?.type!] || queryBuilders.text + prev += builder(param) + + return prev + }, '') + return query +} From 7b1cf3057c41ff79344ae76d90bfaf6350da6a29 Mon Sep 17 00:00:00 2001 From: Suhaha Date: Thu, 27 May 2021 13:41:12 +0800 Subject: [PATCH 22/31] refine(debugapi): param model interface --- pkg/apiserver/debugapi/endpoint/endpoint.go | 15 ++-- .../debugapi/endpoint/endpoint_test.go | 6 +- .../debugapi/endpoint/param_models.go | 70 ++++++++++++++----- .../debugapi/endpoint/param_models_test.go | 26 ++++++- ui/lib/apps/DebugAPI/apilist/ApiForm.tsx | 10 ++- .../DebugAPI/apilist/widgets/Constant.tsx | 6 +- ui/lib/apps/DebugAPI/apilist/widgets/Enum.tsx | 17 ++--- .../apps/DebugAPI/apilist/widgets/index.tsx | 10 ++- 8 files changed, 117 insertions(+), 43 deletions(-) diff --git a/pkg/apiserver/debugapi/endpoint/endpoint.go b/pkg/apiserver/debugapi/endpoint/endpoint.go index 204089b197..185462d5c9 100644 --- a/pkg/apiserver/debugapi/endpoint/endpoint.go +++ b/pkg/apiserver/debugapi/endpoint/endpoint.go @@ -42,15 +42,14 @@ type APIParam struct { Name string `json:"name"` Required bool `json:"required"` // represents what param is - Model APIParamModel `json:"model"` + Model APIParamModel `json:"model" swaggertype:"object,string"` PreModelTransformer ModelTransformer `json:"-"` PostModelTransformer ModelTransformer `json:"-"` } -type APIParamModel struct { - Type string `json:"type"` - Data interface{} `json:"data"` - Transformer ModelTransformer `json:"-"` +type APIParamModel interface { + Transform(ctx *Context) error + PreTransform(ctx *Context) error } // ModelTransformer can transform the incoming param's value in special scenarios @@ -150,6 +149,10 @@ func transformValues(params []APIParam, values map[string]string, forceRequired if err != nil { return nil, ErrInvalidParam.Wrap(err, "param: %s", p.Name) } + err = transform(ctx, p.Model.PreTransform) + if err != nil { + return nil, ErrInvalidParam.Wrap(err, "param: %s", p.Name) + } if ctx.Value() == "" { if forceRequired || p.Required { return nil, ErrMissingRequiredParam.New("missing required param: %s", p.Name) @@ -159,7 +162,7 @@ func transformValues(params []APIParam, values map[string]string, forceRequired continue } - err = transform(ctx, p.Model.Transformer) + err = transform(ctx, p.Model.Transform) if err != nil { return nil, ErrInvalidParam.Wrap(err, "param: %s", p.Name) } diff --git a/pkg/apiserver/debugapi/endpoint/endpoint_test.go b/pkg/apiserver/debugapi/endpoint/endpoint_test.go index f2bf7e3184..cae292747b 100644 --- a/pkg/apiserver/debugapi/endpoint/endpoint_test.go +++ b/pkg/apiserver/debugapi/endpoint/endpoint_test.go @@ -34,7 +34,7 @@ var _ = Suite(&testSchemaSuite{}) type testSchemaSuite struct{} -var testAPIParamModel APIParamModel = APIParamModel{ +var testAPIParamModel = &DefaultAPIParamModel{ Type: "text", } @@ -142,7 +142,7 @@ func (t *testSchemaSuite) Test_NewRequest_missing_required_params_err(c *C) { } func (t *testSchemaSuite) Test_NewRequest_transformer_validation(c *C) { - testParamModel := APIParamModel{ + testParamModel := &DefaultAPIParamModel{ Type: "test", Transformer: func(ctx *Context) error { return fmt.Errorf("test error") @@ -172,7 +172,7 @@ func (t *testSchemaSuite) Test_NewRequest_transformer_validation(c *C) { func (t *testSchemaSuite) Test_NewRequest_transformer_transform(c *C) { testValue := "test_value" - testParamModel := APIParamModel{ + testParamModel := &DefaultAPIParamModel{ Type: "test", Transformer: func(ctx *Context) error { ctx.SetValue(testValue) diff --git a/pkg/apiserver/debugapi/endpoint/param_models.go b/pkg/apiserver/debugapi/endpoint/param_models.go index 834966925c..eeb4523a53 100644 --- a/pkg/apiserver/debugapi/endpoint/param_models.go +++ b/pkg/apiserver/debugapi/endpoint/param_models.go @@ -22,11 +22,33 @@ import ( "github.com/thoas/go-funk" ) -var APIParamModelText = APIParamModel{ +type DefaultAPIParamModel struct { + Type string `json:"type"` + PreTransformer ModelTransformer `json:"-"` + Transformer ModelTransformer `json:"-"` +} + +func (m *DefaultAPIParamModel) PreTransform(ctx *Context) error { + if m.PreTransformer != nil { + return m.PreTransformer(ctx) + } + return nil +} + +func (m *DefaultAPIParamModel) Transform(ctx *Context) error { + if m.Transformer != nil { + return m.Transformer(ctx) + } + return nil +} + +var _ APIParamModel = (*DefaultAPIParamModel)(nil) + +var APIParamModelText = &DefaultAPIParamModel{ Type: "text", } -var APIParamModelMultiTags = APIParamModel{ +var APIParamModelMultiTags = &DefaultAPIParamModel{ Type: "tags", Transformer: func(ctx *Context) error { vals := strings.Split(ctx.Value(), ",") @@ -38,7 +60,7 @@ var APIParamModelMultiTags = APIParamModel{ }, } -var APIParamModelInt = APIParamModel{ +var APIParamModelInt = &DefaultAPIParamModel{ Type: "int", Transformer: func(ctx *Context) error { if _, err := strconv.Atoi(ctx.Value()); err != nil { @@ -48,43 +70,57 @@ var APIParamModelInt = APIParamModel{ }, } +type EnumAPIParamModel struct { + DefaultAPIParamModel + Data []EnumItem `json:"data"` +} + type EnumItem struct { Name string `json:"name"` Value string `json:"value"` } -func CreateAPIParamModelEnum(items []EnumItem) APIParamModel { +func CreateAPIParamModelEnum(items []EnumItem) *EnumAPIParamModel { items = funk.Map(items, func(item EnumItem) EnumItem { if item.Value == "" { item.Value = item.Name } return item }).([]EnumItem) - return APIParamModel{ - Type: "enum", - Data: items, + return &EnumAPIParamModel{ + DefaultAPIParamModel{ + Type: "enum", + }, + items, } } -func CreateAPIParamModelConstant(constVal string) APIParamModel { - return APIParamModel{ - Type: "constant", - Data: constVal, - Transformer: func(ctx *Context) error { - ctx.SetValue(constVal) - return nil +type ConstantAPIParamModel struct { + DefaultAPIParamModel + Data string `json:"data"` +} + +func CreateAPIParamModelConstant(constVal string) *ConstantAPIParamModel { + return &ConstantAPIParamModel{ + DefaultAPIParamModel{ + Type: "constant", + PreTransformer: func(ctx *Context) error { + ctx.SetValue(constVal) + return nil + }, }, + constVal, } } -var APIParamModelDB = APIParamModel{ +var APIParamModelDB = &DefaultAPIParamModel{ Type: "db", } -var APIParamModelTable = APIParamModel{ +var APIParamModelTable = &DefaultAPIParamModel{ Type: "table", } -var APIParamModelTableID = APIParamModel{ +var APIParamModelTableID = &DefaultAPIParamModel{ Type: "table_id", } diff --git a/pkg/apiserver/debugapi/endpoint/param_models_test.go b/pkg/apiserver/debugapi/endpoint/param_models_test.go index a3fe932230..aec0e3b83c 100644 --- a/pkg/apiserver/debugapi/endpoint/param_models_test.go +++ b/pkg/apiserver/debugapi/endpoint/param_models_test.go @@ -74,8 +74,8 @@ func (t *testParamModelsSuite) Test_APIParamModelInt(c *C) { }, }, } - value1 := "value1" + value1 := "value1" _, err := testEndpoint.NewRequest("127.0.0.1", 10080, map[string]string{ "param1": value1, }) @@ -83,7 +83,6 @@ func (t *testParamModelsSuite) Test_APIParamModelInt(c *C) { c.Assert(errorx.IsOfType(err, ErrInvalidParam), Equals, true) value2 := "2" - req2, err := testEndpoint.NewRequest("127.0.0.1", 10080, map[string]string{ "param1": value2, }) @@ -93,3 +92,26 @@ func (t *testParamModelsSuite) Test_APIParamModelInt(c *C) { c.Error(err) } } + +func (t *testParamModelsSuite) Test_APIParamModelConstant(c *C) { + value1 := "value1" + testEndpoint := APIModel{ + ID: "test_endpoint", + Component: model.NodeKindTiDB, + Path: "/test", + Method: http.MethodGet, + QueryParams: []APIParam{ + { + Name: "param1", + Model: CreateAPIParamModelConstant(value1), + }, + }, + } + + req2, err := testEndpoint.NewRequest("127.0.0.1", 10080, map[string]string{}) + if err == nil { + c.Assert(req2.Query, Equals, fmt.Sprintf("param1=%s", value1)) + } else { + c.Error(err) + } +} diff --git a/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx b/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx index 00677b2d83..50352410e0 100644 --- a/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx +++ b/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx @@ -11,7 +11,11 @@ import client, { TopologyStoreInfo, TopologyTiDBInfo, } from '@lib/client' -import { ApiFormWidgetConfig, createFormWidget } from './widgets' +import { + ApiFormWidgetConfig, + createFormWidget, + ParamModelType, +} from './widgets' import { isJSONContentType, download as downloadFile } from './file' export interface Topology { @@ -106,7 +110,9 @@ export default function ApiForm({ {params // hide constant param model widget - .filter((param) => param.model?.type !== 'constant') + .filter( + (param) => (param.model as ParamModelType).type !== 'constant' + ) .map((param) => ( { - return

{param.model?.data}

+ return

{(param.model as ParamModelType).data}

} export const ConstantQueryBuilder: QueryBuilder = (p) => { - return `${p.name}=${p.model?.data}` + return `${p.name}=${(p.model as ParamModelType).data}` } diff --git a/ui/lib/apps/DebugAPI/apilist/widgets/Enum.tsx b/ui/lib/apps/DebugAPI/apilist/widgets/Enum.tsx index 5d3c62d00f..8351fed484 100644 --- a/ui/lib/apps/DebugAPI/apilist/widgets/Enum.tsx +++ b/ui/lib/apps/DebugAPI/apilist/widgets/Enum.tsx @@ -2,19 +2,20 @@ import React from 'react' import { Select } from 'antd' import { useTranslation } from 'react-i18next' -import type { ApiFormWidget } from './index' +import type { ApiFormWidget, ParamModelType } from './index' export const EnumWidget: ApiFormWidget = ({ param }) => { const { t } = useTranslation() return ( ) } diff --git a/ui/lib/apps/DebugAPI/apilist/widgets/index.tsx b/ui/lib/apps/DebugAPI/apilist/widgets/index.tsx index dd3156af8b..7fc4694532 100644 --- a/ui/lib/apps/DebugAPI/apilist/widgets/index.tsx +++ b/ui/lib/apps/DebugAPI/apilist/widgets/index.tsx @@ -31,6 +31,11 @@ export interface ApiFormWidgetConfig { onChange?: (v: string) => void } +export interface ParamModelType { + type: string + data: any +} + // For customized form controls. https://ant.design/components/form-cn/#components-form-demo-customized-form-controls const createJSXElementWrapper = (WidgetDef: ApiFormWidget) => ( config: ApiFormWidgetConfig @@ -56,7 +61,7 @@ export const createFormWidget = (config: ApiFormWidgetConfig) => { const { param, endpoint } = config const widget = paramWidgets[`${endpoint.id}/${param.name!}`] || - paramModelWidgets[param.model?.type!] || + paramModelWidgets[(param.model as any).type] || paramModelWidgets.text return widget(config) } @@ -80,7 +85,8 @@ export const buildQueryString = (params: EndpointAPIParam[]) => { prev += '&' } - const builder = queryBuilders[param.model?.type!] || queryBuilders.text + const builder = + queryBuilders[(param.model as ParamModelType).type] || queryBuilders.text prev += builder(param) return prev From 63da39e7d74eec53effb3f19491c4ed35c420b72 Mon Sep 17 00:00:00 2001 From: Suhaha Date: Thu, 27 May 2021 13:51:55 +0800 Subject: [PATCH 23/31] tweak(debugapi): sticky header --- ui/lib/apps/DebugAPI/apilist/ApiList.tsx | 65 ++++++++++++++---------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/ui/lib/apps/DebugAPI/apilist/ApiList.tsx b/ui/lib/apps/DebugAPI/apilist/ApiList.tsx index 7d9f9d5153..05d2726bb6 100644 --- a/ui/lib/apps/DebugAPI/apilist/ApiList.tsx +++ b/ui/lib/apps/DebugAPI/apilist/ApiList.tsx @@ -4,6 +4,8 @@ import { useTranslation } from 'react-i18next' import { TFunction } from 'i18next' import { SearchOutlined } from '@ant-design/icons' import { debounce } from 'lodash' +import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' +import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky' import { AnimatedSkeleton, Card, Root } from '@lib/components' import { useClientRequest } from '@lib/utils/useClientRequest' @@ -99,6 +101,7 @@ export default function Page() { @@ -133,33 +136,41 @@ export default function Page() { return ( - - - - - } - onChange={(e) => filterBy(e.target.value)} - /> - - - - {endpoints.length ? ( - sortedGroups.map((g) => ( - - )) - ) : ( - - )} - - + + +
+ + + + } + onChange={(e) => filterBy(e.target.value)} + /> + + +
+
+ + + {endpoints.length ? ( + sortedGroups.map((g) => ( + + )) + ) : ( + + )} + + +
) } From a5915d17cd334b51f989f80d3aa8697285ce0ec2 Mon Sep 17 00:00:00 2001 From: Suhaha Date: Thu, 27 May 2021 14:07:34 +0800 Subject: [PATCH 24/31] fix(debugapi): search endpoint name --- ui/lib/apps/DebugAPI/apilist/ApiList.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/ui/lib/apps/DebugAPI/apilist/ApiList.tsx b/ui/lib/apps/DebugAPI/apilist/ApiList.tsx index 05d2726bb6..ac7d8eefb9 100644 --- a/ui/lib/apps/DebugAPI/apilist/ApiList.tsx +++ b/ui/lib/apps/DebugAPI/apilist/ApiList.tsx @@ -15,23 +15,33 @@ import style from './ApiList.module.less' import ApiForm, { Topology } from './ApiForm' import { buildQueryString } from './widgets' +const getEndpointTranslationKey = (endpoint: EndpointAPIModel) => + `debug_api.${endpoint.component}.endpoints.${endpoint.id}` + const useFilterEndpoints = (endpoints?: EndpointAPIModel[]) => { const [keywords, setKeywords] = useState('') const nonNullEndpoints = useMemo(() => endpoints || [], [endpoints]) const [filteredEndpoints, setFilteredEndpoints] = useState< EndpointAPIModel[] >(nonNullEndpoints) + const { t } = useTranslation() useEffect(() => { const k = keywords.trim() if (!!k) { setFilteredEndpoints( - nonNullEndpoints.filter((e) => e.id?.includes(k) || e.path?.includes(k)) + nonNullEndpoints.filter((e) => { + return ( + e.id?.includes(k) || + e.path?.includes(k) || + t(getEndpointTranslationKey(e)).includes(k) + ) + }) ) } else { setFilteredEndpoints(nonNullEndpoints) } - }, [nonNullEndpoints, keywords]) + }, [nonNullEndpoints, keywords, t]) return { endpoints: filteredEndpoints, @@ -189,9 +199,7 @@ function CustomHeader({
-

- {t(`debug_api.${endpoint.component}.endpoints.${endpoint.id}`)} -

+

{t(getEndpointTranslationKey(endpoint))}

From fb7eafde829457d96271e0e1767620a5f96fa296 Mon Sep 17 00:00:00 2001 From: Breezewish Date: Fri, 28 May 2021 01:58:16 +0800 Subject: [PATCH 25/31] Grumble --- pkg/apiserver/debugapi/client.go | 32 ++++++++++++++++++--- pkg/apiserver/debugapi/endpoint/endpoint.go | 13 ++++----- pkg/apiserver/debugapi/service.go | 6 ++-- pkg/pd/client.go | 12 ++++---- pkg/tidb/client.go | 9 +++--- pkg/tiflash/client.go | 12 ++++---- pkg/tikv/client.go | 12 ++++---- ui/lib/apps/DebugAPI/apilist/ApiForm.tsx | 2 +- ui/lib/apps/DebugAPI/apilist/ApiList.tsx | 29 +++++++++---------- ui/lib/apps/DebugAPI/translations/en.yaml | 2 ++ ui/lib/apps/DebugAPI/translations/zh.yaml | 4 +++ 11 files changed, 81 insertions(+), 52 deletions(-) diff --git a/pkg/apiserver/debugapi/client.go b/pkg/apiserver/debugapi/client.go index 644c99ed24..4a1514a86d 100644 --- a/pkg/apiserver/debugapi/client.go +++ b/pkg/apiserver/debugapi/client.go @@ -15,6 +15,7 @@ package debugapi import ( "fmt" + "time" "go.uber.org/fx" @@ -27,6 +28,10 @@ import ( "github.com/pingcap/tidb-dashboard/pkg/tikv" ) +const ( + defaultTimeout = time.Second * 45 // Default profiling can be as long as 30s. +) + type Client interface { Send(request *endpoint.Request) (*httpc.Response, error) Get(request *endpoint.Request) (*httpc.Response, error) @@ -53,13 +58,24 @@ func defaultSendRequest(client Client, req *endpoint.Request) (*httpc.Response, } } +func buildRelativeUri(path string, query string) string { + if len(query) == 0 { + return path + } else { + return fmt.Sprintf("%s?%s", path, query) + } +} + type tidbImplement struct { fx.In Client *tidb.Client } func (impl *tidbImplement) Get(req *endpoint.Request) (*httpc.Response, error) { - return impl.Client.WithEnforcedStatusAPIAddress(req.Host, req.Port).Get(req.Path) + return impl.Client. + WithEnforcedStatusAPIAddress(req.Host, req.Port). + WithStatusAPITimeout(defaultTimeout). + Get(buildRelativeUri(req.Path, req.Query)) } func (impl *tidbImplement) Send(req *endpoint.Request) (*httpc.Response, error) { @@ -72,9 +88,12 @@ type tikvImplement struct { } func (impl *tikvImplement) Get(req *endpoint.Request) (*httpc.Response, error) { - return impl.Client.Get(req.Host, req.Port, req.Path) + return impl.Client. + WithTimeout(defaultTimeout). + Get(req.Host, req.Port, buildRelativeUri(req.Path, req.Query)) } +// FIXME: Deduplicate default implementation. func (impl *tikvImplement) Send(req *endpoint.Request) (*httpc.Response, error) { return defaultSendRequest(impl, req) } @@ -85,7 +104,9 @@ type tiflashImplement struct { } func (impl *tiflashImplement) Get(req *endpoint.Request) (*httpc.Response, error) { - return impl.Client.Get(req.Host, req.Port, req.Path) + return impl.Client. + WithTimeout(defaultTimeout). + Get(req.Host, req.Port, buildRelativeUri(req.Path, req.Query)) } func (impl *tiflashImplement) Send(req *endpoint.Request) (*httpc.Response, error) { @@ -98,7 +119,10 @@ type pdImplement struct { } func (impl *pdImplement) Get(req *endpoint.Request) (*httpc.Response, error) { - return impl.Client.WithAddress(req.Host, req.Port).Get(req.Path) + return impl.Client. + WithAddress(req.Host, req.Port). + WithTimeout(defaultTimeout). + Get(buildRelativeUri(req.Path, req.Query)) } func (impl *pdImplement) Send(req *endpoint.Request) (*httpc.Response, error) { diff --git a/pkg/apiserver/debugapi/endpoint/endpoint.go b/pkg/apiserver/debugapi/endpoint/endpoint.go index 185462d5c9..651c43a8fb 100644 --- a/pkg/apiserver/debugapi/endpoint/endpoint.go +++ b/pkg/apiserver/debugapi/endpoint/endpoint.go @@ -19,7 +19,6 @@ import ( "regexp" "github.com/joomcode/errorx" - "github.com/pingcap/tidb-dashboard/pkg/apiserver/model" ) @@ -70,28 +69,28 @@ const ( MethodGet Method = http.MethodGet ) -func (e *APIModel) NewRequest(host string, port int, data map[string]string) (*Request, error) { +func (m *APIModel) NewRequest(host string, port int, data map[string]string) (*Request, error) { req := &Request{ - Method: e.Method, + Method: m.Method, Host: host, Port: port, } - pathValues, err := transformValues(e.PathParams, data, true) + pathValues, err := transformValues(m.PathParams, data, true) if err != nil { return nil, err } - path, err := populatePath(e.Path, pathValues) + path, err := populatePath(m.Path, pathValues) if err != nil { return nil, err } req.Path = path - queryValues, err := transformValues(e.QueryParams, data, false) + queryValues, err := transformValues(m.QueryParams, data, false) if err != nil { return nil, err } - query, err := encodeQuery(e.QueryParams, queryValues) + query, err := encodeQuery(m.QueryParams, queryValues) if err != nil { return nil, err } diff --git a/pkg/apiserver/debugapi/service.go b/pkg/apiserver/debugapi/service.go index e52535046e..d72bc113c1 100644 --- a/pkg/apiserver/debugapi/service.go +++ b/pkg/apiserver/debugapi/service.go @@ -84,18 +84,18 @@ func (s *Service) RequestEndpoint(c *gin.Context) { return } - endpoint, ok := s.endpointMap[req.ID] + ep, ok := s.endpointMap[req.ID] if !ok { _ = c.Error(ErrEndpointConfig.New("invalid endpoint id: %s", req.ID)) return } - endpointReq, err := endpoint.NewRequest(req.Host, req.Port, req.Params) + endpointReq, err := ep.NewRequest(req.Host, req.Port, req.Params) if err != nil { _ = c.Error(err) return } - res, err := endpoint.Client.Send(endpointReq) + res, err := ep.Client.Send(endpointReq) if err != nil { _ = c.Error(err) return diff --git a/pkg/pd/client.go b/pkg/pd/client.go index 59b621f1cc..31fb03130b 100644 --- a/pkg/pd/client.go +++ b/pkg/pd/client.go @@ -81,20 +81,20 @@ func (c Client) WithBeforeRequest(callback func(req *http.Request)) *Client { return &c } -func (c *Client) Get(path string) (*httpc.Response, error) { - uri := fmt.Sprintf("%s/pd/api/v1%s", c.baseURL, path) +func (c *Client) Get(relativeUri string) (*httpc.Response, error) { + uri := fmt.Sprintf("%s/pd/api/v1%s", c.baseURL, relativeUri) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrPDClientRequestFailed, "PD") } -func (c *Client) SendGetRequest(path string) ([]byte, error) { - res, err := c.Get(path) +func (c *Client) SendGetRequest(relativeUri string) ([]byte, error) { + res, err := c.Get(relativeUri) if err != nil { return nil, err } return res.Body() } -func (c *Client) SendPostRequest(path string, body io.Reader) ([]byte, error) { - uri := fmt.Sprintf("%s/pd/api/v1%s", c.baseURL, path) +func (c *Client) SendPostRequest(relativeUri string, body io.Reader) ([]byte, error) { + uri := fmt.Sprintf("%s/pd/api/v1%s", c.baseURL, relativeUri) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrPDClientRequestFailed, "PD") } diff --git a/pkg/tidb/client.go b/pkg/tidb/client.go index b1ae81af56..ad762d8135 100644 --- a/pkg/tidb/client.go +++ b/pkg/tidb/client.go @@ -159,7 +159,7 @@ func (c *Client) OpenSQLConn(user string, pass string) (*gorm.DB, error) { return db, nil } -func (c *Client) Get(path string) (*httpc.Response, error) { +func (c *Client) Get(relativeUri string) (*httpc.Response, error) { var err error overrideEndpoint := os.Getenv(tidbOverrideStatusEndpointEnvVar) @@ -184,7 +184,7 @@ func (c *Client) Get(path string) (*httpc.Response, error) { } } - uri := fmt.Sprintf("%s://%s%s", c.statusAPIHTTPScheme, addr, path) + uri := fmt.Sprintf("%s://%s%s", c.statusAPIHTTPScheme, addr, relativeUri) res, err := c.statusAPIHTTPClient. WithTimeout(c.statusAPITimeout). Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrTiDBClientRequestFailed, "TiDB") @@ -194,8 +194,9 @@ func (c *Client) Get(path string) (*httpc.Response, error) { return res, err } -func (c *Client) SendGetRequest(path string) ([]byte, error) { - res, err := c.Get(path) +// FIXME: SendGetRequest should be extracted, as a common method. +func (c *Client) SendGetRequest(relativeUri string) ([]byte, error) { + res, err := c.Get(relativeUri) if err != nil { return nil, err } diff --git a/pkg/tiflash/client.go b/pkg/tiflash/client.go index 99b8df6b34..e888931e68 100644 --- a/pkg/tiflash/client.go +++ b/pkg/tiflash/client.go @@ -64,20 +64,20 @@ func (c Client) WithTimeout(timeout time.Duration) *Client { return &c } -func (c *Client) Get(host string, statusPort int, path string) (*httpc.Response, error) { - uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, path) +func (c *Client) Get(host string, statusPort int, relativeUri string) (*httpc.Response, error) { + uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, relativeUri) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrFlashClientRequestFailed, "TiFlash") } -func (c *Client) SendGetRequest(host string, statusPort int, path string) ([]byte, error) { - res, err := c.Get(host, statusPort, path) +func (c *Client) SendGetRequest(host string, statusPort int, relativeUri string) ([]byte, error) { + res, err := c.Get(host, statusPort, relativeUri) if err != nil { return nil, err } return res.Body() } -func (c *Client) SendPostRequest(host string, statusPort int, path string, body io.Reader) ([]byte, error) { - uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, path) +func (c *Client) SendPostRequest(host string, statusPort int, relativeUri string, body io.Reader) ([]byte, error) { + uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, relativeUri) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrFlashClientRequestFailed, "TiFlash") } diff --git a/pkg/tikv/client.go b/pkg/tikv/client.go index 0aa7158449..b8d2808793 100644 --- a/pkg/tikv/client.go +++ b/pkg/tikv/client.go @@ -64,20 +64,20 @@ func (c Client) WithTimeout(timeout time.Duration) *Client { return &c } -func (c *Client) Get(host string, statusPort int, path string) (*httpc.Response, error) { - uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, path) +func (c *Client) Get(host string, statusPort int, relativeUri string) (*httpc.Response, error) { + uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, relativeUri) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrTiKVClientRequestFailed, "TiKV") } -func (c *Client) SendGetRequest(host string, statusPort int, path string) ([]byte, error) { - res, err := c.Get(host, statusPort, path) +func (c *Client) SendGetRequest(host string, statusPort int, relativeUri string) ([]byte, error) { + res, err := c.Get(host, statusPort, relativeUri) if err != nil { return nil, err } return res.Body() } -func (c *Client) SendPostRequest(host string, statusPort int, path string, body io.Reader) ([]byte, error) { - uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, path) +func (c *Client) SendPostRequest(host string, statusPort int, relativeUri string, body io.Reader) ([]byte, error) { + uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, relativeUri) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrTiKVClientRequestFailed, "TiKV") } diff --git a/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx b/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx index 50352410e0..d9f392897f 100644 --- a/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx +++ b/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx @@ -99,7 +99,7 @@ export default function ApiForm({ endpoint={endpoint} param={endpointParam} topology={topology} - > + /> ) return ( diff --git a/ui/lib/apps/DebugAPI/apilist/ApiList.tsx b/ui/lib/apps/DebugAPI/apilist/ApiList.tsx index ac7d8eefb9..ea81311b19 100644 --- a/ui/lib/apps/DebugAPI/apilist/ApiList.tsx +++ b/ui/lib/apps/DebugAPI/apilist/ApiList.tsx @@ -129,7 +129,7 @@ export default function Page() { > {descExists && ( + + +
- - - } - onChange={(e) => filterBy(e.target.value)} - /> - + } + onChange={(e) => filterBy(e.target.value)} + />
diff --git a/ui/lib/apps/DebugAPI/translations/en.yaml b/ui/lib/apps/DebugAPI/translations/en.yaml index 4dd9ff8927..04a92b1f66 100644 --- a/ui/lib/apps/DebugAPI/translations/en.yaml +++ b/ui/lib/apps/DebugAPI/translations/en.yaml @@ -36,6 +36,7 @@ debug_api: tidb_table_regions: Region - by Database + Table tidb_hot_regions: Hot Regions tidb_pprof: TiDB pprof + tidb_pprof_desc: The `seconds` parameter is only effective to `kind=profile` and `kind=trace`. pd: name: PD endpoints: @@ -67,3 +68,4 @@ debug_api: pd_stores: Stores - All (pd-ctl store) pd_store_id: Store - by StoreID (pd-ctl store [id]) pd_pprof: PD pprof + pd_pprof_desc: The `seconds` parameter is only effective to `kind=profile` and `kind=trace`. diff --git a/ui/lib/apps/DebugAPI/translations/zh.yaml b/ui/lib/apps/DebugAPI/translations/zh.yaml index ac60bc5e52..fc5b37425b 100644 --- a/ui/lib/apps/DebugAPI/translations/zh.yaml +++ b/ui/lib/apps/DebugAPI/translations/zh.yaml @@ -20,3 +20,7 @@ debug_api: tidb: endpoints: tidb_stats_dump_timestamp_desc: 时间戳应当在 GC Safe Point 以后 + tidb_pprof_desc: seconds 参数仅对 kind=profile 和 kind=trace 生效 + pd: + endpoints: + pd_pprof_desc: seconds 参数仅对 kind=profile 和 kind=trace 生效 From 5673812939ab1590c26d6e29df8afd91c725e623 Mon Sep 17 00:00:00 2001 From: Breezewish Date: Fri, 28 May 2021 02:02:24 +0800 Subject: [PATCH 26/31] Uri -> URI to make linter happy --- pkg/apiserver/debugapi/client.go | 10 +++++----- pkg/pd/client.go | 12 ++++++------ pkg/tidb/client.go | 8 ++++---- pkg/tiflash/client.go | 12 ++++++------ pkg/tikv/client.go | 12 ++++++------ 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/pkg/apiserver/debugapi/client.go b/pkg/apiserver/debugapi/client.go index 4a1514a86d..afc2457210 100644 --- a/pkg/apiserver/debugapi/client.go +++ b/pkg/apiserver/debugapi/client.go @@ -58,7 +58,7 @@ func defaultSendRequest(client Client, req *endpoint.Request) (*httpc.Response, } } -func buildRelativeUri(path string, query string) string { +func buildRelativeURI(path string, query string) string { if len(query) == 0 { return path } else { @@ -75,7 +75,7 @@ func (impl *tidbImplement) Get(req *endpoint.Request) (*httpc.Response, error) { return impl.Client. WithEnforcedStatusAPIAddress(req.Host, req.Port). WithStatusAPITimeout(defaultTimeout). - Get(buildRelativeUri(req.Path, req.Query)) + Get(buildRelativeURI(req.Path, req.Query)) } func (impl *tidbImplement) Send(req *endpoint.Request) (*httpc.Response, error) { @@ -90,7 +90,7 @@ type tikvImplement struct { func (impl *tikvImplement) Get(req *endpoint.Request) (*httpc.Response, error) { return impl.Client. WithTimeout(defaultTimeout). - Get(req.Host, req.Port, buildRelativeUri(req.Path, req.Query)) + Get(req.Host, req.Port, buildRelativeURI(req.Path, req.Query)) } // FIXME: Deduplicate default implementation. @@ -106,7 +106,7 @@ type tiflashImplement struct { func (impl *tiflashImplement) Get(req *endpoint.Request) (*httpc.Response, error) { return impl.Client. WithTimeout(defaultTimeout). - Get(req.Host, req.Port, buildRelativeUri(req.Path, req.Query)) + Get(req.Host, req.Port, buildRelativeURI(req.Path, req.Query)) } func (impl *tiflashImplement) Send(req *endpoint.Request) (*httpc.Response, error) { @@ -122,7 +122,7 @@ func (impl *pdImplement) Get(req *endpoint.Request) (*httpc.Response, error) { return impl.Client. WithAddress(req.Host, req.Port). WithTimeout(defaultTimeout). - Get(buildRelativeUri(req.Path, req.Query)) + Get(buildRelativeURI(req.Path, req.Query)) } func (impl *pdImplement) Send(req *endpoint.Request) (*httpc.Response, error) { diff --git a/pkg/pd/client.go b/pkg/pd/client.go index 31fb03130b..e600ed1a5d 100644 --- a/pkg/pd/client.go +++ b/pkg/pd/client.go @@ -81,20 +81,20 @@ func (c Client) WithBeforeRequest(callback func(req *http.Request)) *Client { return &c } -func (c *Client) Get(relativeUri string) (*httpc.Response, error) { - uri := fmt.Sprintf("%s/pd/api/v1%s", c.baseURL, relativeUri) +func (c *Client) Get(relativeURI string) (*httpc.Response, error) { + uri := fmt.Sprintf("%s/pd/api/v1%s", c.baseURL, relativeURI) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrPDClientRequestFailed, "PD") } -func (c *Client) SendGetRequest(relativeUri string) ([]byte, error) { - res, err := c.Get(relativeUri) +func (c *Client) SendGetRequest(relativeURI string) ([]byte, error) { + res, err := c.Get(relativeURI) if err != nil { return nil, err } return res.Body() } -func (c *Client) SendPostRequest(relativeUri string, body io.Reader) ([]byte, error) { - uri := fmt.Sprintf("%s/pd/api/v1%s", c.baseURL, relativeUri) +func (c *Client) SendPostRequest(relativeURI string, body io.Reader) ([]byte, error) { + uri := fmt.Sprintf("%s/pd/api/v1%s", c.baseURL, relativeURI) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrPDClientRequestFailed, "PD") } diff --git a/pkg/tidb/client.go b/pkg/tidb/client.go index ad762d8135..856ef162a5 100644 --- a/pkg/tidb/client.go +++ b/pkg/tidb/client.go @@ -159,7 +159,7 @@ func (c *Client) OpenSQLConn(user string, pass string) (*gorm.DB, error) { return db, nil } -func (c *Client) Get(relativeUri string) (*httpc.Response, error) { +func (c *Client) Get(relativeURI string) (*httpc.Response, error) { var err error overrideEndpoint := os.Getenv(tidbOverrideStatusEndpointEnvVar) @@ -184,7 +184,7 @@ func (c *Client) Get(relativeUri string) (*httpc.Response, error) { } } - uri := fmt.Sprintf("%s://%s%s", c.statusAPIHTTPScheme, addr, relativeUri) + uri := fmt.Sprintf("%s://%s%s", c.statusAPIHTTPScheme, addr, relativeURI) res, err := c.statusAPIHTTPClient. WithTimeout(c.statusAPITimeout). Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrTiDBClientRequestFailed, "TiDB") @@ -195,8 +195,8 @@ func (c *Client) Get(relativeUri string) (*httpc.Response, error) { } // FIXME: SendGetRequest should be extracted, as a common method. -func (c *Client) SendGetRequest(relativeUri string) ([]byte, error) { - res, err := c.Get(relativeUri) +func (c *Client) SendGetRequest(relativeURI string) ([]byte, error) { + res, err := c.Get(relativeURI) if err != nil { return nil, err } diff --git a/pkg/tiflash/client.go b/pkg/tiflash/client.go index e888931e68..562c51b8b5 100644 --- a/pkg/tiflash/client.go +++ b/pkg/tiflash/client.go @@ -64,20 +64,20 @@ func (c Client) WithTimeout(timeout time.Duration) *Client { return &c } -func (c *Client) Get(host string, statusPort int, relativeUri string) (*httpc.Response, error) { - uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, relativeUri) +func (c *Client) Get(host string, statusPort int, relativeURI string) (*httpc.Response, error) { + uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, relativeURI) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrFlashClientRequestFailed, "TiFlash") } -func (c *Client) SendGetRequest(host string, statusPort int, relativeUri string) ([]byte, error) { - res, err := c.Get(host, statusPort, relativeUri) +func (c *Client) SendGetRequest(host string, statusPort int, relativeURI string) ([]byte, error) { + res, err := c.Get(host, statusPort, relativeURI) if err != nil { return nil, err } return res.Body() } -func (c *Client) SendPostRequest(host string, statusPort int, relativeUri string, body io.Reader) ([]byte, error) { - uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, relativeUri) +func (c *Client) SendPostRequest(host string, statusPort int, relativeURI string, body io.Reader) ([]byte, error) { + uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, relativeURI) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrFlashClientRequestFailed, "TiFlash") } diff --git a/pkg/tikv/client.go b/pkg/tikv/client.go index b8d2808793..f024db5cda 100644 --- a/pkg/tikv/client.go +++ b/pkg/tikv/client.go @@ -64,20 +64,20 @@ func (c Client) WithTimeout(timeout time.Duration) *Client { return &c } -func (c *Client) Get(host string, statusPort int, relativeUri string) (*httpc.Response, error) { - uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, relativeUri) +func (c *Client) Get(host string, statusPort int, relativeURI string) (*httpc.Response, error) { + uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, relativeURI) return c.httpClient.WithTimeout(c.timeout).Send(c.lifecycleCtx, uri, http.MethodGet, nil, ErrTiKVClientRequestFailed, "TiKV") } -func (c *Client) SendGetRequest(host string, statusPort int, relativeUri string) ([]byte, error) { - res, err := c.Get(host, statusPort, relativeUri) +func (c *Client) SendGetRequest(host string, statusPort int, relativeURI string) ([]byte, error) { + res, err := c.Get(host, statusPort, relativeURI) if err != nil { return nil, err } return res.Body() } -func (c *Client) SendPostRequest(host string, statusPort int, relativeUri string, body io.Reader) ([]byte, error) { - uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, relativeUri) +func (c *Client) SendPostRequest(host string, statusPort int, relativeURI string, body io.Reader) ([]byte, error) { + uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, relativeURI) return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, http.MethodPost, body, ErrTiKVClientRequestFailed, "TiKV") } From 65aba24c5e5abddbf782bd0ce7bb288bbc7fdc2a Mon Sep 17 00:00:00 2001 From: Breezewish Date: Fri, 28 May 2021 02:05:21 +0800 Subject: [PATCH 27/31] Make linter happy again --- pkg/apiserver/debugapi/client.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/apiserver/debugapi/client.go b/pkg/apiserver/debugapi/client.go index afc2457210..44ec363a9f 100644 --- a/pkg/apiserver/debugapi/client.go +++ b/pkg/apiserver/debugapi/client.go @@ -61,9 +61,8 @@ func defaultSendRequest(client Client, req *endpoint.Request) (*httpc.Response, func buildRelativeURI(path string, query string) string { if len(query) == 0 { return path - } else { - return fmt.Sprintf("%s?%s", path, query) } + return fmt.Sprintf("%s?%s", path, query) } type tidbImplement struct { From 29b40e14308b70702a180c42b3673e56183f4a0e Mon Sep 17 00:00:00 2001 From: Breezewish Date: Fri, 28 May 2021 02:20:19 +0800 Subject: [PATCH 28/31] Make linter happy again *2 --- pkg/apiserver/debugapi/endpoint/endpoint.go | 1 + pkg/apiserver/utils/export.go | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/apiserver/debugapi/endpoint/endpoint.go b/pkg/apiserver/debugapi/endpoint/endpoint.go index 651c43a8fb..6600abad21 100644 --- a/pkg/apiserver/debugapi/endpoint/endpoint.go +++ b/pkg/apiserver/debugapi/endpoint/endpoint.go @@ -19,6 +19,7 @@ import ( "regexp" "github.com/joomcode/errorx" + "github.com/pingcap/tidb-dashboard/pkg/apiserver/model" ) diff --git a/pkg/apiserver/utils/export.go b/pkg/apiserver/utils/export.go index 8af46905a9..f8f96904e6 100644 --- a/pkg/apiserver/utils/export.go +++ b/pkg/apiserver/utils/export.go @@ -8,7 +8,9 @@ import ( "io" "io/ioutil" "os" + "reflect" "strings" + "time" "github.com/Xeoncross/go-aesctr-with-hmac" "github.com/gin-gonic/gin" @@ -16,9 +18,6 @@ import ( "github.com/oleiade/reflections" "github.com/pingcap/log" "go.uber.org/zap" - - "reflect" - "time" ) func GenerateCSVFromRaw(rawData []interface{}, fields []string, timeFields []string) (data [][]string) { From e17434179dd292ef18ec67c8451bd53888fdfa18 Mon Sep 17 00:00:00 2001 From: Suhaha Date: Sun, 30 May 2021 23:12:29 +0800 Subject: [PATCH 29/31] fix(debugapi): profile format --- pkg/apiserver/debugapi/client.go | 20 +--------- .../debugapi/endpoint/endpoint_def.go | 24 ++++++++---- pkg/apiserver/debugapi/service.go | 2 +- ui/lib/apps/DebugAPI/apilist/ApiForm.tsx | 39 ++++++++++++------- ui/lib/apps/DebugAPI/apilist/file.ts | 12 +++--- ui/lib/client/index.tsx | 7 +++- 6 files changed, 55 insertions(+), 49 deletions(-) diff --git a/pkg/apiserver/debugapi/client.go b/pkg/apiserver/debugapi/client.go index 44ec363a9f..dfee0e12c2 100644 --- a/pkg/apiserver/debugapi/client.go +++ b/pkg/apiserver/debugapi/client.go @@ -33,7 +33,6 @@ const ( ) type Client interface { - Send(request *endpoint.Request) (*httpc.Response, error) Get(request *endpoint.Request) (*httpc.Response, error) } @@ -49,7 +48,7 @@ func newClientMap(tidbImpl tidbImplement, tikvImpl tikvImplement, tiflashImpl ti return &clientMap } -func defaultSendRequest(client Client, req *endpoint.Request) (*httpc.Response, error) { +func SendRequest(client Client, req *endpoint.Request) (*httpc.Response, error) { switch req.Method { case endpoint.MethodGet: return client.Get(req) @@ -77,10 +76,6 @@ func (impl *tidbImplement) Get(req *endpoint.Request) (*httpc.Response, error) { Get(buildRelativeURI(req.Path, req.Query)) } -func (impl *tidbImplement) Send(req *endpoint.Request) (*httpc.Response, error) { - return defaultSendRequest(impl, req) -} - type tikvImplement struct { fx.In Client *tikv.Client @@ -92,11 +87,6 @@ func (impl *tikvImplement) Get(req *endpoint.Request) (*httpc.Response, error) { Get(req.Host, req.Port, buildRelativeURI(req.Path, req.Query)) } -// FIXME: Deduplicate default implementation. -func (impl *tikvImplement) Send(req *endpoint.Request) (*httpc.Response, error) { - return defaultSendRequest(impl, req) -} - type tiflashImplement struct { fx.In Client *tiflash.Client @@ -108,10 +98,6 @@ func (impl *tiflashImplement) Get(req *endpoint.Request) (*httpc.Response, error Get(req.Host, req.Port, buildRelativeURI(req.Path, req.Query)) } -func (impl *tiflashImplement) Send(req *endpoint.Request) (*httpc.Response, error) { - return defaultSendRequest(impl, req) -} - type pdImplement struct { fx.In Client *pd.Client @@ -123,7 +109,3 @@ func (impl *pdImplement) Get(req *endpoint.Request) (*httpc.Response, error) { WithTimeout(defaultTimeout). Get(buildRelativeURI(req.Path, req.Query)) } - -func (impl *pdImplement) Send(req *endpoint.Request) (*httpc.Response, error) { - return defaultSendRequest(impl, req) -} diff --git a/pkg/apiserver/debugapi/endpoint/endpoint_def.go b/pkg/apiserver/debugapi/endpoint/endpoint_def.go index f855088a6d..03059c08d4 100644 --- a/pkg/apiserver/debugapi/endpoint/endpoint_def.go +++ b/pkg/apiserver/debugapi/endpoint/endpoint_def.go @@ -39,6 +39,20 @@ var pprofKindsParam = APIParam{ }), } +// TODO: After http client refactor. +// Recorvery the second options as same as profiling module or just not limit it. +// Now limit the seconds according to `defaultTimeout` in debugapi/client.go +var pprofSecondsParam = APIParam{ + Name: "seconds", + Model: CreateAPIParamModelEnum([]EnumItem{ + {Name: "10s", Value: "10"}, + {Name: "30s", Value: "30"}, + {Name: "45s", Value: "45"}, + // {Name: "60s", Value: "60"}, + // {Name: "120s", Value: "120"}, + }), +} + // tidb endpoints var tidbStatsDump = APIModel{ @@ -220,10 +234,7 @@ var tidbPprof = APIModel{ Name: "debug", Model: CreateAPIParamModelConstant("1"), }, - { - Name: "seconds", - Model: APIParamModelInt, - }, + pprofSecondsParam, }, } @@ -615,10 +626,7 @@ var pdPprof = APIModel{ Name: "debug", Model: CreateAPIParamModelConstant("1"), }, - { - Name: "seconds", - Model: APIParamModelInt, - }, + pprofSecondsParam, }, } diff --git a/pkg/apiserver/debugapi/service.go b/pkg/apiserver/debugapi/service.go index d72bc113c1..7a2e3fa4bf 100644 --- a/pkg/apiserver/debugapi/service.go +++ b/pkg/apiserver/debugapi/service.go @@ -95,7 +95,7 @@ func (s *Service) RequestEndpoint(c *gin.Context) { return } - res, err := ep.Client.Send(endpointReq) + res, err := SendRequest(ep.Client, endpointReq) if err != nil { _ = c.Error(err) return diff --git a/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx b/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx index d9f392897f..eddab74c28 100644 --- a/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx +++ b/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx @@ -16,7 +16,11 @@ import { createFormWidget, ParamModelType, } from './widgets' -import { isJSONContentType, download as downloadFile } from './file' +import { + isJSONContentType, + isBinaryContentType, + download as downloadFile, +} from './file' export interface Topology { tidb: TopologyTiDBInfo[] @@ -45,38 +49,43 @@ export default function ApiForm({ const download = useCallback( async (values: any) => { - let data: string - let headers: any + let data: Blob try { setLoading(true) const { [endpointHostParamKey]: host, ...p } = values const [hostname, port] = host.split(':') + // filter the null value params const params = Object.entries(p).reduce((prev, [k, v]) => { if (!(isUndefined(v) || isNull(v) || v === '')) { prev[k] = v } return prev }, {}) - const resp = await client.getInstance().debugapiRequestEndpointPost({ - id, - host: hostname, - port: Number(port), - params, - }) - data = resp.data - headers = resp.headers + const resp = await client.getInstance().debugapiRequestEndpointPost( + { + id, + host: hostname, + port: Number(port), + params, + }, + { + responseType: 'blob', + } + ) + data = (resp.data as unknown) as Blob } catch (e) { setLoading(false) console.error(e) return } - if (isJSONContentType(headers['content-type'])) { + if (isJSONContentType(data.type)) { // quick view backdoor - console.log(data) - data = JSON.stringify(data) + data.text().then((d) => console.log(d)) } - downloadFile(data, `${id}_${Date.now()}`, headers['content-type']) + isBinaryContentType(data.type) + ? downloadFile(data, `${id}_${Date.now()}`, 'pb.gz') + : downloadFile(data, `${id}_${Date.now()}`) setLoading(false) }, [id, endpointHostParamKey] diff --git a/ui/lib/apps/DebugAPI/apilist/file.ts b/ui/lib/apps/DebugAPI/apilist/file.ts index 04a79aef40..4ac0c183ef 100644 --- a/ui/lib/apps/DebugAPI/apilist/file.ts +++ b/ui/lib/apps/DebugAPI/apilist/file.ts @@ -4,17 +4,19 @@ export const isJSONContentType = (contentType: string) => { return mime.extension(contentType) === mime.extension(mime.lookup('json')) } +export const isBinaryContentType = (contentType: string) => { + return mime.extension(contentType) === mime.extension(mime.lookup('bin')) +} + export const download = ( - data: string, + data: Blob, fileName: string, - contentType: string, - ext: string = mime.extension(contentType) + ext: string = mime.extension(data.type) ) => { - const blob = new Blob([data], { type: contentType }) const link = document.createElement('a') const fileNameWithExt = `${fileName}.${ext}` - link.href = window.URL.createObjectURL(blob) + link.href = window.URL.createObjectURL(data) link.download = fileNameWithExt link.click() window.URL.revokeObjectURL(link.href) diff --git a/ui/lib/client/index.tsx b/ui/lib/client/index.tsx index da890fcdbb..a2e339e241 100644 --- a/ui/lib/client/index.tsx +++ b/ui/lib/client/index.tsx @@ -54,11 +54,16 @@ export enum ErrorStrategy { const ERR_CODE_OTHER = 'error.api.other' function applyErrorHandlerInterceptor(instance: AxiosInstance) { - instance.interceptors.response.use(undefined, function (err) { + instance.interceptors.response.use(undefined, async function (err) { const { response, config } = err const errorStrategy = config.errorStrategy as ErrorStrategy const method = (config.method as string).toLowerCase() + if (err.response.data instanceof Blob) { + const d = await err.response.data.text() + err.response.data = JSON.parse(d) + } + let errCode: string let content: string if (err.message === 'Network Error') { From da2d030dec7c7abc2ad80d216564cd9004e1f8d5 Mon Sep 17 00:00:00 2001 From: Suhaha Date: Mon, 31 May 2021 11:57:22 +0800 Subject: [PATCH 30/31] refine(debugapi): transform data by stream --- go.mod | 1 + go.sum | 3 + pkg/apiserver/debugapi/service.go | 93 ++++++++++---- .../utils/{exp.go => experimental.go} | 0 pkg/apiserver/utils/export.go | 3 + pkg/apiserver/utils/fs_stream.go | 115 ++++++++++++++++++ pkg/apiserver/utils/jwt.go | 4 +- pkg/httpc/client.go | 2 +- ui/lib/apps/DebugAPI/apilist/ApiForm.tsx | 40 ++---- ui/lib/apps/DebugAPI/apilist/ApiList.tsx | 2 +- ui/lib/apps/DebugAPI/apilist/file.ts | 23 ---- ui/lib/client/index.tsx | 5 - ui/package.json | 1 - ui/yarn.lock | 12 -- 14 files changed, 204 insertions(+), 100 deletions(-) rename pkg/apiserver/utils/{exp.go => experimental.go} (100%) create mode 100644 pkg/apiserver/utils/fs_stream.go delete mode 100644 ui/lib/apps/DebugAPI/apilist/file.ts diff --git a/go.mod b/go.mod index 2dbc85b4b4..8ea8d25caa 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69 github.com/joho/godotenv v1.3.0 github.com/joomcode/errorx v1.0.1 + github.com/minio/sio v0.3.0 github.com/oleiade/reflections v1.0.1 github.com/pingcap/check v0.0.0-20191216031241-8a5a85928f12 github.com/pingcap/errors v0.11.5-0.20200917111840-a15ef68f753d diff --git a/go.sum b/go.sum index 7ac680e024..ddaeff8f12 100644 --- a/go.sum +++ b/go.sum @@ -195,6 +195,8 @@ github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KK github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/minio/sio v0.3.0 h1:syEFBewzOMOYVzSTFpp1MqpSZk8rUNbz8VIIc+PNzus= +github.com/minio/sio v0.3.0/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebhw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -355,6 +357,7 @@ go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/pkg/apiserver/debugapi/service.go b/pkg/apiserver/debugapi/service.go index 7a2e3fa4bf..13e414bedb 100644 --- a/pkg/apiserver/debugapi/service.go +++ b/pkg/apiserver/debugapi/service.go @@ -14,29 +14,31 @@ package debugapi import ( + "fmt" + "io" + "mime" "net/http" + "time" "github.com/gin-gonic/gin" - "github.com/joomcode/errorx" "github.com/pingcap/tidb-dashboard/pkg/apiserver/debugapi/endpoint" "github.com/pingcap/tidb-dashboard/pkg/apiserver/user" "github.com/pingcap/tidb-dashboard/pkg/apiserver/utils" ) -var ( - ErrNS = errorx.NewNamespace("error.api.debugapi") - ErrComponentClient = ErrNS.NewType("invalid_component_client") - ErrEndpointConfig = ErrNS.NewType("invalid_endpoint_config") - ErrInvalidStatusPort = ErrNS.NewType("invalid_status_port") +const ( + tokenIssuer = "debugAPI" ) func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { - endpoint := r.Group("/debugapi") - endpoint.Use(auth.MWAuthRequired()) - - endpoint.POST("/request_endpoint", s.RequestEndpoint) - endpoint.GET("/endpoints", s.GetEndpointList) + ep := r.Group("/debug_api") + ep.GET("/download", s.Download) + { + ep.Use(auth.MWAuthRequired()) + ep.GET("/endpoints", s.GetEndpoints) + ep.POST("/endpoint", s.RequestEndpoint) + } } type endpointModel struct { @@ -54,7 +56,7 @@ func newService(clientMap *ClientMap) (*Service, error) { for _, e := range endpoint.APIListDef { client, ok := (*clientMap)[e.Component] if !ok { - panic(ErrComponentClient.New("%s type client not found, id: %s", e.Component, e.ID)) + panic(fmt.Sprintf("%s type client not found, id: %s", e.Component, e.ID)) } s.endpointMap[e.ID] = endpointModel{APIModel: e, Client: client} } @@ -62,23 +64,38 @@ func newService(clientMap *ClientMap) (*Service, error) { return s, nil } -type EndpointRequest struct { +type RequestPayload struct { ID string `json:"id"` Host string `json:"host"` Port int `json:"port"` Params map[string]string `json:"params"` } -// @Summary RequestEndpoint send request to tidb/tikv/tiflash/pd http api +func getExtFromContentTypeHeader(contentType string) string { + mediaType, _, err := mime.ParseMediaType(contentType) + if err != nil || len(mediaType) == 0 { + return ".bin" + } + + exts, err := mime.ExtensionsByType(mediaType) + if err == nil && len(exts) > 0 { + return exts[0] + } + + return ".bin" +} + +// @Summary Send request remote endpoint and return a token for downloading results // @Security JwtAuth -// @Param req body EndpointRequest true "endpoint request param" +// @ID debugAPIRequestEndpoint +// @Param req body RequestPayload true "request payload" // @Success 200 {object} string // @Failure 400 {object} utils.APIError "Bad request" // @Failure 401 {object} utils.APIError "Unauthorized failure" // @Failure 500 {object} utils.APIError -// @Router /debugapi/request_endpoint [post] +// @Router /debug_api/endpoint [post] func (s *Service) RequestEndpoint(c *gin.Context) { - var req EndpointRequest + var req RequestPayload if err := c.ShouldBindJSON(&req); err != nil { utils.MakeInvalidRequestErrorFromError(c, err) return @@ -86,7 +103,7 @@ func (s *Service) RequestEndpoint(c *gin.Context) { ep, ok := s.endpointMap[req.ID] if !ok { - _ = c.Error(ErrEndpointConfig.New("invalid endpoint id: %s", req.ID)) + utils.MakeInvalidRequestErrorWithMessage(c, "Invalid endpoint id: %s", req.ID) return } endpointReq, err := ep.NewRequest(req.Host, req.Port, req.Params) @@ -100,22 +117,48 @@ func (s *Service) RequestEndpoint(c *gin.Context) { _ = c.Error(err) return } - body, err := res.Body() + defer res.Response.Body.Close() //nolint:errcheck + + ext := getExtFromContentTypeHeader(res.Header.Get("Content-Type")) + fileName := fmt.Sprintf("%s_%d%s", req.ID, time.Now().Unix(), ext) + + writer, token, err := utils.FSPersist(utils.FSPersistConfig{ + TokenIssuer: tokenIssuer, + TokenExpire: time.Minute * 5, // Note: the expire time should include request time. + TempFilePattern: "debug_api", + DownloadFileName: fileName, + }) if err != nil { _ = c.Error(err) return } + defer writer.Close() //nolint:errcheck + _, err = io.Copy(writer, res.Response.Body) + if err != nil { + _ = c.Error(err) + return + } + + c.String(http.StatusOK, token) +} - c.Data(200, res.Header.Get("Content-Type"), body) +// @Summary Download a finished request result. +// @Param token query string true "download token" +// @Success 200 {object} string +// @Failure 400 {object} utils.APIError "Bad request" +// @Failure 500 {object} utils.APIError +// @Router /debug_api/download [get] +func (s *Service) Download(c *gin.Context) { + token := c.Query("token") + utils.FSServe(c, token, tokenIssuer) } -// @Summary Get all endpoint configs +// @Summary Get all endpoints +// @ID debugAPIGetEndpoints // @Security JwtAuth // @Success 200 {array} endpoint.APIModel -// @Failure 400 {object} utils.APIError "Bad request" // @Failure 401 {object} utils.APIError "Unauthorized failure" -// @Failure 500 {object} utils.APIError -// @Router /debugapi/endpoints [get] -func (s *Service) GetEndpointList(c *gin.Context) { +// @Router /debug_api/endpoints [get] +func (s *Service) GetEndpoints(c *gin.Context) { c.JSON(http.StatusOK, endpoint.APIListDef) } diff --git a/pkg/apiserver/utils/exp.go b/pkg/apiserver/utils/experimental.go similarity index 100% rename from pkg/apiserver/utils/exp.go rename to pkg/apiserver/utils/experimental.go diff --git a/pkg/apiserver/utils/export.go b/pkg/apiserver/utils/export.go index f8f96904e6..e7ba1d451f 100644 --- a/pkg/apiserver/utils/export.go +++ b/pkg/apiserver/utils/export.go @@ -20,6 +20,7 @@ import ( "go.uber.org/zap" ) +// TODO: Better to be a streaming interface. func GenerateCSVFromRaw(rawData []interface{}, fields []string, timeFields []string) (data [][]string) { timeFieldsMap := make(map[string]struct{}) for _, f := range timeFields { @@ -68,6 +69,7 @@ func GenerateCSVFromRaw(rawData []interface{}, fields []string, timeFields []str return } +// TODO: Better to be a streaming interface. func ExportCSV(data [][]string, filename, tokenNamespace string) (token string, err error) { csvFile, err := ioutil.TempFile("", filename) if err != nil { @@ -95,6 +97,7 @@ func ExportCSV(data [][]string, filename, tokenNamespace string) (token string, return } +// FIXME: Remove or refine this function, as it is not general. func DownloadByToken(token, tokenNamespace string, c *gin.Context) { tokenPlain, err := ParseJWTString(tokenNamespace, token) if err != nil { diff --git a/pkg/apiserver/utils/fs_stream.go b/pkg/apiserver/utils/fs_stream.go new file mode 100644 index 0000000000..0169af9b56 --- /dev/null +++ b/pkg/apiserver/utils/fs_stream.go @@ -0,0 +1,115 @@ +package utils + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "time" + + "github.com/gin-gonic/gin" + "github.com/gtank/cryptopasta" + "github.com/minio/sio" +) + +type FSPersistConfig struct { + TokenIssuer string + TokenExpire time.Duration + TempFilePattern string + DownloadFileName string +} + +type FSPersistTokenBody struct { + TempFileName string + TempFileKeyInHex string + DownloadFileName string +} + +// FSPersist returns a writer and corresponding download token that will persist a stream in the FS in an encrypted way. +func FSPersist(config FSPersistConfig) (io.WriteCloser, string, error) { + file, err := ioutil.TempFile("", config.TempFilePattern) + if err != nil { + return nil, "", err + } + + key := cryptopasta.NewEncryptionKey()[:] + w, err := sio.EncryptWriter(file, sio.Config{ + Key: key, + }) + if err != nil { + _ = file.Close() + _ = os.Remove(file.Name()) + return nil, "", err + } + + keyInHex := hex.EncodeToString(key) + tokenBody := FSPersistTokenBody{ + TempFileName: file.Name(), + TempFileKeyInHex: keyInHex, + DownloadFileName: config.DownloadFileName, + } + tokenBodyStr, err := json.Marshal(tokenBody) + if err != nil { + _ = file.Close() + _ = os.Remove(file.Name()) + return nil, "", err + } + // TODO: Maybe better to generate the token after `w.Close()`. + token, err := NewJWTStringWithExpire(config.TokenIssuer, string(tokenBodyStr), config.TokenExpire) + if err != nil { + _ = file.Close() + _ = os.Remove(file.Name()) + return nil, "", err + } + + // Note: we intentionally keep the temp file not removed, so that it can be downloaded later. + return w, token, nil +} + +// FSServe serves the persisted file in the FS to the user by using the download token. +// The file will be removed after it is served to the user. +func FSServe(c *gin.Context, token string, requiredIssuer string) { + str, err := ParseJWTString(requiredIssuer, token) + if err != nil { + MakeInvalidRequestErrorWithMessage(c, "Invalid download request: %s.", err.Error()) + return + } + var tokenBody FSPersistTokenBody + err = json.Unmarshal([]byte(str), &tokenBody) + if err != nil { + _ = c.Error(err) + return + } + + file, err := os.Open(tokenBody.TempFileName) + if err != nil { + if os.IsNotExist(err) { + // It is possible that token is reused. In this case, raise invalid request error. + MakeInvalidRequestErrorWithMessage(c, "Download file not found: %s.", err.Error()) + } else { + _ = c.Error(err) + } + return + } + defer file.Close() //nolint:errcheck + defer os.Remove(tokenBody.TempFileName) //nolint:errcheck + + key, err := hex.DecodeString(tokenBody.TempFileKeyInHex) + if err != nil { + _ = c.Error(err) + return + } + + c.Writer.Header().Set("Content-type", "application/octet-stream") + c.Writer.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, tokenBody.DownloadFileName)) + + _, err = sio.Decrypt(c.Writer, file, sio.Config{ + Key: key, + }) + if err != nil { + _ = c.Error(err) + return + } +} diff --git a/pkg/apiserver/utils/jwt.go b/pkg/apiserver/utils/jwt.go index b84a908196..86cc3466cb 100644 --- a/pkg/apiserver/utils/jwt.go +++ b/pkg/apiserver/utils/jwt.go @@ -64,10 +64,10 @@ func ParseJWTString(requiredIssuer string, tokenStr string) (string, error) { return "", err } if !token.Valid { - return "", fmt.Errorf("token is invalid") + return "", fmt.Errorf("token is invalid or expired") } if claims.Issuer != requiredIssuer { - return "", fmt.Errorf("invalid issuer") + return "", fmt.Errorf("token is invalid (invalid issuer)") } return claims.Data, nil } diff --git a/pkg/httpc/client.go b/pkg/httpc/client.go index 0ca59ffaef..7424f08e51 100644 --- a/pkg/httpc/client.go +++ b/pkg/httpc/client.go @@ -98,7 +98,7 @@ func (c *Client) Send( req, err := http.NewRequestWithContext(ctx, method, uri, body) if err != nil { e := errType.Wrap(err, "Failed to build %s API request", errOriginComponent) - log.Warn("SendRequest failed", zap.String("uri", uri), zap.Error(e)) + log.Warn("SendRequest failed", zap.String("uri", uri), zap.Error(err)) return nil, e } diff --git a/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx b/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx index eddab74c28..18acf4edea 100644 --- a/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx +++ b/ui/lib/apps/DebugAPI/apilist/ApiForm.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next' import { Form, Button, Space, Row, Col } from 'antd' import { isNull, isUndefined } from 'lodash' import { DownloadOutlined, UndoOutlined } from '@ant-design/icons' - import client, { EndpointAPIModel, EndpointAPIParam, @@ -16,11 +15,6 @@ import { createFormWidget, ParamModelType, } from './widgets' -import { - isJSONContentType, - isBinaryContentType, - download as downloadFile, -} from './file' export interface Topology { tidb: TopologyTiDBInfo[] @@ -49,7 +43,6 @@ export default function ApiForm({ const download = useCallback( async (values: any) => { - let data: Blob try { setLoading(true) const { [endpointHostParamKey]: host, ...p } = values @@ -61,32 +54,19 @@ export default function ApiForm({ } return prev }, {}) - const resp = await client.getInstance().debugapiRequestEndpointPost( - { - id, - host: hostname, - port: Number(port), - params, - }, - { - responseType: 'blob', - } - ) - data = (resp.data as unknown) as Blob + const resp = await client.getInstance().debugAPIRequestEndpoint({ + id, + host: hostname, + port: Number(port), + params, + }) + const token = resp.data + window.location.href = `${client.getBasePath()}/debug_api/download?token=${token}` } catch (e) { - setLoading(false) console.error(e) - return - } - - if (isJSONContentType(data.type)) { - // quick view backdoor - data.text().then((d) => console.log(d)) + } finally { + setLoading(false) } - isBinaryContentType(data.type) - ? downloadFile(data, `${id}_${Date.now()}`, 'pb.gz') - : downloadFile(data, `${id}_${Date.now()}`) - setLoading(false) }, [id, endpointHostParamKey] ) diff --git a/ui/lib/apps/DebugAPI/apilist/ApiList.tsx b/ui/lib/apps/DebugAPI/apilist/ApiList.tsx index ea81311b19..b62111e30f 100644 --- a/ui/lib/apps/DebugAPI/apilist/ApiList.tsx +++ b/ui/lib/apps/DebugAPI/apilist/ApiList.tsx @@ -55,7 +55,7 @@ export default function Page() { data: endpointData, isLoading: isEndpointLoading, } = useClientRequest((reqConfig) => - client.getInstance().debugapiEndpointsGet(reqConfig) + client.getInstance().debugAPIGetEndpoints(reqConfig) ) const { endpoints, filterBy } = useFilterEndpoints(endpointData) diff --git a/ui/lib/apps/DebugAPI/apilist/file.ts b/ui/lib/apps/DebugAPI/apilist/file.ts deleted file mode 100644 index 4ac0c183ef..0000000000 --- a/ui/lib/apps/DebugAPI/apilist/file.ts +++ /dev/null @@ -1,23 +0,0 @@ -import mime from 'mime-types' - -export const isJSONContentType = (contentType: string) => { - return mime.extension(contentType) === mime.extension(mime.lookup('json')) -} - -export const isBinaryContentType = (contentType: string) => { - return mime.extension(contentType) === mime.extension(mime.lookup('bin')) -} - -export const download = ( - data: Blob, - fileName: string, - ext: string = mime.extension(data.type) -) => { - const link = document.createElement('a') - const fileNameWithExt = `${fileName}.${ext}` - - link.href = window.URL.createObjectURL(data) - link.download = fileNameWithExt - link.click() - window.URL.revokeObjectURL(link.href) -} diff --git a/ui/lib/client/index.tsx b/ui/lib/client/index.tsx index a2e339e241..9dce0abfea 100644 --- a/ui/lib/client/index.tsx +++ b/ui/lib/client/index.tsx @@ -59,11 +59,6 @@ function applyErrorHandlerInterceptor(instance: AxiosInstance) { const errorStrategy = config.errorStrategy as ErrorStrategy const method = (config.method as string).toLowerCase() - if (err.response.data instanceof Blob) { - const d = await err.response.data.text() - err.response.data = JSON.parse(d) - } - let errCode: string let content: string if (err.message === 'Network Error') { diff --git a/ui/package.json b/ui/package.json index 3947746cee..79d4b9c4ee 100644 --- a/ui/package.json +++ b/ui/package.json @@ -27,7 +27,6 @@ "i18next": "^19.6.3", "i18next-browser-languagedetector": "^5.0.0", "lodash": "^4.17.21", - "mime-types": "^2.1.30", "moize": "^5.4.7", "nprogress": "^0.2.0", "office-ui-fabric-react": "^7.123.10", diff --git a/ui/yarn.lock b/ui/yarn.lock index f4341fa384..6171b866dd 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -11676,11 +11676,6 @@ mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-db@1.47.0: - version "1.47.0" - resolved "http://mirrors.cloud.tencent.com/npm/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c" - integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw== - mime-types@^2.1.12, mime-types@^2.1.26, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" @@ -11688,13 +11683,6 @@ mime-types@^2.1.12, mime-types@^2.1.26, mime-types@~2.1.17, mime-types@~2.1.19, dependencies: mime-db "1.44.0" -mime-types@^2.1.30: - version "2.1.30" - resolved "http://mirrors.cloud.tencent.com/npm/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d" - integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg== - dependencies: - mime-db "1.47.0" - mime@1.6.0, mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" From e6780ad5f8f860f07b3309df2d94406f4d14235f Mon Sep 17 00:00:00 2001 From: Suhaha Date: Tue, 1 Jun 2021 09:55:23 +0800 Subject: [PATCH 31/31] feat(debugapi): support bool-type model & add tikv config endpoint --- .../debugapi/endpoint/endpoint_def.go | 18 +++++++++++++ .../debugapi/endpoint/param_models.go | 4 +++ ui/lib/apps/DebugAPI/apilist/widgets/Bool.tsx | 27 +++++++++++++++++++ .../apps/DebugAPI/apilist/widgets/index.tsx | 2 ++ ui/lib/apps/DebugAPI/translations/en.yaml | 5 ++++ 5 files changed, 56 insertions(+) create mode 100644 ui/lib/apps/DebugAPI/apilist/widgets/Bool.tsx diff --git a/pkg/apiserver/debugapi/endpoint/endpoint_def.go b/pkg/apiserver/debugapi/endpoint/endpoint_def.go index 03059c08d4..27a79da982 100644 --- a/pkg/apiserver/debugapi/endpoint/endpoint_def.go +++ b/pkg/apiserver/debugapi/endpoint/endpoint_def.go @@ -630,7 +630,22 @@ var pdPprof = APIModel{ }, } +// tikv +var tikvConfig = APIModel{ + ID: "tikv_config", + Component: model.NodeKindTiKV, + Path: "/config", + Method: MethodGet, + QueryParams: []APIParam{ + { + Name: "full", + Model: APIParamModelBool, + }, + }, +} + var APIListDef = []APIModel{ + // tidb tidbStatsDump, tidbStatsDumpWithTimestamp, tidbConfig, @@ -646,6 +661,7 @@ var APIListDef = []APIModel{ tidbTableRegions, tidbHotRegions, tidbPprof, + // pd pdCluster, pdClusterStatus, pdConfigShowAll, @@ -675,4 +691,6 @@ var APIListDef = []APIModel{ pdStores, pdStoreID, pdPprof, + // tikv + tikvConfig, } diff --git a/pkg/apiserver/debugapi/endpoint/param_models.go b/pkg/apiserver/debugapi/endpoint/param_models.go index eeb4523a53..1e45da4177 100644 --- a/pkg/apiserver/debugapi/endpoint/param_models.go +++ b/pkg/apiserver/debugapi/endpoint/param_models.go @@ -48,6 +48,10 @@ var APIParamModelText = &DefaultAPIParamModel{ Type: "text", } +var APIParamModelBool = &DefaultAPIParamModel{ + Type: "bool", +} + var APIParamModelMultiTags = &DefaultAPIParamModel{ Type: "tags", Transformer: func(ctx *Context) error { diff --git a/ui/lib/apps/DebugAPI/apilist/widgets/Bool.tsx b/ui/lib/apps/DebugAPI/apilist/widgets/Bool.tsx new file mode 100644 index 0000000000..cc8d2aa984 --- /dev/null +++ b/ui/lib/apps/DebugAPI/apilist/widgets/Bool.tsx @@ -0,0 +1,27 @@ +// Copyright 2021 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react' +import { Switch } from 'antd' + +import type { ApiFormWidget } from './index' + +export const BoolWidget: ApiFormWidget = ({ onChange, value }) => { + return ( + onChange!(v ? 'true' : (undefined as any))} + /> + ) +} diff --git a/ui/lib/apps/DebugAPI/apilist/widgets/index.tsx b/ui/lib/apps/DebugAPI/apilist/widgets/index.tsx index 7fc4694532..984a90b1f3 100644 --- a/ui/lib/apps/DebugAPI/apilist/widgets/index.tsx +++ b/ui/lib/apps/DebugAPI/apilist/widgets/index.tsx @@ -13,6 +13,7 @@ import { DatabaseWidget } from './Database' import { TableWidget } from './Table' import { TableIDWidget } from './TableID' import { StoresStateWidget } from './StoresState' +import { BoolWidget } from './Bool' export interface Widgets { [type: string]: ApiFormWidget @@ -44,6 +45,7 @@ const createJSXElementWrapper = (WidgetDef: ApiFormWidget) => ( const paramModelWidgets: Widgets = { host: HostSelectWidget, text: TextWidget, + bool: createJSXElementWrapper(BoolWidget), tags: createJSXElementWrapper(TagsWidget), int: createJSXElementWrapper(IntWidget), enum: EnumWidget, diff --git a/ui/lib/apps/DebugAPI/translations/en.yaml b/ui/lib/apps/DebugAPI/translations/en.yaml index 04a92b1f66..5dcea79f04 100644 --- a/ui/lib/apps/DebugAPI/translations/en.yaml +++ b/ui/lib/apps/DebugAPI/translations/en.yaml @@ -37,11 +37,16 @@ debug_api: tidb_hot_regions: Hot Regions tidb_pprof: TiDB pprof tidb_pprof_desc: The `seconds` parameter is only effective to `kind=profile` and `kind=trace`. + tikv: + name: TiKV + endpoints: + tikv_config: Current TiKV Config pd: name: PD endpoints: pd_cluster: Cluster Information (pd-ctl cluster) pd_cluster_status: Cluster Status + pd_config_show_all: Current PD Config pd_health: Cluster Health Information (pd-ctl health) pd_hot_read: Hot - Read (pd-ctl hot read) pd_hot_write: Hot - Write (pd-ctl hot write)