From ff14cd98b3d5a1e70c91a6faae131b39c77058cb Mon Sep 17 00:00:00 2001 From: Sparkle <1284531+baurine@users.noreply.github.com> Date: Tue, 25 Feb 2020 12:01:55 +0800 Subject: [PATCH] Statement API and integration (#34) --- pkg/apiserver/apiserver.go | 2 + pkg/apiserver/statement/models.go | 57 ++++ pkg/apiserver/statement/queries.go | 154 +++++++++++ pkg/apiserver/statement/statement.go | 163 +++++++++++ ui/.github_release_version | 2 +- ui/package.json | 1 + ui/src/apps/statement/RootComponent.js | 60 ++-- ui/src/apps/statement/StatementDetailDemo.js | 88 ------ ui/src/apps/statement/StatementDetailPage.js | 37 +++ ui/src/apps/statement/StatementListDemo.js | 95 ------- .../apps/statement/StatementsOverviewPage.js | 60 ++++ .../statement/components/StatementDetail.tsx | 89 ++++-- .../components/StatementDetailTable.tsx | 110 -------- .../components/StatementNodesTable.tsx | 123 +++++++++ .../StatementSettingModal.module.css | 3 - .../components/StatementSettingModal.tsx | 17 +- .../components/StatementSummaryTable.tsx | 41 ++- ...atementList.tsx => StatementsOverview.tsx} | 261 +++++++++++------- .../statement/components/StatementsTable.tsx | 110 +++++--- ui/src/apps/statement/components/index.ts | 6 +- .../components/search-options-context.ts | 23 ++ .../statement/components/statement-types.ts | 64 ++--- ...entDetail.module.css => styles.module.css} | 9 +- ui/src/apps/statement/index.js | 3 +- ui/src/apps/statement/translations/en.yaml | 21 ++ ui/src/apps/statement/translations/zh-CN.yaml | 21 ++ ui/src/layout/RootComponent.js | 2 +- ui/src/utils/client/index.js | 3 + 28 files changed, 1082 insertions(+), 543 deletions(-) create mode 100644 pkg/apiserver/statement/models.go create mode 100644 pkg/apiserver/statement/queries.go create mode 100644 pkg/apiserver/statement/statement.go delete mode 100644 ui/src/apps/statement/StatementDetailDemo.js create mode 100644 ui/src/apps/statement/StatementDetailPage.js delete mode 100644 ui/src/apps/statement/StatementListDemo.js create mode 100644 ui/src/apps/statement/StatementsOverviewPage.js delete mode 100644 ui/src/apps/statement/components/StatementDetailTable.tsx create mode 100644 ui/src/apps/statement/components/StatementNodesTable.tsx delete mode 100644 ui/src/apps/statement/components/StatementSettingModal.module.css rename ui/src/apps/statement/components/{StatementList.tsx => StatementsOverview.tsx} (57%) create mode 100644 ui/src/apps/statement/components/search-options-context.ts rename ui/src/apps/statement/components/{StatementDetail.module.css => styles.module.css} (73%) create mode 100644 ui/src/apps/statement/translations/en.yaml create mode 100644 ui/src/apps/statement/translations/zh-CN.yaml diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 096fcf2e40..a35f25f362 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -23,6 +23,7 @@ import ( "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/foo" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/info" + "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/statement" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" "github.com/pingcap-incubator/tidb-dashboard/pkg/config" @@ -59,6 +60,7 @@ func Handler(apiPrefix string, config *config.Config, services *Services) http.H foo.NewService(config).Register(endpoint, auth) info.NewService(config, services.TiDBForwarder, services.Store).Register(endpoint, auth) services.KeyVisual.Register(endpoint, auth) + statement.NewService(config, services.TiDBForwarder).Register(endpoint, auth) return r } diff --git a/pkg/apiserver/statement/models.go b/pkg/apiserver/statement/models.go new file mode 100644 index 0000000000..935931292c --- /dev/null +++ b/pkg/apiserver/statement/models.go @@ -0,0 +1,57 @@ +// Copyright 2020 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package statement + +// TimeRange represents a range of time +type TimeRange struct { + BeginTime string `json:"begin_time"` + EndTime string `json:"end_time"` +} + +// Overview represents the overview of a statement +type Overview struct { + SchemaName string `json:"schema_name"` + Digest string `json:"digest"` + DigestText string `json:"digest_text"` + AggSumLatency int `json:"sum_latency"` + AggAvgLatency int `json:"avg_latency"` + AggExecCount int `json:"exec_count"` + AggAvgAffectedRows int `json:"avg_affected_rows"` + AggAvgMem int `json:"avg_mem"` +} + +// Detail represents the detail of a statement +type Detail struct { + SchemaName string `json:"schema_name"` + Digest string `json:"digest"` + DigestText string `json:"digest_text"` + AggSumLatency int `json:"sum_latency"` + AggExecCount int `json:"exec_count"` + AggAvgAffectedRows int `json:"avg_affected_rows"` + AggAvgTotalKeys int `json:"avg_total_keys"` + + QuerySampleText string `json:"query_sample_text"` + LastSeen string `json:"last_seen"` +} + +// Node represents the statement in each node +type Node struct { + Address string `json:"address"` + SumLatency int `json:"sum_latency"` + ExecCount int `json:"exec_count"` + AvgLatency int `json:"avg_latency"` + MaxLatency int `json:"max_latency"` + AvgMem int `json:"avg_mem"` + SumBackoffTimes int `json:"sum_backoff_times"` +} diff --git a/pkg/apiserver/statement/queries.go b/pkg/apiserver/statement/queries.go new file mode 100644 index 0000000000..7507fe5f90 --- /dev/null +++ b/pkg/apiserver/statement/queries.go @@ -0,0 +1,154 @@ +// Copyright 2020 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package statement + +import ( + "fmt" + "regexp" + "sort" + "strings" + + "github.com/jinzhu/gorm" +) + +func QuerySchemas(db *gorm.DB) ([]string, error) { + sql := `SHOW DATABASES` + + var schemas []string + err := db.Raw(sql).Pluck("Database", &schemas).Error + if err != nil { + return nil, err + } + + for i, v := range schemas { + schemas[i] = strings.ToLower(v) + } + sort.Strings(schemas) + return schemas, nil +} + +func QueryTimeRanges(db *gorm.DB) (result []*TimeRange, err error) { + err = db. + Select(` + DISTINCT + summary_begin_time AS begin_time, + summary_end_time AS end_time + `). + Table("PERFORMANCE_SCHEMA.cluster_events_statements_summary_by_digest_history"). + Order("summary_begin_time DESC"). + Find(&result).Error + return result, err +} + +// Sample params: +// schemas: ["tpcc", "test"] +// beginTime: "2020-02-13 10:30:00" +// endTime: "2020-02-13 11:00:00" +func QueryStatementsOverview(db *gorm.DB, schemas []string, beginTime, endTime string) (result []*Overview, err error) { + query := db. + Select(` + schema_name, + digest, + digest_text, + sum(sum_latency) AS agg_sum_latency, + sum(exec_count) AS agg_exec_count, + round(sum(exec_count*avg_affected_rows)/sum(exec_count)) AS agg_avg_affected_rows, + round(sum(exec_count*avg_latency)/sum(exec_count)) AS agg_avg_latency, + round(sum(exec_count*avg_mem)/sum(exec_count)) AS agg_avg_mem + `). + Table("PERFORMANCE_SCHEMA.cluster_events_statements_summary_by_digest_history"). + Where("summary_begin_time = ? AND summary_end_time = ?", beginTime, endTime). + Group("schema_name, digest, digest_text"). + Order("agg_sum_latency DESC") + + if len(schemas) > 0 { + regex := make([]string, 0, len(schemas)) + for _, schema := range schemas { + regex = append(regex, fmt.Sprintf("\\b%s\\.", regexp.QuoteMeta(schema))) + } + regexAll := strings.Join(regex, "|") + query = query.Where("table_names REGEXP ?", regexAll) + } + + err = query.Find(&result).Error + return result, err +} + +// Sample params: +// schemas: "tpcc" +// beginTime: "2020-02-13 10:30:00" +// endTime: "2020-02-13 11:00:00" +// digest: "bcaa7bdb37e24d03fb48f20cc32f4ff3f51c0864dc378829e519650df5c7b923" +func QueryStatementDetail(db *gorm.DB, schema, beginTime, endTime, digest string) (*Detail, error) { + result := &Detail{} + + query := db. + Select(` + schema_name, + digest, + digest_text, + sum(sum_latency) AS agg_sum_latency, + sum(exec_count) AS agg_exec_count, + round(sum(exec_count*avg_affected_rows)/sum(exec_count)) AS agg_avg_affected_rows, + round(sum(exec_count*avg_total_keys)/sum(exec_count)) AS agg_avg_total_keys + `). + Table("PERFORMANCE_SCHEMA.cluster_events_statements_summary_by_digest_history"). + Where("schema_name = ?", schema). + Where("summary_begin_time = ? AND summary_end_time = ?", beginTime, endTime). + Where("digest = ?", digest). + Group("digest, digest_text, schema_name") + + if err := query.Scan(&result).Error; err != nil { + return nil, err + } + + query = db. + Select(`query_sample_text, last_seen`). + Table("PERFORMANCE_SCHEMA.cluster_events_statements_summary_by_digest_history"). + Where("schema_name = ?", schema). + Where("summary_begin_time = ? AND summary_end_time = ?", beginTime, endTime). + Where("digest = ?", digest). + Order("last_seen DESC") + + if err := query.First(&result).Error; err != nil { + return nil, err + } + + return result, nil +} + +// Sample params: +// schemas: "tpcc" +// beginTime: "2020-02-13 10:30:00" +// endTime: "2020-02-13 11:00:00" +// digest: "bcaa7bdb37e24d03fb48f20cc32f4ff3f51c0864dc378829e519650df5c7b923" +func QueryStatementNodes(db *gorm.DB, schema, beginTime, endTime, digest string) (result []*Node, err error) { + err = db. + Select(` + address, + sum_latency, + exec_count, + avg_latency, + max_latency, + avg_mem, + sum_backoff_times + `). + Table("PERFORMANCE_SCHEMA.cluster_events_statements_summary_by_digest_history"). + Where("schema_name = ?", schema). + Where("summary_begin_time = ? AND summary_end_time = ?", beginTime, endTime). + Where("digest = ?", digest). + Order("sum_latency DESC"). + Find(&result).Error + return result, err +} diff --git a/pkg/apiserver/statement/statement.go b/pkg/apiserver/statement/statement.go new file mode 100644 index 0000000000..3ec3288dc0 --- /dev/null +++ b/pkg/apiserver/statement/statement.go @@ -0,0 +1,163 @@ +// Copyright 2020 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package statement + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/jinzhu/gorm" + + "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user" + "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" + "github.com/pingcap-incubator/tidb-dashboard/pkg/config" + "github.com/pingcap-incubator/tidb-dashboard/pkg/tidb" +) + +type Service struct { + config *config.Config + tidbForwarder *tidb.Forwarder +} + +func NewService(config *config.Config, tidbForwarder *tidb.Forwarder) *Service { + return &Service{config: config, tidbForwarder: tidbForwarder} +} + +func (s *Service) Register(r *gin.RouterGroup, auth *user.AuthService) { + endpoint := r.Group("/statements") + endpoint.Use(auth.MWAuthRequired()) + endpoint.Use(utils.MWConnectTiDB(s.tidbForwarder)) + endpoint.GET("/schemas", s.schemasHandler) + endpoint.GET("/time_ranges", s.timeRangesHandler) + endpoint.GET("/overviews", s.overviewsHandler) + endpoint.GET("/detail", s.detailHandler) + endpoint.GET("/nodes", s.nodesHandler) +} + +// @Summary TiDB databases +// @Description Get all databases of TiDB +// @Produce json +// @Success 200 {array} string +// @Router /statements/schemas [get] +// @Security JwtAuth +// @Failure 401 {object} utils.APIError "Unauthorized failure" +func (s *Service) schemasHandler(c *gin.Context) { + db := c.MustGet(utils.TiDBConnectionKey).(*gorm.DB) + schemas, err := QuerySchemas(db) + if err != nil { + _ = c.Error(err) + return + } + c.JSON(http.StatusOK, schemas) +} + +// @Summary Statement time ranges +// @Description Get all time ranges of the statements +// @Produce json +// @Success 200 {array} statement.TimeRange +// @Router /statements/time_ranges [get] +// @Security JwtAuth +// @Failure 401 {object} utils.APIError "Unauthorized failure" +func (s *Service) timeRangesHandler(c *gin.Context) { + db := c.MustGet(utils.TiDBConnectionKey).(*gorm.DB) + timeRanges, err := QueryTimeRanges(db) + if err != nil { + _ = c.Error(err) + return + } + c.JSON(http.StatusOK, timeRanges) +} + +// @Summary Statements overview +// @Description Get statements overview +// @Produce json +// @Param schemas query string false "Target schemas" +// @Param begin_time query string true "Statement begin time" +// @Param end_time query string true "Statement end time" +// @Success 200 {array} statement.Overview +// @Router /statements/overviews [get] +// @Security JwtAuth +// @Failure 401 {object} utils.APIError "Unauthorized failure" +func (s *Service) overviewsHandler(c *gin.Context) { + var schemas []string + schemasQuery := c.Query("schemas") + if schemasQuery != "" { + schemas = strings.Split(schemasQuery, ",") + } + beginTime := c.Query("begin_time") + endTime := c.Query("end_time") + if beginTime == "" || endTime == "" { + _ = c.Error(fmt.Errorf("invalid begin_time or end_time")) + return + } + db := c.MustGet(utils.TiDBConnectionKey).(*gorm.DB) + overviews, err := QueryStatementsOverview(db, schemas, beginTime, endTime) + if err != nil { + _ = c.Error(err) + return + } + c.JSON(http.StatusOK, overviews) +} + +// @Summary Statement detail +// @Description Get statement detail +// @Produce json +// @Param schema query string true "Statement schema" +// @Param begin_time query string true "Statement begin time" +// @Param end_time query string true "Statement end time" +// @Param digest query string true "Statement digest" +// @Success 200 {object} statement.Detail +// @Router /statements/detail [get] +// @Security JwtAuth +// @Failure 401 {object} utils.APIError "Unauthorized failure" +func (s *Service) detailHandler(c *gin.Context) { + db := c.MustGet(utils.TiDBConnectionKey).(*gorm.DB) + schema := c.Query("schema") + beginTime := c.Query("begin_time") + endTime := c.Query("end_time") + digest := c.Query("digest") + detail, err := QueryStatementDetail(db, schema, beginTime, endTime, digest) + if err != nil { + _ = c.Error(err) + return + } + c.JSON(http.StatusOK, detail) +} + +// @Summary Statement nodes +// @Description Get statement in each node +// @Produce json +// @Param schema query string true "Statement schema" +// @Param begin_time query string true "Statement begin time" +// @Param end_time query string true "Statement end time" +// @Param digest query string true "Statement digest" +// @Success 200 {array} statement.Node +// @Router /statements/nodes [get] +// @Security JwtAuth +// @Failure 401 {object} utils.APIError "Unauthorized failure" +func (s *Service) nodesHandler(c *gin.Context) { + db := c.MustGet(utils.TiDBConnectionKey).(*gorm.DB) + schema := c.Query("schema") + beginTime := c.Query("begin_time") + endTime := c.Query("end_time") + digest := c.Query("digest") + nodes, err := QueryStatementNodes(db, schema, beginTime, endTime, digest) + if err != nil { + _ = c.Error(err) + return + } + c.JSON(http.StatusOK, nodes) +} diff --git a/ui/.github_release_version b/ui/.github_release_version index b9d5d1a726..24e4e71912 100644 --- a/ui/.github_release_version +++ b/ui/.github_release_version @@ -1,3 +1,3 @@ # This file contains a version number which will be used to release assets to # GitHub. To trigger a new asset release, simply increase this version number. -20200218_1 +20200225_1 diff --git a/ui/package.json b/ui/package.json index 0659e5a66a..aade81e55f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,7 @@ "i18next": "^19.1.0", "i18next-browser-languagedetector": "^4.0.1", "lodash": "^4.17.15", + "moment": "^2.24.0", "react": "^16.12.0", "react-dom": "^16.12.0", "react-i18next": "^11.3.2", diff --git a/ui/src/apps/statement/RootComponent.js b/ui/src/apps/statement/RootComponent.js index 5379571fea..40add89235 100644 --- a/ui/src/apps/statement/RootComponent.js +++ b/ui/src/apps/statement/RootComponent.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import { HashRouter as Router, Switch, @@ -9,37 +9,47 @@ import { } from 'react-router-dom' import { Breadcrumb } from 'antd' -import StatementListDemo from './StatementListDemo' -import StatementDetailDemo from './StatementDetailDemo' +import StatementsOverviewPage from './StatementsOverviewPage' +import StatementDetailPage from './StatementDetailPage' +import { SearchContext } from './components' const App = withRouter(props => { const { location } = props const page = location.pathname.split('/').pop() + const [searchOptions, setSearchOptions] = useState({ + curInstance: undefined, + curSchemas: [], + curTimeRange: undefined + }) + const searchContext = { searchOptions, setSearchOptions } + return ( -
-
- - - Statement List - - {page === 'detail' && ( - Statement Detail - )} - -
-
- - - - - - - - - + +
+
+ + + Statements Overview + + {page === 'detail' && ( + Statement Detail + )} + +
+
+ + + + + + + + + +
-
+ ) }) diff --git a/ui/src/apps/statement/StatementDetailDemo.js b/ui/src/apps/statement/StatementDetailDemo.js deleted file mode 100644 index 65a70f3c98..0000000000 --- a/ui/src/apps/statement/StatementDetailDemo.js +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react' -import { StatementDetail } from './components' -import { useLocation } from 'react-router-dom' - -// TODO -function fakeReq(res) { - return new Promise((resolve, _reject) => { - setTimeout(() => resolve(res), 2000) - }) -} - -export default function StatementDetailDemo() { - const sqlCategory = new URLSearchParams(useLocation().search).get( - 'sql_category' - ) - - function queryDetail(sqlCategory) { - const res = { - summary: { - sql_category: sqlCategory, - last_sql: - 'select name, name_number from table1 where class_number in ( 1,2,3 ) group by row order by name_number', - last_time: '2019-10-10 13:01:03', - schemas: ['schema1', 'schema2', 'schema3'] - }, - statis: { - total_duration: 100, - total_times: 100110, - avg_affect_lines: 100010, - avg_scan_lines: 1000 - }, - nodes: [ - { - node: 'node-1', - total_duration: 100, - total_times: 100, - avg_duration: 10, - max_duration: 10, - avg_cost_mem: 20, - back_off_times: 200 - }, - { - node: 'node-2', - total_duration: 99, - total_times: 100, - avg_duration: 10, - max_duration: 10, - avg_cost_mem: 20, - back_off_times: 200 - }, - { - node: 'node-3', - total_duration: 98, - total_times: 100, - avg_duration: 20, - max_duration: 10, - avg_cost_mem: 20, - back_off_times: 200 - }, - { - node: 'node-4', - total_duration: 90, - total_times: 100, - avg_duration: 10, - max_duration: 50, - avg_cost_mem: 10, - back_off_times: 200 - }, - { - node: 'node-5', - total_duration: 9, - total_times: 100, - avg_duration: 10, - max_duration: 10, - avg_cost_mem: 20, - back_off_times: 10 - } - ] - } - return fakeReq(res) - } - - return sqlCategory ? ( - - ) : ( -

