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' && (
-
)
}
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);
});