No sql_category

- ) -} diff --git a/ui/src/apps/statement/StatementDetailPage.js b/ui/src/apps/statement/StatementDetailPage.js new file mode 100644 index 0000000000..0b4fb87491 --- /dev/null +++ b/ui/src/apps/statement/StatementDetailPage.js @@ -0,0 +1,37 @@ +import React from 'react' +import { StatementDetail } from './components' +import { useLocation } from 'react-router-dom' +import client from '@/utils/client' + +export default function StatementDetailPage() { + const params = new URLSearchParams(useLocation().search) + const digest = params.get('digest') + const schemaName = params.get('schema') + const beginTime = params.get('begin_time') + const endTime = params.get('end_time') + + function queryDetail(digest, schemaName, beginTime, endTime) { + return client.dashboard + .statementsDetailGet(schemaName, beginTime, endTime, digest) + .then(res => res.data) + } + + function queryNodes(digest, schemaName, beginTime, endTime) { + return client.dashboard + .statementsNodesGet(schemaName, beginTime, endTime, digest) + .then(res => res.data) + } + + return digest ? ( + + ) : ( +

No sql digest

+ ) +} diff --git a/ui/src/apps/statement/StatementListDemo.js b/ui/src/apps/statement/StatementListDemo.js deleted file mode 100644 index e2152bdf15..0000000000 --- a/ui/src/apps/statement/StatementListDemo.js +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react' -import { StatementList } from './components' - -function fakeReq(res) { - return new Promise((resolve, reject) => { - setTimeout(() => resolve(res), 2000) - }) -} - -export default function StatementListDemo() { - function queryInstance() { - return fakeReq([ - { uuid: 'ins-1', name: 'ins-1' }, - { uuid: 'ins-2', name: 'ins-2' }, - { uuid: 'ins-3', name: 'ins-3' }, - { uuid: 'ins-4', name: 'ins-4' } - ]) - } - - function querySchemas() { - return fakeReq(['schema-1', 'schema-2', 'schema-3', 'schema-4']) - } - - function queryTimeRanges() { - return fakeReq(['10:00~11:00', '11:00~12:00', '12:00~']) - } - - function queryStatements() { - return fakeReq([ - { - sql_category: 'select from table1', - total_duration: 97, - total_times: 10000, - avg_affect_lines: 13748, - avg_duration: 20, - avg_cost_mem: 20 - }, - { - sql_category: 'select from table2', - total_duration: 99, - total_times: 10000, - avg_affect_lines: 13748, - avg_duration: 10, - avg_cost_mem: 10 - }, - { - sql_category: 'update table1', - total_duration: 98, - total_times: 8000, - avg_affect_lines: 13748, - avg_duration: 20, - avg_cost_mem: 20 - }, - { - sql_category: 'select from table3', - total_duration: 100, - total_times: 1000, - avg_affect_lines: 13748, - avg_duration: 20, - avg_cost_mem: 20 - } - ]) - } - - function queryStatementStatus() { - return fakeReq('ok') - } - - function updateStatementStatus() { - return fakeReq('ok') - } - - const queryConfig = () => - fakeReq({ - refresh_interval: 100, - keep_duration: 100, - max_sql_count: 1000, - max_sql_length: 100 - }) - - const updateConfig = () => fakeReq('ok') - - return ( - - ) -} diff --git a/ui/src/apps/statement/StatementsOverviewPage.js b/ui/src/apps/statement/StatementsOverviewPage.js new file mode 100644 index 0000000000..cc942f722a --- /dev/null +++ b/ui/src/apps/statement/StatementsOverviewPage.js @@ -0,0 +1,60 @@ +import React from 'react' +import { StatementsOverview } from './components' +import client from '../../utils/client' + +function fakeReq(res) { + return new Promise((resolve, reject) => { + setTimeout(() => resolve(res), 2000) + }) +} + +export default function StatementsOverviewPage() { + function queryInstance() { + return Promise.resolve([{ uuid: 'current', name: 'current cluster' }]) + } + + function querySchemas() { + return client.dashboard.statementsSchemasGet().then(res => res.data) + } + + function queryTimeRanges() { + return client.dashboard.statementsTimeRangesGet().then(res => res.data) + } + + function queryStatements(_instanceId, schemas, beginTime, endTime) { + return client.dashboard + .statementsOverviewsGet(beginTime, endTime, schemas.join(',')) + .then(res => res.data) + } + + function queryStatementStatus() { + return fakeReq('ok') + } + + function updateStatementStatus() { + return fakeReq('ok') + } + + const queryConfig = () => + fakeReq({ + refresh_interval: 100, + keep_duration: 100, + max_sql_count: 1000, + max_sql_length: 100 + }) + + const updateConfig = () => fakeReq('ok') + + return ( + + ) +} diff --git a/ui/src/apps/statement/components/StatementDetail.tsx b/ui/src/apps/statement/components/StatementDetail.tsx index 92881745ec..9250de7c50 100644 --- a/ui/src/apps/statement/components/StatementDetail.tsx +++ b/ui/src/apps/statement/components/StatementDetail.tsx @@ -2,49 +2,94 @@ import React, { useState, useEffect } from 'react' import { Spin } from 'antd' import { getValueFormat } from '@baurine/grafana-value-formats' -import StatementDetailTable from './StatementDetailTable' +import StatementNodesTable from './StatementNodesTable' import StatementSummaryTable from './StatementSummaryTable' -import { StatementDetailInfo } from './statement-types' +import { StatementDetailInfo, StatementNode } from './statement-types' -import styles from './StatementDetail.module.css' +import styles from './styles.module.css' +import { useTranslation } from 'react-i18next' + +function StatisCard({ detail }: { detail: StatementDetailInfo }) { + const { t } = useTranslation() -function StatisCard({ detail: { statis } }: { detail: StatementDetailInfo }) { return (
-

总时长:{getValueFormat('s')(statis.total_duration, 2, null)}

-

总次数:{getValueFormat('short')(statis.total_times, 0, 0)}

- 平均影响行数:{getValueFormat('short')(statis.avg_affect_lines, 0, 0)} + {t('statement.common.sum_latency')}:{' '} + {getValueFormat('ns')(detail.sum_latency, 2, null)} +

+

+ {t('statement.common.exec_count')}:{' '} + {getValueFormat('short')(detail.exec_count, 0, 0)} +

+

+ {t('statement.common.avg_affected_rows')}:{' '} + {getValueFormat('short')(detail.avg_affected_rows, 0, 0)}

- 平均扫描行数:{getValueFormat('short')(statis.avg_scan_lines, 0, 0)} + {t('statement.common.avg_total_keys')}:{' '} + {getValueFormat('short')(detail.avg_total_keys, 0, 0)}

) } interface Props { - sqlCategory: string - onFetchDetail: (string) => Promise + digest: string + schemaName: string + beginTime: string + endTime: string + onFetchDetail: ( + digest: string, + schemaName: string, + beginTime: string, + endTime: string + ) => Promise + onFetchNodes: ( + digest: string, + schemaName: string, + beginTime: string, + endTime: string + ) => Promise } -export default function StatementDetail({ sqlCategory, onFetchDetail }: Props) { +export default function StatementDetail({ + digest, + schemaName, + beginTime, + endTime, + onFetchDetail, + onFetchNodes +}: Props) { const [detail, setDetail] = useState(null) + const [nodes, setNodes] = useState([]) const [loading, setLoading] = useState(true) useEffect(() => { async function query() { setLoading(true) - const res = await onFetchDetail(sqlCategory) - if (res) { - setDetail(res) - } else { - setDetail(null) - } + const detailRes = await onFetchDetail( + digest, + schemaName, + beginTime, + endTime + ) + setDetail(detailRes || null) + const nodesRes = await onFetchNodes( + digest, + schemaName, + beginTime, + endTime + ) + setNodes(nodesRes || []) setLoading(false) } query() - }, [sqlCategory, onFetchDetail]) + // eslint-disable-next-line + }, [digest, schemaName, beginTime, endTime]) + // don't add the dependent functions likes onFetchDetail into the dependency array + // it will cause the infinite loop if use context inside it in the future + // wrap them by useCallback() in the parent component can fix it but I don't think it is necessary return (
@@ -54,12 +99,16 @@ export default function StatementDetail({ sqlCategory, onFetchDetail }: Props) { <>
- +
- +
)} diff --git a/ui/src/apps/statement/components/StatementDetailTable.tsx b/ui/src/apps/statement/components/StatementDetailTable.tsx deleted file mode 100644 index 303b8ed7f1..0000000000 --- a/ui/src/apps/statement/components/StatementDetailTable.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useMemo } from 'react' -import _ from 'lodash' -import { Table } from 'antd' -import { getValueFormat } from '@baurine/grafana-value-formats' - -import { StatementDetailInfo, StatementNode } from './statement-types' -import { HorizontalBar } from './HorizontalBar' - -const tableColumns = ( - maxAvgDuration: number, - maxMaxDuration: number, - maxCostMem: number -) => [ - { - title: 'node', - dataIndex: 'node', - key: 'node' - }, - { - title: '总时长', - dataIndex: 'total_duration', - key: 'total_duration', - sorter: (a: StatementNode, b: StatementNode) => - a.total_duration - b.total_duration, - render: text => getValueFormat('s')(text, 2, null) - }, - { - title: '总次数', - dataIndex: 'total_times', - key: 'total_times', - render: text => getValueFormat('short')(text, 0, 0) - }, - { - title: '平均时长', - dataIndex: 'avg_duration', - key: 'avg_duration', - render: text => ( -
- {getValueFormat('ms')(text, 2, null)} - -
- ) - }, - { - title: '最大时长', - dataIndex: 'max_duration', - key: 'max_duration', - render: text => ( -
- {getValueFormat('ms')(text, 2, null)} - -
- ) - }, - { - title: '平均消耗内存', - dataIndex: 'avg_cost_mem', - key: 'avg_cost_mem', - render: text => ( -
- {getValueFormat('mbytes')(text, 2, null)} - -
- ) - }, - { - title: 'back_off 重试次数', - dataIndex: 'back_off_times', - key: 'back_off_times', - render: text => getValueFormat('short')(text, 0, 0) - } -] - -export default function StatementDetailTable({ - detail: { nodes } -}: { - detail: StatementDetailInfo -}) { - const maxAvgDuration = useMemo(() => _.max(nodes.map(n => n.avg_duration)), [ - nodes - ]) - const maxMaxDuration = useMemo(() => _.max(nodes.map(n => n.max_duration)), [ - nodes - ]) - const maxCostMem = useMemo(() => _.max(nodes.map(n => n.avg_cost_mem)), [ - nodes - ]) - const columns = useMemo( - () => tableColumns(maxAvgDuration!, maxMaxDuration!, maxCostMem!), - [maxAvgDuration, maxCostMem, maxMaxDuration] - ) - - return ( - - ) -} diff --git a/ui/src/apps/statement/components/StatementNodesTable.tsx b/ui/src/apps/statement/components/StatementNodesTable.tsx new file mode 100644 index 0000000000..982ed0db32 --- /dev/null +++ b/ui/src/apps/statement/components/StatementNodesTable.tsx @@ -0,0 +1,123 @@ +import React, { useMemo } from 'react' +import _ from 'lodash' +import { Table } from 'antd' +import { getValueFormat } from '@baurine/grafana-value-formats' + +import { StatementNode } from './statement-types' +import { HorizontalBar } from './HorizontalBar' +import { useTranslation } from 'react-i18next' + +const tableColumns = ( + maxAvgLatency: number, + maxMaxLatency: number, + maxAvgMem: number, + t: (string) => string +) => [ + { + title: t('statement.detail.node'), + dataIndex: 'address', + key: 'address' + }, + { + title: t('statement.common.sum_latency'), + dataIndex: 'sum_latency', + key: 'sum_latency', + sorter: (a: StatementNode, b: StatementNode) => + a.sum_latency - b.sum_latency, + render: text => getValueFormat('ns')(text, 2, null) + }, + { + title: t('statement.common.exec_count'), + dataIndex: 'exec_count', + key: 'exec_count', + sorter: (a: StatementNode, b: StatementNode) => a.exec_count - b.exec_count, + render: text => getValueFormat('short')(text, 0, 0) + }, + { + title: t('statement.common.avg_latency'), + dataIndex: 'avg_latency', + key: 'avg_latency', + sorter: (a: StatementNode, b: StatementNode) => + a.avg_latency - b.avg_latency, + render: text => ( +
+ {getValueFormat('ns')(text, 2, null)} + +
+ ) + }, + { + title: t('statement.common.max_latency'), + dataIndex: 'max_latency', + key: 'max_latency', + sorter: (a: StatementNode, b: StatementNode) => + a.max_latency - b.max_latency, + render: text => ( +
+ {getValueFormat('ns')(text, 2, null)} + +
+ ) + }, + { + title: t('statement.common.avg_mem'), + dataIndex: 'avg_mem', + key: 'avg_mem', + sorter: (a: StatementNode, b: StatementNode) => a.avg_mem - b.avg_mem, + render: text => ( +
+ {getValueFormat('bytes')(text, 2, null)} + +
+ ) + }, + { + title: t('statement.common.sum_backoff_times'), + dataIndex: 'sum_backoff_times', + key: 'sum_backoff_times', + sorter: (a: StatementNode, b: StatementNode) => + a.sum_backoff_times - b.sum_backoff_times, + render: text => getValueFormat('short')(text, 0, 0) + } +] + +export default function StatementNodesTable({ + nodes +}: { + nodes: StatementNode[] +}) { + const { t } = useTranslation() + const maxAvgLatency = useMemo( + () => _.max(nodes.map(n => n.avg_latency)) || 1, + [nodes] + ) + const maxMaxLatency = useMemo( + () => _.max(nodes.map(n => n.max_latency)) || 1, + [nodes] + ) + const maxAvgMem = useMemo(() => _.max(nodes.map(n => n.avg_mem)) || 1, [ + nodes + ]) + const columns = useMemo( + () => tableColumns(maxAvgLatency!, maxMaxLatency!, maxAvgMem!, t), + [maxAvgLatency, maxAvgMem, maxMaxLatency, t] + ) + + return ( +
`${record.address}_${index}`} + pagination={false} + /> + ) +} diff --git a/ui/src/apps/statement/components/StatementSettingModal.module.css b/ui/src/apps/statement/components/StatementSettingModal.module.css deleted file mode 100644 index d6a9eff697..0000000000 --- a/ui/src/apps/statement/components/StatementSettingModal.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.config_form :global(.ant-form-item) { - margin-bottom: 0; -} diff --git a/ui/src/apps/statement/components/StatementSettingModal.tsx b/ui/src/apps/statement/components/StatementSettingModal.tsx index 0b48e53e36..9ba138518e 100644 --- a/ui/src/apps/statement/components/StatementSettingModal.tsx +++ b/ui/src/apps/statement/components/StatementSettingModal.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react' import { Modal, Form, message, Spin, InputNumber } from 'antd' import { StatementConfig } from './statement-types' -import styles from './StatementSettingModal.module.css' +import styles from './styles.module.css' interface Props { instanceId: string @@ -61,7 +61,10 @@ function StatementSettingModal({ } } - function handleConfigChange(configKey: string, configValue: number | undefined) { + function handleConfigChange( + configKey: string, + configValue: number | undefined + ) { setConfig({ ...(config as StatementConfig), [configKey]: configValue @@ -77,9 +80,15 @@ function StatementSettingModal({ confirmLoading={submitting} okButtonProps={{ disabled: loading || config === null }} > - {loading && } + {loading && ( + + )} {!loading && config && ( -
+ Promise onFetchSchemas: (instanceId: string) => Promise onFetchTimeRanges: ( - instanceId: string, - schemas: string[] - ) => Promise + instanceId: string + ) => Promise onFetchStatements: ( instanceId: string, schemas: string[], - timeRange: string | undefined - ) => Promise + beginTime: string, + endTime: string + ) => Promise onGetStatementStatus: (instanceId: string) => Promise onSetStatementStatus: ( @@ -138,7 +140,7 @@ interface Props { onUpdateConfig: (instanceId: string, config: StatementConfig) => Promise } -export default function StatementList({ +export default function StatementsOverview({ onFetchInstances, onFetchSchemas, onFetchTimeRanges, @@ -150,7 +152,12 @@ export default function StatementList({ onFetchConfig, onUpdateConfig }: Props) { - const [state, dispatch] = useReducer(reducer, initState) + const { searchOptions, setSearchOptions } = useContext(SearchContext) + // combine the context to state + const [state, dispatch] = useReducer(reducer, { + ...initState, + ...searchOptions + }) const [ enableStatementModalVisible, setEnableStatementModalVisible @@ -159,6 +166,7 @@ export default function StatementList({ statementSettingModalVisible, setStatementSettingModalVisible ] = useState(false) + const { t } = useTranslation() useEffect(() => { async function queryInstances() { @@ -167,80 +175,120 @@ export default function StatementList({ type: 'save_instances', payload: res || [] }) + if (res?.length === 1 && !state.curInstance) { + dispatch({ + type: 'change_instance', + payload: res[0].uuid + }) + } } + queryInstances() - }, [onFetchInstances]) + // eslint-disable-next-line + }, []) + // empty dependency represents only run this effect once at the begining time - function handleInstanceChange(val: string | undefined) { - dispatch({ - type: 'change_instance', - payload: val - }) - if (val === undefined) { - return + useEffect(() => { + async function queryStatementStatus() { + if (state.curInstance) { + const res = await onGetStatementStatus(state.curInstance) + if (res !== undefined) { + // TODO: set on or off according res + // dispatch({ + // type: 'change_statement_status', + // payload: 'on' + // }) + } + } + } + + async function querySchemas() { + if (state.curInstance) { + const res = await onFetchSchemas(state.curInstance) + dispatch({ + type: 'save_schemas', + payload: res || [] + }) + } + } + + async function queryTimeRanges() { + if (state.curInstance) { + const res = await onFetchTimeRanges(state.curInstance) + dispatch({ + type: 'save_time_ranges', + payload: res || [] + }) + if (res && res.length > 0 && !state.curTimeRange) { + dispatch({ + type: 'change_time_range', + payload: res[0] + }) + } + } } + queryStatementStatus() querySchemas() queryTimeRanges() - queryStatementList() - } - - function handleSchemaChange(val: string[]) { - dispatch({ - type: 'change_schema', - payload: val - }) - queryTimeRanges() - queryStatementList() - } - - function handleTimeRangeChange(val: string | undefined) { - dispatch({ - type: 'change_time_range', - payload: val - }) - queryStatementList() - } + // eslint-disable-next-line + }, [state.curInstance]) + // don't add the dependent functions likes onFetchTimeRanges into the dependency array + // it will cause the infinite loop + // wrap them by useCallback() in the parent component can fix it but I don't think it is necessary - async function queryStatementStatus() { - const res = await onGetStatementStatus(state.curInstance!) - if (res !== undefined) { - // TODO: set on or off according res + useEffect(() => { + async function queryStatementList() { + if (!state.curInstance || !state.curTimeRange) { + return + } + dispatch({ + type: 'set_statements_loading' + }) + const res = await onFetchStatements( + state.curInstance, + state.curSchemas, + state.curTimeRange.begin_time, + state.curTimeRange.end_time + ) dispatch({ - type: 'change_statement_status', - payload: 'on' + type: 'save_statements', + payload: res || [] }) } - } - async function querySchemas() { - const res = await onFetchSchemas(state.curInstance!) - dispatch({ - type: 'save_schemas', - payload: res || [] + queryStatementList() + // update context + setSearchOptions({ + curInstance: state.curInstance, + curSchemas: state.curSchemas, + curTimeRange: state.curTimeRange }) - } + // eslint-disable-next-line + }, [state.curInstance, state.curSchemas, state.curTimeRange]) + // don't add the dependent functions likes onFetchStatements into the dependency array + // it will cause the infinite loop + // wrap them by useCallback() in the parent component can fix it but I don't think it is necessary - async function queryTimeRanges() { - const res = await onFetchTimeRanges(state.curInstance!, state.curSchema) + function handleInstanceChange(val: string | undefined) { dispatch({ - type: 'save_time_ranges', - payload: res || [] + type: 'change_instance', + payload: val }) } - async function queryStatementList() { + function handleSchemaChange(val: string[]) { dispatch({ - type: 'set_statements_loading' + type: 'change_schema', + payload: val }) - const res = await onFetchStatements( - state.curInstance!, - state.curSchema, - state.curTimeRange - ) + } + + function handleTimeRangeChange(val: string | undefined) { + const timeRange = state.timeRanges.find(item => item.begin_time === val) dispatch({ - type: 'save_statements', - payload: res || [] + type: 'change_time_range', + payload: timeRange }) } @@ -271,23 +319,38 @@ export default function StatementList({ return (
+ {false && ( + + )} - {state.statementStatus === 'on' && (
)} {state.statementStatus === 'off' && ( - )} @@ -354,10 +404,13 @@ export default function StatementList({ onUpdateConfig={onUpdateConfig} /> )} - +
+ +
) } diff --git a/ui/src/apps/statement/components/StatementsTable.tsx b/ui/src/apps/statement/components/StatementsTable.tsx index 06843ffaf4..5d305d2a08 100644 --- a/ui/src/apps/statement/components/StatementsTable.tsx +++ b/ui/src/apps/statement/components/StatementsTable.tsx @@ -4,71 +4,93 @@ import { Link } from 'react-router-dom' import { Table } from 'antd' import { getValueFormat } from '@baurine/grafana-value-formats' import { HorizontalBar } from './HorizontalBar' -import { Statement } from './statement-types' +import { StatementOverview, StatementTimeRange } from './statement-types' +import { useTranslation } from 'react-i18next' const tableColumns = ( - maxTotalTimes: number, - maxAvgDuration: number, - maxCostMem: number + timeRange: StatementTimeRange, + maxExecCount: number, + maxAvgLatency: number, + maxAvgMem: number, + t: (string) => string ) => [ { - title: 'SQL 类别', - dataIndex: 'sql_category', - key: 'sql_category', - render: text => ( - {text} + title: t('statement.common.schema'), + dataIndex: 'schema_name', + key: 'schema_name' + }, + { + title: t('statement.common.digest_text'), + dataIndex: 'digest_text', + key: 'digest_text', + width: 400, + render: (text, record: StatementOverview) => ( + + {text} + ) }, { - title: '总时长', - dataIndex: 'total_duration', - key: 'total_duration', - sorter: (a: Statement, b: Statement) => a.total_duration - b.total_duration, - render: text => getValueFormat('s')(text, 2, null) + title: t('statement.common.sum_latency'), + dataIndex: 'sum_latency', + key: 'sum_latency', + sorter: (a: StatementOverview, b: StatementOverview) => + a.sum_latency - b.sum_latency, + render: text => getValueFormat('ns')(text, 2, null) }, { - title: '总次数', - dataIndex: 'total_times', - key: 'total_times', + title: t('statement.common.exec_count'), + dataIndex: 'exec_count', + key: 'exec_count', + sorter: (a: StatementOverview, b: StatementOverview) => + a.exec_count - b.exec_count, render: text => (
{getValueFormat('short')(text, 0, 0)}
) }, { - title: '平均影响行数', - dataIndex: 'avg_affect_lines', - key: 'avg_affect_lines', + title: t('statement.common.avg_affected_rows'), + dataIndex: 'avg_affected_rows', + key: 'avg_affected_rows', + sorter: (a: StatementOverview, b: StatementOverview) => + a.avg_affected_rows - b.avg_affected_rows, render: text => getValueFormat('short')(text, 0, 0) }, { - title: '平均时长', - dataIndex: 'avg_duration', - key: 'avg_duration', + title: t('statement.common.avg_latency'), + dataIndex: 'avg_latency', + key: 'avg_latency', + sorter: (a: StatementOverview, b: StatementOverview) => + a.avg_latency - b.avg_latency, render: text => (
- {getValueFormat('ms')(text, 2, null)} + {getValueFormat('ns')(text, 2, null)}
) }, { - title: '平均消耗内存', - dataIndex: 'avg_cost_mem', - key: 'avg_cost_mem', + title: t('statement.common.avg_mem'), + dataIndex: 'avg_mem', + key: 'avg_mem', + sorter: (a: StatementOverview, b: StatementOverview) => + a.avg_mem - b.avg_mem, render: text => (
- {getValueFormat('mbytes')(text, 2, null)} + {getValueFormat('bytes')(text, 2, null)}
@@ -77,32 +99,38 @@ const tableColumns = ( ] interface Props { - statements: Statement[] + statements: StatementOverview[] loading: boolean + timeRange: StatementTimeRange } -export default function StatementsTable({ statements, loading }: Props) { - const maxTotalTimes = useMemo( - () => _.max(statements.map(s => s.total_times)), +export default function StatementsTable({ + statements, + loading, + timeRange +}: Props) { + const {t} = useTranslation() + const maxExecCount = useMemo( + () => _.max(statements.map(s => s.exec_count)) || 1, [statements] ) - const maxAvgDuration = useMemo( - () => _.max(statements.map(s => s.avg_duration)), + const maxAvgLatency = useMemo( + () => _.max(statements.map(s => s.avg_latency)) || 1, [statements] ) - const maxCostMem = useMemo(() => _.max(statements.map(s => s.avg_cost_mem)), [ + const maxAvgMem = useMemo(() => _.max(statements.map(s => s.avg_mem)) || 1, [ statements ]) const columns = useMemo( - () => tableColumns(maxTotalTimes!, maxAvgDuration!, maxCostMem!), - [maxAvgDuration, maxCostMem, maxTotalTimes] + () => tableColumns(timeRange, maxExecCount!, maxAvgLatency!, maxAvgMem!, t), + [timeRange, maxExecCount, maxAvgLatency, maxAvgMem, t] ) return (
`${record.digest}_${index}`} pagination={false} /> ) diff --git a/ui/src/apps/statement/components/index.ts b/ui/src/apps/statement/components/index.ts index f0dc3124d3..28154dee6e 100644 --- a/ui/src/apps/statement/components/index.ts +++ b/ui/src/apps/statement/components/index.ts @@ -1,6 +1,8 @@ -import StatementList from './StatementList' +import StatementsOverview from './StatementsOverview' import StatementDetail from './StatementDetail' -export { StatementList, StatementDetail } +export { StatementsOverview, StatementDetail } export * from './statement-types' + +export * from './search-options-context' diff --git a/ui/src/apps/statement/components/search-options-context.ts b/ui/src/apps/statement/components/search-options-context.ts new file mode 100644 index 0000000000..a6ee3a5a3e --- /dev/null +++ b/ui/src/apps/statement/components/search-options-context.ts @@ -0,0 +1,23 @@ +import React from 'react' + +import { StatementTimeRange } from './statement-types' + +export interface SearchOptions { + curInstance: string | undefined + curSchemas: string[] + curTimeRange: StatementTimeRange | undefined +} + +export interface SearchContextType { + searchOptions: SearchOptions + setSearchOptions: (otpions: SearchOptions) => void +} + +export const SearchContext = React.createContext({ + searchOptions: { + curInstance: undefined, + curSchemas: [], + curTimeRange: undefined + }, + setSearchOptions: (_options: SearchOptions) => {} +}) diff --git a/ui/src/apps/statement/components/statement-types.ts b/ui/src/apps/statement/components/statement-types.ts index cebf775457..216faede03 100644 --- a/ui/src/apps/statement/components/statement-types.ts +++ b/ui/src/apps/statement/components/statement-types.ts @@ -12,43 +12,45 @@ export interface StatementConfig { max_sql_length: number } -export interface Statement { - sql_category: string - total_duration: number - total_times: number - avg_affect_lines: number - avg_duration: number - avg_cost_mem: number -} - ////////////////// -export interface StatementSummary { - sql_category: string - last_sql: string - last_time: string - schemas: string[] +export interface StatementTimeRange { + begin_time: string + end_time: string } -export interface StatementStatis { - total_duration: number - total_times: number - avg_affect_lines: number - avg_scan_lines: number +export interface StatementOverview { + schema_name: string + digest: string + digest_text: string + sum_latency: number + exec_count: number + avg_affected_rows: number + avg_latency: number + avg_mem: number } -export interface StatementNode { - node_name: string - total_duration: number - total_times: number - avg_duration: number - max_duration: number - avg_cost_mem: number - back_off_times: number -} +////////////////// export interface StatementDetailInfo { - summary: StatementSummary - statis: StatementStatis - nodes: StatementNode[] + schema_name: string + digest: string + digest_text: string + sum_latency: number + exec_count: number + avg_affected_rows: number + avg_total_keys: number + + query_sample_text: string + last_seen: string +} + +export interface StatementNode { + address: string + sum_latency: number + exec_count: number + avg_latency: number + max_latency: number + avg_mem: number + sum_backoff_times: number } diff --git a/ui/src/apps/statement/components/StatementDetail.module.css b/ui/src/apps/statement/components/styles.module.css similarity index 73% rename from ui/src/apps/statement/components/StatementDetail.module.css rename to ui/src/apps/statement/components/styles.module.css index db681cb5aa..5c6a967516 100644 --- a/ui/src/apps/statement/components/StatementDetail.module.css +++ b/ui/src/apps/statement/components/styles.module.css @@ -1,7 +1,3 @@ -.statement_detail { - /* background-color: white; */ -} - .statement_summary { display: flex; margin-bottom: 12px; @@ -20,3 +16,8 @@ padding: 12px; margin-left: 12px; } + +/* for StatementSettingModal */ +.config_form :global(.ant-form-item) { + margin-bottom: 0; +} diff --git a/ui/src/apps/statement/index.js b/ui/src/apps/statement/index.js index 4947c82395..1a302cc2bf 100644 --- a/ui/src/apps/statement/index.js +++ b/ui/src/apps/statement/index.js @@ -3,4 +3,5 @@ module.exports = { loader: () => import('./app.js'), routerPrefix: '/statement', icon: 'line-chart', -}; + translations: require.context('./translations/', false, /\.yaml$/) +} diff --git a/ui/src/apps/statement/translations/en.yaml b/ui/src/apps/statement/translations/en.yaml new file mode 100644 index 0000000000..274f25ea7a --- /dev/null +++ b/ui/src/apps/statement/translations/en.yaml @@ -0,0 +1,21 @@ +statement: + nav_title: Statement + filters: + select_time: Select time range + select_schemas: Select schemas + common: + schema: Schema + digest_text: SQL Category + sum_latency: Sum Latency + exec_count: Exec Count + avg_affected_rows: Avg Affected Rows + avg_latency: Avg Latency + max_latency: Max Latency + avg_mem: Avg Cost Memory + sum_backoff_times: Sum Backoff Times + avg_total_keys: Avg Scan Rows + detail: + node: Node + time_range: Time Range + query_sample_text: Last SQL Statement + last_seen: Last Seen diff --git a/ui/src/apps/statement/translations/zh-CN.yaml b/ui/src/apps/statement/translations/zh-CN.yaml new file mode 100644 index 0000000000..79d594e872 --- /dev/null +++ b/ui/src/apps/statement/translations/zh-CN.yaml @@ -0,0 +1,21 @@ +statement: + nav_title: Statement + filters: + select_time: 选择时间段 + select_schemas: 选择数据库 + common: + schema: 数据库 + digest_text: SQL 类别 + sum_latency: 总时长 + exec_count: 总次数 + avg_affected_rows: 平均影响行数 + avg_latency: 平均时长 + max_latency: 最大时长 + avg_mem: 平均消耗内存 + sum_backoff_times: Backoff 重试次数 + avg_total_keys: 平均扫描行数 + detail: + node: 节点 + time_range: 时间段 + query_sample_text: 最后出现 SQL 语句 + last_seen: 最后出现时间 diff --git a/ui/src/layout/RootComponent.js b/ui/src/layout/RootComponent.js index 377f56d0ad..553fec7369 100644 --- a/ui/src/layout/RootComponent.js +++ b/ui/src/layout/RootComponent.js @@ -58,7 +58,7 @@ class App extends React.PureComponent { {app.icon ? : null} - {this.props.t(`${appId}.nav_title`)} + {this.props.t(`${appId}.nav_title`, appId)} ); diff --git a/ui/src/utils/client/index.js b/ui/src/utils/client/index.js index eb1beb6ceb..73eec9603e 100644 --- a/ui/src/utils/client/index.js +++ b/ui/src/utils/client/index.js @@ -36,6 +36,9 @@ axios.interceptors.response.use(undefined, function(err) { } else if (err.message === 'Network Error') { message.error(i18n.t('error.message.network')); err.handled = true; + } else if (response && response.data) { + message.error(response.data.message); + err.handled = true; } return Promise.reject(err); });