diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 898779a880..985aaf3d57 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -12,12 +12,14 @@ Thanks for using TiDB Dashboard! Before asking a question, please take a look in - GitHub issues https://github.com/pingcap-incubator/tidb-dashboard/issues?q=is%3Aissue +- Documentation (English) + https://docs.pingcap.com/tidb/stable/dashboard-intro - Documentation (Chinese) - https://pingcap.com/docs-cn/stable/dashboard/dashboard-intro/ + https://docs.pingcap.com/zh/tidb/stable/dashboard-intro - AskTUG forum (Chinese) https://asktug.com/ You might also get a faster response in Slack (English / Chinese): - https://slack.tidb.io/invite?team=tidb-community&channel=sig-dashboard&ref=github_issue_create + https://slack.tidb.io/invite?team=tidb-community&channel=sig-diagnosis&ref=github_issue_create --> diff --git a/.github/workflows/e2e-test.yaml b/.github/workflows/e2e-test.yaml index 3854554b05..4c55365be4 100644 --- a/.github/workflows/e2e-test.yaml +++ b/.github/workflows/e2e-test.yaml @@ -35,7 +35,7 @@ jobs: key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - - name: Load tiup cache + - name: Load TiUP cache uses: actions/cache@v1 with: path: ~/.tiup/components @@ -46,7 +46,7 @@ jobs: run: | curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh source /home/runner/.profile - tiup update --nightly --all + tiup update --nightly tiup playground nightly --tiflash=0 & - name: Build UI run: | @@ -55,6 +55,20 @@ jobs: NO_MINIMIZE: true CI: true REACT_APP_MIXPANEL_TOKEN: "" + - name: Debug TiUP + run: | + source /home/runner/.profile + tiup --version + ls /home/runner/.tiup/components/playground/ + DATA_PATH=$(ls /home/runner/.tiup/data/) + echo $DATA_PATH + tiup playground display + echo "==== TiDB Log ====" + head -n 3 /home/runner/.tiup/data/$DATA_PATH/tidb-0/tidb.log + echo "==== TiKV Log ====" + head -n 3 /home/runner/.tiup/data/$DATA_PATH/tikv-0/tikv.log + echo "==== PD Log ====" + head -n 3 /home/runner/.tiup/data/$DATA_PATH/pd-0/pd.log - name: Build and run backend in the background run: | make diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 46b5b041c6..97d028dc77 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,9 +2,9 @@ Thanks for your interest in contributing to TiDB Dashboard! This document outlines some of the conventions on building, running, and testing TiDB Dashboard, the development workflow, commit message formatting, contact points and other resources. -If you need any help (for example, mentoring getting started or understanding the codebase), feel free to join the discussion of [TiDB Dashboard SIG] (Special Interest Group): +If you need any help (for example, mentoring getting started or understanding the codebase), feel free to join the discussion of [Diagnosis SIG] (Special Interest Group): -- Slack: [#sig-dashboard](https://slack.tidb.io/invite?team=tidb-community&channel=sig-dashboard&ref=github_dashboard_repo) +- Slack: [#sig-diagnosis](https://slack.tidb.io/invite?team=tidb-community&channel=sig-diagnosis&ref=github_dashboard_repo) ## Setting up a development workspace @@ -251,7 +251,7 @@ If the change affects many subsystems, you can use `*` instead, like `*: foo`. The body of the commit message should describe why the change was made and at a high level, how the code works. -[tidb dashboard sig]: https://github.com/pingcap/community/tree/master/special-interest-groups/sig-dashboard +[diagnosis sig]: https://github.com/pingcap/community/tree/master/special-interest-groups/sig-diagnosis [pd]: https://github.com/pingcap/pd [tidb]: https://github.com/pingcap/tidb [tikv]: https://github.com/tikv/tikv diff --git a/Makefile b/Makefile index 31b813d50f..7ee85030a8 100644 --- a/Makefile +++ b/Makefile @@ -42,4 +42,4 @@ endif go build -o bin/tidb-dashboard -ldflags '$(LDFLAGS)' -tags "${BUILD_TAGS}" cmd/tidb-dashboard/main.go run: - bin/tidb-dashboard --debug + bin/tidb-dashboard --debug --experimental diff --git a/README.md b/README.md index a82dba6957..7761d82ca6 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,17 @@ TiDB Dashboard is a Web UI for monitoring, diagnosing and managing the TiDB clus ## Documentation -- [Product User Manual (Chinese)](https://pingcap.com/docs-cn/stable/dashboard/dashboard-intro/) -- [FAQ (Chinese)](https://pingcap.com/docs-cn/stable/dashboard/dashboard-faq/) +- [Product User Manual (English)](https://docs.pingcap.com/tidb/stable/dashboard-intro) +- [Product User Manual (Chinese)](https://docs.pingcap.com/zh/tidb/stable/dashboard-intro) +- [FAQ (English)](https://docs.pingcap.com/tidb/stable/dashboard-faq) +- [FAQ (Chinese)](https://docs.pingcap.com/zh/tidb/stable/dashboard-faq) ## Question, Suggestion Feel free to [open GitHub issues](https://github.com/pingcap-incubator/tidb-dashboard/issues/new/choose) for questions, support and suggestions. -You may also consider join our community chat in the Slack channel [#sig-dashboard]. +You may also consider join our community chat in the Slack channel [#sig-diagnosis]. For Chinese users, you can visit the PingCAP official user forum [AskTUG.com] to make life easier. @@ -35,7 +37,7 @@ for a list of recommended tasks, in which we have also marked the difficulty lev See [CONTRIBUTING.md](./CONTRIBUTING.md) for a detailed step-by-step contributing guide, or steps to build TiDB Dashboard from source. -If you need any help, feel free to community chat in the Slack channel [#sig-dashboard]. +If you need any help, feel free to community chat in the Slack channel [#sig-diagnosis]. Thank you to all the people who already contributed to TiDB Dashboard! @@ -65,6 +67,7 @@ Thank you to all the people who already contributed to TiDB Dashboard! + ## Architecture @@ -83,5 +86,5 @@ TiDB Dashboard can also be integrated into PD, as follows: Copyright 2020 PingCAP, Inc. [pd]: https://github.com/pingcap/pd -[#sig-dashboard]: https://slack.tidb.io/invite?team=tidb-community&channel=sig-dashboard&ref=github_dashboard_repo +[#sig-diagnosis]: https://slack.tidb.io/invite?team=tidb-community&channel=sig-diagnosis&ref=github_dashboard_repo [asktug.com]: https://asktug.com/ diff --git a/cmd/tidb-dashboard/main.go b/cmd/tidb-dashboard/main.go index a05c13fd18..bccfba6651 100644 --- a/cmd/tidb-dashboard/main.go +++ b/cmd/tidb-dashboard/main.go @@ -63,15 +63,16 @@ type DashboardCLIConfig struct { // NewCLIConfig generates the configuration of the dashboard in standalone mode. func NewCLIConfig() *DashboardCLIConfig { cfg := &DashboardCLIConfig{} - cfg.CoreConfig = &config.Config{} + cfg.CoreConfig = config.Default() flag.StringVarP(&cfg.ListenHost, "host", "h", "127.0.0.1", "listen host of the Dashboard Server") flag.IntVarP(&cfg.ListenPort, "port", "p", 12333, "listen port of the Dashboard Server") flag.BoolVarP(&cfg.EnableDebugLog, "debug", "d", false, "enable debug logs") - flag.StringVar(&cfg.CoreConfig.DataDir, "data-dir", "/tmp/dashboard-data", "path to the Dashboard Server data directory") - flag.StringVar(&cfg.CoreConfig.PublicPathPrefix, "path-prefix", config.DefaultPublicPathPrefix, "public URL path prefix for reverse proxies") - flag.StringVar(&cfg.CoreConfig.PDEndPoint, "pd", "http://127.0.0.1:2379", "PD endpoint address that Dashboard Server connects to") - flag.BoolVar(&cfg.CoreConfig.EnableTelemetry, "enable-telemetry", true, "enable client to report data for analysis") + flag.StringVar(&cfg.CoreConfig.DataDir, "data-dir", cfg.CoreConfig.DataDir, "path to the Dashboard Server data directory") + flag.StringVar(&cfg.CoreConfig.PublicPathPrefix, "path-prefix", cfg.CoreConfig.PublicPathPrefix, "public URL path prefix for reverse proxies") + flag.StringVar(&cfg.CoreConfig.PDEndPoint, "pd", cfg.CoreConfig.PDEndPoint, "PD endpoint address that Dashboard Server connects to") + flag.BoolVar(&cfg.CoreConfig.EnableTelemetry, "telemetry", cfg.CoreConfig.EnableTelemetry, "allow telemetry") + flag.BoolVar(&cfg.CoreConfig.EnableExperimental, "experimental", cfg.CoreConfig.EnableExperimental, "allow experimental features") showVersion := flag.BoolP("version", "v", false, "print version information and exit") diff --git a/etc/manualTestEnv/multiHost/README.md b/etc/manualTestEnv/multiHost/README.md index a747f7db4a..60b8058c4f 100644 --- a/etc/manualTestEnv/multiHost/README.md +++ b/etc/manualTestEnv/multiHost/README.md @@ -13,7 +13,7 @@ TiDB, PD, TiKV, TiFlash each in different hosts. 1. Use [TiUP](https://tiup.io/) to deploy the cluster to the box (only need to do it once): ```bash - tiup cluster deploy multiHost v4.0.0 topology.yaml -i ../_shared/vagrant_key -y --user vagrant + tiup cluster deploy multiHost v4.0.4 topology.yaml -i ../_shared/vagrant_key -y --user vagrant ``` 1. Start the cluster in the box: diff --git a/etc/manualTestEnv/multiReplica/README.md b/etc/manualTestEnv/multiReplica/README.md index b1a027e644..fc31ff5941 100644 --- a/etc/manualTestEnv/multiReplica/README.md +++ b/etc/manualTestEnv/multiReplica/README.md @@ -13,7 +13,7 @@ Multiple TiKV nodes in different labels. 1. Use [TiUP](https://tiup.io/) to deploy the cluster to the box (only need to do it once): ```bash - tiup cluster deploy multiReplica v4.0.0 topology.yaml -i ../_shared/vagrant_key -y --user vagrant + tiup cluster deploy multiReplica v4.0.4 topology.yaml -i ../_shared/vagrant_key -y --user vagrant ``` 1. Start the cluster in the box: diff --git a/etc/manualTestEnv/singleHost/README.md b/etc/manualTestEnv/singleHost/README.md index 27e1e2995c..4d3f7413ab 100644 --- a/etc/manualTestEnv/singleHost/README.md +++ b/etc/manualTestEnv/singleHost/README.md @@ -13,7 +13,7 @@ TiDB, PD, TiKV, TiFlash in the same host. 1. Use [TiUP](https://tiup.io/) to deploy the cluster to the box (only need to do it once): ```bash - tiup cluster deploy singleHost v4.0.0 topology.yaml -i ../_shared/vagrant_key -y --user vagrant + tiup cluster deploy singleHost v4.0.4 topology.yaml -i ../_shared/vagrant_key -y --user vagrant ``` 1. Start the cluster in the box: diff --git a/etc/manualTestEnv/singleHostMultiDisk/README.md b/etc/manualTestEnv/singleHostMultiDisk/README.md index 0a591b450b..4cb2dd419e 100644 --- a/etc/manualTestEnv/singleHostMultiDisk/README.md +++ b/etc/manualTestEnv/singleHostMultiDisk/README.md @@ -13,7 +13,7 @@ All instances in a single host, but on different disks. 1. Use [TiUP](https://tiup.io/) to deploy the cluster to the box (only need to do it once): ```bash - tiup cluster deploy singleHostMultiDisk v4.0.0 topology.yaml -i ../_shared/vagrant_key -y --user vagrant + tiup cluster deploy singleHostMultiDisk v4.0.4 topology.yaml -i ../_shared/vagrant_key -y --user vagrant ``` 1. Start the cluster in the box: diff --git a/go.mod b/go.mod index 1669a4e1a7..8dfbb9352d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/pingcap-incubator/tidb-dashboard go 1.13 require ( + github.com/VividCortex/mysqlerr v0.0.0-20200629151747-c28746d985dd github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 github.com/appleboy/gin-jwt/v2 v2.6.3 github.com/cenkalti/backoff/v4 v4.0.2 @@ -29,9 +30,10 @@ require ( github.com/shurcooL/httpgzip v0.0.0-20190720172056-320755c1c1b0 github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd github.com/spf13/pflag v1.0.1 - github.com/stretchr/testify v1.4.0 + github.com/stretchr/testify v1.5.1 github.com/swaggo/http-swagger v0.0.0-20200308142732-58ac5e232fba github.com/swaggo/swag v1.6.6-0.20200529100950-7c765ddd0476 + github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 go.uber.org/atomic v1.5.0 go.uber.org/fx v1.10.0 diff --git a/go.sum b/go.sum index c33fe93f29..a5369d5a0d 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VividCortex/mysqlerr v0.0.0-20200629151747-c28746d985dd h1:59Whn6shj5MTVjTf2OX6+7iMcmY6h5CK0kTWwRaplL4= +github.com/VividCortex/mysqlerr v0.0.0-20200629151747-c28746d985dd/go.mod h1:f3HiCrHjHBdcm6E83vGaXh1KomZMA2P6aeo3hKx/wg0= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -127,6 +129,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= @@ -299,6 +303,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0= @@ -329,6 +335,11 @@ github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 h1:d71/KA0LhvkrJ/Ok+Wx9qK7bU8meKA1Hk0jpVI5kJjk= +github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oTuqa03RjMwMfk/7/TCs+QI= +github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yookoala/realpath v1.0.0/go.mod h1:gJJMA9wuX7AcqLy1+ffPatSCySA1FQ2S8Ya9AIoYBpE= @@ -391,6 +402,7 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -399,6 +411,7 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 h1:2mqDk8w/o6UmeUCu5Qiq2y7iMf6anbx+YA8d1JFoFrs= golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 h1:eDrdRpKgkcCqKZQwyZRyeFZgfqt37SL7Kv3tok06cKE= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -457,6 +470,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index eb6b9a9035..3b23d5e37f 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -26,23 +26,25 @@ import ( "go.uber.org/fx" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/clusterinfo" + "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/configuration" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/diagnose" - "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/logsearch" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/metrics" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/profiling" + "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/queryeditor" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/slowquery" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/statement" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user" apiutils "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" "github.com/pingcap-incubator/tidb-dashboard/pkg/config" "github.com/pingcap-incubator/tidb-dashboard/pkg/dbstore" - pkghttp "github.com/pingcap-incubator/tidb-dashboard/pkg/http" + "github.com/pingcap-incubator/tidb-dashboard/pkg/httpc" "github.com/pingcap-incubator/tidb-dashboard/pkg/keyvisual" keyvisualregion "github.com/pingcap-incubator/tidb-dashboard/pkg/keyvisual/region" "github.com/pingcap-incubator/tidb-dashboard/pkg/pd" "github.com/pingcap-incubator/tidb-dashboard/pkg/tidb" + "github.com/pingcap-incubator/tidb-dashboard/pkg/tikv" "github.com/pingcap-incubator/tidb-dashboard/pkg/utils" "github.com/pingcap-incubator/tidb-dashboard/pkg/utils/version" ) @@ -103,14 +105,13 @@ func (s *Service) Start(ctx context.Context) error { newAPIHandlerEngine, s.provideLocals, dbstore.NewDBStore, + httpc.NewHTTPClient, pd.NewEtcdClient, pd.NewPDClient, config.NewDynamicConfigManager, - tidb.NewForwarderConfig, - tidb.NewForwarder, - pkghttp.NewHTTPClientWithConf, + tidb.NewTiDBClient, + tikv.NewTiKVClient, user.NewAuthService, - foo.NewService, info.NewService, clusterinfo.NewService, profiling.NewService, @@ -120,11 +121,12 @@ func (s *Service) Start(ctx context.Context) error { diagnose.NewService, keyvisual.NewService, metrics.NewService, + queryeditor.NewService, + configuration.NewService, ), fx.Populate(&s.apiHandlerEngine), fx.Invoke( user.Register, - foo.Register, info.Register, clusterinfo.Register, profiling.Register, @@ -134,6 +136,8 @@ func (s *Service) Start(ctx context.Context) error { diagnose.Register, keyvisual.Register, metrics.Register, + queryeditor.Register, + configuration.Register, // Must be at the end s.status.Register, ), @@ -192,7 +196,7 @@ func newAPIHandlerEngine() (apiHandlerEngine *gin.Engine, endpoint *gin.RouterGr apiHandlerEngine = gin.New() apiHandlerEngine.Use(gin.Recovery()) apiHandlerEngine.Use(cors.AllowAll()) - apiHandlerEngine.Use(gzip.Gzip(gzip.BestSpeed)) + apiHandlerEngine.Use(gzip.Gzip(gzip.DefaultCompression)) apiHandlerEngine.Use(apiutils.MWHandleErrors()) endpoint = apiHandlerEngine.Group("/dashboard/api") diff --git a/pkg/apiserver/clusterinfo/service.go b/pkg/apiserver/clusterinfo/service.go index 9dcdce7f7a..6b9eb70591 100644 --- a/pkg/apiserver/clusterinfo/service.go +++ b/pkg/apiserver/clusterinfo/service.go @@ -30,27 +30,27 @@ import ( "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/httpc" "github.com/pingcap-incubator/tidb-dashboard/pkg/pd" "github.com/pingcap-incubator/tidb-dashboard/pkg/tidb" "github.com/pingcap-incubator/tidb-dashboard/pkg/utils/topology" ) +type ServiceParams struct { + fx.In + PDClient *pd.Client + EtcdClient *clientv3.Client + HTTPClient *httpc.Client + TiDBClient *tidb.Client +} + type Service struct { + params ServiceParams lifecycleCtx context.Context - - pdClient *pd.Client - etcdClient *clientv3.Client - httpClient *http.Client - tidbForwarder *tidb.Forwarder } -func NewService(lc fx.Lifecycle, pdClient *pd.Client, etcdClient *clientv3.Client, httpClient *http.Client, tidbForwarder *tidb.Forwarder) *Service { - s := &Service{ - pdClient: pdClient, - etcdClient: etcdClient, - httpClient: httpClient, - tidbForwarder: tidbForwarder, - } +func NewService(lc fx.Lifecycle, p ServiceParams) *Service { + s := &Service{params: p} lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { s.lifecycleCtx = ctx @@ -71,15 +71,15 @@ func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint.GET("/alertmanager/:address/count", s.getAlertManagerCounts) endpoint.GET("/grafana", s.getGrafanaTopology) + endpoint.GET("/store_location", s.getStoreLocationTopology) + endpoint = r.Group("/host") endpoint.Use(auth.MWAuthRequired()) - endpoint.Use(utils.MWConnectTiDB(s.tidbForwarder)) + endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient)) endpoint.GET("/all", s.getHostsInfo) } -// @Summary Delete etcd's tidb key. -// @Description Delete etcd's TiDB key with ip:port. -// @Produce json +// @Summary Hide a TiDB instance // @Param address path string true "ip:port" // @Success 200 "delete ok" // @Failure 401 {object} utils.APIError "Unauthorized failure" @@ -98,7 +98,7 @@ func (s *Service) deleteTiDBTopology(c *gin.Context) { wg.Add(1) go func(toDel string) { defer wg.Done() - if _, err := s.etcdClient.Delete(ctx, toDel); err != nil { + if _, err := s.params.EtcdClient.Delete(ctx, toDel); err != nil { errorChannel <- err } }(key) @@ -119,15 +119,13 @@ func (s *Service) deleteTiDBTopology(c *gin.Context) { } // @ID getTiDBTopology -// @Summary Get TiDB instances -// @Description Get TiDB instances topology -// @Produce json +// @Summary Get all TiDB instances // @Success 200 {array} topology.TiDBInfo // @Router /topology/tidb [get] // @Security JwtAuth // @Failure 401 {object} utils.APIError "Unauthorized failure" func (s *Service) getTiDBTopology(c *gin.Context) { - instances, err := topology.FetchTiDBTopology(s.lifecycleCtx, s.etcdClient) + instances, err := topology.FetchTiDBTopology(s.lifecycleCtx, s.params.EtcdClient) if err != nil { _ = c.Error(err) return @@ -141,15 +139,13 @@ type StoreTopologyResponse struct { } // @ID getStoreTopology -// @Summary Get TiKV / TiFlash instances -// @Description Get TiKV / TiFlash instances topology -// @Produce json +// @Summary Get all TiKV / TiFlash instances // @Success 200 {object} StoreTopologyResponse // @Router /topology/store [get] // @Security JwtAuth // @Failure 401 {object} utils.APIError "Unauthorized failure" func (s *Service) getStoreTopology(c *gin.Context) { - tikvInstances, tiFlashInstances, err := topology.FetchStoreTopology(s.pdClient) + tikvInstances, tiFlashInstances, err := topology.FetchStoreTopology(s.params.PDClient) if err != nil { _ = c.Error(err) return @@ -160,16 +156,29 @@ func (s *Service) getStoreTopology(c *gin.Context) { }) } +// @ID getStoreLocationTopology +// @Summary Get location labels of all TiKV / TiFlash instances +// @Success 200 {object} topology.StoreLocation +// @Router /topology/store_location [get] +// @Security JwtAuth +// @Failure 401 {object} utils.APIError "Unauthorized failure" +func (s *Service) getStoreLocationTopology(c *gin.Context) { + storeLocation, err := topology.FetchStoreLocation(s.params.PDClient) + if err != nil { + _ = c.Error(err) + return + } + c.JSON(http.StatusOK, storeLocation) +} + // @ID getPDTopology -// @Summary Get PD instances -// @Description Get PD instances topology -// @Produce json +// @Summary Get all PD instances // @Success 200 {array} topology.PDInfo // @Router /topology/pd [get] // @Security JwtAuth // @Failure 401 {object} utils.APIError "Unauthorized failure" func (s *Service) getPDTopology(c *gin.Context) { - instances, err := topology.FetchPDTopology(s.pdClient) + instances, err := topology.FetchPDTopology(s.params.PDClient) if err != nil { _ = c.Error(err) return @@ -179,14 +188,12 @@ func (s *Service) getPDTopology(c *gin.Context) { // @ID getAlertManagerTopology // @Summary Get AlertManager instance -// @Description Get AlertManager instance topology -// @Produce json // @Success 200 {object} topology.AlertManagerInfo // @Router /topology/alertmanager [get] // @Security JwtAuth // @Failure 401 {object} utils.APIError "Unauthorized failure" func (s *Service) getAlertManagerTopology(c *gin.Context) { - instance, err := topology.FetchAlertManagerTopology(s.lifecycleCtx, s.etcdClient) + instance, err := topology.FetchAlertManagerTopology(s.lifecycleCtx, s.params.EtcdClient) if err != nil { _ = c.Error(err) return @@ -196,14 +203,12 @@ func (s *Service) getAlertManagerTopology(c *gin.Context) { // @ID getGrafanaTopology // @Summary Get Grafana instance -// @Description Get Grafana instance topology -// @Produce json // @Success 200 {object} topology.GrafanaInfo // @Router /topology/grafana [get] // @Security JwtAuth // @Failure 401 {object} utils.APIError "Unauthorized failure" func (s *Service) getGrafanaTopology(c *gin.Context) { - instance, err := topology.FetchGrafanaTopology(s.lifecycleCtx, s.etcdClient) + instance, err := topology.FetchGrafanaTopology(s.lifecycleCtx, s.params.EtcdClient) if err != nil { _ = c.Error(err) return @@ -212,9 +217,7 @@ func (s *Service) getGrafanaTopology(c *gin.Context) { } // @ID getAlertManagerCounts -// @Summary Get alert count -// @Description Get alert count from alert manager -// @Produce json +// @Summary Get current alert count from AlertManager // @Success 200 {object} int // @Param address path string true "ip:port" // @Router /topology/alertmanager/{address}/count [get] @@ -222,7 +225,7 @@ func (s *Service) getGrafanaTopology(c *gin.Context) { // @Failure 401 {object} utils.APIError "Unauthorized failure" func (s *Service) getAlertManagerCounts(c *gin.Context) { address := c.Param("address") - cnt, err := fetchAlertManagerCounts(s.lifecycleCtx, address, s.httpClient) + cnt, err := fetchAlertManagerCounts(s.lifecycleCtx, address, s.params.HTTPClient) if err != nil { _ = c.Error(err) return @@ -231,9 +234,8 @@ func (s *Service) getAlertManagerCounts(c *gin.Context) { } // @ID getHostsInfo -// @Summary Get all host information in the cluster +// @Summary Get information of all hosts // @Description Get information about host in the cluster -// @Produce json // @Success 200 {array} HostInfo // @Router /host/all [get] // @Security JwtAuth @@ -278,7 +280,7 @@ func (s *Service) getHostsInfo(c *gin.Context) { func (s *Service) fetchAllInstanceHostsMap() (map[string]struct{}, error) { allHosts := make(map[string]struct{}) - pdInfo, err := topology.FetchPDTopology(s.pdClient) + pdInfo, err := topology.FetchPDTopology(s.params.PDClient) if err != nil { return nil, err } @@ -286,7 +288,7 @@ func (s *Service) fetchAllInstanceHostsMap() (map[string]struct{}, error) { allHosts[i.IP] = struct{}{} } - tikvInfo, tiFlashInfo, err := topology.FetchStoreTopology(s.pdClient) + tikvInfo, tiFlashInfo, err := topology.FetchStoreTopology(s.params.PDClient) if err != nil { return nil, err } @@ -297,7 +299,7 @@ func (s *Service) fetchAllInstanceHostsMap() (map[string]struct{}, error) { allHosts[i.IP] = struct{}{} } - tidbInfo, err := topology.FetchTiDBTopology(s.lifecycleCtx, s.etcdClient) + tidbInfo, err := topology.FetchTiDBTopology(s.lifecycleCtx, s.params.EtcdClient) if err != nil { return nil, err } diff --git a/pkg/apiserver/clusterinfo/topology.go b/pkg/apiserver/clusterinfo/topology.go index 446af76e24..d46dba7dae 100644 --- a/pkg/apiserver/clusterinfo/topology.go +++ b/pkg/apiserver/clusterinfo/topology.go @@ -6,9 +6,13 @@ import ( "fmt" "io/ioutil" "net/http" + + "github.com/pingcap-incubator/tidb-dashboard/pkg/httpc" ) -func fetchAlertManagerCounts(ctx context.Context, alertManagerAddr string, httpClient *http.Client) (int, error) { +func fetchAlertManagerCounts(ctx context.Context, alertManagerAddr string, httpClient *httpc.Client) (int, error) { + // FIXME: Use httpClient.SendGetRequest + uri := fmt.Sprintf("http://%s/api/v2/alerts", alertManagerAddr) req, err := http.NewRequestWithContext(ctx, "GET", uri, nil) if err != nil { diff --git a/pkg/apiserver/configuration/editable.go b/pkg/apiserver/configuration/editable.go new file mode 100644 index 0000000000..af70c36f44 --- /dev/null +++ b/pkg/apiserver/configuration/editable.go @@ -0,0 +1,618 @@ +// 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 configuration + +import "strings" + +// Hard coded items comes from https://docs.pingcap.com/tidb/stable/dynamic-config + +var editableConfigItemsRaw = map[ItemKind]string{ + ItemKindTiKVConfig: ` +raftstore.sync-log +raftstore.raft-entry-max-size +raftstore.raft-log-gc-tick-interval +raftstore.raft-log-gc-threshold +raftstore.raft-log-gc-count-limit +raftstore.raft-log-gc-size-limit +raftstore.raft-entry-cache-life-time +raftstore.raft-reject-transfer-leader-duration +raftstore.split-region-check-tick-interval +raftstore.region-split-check-diff +raftstore.region-compact-check-interval +raftstore.region-compact-check-step +raftstore.region-compact-min-tombstones +raftstore.region-compact-tombstones-percent +raftstore.pd-heartbeat-tick-interval +raftstore.pd-store-heartbeat-tick-interval +raftstore.snap-mgr-gc-tick-interval +raftstore.snap-gc-timeout +raftstore.lock-cf-compact-interval +raftstore.lock-cf-compact-bytes-threshold +raftstore.messages-per-tick +raftstore.max-peer-down-duration +raftstore.max-leader-missing-duration +raftstore.abnormal-leader-missing-duration +raftstore.peer-stale-state-check-interval +raftstore.consistency-check-interval +raftstore.raft-store-max-leader-lease +raftstore.allow-remove-leader +raftstore.merge-check-tick-interval +raftstore.cleanup-import-sst-interval +raftstore.local-read-batch-size +raftstore.hibernate-timeout +coprocessor.split-region-on-table +coprocessor.batch-split-limit +coprocessor.region-max-size +coprocessor.region-split-size +coprocessor.region-max-keys +coprocessor.region-split-keys +pessimistic-txn.wait-for-lock-timeout +pessimistic-txn.wake-up-delay-duration +pessimistic-txn.pipelined +gc.ratio-threshold +gc.batch-keys +gc.max-write-bytes-per-sec +gc.enable-compaction-filter +gc.compaction-filter-skip-version-check +raftdb.defaultcf.block-cache-size +raftdb.defaultcf.write-buffer-size +raftdb.defaultcf.max-write-buffer-number +raftdb.defaultcf.max-bytes-for-level-base +raftdb.defaultcf.target-file-size-base +raftdb.defaultcf.level0-file-num-compaction-trigger +raftdb.defaultcf.level0-slowdown-writes-trigger +raftdb.defaultcf.level0-stop-writes-trigger +raftdb.defaultcf.max-compaction-bytes +raftdb.defaultcf.max-bytes-for-level-multiplier +raftdb.defaultcf.disable-auto-compactions +raftdb.defaultcf.soft-pending-compaction-bytes-limit +raftdb.defaultcf.hard-pending-compaction-bytes-limit +raftdb.defaultcf.titan.blob-run-mode +rocksdb.max-total-wal-size +rocksdb.max-background-jobs +rocksdb.max-open-files +rocksdb.compaction-readahead-size +rocksdb.bytes-per-sync +rocksdb.wal-bytes-per-sync +rocksdb.writable-file-max-buffer-size +rocksdb.raftcf.block-cache-size +rocksdb.raftcf.write-buffer-size +rocksdb.raftcf.max-write-buffer-number +rocksdb.raftcf.max-bytes-for-level-base +rocksdb.raftcf.target-file-size-base +rocksdb.raftcf.level0-file-num-compaction-trigger +rocksdb.raftcf.level0-slowdown-writes-trigger +rocksdb.raftcf.level0-stop-writes-trigger +rocksdb.raftcf.max-compaction-bytes +rocksdb.raftcf.max-bytes-for-level-multiplier +rocksdb.raftcf.disable-auto-compactions +rocksdb.raftcf.soft-pending-compaction-bytes-limit +rocksdb.raftcf.hard-pending-compaction-bytes-limit +rocksdb.raftcf.titan.blob-run-mode +rocksdb.defaultcf.block-cache-size +rocksdb.defaultcf.write-buffer-size +rocksdb.defaultcf.max-write-buffer-number +rocksdb.defaultcf.max-bytes-for-level-base +rocksdb.defaultcf.target-file-size-base +rocksdb.defaultcf.level0-file-num-compaction-trigger +rocksdb.defaultcf.level0-slowdown-writes-trigger +rocksdb.defaultcf.level0-stop-writes-trigger +rocksdb.defaultcf.max-compaction-bytes +rocksdb.defaultcf.max-bytes-for-level-multiplier +rocksdb.defaultcf.disable-auto-compactions +rocksdb.defaultcf.soft-pending-compaction-bytes-limit +rocksdb.defaultcf.hard-pending-compaction-bytes-limit +rocksdb.defaultcf.titan.blob-run-mode +rocksdb.lockcf.block-cache-size +rocksdb.lockcf.write-buffer-size +rocksdb.lockcf.max-write-buffer-number +rocksdb.lockcf.max-bytes-for-level-base +rocksdb.lockcf.target-file-size-base +rocksdb.lockcf.level0-file-num-compaction-trigger +rocksdb.lockcf.level0-slowdown-writes-trigger +rocksdb.lockcf.level0-stop-writes-trigger +rocksdb.lockcf.max-compaction-bytes +rocksdb.lockcf.max-bytes-for-level-multiplier +rocksdb.lockcf.disable-auto-compactions +rocksdb.lockcf.soft-pending-compaction-bytes-limit +rocksdb.lockcf.hard-pending-compaction-bytes-limit +rocksdb.lockcf.titan.blob-run-mode +storage.block-cache.capacity +backup.num-threads +split.qps-threshold +split.split-balance-score +split.split-contained-score +`, + ItemKindPDConfig: ` +log.level +cluster-version +schedule.max-merge-region-size +schedule.max-merge-region-keys +schedule.patrol-region-interval +schedule.split-merge-interval +schedule.max-snapshot-count +schedule.max-pending-peer-count +schedule.max-store-down-time +schedule.leader-schedule-policy +schedule.leader-schedule-limit +schedule.region-schedule-limit +schedule.replica-schedule-limit +schedule.merge-schedule-limit +schedule.hot-region-schedule-limit +schedule.hot-region-cache-hits-threshold +schedule.high-space-ratio +schedule.low-space-ratio +schedule.tolerant-size-ratio +schedule.enable-remove-down-replica +schedule.enable-replace-offline-replica +schedule.enable-make-up-replica +schedule.enable-remove-extra-replica +schedule.enable-location-replacement +schedule.enable-cross-table-merge +schedule.enable-one-way-merge +replication.max-replicas +replication.location-labels +replication.enable-placement-rules +replication.strictly-match-label +pd-server.use-region-storage +pd-server.max-gap-reset-ts +pd-server.key-type +pd-server.metric-storage +pd-server.dashboard-address +replication-mode.replication-mode +`, + + // Mark all global variables in TiDB being editable. + // Due to https://github.com/pingcap/tidb/issues/18517 we have to hard code all global variables for now. + // TODO: We'd better provide a Editable system variable table as well. + ItemKindTiDBVariable: ` +gtid_mode +flush_time +low_priority_updates +session_track_gtids +ndbinfo_max_rows +ndb_index_stat_option +old_passwords +max_connections +big_tables +slave_pending_jobs_size_max +validate_password_check_user_name +validate_password_number_count +sql_select_limit +ndb_show_foreign_key_mock_tables +default_week_format +binlog_error_action +slave_transaction_retries +default_storage_engine +max_connect_errors +sync_binlog +innodb_fast_shutdown +log_backward_compatible_user_definitions +ft_boolean_syntax +table_definition_cache +sql_mode +server_id +innodb_flushing_avg_loops +tmp_table_size +innodb_max_purge_lag +preload_buffer_size +slave_checkpoint_period +check_proxy_users +innodb_flush_log_at_timeout +innodb_max_undo_log_size +range_alloc_block_size +connect_timeout +max_execution_time +collation_server +innodb_old_blocks_pct +innodb_file_format +innodb_compression_failure_threshold_pct +innodb_checksum_algorithm +relay_log_info_repository +sql_log_bin +super_read_only +max_delayed_threads +new +myisam_sort_buffer_size +optimizer_trace_offset +innodb_buffer_pool_dump_at_shutdown +sql_notes +innodb_cmp_per_index_enabled +innodb_ft_server_stopword_table +binlog_group_commit_sync_delay +binlog_group_commit_sync_no_delay_count +innodb_log_write_ahead_size +general_log +validate_password_dictionary_file +binlog_order_commits +master_verify_checksum +key_cache_division_limit +rpl_semi_sync_master_trace_level +max_insert_delayed_threads +time_zone +innodb_max_dirty_pages_pct +innodb_file_per_table +innodb_log_compressed_pages +master_info_repository +rpl_stop_slave_timeout +innodb_monitor_reset +innodb_print_all_deadlocks +slave_net_timeout +key_buffer_size +foreign_key_checks +host_cache_size +delay_key_write +innodb_file_format_max +debug +log_warnings +offline_mode +innodb_strict_mode +innodb_rollback_segments +join_buffer_size +max_binlog_size +sync_master_info +concurrent_insert +innodb_adaptive_hash_index +innodb_ft_enable_stopword +general_log_file +innodb_support_xa +innodb_compression_level +init_slave +block_encryption_mode +max_length_for_sort_data +interactive_timeout +innodb_optimize_fulltext_only +query_cache_type +query_alloc_block_size +slave_compressed_protocol +init_connect +rpl_semi_sync_slave_trace_level +query_prealloc_size +max_user_connections +innodb_api_trx_level +expire_logs_days +binlog_rows_query_log_events +default_password_lifetime +innodb_status_output_locks +max_error_count +max_write_lock_count +innodb_stats_persistent_sample_pages +show_compatibility_56 +log_slow_slave_statements +innodb_spin_wait_delay +thread_cache_size +log_slow_admin_statements +auto_increment_offset +innodb_max_dirty_pages_pct_lwm +log_queries_not_using_indexes +query_cache_wlock_invalidate +sql_buffer_result +character_set_filesystem +collation_database +auto_increment_increment +auto_increment_offset +max_heap_table_size +div_precision_increment +innodb_lru_scan_depth +innodb_purge_rseg_truncate_frequency +sql_auto_is_null +innodb_ft_user_stopword_table +innodb_log_checksum_algorithm +sort_buffer_size +innodb_flush_neighbors +innodb_purge_batch_size +slave_checkpoint_group +character_set_client +innodb_buffer_pool_dump_now +relay_log_purge +ndb_distribution +myisam_data_pointer_size +ndb_optimization_delay +innodb_ft_num_word_optimize +max_join_size +max_seeks_for_key +delayed_insert_timeout +max_relay_log_size +max_sort_length +ndb_eventbuffer_free_percent +binlog_max_flush_queue_time +innodb_fill_factor +log_syslog_facility +transaction_write_set_extraction +ndb_blob_write_batch_bytes +automatic_sp_privileges +innodb_flush_sync +innodb_monitor_disable +slave_parallel_type +innodb_adaptive_flushing_lwm +innodb_buffer_pool_load_now +profiling +sha256_password_proxy_users +sql_quote_show_create +binlogging_impossible_mode +query_cache_size +innodb_stats_transient_sample_pages +innodb_stats_on_metadata +ndb_force_send +log_timestamps +slave_parallel_workers +event_scheduler +ndb_deferred_constraints +log_syslog_include_pid +innodb_disable_sort_file_cache +log_error_verbosity +innodb_replication_delay +slow_query_log +innodb_stats_auto_recalc +lc_messages +bulk_insert_buffer_size +binlog_direct_non_transactional_updates +innodb_change_buffering +sql_big_selects +character_set_results +innodb_max_purge_lag_delay +session_track_schema +innodb_io_capacity_max +innodb_autoextend_increment +binlog_format +optimizer_trace +read_rnd_buffer_size +net_write_timeout +innodb_buffer_pool_load_abort +tx_isolation +transaction_isolation +collation_connection +rpl_semi_sync_master_timeout +transaction_prealloc_size +sync_relay_log +innodb_ft_result_cache_limit +innodb_ft_enable_diag_print +stored_program_cache +innodb_adaptive_max_sleep_delay +session_track_system_variables +innodb_change_buffer_max_size +log_bin_trust_function_creators +mysql_native_password_proxy_users +read_only +innodb_stats_persistent +session_track_state_change +delayed_queue_size +log_syslog +transaction_alloc_block_size +sql_slave_skip_counter +innodb_large_prefix +innodb_io_capacity +max_binlog_cache_size +ndb_index_stat_enable +executed_gtids_compression_period +old_alter_table +long_query_time +log_throttle_queries_not_using_indexes +binlog_cache_size +innodb_compression_pad_pct_max +innodb_commit_concurrency +enforce_gtid_consistency +secure_auth +innodb_random_read_ahead +unique_checks +internal_tmp_disk_storage_engine +myisam_repair_threads +ndb_eventbuffer_max_alloc +innodb_read_ahead_threshold +key_cache_block_size +rpl_semi_sync_slave_enabled +gtid_purged +max_binlog_stmt_cache_size +lock_wait_timeout +read_buffer_size +max_sp_recursion_depth +rpl_semi_sync_master_enabled +slow_query_log_file +innodb_thread_sleep_delay +innodb_ft_aux_table +sql_warnings +keep_files_on_create +slave_preserve_commit_order +slave_exec_mode +binlog_stmt_cache_size +table_open_cache +autocommit +default_tmp_storage_engine +optimizer_search_depth +max_points_in_geometry +innodb_stats_sample_pages +profiling_history_size +character_set_database +storage_engine +sql_log_off +log_syslog_tag +tx_read_only +transaction_read_only +rpl_semi_sync_master_wait_point +innodb_undo_log_truncate +gtid_executed_compression_period +ndb_log_empty_epochs +max_prepared_stmt_count +optimizer_trace_max_mem_size +net_retry_count +optimizer_trace_features +innodb_flush_log_at_trx_commit +rewriter_enabled +query_cache_min_res_unit +updatable_views_with_limit +optimizer_prune_level +slave_sql_verify_checksum +completion_type +binlog_checksum +show_old_temporals +query_cache_limit +innodb_buffer_pool_size +innodb_adaptive_flushing +wait_timeout +innodb_monitor_enable +innodb_buffer_pool_filename +slow_launch_time +slave_max_allowed_packet +ndb_use_transactions +innodb_concurrency_tickets +innodb_monitor_reset_all +ndb_log_updated_only +innodb_old_blocks_time +innodb_stats_method +innodb_lock_wait_timeout +local_infile +myisam_stats_method +innodb_table_locks +net_buffer_length +rpl_semi_sync_master_wait_for_slave_count +binlog_row_image +myisam_max_sort_file_size +rpl_semi_sync_master_wait_no_slave +group_concat_max_len +rewriter_verbose +innodb_undo_logs +delayed_insert_limit +flush +eq_range_index_dive_limit +character_set_connection +myisam_use_mmap +ndb_join_pushdown +character_set_server +validate_password_special_char_count +slave_rows_search_algorithms +ndbinfo_show_hidden +net_read_timeout +max_allowed_packet +sync_relay_log_info +optimizer_trace_limit +validate_password_length +ndb_log_binlog_index +innodb_api_bk_commit_interval +innodb_sync_spin_loops +sql_safe_updates +innodb_thread_concurrency +slave_allow_batching +innodb_buffer_pool_dump_pct +lc_time_names +max_statement_time +end_markers_in_json +avoid_temporal_upgrade +key_cache_age_threshold +innodb_status_output +min_examined_row_limit +sync_frm +innodb_online_alter_log_max_size +information_schema_stats_expiry +thread_pool_size +windowing_use_high_precision +tidb_opt_broadcast_join +tidb_build_stats_concurrency +tidb_auto_analyze_ratio +tidb_auto_analyze_start_time +tidb_auto_analyze_end_time +tidb_executor_concurrency +tidb_distsql_scan_concurrency +tidb_opt_insubq_to_join_and_agg +tidb_opt_correlation_threshold +tidb_opt_correlation_exp_factor +tidb_opt_cpu_factor +tidb_opt_tiflash_concurrency_factor +tidb_opt_copcpu_factor +tidb_opt_network_factor +tidb_opt_scan_factor +tidb_opt_desc_factor +tidb_opt_seek_factor +tidb_opt_memory_factor +tidb_opt_disk_factor +tidb_opt_concurrency_factor +tidb_index_join_batch_size +tidb_index_lookup_size +tidb_index_lookup_concurrency +tidb_index_lookup_join_concurrency +tidb_index_serial_scan_concurrency +tidb_skip_utf8_check +tidb_skip_ascii_check +tidb_max_chunk_size +tidb_allow_batch_cop +tidb_init_chunk_size +tidb_enable_cascades_planner +tidb_enable_index_merge +tidb_enable_table_partition +tidb_hash_join_concurrency +tidb_projection_concurrency +tidb_hashagg_partial_concurrency +tidb_hashagg_final_concurrency +tidb_window_concurrency +tidb_enable_parallel_apply +tidb_backoff_lock_fast +tidb_backoff_weight +tidb_retry_limit +tidb_disable_txn_auto_retry +tidb_constraint_check_in_place +tidb_txn_mode +tidb_row_format_version +tidb_enable_window_function +tidb_enable_vectorized_expression +tidb_enable_fast_analyze +tidb_skip_isolation_level_check +tidb_ddl_reorg_worker_cnt +tidb_ddl_reorg_batch_size +tidb_ddl_error_count_limit +tidb_max_delta_schema_count +tidb_opt_join_reorder_threshold +tidb_scatter_region +tidb_enable_noop_functions +tidb_enable_stmt_summary +tidb_stmt_summary_internal_query +tidb_stmt_summary_refresh_interval +tidb_stmt_summary_history_size +tidb_stmt_summary_max_stmt_count +tidb_stmt_summary_max_sql_length +tidb_capture_plan_baselines +tidb_use_plan_baselines +tidb_evolve_plan_baselines +tidb_evolve_plan_task_max_time +tidb_evolve_plan_task_start_time +tidb_evolve_plan_task_end_time +tidb_store_limit +allow_auto_random_explicit_insert +tidb_enable_clustered_index +tidb_slow_log_masking +tidb_log_desensitization +tidb_shard_allocate_step +tidb_enable_telemetry +`, +} + +var editableConfigItems = map[ItemKind]map[string]struct{}{} + +func init() { + for kind, str := range editableConfigItemsRaw { + editableConfigItems[kind] = make(map[string]struct{}) + configItems := strings.Split(strings.TrimSpace(str), "\n") + for _, key := range configItems { + editableConfigItems[kind][key] = struct{}{} + } + } +} + +func isConfigItemEditable(kind ItemKind, key string) bool { + if _, ok := editableConfigItems[kind]; !ok { + return false + } + if _, ok := editableConfigItems[kind][key]; !ok { + return false + } + return true +} diff --git a/pkg/apiserver/configuration/flatten.go b/pkg/apiserver/configuration/flatten.go new file mode 100644 index 0000000000..a42c58332e --- /dev/null +++ b/pkg/apiserver/configuration/flatten.go @@ -0,0 +1,53 @@ +// 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 configuration + +import ( + "encoding/json" + + "github.com/pingcap/log" + "go.uber.org/zap" +) + +func flattenRecursive(nestedConfig map[string]interface{}) map[string]interface{} { + flatMap := make(map[string]interface{}) + flatten(flatMap, nestedConfig, "") + return flatMap +} + +func flatten(flatMap map[string]interface{}, nested interface{}, prefix string) { + switch n := nested.(type) { + case map[string]interface{}: + for k, v := range n { + path := k + if prefix != "" { + path = prefix + "." + k + } + flatten(flatMap, v, path) + } + case []interface{}: + // For array, serialize as json string directly + j, err := json.Marshal(n) + if err != nil { + log.Warn("Failed to serialize config value", zap.Any("value", n), zap.Error(err)) + flatMap[prefix] = nil + } else { + flatMap[prefix] = string(j) + } + case nil: + flatMap[prefix] = "" + default: // don't flatten arrays + flatMap[prefix] = nested + } +} diff --git a/pkg/apiserver/configuration/router.go b/pkg/apiserver/configuration/router.go new file mode 100644 index 0000000000..00bf34670c --- /dev/null +++ b/pkg/apiserver/configuration/router.go @@ -0,0 +1,90 @@ +// 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 configuration + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user" + "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" +) + +func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { + endpoint := r.Group("/configuration") + endpoint.Use(auth.MWAuthRequired()) + endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient)) + endpoint.Use(utils.MWForbidByExperimentalFlag(s.params.Config.EnableExperimental)) + endpoint.GET("/all", s.getHandler) + endpoint.POST("/edit", s.editHandler) +} + +// @ID configurationGetAll +// @Summary Get all configurations +// @Success 200 {object} AllConfigItems +// @Router /configuration/all [get] +// @Security JwtAuth +// @Failure 401 {object} utils.APIError "Unauthorized failure" +// @Failure 403 {object} utils.APIError "Experimental feature not enabled" +// @Failure 500 {object} utils.APIError "Internal error" +func (s *Service) getHandler(c *gin.Context) { + db := utils.GetTiDBConnection(c) + r, err := s.getAllConfigItems(db) + if err != nil { + _ = c.Error(err) + return + } + c.JSON(http.StatusOK, r) +} + +type EditRequest struct { + Kind ItemKind `json:"kind"` + ID string `json:"id"` + NewValue interface{} `json:"new_value"` +} + +type EditResponse struct { + Warnings []*utils.APIError `json:"warnings"` +} + +// @ID configurationEdit +// @Summary Edit a configuration +// @Param request body EditRequest true "Request body" +// @Success 200 {object} EditResponse +// @Router /configuration/edit [post] +// @Security JwtAuth +// @Failure 400 {object} utils.APIError "Bad request" +// @Failure 401 {object} utils.APIError "Unauthorized failure" +// @Failure 403 {object} utils.APIError "Experimental feature not enabled" +// @Failure 500 {object} utils.APIError "Internal error" +func (s *Service) editHandler(c *gin.Context) { + var req EditRequest + if err := c.ShouldBindJSON(&req); err != nil { + utils.MakeInvalidRequestErrorFromError(c, err) + return + } + + db := utils.GetTiDBConnection(c) + warnings, err := s.editConfig(db, req.Kind, req.ID, req.NewValue) + if err != nil { + _ = c.Error(err) + return + } + + var resp EditResponse + resp.Warnings = warnings + + c.JSON(http.StatusOK, resp) +} diff --git a/pkg/apiserver/configuration/service.go b/pkg/apiserver/configuration/service.go new file mode 100644 index 0000000000..fdeae3880b --- /dev/null +++ b/pkg/apiserver/configuration/service.go @@ -0,0 +1,372 @@ +// 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 configuration + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "sort" + + "github.com/jinzhu/gorm" + "github.com/joomcode/errorx" + "go.etcd.io/etcd/clientv3" + "go.uber.org/fx" + + "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" + "github.com/pingcap-incubator/tidb-dashboard/pkg/config" + "github.com/pingcap-incubator/tidb-dashboard/pkg/pd" + "github.com/pingcap-incubator/tidb-dashboard/pkg/tidb" + "github.com/pingcap-incubator/tidb-dashboard/pkg/tikv" + "github.com/pingcap-incubator/tidb-dashboard/pkg/utils/topology" +) + +var ( + ErrNS = errorx.NewNamespace("error.api.config") + ErrListTopologyFailed = ErrNS.NewType("list_topology_failed") + ErrListConfigItemsFailed = ErrNS.NewType("list_config_items_failed") + ErrNotEditable = ErrNS.NewType("not_editable") + ErrEditFailed = ErrNS.NewType("edit_failed") +) + +type ServiceParams struct { + fx.In + Config *config.Config + PDClient *pd.Client + EtcdClient *clientv3.Client + TiDBClient *tidb.Client + TiKVClient *tikv.Client +} + +type Service struct { + params ServiceParams + lifecycleCtx context.Context +} + +func NewService(lc fx.Lifecycle, p ServiceParams) *Service { + service := &Service{params: p} + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + service.lifecycleCtx = ctx + return nil + }, + }) + + return service +} + +type ItemKind string + +const ( + ItemKindTiKVConfig ItemKind = "tikv_config" + ItemKindPDConfig ItemKind = "pd_config" + ItemKindTiDBConfig ItemKind = "tidb_config" + ItemKindTiDBVariable ItemKind = "tidb_variable" +) + +type channelItem struct { + Err error + SourceDisplayAddress string + SourceKind ItemKind + Values map[string]interface{} +} + +func processNestedConfigAPIResponse(data []byte) (map[string]interface{}, error) { + nestedConfig := make(map[string]interface{}) + if err := json.Unmarshal(data, &nestedConfig); err != nil { + return nil, err + } + + plainConfig := flattenRecursive(nestedConfig) + return plainConfig, nil +} + +func (s *Service) getConfigItemsFromPDToChannel(ch chan<- channelItem) { + r, err := s.getConfigItemsFromPD() + if err != nil { + ch <- channelItem{Err: ErrListConfigItemsFailed.Wrap(err, "Failed to list PD config items")} + return + } + ch <- channelItem{ + Err: nil, + SourceKind: ItemKindPDConfig, + Values: r, + } +} + +func (s *Service) getConfigItemsFromPD() (map[string]interface{}, error) { + data, err := s.params.PDClient.SendGetRequest("/config") + if err != nil { + return nil, err + } + return processNestedConfigAPIResponse(data) +} + +func (s *Service) getConfigItemsFromTiDBToChannel(tidb *topology.TiDBInfo, ch chan<- channelItem) { + displayAddress := fmt.Sprintf("%s:%d", tidb.IP, tidb.Port) + + r, err := s.getConfigItemsFromTiDB(tidb.IP, int(tidb.StatusPort)) + if err != nil { + ch <- channelItem{Err: ErrListConfigItemsFailed.Wrap(err, "Failed to list TiDB config items of %s", displayAddress)} + return + } + ch <- channelItem{ + Err: nil, + SourceDisplayAddress: displayAddress, + SourceKind: ItemKindTiDBConfig, + Values: r, + } +} + +func (s *Service) getConfigItemsFromTiDB(host string, statusPort int) (map[string]interface{}, error) { + data, err := s.params.TiDBClient.WithStatusAPIAddress(host, statusPort).SendGetRequest("/config") + if err != nil { + return nil, err + } + return processNestedConfigAPIResponse(data) +} + +func (s *Service) getConfigItemsFromTiKVToChannel(tikv *topology.StoreInfo, ch chan<- channelItem) { + displayAddress := fmt.Sprintf("%s:%d", tikv.IP, tikv.Port) + + r, err := s.getConfigItemsFromTiKV(tikv.IP, int(tikv.StatusPort)) + if err != nil { + ch <- channelItem{Err: ErrListConfigItemsFailed.Wrap(err, "Failed to list TiKV config items of %s", displayAddress)} + return + } + ch <- channelItem{ + Err: nil, + SourceDisplayAddress: displayAddress, + SourceKind: ItemKindTiKVConfig, + Values: r, + } +} + +func (s *Service) getConfigItemsFromTiKV(host string, statusPort int) (map[string]interface{}, error) { + data, err := s.params.TiKVClient.SendGetRequest(host, statusPort, "/config") + if err != nil { + return nil, err + } + return processNestedConfigAPIResponse(data) +} + +type ShowVariableItem struct { + Name string `gorm:"column:Variable_name"` + Value string `gorm:"column:Value"` +} + +func (s *Service) getGlobalVariablesFromTiDBToChannel(db *gorm.DB, ch chan<- channelItem) { + r, err := s.getGlobalVariablesFromTiDB(db) + if err != nil { + ch <- channelItem{Err: ErrListConfigItemsFailed.Wrap(err, "Failed to list TiDB variables")} + return + } + ch <- channelItem{ + Err: nil, + SourceKind: ItemKindTiDBVariable, + Values: r, + } +} + +func (s *Service) getGlobalVariablesFromTiDB(db *gorm.DB) (map[string]interface{}, error) { + var rows []ShowVariableItem + if err := db.Raw("SHOW GLOBAL VARIABLES").Find(&rows).Error; err != nil { + return nil, err + } + result := make(map[string]interface{}) + for _, r := range rows { + result[r.Name] = r.Value + } + return result, nil +} + +type Item struct { + ID string `json:"id"` + IsEditable bool `json:"is_editable"` + IsMultiValue bool `json:"is_multi_value"` // TODO: Support per-instance config + Value interface{} `json:"value"` // When multi value present, this contains one of the value +} + +type AllConfigItems struct { + Errors []*utils.APIError `json:"errors"` + Items map[ItemKind][]Item `json:"items"` +} + +func (s *Service) getAllConfigItems(db *gorm.DB) (*AllConfigItems, error) { + tikvInfo, _, err := topology.FetchStoreTopology(s.params.PDClient) + if err != nil { + return nil, ErrListTopologyFailed.Wrap(err, "Failed to list TiKV stores") + } + + tidbInfo, err := topology.FetchTiDBTopology(s.lifecycleCtx, s.params.EtcdClient) + if err != nil { + return nil, ErrListTopologyFailed.Wrap(err, "Failed to list TiDB instances") + } + + ch := make(chan channelItem) + waitItems := 0 + + { + waitItems++ + go s.getConfigItemsFromPDToChannel(ch) + } + { + waitItems++ + go s.getGlobalVariablesFromTiDBToChannel(db, ch) + } + for _, item := range tikvInfo { + // TODO: What about tombstone stores? + waitItems++ + item2 := item + go s.getConfigItemsFromTiKVToChannel(&item2, ch) + } + for _, item := range tidbInfo { + waitItems++ + item2 := item + go s.getConfigItemsFromTiDBToChannel(&item2, ch) + } + + errors := make([]*utils.APIError, 0) + successItems := make([]channelItem, 0) + + for i := 0; i < waitItems; i++ { + item := <-ch + if item.Err != nil { + errors = append(errors, utils.NewAPIError(err)) + continue + } + successItems = append(successItems, item) + } + close(ch) + + // The first occurred value of each config item + valuesMap := make(map[ItemKind]map[string]interface{}) + // Number of config item key occurred to detect missing config items + occurTimesMap := make(map[ItemKind]map[string]int) + // Whether each config item has different values + identicalMap := make(map[ItemKind]map[string]bool) + // The expected number of occur times + expectedOccurTimes := make(map[ItemKind]int) + + for _, item := range successItems { + if _, ok := expectedOccurTimes[item.SourceKind]; !ok { + expectedOccurTimes[item.SourceKind] = 1 + } else { + expectedOccurTimes[item.SourceKind]++ + } + if _, ok := valuesMap[item.SourceKind]; !ok { + valuesMap[item.SourceKind] = make(map[string]interface{}) + occurTimesMap[item.SourceKind] = make(map[string]int) + identicalMap[item.SourceKind] = make(map[string]bool) + } + for key, value := range item.Values { + if _, ok := valuesMap[item.SourceKind][key]; !ok { + valuesMap[item.SourceKind][key] = value + occurTimesMap[item.SourceKind][key] = 1 + identicalMap[item.SourceKind][key] = true + } else { + occurTimesMap[item.SourceKind][key]++ + if value != valuesMap[item.SourceKind][key] { + identicalMap[item.SourceKind][key] = false + } + } + } + } + + result := make(map[ItemKind][]Item) + for kind, v := range valuesMap { + result[kind] = make([]Item, 0) + for configKey, configValue := range v { + // There are two cases when a config item has multiple values: + // 1. Values are not equal + // 2. Value is missing + isMultiValue := !identicalMap[kind][configKey] + value := configValue + if !isMultiValue && occurTimesMap[kind][configKey] < expectedOccurTimes[kind] { + isMultiValue = false + } + + result[kind] = append(result[kind], Item{ + ID: configKey, + IsEditable: isConfigItemEditable(kind, configKey), + IsMultiValue: isMultiValue, + Value: value, + }) + } + + s := result[kind] + sort.Slice(s, func(i, j int) bool { + return s[i].ID < s[j].ID + }) + } + + return &AllConfigItems{ + Errors: errors, + Items: result, + }, nil +} + +func (s *Service) editConfig(db *gorm.DB, kind ItemKind, id string, newValue interface{}) ([]*utils.APIError, error) { + if !isConfigItemEditable(kind, id) { + return nil, ErrNotEditable.New("Configuration `%s` is not editable", id) + } + body := make(map[string]interface{}) + body[id] = newValue + bodyJSON, err := json.Marshal(&body) + if err != nil { + return nil, ErrEditFailed.WrapWithNoMessage(err) + } + + switch kind { + case ItemKindPDConfig: + _, err := s.params.PDClient.SendPostRequest("/config", bytes.NewBuffer(bodyJSON)) + if err != nil { + return nil, ErrEditFailed.WrapWithNoMessage(err) + } + case ItemKindTiKVConfig: + tikvInfo, _, err := topology.FetchStoreTopology(s.params.PDClient) + if err != nil { + return nil, ErrEditFailed.WrapWithNoMessage(ErrListTopologyFailed.WrapWithNoMessage(err)) + } + failures := make([]error, 0) + for _, kvStore := range tikvInfo { + // TODO: What about tombstone stores? + _, err := s.params.TiKVClient.SendPostRequest(kvStore.IP, int(kvStore.StatusPort), "/config", bytes.NewBuffer(bodyJSON)) + if err != nil { + failures = append(failures, ErrEditFailed.Wrap(err, "Failed to edit config for TiKV instance `%s:%d`", kvStore.IP, kvStore.Port)) + } + } + if len(failures) == len(tikvInfo) { + if len(failures) > 0 { + return nil, failures[0] + } + return nil, nil + } + warnings := make([]*utils.APIError, 0) + for _, err := range failures { + warnings = append(warnings, utils.NewAPIError(err)) + } + return warnings, nil + case ItemKindTiDBVariable: + // We have checked the correctness of id, so no need to worry about injections + if err := db.Exec(fmt.Sprintf("SET GLOBAL %s = ?", id), newValue).Error; err != nil { + return nil, ErrEditFailed.WrapWithNoMessage(err) + } + default: + return nil, ErrEditFailed.New("Edit failed, not implemented") + } + + return nil, nil +} diff --git a/pkg/apiserver/diagnose/diagnose.go b/pkg/apiserver/diagnose/diagnose.go index 492f749af0..130623be0a 100644 --- a/pkg/apiserver/diagnose/diagnose.go +++ b/pkg/apiserver/diagnose/diagnose.go @@ -25,7 +25,7 @@ import ( "github.com/pingcap-incubator/tidb-dashboard/pkg/uiserver" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user" - apiutils "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" + "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" "github.com/pingcap-incubator/tidb-dashboard/pkg/config" "github.com/pingcap-incubator/tidb-dashboard/pkg/dbstore" "github.com/pingcap-incubator/tidb-dashboard/pkg/tidb" @@ -36,23 +36,24 @@ const ( ) type Service struct { - config *config.Config - db *dbstore.DB - tidbForwarder *tidb.Forwarder - fileServer http.Handler + // FIXME: Use fx.In + config *config.Config + db *dbstore.DB + tidbClient *tidb.Client + fileServer http.Handler } -func NewService(config *config.Config, tidbForwarder *tidb.Forwarder, db *dbstore.DB, uiAssetFS http.FileSystem) *Service { +func NewService(config *config.Config, tidbClient *tidb.Client, db *dbstore.DB, uiAssetFS http.FileSystem) *Service { err := autoMigrate(db) if err != nil { log.Fatal("Failed to initialize database", zap.Error(err)) } return &Service{ - config: config, - db: db, - tidbForwarder: tidbForwarder, - fileServer: uiserver.Handler(uiAssetFS), + config: config, + db: db, + tidbClient: tidbClient, + fileServer: uiserver.Handler(uiAssetFS), } } @@ -63,7 +64,7 @@ func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { s.reportsHandler) endpoint.POST("/reports", auth.MWAuthRequired(), - apiutils.MWConnectTiDB(s.tidbForwarder), + utils.MWConnectTiDB(s.tidbClient), s.genReportHandler) endpoint.GET("/reports/:id/detail", s.reportHTMLHandler) endpoint.GET("/reports/:id/data.js", s.reportDataHandler) @@ -81,7 +82,6 @@ type GenerateReportRequest struct { // @Summary SQL diagnosis reports history // @Description Get sql diagnosis reports history -// @Produce json // @Success 200 {array} Report // @Router /diagnose/reports [get] // @Security JwtAuth @@ -97,17 +97,16 @@ func (s *Service) reportsHandler(c *gin.Context) { // @Summary SQL diagnosis report // @Description Generate sql diagnosis report -// @Produce json // @Param request body GenerateReportRequest true "Request body" // @Success 200 {object} int // @Router /diagnose/reports [post] // @Security JwtAuth +// @Failure 400 {object} utils.APIError "Bad request" // @Failure 401 {object} utils.APIError "Unauthorized failure" func (s *Service) genReportHandler(c *gin.Context) { var req GenerateReportRequest if err := c.ShouldBindJSON(&req); err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(apiutils.ErrInvalidRequest.WrapWithNoMessage(err)) + utils.MakeInvalidRequestErrorFromError(c, err) return } @@ -127,7 +126,7 @@ func (s *Service) genReportHandler(c *gin.Context) { return } - db := apiutils.TakeTiDBConnection(c) + db := utils.TakeTiDBConnection(c) go func() { defer db.Close() @@ -153,7 +152,6 @@ func (s *Service) genReportHandler(c *gin.Context) { // @Summary Diagnosis report status // @Description Get diagnosis report status -// @Produce json // @Param id path string true "report id" // @Success 200 {object} Report // @Router /diagnose/reports/{id}/status [get] diff --git a/pkg/apiserver/foo/foo.go b/pkg/apiserver/foo/foo.go deleted file mode 100644 index a155d6ca99..0000000000 --- a/pkg/apiserver/foo/foo.go +++ /dev/null @@ -1,53 +0,0 @@ -// 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 foo - -import ( - "net/http" - - "github.com/gin-gonic/gin" - - "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user" - - // Import for swag go doc - _ "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" - "github.com/pingcap-incubator/tidb-dashboard/pkg/config" -) - -type Service struct { -} - -func NewService(config *config.Config) *Service { - return &Service{} -} - -func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { - endpoint := r.Group("/foo") - endpoint.Use(auth.MWAuthRequired()) - endpoint.GET("/bar/:name", s.greetHandler) -} - -// @Summary Greet -// @Description Hello world! -// @Accept json -// @Produce json -// @Param name path string true "Name" -// @Success 200 {string} string -// @Router /foo/bar/{name} [get] -// @Security JwtAuth -// @Failure 401 {object} utils.APIError "Unauthorized failure" -func (s *Service) greetHandler(c *gin.Context) { - name := c.Param("name") - c.String(http.StatusOK, "Hello %s", name) -} diff --git a/pkg/apiserver/info/info.go b/pkg/apiserver/info/info.go index 26e402e09b..45b405210b 100644 --- a/pkg/apiserver/info/info.go +++ b/pkg/apiserver/info/info.go @@ -19,6 +19,7 @@ import ( "strings" "github.com/gin-gonic/gin" + "go.uber.org/fx" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" @@ -28,14 +29,19 @@ import ( "github.com/pingcap-incubator/tidb-dashboard/pkg/utils/version" ) +type ServiceParams struct { + fx.In + Config *config.Config + LocalStore *dbstore.DB + TiDBClient *tidb.Client +} + type Service struct { - config *config.Config - db *dbstore.DB - tidbForwarder *tidb.Forwarder + params ServiceParams } -func NewService(config *config.Config, tidbForwarder *tidb.Forwarder, db *dbstore.DB) *Service { - return &Service{config: config, db: db, tidbForwarder: tidbForwarder} +func NewService(p ServiceParams) *Service { + return &Service{params: p} } func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { @@ -43,52 +49,54 @@ func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint.GET("/info", s.infoHandler) endpoint.Use(auth.MWAuthRequired()) endpoint.GET("/whoami", s.whoamiHandler) - endpoint.GET("/databases", utils.MWConnectTiDB(s.tidbForwarder), s.databasesHandler) + endpoint.GET("/databases", utils.MWConnectTiDB(s.params.TiDBClient), s.databasesHandler) } type InfoResponse struct { //nolint:golint - Version *version.Info `json:"version"` - EnableTelemetry bool `json:"enable_telemetry"` + Version *version.Info `json:"version"` + EnableTelemetry bool `json:"enable_telemetry"` + EnableExperimental bool `json:"enable_experimental"` } -// @Summary Dashboard info -// @Description Get information about the dashboard service. -// @ID getInfo -// @Produce json +// @ID infoGet +// @Summary Get information about this TiDB Dashboard // @Success 200 {object} InfoResponse // @Router /info/info [get] // @Security JwtAuth // @Failure 401 {object} utils.APIError "Unauthorized failure" func (s *Service) infoHandler(c *gin.Context) { resp := InfoResponse{ - Version: version.GetInfo(), - EnableTelemetry: s.config.EnableTelemetry, + Version: version.GetInfo(), + EnableTelemetry: s.params.Config.EnableTelemetry, + EnableExperimental: s.params.Config.EnableExperimental, } c.JSON(http.StatusOK, resp) } type WhoAmIResponse struct { Username string `json:"username"` + IsShared bool `json:"is_shared"` } -// @Summary Current login -// @Description Get current login session -// @Produce json +// @ID infoWhoami +// @Summary Get information about current session // @Success 200 {object} WhoAmIResponse // @Router /info/whoami [get] // @Security JwtAuth // @Failure 401 {object} utils.APIError "Unauthorized failure" func (s *Service) whoamiHandler(c *gin.Context) { sessionUser := c.MustGet(utils.SessionUserKey).(*utils.SessionUser) - resp := WhoAmIResponse{Username: sessionUser.TiDBUsername} + resp := WhoAmIResponse{ + Username: sessionUser.TiDBUsername, + IsShared: sessionUser.IsShared, + } c.JSON(http.StatusOK, resp) } type DatabaseResponse = []string -// @Summary Example: Get all databases -// @Description Get all databases. -// @Produce json +// @ID infoListDatabases +// @Summary List all databases // @Success 200 {object} DatabaseResponse // @Router /info/databases [get] // @Security JwtAuth diff --git a/pkg/apiserver/logsearch/pack.go b/pkg/apiserver/logsearch/pack.go index 9de7ca6ed2..eb0e55d0ed 100644 --- a/pkg/apiserver/logsearch/pack.go +++ b/pkg/apiserver/logsearch/pack.go @@ -14,12 +14,8 @@ package logsearch import ( - "archive/tar" "fmt" - "io" "net/http" - "os" - "path" "github.com/gin-gonic/gin" "github.com/pingcap/log" @@ -28,87 +24,37 @@ import ( "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" ) -func packLogsAsTarball(tasks []*TaskModel, w io.Writer) { - tw := tar.NewWriter(w) - defer tw.Close() +func serveTaskForDownload(task *TaskModel, c *gin.Context) { + logPath := task.LogStorePath + if logPath == nil { + logPath = task.SlowLogStorePath + } + if logPath == nil { + utils.MakeInvalidRequestErrorWithMessage(c, "Log is not ready") + return + } + c.FileAttachment(*logPath, fmt.Sprintf("logs-%s.zip", task.Target.FileName())) +} +func serveMultipleTaskForDownload(tasks []*TaskModel, c *gin.Context) { + filePaths := make([]string, 0, len(tasks)) for _, task := range tasks { - if task.LogStorePath == nil && task.SlowLogStorePath == nil { - continue - } - if task.LogStorePath != nil { - if err := dumpLog(*task.LogStorePath, tw); err != nil { - log.Warn("Failed to pack log", - zap.Any("task", task), - zap.Error(err)) - continue - } + logPath := task.LogStorePath + if logPath == nil { + logPath = task.SlowLogStorePath } - if task.SlowLogStorePath != nil { - if err := dumpLog(*task.SlowLogStorePath, tw); err != nil { - log.Warn("Failed to pack slow log", - zap.Any("task", task), - zap.Error(err)) - continue - } + if logPath == nil { + c.Status(http.StatusInternalServerError) + _ = c.Error(utils.ErrInvalidRequest.New("Some logs are not available")) + return } + filePaths = append(filePaths, *logPath) } -} -func dumpLog(savedPath string, tw *tar.Writer) error { - f, err := os.Open(savedPath) - if err != nil { - return err - } - defer f.Close() - fi, err := f.Stat() + c.Writer.Header().Set("Content-type", "application/octet-stream") + c.Writer.Header().Set("Content-Disposition", "attachment; filename=\"logs.zip\"") + err := utils.StreamZipPack(c.Writer, filePaths, false) if err != nil { - return err - } - err = tw.WriteHeader(&tar.Header{ - Name: path.Base(savedPath), - Mode: int64(fi.Mode()), - ModTime: fi.ModTime(), - Size: fi.Size(), - }) - if err != nil { - return err - } - - _, err = io.Copy(tw, f) - if err != nil { - return err - } - return nil -} - -func serveTaskForDownload(task *TaskModel, c *gin.Context) { - if task.LogStorePath == nil && task.SlowLogStorePath == nil { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.New("Log is not available")) - return - } - reader, writer := io.Pipe() - go func() { - defer writer.Close() - packLogsAsTarball([]*TaskModel{task}, writer) - }() - contentType := "application/tar" - extraHeaders := map[string]string{ - "Content-Disposition": fmt.Sprintf(`attachment; filename="logs-%s.tar"`, task.Target.FileName()), - } - c.DataFromReader(http.StatusOK, -1, contentType, reader, extraHeaders) -} - -func serveMultipleTaskForDownload(tasks []*TaskModel, c *gin.Context) { - reader, writer := io.Pipe() - go func() { - defer writer.Close() - packLogsAsTarball(tasks, writer) - }() - contentType := "application/tar" - extraHeaders := map[string]string{ - "Content-Disposition": `attachment; filename="logs.tar"`, + log.Error("Stream zip pack failed", zap.Error(err)) } - c.DataFromReader(http.StatusOK, -1, contentType, reader, extraHeaders) } diff --git a/pkg/apiserver/logsearch/service.go b/pkg/apiserver/logsearch/service.go index a3249da431..ea48b169ee 100644 --- a/pkg/apiserver/logsearch/service.go +++ b/pkg/apiserver/logsearch/service.go @@ -15,7 +15,6 @@ package logsearch import ( "context" - "fmt" "io/ioutil" "net/http" "strconv" @@ -34,6 +33,7 @@ import ( ) type Service struct { + // FIXME: Use fx.In lifecycleCtx context.Context config *config.Config @@ -100,26 +100,22 @@ type TaskGroupResponse struct { Tasks []*TaskModel `json:"tasks"` } -// @Summary Create and run task group -// @Description Create and run task group -// @Produce json +// @Summary Create and run a new log search task group // @Param request body CreateTaskGroupRequest true "Request body" // @Security JwtAuth // @Success 200 {object} TaskGroupResponse -// @Failure 400 {object} utils.APIError +// @Failure 400 {object} utils.APIError "Bad request" // @Failure 401 {object} utils.APIError "Unauthorized failure" // @Failure 500 {object} utils.APIError // @Router /logs/taskgroup [put] func (s *Service) CreateTaskGroup(c *gin.Context) { var req CreateTaskGroupRequest if err := c.ShouldBindJSON(&req); err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + utils.MakeInvalidRequestErrorFromError(c, err) return } if len(req.Targets) == 0 { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.NewWithNoMessage()) + utils.MakeInvalidRequestErrorWithMessage(c, "Expect at least 1 target") return } stats := model.NewRequestTargetStatisticsFromArray(&req.Targets) @@ -154,9 +150,7 @@ func (s *Service) CreateTaskGroup(c *gin.Context) { c.JSON(http.StatusOK, resp) } -// @Summary List all task groups -// @Description list all log search taskgroups -// @Produce json +// @Summary List all log search task groups // @Security JwtAuth // @Success 200 {array} TaskGroupModel // @Failure 401 {object} utils.APIError "Unauthorized failure" @@ -173,9 +167,7 @@ func (s *Service) GetAllTaskGroups(c *gin.Context) { c.JSON(http.StatusOK, taskGroups) } -// @Summary List tasks in a task group -// @Description list all log search tasks in a task group by providing task group ID -// @Produce json +// @Summary List tasks in a log search task group // @Param id path string true "Task Group ID" // @Security JwtAuth // @Success 200 {object} TaskGroupResponse @@ -203,9 +195,7 @@ func (s *Service) GetTaskGroup(c *gin.Context) { c.JSON(http.StatusOK, resp) } -// @Summary Preview logs in a task group -// @Description preview fetched logs in a task group by providing task group ID -// @Produce json +// @Summary Preview a log search task group // @Param id path string true "task group id" // @Security JwtAuth // @Success 200 {array} PreviewModel @@ -227,9 +217,7 @@ func (s *Service) GetTaskGroupPreview(c *gin.Context) { c.JSON(http.StatusOK, lines) } -// @Summary Retry failed tasks -// @Description retry tasks that has been failed in a task group -// @Produce json +// @Summary Retry failed tasks in a log search task group // @Param id path string true "task group id" // @Security JwtAuth // @Success 200 {object} utils.APIEmptyResponse @@ -240,8 +228,7 @@ func (s *Service) GetTaskGroupPreview(c *gin.Context) { func (s *Service) RetryTask(c *gin.Context) { taskGroupID, err := strconv.Atoi(c.Param("id")) if err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + utils.MakeInvalidRequestErrorFromError(c, err) return } @@ -279,9 +266,7 @@ func (s *Service) RetryTask(c *gin.Context) { c.JSON(http.StatusOK, utils.APIEmptyResponse{}) } -// @Summary Cancel running tasks -// @Description cancel all running tasks in a task group -// @Produce json +// @Summary Cancel running tasks in a log search task group // @Param id path string true "task group id" // @Security JwtAuth // @Success 200 {object} utils.APIEmptyResponse @@ -291,8 +276,7 @@ func (s *Service) RetryTask(c *gin.Context) { func (s *Service) CancelTask(c *gin.Context) { taskGroupID, err := strconv.Atoi(c.Param("id")) if err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + utils.MakeInvalidRequestErrorFromError(c, err) return } taskGroup := TaskGroupModel{} @@ -302,17 +286,14 @@ func (s *Service) CancelTask(c *gin.Context) { return } if taskGroup.State != TaskGroupStateRunning { - c.Status(http.StatusBadRequest) - _ = c.Error(fmt.Errorf("taskGroup is not running")) + utils.MakeInvalidRequestErrorWithMessage(c, "Task is not running") return } s.scheduler.AsyncAbort(uint(taskGroupID)) c.JSON(http.StatusOK, utils.APIEmptyResponse{}) } -// @Summary Delete task group -// @Description delete a task group by providing task group ID -// @Produce json +// @Summary Delete a log search task group // @Param id path string true "task group id" // @Security JwtAuth // @Success 200 {object} utils.APIEmptyResponse @@ -331,8 +312,7 @@ func (s *Service) DeleteTaskGroup(c *gin.Context) { c.JSON(http.StatusOK, utils.APIEmptyResponse{}) } -// @Summary Get download token -// @Description get download token with multiple task IDs +// @Summary Generate a download token for downloading logs // @Produce plain // @Param id query []string false "task id" collectionFormat(csv) // @Security JwtAuth @@ -345,15 +325,13 @@ func (s *Service) GetDownloadToken(c *gin.Context) { str := strings.Join(ids, ",") token, err := utils.NewJWTString("logs/download", str) if err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + _ = c.Error(err) return } c.String(http.StatusOK, token) } -// @Summary Download -// @Description download logs by multiple task IDs +// @Summary Download logs // @Produce application/x-tar,application/zip // @Param token query string true "download token" // @Failure 400 {object} utils.APIError @@ -364,8 +342,7 @@ func (s *Service) DownloadLogs(c *gin.Context) { token := c.Query("token") str, err := utils.ParseJWTString("logs/download", token) if err != nil { - c.Status(http.StatusUnauthorized) - _ = c.Error(utils.ErrInvalidRequest.New(err.Error())) + utils.MakeInvalidRequestErrorFromError(c, err) return } ids := strings.Split(str, ",") @@ -383,8 +360,7 @@ func (s *Service) DownloadLogs(c *gin.Context) { switch len(tasks) { case 0: - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.New("At least one target should be provided")) + utils.MakeInvalidRequestErrorWithMessage(c, "Expect at least 1 target") case 1: serveTaskForDownload(tasks[0], c) default: diff --git a/pkg/apiserver/metrics/metrics.go b/pkg/apiserver/metrics/metrics.go index dc72cf0684..d41c9da3f1 100644 --- a/pkg/apiserver/metrics/metrics.go +++ b/pkg/apiserver/metrics/metrics.go @@ -15,6 +15,8 @@ import ( "go.uber.org/fx" "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/httpc" "github.com/pingcap-incubator/tidb-dashboard/pkg/utils/topology" ) @@ -24,18 +26,23 @@ var ( ErrPrometheusQueryFailed = ErrNS.NewType("prometheus_query_failed") ) +const ( + defaultPromQueryTimeout = time.Second * 30 +) + +type ServiceParams struct { + fx.In + HTTPClient *httpc.Client + EtcdClient *clientv3.Client +} + type Service struct { + params ServiceParams lifecycleCtx context.Context - - httpClient *http.Client - etcdClient *clientv3.Client } -func NewService(lc fx.Lifecycle, httpClient *http.Client, etcdClient *clientv3.Client) *Service { - s := &Service{ - httpClient: httpClient, - etcdClient: etcdClient, - } +func NewService(lc fx.Lifecycle, p ServiceParams) *Service { + s := &Service{params: p} lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { @@ -67,7 +74,6 @@ type QueryResponse struct { // @Summary Query metrics // @Description Query metrics in the given range -// @Produce json // @Param q query QueryRequest true "Query" // @Success 200 {object} QueryResponse // @Failure 401 {object} utils.APIError "Unauthorized failure" @@ -76,11 +82,11 @@ type QueryResponse struct { func (s *Service) queryHandler(c *gin.Context) { var req QueryRequest if err := c.ShouldBindQuery(&req); err != nil { - _ = c.Error(err) + utils.MakeInvalidRequestErrorFromError(c, err) return } - pi, err := topology.FetchPrometheusTopology(s.lifecycleCtx, s.etcdClient) + pi, err := topology.FetchPrometheusTopology(s.lifecycleCtx, s.params.EtcdClient) if err != nil { _ = c.Error(err) return @@ -103,9 +109,7 @@ func (s *Service) queryHandler(c *gin.Context) { return } - newHTTPClient := *s.httpClient - newHTTPClient.Timeout = 10 * time.Second - promResp, err := newHTTPClient.Do(promReq) + promResp, err := s.params.HTTPClient.WithTimeout(defaultPromQueryTimeout).Do(promReq) if err != nil { _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to send requests to Prometheus")) return diff --git a/pkg/apiserver/profiling/model.go b/pkg/apiserver/profiling/model.go index 36a990b3d6..57d53ff9dc 100644 --- a/pkg/apiserver/profiling/model.go +++ b/pkg/apiserver/profiling/model.go @@ -16,11 +16,11 @@ package profiling import ( "context" "fmt" - "net/http" "time" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/model" "github.com/pingcap-incubator/tidb-dashboard/pkg/dbstore" + "github.com/pingcap-incubator/tidb-dashboard/pkg/httpc" ) // TaskState is used to represent the task/task group state. @@ -70,14 +70,14 @@ func autoMigrate(db *dbstore.DB) error { // Task is the unit to fetch profiling information. type Task struct { *TaskModel - ctx context.Context - cancel context.CancelFunc - taskGroup *TaskGroup - tls bool + ctx context.Context + cancel context.CancelFunc + taskGroup *TaskGroup + httpScheme string } // NewTask creates a new profiling task. -func NewTask(ctx context.Context, taskGroup *TaskGroup, target model.RequestTargetNode, tls bool) *Task { +func NewTask(ctx context.Context, taskGroup *TaskGroup, target model.RequestTargetNode, httpScheme string) *Task { ctx, cancel := context.WithCancel(ctx) return &Task{ TaskModel: &TaskModel{ @@ -86,16 +86,16 @@ func NewTask(ctx context.Context, taskGroup *TaskGroup, target model.RequestTarg Target: target, StartedAt: time.Now().Unix(), }, - ctx: ctx, - cancel: cancel, - taskGroup: taskGroup, - tls: tls, + ctx: ctx, + cancel: cancel, + taskGroup: taskGroup, + httpScheme: httpScheme, } } -func (t *Task) run(httpClient *http.Client) { +func (t *Task) run(httpClient *httpc.Client) { fileNameWithoutExt := fmt.Sprintf("profiling_%d_%d_%s", t.TaskGroupID, t.ID, t.Target.FileName()) - svgFilePath, err := profileAndWriteSVG(t.ctx, &t.Target, fileNameWithoutExt, t.taskGroup.ProfileDurationSecs, httpClient, t.tls) + svgFilePath, err := profileAndWriteSVG(t.ctx, &t.Target, fileNameWithoutExt, t.taskGroup.ProfileDurationSecs, httpClient, t.httpScheme) if err != nil { t.Error = err.Error() t.State = TaskStateError diff --git a/pkg/apiserver/profiling/profile.go b/pkg/apiserver/profiling/profile.go index db73d88f60..42c9010321 100644 --- a/pkg/apiserver/profiling/profile.go +++ b/pkg/apiserver/profiling/profile.go @@ -30,6 +30,7 @@ import ( "github.com/google/pprof/profile" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/model" + "github.com/pingcap-incubator/tidb-dashboard/pkg/httpc" ) var ( @@ -42,7 +43,11 @@ type flagSet struct { args []string } -func fetchPprof(ctx context.Context, httpClient *http.Client, target *model.RequestTargetNode, fileNameWithoutExt, format string, profileDurationSecs uint, schema string) (string, error) { +const ( + maxProfilingTimeout = time.Minute * 5 +) + +func fetchPprof(ctx context.Context, httpClient *httpc.Client, target *model.RequestTargetNode, fileNameWithoutExt, format string, profileDurationSecs uint, httpScheme string) (string, error) { tmpfile, err := ioutil.TempFile("", fileNameWithoutExt) if err != nil { return "", fmt.Errorf("failed to create temp file: %v", err) @@ -63,7 +68,7 @@ func fetchPprof(ctx context.Context, httpClient *http.Client, target *model.Requ args: args, } if err := driver.PProf(&driver.Options{ - Fetch: &fetcher{ctx: ctx, httpClient: httpClient, target: target, output: tmpPath, schema: schema}, + Fetch: &fetcher{ctx: ctx, httpClient: httpClient, target: target, output: tmpPath, httpScheme: httpScheme}, Flagset: f, UI: &blankPprofUI{}, Writer: &oswriter{output: tmpPath}, @@ -101,10 +106,10 @@ func (o *oswriter) Open(name string) (io.WriteCloser, error) { type fetcher struct { ctx context.Context - httpClient *http.Client + httpClient *httpc.Client target *model.RequestTargetNode output string - schema string + httpScheme string } func (f *fetcher) Fetch(src string, duration, timeout time.Duration) (*profile.Profile, string, error) { @@ -118,7 +123,7 @@ func (f *fetcher) Fetch(src string, duration, timeout time.Duration) (*profile.P default: return nil, "", fmt.Errorf("unsupported target %s", f.target) } - url = fmt.Sprintf("%s://%s:%d%s", f.schema, f.target.IP, f.target.Port, url) + url = fmt.Sprintf("%s://%s:%d%s", f.httpScheme, f.target.IP, f.target.Port, url) p, err := f.getProfile(f.target, url) return p, url, err @@ -133,7 +138,7 @@ func (f *fetcher) getProfile(target *model.RequestTargetNode, source string) (*p // forbidden PD follower proxy req.Header.Add("PD-Allow-follower-handle", "true") } - resp, err := f.httpClient.Do(req) + resp, err := f.httpClient.WithTimeout(maxProfilingTimeout).Do(req) if err != nil { return nil, fmt.Errorf("request %s failed: %v", source, err) } @@ -144,28 +149,25 @@ func (f *fetcher) getProfile(target *model.RequestTargetNode, source string) (*p return profile.Parse(resp.Body) } -func profileAndWriteSVG(ctx context.Context, target *model.RequestTargetNode, fileNameWithoutExt string, profileDurationSecs uint, httpClient *http.Client, tls bool) (string, error) { - schema := "http" - if tls { - schema = "https" - } +func profileAndWriteSVG(ctx context.Context, target *model.RequestTargetNode, fileNameWithoutExt string, profileDurationSecs uint, httpClient *httpc.Client, httpScheme string) (string, error) { switch target.Kind { case model.NodeKindTiKV: - return fetchTiKVFlameGraphSVG(ctx, httpClient, target, fileNameWithoutExt, profileDurationSecs, schema) + return fetchTiKVFlameGraphSVG(ctx, httpClient, target, fileNameWithoutExt, profileDurationSecs, httpScheme) case model.NodeKindPD, model.NodeKindTiDB: - return fetchPprofSVG(ctx, httpClient, target, fileNameWithoutExt, profileDurationSecs, schema) + return fetchPprofSVG(ctx, httpClient, target, fileNameWithoutExt, profileDurationSecs, httpScheme) default: return "", fmt.Errorf("unsupported target %s", target) } } -func fetchTiKVFlameGraphSVG(ctx context.Context, httpClient *http.Client, target *model.RequestTargetNode, fileNameWithoutExt string, profileDurationSecs uint, schema string) (string, error) { - uri := fmt.Sprintf("%s://%s:%d/debug/pprof/profile?seconds=%d", schema, target.IP, target.Port, profileDurationSecs) +func fetchTiKVFlameGraphSVG(ctx context.Context, httpClient *httpc.Client, target *model.RequestTargetNode, fileNameWithoutExt string, profileDurationSecs uint, httpScheme string) (string, error) { + // TODO: Switch to use tikv.Client + uri := fmt.Sprintf("%s://%s:%d/debug/pprof/profile?seconds=%d", httpScheme, target.IP, target.Port, profileDurationSecs) req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) if err != nil { return "", fmt.Errorf("failed to create a new request %s: %v", uri, err) } - resp, err := httpClient.Do(req) + resp, err := httpClient.WithTimeout(maxProfilingTimeout).Do(req) if err != nil { return "", fmt.Errorf("request %s failed: %v", uri, err) } @@ -197,8 +199,9 @@ func writePprofRsSVG(body io.ReadCloser, fileNameWithoutExt string) (string, err return svgFilePath, nil } -func fetchPprofSVG(ctx context.Context, httpClient *http.Client, target *model.RequestTargetNode, fileNameWithoutExt string, profileDurationSecs uint, schema string) (string, error) { - f, err := fetchPprof(ctx, httpClient, target, fileNameWithoutExt, "dot", profileDurationSecs, schema) +func fetchPprofSVG(ctx context.Context, httpClient *httpc.Client, target *model.RequestTargetNode, fileNameWithoutExt string, profileDurationSecs uint, httpScheme string) (string, error) { + // TODO: Switch to use tidb.Client or pd.Client + f, err := fetchPprof(ctx, httpClient, target, fileNameWithoutExt, "dot", profileDurationSecs, httpScheme) if err != nil { return "", fmt.Errorf("failed to get DOT output from file: %v", err) } diff --git a/pkg/apiserver/profiling/router.go b/pkg/apiserver/profiling/router.go index e8f97c2ce5..97bb1db488 100644 --- a/pkg/apiserver/profiling/router.go +++ b/pkg/apiserver/profiling/router.go @@ -21,6 +21,8 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/pingcap/log" + "go.uber.org/zap" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" @@ -48,24 +50,21 @@ func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { // @ID startProfiling // @Summary Start profiling // @Description Start a profiling task group -// @Produce json // @Param req body StartRequest true "profiling request" // @Security JwtAuth // @Success 200 {object} TaskGroupModel "task group" -// @Failure 400 {object} utils.APIError +// @Failure 400 {object} utils.APIError "Bad request" // @Failure 401 {object} utils.APIError "Unauthorized failure" // @Failure 500 {object} utils.APIError // @Router /profiling/group/start [post] func (s *Service) handleStartGroup(c *gin.Context) { var req StartRequest if err := c.ShouldBindJSON(&req); err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(err) + utils.MakeInvalidRequestErrorFromError(c, err) return } if len(req.Targets) == 0 { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.NewWithNoMessage()) + utils.MakeInvalidRequestErrorWithMessage(c, "Expect at least 1 target") return } @@ -84,13 +83,11 @@ func (s *Service) handleStartGroup(c *gin.Context) { select { case <-session.ch: if session.err != nil { - c.Status(http.StatusInternalServerError) _ = c.Error(session.err) } else { c.JSON(http.StatusOK, session.taskGroup.TaskGroupModel) } case <-time.After(Timeout): - c.Status(http.StatusInternalServerError) _ = c.Error(ErrTimeout.NewWithNoMessage()) } } @@ -98,16 +95,14 @@ func (s *Service) handleStartGroup(c *gin.Context) { // @ID getProfilingGroups // @Summary List all profiling groups // @Description List all profiling groups -// @Produce json // @Security JwtAuth // @Success 200 {array} TaskGroupModel // @Failure 401 {object} utils.APIError "Unauthorized failure" // @Router /profiling/group/list [get] func (s *Service) getGroupList(c *gin.Context) { var resp []TaskGroupModel - err := s.db.Order("id DESC").Find(&resp).Error + err := s.params.LocalStore.Order("id DESC").Find(&resp).Error if err != nil { - c.Status(http.StatusInternalServerError) _ = c.Error(err) return } @@ -123,7 +118,6 @@ type GroupDetailResponse struct { // @ID getProfilingGroupDetail // @Summary List all tasks with a given group ID // @Description List all profiling tasks with a given group ID -// @Produce json // @Param groupId path string true "group ID" // @Security JwtAuth // @Success 200 {object} GroupDetailResponse @@ -133,22 +127,19 @@ type GroupDetailResponse struct { func (s *Service) getGroupDetail(c *gin.Context) { taskGroupID, err := strconv.Atoi(c.Param("groupId")) if err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(err) + utils.MakeInvalidRequestErrorFromError(c, err) return } var taskGroup TaskGroupModel - err = s.db.Where("id = ?", taskGroupID).Find(&taskGroup).Error + err = s.params.LocalStore.Where("id = ?", taskGroupID).Find(&taskGroup).Error if err != nil { - c.Status(http.StatusBadRequest) _ = c.Error(err) return } var tasks []TaskModel - err = s.db.Where("task_group_id = ?", taskGroupID).Find(&tasks).Error + err = s.params.LocalStore.Where("task_group_id = ?", taskGroupID).Find(&tasks).Error if err != nil { - c.Status(http.StatusBadRequest) _ = c.Error(err) return } @@ -163,7 +154,6 @@ func (s *Service) getGroupDetail(c *gin.Context) { // @ID cancelProfilingGroup // @Summary Cancel all tasks with a given group ID // @Description Cancel all profling tasks with a given group ID -// @Produce json // @Param groupId path string true "group ID" // @Security JwtAuth // @Success 200 {object} utils.APIEmptyResponse @@ -173,12 +163,10 @@ func (s *Service) getGroupDetail(c *gin.Context) { func (s *Service) handleCancelGroup(c *gin.Context) { taskGroupID, err := strconv.Atoi(c.Param("groupId")) if err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(err) + utils.MakeInvalidRequestErrorFromError(c, err) return } if err := s.cancelGroup(uint(taskGroupID)); err != nil { - c.Status(http.StatusBadRequest) _ = c.Error(err) return } @@ -195,14 +183,14 @@ func (s *Service) handleCancelGroup(c *gin.Context) { // @Success 200 {string} string // @Failure 400 {object} utils.APIError // @Failure 401 {object} utils.APIError "Unauthorized failure" +// @Failure 500 {object} utils.APIError // @Router /profiling/action_token [get] func (s *Service) getActionToken(c *gin.Context) { id := c.Query("id") action := c.Query("action") // group_download, single_download, single_view token, err := utils.NewJWTString("profiling/"+action, id) if err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + _ = c.Error(err) return } c.String(http.StatusOK, token) @@ -222,20 +210,17 @@ func (s *Service) downloadGroup(c *gin.Context) { token := c.Query("token") str, err := utils.ParseJWTString("profiling/group_download", token) if err != nil { - c.Status(http.StatusUnauthorized) - _ = c.Error(utils.ErrInvalidRequest.New(err.Error())) + utils.MakeInvalidRequestErrorFromError(c, err) return } taskGroupID, err := strconv.Atoi(str) if err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(err) + utils.MakeInvalidRequestErrorFromError(c, err) return } var tasks []TaskModel - err = s.db.Where("task_group_id = ? AND state = ?", taskGroupID, TaskStateFinish).Find(&tasks).Error + err = s.params.LocalStore.Where("task_group_id = ? AND state = ?", taskGroupID, TaskStateFinish).Find(&tasks).Error if err != nil { - c.Status(http.StatusBadRequest) _ = c.Error(err) return } @@ -245,23 +230,13 @@ func (s *Service) downloadGroup(c *gin.Context) { filePathes[i] = task.FilePath } - temp, err := ioutil.TempFile("", fmt.Sprintf("taskgroup_%d", taskGroupID)) + fileName := fmt.Sprintf("profiling_pack_%d.zip", taskGroupID) + c.Writer.Header().Set("Content-type", "application/octet-stream") + c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName)) + err = utils.StreamZipPack(c.Writer, filePathes, true) if err != nil { - c.Status(http.StatusInternalServerError) - _ = c.Error(err) - return - } - - err = createTarball(temp, filePathes) - defer temp.Close() - if err != nil { - c.Status(http.StatusInternalServerError) - _ = c.Error(err) - return + log.Error("Stream zip pack failed", zap.Error(err)) } - - fileName := fmt.Sprintf("profiling_pack_%d.tar.gz", taskGroupID) - c.FileAttachment(temp.Name(), fileName) } // @ID downloadProfilingSingle @@ -279,41 +254,28 @@ func (s *Service) downloadSingle(c *gin.Context) { token := c.Query("token") str, err := utils.ParseJWTString("profiling/single_download", token) if err != nil { - c.Status(http.StatusUnauthorized) - _ = c.Error(utils.ErrInvalidRequest.New(err.Error())) + utils.MakeInvalidRequestErrorFromError(c, err) return } taskID, err := strconv.Atoi(str) if err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(err) + utils.MakeInvalidRequestErrorFromError(c, err) return } task := TaskModel{} - err = s.db.Where("id = ? AND state = ?", taskID, TaskStateFinish).First(&task).Error - if err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(err) - return - } - - temp, err := ioutil.TempFile("", fmt.Sprintf("task_%d", taskID)) + err = s.params.LocalStore.Where("id = ? AND state = ?", taskID, TaskStateFinish).First(&task).Error if err != nil { - c.Status(http.StatusInternalServerError) _ = c.Error(err) return } - err = createTarball(temp, []string{task.FilePath}) - defer temp.Close() + fileName := fmt.Sprintf("profiling_%d.zip", taskID) + c.Writer.Header().Set("Content-type", "application/octet-stream") + c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName)) + err = utils.StreamZipPack(c.Writer, []string{task.FilePath}, true) if err != nil { - c.Status(http.StatusInternalServerError) - _ = c.Error(err) - return + log.Error("Stream zip pack failed", zap.Error(err)) } - - fileName := fmt.Sprintf("profiling_%d.tar.gz", taskID) - c.FileAttachment(temp.Name(), fileName) } // @ID viewProfilingSingle @@ -330,27 +292,23 @@ func (s *Service) viewSingle(c *gin.Context) { token := c.Query("token") str, err := utils.ParseJWTString("profiling/single_view", token) if err != nil { - c.Status(http.StatusUnauthorized) - _ = c.Error(utils.ErrInvalidRequest.New(err.Error())) + utils.MakeInvalidRequestErrorFromError(c, err) return } taskID, err := strconv.Atoi(str) if err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(err) + utils.MakeInvalidRequestErrorFromError(c, err) return } task := TaskModel{} - err = s.db.Where("id = ? AND state = ?", taskID, TaskStateFinish).First(&task).Error + err = s.params.LocalStore.Where("id = ? AND state = ?", taskID, TaskStateFinish).First(&task).Error if err != nil { - c.Status(http.StatusBadRequest) _ = c.Error(err) return } content, err := ioutil.ReadFile(task.FilePath) if err != nil { - c.Status(http.StatusInternalServerError) _ = c.Error(err) return } @@ -360,7 +318,6 @@ func (s *Service) viewSingle(c *gin.Context) { // @ID deleteProfilingGroup // @Summary Delete all tasks with a given group ID // @Description Delete all finished profiling tasks with a given group ID -// @Produce json // @Param groupId path string true "group ID" // @Security JwtAuth // @Success 200 {object} utils.APIEmptyResponse @@ -371,23 +328,19 @@ func (s *Service) viewSingle(c *gin.Context) { func (s *Service) deleteGroup(c *gin.Context) { taskGroupID, err := strconv.Atoi(c.Param("groupId")) if err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(err) + utils.MakeInvalidRequestErrorFromError(c, err) return } if err := s.cancelGroup(uint(taskGroupID)); err != nil { - c.Status(http.StatusBadRequest) _ = c.Error(err) return } - if err = s.db.Where("task_group_id = ?", taskGroupID).Delete(&TaskModel{}).Error; err != nil { - c.Status(http.StatusInternalServerError) + if err = s.params.LocalStore.Where("task_group_id = ?", taskGroupID).Delete(&TaskModel{}).Error; err != nil { _ = c.Error(err) return } - if err = s.db.Where("id = ?", taskGroupID).Delete(&TaskGroupModel{}).Error; err != nil { - c.Status(http.StatusInternalServerError) + if err = s.params.LocalStore.Where("id = ?", taskGroupID).Delete(&TaskGroupModel{}).Error; err != nil { _ = c.Error(err) return } @@ -395,14 +348,13 @@ func (s *Service) deleteGroup(c *gin.Context) { } // @Summary Get Profiling Dynamic Config -// @Produce json // @Success 200 {object} config.ProfilingConfig // @Router /profiling/config [get] // @Security JwtAuth // @Failure 401 {object} utils.APIError "Unauthorized failure" // @Failure 500 {object} utils.APIError func (s *Service) getDynamicConfig(c *gin.Context) { - dc, err := s.cfgManager.Get() + dc, err := s.params.ConfigManager.Get() if err != nil { _ = c.Error(err) return @@ -411,27 +363,24 @@ func (s *Service) getDynamicConfig(c *gin.Context) { } // @Summary Set Profiling Dynamic Config -// @Produce json // @Param request body config.ProfilingConfig true "Request body" // @Success 200 {object} config.ProfilingConfig // @Router /profiling/config [put] // @Security JwtAuth -// @Failure 400 {object} utils.APIError +// @Failure 400 {object} utils.APIError "Bad request" // @Failure 401 {object} utils.APIError "Unauthorized failure" // @Failure 500 {object} utils.APIError func (s *Service) setDynamicConfig(c *gin.Context) { var req config.ProfilingConfig if err := c.ShouldBindJSON(&req); err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + utils.MakeInvalidRequestErrorFromError(c, err) return } var opt config.DynamicConfigOption = func(dc *config.DynamicConfig) { dc.Profiling = req } - if err := s.cfgManager.Modify(opt); err != nil { - c.Status(http.StatusInternalServerError) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + if err := s.params.ConfigManager.Modify(opt); err != nil { + _ = c.Error(err) return } c.JSON(http.StatusOK, req) diff --git a/pkg/apiserver/profiling/service.go b/pkg/apiserver/profiling/service.go index 0e0e6cf921..c03d5341a1 100644 --- a/pkg/apiserver/profiling/service.go +++ b/pkg/apiserver/profiling/service.go @@ -15,7 +15,6 @@ package profiling import ( "context" - "net/http" "sync" "time" @@ -27,6 +26,7 @@ import ( "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/model" "github.com/pingcap-incubator/tidb-dashboard/pkg/config" "github.com/pingcap-incubator/tidb-dashboard/pkg/dbstore" + "github.com/pingcap-incubator/tidb-dashboard/pkg/httpc" ) const ( @@ -51,35 +51,27 @@ type StartRequestSession struct { err error } -// Service is used to provide a kind of feature. -type Service struct { - wg sync.WaitGroup - - config *config.Config - cfgManager *config.DynamicConfigManager - db *dbstore.DB - httpClient *http.Client +type ServiceParams struct { + fx.In + Config *config.Config + ConfigManager *config.DynamicConfigManager + LocalStore *dbstore.DB + HTTPClient *httpc.Client +} +type Service struct { + params ServiceParams + wg sync.WaitGroup sessionCh chan *StartRequestSession lastTaskGroup *TaskGroup tasks sync.Map } -// NewService creates a new service. -func NewService( - lc fx.Lifecycle, - cfg *config.Config, - cfgManager *config.DynamicConfigManager, - db *dbstore.DB, - httpClient *http.Client, -) (*Service, error) { - if err := autoMigrate(db); err != nil { +func NewService(lc fx.Lifecycle, p ServiceParams) (*Service, error) { + if err := autoMigrate(p.LocalStore); err != nil { return nil, err } - // Change the default timeout - newHTTPClient := *httpClient - newHTTPClient.Timeout = 0 - s := &Service{config: cfg, cfgManager: cfgManager, db: db, httpClient: &newHTTPClient} + s := &Service{params: p} lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { s.wg.Add(1) @@ -99,7 +91,7 @@ func NewService( } func (s *Service) serviceLoop(ctx context.Context) { - cfgCh := s.cfgManager.NewPushChannel() + cfgCh := s.params.ConfigManager.NewPushChannel() s.sessionCh = make(chan *StartRequestSession, 1000) defer close(s.sessionCh) @@ -161,16 +153,16 @@ func (s *Service) exclusiveExecute(ctx context.Context, req *StartRequest) (*Tas } func (s *Service) startGroup(ctx context.Context, req *StartRequest) (*TaskGroup, error) { - taskGroup := NewTaskGroup(s.db, req.DurationSecs, model.NewRequestTargetStatisticsFromArray(&req.Targets)) - if err := s.db.Create(taskGroup.TaskGroupModel).Error; err != nil { + taskGroup := NewTaskGroup(s.params.LocalStore, req.DurationSecs, model.NewRequestTargetStatisticsFromArray(&req.Targets)) + if err := s.params.LocalStore.Create(taskGroup.TaskGroupModel).Error; err != nil { log.Warn("failed to start task group", zap.Error(err)) return nil, err } tasks := make([]*Task, 0, len(req.Targets)) for _, target := range req.Targets { - t := NewTask(ctx, taskGroup, target, s.config.ClusterTLSConfig != nil) - s.db.Create(t.TaskModel) + t := NewTask(ctx, taskGroup, target, s.params.Config.GetClusterHTTPScheme()) + s.params.LocalStore.Create(t.TaskModel) s.tasks.Store(t.ID, t) tasks = append(tasks, t) } @@ -183,13 +175,13 @@ func (s *Service) startGroup(ctx context.Context, req *StartRequest) (*TaskGroup wg.Add(1) go func(idx int) { defer wg.Done() - tasks[idx].run(s.httpClient) + tasks[idx].run(s.params.HTTPClient) s.tasks.Delete(tasks[idx].ID) }(i) } wg.Wait() taskGroup.State = TaskStateFinish - s.db.Save(taskGroup.TaskGroupModel) + s.params.LocalStore.Save(taskGroup.TaskGroupModel) }() return taskGroup, nil @@ -197,7 +189,7 @@ func (s *Service) startGroup(ctx context.Context, req *StartRequest) (*TaskGroup func (s *Service) cancelGroup(taskGroupID uint) error { var tasks []TaskModel - if err := s.db.Where("task_group_id = ? AND state = ?", taskGroupID, TaskStateRunning).Find(&tasks).Error; err != nil { + if err := s.params.LocalStore.Where("task_group_id = ? AND state = ?", taskGroupID, TaskStateRunning).Find(&tasks).Error; err != nil { log.Warn("failed to cancel task group", zap.Error(err)) return err } @@ -214,7 +206,7 @@ func (s *Service) cancelGroup(taskGroupID uint) error { defer ticker.Stop() for { var runningTasks []TaskModel - if err := s.db.Where("task_group_id = ? AND state = ?", taskGroupID, TaskStateRunning).Find(&runningTasks).Error; err != nil { + if err := s.params.LocalStore.Where("task_group_id = ? AND state = ?", taskGroupID, TaskStateRunning).Find(&runningTasks).Error; err != nil { log.Warn("failed to cancel task group", zap.Error(err)) return err } diff --git a/pkg/apiserver/profiling/util.go b/pkg/apiserver/profiling/util.go deleted file mode 100644 index 29c9a82aea..0000000000 --- a/pkg/apiserver/profiling/util.go +++ /dev/null @@ -1,80 +0,0 @@ -// 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 profiling - -import ( - "archive/tar" - "compress/gzip" - "io" - "os" -) - -func createTarball(d *os.File, files []string) error { - gw := gzip.NewWriter(d) - defer gw.Close() - tw := tar.NewWriter(gw) - defer tw.Close() - for _, file := range files { - f, err := os.Open(file) - if err != nil { - return err - } - defer f.Close() - err = compress(f, "", tw) - if err != nil { - return err - } - } - return nil -} - -func compress(file *os.File, prefix string, tw *tar.Writer) error { - info, err := file.Stat() - if err != nil { - return err - } - if info.IsDir() { - prefix = prefix + "/" + info.Name() - fileInfos, err := file.Readdir(-1) - if err != nil { - return err - } - for _, fi := range fileInfos { - f, err := os.Open(file.Name() + "/" + fi.Name()) - if err != nil { - return err - } - err = compress(f, prefix, tw) - if err != nil { - return err - } - } - } else { - header, err := tar.FileInfoHeader(info, "") - header.Name = prefix + "/" + header.Name - if err != nil { - return err - } - err = tw.WriteHeader(header) - if err != nil { - return err - } - _, err = io.Copy(tw, file) - file.Close() - if err != nil { - return err - } - } - return nil -} diff --git a/pkg/apiserver/queryeditor/service.go b/pkg/apiserver/queryeditor/service.go new file mode 100644 index 0000000000..6c3e3c6db0 --- /dev/null +++ b/pkg/apiserver/queryeditor/service.go @@ -0,0 +1,170 @@ +// 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 queryeditor + +import ( + "context" + "database/sql" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/pingcap/log" + "go.uber.org/fx" + "go.uber.org/zap" + + "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 ServiceParams struct { + fx.In + Config *config.Config + TiDBClient *tidb.Client +} + +type Service struct { + params ServiceParams + lifecycleCtx context.Context +} + +func NewService(lc fx.Lifecycle, p ServiceParams) *Service { + service := &Service{params: p} + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + service.lifecycleCtx = ctx + return nil + }, + }) + + return service +} + +func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { + endpoint := r.Group("/query_editor") + endpoint.Use(auth.MWAuthRequired()) + endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient)) + endpoint.Use(utils.MWForbidByExperimentalFlag(s.params.Config.EnableExperimental)) + endpoint.POST("/run", s.runHandler) +} + +type RunRequest struct { + Statements string `json:"statements" example:"show databases;"` + MaxRows int `json:"max_rows" example:"1000"` +} + +type RunResponse struct { + ErrorMsg string `json:"error_msg"` + ColumnNames []string `json:"column_names"` + Rows [][]interface{} `json:"rows"` + ExecutionMs int64 `json:"execution_ms"` + ActualRows int `json:"actual_rows"` +} + +func executeStatements(context context.Context, db *sql.DB, statements string) ([]string, [][]interface{}, error) { + rows, err := db.QueryContext(context, statements) + if err != nil { + return nil, nil, err + } + + defer rows.Close() + + colNames, err := rows.Columns() + if err != nil { + return nil, nil, err + } + + retRows := make([][]interface{}, 0) + + values := make([]sql.RawBytes, len(colNames)) + scanArgs := make([]interface{}, len(values)) + for i := range values { + scanArgs[i] = &values[i] + } + + for rows.Next() { + err = rows.Scan(scanArgs...) + if err != nil { + return nil, nil, err + } + + retRow := make([]interface{}, 0, len(values)) + var value interface{} + for _, col := range values { + if col == nil { + value = nil + } else { + value = string(col) + } + retRow = append(retRow, value) + } + retRows = append(retRows, retRow) + } + + if err = rows.Err(); err != nil { + return nil, nil, err + } + + return colNames, retRows, nil +} + +// @ID queryEditorRun +// @Summary Run statements +// @Param request body RunRequest true "Request body" +// @Success 200 {object} RunResponse +// @Router /query_editor/run [post] +// @Security JwtAuth +// @Failure 400 {object} utils.APIError "Bad request" +// @Failure 401 {object} utils.APIError "Unauthorized failure" +// @Failure 403 {object} utils.APIError "Experimental feature not enabled" +func (s *Service) runHandler(c *gin.Context) { + var req RunRequest + if err := c.ShouldBindJSON(&req); err != nil { + utils.MakeInvalidRequestErrorFromError(c, err) + return + } + + ctx, cancel := context.WithTimeout(s.lifecycleCtx, time.Minute*5) + defer cancel() + + startTime := time.Now() + colNames, rows, err := executeStatements(ctx, utils.GetTiDBConnection(c).DB(), req.Statements) + elapsedTime := time.Since(startTime) + + if err != nil { + log.Warn("Failed to execute user input statements", zap.String("statements", req.Statements), zap.Error(err)) + c.JSON(http.StatusOK, RunResponse{ + ErrorMsg: err.Error(), + ColumnNames: nil, + Rows: nil, + ExecutionMs: elapsedTime.Milliseconds(), + ActualRows: 0, + }) + return + } + + truncatedRows := rows + if len(truncatedRows) > req.MaxRows { + truncatedRows = truncatedRows[:req.MaxRows] + } + + c.JSON(http.StatusOK, RunResponse{ + ColumnNames: colNames, + Rows: truncatedRows, + ExecutionMs: elapsedTime.Milliseconds(), + ActualRows: len(rows), + }) +} diff --git a/pkg/apiserver/slowquery/service.go b/pkg/apiserver/slowquery/service.go index 9a06a097a2..c807e2735c 100644 --- a/pkg/apiserver/slowquery/service.go +++ b/pkg/apiserver/slowquery/service.go @@ -17,35 +17,35 @@ import ( "net/http" "github.com/gin-gonic/gin" + "go.uber.org/fx" "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/dbstore" "github.com/pingcap-incubator/tidb-dashboard/pkg/tidb" ) +type ServiceParams struct { + fx.In + TiDBClient *tidb.Client +} + type Service struct { - config *config.Config - db *dbstore.DB - tidbForwarder *tidb.Forwarder + params ServiceParams } -func NewService(config *config.Config, tidbForwarder *tidb.Forwarder, db *dbstore.DB) *Service { - return &Service{config: config, db: db, tidbForwarder: tidbForwarder} +func NewService(p ServiceParams) *Service { + return &Service{params: p} } func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/slow_query") endpoint.Use(auth.MWAuthRequired()) - endpoint.Use(utils.MWConnectTiDB(s.tidbForwarder)) + endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient)) endpoint.GET("/list", s.listHandler) endpoint.GET("/detail", s.detailhandler) } -// @Summary Example: Get all databases -// @Description Get all databases. -// @Produce json +// @Summary List all slow queries // @Param q query GetListRequest true "Query" // @Success 200 {array} Base // @Router /slow_query/list [get] @@ -54,7 +54,7 @@ func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { func (s *Service) listHandler(c *gin.Context) { var req GetListRequest if err := c.ShouldBindQuery(&req); err != nil { - _ = c.Error(err) + utils.MakeInvalidRequestErrorFromError(c, err) return } @@ -67,9 +67,7 @@ func (s *Service) listHandler(c *gin.Context) { c.JSON(http.StatusOK, results) } -// @Summary Example: Get all databases -// @Description Get all databases. -// @Produce json +// @Summary Get details of a slow query // @Param q query GetDetailRequest true "Query" // @Success 200 {object} SlowQuery // @Router /slow_query/detail [get] @@ -78,8 +76,7 @@ func (s *Service) listHandler(c *gin.Context) { func (s *Service) detailhandler(c *gin.Context) { var req GetDetailRequest if err := c.ShouldBindQuery(&req); err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + utils.MakeInvalidRequestErrorFromError(c, err) return } diff --git a/pkg/apiserver/statement/models.go b/pkg/apiserver/statement/models.go index 40d4030d22..a32e32ada8 100644 --- a/pkg/apiserver/statement/models.go +++ b/pkg/apiserver/statement/models.go @@ -34,6 +34,7 @@ type TimeRange struct { } type Model struct { + AggPlanCount int `json:"plan_count" agg:"COUNT(DISTINCT plan_digest)"` AggExecCount int `json:"exec_count" agg:"SUM(exec_count)"` AggSumErrors int `json:"sum_errors" agg:"SUM(sum_errors)"` AggSumWarnings int `json:"sum_warnings" agg:"SUM(sum_warnings)"` diff --git a/pkg/apiserver/statement/queries.go b/pkg/apiserver/statement/queries.go index cf935bb238..d2b4e24bd4 100644 --- a/pkg/apiserver/statement/queries.go +++ b/pkg/apiserver/statement/queries.go @@ -134,6 +134,7 @@ func QueryStatementsOverview( schemas, stmtTypes []string, text string) (result []Model, err error) { fields := getAggrFields( + "plan_count", "table_names", "schema_name", "digest", @@ -157,8 +158,8 @@ func QueryStatementsOverview( query := db. Select(strings.Join(fields, ", ")). Table(statementsTable). - Where("UNIX_TIMESTAMP(summary_begin_time) >= ? AND UNIX_TIMESTAMP(summary_end_time) <= ?", beginTime, endTime). - Group("schema_name, digest, digest_text"). + Where("summary_begin_time >= FROM_UNIXTIME(?) AND summary_end_time <= FROM_UNIXTIME(?)", beginTime, endTime). + Group("schema_name, digest"). Order("agg_sum_latency DESC") if len(schemas) > 0 { @@ -212,7 +213,7 @@ func QueryPlans( err = db. Select(strings.Join(fields, ", ")). Table(statementsTable). - Where("UNIX_TIMESTAMP(summary_begin_time) >= ? AND UNIX_TIMESTAMP(summary_end_time) <= ?", beginTime, endTime). + Where("summary_begin_time >= FROM_UNIXTIME(?) AND summary_end_time <= FROM_UNIXTIME(?)", beginTime, endTime). Where("schema_name = ?", schemaName). Where("digest = ?", digest). Group("plan_digest"). @@ -230,7 +231,7 @@ func QueryPlanDetail( query := db. Select(strings.Join(fields, ", ")). Table(statementsTable). - Where("UNIX_TIMESTAMP(summary_begin_time) >= ? AND UNIX_TIMESTAMP(summary_end_time) <= ?", beginTime, endTime). + Where("summary_begin_time >= FROM_UNIXTIME(?) AND summary_end_time <= FROM_UNIXTIME(?)", beginTime, endTime). Where("schema_name = ?", schemaName). Where("digest = ?", digest) if len(plans) > 0 { diff --git a/pkg/apiserver/statement/statement.go b/pkg/apiserver/statement/statement.go index 4a96a2dac6..f6e845eece 100644 --- a/pkg/apiserver/statement/statement.go +++ b/pkg/apiserver/statement/statement.go @@ -17,26 +17,30 @@ import ( "net/http" "github.com/gin-gonic/gin" + "go.uber.org/fx" "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 ServiceParams struct { + fx.In + TiDBClient *tidb.Client +} + type Service struct { - config *config.Config - tidbForwarder *tidb.Forwarder + params ServiceParams } -func NewService(config *config.Config, tidbForwarder *tidb.Forwarder) *Service { - return &Service{config: config, tidbForwarder: tidbForwarder} +func NewService(p ServiceParams) *Service { + return &Service{params: p} } func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/statements") endpoint.Use(auth.MWAuthRequired()) - endpoint.Use(utils.MWConnectTiDB(s.tidbForwarder)) + endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient)) endpoint.GET("/config", s.configHandler) endpoint.POST("/config", s.modifyConfigHandler) endpoint.GET("/time_ranges", s.timeRangesHandler) @@ -46,9 +50,7 @@ func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint.GET("/plan/detail", s.getPlanDetailHandler) } -// @Summary Statement configuration -// @Description Get configuration of statements -// @Produce json +// @Summary Get statement configurations // @Success 200 {object} statement.Config // @Router /statements/config [get] // @Security JwtAuth @@ -63,8 +65,7 @@ func (s *Service) configHandler(c *gin.Context) { c.JSON(http.StatusOK, cfg) } -// @Summary Statement configurationt -// @Description Modify configuration of statements +// @Summary Update statement configurations // @Param request body statement.Config true "Request body" // @Success 204 {object} string // @Router /statements/config [post] @@ -73,7 +74,7 @@ func (s *Service) configHandler(c *gin.Context) { func (s *Service) modifyConfigHandler(c *gin.Context) { var req Config if err := c.ShouldBindJSON(&req); err != nil { - _ = c.Error(err) + utils.MakeInvalidRequestErrorFromError(c, err) return } db := utils.GetTiDBConnection(c) @@ -85,9 +86,7 @@ func (s *Service) modifyConfigHandler(c *gin.Context) { c.Status(http.StatusNoContent) } -// @Summary Statement time ranges -// @Description Get all time ranges of the statements -// @Produce json +// @Summary Get available statement time ranges // @Success 200 {array} statement.TimeRange // @Router /statements/time_ranges [get] // @Security JwtAuth @@ -102,9 +101,7 @@ func (s *Service) timeRangesHandler(c *gin.Context) { c.JSON(http.StatusOK, timeRanges) } -// @Summary Statement types -// @Description Get all statement types -// @Produce json +// @Summary Get all statement types // @Success 200 {array} string // @Router /statements/stmt_types [get] // @Security JwtAuth @@ -127,9 +124,7 @@ type GetStatementsRequest struct { Text string `json:"text" form:"text"` } -// @Summary Statements overview -// @Description Get statements overview -// @Produce json +// @Summary Get a list of statement overviews // @Param q query GetStatementsRequest true "Query" // @Success 200 {array} Model // @Router /statements/overviews [get] @@ -138,8 +133,7 @@ type GetStatementsRequest struct { func (s *Service) overviewsHandler(c *gin.Context) { var req GetStatementsRequest if err := c.ShouldBindQuery(&req); err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + utils.MakeInvalidRequestErrorFromError(c, err) return } db := utils.GetTiDBConnection(c) @@ -158,9 +152,7 @@ type GetPlansRequest struct { EndTime int `json:"end_time" form:"end_time"` } -// @Summary Get statement plans -// @Description Get statement plans -// @Produce json +// @Summary Get execution plans of a statement // @Param q query GetPlansRequest true "Query" // @Success 200 {array} Model // @Router /statements/plans [get] @@ -169,8 +161,7 @@ type GetPlansRequest struct { func (s *Service) getPlansHandler(c *gin.Context) { var req GetPlansRequest if err := c.ShouldBindQuery(&req); err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + utils.MakeInvalidRequestErrorFromError(c, err) return } db := utils.GetTiDBConnection(c) @@ -187,9 +178,7 @@ type GetPlanDetailRequest struct { Plans []string `json:"plans" form:"plans"` } -// @Summary Get statement plan detail -// @Description Get statement plan detail -// @Produce json +// @Summary Get details of a statement in an execution plan // @Param q query GetPlanDetailRequest true "Query" // @Success 200 {object} Model // @Router /statements/plan/detail [get] @@ -198,8 +187,7 @@ type GetPlanDetailRequest struct { func (s *Service) getPlanDetailHandler(c *gin.Context) { var req GetPlanDetailRequest if err := c.ShouldBindQuery(&req); err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + utils.MakeInvalidRequestErrorFromError(c, err) return } db := utils.GetTiDBConnection(c) diff --git a/pkg/apiserver/user/auth.go b/pkg/apiserver/user/auth.go index 573604a638..64ca155d3f 100644 --- a/pkg/apiserver/user/auth.go +++ b/pkg/apiserver/user/auth.go @@ -37,16 +37,27 @@ var ( ErrNSSignIn = ErrNS.NewSubNamespace("signin") ErrSignInUnsupportedAuthType = ErrNSSignIn.NewType("unsupported_auth_type") ErrSignInOther = ErrNSSignIn.NewType("other") + ErrSignInInvalidCode = ErrNSSignIn.NewType("invalid_code") // Invalid or expired + ErrShareFailed = ErrNS.NewType("share_failed") ) type AuthService struct { middleware *jwt.GinJWTMiddleware + tidbClient *tidb.Client } +type AuthType int + +const ( + AuthTypeSQLUser AuthType = iota + AuthTypeSharingCode + // TODO: Add more auth type +) + type authenticateForm struct { - IsTiDBAuth bool `json:"is_tidb_auth" binding:"required"` - Username string `json:"username" binding:"required"` - Password string `json:"password"` + Type AuthType `json:"type" example:"0"` + Username string `json:"username" example:"root"` // Does not present for AuthTypeSharingCode + Password string `json:"password"` } type TokenResponse struct { @@ -54,34 +65,7 @@ type TokenResponse struct { Expire time.Time `json:"expire"` } -func (f *authenticateForm) Authenticate(tidbForwarder *tidb.Forwarder) (*utils.SessionUser, error) { - // TODO: Support non TiDB auth - if !f.IsTiDBAuth { - return nil, ErrSignInUnsupportedAuthType.New("unsupported auth type, only TiDB auth is supported") - } - db, err := tidbForwarder.OpenTiDB(f.Username, f.Password) - if err != nil { - if errorx.Cast(err) == nil { - return nil, ErrSignInOther.WrapWithNoMessage(err) - } - // Possible errors could be: - // tidb.ErrNoAliveTiDB - // tidb.ErrPDAccessFailed - // tidb.ErrTiDBConnFailed - // tidb.ErrTiDBAuthFailed - return nil, err - } - defer db.Close() //nolint:errcheck - - // TODO: Fill privilege tables here - return &utils.SessionUser{ - IsTiDBAuth: f.IsTiDBAuth, - TiDBUsername: f.Username, - TiDBPassword: f.Password, - }, nil -} - -func NewAuthService(tidbForwarder *tidb.Forwarder) *AuthService { +func NewAuthService(tidbClient *tidb.Client) *AuthService { var secret *[32]byte secretStr := os.Getenv("DASHBOARD_SESSION_SECRET") @@ -97,6 +81,11 @@ func NewAuthService(tidbForwarder *tidb.Forwarder) *AuthService { secret = cryptopasta.NewEncryptionKey() } + service := &AuthService{ + middleware: nil, + tidbClient: tidbClient, + } + middleware, err := jwt.New(&jwt.GinJWTMiddleware{ IdentityKey: utils.SessionUserKey, Realm: "dashboard", @@ -108,7 +97,7 @@ func NewAuthService(tidbForwarder *tidb.Forwarder) *AuthService { if err := c.ShouldBindJSON(&form); err != nil { return nil, utils.ErrInvalidRequest.WrapWithNoMessage(err) } - u, err := form.Authenticate(tidbForwarder) + u, err := service.authForm(&form) if err != nil { return nil, errorx.Decorate(err, "authenticate failed") } @@ -163,8 +152,7 @@ func NewAuthService(tidbForwarder *tidb.Forwarder) *AuthService { if user == nil { return false } - // Currently we don't support privileges, so only root user is allowed to sign in. - if user.TiDBUsername != "root" { + if user.IsShared && time.Now().After(user.SharedSessionExpireAt) { return false } return true @@ -200,12 +188,67 @@ func NewAuthService(tidbForwarder *tidb.Forwarder) *AuthService { log.Fatal("Failed to configure auth service", zap.Error(err)) } - return &AuthService{middleware: middleware} + service.middleware = middleware + + return service +} + +func (s *AuthService) authForm(f *authenticateForm) (*utils.SessionUser, error) { + switch f.Type { + case AuthTypeSQLUser: + return s.authSQLForm(f) + case AuthTypeSharingCode: + return s.authSharingCodeForm(f) + default: + return nil, ErrSignInUnsupportedAuthType.NewWithNoMessage() + } +} + +func (s *AuthService) authSQLForm(f *authenticateForm) (*utils.SessionUser, error) { + if f.Type != AuthTypeSQLUser { + panic("Expect AuthTypeSQLUser") + } + // Currently we don't support privileges, so only root user is allowed to sign in. + if f.Username != "root" { + return nil, ErrSignInOther.New("non root user is not allowed") + } + db, err := s.tidbClient.OpenSQLConn(f.Username, f.Password) + if err != nil { + if errorx.Cast(err) == nil { + return nil, ErrSignInOther.WrapWithNoMessage(err) + } + // Possible errors could be: + // tidb.ErrNoAliveTiDB + // tidb.ErrPDAccessFailed + // tidb.ErrTiDBConnFailed + // tidb.ErrTiDBAuthFailed + return nil, err + } + defer db.Close() //nolint:errcheck + + return &utils.SessionUser{ + HasTiDBAuth: true, + TiDBUsername: f.Username, + TiDBPassword: f.Password, + IsShared: false, + }, nil +} + +func (s *AuthService) authSharingCodeForm(f *authenticateForm) (*utils.SessionUser, error) { + if f.Type != AuthTypeSharingCode { + panic("Expect AuthTypeSharingCode") + } + session := utils.NewSessionFromSharingCode(f.Password) + if session == nil { + return nil, ErrSignInInvalidCode.NewWithNoMessage() + } + return session, nil } func Register(r *gin.RouterGroup, s *AuthService) { endpoint := r.Group("/user") endpoint.POST("/login", s.loginHandler) + endpoint.POST("/share", s.MWAuthRequired(), s.shareSessionHandler) } // MWAuthRequired creates a middleware that verifies the authentication token (JWT) in the request. If the token @@ -215,13 +258,55 @@ func (s *AuthService) MWAuthRequired() gin.HandlerFunc { return s.middleware.MiddlewareFunc() } +// @ID userLogin // @Summary Log in -// @Description Log into dashboard. -// @Accept json // @Param message body authenticateForm true "Credentials" // @Success 200 {object} TokenResponse -// @Failure 401 {object} utils.APIError "Login failure" +// @Failure 401 {object} utils.APIError // @Router /user/login [post] func (s *AuthService) loginHandler(c *gin.Context) { s.middleware.LoginHandler(c) } + +type ShareRequest struct { + ExpireInSeconds int64 `json:"expire_in_sec"` +} + +type ShareResponse struct { + Code string `json:"code"` +} + +// @ID userShareSession +// @Summary Share current session and generate a sharing code +// @Param request body ShareRequest true "Request body" +// @Security JwtAuth +// @Success 200 {object} ShareResponse +// @Router /user/share [post] +func (s *AuthService) shareSessionHandler(c *gin.Context) { + var req ShareRequest + if err := c.ShouldBindJSON(&req); err != nil { + utils.MakeInvalidRequestErrorFromError(c, err) + return + } + + expiry := time.Second * time.Duration(req.ExpireInSeconds) + + if expiry > utils.MaxSessionShareExpiry || expiry < 0 { + utils.MakeInvalidRequestErrorWithMessage(c, "Invalid share expiry") + return + } + + sessionUser := c.MustGet(utils.SessionUserKey).(*utils.SessionUser) + if sessionUser.IsShared { + utils.MakeInvalidRequestErrorWithMessage(c, "Shared session cannot be shared again") + return + } + + code := sessionUser.ToSharingCode(expiry) + if code == nil { + _ = c.Error(ErrShareFailed.NewWithNoMessage()) + return + } + + c.JSON(http.StatusOK, ShareResponse{Code: *code}) +} diff --git a/pkg/apiserver/utils/auth.go b/pkg/apiserver/utils/auth.go index 543edb1b07..d4cd8d7e78 100644 --- a/pkg/apiserver/utils/auth.go +++ b/pkg/apiserver/utils/auth.go @@ -14,29 +14,95 @@ package utils import ( - "net/http" + "encoding/hex" + "time" - "github.com/gin-gonic/gin" + "github.com/gtank/cryptopasta" + "github.com/vmihailenco/msgpack/v5" ) type SessionUser struct { - IsTiDBAuth bool + HasTiDBAuth bool TiDBUsername string TiDBPassword string + + // Whether this session is shared, i.e. built from another existing session. + // For security consideration, we do not allow shared session to be shared again + // since sharing can extend session lifetime. + IsShared bool `msgpack:"-"` + SharedSessionExpireAt time.Time `msgpack:"-"` + // TODO: Add privilege table fields } const ( // The key that attached the SessionUser in the gin Context. SessionUserKey = "user" + + // Max permitted lifetime of a shared session. + MaxSessionShareExpiry = time.Hour * 24 ) -func MakeUnauthorizedError(c *gin.Context) { - _ = c.Error(ErrUnauthorized.NewWithNoMessage()) - c.Status(http.StatusUnauthorized) +// The secret is always regenerated each time starting TiDB Dashboard. +var sharingCodeSecret = cryptopasta.NewEncryptionKey() + +type sharedSession struct { + Session *SessionUser + ExpireAt time.Time +} + +func (session *SessionUser) ToSharingCode(expireIn time.Duration) *string { + if session.IsShared { + return nil + } + if expireIn < 0 { + return nil + } + if expireIn > MaxSessionShareExpiry { + return nil + } + + shared := sharedSession{ + Session: session, + ExpireAt: time.Now().Add(expireIn), + } + + b, err := msgpack.Marshal(&shared) + if err != nil { + // Do not output anything about how serialization is failed to avoid potential leaks. + return nil + } + + encrypted, err := cryptopasta.Encrypt(b, sharingCodeSecret) + if err != nil { + return nil + } + + codeInHex := hex.EncodeToString(encrypted) + return &codeInHex } -func MakeInsufficientPrivilegeError(c *gin.Context) { - _ = c.Error(ErrInsufficientPrivilege.NewWithNoMessage()) - c.Status(http.StatusForbidden) +func NewSessionFromSharingCode(codeInHex string) *SessionUser { + encrypted, err := hex.DecodeString(codeInHex) + if err != nil { + return nil + } + + b, err := cryptopasta.Decrypt(encrypted, sharingCodeSecret) + if err != nil { + return nil + } + + var shared sharedSession + if err := msgpack.Unmarshal(b, &shared); err != nil { + return nil + } + + if time.Now().After(shared.ExpireAt) { + return nil + } + + shared.Session.IsShared = true + shared.Session.SharedSessionExpireAt = shared.ExpireAt + return shared.Session } diff --git a/pkg/apiserver/utils/error.go b/pkg/apiserver/utils/error.go index d927a8fe86..0c9ae4b018 100644 --- a/pkg/apiserver/utils/error.go +++ b/pkg/apiserver/utils/error.go @@ -22,13 +22,38 @@ import ( ) var ( - ErrNS = errorx.NewNamespace("error.api") - ErrOther = ErrNS.NewType("other") - ErrUnauthorized = ErrNS.NewType("unauthorized") - ErrInsufficientPrivilege = ErrNS.NewType("insufficient_privilege") - ErrInvalidRequest = ErrNS.NewType("invalid_request") + ErrNS = errorx.NewNamespace("error.api") + ErrOther = ErrNS.NewType("other") ) +var ErrUnauthorized = ErrNS.NewType("unauthorized") + +func MakeUnauthorizedError(c *gin.Context) { + _ = c.Error(ErrUnauthorized.NewWithNoMessage()) + c.Status(http.StatusUnauthorized) +} + +var ErrInsufficientPrivilege = ErrNS.NewType("insufficient_privilege") + +func MakeInsufficientPrivilegeError(c *gin.Context) { + _ = c.Error(ErrInsufficientPrivilege.NewWithNoMessage()) + c.Status(http.StatusForbidden) +} + +var ErrInvalidRequest = ErrNS.NewType("invalid_request") + +func MakeInvalidRequestErrorWithMessage(c *gin.Context, message string, args ...interface{}) { + _ = c.Error(ErrInvalidRequest.New(message, args...)) + c.Status(http.StatusBadRequest) +} + +func MakeInvalidRequestErrorFromError(c *gin.Context, err error) { + _ = c.Error(ErrInvalidRequest.WrapWithNoMessage(err)) + c.Status(http.StatusBadRequest) +} + +var ErrExpNotEnabled = ErrNS.NewType("experimental_feature_not_enabled") + type APIError struct { Error bool `json:"error"` Message string `json:"message"` @@ -36,6 +61,19 @@ type APIError struct { FullText string `json:"full_text"` } +func NewAPIError(err error) *APIError { + innerErr := errorx.Cast(err) + if innerErr == nil { + innerErr = ErrOther.WrapWithNoMessage(err) + } + return &APIError{ + Error: true, + Message: innerErr.Error(), + Code: errorx.GetTypeName(innerErr), + FullText: fmt.Sprintf("%+v", innerErr), + } +} + // MWHandleErrors creates a middleware that turns (last) error in the context into an APIError json response. // In handlers, `c.Error(err)` can be used to attach the error to the context. // When error is attached in the context: @@ -55,16 +93,6 @@ func MWHandleErrors() gin.HandlerFunc { statusCode = http.StatusInternalServerError } - innerErr := errorx.Cast(err.Err) - if innerErr == nil { - innerErr = ErrOther.WrapWithNoMessage(err.Err) - } - - c.AbortWithStatusJSON(statusCode, APIError{ - Error: true, - Message: innerErr.Error(), - Code: errorx.GetTypeName(innerErr), - FullText: fmt.Sprintf("%+v", innerErr), - }) + c.AbortWithStatusJSON(statusCode, NewAPIError(err.Err)) } } diff --git a/pkg/apiserver/utils/exp.go b/pkg/apiserver/utils/exp.go new file mode 100644 index 0000000000..647af856f7 --- /dev/null +++ b/pkg/apiserver/utils/exp.go @@ -0,0 +1,33 @@ +// 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 utils + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func MWForbidByExperimentalFlag(enableExp bool) gin.HandlerFunc { + return func(c *gin.Context) { + if !enableExp { + c.Status(http.StatusForbidden) + _ = c.Error(ErrExpNotEnabled.NewWithNoMessage()) + c.Abort() + return + } + + c.Next() + } +} diff --git a/pkg/apiserver/utils/jwt.go b/pkg/apiserver/utils/jwt.go index a3b86860dd..b84a908196 100644 --- a/pkg/apiserver/utils/jwt.go +++ b/pkg/apiserver/utils/jwt.go @@ -14,7 +14,7 @@ package utils import ( - "errors" + "fmt" "time" "github.com/dgrijalva/jwt-go" @@ -29,19 +29,23 @@ type Claims struct { jwt.StandardClaims } -func newClaims(issuer string, data string) *Claims { +func newClaims(issuer string, data string, expireIn time.Duration) *Claims { return &Claims{ Data: data, StandardClaims: jwt.StandardClaims{ - ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), + ExpiresAt: time.Now().Add(expireIn).Unix(), Issuer: issuer, }, } } -// NewJWTString create a JWT string by given data +// NewJWTString create a JWT string by given data, expire in 24 hours. func NewJWTString(issuer string, data string) (string, error) { - claims := newClaims(issuer, data) + return NewJWTStringWithExpire(issuer, data, 24*time.Hour) +} + +func NewJWTStringWithExpire(issuer string, data string, expireIn time.Duration) (string, error) { + claims := newClaims(issuer, data, expireIn) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(hmacSampleSecret[:]) if err != nil { @@ -60,10 +64,10 @@ func ParseJWTString(requiredIssuer string, tokenStr string) (string, error) { return "", err } if !token.Valid { - return "", errors.New("token is invalid") + return "", fmt.Errorf("token is invalid") } if claims.Issuer != requiredIssuer { - return "", errors.New("invalid issuer") + return "", fmt.Errorf("invalid issuer") } return claims.Data, nil } diff --git a/pkg/apiserver/utils/tidb_conn.go b/pkg/apiserver/utils/tidb_conn.go index 25d2b98013..c6c3f1c905 100644 --- a/pkg/apiserver/utils/tidb_conn.go +++ b/pkg/apiserver/utils/tidb_conn.go @@ -33,14 +33,14 @@ const ( // and errors will be generated. // // This middleware must be placed after the `MWAuthRequired()` middleware, otherwise it will panic. -func MWConnectTiDB(tidbForwarder *tidb.Forwarder) gin.HandlerFunc { +func MWConnectTiDB(tidbClient *tidb.Client) gin.HandlerFunc { return func(c *gin.Context) { sessionUser := c.MustGet(SessionUserKey).(*SessionUser) if sessionUser == nil { panic("invalid sessionUser") } - if !sessionUser.IsTiDBAuth { + if !sessionUser.HasTiDBAuth { // Only TiDBAuth is able to access. Raise error in this case. // The error is privilege error instead of authorization error so that user will not be redirected. MakeInsufficientPrivilegeError(c) @@ -48,7 +48,7 @@ func MWConnectTiDB(tidbForwarder *tidb.Forwarder) gin.HandlerFunc { return } - db, err := tidbForwarder.OpenTiDB(sessionUser.TiDBUsername, sessionUser.TiDBPassword) + db, err := tidbClient.OpenSQLConn(sessionUser.TiDBUsername, sessionUser.TiDBPassword) if err != nil { if errorx.IsOfType(err, tidb.ErrTiDBAuthFailed) { diff --git a/pkg/apiserver/utils/zip.go b/pkg/apiserver/utils/zip.go new file mode 100644 index 0000000000..db881511f4 --- /dev/null +++ b/pkg/apiserver/utils/zip.go @@ -0,0 +1,66 @@ +// 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 utils + +import ( + "archive/zip" + "io" + "os" +) + +func StreamZipPack(w io.Writer, files []string, needCompress bool) error { + pack := zip.NewWriter(w) + defer pack.Close() + + for _, file := range files { + err := streamZipFile(pack, file, needCompress) + if err != nil { + return err + } + } + + return nil +} + +func streamZipFile(zipPack *zip.Writer, file string, needCompress bool) error { + f, err := os.Open(file) + if err != nil { + return err + } + defer f.Close() + + fileInfo, err := f.Stat() + if err != nil { + return err + } + + zipMethod := zip.Store // no compress + if needCompress { + zipMethod = zip.Deflate // compress + } + zipFile, err := zipPack.CreateHeader(&zip.FileHeader{ + Name: fileInfo.Name(), + Method: zipMethod, + }) + if err != nil { + return err + } + + _, err = io.Copy(zipFile, f) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 41482ff968..69616a18db 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -15,13 +15,12 @@ package config import ( "crypto/tls" - "fmt" "net/url" "strings" ) const ( - DefaultPublicPathPrefix = "/dashboard" + defaultPublicPathPrefix = "/dashboard" UIPathPrefix = "/dashboard/" APIPathPrefix = "/dashboard/api/" @@ -33,19 +32,35 @@ type Config struct { PDEndPoint string PublicPathPrefix string - // TLS config for mTLS authentication between TiDB components. - ClusterTLSConfig *tls.Config + ClusterTLSConfig *tls.Config // TLS config for mTLS authentication between TiDB components. + TiDBTLSConfig *tls.Config // TLS config for mTLS authentication between TiDB and MySQL client. - // TLS config for mTLS authentication between TiDB and MySQL client. - TiDBTLSConfig *tls.Config + EnableTelemetry bool + EnableExperimental bool +} - // Enable client to report data for analysis - EnableTelemetry bool +func Default() *Config { + return &Config{ + DataDir: "/tmp/dashboard-data", + PDEndPoint: "http://127.0.0.1:2379", + PublicPathPrefix: defaultPublicPathPrefix, + ClusterTLSConfig: nil, + TiDBTLSConfig: nil, + EnableTelemetry: true, + EnableExperimental: false, + } +} + +func (c *Config) GetClusterHTTPScheme() string { + if c.ClusterTLSConfig != nil { + return "https" + } + return "http" } func (c *Config) NormalizePDEndPoint() error { - if !strings.HasPrefix(c.PDEndPoint, "http") { - c.PDEndPoint = fmt.Sprintf("http://%s", c.PDEndPoint) + if !strings.HasPrefix(c.PDEndPoint, "http://") && !strings.HasPrefix(c.PDEndPoint, "https://") { + c.PDEndPoint = "http://" + c.PDEndPoint } pdEndPoint, err := url.Parse(c.PDEndPoint) @@ -53,18 +68,14 @@ func (c *Config) NormalizePDEndPoint() error { return err } - pdEndPoint.Scheme = "http" - if c.ClusterTLSConfig != nil { - pdEndPoint.Scheme = "https" - } - + pdEndPoint.Scheme = c.GetClusterHTTPScheme() c.PDEndPoint = pdEndPoint.String() return nil } func (c *Config) NormalizePublicPathPrefix() { if c.PublicPathPrefix == "" { - c.PublicPathPrefix = DefaultPublicPathPrefix + c.PublicPathPrefix = defaultPublicPathPrefix } c.PublicPathPrefix = strings.TrimRight(c.PublicPathPrefix, "/") } diff --git a/pkg/http/http.go b/pkg/http/http.go deleted file mode 100644 index b47d52937d..0000000000 --- a/pkg/http/http.go +++ /dev/null @@ -1,52 +0,0 @@ -// 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 http - -import ( - "context" - "crypto/tls" - "net" - "net/http" - "time" - - "go.uber.org/fx" - - "github.com/pingcap-incubator/tidb-dashboard/pkg/config" -) - -const ( - Timeout = time.Second * 10 -) - -func NewHTTPClientWithConf(lc fx.Lifecycle, config *config.Config) *http.Client { - cli := &http.Client{ - Transport: &http.Transport{ - DialTLS: func(network, addr string) (net.Conn, error) { - conn, err := tls.Dial(network, addr, config.ClusterTLSConfig) - return conn, err - }, - TLSClientConfig: config.ClusterTLSConfig, - }, - Timeout: Timeout, - } - - lc.Append(fx.Hook{ - OnStop: func(context.Context) error { - cli.CloseIdleConnections() - return nil - }, - }) - - return cli -} diff --git a/pkg/httpc/client.go b/pkg/httpc/client.go new file mode 100644 index 0000000000..d30b4b330a --- /dev/null +++ b/pkg/httpc/client.go @@ -0,0 +1,108 @@ +// 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 httpc + +import ( + "context" + "crypto/tls" + "io" + "io/ioutil" + "net" + "net/http" + "time" + + "github.com/joomcode/errorx" + "github.com/pingcap/log" + "go.uber.org/fx" + "go.uber.org/zap" + + "github.com/pingcap-incubator/tidb-dashboard/pkg/config" +) + +const ( + defaultTimeout = time.Second * 10 +) + +type Client struct { + http.Client +} + +func NewHTTPClient(lc fx.Lifecycle, config *config.Config) *Client { + cli := http.Client{ + Transport: &http.Transport{ + DialTLS: func(network, addr string) (net.Conn, error) { + conn, err := tls.Dial(network, addr, config.ClusterTLSConfig) + return conn, err + }, + TLSClientConfig: config.ClusterTLSConfig, + }, + Timeout: defaultTimeout, + } + + lc.Append(fx.Hook{ + OnStop: func(context.Context) error { + cli.CloseIdleConnections() + return nil + }, + }) + + return &Client{ + Client: cli, + } +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c2 := *c + c2.Timeout = timeout + return &c2 +} + +// TODO: Replace using go-resty +func (c *Client) SendRequest( + ctx context.Context, + uri string, + method string, + body io.Reader, + errType *errorx.Type, + errOriginComponent string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, method, uri, body) + if err != nil { + e := errType.Wrap(err, "Failed to build %s API request", errOriginComponent) + log.Warn("SendRequest failed", zap.String("uri", uri), zap.Error(e)) + return nil, e + } + + resp, err := c.Do(req) + if err != nil { + e := errType.Wrap(err, "Failed to send %s API request", errOriginComponent) + log.Warn("SendRequest failed", zap.String("uri", uri), zap.Error(err)) + return nil, e + } + defer resp.Body.Close() + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + e := errType.Wrap(err, "Failed to read %s API response", errOriginComponent) + log.Warn("SendRequest failed", zap.String("uri", uri), zap.Error(err)) + return nil, e + } + + if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { + e := errType.New("Request failed with status code %d from %s API: %s", resp.StatusCode, errOriginComponent, string(data)) + log.Warn("SendRequest failed", zap.String("uri", uri), zap.Error(err)) + return nil, e + } + + return data, nil +} diff --git a/pkg/keyvisual/decorator/tidb.go b/pkg/keyvisual/decorator/tidb.go index d54cfc4a69..7a5f31b505 100644 --- a/pkg/keyvisual/decorator/tidb.go +++ b/pkg/keyvisual/decorator/tidb.go @@ -41,17 +41,17 @@ type tidbLabelStrategy struct { EtcdClient *clientv3.Client TableMap sync.Map - forwarder *tidb.Forwarder + tidbClient *tidb.Client SchemaVersion int64 TidbAddress []string } // TiDBLabelStrategy implements the LabelStrategy interface. Get Label Information from TiDB. -func TiDBLabelStrategy(lc fx.Lifecycle, wg *sync.WaitGroup, cfg *config.Config, etcdClient *clientv3.Client, forwarder *tidb.Forwarder) LabelStrategy { +func TiDBLabelStrategy(lc fx.Lifecycle, wg *sync.WaitGroup, cfg *config.Config, etcdClient *clientv3.Client, tidbClient *tidb.Client) LabelStrategy { s := &tidbLabelStrategy{ Config: cfg, EtcdClient: etcdClient, - forwarder: forwarder, + tidbClient: tidbClient, SchemaVersion: -1, } diff --git a/pkg/keyvisual/decorator/tidb_requests.go b/pkg/keyvisual/decorator/tidb_requests.go index c39a9451ed..20a3e3f1a6 100644 --- a/pkg/keyvisual/decorator/tidb_requests.go +++ b/pkg/keyvisual/decorator/tidb_requests.go @@ -111,7 +111,7 @@ func (s *tidbLabelStrategy) updateMap(ctx context.Context) { } func (s *tidbLabelStrategy) request(path string, v interface{}) error { - data, err := s.forwarder.SendGetRequest(path) + data, err := s.tidbClient.SendGetRequest(path) if err != nil { return err } diff --git a/pkg/keyvisual/manager.go b/pkg/keyvisual/manager.go index 7a88b87d85..0784619225 100644 --- a/pkg/keyvisual/manager.go +++ b/pkg/keyvisual/manager.go @@ -98,7 +98,6 @@ func (s *Service) stopService() { } // @Summary Get Key Visual Dynamic Config -// @Produce json // @Success 200 {object} config.KeyVisualConfig // @Router /keyvisual/config [get] // @Security JwtAuth @@ -114,7 +113,6 @@ func (s *Service) getDynamicConfig(c *gin.Context) { } // @Summary Set Key Visual Dynamic Config -// @Produce json // @Param request body config.KeyVisualConfig true "Request body" // @Success 200 {object} config.KeyVisualConfig // @Router /keyvisual/config [put] @@ -125,16 +123,14 @@ func (s *Service) getDynamicConfig(c *gin.Context) { func (s *Service) setDynamicConfig(c *gin.Context) { var req config.KeyVisualConfig if err := c.ShouldBindJSON(&req); err != nil { - c.Status(http.StatusBadRequest) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + utils.MakeInvalidRequestErrorFromError(c, err) return } var opt config.DynamicConfigOption = func(dc *config.DynamicConfig) { dc.KeyVisual = req } if err := s.cfgManager.Modify(opt); err != nil { - c.Status(http.StatusInternalServerError) - _ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err)) + _ = c.Error(err) return } c.JSON(http.StatusOK, req) diff --git a/pkg/keyvisual/service.go b/pkg/keyvisual/service.go index de2684df31..ca8005bda0 100644 --- a/pkg/keyvisual/service.go +++ b/pkg/keyvisual/service.go @@ -79,13 +79,14 @@ type Service struct { etcdClient *clientv3.Client pdClient *pd.Client db *dbstore.DB - forwarder *tidb.Forwarder + tidbClient *tidb.Client stat *storage.Stat strategy matrix.Strategy labelStrategy decorator.LabelStrategy } +// FIXME: Simplify these things func NewService( lc fx.Lifecycle, cfg *config.Config, @@ -94,7 +95,7 @@ func NewService( etcdClient *clientv3.Client, pdClient *pd.Client, db *dbstore.DB, - forwarder *tidb.Forwarder, + tidbClient *tidb.Client, ) *Service { s := &Service{ status: utils.NewServiceStatus(), @@ -104,7 +105,7 @@ func NewService( etcdClient: etcdClient, pdClient: pdClient, db: db, - forwarder: forwarder, + tidbClient: tidbClient, } lc.Append(s.managerHook()) @@ -165,12 +166,12 @@ func (s *Service) newLabelStrategy( wg *sync.WaitGroup, cfg *config.Config, etcdClient *clientv3.Client, - forwarder *tidb.Forwarder, + tidbClient *tidb.Client, ) decorator.LabelStrategy { switch s.keyVisualCfg.Policy { case config.KeyVisualDBPolicy: log.Debug("New LabelStrategy", zap.String("policy", s.keyVisualCfg.Policy)) - return decorator.TiDBLabelStrategy(lc, wg, cfg, etcdClient, forwarder) + return decorator.TiDBLabelStrategy(lc, wg, cfg, etcdClient, tidbClient) case config.KeyVisualKVPolicy: log.Debug("New LabelStrategy", zap.String("policy", s.keyVisualCfg.Policy), zap.String("separator", s.keyVisualCfg.PolicyKVSeparator)) @@ -229,7 +230,6 @@ func (s *Service) Stop(ctx context.Context) error { // @Summary Key Visual Heatmaps // @Description Heatmaps in a given range to visualize TiKV usage -// @Produce json // @Param startkey query string false "The start of the key range" // @Param endkey query string false "The end of the key range" // @Param starttime query int false "The start of the time range (Unix)" @@ -303,8 +303,8 @@ func (s *Service) heatmaps(c *gin.Context) { c.JSON(http.StatusOK, resp) } -func (s *Service) provideLocals() (*config.Config, *clientv3.Client, *pd.Client, *dbstore.DB, *tidb.Forwarder) { - return s.config, s.etcdClient, s.pdClient, s.db, s.forwarder +func (s *Service) provideLocals() (*config.Config, *clientv3.Client, *pd.Client, *dbstore.DB, *tidb.Client) { + return s.config, s.etcdClient, s.pdClient, s.db, s.tidbClient } func newWaitGroup(lc fx.Lifecycle) *sync.WaitGroup { diff --git a/pkg/pd/client.go b/pkg/pd/client.go new file mode 100644 index 0000000000..9986f26425 --- /dev/null +++ b/pkg/pd/client.go @@ -0,0 +1,75 @@ +// 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 pd + +import ( + "context" + "fmt" + "io" + "time" + + "go.uber.org/fx" + + "github.com/pingcap-incubator/tidb-dashboard/pkg/config" + "github.com/pingcap-incubator/tidb-dashboard/pkg/httpc" +) + +var ( + ErrPDClientRequestFailed = ErrNS.NewType("client_request_failed") +) + +const ( + defaultPDTimeout = time.Second * 10 +) + +type Client struct { + baseURL string + httpClient *httpc.Client + lifecycleCtx context.Context + timeout time.Duration +} + +func NewPDClient(lc fx.Lifecycle, httpClient *httpc.Client, config *config.Config) *Client { + client := &Client{ + httpClient: httpClient, + baseURL: config.PDEndPoint, + lifecycleCtx: nil, + timeout: defaultPDTimeout, + } + + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + client.lifecycleCtx = ctx + return nil + }, + }) + + return client +} + +func (c *Client) WithBaseURL(baseURL string) *Client { + c2 := *c + c2.baseURL = baseURL + return &c2 +} + +func (c *Client) SendGetRequest(path string) ([]byte, error) { + uri := fmt.Sprintf("%s/pd/api/v1%s", c.baseURL, path) + return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, "GET", nil, ErrPDClientRequestFailed, "PD") +} + +func (c *Client) SendPostRequest(path string, body io.Reader) ([]byte, error) { + uri := fmt.Sprintf("%s/pd/api/v1%s", c.baseURL, path) + return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, "POST", body, ErrPDClientRequestFailed, "PD") +} diff --git a/pkg/pd/etcd.go b/pkg/pd/etcd.go index 275b142343..2a59d19e15 100644 --- a/pkg/pd/etcd.go +++ b/pkg/pd/etcd.go @@ -27,10 +27,6 @@ import ( "github.com/pingcap-incubator/tidb-dashboard/pkg/utils" ) -const ( - TiDBServerInformationPath = "/tidb/server/info" -) - func newZapEncoder(zapcore.EncoderConfig) (zapcore.Encoder, error) { logCfg := log.Config{ DisableTimestamp: false, diff --git a/pkg/pd/http_client.go b/pkg/pd/http_client.go deleted file mode 100644 index 832172c57e..0000000000 --- a/pkg/pd/http_client.go +++ /dev/null @@ -1,75 +0,0 @@ -// 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 pd - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - - "go.uber.org/fx" - - "github.com/pingcap-incubator/tidb-dashboard/pkg/config" -) - -var ( - ErrPDClientRequestFailed = ErrNS.NewType("client_request_failed") -) - -type Client struct { - address string - httpClient *http.Client - lifecycleCtx context.Context -} - -func NewPDClient(lc fx.Lifecycle, httpClient *http.Client, config *config.Config) *Client { - client := &Client{ - httpClient: httpClient, - address: config.PDEndPoint, - } - - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - client.lifecycleCtx = ctx - return nil - }, - }) - - return client -} - -func (pd *Client) SendGetRequest(path string) ([]byte, error) { - uri := fmt.Sprintf("%s/pd/api/v1%s", pd.address, path) - req, err := http.NewRequestWithContext(pd.lifecycleCtx, "GET", uri, nil) - if err != nil { - return nil, ErrPDClientRequestFailed.Wrap(err, "failed to build request for PD API %s", path) - } - - resp, err := pd.httpClient.Do(req) - if err != nil { - return nil, ErrPDClientRequestFailed.Wrap(err, "failed to send request to PD API %s", path) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, ErrPDClientRequestFailed.New("received non success status code %d from PD API %s", resp.StatusCode, path) - } - - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, ErrPDClientRequestFailed.Wrap(err, "failed to read response from PD API %s", path) - } - - return data, nil -} diff --git a/pkg/tidb/client.go b/pkg/tidb/client.go new file mode 100644 index 0000000000..f481ec3d7f --- /dev/null +++ b/pkg/tidb/client.go @@ -0,0 +1,160 @@ +package tidb + +import ( + "context" + "database/sql/driver" + "fmt" + "net" + "os" + "strings" + "time" + + "github.com/VividCortex/mysqlerr" + "github.com/go-sql-driver/mysql" + "github.com/jinzhu/gorm" + "github.com/pingcap/log" + "go.etcd.io/etcd/clientv3" + "go.uber.org/fx" + "go.uber.org/zap" + + // MySQL driver used by gorm + _ "github.com/jinzhu/gorm/dialects/mysql" + + "github.com/pingcap-incubator/tidb-dashboard/pkg/config" + "github.com/pingcap-incubator/tidb-dashboard/pkg/httpc" +) + +var ( + ErrTiDBConnFailed = ErrNS.NewType("tidb_conn_failed") + ErrTiDBAuthFailed = ErrNS.NewType("tidb_auth_failed") + ErrTiDBClientRequestFailed = ErrNS.NewType("client_request_failed") +) + +const ( + defaultTiDBStatusAPITimeout = time.Second * 10 + + // When this environment variable is set, SQL requests will be always sent to this specific TiDB instance. + // Calling `WithSQLAPIAddress` to enforce a SQL request endpoint will fail when opening the connection. + tidbOverrideSQLEndpointEnvVar = "TIDB_OVERRIDE_ENDPOINT" + // When this environment variable is set, status requests will be always sent to this specific TiDB instance. + // Calling `WithStatusAPIAddress` to enforce a status API request endpoint will fail when opening the connection. + tidbOverrideStatusEndpointEnvVar = "TIDB_OVERRIDE_STATUS_ENDPOINT" +) + +type Client struct { + lifecycleCtx context.Context + forwarder *Forwarder + statusAPIHTTPScheme string + statusAPIAddress string // Empty means to use address provided by forwarder + statusAPIHTTPClient *httpc.Client + statusAPITimeout time.Duration + sqlAPITLSKey string // Non empty means use this key as MySQL TLS config + sqlAPIAddress string // Empty means to use address provided by forwarder +} + +func NewTiDBClient(lc fx.Lifecycle, config *config.Config, etcdClient *clientv3.Client, httpClient *httpc.Client) *Client { + sqlAPITLSKey := "" + if config.TiDBTLSConfig != nil { + sqlAPITLSKey = "tidb" + _ = mysql.RegisterTLSConfig(sqlAPITLSKey, config.TiDBTLSConfig) + } + + client := &Client{ + lifecycleCtx: nil, + forwarder: newForwarder(lc, etcdClient), + statusAPIHTTPScheme: config.GetClusterHTTPScheme(), + statusAPIAddress: "", + statusAPIHTTPClient: httpClient, + statusAPITimeout: defaultTiDBStatusAPITimeout, + sqlAPITLSKey: sqlAPITLSKey, + sqlAPIAddress: "", + } + + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + client.lifecycleCtx = ctx + return nil + }, + }) + + return client +} + +func (c *Client) WithStatusAPIAddress(host string, statusPort int) *Client { + c2 := *c + c2.statusAPIAddress = fmt.Sprintf("%s:%d", host, statusPort) + return &c2 +} + +func (c *Client) WithSQLAPIAddress(host string, sqlPort int) *Client { + c2 := *c + c2.sqlAPIAddress = fmt.Sprintf("%s:%d", host, sqlPort) + return &c2 +} + +func (c *Client) OpenSQLConn(user string, pass string) (*gorm.DB, error) { + overrideEndpoint := os.Getenv(tidbOverrideSQLEndpointEnvVar) + if overrideEndpoint != "" && c.sqlAPIAddress != "" { + log.Warn(fmt.Sprintf("Reject to establish a target specified TiDB SQL connection since `%s` is set", tidbOverrideSQLEndpointEnvVar)) + return nil, ErrTiDBConnFailed.New("TiDB Dashboard is configured to only connect to specified TiDB host") + } + + addr := c.sqlAPIAddress + if addr == "" { + if overrideEndpoint != "" { + addr = overrideEndpoint + } else { + addr = fmt.Sprintf("127.0.0.1:%d", c.forwarder.sqlPort) + } + } + + dsnConfig := mysql.NewConfig() + dsnConfig.Net = "tcp" + dsnConfig.Addr = addr + dsnConfig.User = user + dsnConfig.Passwd = pass + dsnConfig.Timeout = time.Second + dsnConfig.ParseTime = true + dsnConfig.Loc = time.Local + dsnConfig.MultiStatements = true + dsnConfig.TLSConfig = c.sqlAPITLSKey + dsn := dsnConfig.FormatDSN() + + db, err := gorm.Open("mysql", dsn) + if err != nil { + if _, ok := err.(*net.OpError); ok || err == driver.ErrBadConn { + if strings.HasPrefix(addr, "0.0.0.0:") { + log.Warn("TiDB reported its address to be 0.0.0.0. Please specify `-advertise-address` command line parameter when running TiDB") + } + return nil, ErrTiDBConnFailed.Wrap(err, "failed to connect to TiDB") + } else if mysqlErr, ok := err.(*mysql.MySQLError); ok { + if mysqlErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR { + return nil, ErrTiDBAuthFailed.New("bad TiDB username or password") + } + } + log.Warn("Unknown error occurred while opening TiDB connection", zap.Error(err)) + return nil, err + } + + return db, nil +} + +func (c *Client) SendGetRequest(path string) ([]byte, error) { + overrideEndpoint := os.Getenv(tidbOverrideStatusEndpointEnvVar) + if overrideEndpoint != "" && c.statusAPIAddress != "" { + log.Warn(fmt.Sprintf("Reject to establish a target specified TiDB status connection since `%s` is set", tidbOverrideStatusEndpointEnvVar)) + return nil, ErrTiDBConnFailed.New("TiDB Dashboard is configured to only connect to specified TiDB host") + } + + addr := c.statusAPIAddress + if addr == "" { + if overrideEndpoint != "" { + addr = overrideEndpoint + } else { + addr = fmt.Sprintf("127.0.0.1:%d", c.forwarder.statusPort) + } + } + + uri := fmt.Sprintf("%s://%s%s", c.statusAPIHTTPScheme, addr, path) + return c.statusAPIHTTPClient.WithTimeout(c.statusAPITimeout).SendRequest(c.lifecycleCtx, uri, "GET", nil, ErrTiDBClientRequestFailed, "TiDB") +} diff --git a/pkg/tidb/conn.go b/pkg/tidb/conn.go deleted file mode 100644 index d357fe49c2..0000000000 --- a/pkg/tidb/conn.go +++ /dev/null @@ -1,105 +0,0 @@ -// 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 tidb - -import ( - "database/sql/driver" - "fmt" - "io/ioutil" - "net" - "net/http" - "os" - "strings" - "time" - - "github.com/go-sql-driver/mysql" - "github.com/jinzhu/gorm" - "github.com/pingcap/log" - "go.uber.org/zap" - - // MySQL driver used by gorm - _ "github.com/jinzhu/gorm/dialects/mysql" -) - -const ( - envTidbOverrideEndpointKey = "TIDB_OVERRIDE_ENDPOINT" -) - -var ( - ErrTiDBConnFailed = ErrNS.NewType("tidb_conn_failed") - ErrTiDBAuthFailed = ErrNS.NewType("tidb_auth_failed") - ErrTiDBClientRequestFailed = ErrNS.NewType("client_request_failed") -) - -func (f *Forwarder) OpenTiDB(user string, pass string) (*gorm.DB, error) { - var addr string - addr = os.Getenv(envTidbOverrideEndpointKey) - if len(addr) < 1 { - addr = fmt.Sprintf("127.0.0.1:%d", f.tidbPort) - } - dsnConfig := mysql.NewConfig() - dsnConfig.Net = "tcp" - dsnConfig.Addr = addr - dsnConfig.User = user - dsnConfig.Passwd = pass - dsnConfig.Timeout = time.Second - dsnConfig.ParseTime = true - dsnConfig.Loc = time.Local - if f.config.TiDBTLSConfig != nil { - dsnConfig.TLSConfig = "tidb" - } - dsn := dsnConfig.FormatDSN() - - db, err := gorm.Open("mysql", dsn) - if err != nil { - if _, ok := err.(*net.OpError); ok || err == driver.ErrBadConn { - if strings.HasPrefix(addr, "0.0.0.0:") { - log.Warn("The IP reported by TiDB is 0.0.0.0, which may not have the -advertise-address option") - } - return nil, ErrTiDBConnFailed.Wrap(err, "failed to connect to TiDB") - } else if mysqlErr, ok := err.(*mysql.MySQLError); ok { - if mysqlErr.Number == 1045 { - return nil, ErrTiDBAuthFailed.New("bad TiDB username or password") - } - } - log.Warn("unknown error occurred while OpenTiDB", zap.Error(err)) - return nil, err - } - - return db, nil -} - -func (f *Forwarder) SendGetRequest(path string) ([]byte, error) { - uri := fmt.Sprintf("%s://127.0.0.1:%d%s", f.uriScheme, f.statusPort, path) - req, err := http.NewRequestWithContext(f.lifecycleCtx, "GET", uri, nil) - if err != nil { - return nil, ErrTiDBClientRequestFailed.Wrap(err, "failed to build request for TiDB API %s", path) - } - - resp, err := f.httpClient.Do(req) - if err != nil { - return nil, ErrTiDBClientRequestFailed.Wrap(err, "failed to send request to TiDB API %s", path) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, ErrTiDBClientRequestFailed.New("received non success status code %d from TiDB API %s", resp.StatusCode, path) - } - - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, ErrTiDBClientRequestFailed.Wrap(err, "failed to read response from TiDB API %s", path) - } - - return data, nil -} diff --git a/pkg/tidb/forwarder.go b/pkg/tidb/forwarder.go index acbe5cf12d..b6345cbe45 100644 --- a/pkg/tidb/forwarder.go +++ b/pkg/tidb/forwarder.go @@ -15,81 +15,58 @@ package tidb import ( "context" - "crypto/tls" "fmt" "net" - "net/http" "time" "github.com/cenkalti/backoff/v4" - "github.com/go-sql-driver/mysql" "github.com/joomcode/errorx" "go.etcd.io/etcd/clientv3" "go.uber.org/fx" - "github.com/pingcap-incubator/tidb-dashboard/pkg/config" "github.com/pingcap-incubator/tidb-dashboard/pkg/utils/topology" ) var ( - ErrPDAccessFailed = ErrNS.NewType("pd_access_failed") - ErrNoAliveTiDB = ErrNS.NewType("no_alive_tidb") + ErrNoAliveTiDB = ErrNS.NewType("no_alive_tidb") ) -type ForwarderConfig struct { - ClusterTLSConfig *tls.Config - TiDBTLSConfig *tls.Config +type forwarderConfig struct { TiDBRetrieveTimeout time.Duration TiDBPollInterval time.Duration ProxyTimeout time.Duration ProxyCheckInterval time.Duration } -func NewForwarderConfig(cfg *config.Config) *ForwarderConfig { - if cfg.TiDBTLSConfig != nil { - _ = mysql.RegisterTLSConfig("tidb", cfg.TiDBTLSConfig) - } - return &ForwarderConfig{ - ClusterTLSConfig: cfg.ClusterTLSConfig, - TiDBTLSConfig: cfg.TiDBTLSConfig, - TiDBRetrieveTimeout: time.Second, - TiDBPollInterval: 5 * time.Second, - ProxyTimeout: 3 * time.Second, - ProxyCheckInterval: 2 * time.Second, - } -} - type Forwarder struct { lifecycleCtx context.Context - config *ForwarderConfig + config *forwarderConfig etcdClient *clientv3.Client - httpClient *http.Client - uriScheme string - tidbProxy *proxy - tidbStatusProxy *proxy - tidbPort int - statusPort int + sqlProxy *proxy + sqlPort int + statusProxy *proxy + statusPort int } func (f *Forwarder) Start(ctx context.Context) error { f.lifecycleCtx = ctx var err error - if f.tidbProxy, err = f.createProxy(); err != nil { + if f.sqlProxy, err = f.createProxy(); err != nil { return err } - if f.tidbStatusProxy, err = f.createProxy(); err != nil { + if f.statusProxy, err = f.createProxy(); err != nil { return err } - f.tidbPort = f.tidbProxy.port() - f.statusPort = f.tidbStatusProxy.port() + f.sqlPort = f.sqlProxy.port() + f.statusPort = f.statusProxy.port() go f.pollingForTiDB() - go f.tidbProxy.run(ctx) - go f.tidbStatusProxy.run(ctx) + go f.sqlProxy.run(ctx) + go f.statusProxy.run(ctx) return nil } @@ -117,8 +94,8 @@ func (f *Forwarder) pollingForTiDB() { }, bo) if err != nil { if errorx.IsOfType(err, ErrNoAliveTiDB) { - f.tidbProxy.updateRemotes(nil) - f.tidbStatusProxy.updateRemotes(nil) + f.sqlProxy.updateRemotes(nil) + f.statusProxy.updateRemotes(nil) } } else { statusEndpoints := make(map[string]struct{}, len(allTiDB)) @@ -127,8 +104,8 @@ func (f *Forwarder) pollingForTiDB() { tidbEndpoints[fmt.Sprintf("%s:%d", server.IP, server.Port)] = struct{}{} statusEndpoints[fmt.Sprintf("%s:%d", server.IP, server.StatusPort)] = struct{}{} } - f.tidbProxy.updateRemotes(tidbEndpoints) - f.tidbStatusProxy.updateRemotes(statusEndpoints) + f.sqlProxy.updateRemotes(tidbEndpoints) + f.statusProxy.updateRemotes(statusEndpoints) } select { @@ -139,22 +116,18 @@ func (f *Forwarder) pollingForTiDB() { } } -func NewForwarder(lc fx.Lifecycle, config *ForwarderConfig, etcdClient *clientv3.Client, httpClient *http.Client) *Forwarder { +func newForwarder(lc fx.Lifecycle, etcdClient *clientv3.Client) *Forwarder { f := &Forwarder{ - config: config, + config: &forwarderConfig{ + TiDBRetrieveTimeout: time.Second, + TiDBPollInterval: 5 * time.Second, + ProxyTimeout: 3 * time.Second, + ProxyCheckInterval: 2 * time.Second, + }, etcdClient: etcdClient, - httpClient: httpClient, } - - if config.ClusterTLSConfig == nil { - f.uriScheme = "http" - } else { - f.uriScheme = "https" - } - lc.Append(fx.Hook{ OnStart: f.Start, }) - return f } diff --git a/pkg/tikv/client.go b/pkg/tikv/client.go new file mode 100644 index 0000000000..145bd34ea0 --- /dev/null +++ b/pkg/tikv/client.go @@ -0,0 +1,75 @@ +// 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 tikv + +import ( + "context" + "fmt" + "io" + "time" + + "go.uber.org/fx" + + "github.com/pingcap-incubator/tidb-dashboard/pkg/config" + "github.com/pingcap-incubator/tidb-dashboard/pkg/httpc" +) + +var ( + ErrTiKVClientRequestFailed = ErrNS.NewType("client_request_failed") +) + +const ( + defaultTiKVStatusAPITimeout = time.Second * 10 +) + +type Client struct { + httpClient *httpc.Client + httpScheme string + lifecycleCtx context.Context + timeout time.Duration +} + +func NewTiKVClient(lc fx.Lifecycle, httpClient *httpc.Client, config *config.Config) *Client { + client := &Client{ + httpClient: httpClient, + httpScheme: config.GetClusterHTTPScheme(), + lifecycleCtx: nil, + timeout: defaultTiKVStatusAPITimeout, + } + + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + client.lifecycleCtx = ctx + return nil + }, + }) + + return client +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c2 := *c + c2.timeout = timeout + return &c2 +} + +func (c *Client) SendGetRequest(host string, statusPort int, path string) ([]byte, error) { + uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, path) + return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, "GET", nil, ErrTiKVClientRequestFailed, "TiKV") +} + +func (c *Client) SendPostRequest(host string, statusPort int, path string, body io.Reader) ([]byte, error) { + uri := fmt.Sprintf("%s://%s:%d%s", c.httpScheme, host, statusPort, path) + return c.httpClient.WithTimeout(c.timeout).SendRequest(c.lifecycleCtx, uri, "POST", body, ErrTiKVClientRequestFailed, "TiKV") +} diff --git a/pkg/tikv/tikv.go b/pkg/tikv/tikv.go new file mode 100644 index 0000000000..aa9fc58873 --- /dev/null +++ b/pkg/tikv/tikv.go @@ -0,0 +1,22 @@ +// 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 tikv + +import ( + "github.com/joomcode/errorx" +) + +var ( + ErrNS = errorx.NewNamespace("error.tikv") +) diff --git a/pkg/utils/topology/models.go b/pkg/utils/topology/models.go index 8a9104c88c..52c857dd36 100644 --- a/pkg/utils/topology/models.go +++ b/pkg/utils/topology/models.go @@ -57,6 +57,16 @@ type StoreInfo struct { StartTimestamp int64 `json:"start_timestamp"` } +type StoreLabels struct { + Address string `json:"address"` + Labels map[string]string `json:"labels"` +} + +type StoreLocation struct { + LocationLabels []string `json:"location_labels"` + Stores []StoreLabels `json:"stores"` +} + type StandardComponentInfo struct { IP string `json:"ip"` Port uint `json:"port"` diff --git a/pkg/utils/topology/pd.go b/pkg/utils/topology/pd.go index 4840ab5817..eadf6e8777 100644 --- a/pkg/utils/topology/pd.go +++ b/pkg/utils/topology/pd.go @@ -16,6 +16,7 @@ package topology import ( "encoding/json" "sort" + "strings" "github.com/pingcap/log" "go.uber.org/zap" @@ -135,3 +136,20 @@ func fetchPDHealth(pdClient *pd.Client) (map[uint64]struct{}, error) { } return memberHealth, nil } + +func fetchLocationLabels(pdClient *pd.Client) ([]string, error) { + data, err := pdClient.SendGetRequest("/config/replicate") + if err != nil { + return nil, err + } + + var replicateConfig struct { + LocationLabels string `json:"location-labels"` + } + err = json.Unmarshal(data, &replicateConfig) + if err != nil { + return nil, ErrInvalidTopologyData.Wrap(err, "PD config/replicate API unmarshal failed") + } + labels := strings.Split(replicateConfig.LocationLabels, ",") + return labels, nil +} diff --git a/pkg/utils/topology/store.go b/pkg/utils/topology/store.go index 06b766691a..1750fe3a63 100644 --- a/pkg/utils/topology/store.go +++ b/pkg/utils/topology/store.go @@ -50,6 +50,37 @@ func FetchStoreTopology(pdClient *pd.Client) ([]StoreInfo, []StoreInfo, error) { return buildStoreTopology(tiKVStores), buildStoreTopology(tiFlashStores), nil } +func FetchStoreLocation(pdClient *pd.Client) (*StoreLocation, error) { + locationLabels, err := fetchLocationLabels(pdClient) + if err != nil { + return nil, err + } + + stores, err := fetchStores(pdClient) + if err != nil { + return nil, err + } + + nodes := make([]StoreLabels, 0, len(stores)) + for _, s := range stores { + node := StoreLabels{ + Address: s.Address, + Labels: map[string]string{}, + } + for _, l := range s.Labels { + node.Labels[l.Key] = l.Value + } + nodes = append(nodes, node) + } + + storeLocation := StoreLocation{ + LocationLabels: locationLabels, + Stores: nodes, + } + + return &storeLocation, nil +} + func buildStoreTopology(stores []store) []StoreInfo { nodes := make([]StoreInfo, 0, len(stores)) for _, v := range stores { @@ -81,7 +112,7 @@ func buildStoreTopology(stores []store) []StoreInfo { StartTimestamp: v.StartTimestamp, } for _, v := range v.Labels { - node.Labels[v.Key] = node.Labels[v.Value] + node.Labels[v.Key] = v.Value } nodes = append(nodes, node) } @@ -95,7 +126,7 @@ type store struct { Labels []struct { Key string `json:"key"` Value string `json:"value"` - } + } `json:"labels"` StateName string `json:"state_name"` Version string `json:"version"` StatusAddress string `json:"status_address"` diff --git a/release-version b/release-version index c7c8ea342a..970227a28d 100644 --- a/release-version +++ b/release-version @@ -1,3 +1,3 @@ # This file specifies the TiDB Dashboard internal version, which will be printed in `--version` # and UI. In release branch, changing this file will result in publishing a new version and tag. -2020.08.07.1 +2020.09.08.1 diff --git a/ui/.storybook/preview.js b/ui/.storybook/preview.js index 96fff89e36..467451de28 100644 --- a/ui/.storybook/preview.js +++ b/ui/.storybook/preview.js @@ -10,10 +10,10 @@ function StoryRoot({ children }) { apiClient.init() client .getInstance() - .userLoginPost({ + .userLogin({ username: 'root', password: '', - is_tidb_auth: true, + type: 0, }) .then((r) => auth.setAuthToken(r.data.token)) }, []) diff --git a/ui/dashboardApp/index.ts b/ui/dashboardApp/index.ts index a4e2351b5a..251697dff9 100644 --- a/ui/dashboardApp/index.ts +++ b/ui/dashboardApp/index.ts @@ -3,6 +3,8 @@ import '@lib/utils/wdyr' import * as singleSpa from 'single-spa' import i18next from 'i18next' import { Modal } from 'antd' +import NProgress from 'nprogress' +import './nprogress.less' import AppRegistry from '@lib/utils/registry' import * as routing from '@lib/utils/routing' @@ -13,19 +15,20 @@ import { saveAppOptions, loadAppOptions } from '@lib/utils/appOptions' import * as telemetry from '@lib/utils/telemetry' import client, { InfoInfoResponse } from '@lib/client' -import LayoutRoot from '@dashboard/layout/root' import LayoutMain from '@dashboard/layout/main' import LayoutSignIn from '@dashboard/layout/signin' import AppUserProfile from '@lib/apps/UserProfile/index.meta' import AppOverview from '@lib/apps/Overview/index.meta' +import AppClusterInfo from '@lib/apps/ClusterInfo/index.meta' import AppKeyViz from '@lib/apps/KeyViz/index.meta' import AppStatement from '@lib/apps/Statement/index.meta' +import AppSlowQuery from '@lib/apps/SlowQuery/index.meta' import AppDiagnose from '@lib/apps/Diagnose/index.meta' import AppSearchLogs from '@lib/apps/SearchLogs/index.meta' import AppInstanceProfiling from '@lib/apps/InstanceProfiling/index.meta' -import AppClusterInfo from '@lib/apps/ClusterInfo/index.meta' -import AppSlowQuery from '@lib/apps/SlowQuery/index.meta' +import AppQueryEditor from '@lib/apps/QueryEditor/index.meta' +import AppConfiguration from '@lib/apps/Configuration/index.meta' function removeSpinner() { const spinner = document.getElementById('dashboard_page_spinner') @@ -48,7 +51,7 @@ async function main() { let info: InfoInfoResponse try { - const i = await client.getInstance().getInfo() + const i = await client.getInstance().infoGet() info = i.data } catch (e) { Modal.error({ @@ -65,16 +68,19 @@ async function main() { const registry = new AppRegistry(options) - singleSpa.registerApplication( - 'root', - AppRegistry.newReactSpaApp(() => LayoutRoot, 'root'), - () => true, - { registry } - ) + NProgress.configure({ + showSpinner: false, + }) + window.addEventListener('single-spa:before-routing-event', () => { + NProgress.set(0.2) + }) + window.addEventListener('single-spa:routing-event', () => { + NProgress.done(true) + }) singleSpa.registerApplication( 'layout', - AppRegistry.newReactSpaApp(() => LayoutMain, '__spa__main__'), + AppRegistry.newReactSpaApp(() => LayoutMain, 'root'), () => { return !routing.isSignInPage() }, @@ -83,7 +89,7 @@ async function main() { singleSpa.registerApplication( 'signin', - AppRegistry.newReactSpaApp(() => LayoutSignIn, '__spa__main__'), + AppRegistry.newReactSpaApp(() => LayoutSignIn, 'root'), () => { return routing.isSignInPage() }, @@ -93,13 +99,15 @@ async function main() { registry .register(AppUserProfile) .register(AppOverview) + .register(AppClusterInfo) .register(AppKeyViz) .register(AppStatement) - .register(AppClusterInfo) + .register(AppSlowQuery) .register(AppDiagnose) .register(AppSearchLogs) .register(AppInstanceProfiling) - .register(AppSlowQuery) + .register(AppQueryEditor) + .register(AppConfiguration) if (routing.isLocationMatch('/')) { singleSpa.navigateToUrl('#' + registry.getDefaultRouter()) diff --git a/ui/dashboardApp/layout/main/Sider/Banner.module.less b/ui/dashboardApp/layout/main/Sider/Banner.module.less index 8bb9acc325..766dad10e4 100644 --- a/ui/dashboardApp/layout/main/Sider/Banner.module.less +++ b/ui/dashboardApp/layout/main/Sider/Banner.module.less @@ -15,10 +15,6 @@ padding: 20px 16px 20px 24px; } -.bannerLeftAnimationWrapper { - overflow: hidden; -} - .bannerRight { position: absolute; top: 0; diff --git a/ui/dashboardApp/layout/main/Sider/Banner.tsx b/ui/dashboardApp/layout/main/Sider/Banner.tsx index 22a2a7b777..ed9e21ad34 100644 --- a/ui/dashboardApp/layout/main/Sider/Banner.tsx +++ b/ui/dashboardApp/layout/main/Sider/Banner.tsx @@ -56,7 +56,7 @@ export default function ToggleBanner({ }) const { data, isLoading } = useClientRequest((cancelToken) => - client.getInstance().getInfo({ cancelToken }) + client.getInstance().infoGet({ cancelToken }) ) const version = useMemo(() => { diff --git a/ui/dashboardApp/layout/main/Sider/index.tsx b/ui/dashboardApp/layout/main/Sider/index.tsx index c93f36dc1c..5a3679e2a6 100644 --- a/ui/dashboardApp/layout/main/Sider/index.tsx +++ b/ui/dashboardApp/layout/main/Sider/index.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect } from 'react' -import { ExperimentOutlined } from '@ant-design/icons' +import React, { useState, useEffect, useMemo } from 'react' +import { ExperimentOutlined, BugOutlined } from '@ant-design/icons' import { Layout, Menu } from 'antd' import { Link } from 'react-router-dom' import { useEventListener } from '@umijs/hooks' @@ -9,6 +9,7 @@ import client, { InfoWhoAmIResponse } from '@lib/client' import Banner from './Banner' import styles from './index.module.less' +import { useClientRequest } from '@lib/utils/useClientRequest' function useAppMenuItem(registry, appId, title?: string) { const { t } = useTranslation() @@ -41,7 +42,7 @@ function useCurrentLogin() { const [login, setLogin] = useState(null) useEffect(() => { async function fetch() { - const resp = await client.getInstance().infoWhoamiGet() + const resp = await client.getInstance().infoWhoami() if (resp.data) { setLogin(resp.data) } @@ -64,13 +65,17 @@ function Sider({ const activeAppId = useActiveAppId(registry) const currentLogin = useCurrentLogin() + const { data } = useClientRequest((cancelToken) => + client.getInstance().infoGet({ cancelToken }) + ) + const debugSubMenuItems = [useAppMenuItem(registry, 'instance_profiling')] const debugSubMenu = ( - + {t('nav.sider.debug')} } @@ -79,6 +84,24 @@ function Sider({ ) + const experimentalSubMenuItems = [ + useAppMenuItem(registry, 'query_editor'), + useAppMenuItem(registry, 'configuration'), + ] + const experimentalSubMenu = ( + + + {t('nav.sider.experimental')} + + } + > + {experimentalSubMenuItems} + + ) + const menuItems = [ useAppMenuItem(registry, 'overview'), useAppMenuItem(registry, 'cluster_info'), @@ -90,19 +113,32 @@ function Sider({ debugSubMenu, ] + if (data?.enable_experimental) { + menuItems.push(experimentalSubMenu) + } + + let displayName = currentLogin?.username ?? '...' + if (currentLogin?.is_shared) { + displayName += ' (Shared)' + } + const extraMenuItems = [ useAppMenuItem(registry, 'dashboard_settings'), - useAppMenuItem( - registry, - 'user_profile', - currentLogin ? currentLogin.username : '...' - ), + useAppMenuItem(registry, 'user_profile', displayName), ] const transSider = useSpring({ width: collapsed ? collapsedWidth : fullWidth, }) + const defaultOpenKeys = useMemo(() => { + if (defaultCollapsed) { + return [] + } else { + return ['debug', 'experimental'] + } + }, [defaultCollapsed]) + return ( {menuItems} diff --git a/ui/dashboardApp/layout/root/index.tsx b/ui/dashboardApp/layout/root/index.tsx deleted file mode 100644 index 49fe281a4e..0000000000 --- a/ui/dashboardApp/layout/root/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react' -import { HashRouter as Router } from 'react-router-dom' - -import { Root, TopLoadingBar } from '@lib/components' - -function App() { - return ( - - - -
-
-
- ) -} - -export default App diff --git a/ui/dashboardApp/layout/signin/index.module.less b/ui/dashboardApp/layout/signin/index.module.less index 9b354f7178..a58bd5b744 100644 --- a/ui/dashboardApp/layout/signin/index.module.less +++ b/ui/dashboardApp/layout/signin/index.module.less @@ -1,4 +1,7 @@ -@dialog-width: 400px; +@import '~antd/es/style/themes/default.less'; +@import '~antd/es/button/style/mixin.less'; + +@content-width: 400px; .container { position: fixed; @@ -11,10 +14,15 @@ align-items: stretch; } -.dialogContainer { - min-width: @dialog-width; +.contantContainer { + min-width: @content-width; width: 38%; background: #fff; + position: relative; +} + +.dialogContainer { + height: 100%; display: flex; align-items: center; justify-content: center; @@ -51,9 +59,110 @@ font-size: 0.8rem; margin: 15px 0; a { - color: #888; + color: @gray-7; &:hover { - color: #666; + color: @gray-7; + } + } + + &.clickable { + a:hover { + color: @link-hover-color; + } + } +} + +.alternativeFormLayer { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: #fff; + z-index: 2; +} + +.alternativeCloseButton { + font-size: 1rem; + border: 0; + background: #fff; + color: @text-color; + cursor: pointer; + padding: @padding-xss; + padding-right: 0; + + &:hover, + &:active, + &:focus { + color: @link-hover-color; + outline: 0; + } +} + +.alternativeButton { + .btn; + .btn-default; + + height: auto; + width: 100%; + color: @text-color; + + margin-bottom: @padding-sm; + text-align: left; + padding: @padding-md; + padding-right: 50px + @padding-md; + position: relative; + word-wrap: normal; + white-space: normal; + line-height: 1; + + &:hover, + &:active, + &:focus { + color: @text-color; + } + + .title { + color: @heading-color; + margin-bottom: @padding-sm; + } + + .icon { + font-size: 1.3rem; + position: absolute; + right: 0; + width: 50px; + text-align: center; + opacity: 0; + transform: translateX(-5px); + transition: opacity 0.2s linear, transform 0.2s linear; + color: @gray-6; + + // For vertical align + top: 50%; + margin-top: -20px; + line-height: 40px; + } + + &:hover, + &:active, + &:focus { + .icon { + opacity: 1; + transform: none; } } } + +.container :global { + .formAnimation { + animation: 0.2s @ease-out-circ 0.5s antZoomBigIn; + animation-fill-mode: both; + animation-iteration-count: 1; + } + .landingAnimation { + animation: 0.5s linear 0 antFadeIn; + animation-fill-mode: both; + animation-iteration-count: 1; + } +} diff --git a/ui/dashboardApp/layout/signin/index.tsx b/ui/dashboardApp/layout/signin/index.tsx index 46478df810..d5cf833698 100644 --- a/ui/dashboardApp/layout/signin/index.tsx +++ b/ui/dashboardApp/layout/signin/index.tsx @@ -1,54 +1,159 @@ +import CSSMotion from 'rc-animate/es/CSSMotion' +import cx from 'classnames' import * as singleSpa from 'single-spa' -import { Root } from '@lib/components' -import React, { useState, useEffect, useRef } from 'react' +import { Root, AppearAnimate } from '@lib/components' +import React, { useState, useRef, useCallback, useMemo } from 'react' import { DownOutlined, GlobalOutlined, LockOutlined, UserOutlined, + KeyOutlined, + ArrowRightOutlined, + CloseOutlined, } from '@ant-design/icons' -import { Form, Input, Button, message } from 'antd' -import { motion } from 'framer-motion' +import { Form, Input, Button, message, Typography } from 'antd' import { useTranslation } from 'react-i18next' import LanguageDropdown from '@lib/components/LanguageDropdown' -import client from '@lib/client' +import client, { UserAuthenticateForm } from '@lib/client' import * as auth from '@lib/utils/auth' +import { useMount } from 'react-use' +import Flexbox from '@g07cha/flexbox-react' +import { usePersistFn } from '@umijs/hooks' import { ReactComponent as Logo } from './logo.svg' import styles from './index.module.less' -const AnimationItem = (props) => { +enum DisplayFormType { + tidbCredential, + shareCode, +} + +function AlternativeAuthLink({ onClick }) { + const { t } = useTranslation() + return ( +
+ + {t('signin.form.use_alternative')} + +
+ ) +} + +function LanguageDrop() { + return ( +
+ + + Switch Language + + +
+ ) +} + +interface IAlternativeFormButtonProps + extends React.ButtonHTMLAttributes { + title: string + description: string + className?: string +} + +function AlternativeFormButton({ + title, + description, + className, + ...restProps +}: IAlternativeFormButtonProps) { return ( - - {props.children} - + + ) +} + +function AlternativeAuthForm({ + className, + onClose, + onSwitchForm, + ...restProps +}) { + const { t } = useTranslation() + + return ( +
+
+
+
+ +

+ +
{t('signin.form.alternative.title')}
+ +
+

+
+ + onSwitchForm(DisplayFormType.tidbCredential)} + /> + + + onSwitchForm(DisplayFormType.shareCode)} + /> + + + +
+
+
) } -function TiDBSignInForm({ registry }) { +function useSignInSubmit( + successRoute, + fnLoginForm: (form) => UserAuthenticateForm, + onFailure: () => void +) { const { t } = useTranslation() const [loading, setLoading] = useState(false) - const [signInError, setSignInError] = useState(null) + const [error, setError] = useState(null) - const [refForm] = Form.useForm() - const refPassword = useRef(null) + const clearErrorMsg = useCallback(() => { + setError(null) + }, []) - const signIn = async (form) => { + const handleSubmit = usePersistFn(async (form) => { setLoading(true) - clearErrorMessages() + clearErrorMsg() try { - const r = await client.getInstance().userLoginPost({ - username: form.username, - password: form.password, - is_tidb_auth: true, - }) + const r = await client.getInstance().userLogin(fnLoginForm(form)) auth.setAuthToken(r.data.token) message.success(t('signin.message.success')) - singleSpa.navigateToUrl('#' + registry.getDefaultRouter()) + singleSpa.navigateToUrl(successRoute) } catch (e) { console.log(e) if (!e.handled) { @@ -58,89 +163,78 @@ function TiDBSignInForm({ registry }) { } else { msg = e.message } - setSignInError(t('signin.message.error', { msg })) - refForm.setFieldsValue({ password: '' }) - setTimeout(() => { - // Focus after disable state is removed - refPassword?.current?.focus() - }, 0) + setError(t('signin.message.error', { msg })) + onFailure() } } setLoading(false) - } + }) - const handleSubmit = (values) => { - signIn(values) - } + return { handleSubmit, loading, errorMsg: error, clearErrorMsg } +} - const clearErrorMessages = () => { - setSignInError(null) - } +function TiDBSignInForm({ successRoute, onClickAlternative }) { + const { t } = useTranslation() + + const [refForm] = Form.useForm() + const refPassword = useRef(null) + + const { handleSubmit, loading, errorMsg, clearErrorMsg } = useSignInSubmit( + successRoute, + (form) => ({ + username: form.username, + password: form.password, + type: 0, + }), + () => { + refForm.setFieldsValue({ password: '' }) + setTimeout(() => { + refPassword.current?.focus() + }, 0) + } + ) - useEffect(() => { + useMount(() => { refPassword?.current?.focus() - }, []) + }) return ( -
- - +
+
+ - -

{t('signin.form.tidb_auth.title')}

-
- - } - disabled - /> + } disabled /> - - } + prefix={} type="password" disabled={loading} - onInput={clearErrorMessages} + onInput={clearErrorMsg} ref={refPassword} /> - -
+
+ ) +} + +function CodeSignInForm({ successRoute, onClickAlternative }) { + const { t } = useTranslation() + + const [refForm] = Form.useForm() + const refPassword = useRef(null) + + const { handleSubmit, loading, errorMsg, clearErrorMsg } = useSignInSubmit( + successRoute, + (form) => ({ + password: form.code, + type: 1, + }), + () => { + refForm.setFieldsValue({ code: '' }) + setTimeout(() => { + refPassword.current?.focus() + }, 0) + } + ) + + useMount(() => { + refPassword?.current?.focus() + }) + + return ( +
+
+
+ + +

{t('signin.form.code_auth.title')}

+
+ + } + type="password" + onInput={clearErrorMsg} + disabled={loading} + ref={refPassword} + allowClear + /> + + + + + + + +
+
) } function App({ registry }) { + const successRoute = useMemo(() => `#${registry.getDefaultRouter()}`, [ + registry, + ]) + const [alternativeVisible, setAlternativeVisible] = useState(false) + const [formType, setFormType] = useState(DisplayFormType.tidbCredential) + + const handleClickAlternative = useCallback(() => { + setAlternativeVisible(true) + }, []) + + const handleAlternativeClose = useCallback(() => { + setAlternativeVisible(false) + }, []) + + const handleSwitchForm = useCallback((k: DisplayFormType) => { + setFormType(k) + setAlternativeVisible(false) + }, []) + return (
-
-
- -
-
- + + {({ style, className }) => ( + + )} + + {formType === DisplayFormType.tidbCredential && ( + + )} + {formType === DisplayFormType.shareCode && ( + + )} + + + motionName="landingAnimation" + />
) diff --git a/ui/dashboardApp/layout/translations/en.yaml b/ui/dashboardApp/layout/translations/en.yaml index fbb8c1d14b..b1b766195e 100644 --- a/ui/dashboardApp/layout/translations/en.yaml +++ b/ui/dashboardApp/layout/translations/en.yaml @@ -8,6 +8,9 @@ error: tidb_conn_failed: Failed to connect to TiDB tidb_auth_failed: TiDB authentication failed api: + user: + signin: + invalid_code: Authorization Code is invalid or expired other: Other error signin: message: @@ -19,10 +22,21 @@ signin: button: Sign In tidb_auth: title: SQL User Sign In - check: - message: Username is required + switch: + title: SQL User + description: I know the username and password to connect to the database + code_auth: + title: Authorization Code Sign In + switch: + title: Authorization Code + description: I was invited by others with an authorization code + code: Code + use_alternative: Use Alternative Authentication + alternative: + title: Select Authentication nav: user: signout: Sign Out sider: debug: Advanced Debugging + experimental: Experimental Features diff --git a/ui/dashboardApp/layout/translations/zh.yaml b/ui/dashboardApp/layout/translations/zh.yaml index 42af71fdb8..21254f306d 100644 --- a/ui/dashboardApp/layout/translations/zh.yaml +++ b/ui/dashboardApp/layout/translations/zh.yaml @@ -8,6 +8,9 @@ error: tidb_conn_failed: 无法连接到 TiDB tidb_auth_failed: TiDB 登录验证失败 api: + user: + signin: + invalid_code: 授权码无效或已过期 other: 其他错误 signin: message: @@ -19,10 +22,22 @@ signin: button: 登录 tidb_auth: title: SQL 用户登录 - check: - message: 请输入用户名 + switch: + title: SQL 用户 + description: 我知道数据库的登录用户名和密码 + code_auth: + title: 授权码登录 + switch: + title: 授权码 + description: 其他人通过授权码邀请我使用 + code: 授权码 + use_alternative: 使用其他登录方式 + alternative: + title: 选择登录方式 + nav: user: signout: 登出 sider: debug: 高级调试 + experimental: 实验性功能 diff --git a/ui/dashboardApp/nprogress.less b/ui/dashboardApp/nprogress.less new file mode 100644 index 0000000000..2cc90d5caf --- /dev/null +++ b/ui/dashboardApp/nprogress.less @@ -0,0 +1,69 @@ +@progress-color: #ffc53d; + +#nprogress { + pointer-events: none; +} + +#nprogress .bar { + background: @progress-color; + + position: fixed; + z-index: 1031; + top: 0; + left: 0; + + width: 100%; + height: 2px; +} + +/* Fancy blur effect */ +#nprogress .peg { + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px @progress-color, 0 0 5px @progress-color; + opacity: 1; + transform: rotate(3deg) translate(0px, -4px); +} + +/* Remove these to get rid of the spinner */ +#nprogress .spinner { + display: block; + position: fixed; + z-index: 1031; + top: 15px; + right: 15px; +} + +#nprogress .spinner-icon { + width: 18px; + height: 18px; + box-sizing: border-box; + + border: solid 2px transparent; + border-top-color: @progress-color; + border-left-color: @progress-color; + border-radius: 50%; + animation: nprogress-spinner 400ms linear infinite; +} + +.nprogress-custom-parent { + overflow: hidden; + position: relative; +} + +.nprogress-custom-parent #nprogress .spinner, +.nprogress-custom-parent #nprogress .bar { + position: absolute; +} + +@keyframes nprogress-spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/ui/lib/apps/ClusterInfo/components/StoreLocation.tsx b/ui/lib/apps/ClusterInfo/components/StoreLocation.tsx new file mode 100644 index 0000000000..32a3fba94e --- /dev/null +++ b/ui/lib/apps/ClusterInfo/components/StoreLocation.tsx @@ -0,0 +1,60 @@ +import React, { useMemo } from 'react' +import { useClientRequest } from '@lib/utils/useClientRequest' +import client, { TopologyStoreLocation } from '@lib/client' +import { ErrorBar, AnimatedSkeleton } from '@lib/components' +import StoreLocationTree from './StoreLocationTree' + +type TreeNode = { + name: string + value: string + children: TreeNode[] +} + +function buildTreeData(data: TopologyStoreLocation | undefined): TreeNode { + let treeData: TreeNode = { name: 'Stores', value: '', children: [] } + if ((data?.location_labels?.length || 0) > 0) { + const locationLabels: string[] = data?.location_labels || [] + + for (const store of data?.stores || []) { + // reset curNode, point to tree nodes beginning + let curNode = treeData + for (const curLabel of locationLabels) { + const curLabelVal = store.labels![curLabel] + if (curLabelVal === undefined) { + continue + } + let subNode: TreeNode | undefined = curNode.children.find( + (el) => el.name === curLabel && el.value === curLabelVal + ) + if (subNode === undefined) { + subNode = { name: curLabel, value: curLabelVal, children: [] } + curNode.children.push(subNode) + } + // make curNode point to subNode + curNode = subNode + } + curNode.children.push({ + name: store.address!, + value: '', + children: [], + }) + } + } + return treeData +} + +export default function StoreLocation() { + const { data, isLoading, error } = useClientRequest((cancelToken) => + client.getInstance().getStoreLocationTopology({ cancelToken }) + ) + const treeData = useMemo(() => buildTreeData(data), [data]) + + return ( +
+ + + + +
+ ) +} diff --git a/ui/lib/apps/ClusterInfo/components/StoreLocationTree/index.stories.tsx b/ui/lib/apps/ClusterInfo/components/StoreLocationTree/index.stories.tsx new file mode 100644 index 0000000000..9997f80e92 --- /dev/null +++ b/ui/lib/apps/ClusterInfo/components/StoreLocationTree/index.stories.tsx @@ -0,0 +1,136 @@ +import React from 'react' +import StoreLocationTree from '.' + +export default { + title: 'StoreLocationTree', +} + +const dataSource1 = { + name: 'labels', + children: [ + { + name: 'sh', + children: [ + { + name: 'r1', + children: [ + { + name: 'h1', + children: [ + { + name: '127.0.0.1:20160', + children: [], + }, + ], + }, + { + name: 'h2', + children: [ + { + name: '127.0.0.1:20161', + children: [], + }, + ], + }, + ], + }, + ], + }, + { + name: 'bj', + children: [ + { + name: 'r1', + children: [ + { + name: 'h1', + children: [ + { + name: '127.0.0.1:20162', + children: [], + }, + ], + }, + ], + }, + { + name: '127.0.0.1:3930', + children: [], + }, + ], + }, + ], +} + +export const onlyName = () => + +const dataSource2 = { + name: 'labels', + value: '', + children: [ + { + name: 'zone', + value: 'sh', + children: [ + { + name: 'rack', + value: 'r1', + children: [ + { + name: 'host', + value: 'h1', + children: [ + { + name: '127.0.0.1:20160', + value: '', + children: [], + }, + ], + }, + { + name: 'host', + value: 'h2', + children: [ + { + name: '127.0.0.1:20162', + value: '', + children: [], + }, + ], + }, + ], + }, + ], + }, + { + name: 'zone', + value: 'bj', + children: [ + { + name: 'rack', + value: 'r1', + children: [ + { + name: 'host', + value: 'h1', + children: [ + { + name: '127.0.0.1:20161', + value: '', + children: [], + }, + ], + }, + ], + }, + { + name: '127.0.0.1:3930', + value: '', + children: [], + }, + ], + }, + ], +} + +export const nameAndValue = () => diff --git a/ui/lib/apps/ClusterInfo/components/StoreLocationTree/index.tsx b/ui/lib/apps/ClusterInfo/components/StoreLocationTree/index.tsx new file mode 100644 index 0000000000..996a9578ca --- /dev/null +++ b/ui/lib/apps/ClusterInfo/components/StoreLocationTree/index.tsx @@ -0,0 +1,180 @@ +import React, { useRef, useEffect } from 'react' +import * as d3 from 'd3' + +export interface IStoreLocationProps { + dataSource: any +} + +const margin = { top: 40, right: 120, bottom: 10, left: 80 } +const width = 954 +const dx = 40 +const dy = width / 6 + +const tree = d3.tree().nodeSize([dx, dy]) + +const diagonal = d3 + .linkHorizontal() + .x((d: any) => d.y) + .y((d: any) => d.x) + +export default function StoreLocationTree({ dataSource }: IStoreLocationProps) { + const ref = useRef(null) + + useEffect(() => { + const root = d3.hierarchy(dataSource) as any + root.x0 = dy / 2 + root.y0 = 0 + root.descendants().forEach((d, i) => { + d.id = i + d._children = d.children + // collapse all nodes default + // if (d.depth) d.children = null + }) + + const svg = d3.select(ref.current) + svg.selectAll('g').remove() + svg + .attr('viewBox', [-margin.left, -margin.top, width, dx] as any) + .style('font', '16px sans-serif') + .style('user-select', 'none') + + const gLink = svg + .append('g') + .attr('fill', 'none') + .attr('stroke', '#555') + .attr('stroke-opacity', 0.4) + .attr('stroke-width', 2) + + const gNode = svg + .append('g') + .attr('cursor', 'pointer') + .attr('pointer-events', 'all') + + function update(source) { + const duration = d3.event && d3.event.altKey ? 2500 : 250 + const nodes = root.descendants().reverse() + const links = root.links() + + // compute the new tree layout + // it modifies root self + tree(root) + + let left = root + let right = root + root.eachBefore((node) => { + if (node.x < left.x) left = node + if (node.x > right.x) right = node + }) + + const height = right.x - left.x + margin.top + margin.bottom + + const transition = svg + .transition() + .duration(duration) + .attr('viewBox', [ + -margin.left, + left.x - margin.top, + width, + height, + ] as any) + .tween('resize', () => () => svg.dispatch('toggle')) + + // update the nodes + const node = gNode.selectAll('g').data(nodes, (d: any) => d.id) + + // enter any new nodes at the parent's previous position + const nodeEnter = node + .enter() + .append('g') + .attr('transform', (_d) => `translate(${source.y0},${source.x0})`) + .attr('fill-opacity', 0) + .attr('stroke-opacity', 0) + .on('click', (d: any) => { + d.children = d.children ? null : d._children + update(d) + }) + + nodeEnter + .append('circle') + .attr('r', 6) + .attr('fill', (d: any) => (d._children ? '#ff4d4f' : '#3351ff')) + .attr('stroke-width', 10) + + nodeEnter + .append('text') + .attr('dy', '0.31em') + .attr('x', (d: any) => (d._children ? -8 : 8)) + .attr('text-anchor', (d: any) => (d._children ? 'end' : 'start')) + .text(({ data: { name, value } }: any) => { + if (value) { + return `${name}: ${value}` + } + return name + }) + .clone(true) + .lower() + .attr('stroke-linejoin', 'round') + .attr('stroke-width', 3) + .attr('stroke', 'white') + + // transition nodes to their new position + node + .merge(nodeEnter as any) + .transition(transition as any) + .attr('transform', (d: any) => `translate(${d.y},${d.x})`) + .attr('fill-opacity', 1) + .attr('stroke-opacity', 1) + + // transition exiting nodes to the parent's new position + node + .exit() + .transition(transition as any) + .remove() + .attr('transform', (d) => `translate(${source.y},${source.x})`) + .attr('fill-opacity', 0) + .attr('stroke-opacity', 0) + + // update the links + const link = gLink.selectAll('path').data(links, (d: any) => d.target.id) + + // enter any new links at the parent's previous position + const linkEnter = link + .enter() + .append('path') + .attr('d', (_d) => { + const o = { x: source.x0, y: source.y0 } + return diagonal({ source: o, target: o } as any) + }) + + // transition links to their new position + link + .merge(linkEnter as any) + .transition(transition as any) + .attr('d', diagonal as any) + + // transition exiting nodes to the parent's new position + link + .exit() + .transition(transition as any) + .remove() + .attr('d', (_d) => { + const o = { x: source.x, y: source.y } + return diagonal({ source: o, target: o } as any) + }) + + // stash the old positions for transition + root.eachBefore((d) => { + d.x0 = d.x + d.y0 = d.y + }) + } + + update(root) + }, [dataSource]) + + return +} + +// refs: +// https://observablehq.com/@d3/tidy-tree +// https://observablehq.com/@d3/collapsible-tree diff --git a/ui/lib/apps/ClusterInfo/pages/List.tsx b/ui/lib/apps/ClusterInfo/pages/List.tsx index bc2cd90d27..b4ead3a335 100644 --- a/ui/lib/apps/ClusterInfo/pages/List.tsx +++ b/ui/lib/apps/ClusterInfo/pages/List.tsx @@ -9,6 +9,7 @@ import CardTabs from '@lib/components/CardTabs' import HostTable from '../components/HostTable' import InstanceTable from '../components/InstanceTable' +import StoreLocation from '../components/StoreLocation' function renderTabBar(props, DefaultTabBar) { return ( @@ -46,6 +47,12 @@ export default function ListPage() { > + + + diff --git a/ui/lib/apps/ClusterInfo/translations/en.yaml b/ui/lib/apps/ClusterInfo/translations/en.yaml index b2cbc1d260..74009096f7 100644 --- a/ui/lib/apps/ClusterInfo/translations/en.yaml +++ b/ui/lib/apps/ClusterInfo/translations/en.yaml @@ -26,5 +26,7 @@ cluster_info: disk_size: Disk Size disk_usage: Disk Usage instanceUnavailable: Host information is unknow due to instance unreachable + store_topology: + title: Store Topology error: load: 'Load component {{comp}} error: {{cause}}' diff --git a/ui/lib/apps/ClusterInfo/translations/zh.yaml b/ui/lib/apps/ClusterInfo/translations/zh.yaml index 464b0bb095..84febfa668 100644 --- a/ui/lib/apps/ClusterInfo/translations/zh.yaml +++ b/ui/lib/apps/ClusterInfo/translations/zh.yaml @@ -26,5 +26,7 @@ cluster_info: disk_size: 磁盘容量 disk_usage: 磁盘使用率 instanceUnavailable: 获取该主机信息失败:无法访问实例 + store_topology: + title: 存储拓扑 error: load: '加载组件 {{comp}} 失败: {{cause}}' diff --git a/ui/lib/apps/Configuration/InlineEditor.tsx b/ui/lib/apps/Configuration/InlineEditor.tsx new file mode 100644 index 0000000000..e126203516 --- /dev/null +++ b/ui/lib/apps/Configuration/InlineEditor.tsx @@ -0,0 +1,143 @@ +import { useState, useCallback, useEffect } from 'react' +import React from 'react' +import { EditOutlined } from '@ant-design/icons' +import { Input, Popover, Button, Space, Tooltip, Modal } from 'antd' +import { usePersistFn } from '@umijs/hooks' + +interface IInlineEditorProps { + title?: string + value: any + displayValue: string + onSave?: (newValue: any) => Promise +} + +function valueWithSameType(newValue, oldValue) { + if (typeof oldValue === 'string') { + return newValue + } else if (typeof oldValue === 'number') { + // Note: `Number()` is more strict than `parseFloat()`. + const v = Number(newValue) + if (isNaN(v)) { + throw new Error(`"${newValue}" is not a number`) + } + return v + } else if (typeof oldValue === 'boolean') { + switch (String(newValue).toLowerCase().trim()) { + case 'true': + case 'yes': + case '1': + return true + case 'false': + case 'no': + case '0': + return false + default: + throw new Error(`"${newValue}" is not a boolean`) + } + } else { + // Otherwise, return as string + return newValue + } +} + +function InlineEditor({ + value, + displayValue, + title, + onSave, +}: IInlineEditorProps) { + const [isVisible, setIsVisible] = useState(false) + const [inputVal, setInputVal] = useState(displayValue) + const [isPosting, setIsPosting] = useState(false) + + const handleCancel = useCallback(() => { + setIsVisible(false) + setInputVal(displayValue) + }, [displayValue]) + + const handleSave = usePersistFn(async () => { + if (!onSave) { + setIsVisible(false) + return + } + setIsPosting(true) + try { + // PD only accept modified config in the same value type, + // i.e. true => false, but not true => "false" + const r = await onSave(valueWithSameType(inputVal, value)) + if (r !== false) { + // When onSave returns non-false, input value is not reverted and only popup is hidden + setIsVisible(false) + } else { + // When onSave returns false, popup is not hidden and value is reverted + setInputVal(displayValue) + } + } catch (e) { + Modal.error({ + content: e.message, + zIndex: 2000, // higher than Popover + }) + setInputVal(displayValue) + setIsVisible(false) + } + setIsPosting(false) + }) + + const handleInputValueChange = useCallback((e) => { + setInputVal(e.target.value) + }, []) + + useEffect(() => { + setInputVal(displayValue) + }, [displayValue]) + + const renderPopover = usePersistFn(() => { + return ( + +
+ +
+
+ + + + +
+
+ ) + }) + + return ( + + + {' '} + + {displayValue} + + + + ) +} + +export default InlineEditor diff --git a/ui/lib/apps/Configuration/index.meta.ts b/ui/lib/apps/Configuration/index.meta.ts new file mode 100644 index 0000000000..5ed4c33271 --- /dev/null +++ b/ui/lib/apps/Configuration/index.meta.ts @@ -0,0 +1,9 @@ +import { ToolOutlined } from '@ant-design/icons' + +export default { + id: 'configuration', + routerPrefix: '/configuration', + icon: ToolOutlined, + translations: require.context('./translations/', false, /\.yaml$/), + reactRoot: () => import(/* webpackChunkName: "app_configuration" */ '.'), +} diff --git a/ui/lib/apps/Configuration/index.tsx b/ui/lib/apps/Configuration/index.tsx new file mode 100644 index 0000000000..cc653e3f1c --- /dev/null +++ b/ui/lib/apps/Configuration/index.tsx @@ -0,0 +1,238 @@ +import React, { useMemo, useCallback, useRef, useState, useEffect } from 'react' +import { Root, CardTable, Card, Pre } from '@lib/components' +import { useClientRequest } from '@lib/utils/useClientRequest' +import client, { ConfigurationItem } from '@lib/client' +import { IGroup, IColumn } from 'office-ui-fabric-react/lib/DetailsList' +import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' +import InlineEditor from './InlineEditor' +import { Modal, Spin, Tooltip, Input } from 'antd' +import { usePersistFn, useDebounce } from '@umijs/hooks' +import { LoadingOutlined } from '@ant-design/icons' +import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky' +import { useTranslation } from 'react-i18next' + +interface IRow extends ConfigurationItem { + kind: string +} + +interface IValueProps { + item: IRow + onSaved?: () => void +} + +const loadingSpinner = + +function Value({ item, onSaved }: IValueProps) { + const handleSave = usePersistFn(async (newValue) => { + try { + const resp = await client.getInstance().configurationEdit({ + id: item.id, + kind: item.kind, + new_value: newValue, + }) + if ((resp?.data?.warnings?.length ?? 0) > 0) { + Modal.warning({ + title: 'Edit configuration is partially done', + content: ( +
{resp.data.warnings?.map((w) => w.message).join('\n\n')}
+ ), + }) + } + } catch (e) { + Modal.error({ + title: 'Edit configuration failed', + content:
{e?.response?.data?.message ?? e.message}
, + zIndex: 2000, // higher than Popover + }) + return false + } + onSaved?.() + }) + + const stringValue = String(item.value) + + if (item.is_multi_value) { + return ( + + (multiple values){' '} + + {stringValue} + + + ) + } else if (!item.is_editable) { + return ( + + {stringValue} + + ) + } else { + // Note: We preserve the original value so that newValue's type can be inferred. + return ( + + ) + } +} + +function getKey(item: IRow) { + return `${item.kind}.${item.id}` +} + +export default function () { + const { + data, + isLoading, + error, + sendRequest, + } = useClientRequest((cancelToken) => + client.getInstance().configurationGetAll({ cancelToken }) + ) + + const { t } = useTranslation() + const [filterValueLower, setFilterValueLower] = useState('') + const debouncedFilterValue = useDebounce(filterValueLower, 200) + + const handleSaved = useCallback(() => { + sendRequest() + }, [sendRequest]) + + const handleFilterChange = useCallback((e) => { + setFilterValueLower(e.target.value.toLowerCase()) + }, []) + + const errors = useMemo(() => { + if (error) { + return [error] + } + if (data?.errors) { + return data.errors + } + return [] + }, [data, error]) + + const [rows, setRows] = useState([]) + const [groups, setGroups] = useState([]) + const lastSavedGroups = useRef([]) + + // When data is changed, re-calculate rows and groups. + useEffect(() => { + if (!data) { + setRows([]) + setGroups([]) + lastSavedGroups.current = [] + return + } + + const newRows: IRow[] = [] + const newGroups: IGroup[] = [] + let startIndex = 0 + for (const configKind of [ + 'tidb_variable', + 'pd_config', + 'tikv_config', + 'tidb_config', + ]) { + const items = data?.items?.[configKind] ?? [] + for (const item of items) { + if (debouncedFilterValue.length > 0) { + if ( + item.id?.toLowerCase().indexOf(debouncedFilterValue) === -1 && + String(item.value).toLowerCase().indexOf(debouncedFilterValue) === + -1 + ) { + continue + } + } + newRows.push({ + ...item, + kind: configKind, + }) + } + newGroups.push({ + key: configKind, + name: t(`configuration.common.kind.${configKind}`), + startIndex: startIndex, + count: newRows.length - startIndex, + }) + startIndex = newRows.length + } + + setRows(newRows) + + // DetailsList internally changes the group element and add new fields. When assigning new + // fresh groups, group states will be changed, result in UI state not preserved. + // Thus, we update to use new groups only when groups are different. + if (JSON.stringify(lastSavedGroups.current) === JSON.stringify(newGroups)) { + // Update group reference, otherwise DetailsList won't update + setGroups((g) => [...g]) + } else { + setGroups(newGroups) + lastSavedGroups.current = JSON.parse(JSON.stringify(newGroups)) + } + }, [data, debouncedFilterValue, t]) + + const columns = useMemo(() => { + const columns: IColumn[] = [ + { + key: 'key', + name: 'Config', + minWidth: 300, + maxWidth: 300, + onRender: (item) => { + return ( + + {item.id} + + ) + }, + }, + { + key: 'value', + name: 'Value', + onRender: (item) => { + return + }, + minWidth: 300, + maxWidth: 300, + }, + ] + return columns + }, [handleSaved]) + + return ( + + + +
+ + + +
+
+ + + + + +
+
+ ) +} diff --git a/ui/lib/apps/Configuration/translations/en.yaml b/ui/lib/apps/Configuration/translations/en.yaml new file mode 100644 index 0000000000..082f84e10b --- /dev/null +++ b/ui/lib/apps/Configuration/translations/en.yaml @@ -0,0 +1,8 @@ +configuration: + nav_title: Configurations + common: + kind: + tidb_variable: TiDB Variables + pd_config: PD Configurations + tikv_config: TiKV Configurations + tidb_config: TiDB Configurations diff --git a/ui/lib/apps/Configuration/translations/zh.yaml b/ui/lib/apps/Configuration/translations/zh.yaml new file mode 100644 index 0000000000..8c77f27bc6 --- /dev/null +++ b/ui/lib/apps/Configuration/translations/zh.yaml @@ -0,0 +1,8 @@ +configuration: + nav_title: 实例配置 + common: + kind: + tidb_variable: TiDB 变量 + pd_config: PD 配置 + tikv_config: TiKV 配置 + tidb_config: TiDB 配置 diff --git a/ui/lib/apps/InstanceProfiling/index.meta.ts b/ui/lib/apps/InstanceProfiling/index.meta.ts index 0d0c1c9d3f..da59500eec 100644 --- a/ui/lib/apps/InstanceProfiling/index.meta.ts +++ b/ui/lib/apps/InstanceProfiling/index.meta.ts @@ -1,9 +1,9 @@ -import { HeatMapOutlined } from '@ant-design/icons' +import { AimOutlined } from '@ant-design/icons' export default { id: 'instance_profiling', routerPrefix: '/instance_profiling', - icon: HeatMapOutlined, + icon: AimOutlined, translations: require.context('./translations/', false, /\.yaml$/), reactRoot: () => import(/* webpackChunkName: "app_instance_profiling" */ '.'), } diff --git a/ui/lib/apps/InstanceProfiling/pages/Detail.tsx b/ui/lib/apps/InstanceProfiling/pages/Detail.tsx index e49fa4c660..ef3bbf64f5 100644 --- a/ui/lib/apps/InstanceProfiling/pages/Detail.tsx +++ b/ui/lib/apps/InstanceProfiling/pages/Detail.tsx @@ -151,6 +151,7 @@ export default function Page() { columns={columns} items={data?.tasks_status || []} onRowClicked={handleRowClick} + extendLastColumn /> ) diff --git a/ui/lib/apps/QueryEditor/Editor.module.less b/ui/lib/apps/QueryEditor/Editor.module.less new file mode 100644 index 0000000000..b8562272d2 --- /dev/null +++ b/ui/lib/apps/QueryEditor/Editor.module.less @@ -0,0 +1,10 @@ +.editorContainer { + flex-grow: 1; + position: relative; + overflow: hidden; + + :global(.ace_editor) { + position: absolute; + z-index: 1; + } +} diff --git a/ui/lib/apps/QueryEditor/Editor.tsx b/ui/lib/apps/QueryEditor/Editor.tsx new file mode 100644 index 0000000000..c58b0d31d6 --- /dev/null +++ b/ui/lib/apps/QueryEditor/Editor.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import AceEditor, { IAceEditorProps } from 'react-ace' +import { useSize } from '@umijs/hooks' + +import 'ace-builds/src-noconflict/mode-sql' +import 'ace-builds/src-noconflict/ext-searchbox' +import './editorThemes/oneHalfDark' +import './editorThemes/oneHalfLight' + +import styles from './Editor.module.less' + +interface IEditorProps extends IAceEditorProps {} + +function Editor({ ...props }: IEditorProps, ref: React.Ref) { + const [state, containerRef] = useSize() + return ( +
+ +
+ ) +} + +export default React.memo(React.forwardRef(Editor)) diff --git a/ui/lib/apps/QueryEditor/ResultTable.module.less b/ui/lib/apps/QueryEditor/ResultTable.module.less new file mode 100644 index 0000000000..439608c0a7 --- /dev/null +++ b/ui/lib/apps/QueryEditor/ResultTable.module.less @@ -0,0 +1,7 @@ +.resultTable { + position: absolute; + top: @padding-page; // FIXME: This is hacky. Can we provide a component? + bottom: 0; + left: 0; + width: 100%; +} diff --git a/ui/lib/apps/QueryEditor/ResultTable.tsx b/ui/lib/apps/QueryEditor/ResultTable.tsx new file mode 100644 index 0000000000..f668ab4b84 --- /dev/null +++ b/ui/lib/apps/QueryEditor/ResultTable.tsx @@ -0,0 +1,64 @@ +import React, { useMemo } from 'react' +import { QueryeditorRunResponse } from '@lib/client' +import { CardTable } from '@lib/components' +import { IColumn } from 'office-ui-fabric-react/lib/DetailsList' +import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' + +import styles from './ResultTable.module.less' + +interface IResultTableProps { + results?: QueryeditorRunResponse +} + +function ResultTable({ results }: IResultTableProps) { + const columns: IColumn[] = useMemo(() => { + if (!results) { + return [] + } + if (results.error_msg) { + return [ + { + name: 'Error', + key: 'error', + minWidth: 100, + fieldName: 'error', + isMultiline: true, + }, + ] + } else { + return (results.column_names ?? []).map((cn, idx) => ({ + name: cn, + key: cn, + minWidth: 200, + maxWidth: 500, + fieldName: String(idx), + })) + } + }, [results]) + + const items = useMemo(() => { + if (!results) { + return [] + } + if (results.error_msg) { + return [{ error: results.error_msg }] + } else { + return results.rows ?? [] + } + }, [results]) + + return ( +
+ + + +
+ ) +} + +export default ResultTable diff --git a/ui/lib/apps/QueryEditor/editorThemes/oneHalfDark.js b/ui/lib/apps/QueryEditor/editorThemes/oneHalfDark.js new file mode 100644 index 0000000000..fd9d1022a5 --- /dev/null +++ b/ui/lib/apps/QueryEditor/editorThemes/oneHalfDark.js @@ -0,0 +1,118 @@ +/* eslint-disable no-multi-str */ + +const ace = require('ace-builds/src-noconflict/ace') + +ace.define( + 'ace/theme/oneHalfDark', + ['require', 'exports', 'module', 'ace/lib/dom'], + function (require, exports, module) { + exports.isDark = true + exports.cssClass = 'ace-one-half-dark' + exports.cssText = + '.ace-one-half-dark .ace_gutter {\ +background: #282c34;\ +color: rgb(130,134,140)\ +}\ +.ace-one-half-dark .ace_print-margin {\ +width: 1px;\ +background: #e8e8e8\ +}\ +.ace-one-half-dark {\ +background-color: #282c34;\ +color: #dcdfe4\ +}\ +.ace-one-half-dark .ace_cursor {\ +color: #a3b3cc\ +}\ +.ace-one-half-dark .ace_marker-layer .ace_selection {\ +background: #474e5d\ +}\ +.ace-one-half-dark.ace_multiselect .ace_selection.ace_start {\ +box-shadow: 0 0 3px 0px #282c34;\ +border-radius: 2px\ +}\ +.ace-one-half-dark .ace_marker-layer .ace_step {\ +background: rgb(198, 219, 174)\ +}\ +.ace-one-half-dark .ace_marker-layer .ace_bracket {\ +margin: -1px 0 0 -1px;\ +border: 1px solid #5c6370\ +}\ +.ace-one-half-dark .ace_marker-layer .ace_active-line {\ +background: #313640\ +}\ +.ace-one-half-dark .ace_gutter-active-line {\ +background-color: #313640\ +}\ +.ace-one-half-dark .ace_marker-layer .ace_selected-word {\ +border: 1px solid #474e5d\ +}\ +.ace-one-half-dark .ace_fold {\ +background-color: #61afef;\ +border-color: #dcdfe4\ +}\ +.ace-one-half-dark .ace_keyword {\ +color: #c678dd\ +}\ +.ace-one-half-dark .ace_constant {\ +color: #e5c07b\ +}\ +.ace-one-half-dark .ace_constant.ace_numeric {\ +color: #e5c07b\ +}\ +.ace-one-half-dark .ace_constant.ace_character.ace_escape {\ +color: #56b6c2\ +}\ +.ace-one-half-dark .ace_support.ace_function {\ +color: #61afef\ +}\ +.ace-one-half-dark .ace_support.ace_class {\ +color: #e5c07b\ +}\ +.ace-one-half-dark .ace_storage {\ +color: #c678dd\ +}\ +.ace-one-half-dark .ace_invalid.ace_illegal {\ +color: #dcdfe4;\ +background-color: #e06c75\ +}\ +.ace-one-half-dark .ace_invalid.ace_deprecated {\ +color: #dcdfe4;\ +background-color: #e5c07b\ +}\ +.ace-one-half-dark .ace_string {\ +color: #98c379\ +}\ +.ace-one-half-dark .ace_string.ace_regexp {\ +color: #98c379\ +}\ +.ace-one-half-dark .ace_comment {\ +color: #5c6370\ +}\ +.ace-one-half-dark .ace_variable {\ +color: #e06c75\ +}\ +.ace-one-half-dark .ace_meta.ace_selector {\ +color: #c678dd\ +}\ +.ace-one-half-dark .ace_entity.ace_other.ace_attribute-name {\ +color: #e5c07b\ +}\ +.ace-one-half-dark .ace_entity.ace_name.ace_function {\ +color: #61afef\ +}\ +.ace-one-half-dark .ace_entity.ace_name.ace_tag {\ +color: #e06c75\ +}' + + var dom = require('../lib/dom') + dom.importCssString(exports.cssText, exports.cssClass) + } +) +;(function () { + ace.require(['ace/theme/oneHalfDark'], function (m) { + if (typeof module == 'object' && typeof exports == 'object' && module) { + module.exports = m + } + }) +})() diff --git a/ui/lib/apps/QueryEditor/editorThemes/oneHalfLight.js b/ui/lib/apps/QueryEditor/editorThemes/oneHalfLight.js new file mode 100644 index 0000000000..3e831e2c0c --- /dev/null +++ b/ui/lib/apps/QueryEditor/editorThemes/oneHalfLight.js @@ -0,0 +1,102 @@ +/* eslint-disable no-multi-str */ + +const ace = require('ace-builds/src-noconflict/ace') + +ace.define( + 'ace/theme/oneHalfLight', + ['require', 'exports', 'module', 'ace/lib/dom'], + function (require, exports, module) { + exports.isDark = false + exports.cssClass = 'ace-one-half-light' + exports.cssText = + '.ace-one-half-light .ace_gutter {\ +background: #fafafa;\ +color: rgb(153,154,158)\ +}\ +.ace-one-half-light .ace_print-margin {\ +width: 1px;\ +background: #e8e8e8\ +}\ +.ace-one-half-light {\ +background-color: #fafafa;\ +color: #383a42\ +}\ +.ace-one-half-light .ace_cursor {\ +color: #383a42\ +}\ +.ace-one-half-light .ace_marker-layer .ace_selection {\ +background: #bfceff\ +}\ +.ace-one-half-light.ace_multiselect .ace_selection.ace_start {\ +box-shadow: 0 0 3px 0px #fafafa;\ +border-radius: 2px\ +}\ +.ace-one-half-light .ace_marker-layer .ace_step {\ +background: rgb(198, 219, 174)\ +}\ +.ace-one-half-light .ace_marker-layer .ace_bracket {\ +margin: -1px 0 0 -1px;\ +border: 1px solid #a0a1a7\ +}\ +.ace-one-half-light .ace_marker-layer .ace_active-line {\ +background: #f0f0f0\ +}\ +.ace-one-half-light .ace_gutter-active-line {\ +background-color: #f0f0f0\ +}\ +.ace-one-half-light .ace_marker-layer .ace_selected-word {\ +border: 1px solid #bfceff\ +}\ +.ace-one-half-light .ace_fold {\ +background-color: #0184bc;\ +border-color: #383a42\ +}\ +.ace-one-half-light .ace_keyword,\ +.ace-one-half-light .ace_meta.ace_selector,\ +.ace-one-half-light .ace_storage {\ +color: #a626a4\ +}\ +.ace-one-half-light .ace_constant,\ +.ace-one-half-light .ace_constant.ace_numeric,\ +.ace-one-half-light .ace_entity.ace_other.ace_attribute-name,\ +.ace-one-half-light .ace_support.ace_class {\ +color: #c18401\ +}\ +.ace-one-half-light .ace_constant.ace_character.ace_escape {\ +color: #0997b3\ +}\ +.ace-one-half-light .ace_entity.ace_name.ace_function,\ +.ace-one-half-light .ace_support.ace_function {\ +color: #0184bc\ +}\ +.ace-one-half-light .ace_invalid.ace_illegal {\ +color: #fafafa;\ +background-color: #e06c75\ +}\ +.ace-one-half-light .ace_invalid.ace_deprecated {\ +color: #fafafa;\ +background-color: #e5c07b\ +}\ +.ace-one-half-light .ace_string,\ +.ace-one-half-light .ace_string.ace_regexp {\ +color: #50a14f\ +}\ +.ace-one-half-light .ace_comment {\ +color: #a0a1a7\ +}\ +.ace-one-half-light .ace_entity.ace_name.ace_tag,\ +.ace-one-half-light .ace_variable {\ +color: #e45649\ +}' + + var dom = require('../lib/dom') + dom.importCssString(exports.cssText, exports.cssClass) + } +) +;(function () { + ace.require(['ace/theme/oneHalfLight'], function (m) { + if (typeof module == 'object' && typeof exports == 'object' && module) { + module.exports = m + } + }) +})() diff --git a/ui/lib/apps/QueryEditor/index.meta.ts b/ui/lib/apps/QueryEditor/index.meta.ts new file mode 100644 index 0000000000..39f0b8443f --- /dev/null +++ b/ui/lib/apps/QueryEditor/index.meta.ts @@ -0,0 +1,9 @@ +import { ConsoleSqlOutlined } from '@ant-design/icons' + +export default { + id: 'query_editor', + routerPrefix: '/query_editor', + icon: ConsoleSqlOutlined, + translations: require.context('./translations/', false, /\.yaml$/), + reactRoot: () => import(/* webpackChunkName: "query_editor" */ '.'), +} diff --git a/ui/lib/apps/QueryEditor/index.module.less b/ui/lib/apps/QueryEditor/index.module.less new file mode 100644 index 0000000000..c7049c7dda --- /dev/null +++ b/ui/lib/apps/QueryEditor/index.module.less @@ -0,0 +1,40 @@ +@import '~antd/es/style/themes/default.less'; + +.container { + height: 100vh; + display: flex; + flex-direction: column; + + &:before, + &:after { + // Handle margin collapse + content: ' '; + display: table; + } +} + +.contentContainer { + flex: 1; + min-height: 0; + + > :global(.gutter.gutter-vertical) { + background-color: @gray-3; + cursor: row-resize; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII='); + background-repeat: no-repeat; + background-position: center center; + margin: 0 @padding-page; + } + + &.isCollapsed > :global(.gutter) { + display: none; + } +} + +.successText { + color: @success-color; +} + +.resultTableContainer { + position: relative; +} diff --git a/ui/lib/apps/QueryEditor/index.tsx b/ui/lib/apps/QueryEditor/index.tsx new file mode 100644 index 0000000000..74f313ba7f --- /dev/null +++ b/ui/lib/apps/QueryEditor/index.tsx @@ -0,0 +1,110 @@ +import React, { useState, useCallback, useRef } from 'react' +import cx from 'classnames' +import { Root, Card } from '@lib/components' +import Split from 'react-split' +import { Button, Modal, Space, Typography } from 'antd' +import { + CaretRightOutlined, + LoadingOutlined, + WarningOutlined, + CheckOutlined, +} from '@ant-design/icons' + +import Editor from './Editor' +import ResultTable from './ResultTable' + +import styles from './index.module.less' +import client, { QueryeditorRunResponse } from '@lib/client' +import ReactAce from 'react-ace/lib/ace' +import { getValueFormat } from '@baurine/grafana-value-formats' + +const MAX_DISPLAY_ROWS = 1000 + +function App() { + const [results, setResults] = useState() + const [isRunning, setRunning] = useState(false) + const editor = useRef(null) + + const isResultsEmpty = + !results || + (!results.error_msg && (!results.column_names?.length || !results.rows)) + + const handleRun = useCallback(async () => { + setRunning(true) + setResults(undefined) + try { + const resp = await client.getInstance().queryEditorRun({ + max_rows: MAX_DISPLAY_ROWS, + statements: editor.current?.editor.getValue(), + }) + setResults(resp.data) + } catch (ex) { + Modal.error({ + content: ex.message, + }) + } + setRunning(false) + editor.current?.editor.focus() + }, []) + + return ( + +
+ + + + { + + {isRunning && } + {results && results.error_msg && ( + + Error ( + {getValueFormat('ms')(results.execution_ms || 0, 1)}) + + )} + {results && !results.error_msg && ( + + Success ( + {getValueFormat('ms')(results.execution_ms || 0, 1)}, + {(results.actual_rows || 0) > (results.rows?.length || 0) + ? `Displaying first ${results.rows?.length || 0} of ${ + results.actual_rows || 0 + } rows` + : `${results.rows?.length || 0} rows`} + ) + + )} + + } + + + + + + +
+ {!isResultsEmpty && } +
+
+
+
+ ) +} + +export default App diff --git a/ui/lib/apps/QueryEditor/translations/en.yaml b/ui/lib/apps/QueryEditor/translations/en.yaml new file mode 100644 index 0000000000..f812e9e708 --- /dev/null +++ b/ui/lib/apps/QueryEditor/translations/en.yaml @@ -0,0 +1,2 @@ +query_editor: + nav_title: Query Editor diff --git a/ui/lib/apps/QueryEditor/translations/zh.yaml b/ui/lib/apps/QueryEditor/translations/zh.yaml new file mode 100644 index 0000000000..bfe81914c0 --- /dev/null +++ b/ui/lib/apps/QueryEditor/translations/zh.yaml @@ -0,0 +1,2 @@ +query_editor: + nav_title: 查询编辑器 diff --git a/ui/lib/apps/SearchLogs/components/SearchResult.tsx b/ui/lib/apps/SearchLogs/components/SearchResult.tsx index 9f4642bfb7..544322e316 100644 --- a/ui/lib/apps/SearchLogs/components/SearchResult.tsx +++ b/ui/lib/apps/SearchLogs/components/SearchResult.tsx @@ -27,8 +27,8 @@ function componentRender({ component: target }) { return ( {target.kind ? InstanceKindName[target.kind] : '?'}{' '} - - {target.ip} + + {target.display_name} ) diff --git a/ui/lib/apps/SearchLogs/index.meta.ts b/ui/lib/apps/SearchLogs/index.meta.ts index da67f4dce0..15337190c8 100644 --- a/ui/lib/apps/SearchLogs/index.meta.ts +++ b/ui/lib/apps/SearchLogs/index.meta.ts @@ -1,9 +1,9 @@ -import { FileTextOutlined } from '@ant-design/icons' +import { FileSearchOutlined } from '@ant-design/icons' export default { id: 'search_logs', routerPrefix: '/search_logs', - icon: FileTextOutlined, + icon: FileSearchOutlined, translations: require.context('./translations/', false, /\.yaml$/), reactRoot: () => import(/* webpackChunkName: "app_search_logs" */ '.'), } diff --git a/ui/lib/apps/SlowQuery/utils/useSlowQuery.ts b/ui/lib/apps/SlowQuery/utils/useSlowQuery.ts index 1d0640b5f8..9c3754cd9d 100644 --- a/ui/lib/apps/SlowQuery/utils/useSlowQuery.ts +++ b/ui/lib/apps/SlowQuery/utils/useSlowQuery.ts @@ -81,7 +81,7 @@ export default function useSlowQuery( useEffect(() => { async function querySchemas() { try { - const res = await client.getInstance().infoDatabasesGet() + const res = await client.getInstance().infoListDatabases() setAllSchemas(res?.data || []) } catch (error) { setErrors((prev) => [...prev, { ...error }]) diff --git a/ui/lib/apps/Statement/pages/Detail/index.tsx b/ui/lib/apps/Statement/pages/Detail/index.tsx index d94f413138..6a9ffc1621 100644 --- a/ui/lib/apps/Statement/pages/Detail/index.tsx +++ b/ui/lib/apps/Statement/pages/Detail/index.tsx @@ -132,7 +132,7 @@ function DetailPage() { + } > {plans.length} diff --git a/ui/lib/apps/Statement/pages/List/TimeRangeSelector.tsx b/ui/lib/apps/Statement/pages/List/TimeRangeSelector.tsx index 44d70b2224..b6cb3255d4 100644 --- a/ui/lib/apps/Statement/pages/List/TimeRangeSelector.tsx +++ b/ui/lib/apps/Statement/pages/List/TimeRangeSelector.tsx @@ -214,7 +214,7 @@ export default function TimeRangeSelector({ value={[sliderTimeRange.begin_time!, sliderTimeRange.end_time!]} onChange={handleSliderChange} onAfterChange={handleSliderAfterChange} - tipFormatter={(val) => dayjs.unix(val).format('HH:mm')} + tipFormatter={(val) => dayjs.unix(val!).format('HH:mm')} /> {dayjs.unix(sliderTimeRange.begin_time!).format('MM-DD HH:mm')} ~{' '} diff --git a/ui/lib/apps/Statement/pages/List/index.tsx b/ui/lib/apps/Statement/pages/List/index.tsx index 8ee06e2573..7d8d54e50f 100644 --- a/ui/lib/apps/Statement/pages/List/index.tsx +++ b/ui/lib/apps/Statement/pages/List/index.tsx @@ -31,7 +31,7 @@ const defColumnKeys: IColumnKeys = { sum_latency: true, avg_latency: true, exec_count: true, - avg_mem: true, + plan_count: true, related_schemas: true, } diff --git a/ui/lib/apps/Statement/translations/en.yaml b/ui/lib/apps/Statement/translations/en.yaml index 529b00e9d2..837e8c4481 100644 --- a/ui/lib/apps/Statement/translations/en.yaml +++ b/ui/lib/apps/Statement/translations/en.yaml @@ -7,7 +7,6 @@ statement: title: Statement Information desc: time_range: Selected Time Range - plan_count: Execution Plans plans: note: There are multiple execution plans for this kind of SQL statement. You can choose to view one or multiple of them. title: @@ -65,8 +64,10 @@ statement: digest_text_tooltip: Similar queries have same statement template even for different query parameters sum_latency: Total Latency sum_latency_tooltip: Total execution time for this kind of statement - exec_count: Execution Count + exec_count: '# Exec' exec_count_tooltip: Total execution count for this kind of statement + plan_count: '# Plans' + plan_count_tooltip: Number of distinct execution plans of this statement in current time range avg_latency: Mean Latency avg_latency_tooltip: Execution time of single query avg_mem: Mean Memory diff --git a/ui/lib/apps/Statement/translations/zh.yaml b/ui/lib/apps/Statement/translations/zh.yaml index 670bf52513..6455567230 100644 --- a/ui/lib/apps/Statement/translations/zh.yaml +++ b/ui/lib/apps/Statement/translations/zh.yaml @@ -7,7 +7,6 @@ statement: title: SQL 语句信息 desc: time_range: 时间范围 - plan_count: 执行计划数 plans: note: 该 SQL 模板在选定的时间范围内有多个执行计划,您可以选择查看其中一个或多个执行计划。 title: @@ -66,6 +65,8 @@ statement: sum_latency_tooltip: 该类 SQL 语句在时间段内的累计执行时间 exec_count: 执行次数 exec_count_tooltip: 该类 SQL 语句在时间段内被执行的总次数 + plan_count: 计划数 + plan_count_tooltip: 该类 SQL 语句在时间段内的不同执行计划数量 avg_latency: 平均耗时 avg_latency_tooltip: 单条 SQL 查询的执行时间 avg_mem: 平均内存 diff --git a/ui/lib/apps/Statement/utils/tableColumns.tsx b/ui/lib/apps/Statement/utils/tableColumns.tsx index b6d91b8a04..a249e8610b 100644 --- a/ui/lib/apps/Statement/utils/tableColumns.tsx +++ b/ui/lib/apps/Statement/utils/tableColumns.tsx @@ -15,6 +15,19 @@ function commonColumnName(fieldName: string): any { return } +function planCountColumn( + _rows?: { plan_count?: number }[] // used for type check only +): IColumn { + return { + name: commonColumnName('plan_count'), + key: 'plan_count', + fieldName: 'plan_count', + minWidth: 100, + maxWidth: 300, + columnActionsMode: ColumnActionsMode.clickable, + } +} + function planDigestColumn( _rows?: { plan_digest?: string }[] // used for type check only ): IColumn { @@ -322,6 +335,7 @@ export function statementColumns( sumLatencyColumn(rows), avgMinMaxLatencyColumn(rows), execCountColumn(rows), + planCountColumn(rows), avgMaxMemColumn(rows), errorsWarningsColumn(rows), avgParseLatencyColumn(rows), diff --git a/ui/lib/apps/Statement/utils/useStatement.ts b/ui/lib/apps/Statement/utils/useStatement.ts index 0bb0d81e5f..00eaea1e92 100644 --- a/ui/lib/apps/Statement/utils/useStatement.ts +++ b/ui/lib/apps/Statement/utils/useStatement.ts @@ -95,7 +95,7 @@ export default function useStatement( async function querySchemas() { try { - const res = await client.getInstance().infoDatabasesGet() + const res = await client.getInstance().infoListDatabases() setAllSchemas(res?.data || []) } catch (error) { setErrors((prev) => [...prev, { ...error }]) diff --git a/ui/lib/apps/UserProfile/index.tsx b/ui/lib/apps/UserProfile/index.tsx index b99f97e9a3..c1f43d6dd7 100644 --- a/ui/lib/apps/UserProfile/index.tsx +++ b/ui/lib/apps/UserProfile/index.tsx @@ -1,7 +1,23 @@ -import { Button, Form, Select, Space } from 'antd' -import React, { useCallback } from 'react' +import { + Button, + Form, + Select, + Space, + Modal, + Alert, + Divider, + Tooltip, +} from 'antd' +import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { LogoutOutlined } from '@ant-design/icons' +import { CopyToClipboard } from 'react-copy-to-clipboard' +import { + LogoutOutlined, + ShareAltOutlined, + CopyOutlined, + CheckOutlined, + QuestionCircleOutlined, +} from '@ant-design/icons' import { Card, Root, @@ -9,12 +25,161 @@ import { Descriptions, CopyLink, TextWithInfo, + Pre, } from '@lib/components' import * as auth from '@lib/utils/auth' import { ALL_LANGUAGES } from '@lib/utils/i18n' import _ from 'lodash' import { useClientRequest } from '@lib/utils/useClientRequest' import client from '@lib/client' +import { getValueFormat } from '@baurine/grafana-value-formats' +import ReactMarkdown from 'react-markdown' + +const SHARE_SESSION_EXPIRY_HOURS = [0.25, 0.5, 1, 2, 3, 6, 12, 24] + +function ShareSessionButton() { + const { t } = useTranslation() + const [visible, setVisible] = useState(false) + const [isPosting, setIsPosting] = useState(false) + const [code, setCode] = useState(undefined) + const [isCopied, setIsCopied] = useState(false) + + const { data } = useClientRequest((cancelToken) => + client.getInstance().infoWhoami({ cancelToken }) + ) + + const handleOpen = useCallback(() => { + setVisible(true) + }, []) + + const handleClose = useCallback(() => { + setVisible(false) + setCode(undefined) + setIsPosting(false) + setIsCopied(false) + }, []) + + const handleFinish = useCallback(async (values) => { + setIsPosting(true) + try { + const r = await client.getInstance().userShareSession({ + expire_in_sec: values.expire * 60 * 60, + }) + setCode(r.data.code) + } catch (e) { + // TODO: Extract to a common component + Modal.error({ + content:
{e?.response?.data?.message ?? e.message}
, + }) + } finally { + setIsPosting(false) + } + }, []) + + const handleCopy = useCallback(() => { + setIsCopied(true) + }, []) + + let button = ( + + ) + + if (data?.is_shared) { + button = ( + + {button} + + ) + } + + return ( + <> + {button} + + + + + + + } + visible={!!code} + > + {code}} + type="success" + showIcon + /> + + + {t('user_profile.share_session.close')} + + } + onCancel={handleClose} + width={600} + > + + + +
+ + + + + + +
+
+ + ) +} function App() { const { t, i18n } = useTranslation() @@ -31,16 +196,19 @@ function App() { window.location.reload() }, []) - const { data, isLoading } = useClientRequest((cancelToken) => - client.getInstance().getInfo({ cancelToken }) + const { data: info, isLoading } = useClientRequest((cancelToken) => + client.getInstance().infoGet({ cancelToken }) ) return ( - - + + + + +
@@ -59,29 +227,29 @@ function App() { - {data && ( + {info && ( - + } > - {data.version?.internal_version} + {info.version?.internal_version} - + } > - {data.version?.build_git_hash} + {info.version?.build_git_hash} } > - {data.version?.build_time} + {info.version?.build_time} } > - {data.version?.standalone} + {info.version?.standalone} - + } > - {data.version?.pd_version} + {info.version?.pd_version} )} diff --git a/ui/lib/apps/UserProfile/translations/en.yaml b/ui/lib/apps/UserProfile/translations/en.yaml index ef3e3662b0..4f764c0592 100644 --- a/ui/lib/apps/UserProfile/translations/en.yaml +++ b/ui/lib/apps/UserProfile/translations/en.yaml @@ -2,9 +2,32 @@ user_profile: i18n: title: Language & Localization language: Language - user: - title: User + session: + title: Session sign_out: Sign Out + share: Share Current Session + share_unavailable_tooltip: Current session is a shared session and it cannot be shared again + share_session: + text: > + You can invite others to access this TiDB Dashboard by sharing your + current session via an **Authorization Code**: + + - The Authorization Code can be used multiple times. + + - The shared session will be invalidated after the expiry time you specified. + + - The shared session has the same privilege as your current session. + warning: > + Warning: Shared session will remain valid and cannot be revoked until it is expired. + Keep the Authorization Code safe! + form: + expire: Expire in + submit: Generate Authorization Code + close: Close + success_dialog: + title: Authorization Code Generated + copy: Copy + copied: Copied version: title: Version Information internal_version: TiDB Dashboard Internal Version diff --git a/ui/lib/apps/UserProfile/translations/zh.yaml b/ui/lib/apps/UserProfile/translations/zh.yaml index a0a1960dae..2a7687dcba 100644 --- a/ui/lib/apps/UserProfile/translations/zh.yaml +++ b/ui/lib/apps/UserProfile/translations/zh.yaml @@ -2,9 +2,30 @@ user_profile: i18n: title: 语言和本地化 language: 语言 - user: - title: 用户 + session: + title: 会话 sign_out: 登出 + share: 分享当前会话 + share_unavailable_tooltip: 当前会话是一个被分享的会话,因此无法再次分享 + share_session: + text: > + 您可以生成一个**授权码**来将您当前的会话分享给其他人、邀请他们使用该 TiDB Dashboard: + + - 授权码可以被重复使用。 + + - 分享的会话将在您指定的有效时间后过期。 + + - 分享的会话和您当前会话具有相同权限。 + warning: > + 警告:已分享的会话无法被提前注销,将保持有效直到有效时间过期,因此请妥善保管授权码。 + form: + expire: 有效时间 + submit: 生成授权码 + close: 关闭 + success_dialog: + title: 授权码已生成 + copy: 复制 + copied: 已复制 version: title: 版本信息 internal_version: TiDB Dashboard 内部版本号 diff --git a/ui/lib/components/AnimatedSkeleton/index.module.less b/ui/lib/components/AnimatedSkeleton/index.module.less index dc5bf0e153..f6014fe661 100644 --- a/ui/lib/components/AnimatedSkeleton/index.module.less +++ b/ui/lib/components/AnimatedSkeleton/index.module.less @@ -1,16 +1,19 @@ -.container.isAnimating { - position: relative; +@import '~antd/es/style/mixins/motion.less'; - .skeleton { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; +.container :global { + .skeletonAnimationFirstTime { + animation: 0.5s linear 0.5s antFadeIn; + animation-fill-mode: both; + animation-iteration-count: 1; + } + .skeletonAnimationNotFirstTime { + animation: 0.5s linear 0 antFadeIn; + animation-fill-mode: both; + animation-iteration-count: 1; } - .skeleton, - .content { - will-change: opacity; + .contentAnimation { + animation: 0.2s linear 0s antFadeIn; + animation-fill-mode: both; } } diff --git a/ui/lib/components/AnimatedSkeleton/index.tsx b/ui/lib/components/AnimatedSkeleton/index.tsx index 6c6f7b7510..bb812dccf9 100644 --- a/ui/lib/components/AnimatedSkeleton/index.tsx +++ b/ui/lib/components/AnimatedSkeleton/index.tsx @@ -1,8 +1,8 @@ -import React, { useState, useRef, useEffect } from 'react' +import React, { useEffect, useState } from 'react' import cx from 'classnames' import { Skeleton } from 'antd' import { SkeletonProps } from 'antd/lib/skeleton' -import { animated, useSpring } from 'react-spring' +import { AppearAnimate } from '..' import styles from './index.module.less' @@ -11,62 +11,28 @@ export interface IAnimatedSkeletonProps extends SkeletonProps { children?: React.ReactNode } -function opacityToDisplay(v) { - return v === 0 ? 'none' : 'block' -} - -function getTargetProps(showSkeleton) { - return { - skeletonOpacity: showSkeleton ? 1 : 0, - contentOpacity: showSkeleton ? 0 : 1, - } -} - function AnimatedSkeleton({ showSkeleton, children, ...restProps }: IAnimatedSkeletonProps) { - const skeletonRef = useRef(null) - const contentRef = useRef(null) - const [isAnimating, setAnimating] = useState(false) - const [minHeight, setMinHeight] = useState('auto') - - const [props, setProps] = useSpring(() => ({ - ...getTargetProps(showSkeleton), - onStart: () => { - setAnimating(true) - if (!skeletonRef.current || !contentRef.current) { - return - } - let minHeight = 0 - minHeight = Math.max(minHeight, skeletonRef.current!.offsetHeight) - minHeight = Math.max(minHeight, contentRef.current!.offsetHeight) - setMinHeight(minHeight) - }, - onRest: () => { - setAnimating(false) - setMinHeight('auto') - }, - })) + const [skeletonAppears, setSkeletonAppears] = useState(0) useEffect(() => { - setProps(getTargetProps(showSkeleton)) - }, [showSkeleton, setProps]) + if (showSkeleton) { + setSkeletonAppears((v) => v + 1) + } + }, [showSkeleton]) return ( -
- -
+
+ {showSkeleton && ( +
1, + })} + >
- - -
{children}
-
+ )} + {!showSkeleton && ( + {children} + )}
) } -export default AnimatedSkeleton +export default React.memo(AnimatedSkeleton) diff --git a/ui/lib/components/AppearAnimate/index.tsx b/ui/lib/components/AppearAnimate/index.tsx new file mode 100644 index 0000000000..9b6dc00fed --- /dev/null +++ b/ui/lib/components/AppearAnimate/index.tsx @@ -0,0 +1,35 @@ +import cx from 'classnames' +import React, { useState, useCallback } from 'react' +import { useEventListener } from '@umijs/hooks' + +export interface IAppearAnimateProps + extends React.HTMLAttributes { + motionName: string +} + +// A component similar to CSSMotion but is simpler, and avoids some edge case bugs. +// It simply removes the animation class after animation completes. +function AppearAnimate({ + className, + motionName, + children, +}: IAppearAnimateProps) { + const [isFirst, setIsFirst] = useState(true) + + const handleAnimationEnd = useCallback(() => { + setIsFirst(false) + }, []) + + const ref = useEventListener( + 'animationend', + handleAnimationEnd + ) + + return ( +
+ {children} +
+ ) +} + +export default React.memo(AppearAnimate) diff --git a/ui/lib/components/Card/index.module.less b/ui/lib/components/Card/index.module.less index 2ec351ff3f..e0b03268fc 100644 --- a/ui/lib/components/Card/index.module.less +++ b/ui/lib/components/Card/index.module.less @@ -20,6 +20,10 @@ margin-top: 0; } + &.noMarginBottom { + margin-bottom: 0; + } + &.noMarginLeft { margin-left: 0; } @@ -55,3 +59,15 @@ .hasTitle > .cardContent { margin-top: @padding-lg; } + +.cardContainer.flexGrow { + display: flex; + flex-grow: 1; + flex-direction: column; + + .cardInner, + .cardContent { + display: flex; + flex-grow: 1; + } +} diff --git a/ui/lib/components/Card/index.tsx b/ui/lib/components/Card/index.tsx index 61fe8553cc..13c0500026 100644 --- a/ui/lib/components/Card/index.tsx +++ b/ui/lib/components/Card/index.tsx @@ -9,8 +9,10 @@ export interface ICardProps extra?: ReactNode noMargin?: boolean noMarginTop?: boolean + noMarginBottom?: boolean noMarginLeft?: boolean noMarginRight?: boolean + flexGrow?: boolean } export default function Card({ @@ -20,17 +22,25 @@ export default function Card({ className, noMargin, noMarginTop, + noMarginBottom, noMarginLeft, noMarginRight, + flexGrow, children, ...rest }: ICardProps) { return ( -
+
- {expanded ? children : collapsedContent ?? children} - {/* */} -
- ) + return
{expanded ? children : collapsedContent ?? children}
} const translations = { diff --git a/ui/lib/components/HighlightSQL/index.tsx b/ui/lib/components/HighlightSQL/index.tsx index ae8421f003..687fbcf725 100644 --- a/ui/lib/components/HighlightSQL/index.tsx +++ b/ui/lib/components/HighlightSQL/index.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from 'react' import { Light as SyntaxHighlighter } from 'react-syntax-highlighter' import sql from 'react-syntax-highlighter/dist/esm/languages/hljs/sql' import lightTheme from 'react-syntax-highlighter/dist/esm/styles/hljs/atom-one-light' -import darkTheme from 'react-syntax-highlighter/dist/esm/styles/hljs/atom-one-dark-reasonable' +import darkTheme from 'react-syntax-highlighter/dist/esm/styles/hljs/atom-one-dark' import Pre from '../Pre' import formatSql from '@lib/utils/formatSql' import moize from 'moize' diff --git a/ui/lib/components/TopLoadingBar/index.tsx b/ui/lib/components/TopLoadingBar/index.tsx deleted file mode 100644 index d1efa180eb..0000000000 --- a/ui/lib/components/TopLoadingBar/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React, { useRef } from 'react' -import { useEventListener } from '@umijs/hooks' -import LoadingBar from 'react-top-loading-bar' - -const useLoadingBar = () => { - const loadingBar = useRef() - useEventListener('single-spa:before-routing-event', () => - loadingBar.current.continuousStart() - ) - useEventListener('single-spa:routing-event', () => - loadingBar.current.complete() - ) - - return loadingBar -} - -export default function TopLoadingBar() { - const loadingBar = useLoadingBar() - return -} diff --git a/ui/lib/components/index.ts b/ui/lib/components/index.ts index c60205f04a..51eb2676e8 100644 --- a/ui/lib/components/index.ts +++ b/ui/lib/components/index.ts @@ -50,7 +50,8 @@ export * from './DatePicker' export { default as DatePicker } from './DatePicker' export * from './ErrorBar' export { default as ErrorBar } from './ErrorBar' +export * from './AppearAnimate' +export { default as AppearAnimate } from './AppearAnimate' export { default as LanguageDropdown } from './LanguageDropdown' export { default as ParamsPageWrapper } from './ParamsPageWrapper' -export { default as TopLoadingBar } from './TopLoadingBar' diff --git a/ui/package.json b/ui/package.json index 995f7b164f..503655afe7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -6,40 +6,42 @@ "node": ">=12.0.0" }, "dependencies": { - "@ant-design/icons": "^4.0.6", + "@ant-design/icons": "^4.2.1", "@baurine/grafana-value-formats": "^1.0.0", - "@fortawesome/fontawesome-free": "^5.13.0", + "@fortawesome/fontawesome-free": "^5.14.0", "@g07cha/flexbox-react": "^5.0.0", - "@umijs/hooks": "^1.9.2", - "@welldone-software/why-did-you-render": "^4.2.0", - "antd": "^4.2.5", + "@umijs/hooks": "^1.9.3", + "@welldone-software/why-did-you-render": "^4.2.7", + "ace-builds": "^1.4.12", + "antd": "~4.2", "axios": "^0.19.0", - "bulma": "^0.8.2", + "bulma": "^0.9.0", "classnames": "^2.2.6", - "d3": "^5.15.1", - "dayjs": "^1.8.24", + "d3": "^5.16.0", + "dayjs": "^1.8.31", "echarts": "^4.8.0", "echarts-for-react": "^2.0.16", - "framer-motion": "^1.10.3", - "i18next": "^19.4.2", - "i18next-browser-languagedetector": "^4.0.1", + "i18next": "^19.6.3", + "i18next-browser-languagedetector": "^5.0.0", "lodash": "^4.17.19", - "moize": "^5.4.6", - "office-ui-fabric-react": "^7.105.3", + "moize": "^5.4.7", + "nprogress": "^0.2.0", + "office-ui-fabric-react": "^7.123.10", + "rc-animate": "^3.1.0", "react": "^16.13.1", + "react-ace": "^9.1.1", "react-copy-to-clipboard": "^5.0.2", "react-dom": "^16.13.1", "react-highlight-words": "^0.16.0", - "react-i18next": "^11.3.5", - "react-resize-detector": "^4.2.3", + "react-i18next": "^11.7.0", "react-router": "^6.0.0-alpha.3", "react-router-dom": "^6.0.0-alpha.3", + "react-split": "^2.0.9", "react-spring": "^8.0.27", - "react-syntax-highlighter": "^12.2.1", - "react-top-loading-bar": "^1.2.0", - "react-use": "^14.2.0", - "single-spa": "^5.3.4", - "single-spa-react": "^2.14.0", + "react-syntax-highlighter": "^13.0.0", + "react-use": "^15.3.3", + "single-spa": "^5.5.5", + "single-spa-react": "^3.0.1", "sql-formatter-plus-plus": "^1.4.0", "string-template": "^1.0.0" }, @@ -70,40 +72,41 @@ ] }, "devDependencies": { - "@babel/plugin-proposal-decorators": "^7.8.3", - "@openapitools/openapi-generator-cli": "^1.0.12-4.3.0", + "@babel/plugin-proposal-decorators": "^7.10.5", + "@openapitools/openapi-generator-cli": "^1.0.15-4.3.1", "@storybook/addon-actions": "^6.0.0-rc.3", "@storybook/addon-links": "^6.0.0-rc.3", "@storybook/addons": "^6.0.0-rc.3", "@storybook/preset-create-react-app": "^3.1.4", "@storybook/react": "^6.0.0-rc.3", "@types/d3": "^5.7.2", - "@types/lodash": "^4.14.149", - "@types/node": "^13.11.1", - "@types/react": "^16.9.34", + "@types/lodash": "^4.14.158", + "@types/node": "^14.0.27", + "@types/react": "^16.9.43", + "@types/react-dom": "^16.9.8", "@types/webpack-env": "^1.15.2", "babel-plugin-dynamic-import-node": "^2.3.0", "babel-plugin-import": "^1.13.0", "browserslist-useragent-regexp": "^2.1.0", - "customize-cra": "^1.0.0-alpha.0", + "customize-cra": "^1.0.0", "esm": "^3.2.25", "gulp": "^4.0.2", - "gulp-cli": "^2.2.0", + "gulp-cli": "^2.3.0", "gulp-shell": "^0.8.0", - "http-proxy-middleware": "^1.0.3", + "http-proxy-middleware": "^1.0.5", "husky": "^4.2.5", - "less": "^3.10.3", + "less": "^3.12.2", "less-loader": "^5.0.0", "mixpanel-browser": "^2.38.0", "prettier": "^2.0.4", "pretty-quick": "^2.0.1", - "react-app-rewire-alias": "^0.1.3", + "react-app-rewire-alias": "^0.1.6", "react-app-rewire-multiple-entry": "^2.1.0", "react-app-rewire-yaml": "^1.1.0", "react-app-rewired": "^2.1.5", "react-markdown": "^4.3.1", "react-scripts": "3.4.1", - "typescript": "^3.7.4", + "typescript": "^3.9.7", "webpack-bundle-analyzer": "^3.7.0", "webpackbar": "^4.0.0" } diff --git a/ui/tests/e2e/search_log.test.ts b/ui/tests/e2e/search_log.test.ts index 7752d61d97..8a81f03ca2 100644 --- a/ui/tests/e2e/search_log.test.ts +++ b/ui/tests/e2e/search_log.test.ts @@ -36,9 +36,9 @@ describe('Search Logs', () => { await ppExpect(searchForm).toClick( 'button[data-e2e="timerange-selector"]' ) - const secondsOf4weeks = 28 * 24 * 60 * 60 + const secondsOf1Hour = 60 * 60 await ppExpect(page).toClick( - `div[data-e2e="common-timeranges"] div[data-e2e="timerange-${secondsOf4weeks}"]` + `div[data-e2e="common-timeranges"] div[data-e2e="timerange-${secondsOf1Hour}"]` ) // to hide dropdown await ppExpect(searchForm).toClick( @@ -65,6 +65,9 @@ describe('Search Logs', () => { // to hide dropdown await ppExpect(searchForm).toClick('div#instances') + // input keyword + await ppExpect(page).toFill('input#keywords', 'welcome') + // start search await ppExpect(searchForm).toClick('button#search_btn') diff --git a/ui/yarn.lock b/ui/yarn.lock index 9aff06c033..47c8a528b5 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -19,22 +19,24 @@ resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.1.0.tgz#480b025f4b20ef7fe8f47d4a4846e4fee84ea06c" integrity sha512-Fi03PfuUqRs76aI3UWYpP864lkrfPo0hluwGqh7NJdLhvH4iRDc3jbJqZIvRDLHKbXrvAfPPV3+zjUccfFvWOQ== -"@ant-design/icons@^4.0.6", "@ant-design/icons@^4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-4.1.0.tgz#444edcc3822d5b43b2b038d6f893cd7f7dfcc48d" - integrity sha512-R1aIPJboGq4nVYwW7s0v/V2g6yiY27Kec5ldfK3mWHskw7bihPOKwxkHbITuSJcVNJsSvA6LNMlKZoY1u8DIKQ== +"@ant-design/icons@^4.1.0", "@ant-design/icons@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-4.2.1.tgz#6f3ea5d98ab782072e4e9cbb70f25e4403ae1a6b" + integrity sha512-245ZI40MOr5GGws+sNSiJIRRoEf/J2xvPSMgwRYf3bv8mVGQZ6XTQI/OMeV16KtiSZ3D+mBKXVYSBz2fhigOXQ== dependencies: "@ant-design/colors" "^3.1.0" "@ant-design/icons-svg" "^4.0.0" + "@babel/runtime" "^7.10.1" classnames "^2.2.6" insert-css "^2.0.0" - rc-util "^4.9.0" + rc-util "^5.0.1" "@ant-design/react-slick@~0.26.1": - version "0.26.1" - resolved "https://registry.yarnpkg.com/@ant-design/react-slick/-/react-slick-0.26.1.tgz#1462ad1342a83af51b7ea4ee0ae1d76d91d1b3d3" - integrity sha512-1CR3vNFxAMmMb9btF6w9yT1xlrhZr6f/K+OkqoCLfWxN7h7jC16UCr1RsGBoFUdSq8bYfTr3pe6AiiCEDsALvA== + version "0.26.3" + resolved "https://registry.yarnpkg.com/@ant-design/react-slick/-/react-slick-0.26.3.tgz#5ebdd0cc327ed1a92c0c69e4599efa00834a6ca8" + integrity sha512-FhaFfS+oea0P5WvhaM7BC2/P9r4F0yMoewBpDqVkOq+JxEiKRHJ7iBYJsenv2WEymnWeO3eCuMrz/Eez7pHpGg== dependencies: + "@babel/runtime" "^7.10.4" classnames "^2.2.5" json2mq "^0.2.0" lodash "^4.17.15" @@ -256,6 +258,18 @@ "@babel/helper-replace-supers" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" +"@babel/helper-create-class-features-plugin@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" + integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/helper-create-class-features-plugin@^7.8.3", "@babel/helper-create-class-features-plugin@^7.9.6": version "7.9.6" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.9.6.tgz#965c8b0a9f051801fd9d3b372ca0ccf200a90897" @@ -373,6 +387,13 @@ dependencies: "@babel/types" "^7.10.4" +"@babel/helper-member-expression-to-functions@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.5.tgz#172f56e7a63e78112f3a04055f24365af702e7ee" + integrity sha512-HiqJpYD5+WopCXIAbQDG0zye5XYVvcO9w/DHp5GsaGkRUaamLj2bEtu6i8rnGGprAhHM3qidCMgp71HF4endhA== + dependencies: + "@babel/types" "^7.10.5" + "@babel/helper-member-expression-to-functions@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c" @@ -649,6 +670,15 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-decorators" "^7.8.3" +"@babel/plugin-proposal-decorators@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.5.tgz#42898bba478bc4b1ae242a703a953a7ad350ffb4" + integrity sha512-Sc5TAQSZuLzgY0664mMDn24Vw2P8g/VhyLyGPaWiHahhgLqeZvcGeyBZOrJW0oSKIK2mvQ22a1ENXBIQLhrEiQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-decorators" "^7.10.4" + "@babel/plugin-proposal-dynamic-import@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" @@ -809,6 +839,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-decorators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.4.tgz#6853085b2c429f9d322d02f5a635018cdeb2360c" + integrity sha512-2NaoC6fAk2VMdhY1eerkfHV+lVYC1u8b+jmRJISqANCJlTxYy19HGdIkkQtix2UtkcPuPu+IlDgrVseZnU03bw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-decorators@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.8.3.tgz#8d2c15a9f1af624b0025f961682a9d53d3001bda" @@ -1892,6 +1929,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c" + integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.10.2", "@babel/runtime@^7.5.0", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99" @@ -1965,6 +2009,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15" + integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@baurine/grafana-value-formats@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@baurine/grafana-value-formats/-/grafana-value-formats-1.0.0.tgz#030e19a602799d364814d5f010a55ca2ea67b140" @@ -2024,7 +2077,7 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== -"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.2", "@emotion/is-prop-valid@^0.8.6": +"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.6": version "0.8.8" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== @@ -2090,39 +2143,47 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== -"@fluentui/keyboard-key@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@fluentui/keyboard-key/-/keyboard-key-0.2.0.tgz#8bbe4c27e0166a46007de8c54c55d6ed4b091e19" - integrity sha512-2CdaGMONY1ijub9Y6HbamcimhHsBHll5NwLTvomG15NGCftX9N3jlCnHbBBpiFCG0wDFcn1TVWyvcXRQ4BvUeg== +"@fluentui/date-time-utilities@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@fluentui/date-time-utilities/-/date-time-utilities-7.3.0.tgz#e6ee84a7b5097ca98e1cb8779dd001a1707c5cea" + integrity sha512-VymHB/GFaQM6LebrLHuPrHgk6Ra85CNjMB4+R8MAkna04w3sf07ivTfBUO3eLhAfxuW9YmlB7um8eavHq2xoDw== dependencies: + "@uifabric/set-version" "^7.0.18" tslib "^1.10.0" -"@fluentui/react-focus@^7.12.2": - version "7.12.2" - resolved "https://registry.yarnpkg.com/@fluentui/react-focus/-/react-focus-7.12.2.tgz#85cae6fe1d5cea215738cf9fff4bc2d9bd1612d7" - integrity sha512-7HPijihxlLB9oiIQXXP8dydFAViEIJflWz6GLvFRdea0bh6fdWThXIPE8LZ85AuXOPwrLS22zzA5wcq5cGAmjw== +"@fluentui/keyboard-key@^0.2.7": + version "0.2.7" + resolved "https://registry.yarnpkg.com/@fluentui/keyboard-key/-/keyboard-key-0.2.7.tgz#5a907d917b7c2ec0c06ca5938c5424f5cb36540e" + integrity sha512-NX6BPT/hXOocYCksnqSw3gTFwaMHaIsaqfe6ZbGZpfBIN4idwhVUYDLwcyjUx0FmUJoWfaVsa61fbelg35USiA== dependencies: - "@fluentui/keyboard-key" "^0.2.0" - "@uifabric/merge-styles" "^7.14.0" - "@uifabric/set-version" "^7.0.12" - "@uifabric/styling" "^7.12.12" - "@uifabric/utilities" "^7.20.0" tslib "^1.10.0" -"@fluentui/react-icons@^0.1.21": - version "0.1.21" - resolved "https://registry.yarnpkg.com/@fluentui/react-icons/-/react-icons-0.1.21.tgz#a9501802e689320b15c1401da7ae27d4beb5641c" - integrity sha512-iTNRqMr4m7fk8GkoBtHwuVwwolL5HjHB0dttz2JjJTzL4zLxgLUOb/lB/CyZItV1W+/rUlJCoIuct27Nn2D86A== +"@fluentui/react-focus@^7.12.25": + version "7.12.25" + resolved "https://registry.yarnpkg.com/@fluentui/react-focus/-/react-focus-7.12.25.tgz#fd7c5d371fd79a551d6b8e30f0d7526398230b07" + integrity sha512-NRh5HXNJ7X52I48B+SJGOM0X8vqpoD4NO8KlBpcDJFwN3VV/5Mi5BGRUq87rpbpKdPB2q4Ni3o8E2LB4qQmL0A== dependencies: - "@uifabric/set-version" "^7.0.12" - "@uifabric/styling" "^7.12.12" - "@uifabric/utilities" "^7.20.0" + "@fluentui/keyboard-key" "^0.2.7" + "@uifabric/merge-styles" "^7.16.3" + "@uifabric/set-version" "^7.0.18" + "@uifabric/styling" "^7.14.5" + "@uifabric/utilities" "^7.24.5" tslib "^1.10.0" -"@fortawesome/fontawesome-free@^5.13.0": - version "5.13.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz#fcb113d1aca4b471b709e8c9c168674fbd6e06d9" - integrity sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg== +"@fluentui/react-icons@^0.1.40": + version "0.1.40" + resolved "https://registry.yarnpkg.com/@fluentui/react-icons/-/react-icons-0.1.40.tgz#d643c5e982941a31b7c9a64acd9e377f4deae7c6" + integrity sha512-UFGMeQgajjfFwWtoEvk7eHd6Bye4aeVzOVTVKQ9MWxZZQIzP5gzA8Qk8dMfwSrPV5oplsMlTKijl7kULdY+H6g== + dependencies: + "@microsoft/load-themed-styles" "^1.10.26" + "@uifabric/set-version" "^7.0.18" + "@uifabric/utilities" "^7.24.5" + tslib "^1.10.0" + +"@fortawesome/fontawesome-free@^5.14.0": + version "5.14.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.14.0.tgz#a371e91029ebf265015e64f81bfbf7d228c9681f" + integrity sha512-OfdMsF+ZQgdKHP9jUbmDcRrP0eX90XXrsXIdyjLbkmSBzmMXPABB8eobUJtivaupucYaByz6WNe1PI1JuYm3qA== "@g07cha/flexbox-react@^5.0.0": version "5.0.0" @@ -2344,26 +2405,10 @@ dependencies: mkdirp "^1.0.4" -"@openapitools/openapi-generator-cli@^1.0.12-4.3.0": - version "1.0.12-4.3.0" - resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-1.0.12-4.3.0.tgz#845f0bfd47a73bdaa188667c3085d721e0d91785" - integrity sha512-p6y0ur69/vEslpARrcWg3geujOAjxoQIlIamZGm1cWsu4y4RrEdrolueWA1Lxww2pUzgxvb9PwD6hHFZNNfgrw== - -"@popmotion/easing@^1.0.1", "@popmotion/easing@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@popmotion/easing/-/easing-1.0.2.tgz#17d925c45b4bf44189e5a38038d149df42d8c0b4" - integrity sha512-IkdW0TNmRnWTeWI7aGQIVDbKXPWHVEYdGgd5ZR4SH/Ty/61p63jCjrPxX1XrR7IGkl08bjhJROStD7j+RKgoIw== - -"@popmotion/popcorn@^0.4.2", "@popmotion/popcorn@^0.4.4": - version "0.4.4" - resolved "https://registry.yarnpkg.com/@popmotion/popcorn/-/popcorn-0.4.4.tgz#a5f906fccdff84526e3fcb892712d7d8a98d6adc" - integrity sha512-jYO/8319fKoNLMlY4ZJPiPu8Ea8occYwRZhxpaNn/kZsK4QG2E7XFlXZMJBsTWDw7I1i0uaqyC4zn1nwEezLzg== - dependencies: - "@popmotion/easing" "^1.0.1" - framesync "^4.0.1" - hey-listen "^1.0.8" - style-value-types "^3.1.7" - tslib "^1.10.0" +"@openapitools/openapi-generator-cli@^1.0.15-4.3.1": + version "1.0.15-4.3.1" + resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-1.0.15-4.3.1.tgz#25ef943eba3c82e82379b30f858bb05ed97dae0b" + integrity sha512-U+sanspDmeBElVNjYHQ4U7BbSEJUQzjNKmiTzXpcEw/r93sgxmzS2Sew5t+Zj6kyN1YTvjhRjJikNcW9/bmTKA== "@reach/router@^1.3.3": version "1.3.4" @@ -3360,10 +3405,10 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== -"@types/lodash@^4.14.149": - version "4.14.152" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.152.tgz#7e7679250adce14e749304cdb570969f77ec997c" - integrity sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg== +"@types/lodash@^4.14.158": + version "4.14.158" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.158.tgz#b38ea8b6fe799acd076d7a8d7ab71c26ef77f785" + integrity sha512-InCEXJNTv/59yO4VSfuvNrZHt7eeNtWQEgnieIA+mIC+MOWM9arOWG2eQ8Vhk6NbOre6/BidiXhkZYeDY9U35w== "@types/markdown-to-jsx@^6.11.0": version "6.11.1" @@ -3397,10 +3442,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== -"@types/node@^13.11.1": - version "13.13.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.9.tgz#79df4ae965fb76d31943b54a6419599307a21394" - integrity sha512-EPZBIGed5gNnfWCiwEIwTE2Jdg4813odnG8iNPMQGrqVxrI+wL68SPtPeCX+ZxGBaA6pKAVc6jaKgP/Q0QzfdQ== +"@types/node@^14.0.27": + version "14.0.27" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.27.tgz#a151873af5a5e851b51b3b065c9e63390a9e0eb1" + integrity sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g== "@types/node@^14.0.4": version "14.0.13" @@ -3453,7 +3498,7 @@ "@types/react" "*" "@types/reactcss" "*" -"@types/react-dom@^16.0.3": +"@types/react-dom@^16.0.3", "@types/react-dom@^16.9.8": version "16.9.8" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA== @@ -3467,7 +3512,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.0.34", "@types/react@^16.9.34": +"@types/react@*", "@types/react@^16.0.34": version "16.9.35" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368" integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ== @@ -3475,6 +3520,14 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/react@^16.9.43": + version "16.9.43" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.43.tgz#c287f23f6189666ee3bebc2eb8d0f84bcb6cdb6b" + integrity sha512-PxshAFcnJqIWYpJbLPriClH53Z2WlJcVZE+NP2etUtWQs2s7yIMj3/LDKZT/5CHJ/F62iyjVCDu2H3jHEXIxSg== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + "@types/reactcss@*": version "1.2.3" resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834" @@ -3590,86 +3643,86 @@ semver "^7.3.2" tsutils "^3.17.1" -"@uifabric/foundation@^7.7.19": - version "7.7.19" - resolved "https://registry.yarnpkg.com/@uifabric/foundation/-/foundation-7.7.19.tgz#132f2cf28c09cfc52b9366913f9f39fb172d43b1" - integrity sha512-fD4lCxKxLrBzK9nHqgzuVyDnD2H45nZd/pWAYBJz/N7gu/+maozuaUl2h3dp2EzbdIyFd7qINP8iqOHPu9m5uQ== +"@uifabric/foundation@^7.7.39": + version "7.7.39" + resolved "https://registry.yarnpkg.com/@uifabric/foundation/-/foundation-7.7.39.tgz#d3eb9e0a86a31c631db8eb4c41397304e81d3327" + integrity sha512-dQvUcSbLFPAiLagn8gxPXMVB+I//3pz6QB313mQaNlOgeSw45S8Hm1b/sy/KoMqHl8zCJRmZInX3IYTpmhKrJQ== dependencies: - "@uifabric/merge-styles" "^7.14.0" - "@uifabric/set-version" "^7.0.12" - "@uifabric/styling" "^7.12.12" - "@uifabric/utilities" "^7.20.0" + "@uifabric/merge-styles" "^7.16.3" + "@uifabric/set-version" "^7.0.18" + "@uifabric/styling" "^7.14.5" + "@uifabric/utilities" "^7.24.5" tslib "^1.10.0" -"@uifabric/icons@^7.3.45": - version "7.3.45" - resolved "https://registry.yarnpkg.com/@uifabric/icons/-/icons-7.3.45.tgz#a136b2162badc3ec9a14402e48d17cb2639b9ce4" - integrity sha512-RIDFYF9of6mNjUY2Arryf9IbpuF8o2BWTlsnEiEVF73dGbbAF0MSJ9McM52hl1ntJ2coRMEgzHim/t3C9KN8Xw== +"@uifabric/icons@^7.3.65": + version "7.3.65" + resolved "https://registry.yarnpkg.com/@uifabric/icons/-/icons-7.3.65.tgz#584e5b38cd709504344ef0001472da0707516dd0" + integrity sha512-aDnuRS1+su/slD4pkIzdZfrDQxYfgosGdildvgIAKX9HSShw4BZKH7KkqenAdmhY3iBZoV5htr+kGFZGiCjWTw== dependencies: - "@uifabric/set-version" "^7.0.12" - "@uifabric/styling" "^7.12.12" + "@uifabric/set-version" "^7.0.18" + "@uifabric/styling" "^7.14.5" tslib "^1.10.0" -"@uifabric/merge-styles@^7.14.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@uifabric/merge-styles/-/merge-styles-7.14.0.tgz#309c1d67ffba5824060544d2d6dd122258ff9c02" - integrity sha512-LR70l856QTVYfceUA1HtHws0iFUQI3pN1Z2fyH2M1RPeBA35NimpaGkEaNSPHsCAiJ09evVsG6JP7iTUItWHUQ== +"@uifabric/merge-styles@^7.16.3": + version "7.16.3" + resolved "https://registry.yarnpkg.com/@uifabric/merge-styles/-/merge-styles-7.16.3.tgz#4e55748a7991bbb419240d828c3e18afb4bb4df1" + integrity sha512-MmLPDRVbFENixb77K041y9VlSohcULbYXHlolYedNW+KCr1tyu700GunnBwOnWRhKOoKgStvBZZdy5X7ty41xQ== dependencies: - "@uifabric/set-version" "^7.0.12" + "@uifabric/set-version" "^7.0.18" tslib "^1.10.0" -"@uifabric/react-hooks@^7.4.2": - version "7.4.2" - resolved "https://registry.yarnpkg.com/@uifabric/react-hooks/-/react-hooks-7.4.2.tgz#8784efe70ed30909c3ec54dad5419f30d0c41913" - integrity sha512-TcCsfCrXUwd4zAnCCGC5fTk9fzcm5Aoi1Z+HZICyVI3Vmm5wVh3HUnnjA2bV4O3Fxg5QN4YWKCWnD3mVSF9Cog== +"@uifabric/react-hooks@^7.6.2": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@uifabric/react-hooks/-/react-hooks-7.6.2.tgz#1685e13b2b40a53a07eca08314f38a8e0419b009" + integrity sha512-ETokkVskutNvaWWGUy6x4bOFyek1/tVfUygG1NyiqNs78S2qfl4Pt8pyZs5PDOcGNfPor5IIKSi519ZmRayCIw== dependencies: - "@uifabric/set-version" "^7.0.12" - "@uifabric/utilities" "^7.20.0" + "@uifabric/set-version" "^7.0.18" + "@uifabric/utilities" "^7.24.5" tslib "^1.10.0" -"@uifabric/set-version@^7.0.12": - version "7.0.12" - resolved "https://registry.yarnpkg.com/@uifabric/set-version/-/set-version-7.0.12.tgz#542c261fd5d675cff84290ee5c1327c9d4855e7a" - integrity sha512-XNqfKwNUoHnkW5sj15Zd0cNf5ga9fFjbTUrvc/ix74tkUPc2vrV3qBteW61IaNTNKWOFns5kewvhCK2jU4LnRg== +"@uifabric/set-version@^7.0.18": + version "7.0.18" + resolved "https://registry.yarnpkg.com/@uifabric/set-version/-/set-version-7.0.18.tgz#50860f5b8c2fceaa46ed7e22af0307d4318ca232" + integrity sha512-W/SD7FzukXw1tz8zeD7fy548as1I048dA9tTnfbWMH9iSAbRG1LWmkw2+4BgyoOcEDumcQlpGY2818+atpndyw== dependencies: tslib "^1.10.0" -"@uifabric/styling@^7.12.12": - version "7.12.12" - resolved "https://registry.yarnpkg.com/@uifabric/styling/-/styling-7.12.12.tgz#017ef59cc13544f338a677e5392400be575d6062" - integrity sha512-Z9+Y8ExZD94qZ78oNje9IBID1FAEWE4enYfGWRZzvGdVjE6Bfp29cvuOQgCm2Qf8ExQVf/v1YvEAJgASQz7ThQ== +"@uifabric/styling@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@uifabric/styling/-/styling-7.14.5.tgz#db2b91d403607214a9f37a8e3898779c76316bff" + integrity sha512-9HFZsBMXqW6uilhbL/z9Ih2JwqK0IVM1Z0HYAG9cYOciXvCZ6MUUi08nnkwRq2bkOe6vgaiCovsAh2HkOhb+0Q== dependencies: "@microsoft/load-themed-styles" "^1.10.26" - "@uifabric/merge-styles" "^7.14.0" - "@uifabric/set-version" "^7.0.12" - "@uifabric/utilities" "^7.20.0" + "@uifabric/merge-styles" "^7.16.3" + "@uifabric/set-version" "^7.0.18" + "@uifabric/utilities" "^7.24.5" tslib "^1.10.0" -"@uifabric/utilities@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@uifabric/utilities/-/utilities-7.20.0.tgz#ccfd2c0ba5ac33090f86a8c79d4d6140a4c842da" - integrity sha512-/VkAptQCeFZbk25+0iReqEYEZd23vpyiIlWfp62ANsvqWuis/ze+gtPbdCxtJucHxccquOKxuqpX1E4Azdm3CA== +"@uifabric/utilities@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@uifabric/utilities/-/utilities-7.24.5.tgz#b9c59d140133ede887085c41c85db78e4df46daf" + integrity sha512-pg8pzEwNoytofrcTx0LHBm9NZNgyJqTiF6AQV0sRbL1eF27sxGZXb9PD7PBWv0rRJ79Q0xyEr4PiEsLZ2k/d5w== dependencies: - "@uifabric/merge-styles" "^7.14.0" - "@uifabric/set-version" "^7.0.12" + "@uifabric/merge-styles" "^7.16.3" + "@uifabric/set-version" "^7.0.18" prop-types "^15.7.2" tslib "^1.10.0" -"@umijs/hooks@^1.9.2": - version "1.9.2" - resolved "https://registry.yarnpkg.com/@umijs/hooks/-/hooks-1.9.2.tgz#647becc86d4e6aa96f78371edab987b533845b8b" - integrity sha512-mYT9C7cL4u1ZymCn8zNBiAV011fwnNQFYGkl3HCVyqWTzqtdVngSbVL3vUE9ii6v30zge/tpPQAjosjrrBHfIQ== +"@umijs/hooks@^1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@umijs/hooks/-/hooks-1.9.3.tgz#cc0de9e832714e03c1338aba17969aed2d29afd7" + integrity sha512-h83Zk0x2oO8HeTZlSLsT4KVh+Me1VoXu+DHmT+cpqR7caBhii0T5c8Weko/pFBkEiB4QKzvWi7Zj+78VSKkI5w== dependencies: - "@umijs/use-request" "^1.4.2" + "@umijs/use-request" "^1.4.3" intersection-observer "^0.7.0" lodash.isequal "^4.5.0" resize-observer-polyfill "^1.5.1" screenfull "^5.0.0" -"@umijs/use-request@^1.4.2": - version "1.4.2" - resolved "https://registry.yarnpkg.com/@umijs/use-request/-/use-request-1.4.2.tgz#8673435d3d0c4205062b74e35f3fb0d25397c213" - integrity sha512-PBoOsM+4ISspGGV/5y2i21HLd/h+BkdIDmbxYvUIOF/MVXrumS6C9EFJaSl9+RrLzW9w10/wKpy0GGIg1CJ3gw== +"@umijs/use-request@^1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@umijs/use-request/-/use-request-1.4.3.tgz#bc5fadc4cb07d796eb35f09838d6a15cb12802f7" + integrity sha512-aH4GCdRnMCaaciygdN0KtCDQdBBh1KyiNUAgYDPX8Y4brmbymEpJViX1FU4isOTbV34WlbkWTiBpR9HIi2ciNQ== dependencies: lodash.debounce "^4.0.8" lodash.throttle "^4.1.1" @@ -3978,10 +4031,10 @@ text-table "^0.2.0" webpack-log "^1.1.2" -"@welldone-software/why-did-you-render@^4.2.0": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@welldone-software/why-did-you-render/-/why-did-you-render-4.2.2.tgz#720128a4f626997ece1ac455a7b13f9ef10dae0a" - integrity sha512-v08t2WXFQdnxkPodXzbPeho3FwgrlwzjwxasN+8A1LSZnkcxJpYvOF8/z+OySqehC44JT6oPB1KEnBVMrebHdw== +"@welldone-software/why-did-you-render@^4.2.7": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@welldone-software/why-did-you-render/-/why-did-you-render-4.2.7.tgz#7731bc42ef44e146be3c39f026bc4826dd4e5699" + integrity sha512-La1INHiFnHi9USYGAaRsPhMXMOt2x3qee8cXxRija0h3tQJY1/XmSSelyXDMQkVtDDa61DMfk3H59gxWDmnqsA== dependencies: lodash "^4" @@ -4020,6 +4073,11 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" +ace-builds@^1.4.12, ace-builds@^1.4.6: + version "1.4.12" + resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.4.12.tgz#888efa386e36f4345f40b5233fcc4fe4c588fae7" + integrity sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg== + acorn-globals@^4.1.0, acorn-globals@^4.3.0: version "4.3.4" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" @@ -4242,7 +4300,7 @@ ansi-wrap@0.1.0, ansi-wrap@^0.1.0: resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= -antd@^4.2.5: +antd@~4.2: version "4.2.5" resolved "https://registry.yarnpkg.com/antd/-/antd-4.2.5.tgz#5f68fa9282e49306b8c8d44304321d9b979772d7" integrity sha512-8rKCvik1gbru/nzodt+21r6ksWP9VPUfC74dhTLlRA1bY+7+ZZCS87+ikbAIARQRTh88LOe6nOxn5+3rJ3yOZA== @@ -5446,10 +5504,10 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= -bulma@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.8.2.tgz#5d928f16ed4a84549c2873f95c92c38c69c631a7" - integrity sha512-vMM/ijYSxX+Sm+nD7Lmc1UgWDy2JcL2nTKqwgEqXuOMU+IGALbXd5MLt/BcjBAPLIx36TtzhzBcSnOP974gcqA== +bulma@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.9.0.tgz#948c5445a49e9d7546f0826cb3820d17178a814f" + integrity sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ== bytes@3.0.0: version "3.0.0" @@ -5903,7 +5961,7 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= -clone@^2.1.1, clone@^2.1.2: +clone@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= @@ -6087,10 +6145,10 @@ compression@^1.7.4: safe-buffer "5.1.2" vary "~1.1.2" -compute-scroll-into-view@^1.0.13: - version "1.0.13" - resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.13.tgz#be1b1663b0e3f56cd5f7713082549f562a3477e2" - integrity sha512-o+w9w7A98aAFi/GjK8cxSV+CdASuPa2rR5UWs3+yHkJzWqaKoBEufFNWYaXInCSmUfDCVhesG+v9MTWqOjsxFg== +compute-scroll-into-view@^1.0.14: + version "1.0.14" + resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.14.tgz#80e3ebb25d6aa89f42e533956cb4b16a04cfe759" + integrity sha512-mKDjINe3tc6hGelUMNDzuhorIUZ7kS7BwyY0r2wQd2HOH2tRuJykiC06iSEX8y1TuhNzvz4GcJnK16mM2J1NMQ== concat-map@0.0.1: version "0.0.1" @@ -6619,10 +6677,10 @@ cuint@^0.2.2: resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs= -customize-cra@^1.0.0-alpha.0: - version "1.0.0-alpha.0" - resolved "https://registry.yarnpkg.com/customize-cra/-/customize-cra-1.0.0-alpha.0.tgz#7a9523a93d0720cd2190dc24137691b981abf185" - integrity sha512-Ve/D8ZWGLIfW/rKO0dXmEB4rpstxs0rNlnU3eoYU5ijwZljpJVpLheIoTuGJi+poqFhn1YNkNx7zFVpZNnK5ig== +customize-cra@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/customize-cra/-/customize-cra-1.0.0.tgz#73286563631aa08127ad4d30a2e3c89cf4e93c8d" + integrity sha512-DbtaLuy59224U+xCiukkxSq8clq++MOtJ1Et7LED1fLszWe88EoblEYFBJ895sB1mC6B4uu3xPT/IjClELhMbA== dependencies: lodash.flow "^3.5.0" @@ -6842,7 +6900,7 @@ d3-zoom@1: d3-selection "1" d3-transition "1" -d3@^5.15.1: +d3@^5.16.0: version "5.16.0" resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877" integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw== @@ -6908,10 +6966,10 @@ data-urls@^1.0.0, data-urls@^1.1.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -dayjs@^1.8.24: - version "1.8.27" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.27.tgz#a8ae63ee990af28c05c430f0e160ae835a0fbbf8" - integrity sha512-Jpa2acjWIeOkg8KURUHICk0EqnEFSSF5eMEscsOgyJ92ZukXwmpmRkPSUka7KHSfbj5eKH30ieosYip+ky9emQ== +dayjs@^1.8.31: + version "1.8.31" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.31.tgz#0cd1114c2539dd5ad9428be0c38df6d4bb40b9d3" + integrity sha512-mPh1mslned+5PuIuiUfbw4CikHk6AEAf2Baxih+wP5fssv+wmlVhvgZ7mq+BhLt7Sr/Hc8leWDiwe6YnrpNt3g== debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: version "2.6.9" @@ -7111,6 +7169,11 @@ detect-port@^1.3.0: address "^1.0.1" debug "^2.6.0" +diff-match-patch@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== + diff-sequences@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" @@ -8077,6 +8140,11 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + fast-equals@^1.6.0: version "1.6.3" resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-1.6.3.tgz#84839a1ce20627c463e1892f2ae316380c81b459" @@ -8124,7 +8192,7 @@ fastest-stable-stringify@^1.0.1: resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028" integrity sha1-kSLUBtTJ2YvqZEpraFPVh0uHsCg= -fault@^1.0.2: +fault@^1.0.0, fault@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== @@ -8498,30 +8566,6 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" -framer-motion@^1.10.3: - version "1.11.0" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-1.11.0.tgz#9463a4d8e1be9f25d0f75712a4a835691c028d65" - integrity sha512-DkJFWAHIv57L4xu8Ufq9GMs9Ifqpgt5Yi55pc4NQos8TUGomgbLlnwpO1IXK85X5K1dVNKuTZMiqjWZLeOfG/A== - dependencies: - "@popmotion/easing" "^1.0.2" - "@popmotion/popcorn" "^0.4.2" - framesync "^4.0.4" - hey-listen "^1.0.8" - popmotion "9.0.0-beta-8" - style-value-types "^3.1.6" - stylefire "^7.0.2" - tslib "^1.10.0" - optionalDependencies: - "@emotion/is-prop-valid" "^0.8.2" - -framesync@^4.0.0, framesync@^4.0.1, framesync@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/framesync/-/framesync-4.0.4.tgz#79c42c0118f26821c078570db0ff81fb863516a2" - integrity sha512-mdP0WvVHe0/qA62KG2LFUAOiWLng5GLpscRlwzBxu2VXOp6B8hNs5C5XlFigsMgrfDrr2YbqTsgdWZTc4RXRMQ== - dependencies: - hey-listen "^1.0.8" - tslib "^1.10.0" - fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -8942,6 +8986,30 @@ gulp-cli@^2.2.0: v8flags "^3.0.1" yargs "^7.1.0" +gulp-cli@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/gulp-cli/-/gulp-cli-2.3.0.tgz#ec0d380e29e52aa45e47977f0d32e18fd161122f" + integrity sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A== + dependencies: + ansi-colors "^1.0.1" + archy "^1.0.0" + array-sort "^1.0.0" + color-support "^1.1.3" + concat-stream "^1.6.0" + copy-props "^2.0.1" + fancy-log "^1.3.2" + gulplog "^1.0.0" + interpret "^1.4.0" + isobject "^3.0.1" + liftoff "^3.1.0" + matchdep "^2.0.0" + mute-stdout "^1.0.0" + pretty-hrtime "^1.0.0" + replace-homedir "^1.0.0" + semver-greatest-satisfied-range "^1.1.0" + v8flags "^3.2.0" + yargs "^7.1.0" + gulp-shell@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/gulp-shell/-/gulp-shell-0.8.0.tgz#0ed4980de1d0c67e5f6cce971d7201fd0be50555" @@ -9119,16 +9187,16 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -hey-listen@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" - integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q== - highlight-words-core@^1.2.0: version "1.2.2" resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.2.tgz#1eff6d7d9f0a22f155042a00791237791b1eeaaa" integrity sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg== +highlight.js@^10.1.1, highlight.js@~10.1.0: + version "10.1.2" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.1.2.tgz#c20db951ba1c22c055010648dfffd7b2a968e00c" + integrity sha512-Q39v/Mn5mfBlMff9r+zzA+gWxRsCRKwEMvYTiisLr/XUiFI/4puWt0Ojdko3R3JCNWGdOWaA5g/Yxqa23kC5AA== + highlight.js@~9.15.0, highlight.js@~9.15.1: version "9.15.10" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2" @@ -9352,15 +9420,15 @@ http-proxy-middleware@0.19.1: lodash "^4.17.11" micromatch "^3.1.10" -http-proxy-middleware@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.0.4.tgz#425ea177986a0cda34f9c81ec961c719adb6c2a9" - integrity sha512-8wiqujNWlsZNbeTSSWMLUl/u70xbJ5VYRwPR8RcAbvsNxzAZbgwLzRvT96btbm3fAitZUmo5i8LY6WKGyHDgvA== +http-proxy-middleware@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.0.5.tgz#4c6e25d95a411e3d750bc79ccf66290675176dc2" + integrity sha512-CKzML7u4RdGob8wuKI//H8Ein6wNTEQR7yjVEzPbhBLGdOfkfvgTnp2HLnniKBDP9QW4eG10/724iTWLBeER3g== dependencies: "@types/http-proxy" "^1.17.4" http-proxy "^1.18.1" is-glob "^4.0.1" - lodash "^4.17.15" + lodash "^4.17.19" micromatch "^4.0.2" http-proxy@^1.17.0, http-proxy@^1.18.1: @@ -9407,19 +9475,19 @@ hyphenate-style-name@^1.0.2: resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== -i18next-browser-languagedetector@^4.0.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-4.2.0.tgz#82e35d31f88a1d7c2b6d5913bf8c8481cd40aafb" - integrity sha512-qRSCBWgDUSqVQb3sTxkDC+ImYLhF+wB387Y1RpOcJvyex+V3abi+W83n4Awy+dx719AOBbKTy97FjrUGrAhbyw== +i18next-browser-languagedetector@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-5.0.0.tgz#9e946ed2ea5514a636913fe020a32455e82946e3" + integrity sha512-ekeKbRvTOsSOABSEPHFqyb6Q37JagZXjkISgQKHP84t/VZRW/B3FMVz+tBNQDVdZLsEaOe8fuJpeZsw2TvWeVQ== dependencies: "@babel/runtime" "^7.5.5" -i18next@^19.4.2: - version "19.4.5" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.4.5.tgz#f9ea8bbb48d1ec66bc3436f0bb74a16b11821e11" - integrity sha512-aLvSsURoupi3x9IndmV6+m3IGhzLzhYv7Gw+//K3ovdliyGcFRV0I1MuddI0Bk/zR7BG1U+kJOjeHFUcUIdEgg== +i18next@^19.6.3: + version "19.6.3" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.6.3.tgz#ce2346161b35c4c5ab691b0674119c7b349c0817" + integrity sha512-eYr98kw/C5z6kY21ti745p4IvbOJwY8F2T9tf/Lvy5lFnYRqE45+bppSgMPmcZZqYNT+xO0N0x6rexVR2wtZZQ== dependencies: - "@babel/runtime" "^7.3.1" + "@babel/runtime" "^7.10.1" iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" @@ -9631,7 +9699,7 @@ internal-slot@^1.0.2: has "^1.0.3" side-channel "^1.0.2" -interpret@^1.0.0: +interpret@^1.0.0, interpret@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== @@ -10981,21 +11049,19 @@ less-loader@^5.0.0: loader-utils "^1.1.0" pify "^4.0.1" -less@^3.10.3: - version "3.11.1" - resolved "https://registry.yarnpkg.com/less/-/less-3.11.1.tgz#c6bf08e39e02404fe6b307a3dfffafdc55bd36e2" - integrity sha512-tlWX341RECuTOvoDIvtFqXsKj072hm3+9ymRBe76/mD6O5ZZecnlAOVDlWAleF2+aohFrxNidXhv2773f6kY7g== +less@^3.12.2: + version "3.12.2" + resolved "https://registry.yarnpkg.com/less/-/less-3.12.2.tgz#157e6dd32a68869df8859314ad38e70211af3ab4" + integrity sha512-+1V2PCMFkL+OIj2/HrtrvZw0BC0sYLMICJfbQjuj/K8CEnlrFX6R5cKKgzzttsZDHyxQNL1jqMREjKN3ja/E3Q== dependencies: - clone "^2.1.2" tslib "^1.10.0" optionalDependencies: errno "^0.1.1" graceful-fs "^4.1.2" image-size "~0.5.0" + make-dir "^2.1.0" mime "^1.4.1" - mkdirp "^0.5.0" - promise "^7.1.1" - request "^2.83.0" + native-request "^1.0.5" source-map "~0.6.0" leven@^3.1.0: @@ -11131,11 +11197,6 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash-es@^4.17.15: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" - integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== - lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -11156,6 +11217,11 @@ lodash.flow@^3.5.0: resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o= +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -11260,6 +11326,14 @@ lowlight@1.12.1: fault "^1.0.2" highlight.js "~9.15.0" +lowlight@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.14.0.tgz#83ebc143fec0f9e6c0d3deffe01be129ce56b108" + integrity sha512-N2E7zTM7r1CwbzwspPxJvmjAbxljCPThTFawEX2Z7+P3NGrrvY54u8kyU16IY4qWfoVIxY8SYCS8jTkuG7TqYA== + dependencies: + fault "^1.0.0" + highlight.js "~10.1.0" + lru-cache@4.1.x: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -11663,7 +11737,7 @@ mixpanel-browser@^2.38.0: resolved "https://registry.yarnpkg.com/mixpanel-browser/-/mixpanel-browser-2.38.0.tgz#cfcc9105cbec47b67589b97d755c398dfc13b621" integrity sha512-6Gl95g91PWKBV6E25ga9coD7i4wWl3DgTgtaBPXG+ijNvvj6rQD1tOHJvcZgT4tucGbaNk4oYJC5lmrR0rnOBQ== -mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: +mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -11675,10 +11749,10 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moize@^5.4.6: - version "5.4.6" - resolved "https://registry.yarnpkg.com/moize/-/moize-5.4.6.tgz#9a7e402462151be3a077c03464b20dc67497e0d0" - integrity sha512-gvMOccupuKKD9Yzgda474o/RIl0EIUnCPuNLn9GPo71HJZPhWdzu/RYZQjYRheDMcuJfXifB9ttzAYCrkGKM0A== +moize@^5.4.7: + version "5.4.7" + resolved "https://registry.yarnpkg.com/moize/-/moize-5.4.7.tgz#bffa28806441d9f5cf1c4158b67a29413c438e83" + integrity sha512-7PZH8QFJ51cIVtDv7wfUREBd3gL59JB0v/ARA3RI9zkSRa9LyGjS1Bdldii2J1/NQXRQ/3OOVOSdnZrCcVaZlw== dependencies: fast-equals "^1.6.0" fast-stringify "^1.1.0" @@ -11791,6 +11865,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +native-request@^1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.0.7.tgz#ff742dc555b4c8f2f1c14b548639ba174e573856" + integrity sha512-9nRjinI9bmz+S7dgNtf4A70+/vPhnd+2krGpy4SUlADuOuSa24IDkNaZ+R/QT1wQ6S8jBdi6wE7fLekFZNfUpQ== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -11987,6 +12066,11 @@ npmlog@^4.1.2: gauge "~2.7.3" set-blocking "~2.0.0" +nprogress@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" + integrity sha1-y480xTIT2JVyP8urkH6UIq28r7E= + nth-check@^1.0.2, nth-check@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -12153,21 +12237,22 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -office-ui-fabric-react@^7.105.3: - version "7.115.4" - resolved "https://registry.yarnpkg.com/office-ui-fabric-react/-/office-ui-fabric-react-7.115.4.tgz#618ba876c3c7ec66f27737cc86320fa87a193d2a" - integrity sha512-Z2AhSjw03Pju3czyoAs+FhNBHbW5jCAtuAB7l6bBzODneDbhApskhLE/H/+GuIVRrGiaJaPo6K5LiTPEY8Pbig== +office-ui-fabric-react@^7.123.10: + version "7.123.10" + resolved "https://registry.yarnpkg.com/office-ui-fabric-react/-/office-ui-fabric-react-7.123.10.tgz#9774098c26a49c65e445eab8abd161f1aae04acf" + integrity sha512-EmVZMNOIFRDDsqNjBZb0XfLGcLvBWjIwMAZYfiWZpYpqn3uoK/8AQwKJBR2nGjj1Rg/qgv8WAB7B1S4cAL3Wdw== dependencies: - "@fluentui/react-focus" "^7.12.2" - "@fluentui/react-icons" "^0.1.21" + "@fluentui/date-time-utilities" "^7.3.0" + "@fluentui/react-focus" "^7.12.25" + "@fluentui/react-icons" "^0.1.40" "@microsoft/load-themed-styles" "^1.10.26" - "@uifabric/foundation" "^7.7.19" - "@uifabric/icons" "^7.3.45" - "@uifabric/merge-styles" "^7.14.0" - "@uifabric/react-hooks" "^7.4.2" - "@uifabric/set-version" "^7.0.12" - "@uifabric/styling" "^7.12.12" - "@uifabric/utilities" "^7.20.0" + "@uifabric/foundation" "^7.7.39" + "@uifabric/icons" "^7.3.65" + "@uifabric/merge-styles" "^7.16.3" + "@uifabric/react-hooks" "^7.6.2" + "@uifabric/set-version" "^7.0.18" + "@uifabric/styling" "^7.14.5" + "@uifabric/utilities" "^7.24.5" prop-types "^15.7.2" tslib "^1.10.0" @@ -12457,6 +12542,18 @@ parse-entities@^1.1.0, parse-entities@^1.1.2: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" +parse-entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" + integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + parse-filepath@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" @@ -12759,18 +12856,6 @@ polished@^3.4.4: dependencies: "@babel/runtime" "^7.9.2" -popmotion@9.0.0-beta-8: - version "9.0.0-beta-8" - resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.0.0-beta-8.tgz#f5a709f11737734e84f2a6b73f9bcf25ee30c388" - integrity sha512-6eQzqursPvnP7ePvdfPeY4wFHmS3OLzNP8rJRvmfFfEIfpFqrQgLsM50Gd9AOvGKJtYJOFknNG+dsnzCpgIdAA== - dependencies: - "@popmotion/easing" "^1.0.1" - "@popmotion/popcorn" "^0.4.2" - framesync "^4.0.4" - hey-listen "^1.0.8" - style-value-types "^3.1.6" - tslib "^1.10.0" - popper.js@^1.14.4, popper.js@^1.14.7: version "1.16.1" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" @@ -13527,7 +13612,7 @@ pretty-time@^1.1.0: resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== -prismjs@^1.8.4: +prismjs@^1.20.0, prismjs@^1.8.4, prismjs@~1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03" integrity sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ== @@ -13608,7 +13693,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.4" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -13742,11 +13827,6 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== -raf-schd@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" - integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== - raf@^3.4.0, raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -13812,6 +13892,17 @@ rc-align@^3.0.0, rc-align@^3.0.0-rc.0: rc-util "^4.12.0" resize-observer-polyfill "^1.5.1" +rc-align@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.1.tgz#0566de141a82d9a1923b7672c70bdb19dcde6e23" + integrity sha512-RQ5Fhxl0LW+zsxbY8dxAcpXdaHkHH2jzRSSpvBTS7G9LMK3T+WRcn4ovjg/eqAESM6TdTx0hfqWF2S1pO75jxQ== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + dom-align "^1.7.0" + rc-util "^5.0.1" + resize-observer-polyfill "^1.5.1" + rc-animate@3.x, rc-animate@^3.0.0, rc-animate@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-3.0.0.tgz#f76d7051136b7d650fb3d29366a160e95a369c0a" @@ -13822,6 +13913,16 @@ rc-animate@3.x, rc-animate@^3.0.0, rc-animate@~3.0.0: raf "^3.4.0" rc-util "^4.15.3" +rc-animate@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-3.1.0.tgz#051b689c2c7194e4c8ae016d32a0e5f9de6c8baa" + integrity sha512-8FsM+3B1H+0AyTyGggY6JyVldHTs1CyYT8CfTmG/nGHHXlecvSLeICJhcKgRLjUiQlctNnRtB1rwz79cvBVmrw== + dependencies: + "@ant-design/css-animation" "^1.7.2" + classnames "^2.2.6" + raf "^3.4.0" + rc-util "^5.0.1" + rc-cascader@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-1.1.0.tgz#e4c8058f0d388c4fc63a1257c15e4e7ab60061f5" @@ -13887,20 +13988,19 @@ rc-field-form@~1.2.0: rc-util "^4.20.3" rc-hammerjs@~0.6.0: - version "0.6.9" - resolved "https://registry.yarnpkg.com/rc-hammerjs/-/rc-hammerjs-0.6.9.tgz#9a4ddbda1b2ec8f9b9596091a6a989842a243907" - integrity sha512-4llgWO3RgLyVbEqUdGsDfzUDqklRlQW5VEhE3x35IvhV+w//VPRG34SBavK3D2mD/UaLKaohgU41V4agiftC8g== + version "0.6.10" + resolved "https://registry.yarnpkg.com/rc-hammerjs/-/rc-hammerjs-0.6.10.tgz#1831a3bd8f2199700bfcc5ad6b20a35630aeb5e0" + integrity sha512-Vgh9qIudyN5CHRop4M+v+xUniQBFWXKrsJxQRVtJOi2xgRrCeI52/bkpaL5HWwUhqTK9Ayq0n7lYTItT6ld5rg== dependencies: babel-runtime "6.x" hammerjs "^2.0.8" prop-types "^15.5.9" rc-input-number@~4.6.1: - version "4.6.2" - resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-4.6.2.tgz#1e0c1437a2aa45294588ece933f5b02551fce22d" - integrity sha512-780tUBUNCbQ4l/vKSXxMCiRKNBT6ApKXCDQG20GnT+v1lL4gq4GkdfoTGz6lb0uyY40rk7V09RtIXt7VIHrGRw== + version "4.6.3" + resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-4.6.3.tgz#828e5d11a8ea9be4b01c2463a57acfcaec283f5e" + integrity sha512-eTNIC16/Jvy6cr153BxUH0Ni0QEzz3x4qQNNd4uqBLjhGDwq4i1nj6nuRgJWgPjr3GQYJMY7gjs3AYF6shd8PA== dependencies: - babel-runtime "6.x" classnames "^2.2.0" rc-util "^4.5.1" @@ -13914,7 +14014,7 @@ rc-mentions@~1.1.0: rc-trigger "^4.0.0" rc-util "^4.6.0" -rc-menu@^8.0.1, rc-menu@~8.2.1: +rc-menu@^8.0.1: version "8.2.1" resolved "https://registry.yarnpkg.com/rc-menu/-/rc-menu-8.2.1.tgz#132f26f61b05e5f85d7977dd3cdde6ec23335ce7" integrity sha512-E2HmP9ZGam5hiZ2UJ466m3n/iqbYsPAquyZaNEDcgjhktI/LcwzpEdLIgPVyfgOGQ84QTY9wBF6f9D/032/WxA== @@ -13927,6 +14027,19 @@ rc-menu@^8.0.1, rc-menu@~8.2.1: resize-observer-polyfill "^1.5.0" shallowequal "^1.1.0" +rc-menu@~8.2.1: + version "8.2.2" + resolved "https://registry.yarnpkg.com/rc-menu/-/rc-menu-8.2.2.tgz#69d02cdd0d7cc3b78419b51ad548cf7ffe2c7850" + integrity sha512-Weu6tryEZtqZOBUWkx4RYkfH8/LRFK+F1TMUNtSYf1rRW/A9/Tv7cA69rofYXqwyjei7V6xDP42LE+SUS8mYNQ== + dependencies: + classnames "2.x" + mini-store "^3.0.1" + rc-animate "^3.0.0" + rc-trigger "^4.0.0" + rc-util "^4.13.0" + resize-observer-polyfill "^1.5.0" + shallowequal "^1.1.0" + rc-notification@~4.3.0: version "4.3.2" resolved "https://registry.yarnpkg.com/rc-notification/-/rc-notification-4.3.2.tgz#94d2a46d6797b5f0a4a852e2f991187f3885b9e5" @@ -13937,10 +14050,11 @@ rc-notification@~4.3.0: rc-util "^4.0.4" rc-pagination@~2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-2.2.2.tgz#9773726fb6c5879bd4e5fe8848cb2dea50d80b97" - integrity sha512-7QVY4xcoLcB9jHDxOm/zY1b6wEP8/jEVpcOFUQk2bbKUtrt8cRNB+FIrDUvuVeiIEFYcMXFPgKoHHMrUaIuSGA== + version "2.2.5" + resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-2.2.5.tgz#40d675c06d604099be91831b019ca19f40c1999d" + integrity sha512-7hMFNi8R7C/4cLKgmSpUb3BfMFdt4DLrjTixSRMpMBR5jwGfwRyoV9g9Tm6gCuCaAlVAX1QNtlM1T2UqEOW5lw== dependencies: + "@babel/runtime" "^7.10.1" classnames "^2.2.1" rc-picker@~1.4.16: @@ -13978,10 +14092,23 @@ rc-resize-observer@^0.2.0: rc-util "^4.14.0" resize-observer-polyfill "^1.5.1" -rc-select@^10.1.0, rc-select@~10.3.5: - version "10.3.5" - resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-10.3.5.tgz#a3c8c6013414f3f4d878f1f806c9d8a77770bfb7" - integrity sha512-6/TNVEkMT6TPrgOwAJ/pLKlEgGpvCpAgCb5c0SFfyp5wR9bDjFld/U2hhIZ7tYvuvR1Nq/1Iaj0Tumd4nJDD/w== +rc-select@^10.1.0: + version "10.5.1" + resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-10.5.1.tgz#4d4c5d4f8d2fd3b7e3dccf74e4c43142b2979247" + integrity sha512-fZraoNNhjUmDJccfk6VYgrgHBWFmHWh/NJZh2Xttcm/usOolYI1RjO9ikP4QGhzlJBFtnTLH2pE2nchjn3TCXA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-animate "^3.0.0" + rc-trigger "^4.3.0" + rc-util "^5.0.1" + rc-virtual-list "^1.1.2" + warning "^4.0.3" + +rc-select@~10.3.5: + version "10.3.6" + resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-10.3.6.tgz#d11800c3a8c8e8c75ed9e4b0d6cf790ce59edc42" + integrity sha512-FDiqh48Djuq+NbUQyl4K110EkeLwBmxBQ94bSGdz7CFpT1wptVzDlpBc0PBqO+MUQ4Q+vCOhShArTf9GQyoJbg== dependencies: classnames "2.x" rc-animate "^3.0.0" @@ -14064,7 +14191,18 @@ rc-tree-select@~3.1.0: rc-tree "^3.1.0" rc-util "^4.17.0" -rc-tree@^3.1.0, rc-tree@~3.2.0: +rc-tree@^3.1.0: + version "3.8.2" + resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-3.8.2.tgz#e22c0a4e11726ee676a83227099f16634c5feb7f" + integrity sha512-4RLJhiGpt/Wvj7O9mm0IX+Bv9PrcdKL3j2BIpGLQX3Xd+n4p5PcqL46mRRdGoWvvTJcZ6ZEiZDg46nSyTYLdHA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-animate "^3.1.0" + rc-util "^5.0.0" + rc-virtual-list "^1.1.0" + +rc-tree@~3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-3.2.2.tgz#9822c4929570156a2170037a4faeb010cae10702" integrity sha512-eF4vJJpMnTmPufxplZ2xg+P3ogfFoUfZkZXV2qasC6JSTf+13jB7pA1xXjCwJ86V+7PqEGUsE/mljLusng0XEA== @@ -14085,6 +14223,18 @@ rc-trigger@^4.0.0, rc-trigger@^4.2.1: rc-animate "^3.0.0" rc-util "^4.20.0" +rc-trigger@^4.3.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-4.3.4.tgz#095cf7dddf2643a65621df4b1612d167af506c31" + integrity sha512-GaRqwJ99RA9qpN3crTndOIfQZG+dgs+l2i4bgB7tl1MBTaNbmJyopi+gyoaHwg2/C6mpvQ2XNrzADEyYEkxqlA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.6" + raf "^3.4.1" + rc-align "^4.0.0" + rc-animate "^3.0.0" + rc-util "^5.0.1" + rc-trigger@~4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-4.1.0.tgz#6b13a41161716d6353e6324a01055efacb07cf71" @@ -14104,7 +14254,18 @@ rc-upload@~3.0.4: babel-runtime "6.x" classnames "^2.2.5" -rc-util@^4.0.4, rc-util@^4.11.0, rc-util@^4.12.0, rc-util@^4.13.0, rc-util@^4.14.0, rc-util@^4.15.3, rc-util@^4.16.1, rc-util@^4.17.0, rc-util@^4.20.0, rc-util@^4.20.1, rc-util@^4.20.3, rc-util@^4.20.5, rc-util@^4.20.6, rc-util@^4.5.1, rc-util@^4.6.0, rc-util@^4.8.0, rc-util@^4.9.0: +rc-util@^4.0.4, rc-util@^4.11.0, rc-util@^4.16.1, rc-util@^4.17.0, rc-util@^4.20.1, rc-util@^4.20.3, rc-util@^4.20.5, rc-util@^4.20.6, rc-util@^4.5.1, rc-util@^4.6.0: + version "4.21.1" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.21.1.tgz#88602d0c3185020aa1053d9a1e70eac161becb05" + integrity sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg== + dependencies: + add-dom-event-listener "^1.1.0" + prop-types "^15.5.10" + react-is "^16.12.0" + react-lifecycles-compat "^3.0.4" + shallowequal "^1.1.0" + +rc-util@^4.12.0, rc-util@^4.13.0, rc-util@^4.14.0, rc-util@^4.15.3, rc-util@^4.20.0, rc-util@^4.8.0: version "4.20.7" resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.20.7.tgz#8c19f3a489744ec00c9871ffe3701cdb7ff4a38e" integrity sha512-wzL7bGTimuUK/SgXlUIxcXYdQ27eAaBd8PTuiKz7ZI/Mbm5R6ZcRTWpeIGG56t+/79oi++kobfKbJrPxMTkKIw== @@ -14115,6 +14276,14 @@ rc-util@^4.0.4, rc-util@^4.11.0, rc-util@^4.12.0, rc-util@^4.13.0, rc-util@^4.14 react-lifecycles-compat "^3.0.4" shallowequal "^1.1.0" +rc-util@^5.0.0, rc-util@^5.0.1: + version "5.0.6" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.0.6.tgz#2b828bc87a818a66384b813f76a561ad4609e9b0" + integrity sha512-uLGxF9WjbpJSjd6iDnIjl8ZeMUglpcuh1DwO26aaXh++yAmlB6eIAJMUwwJCuqJvo4quCvsDPg1VkqHILc4U0A== + dependencies: + react-is "^16.12.0" + shallowequal "^1.1.0" + rc-virtual-list@^1.1.0, rc-virtual-list@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-1.1.3.tgz#4b59d1f727f3ba2dc4ccea21f3e92a5e023c11a0" @@ -14124,6 +14293,17 @@ rc-virtual-list@^1.1.0, rc-virtual-list@^1.1.2: raf "^3.4.1" rc-util "^4.8.0" +react-ace@^9.1.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-9.1.1.tgz#fe27e1c668b0186dc1609c422198d1c2df34d2bf" + integrity sha512-dL0w6GwtnS1opsOoWhJaF7rF7xCM+NOEOfePmDfiaeU+EyZQ6nRWDBgyzKsuiB3hyXH3G9D6FX37ur/LKUdKjA== + dependencies: + ace-builds "^1.4.6" + diff-match-patch "^1.0.4" + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + prop-types "^15.7.2" + react-app-polyfill@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-1.0.6.tgz#890f8d7f2842ce6073f030b117de9130a5f385f0" @@ -14136,10 +14316,10 @@ react-app-polyfill@^1.0.6: regenerator-runtime "^0.13.3" whatwg-fetch "^3.0.0" -react-app-rewire-alias@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/react-app-rewire-alias/-/react-app-rewire-alias-0.1.4.tgz#320aa032810c3c25bc549fbb0e22e531d751e04e" - integrity sha512-M2+j/Re/qBrAyaJVK04JGZyNd4fYF1xrpWWM03lgCNNNRkPa7E2OVL0WUDHK2xyyOwBBwFYaPumoSTdzoo+PgA== +react-app-rewire-alias@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/react-app-rewire-alias/-/react-app-rewire-alias-0.1.6.tgz#27fd6b47769871d97665393e2e572caf184948ef" + integrity sha512-RWI9danv1hw2YJQ1LrAImmB4g1TpGe56RZUbfyhZGS2dwpz2s1tz9JlA2HszSdNUUqcjat+Qcw544/Wz/zE8kw== react-app-rewire-multiple-entry@^2.1.0: version "2.2.0" @@ -14309,10 +14489,10 @@ react-hotkeys@2.0.0: dependencies: prop-types "^15.6.1" -react-i18next@^11.3.5: - version "11.5.0" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.5.0.tgz#84a9bb535d44c0c1b336b94de164515c2cc2a714" - integrity sha512-V6rUT7MzYBdFCgUrhfr78FHRfnY3CFoR75ET9EP5Py5UPHKyaGiK1MvPx03TesLwsmIaVHlRFU/WLzqCedXevA== +react-i18next@^11.7.0: + version "11.7.0" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.7.0.tgz#f27c4c237a274e007a48ac1210db83e33719908b" + integrity sha512-8tvVkpuxQlubcszZON+jmoCgiA9gCZ74OAYli9KChPhETtq8pJsANBTe9KRLRLmX3ubumgvidURWr0VvKz1tww== dependencies: "@babel/runtime" "^7.3.1" html-parse-stringify2 "2.0.1" @@ -14371,17 +14551,6 @@ react-popper@^1.3.7: typed-styles "^0.0.7" warning "^4.0.2" -react-resize-detector@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-4.2.3.tgz#7df258668a30bdfd88e655bbdb27db7fd7b23127" - integrity sha512-4AeS6lxdz2KOgDZaOVt1duoDHrbYwSrUX32KeM9j6t9ISyRphoJbTRCMS1aPFxZHFqcCGLT1gMl3lEcSWZNW0A== - dependencies: - lodash "^4.17.15" - lodash-es "^4.17.15" - prop-types "^15.7.2" - raf-schd "^4.0.2" - resize-observer-polyfill "^1.5.1" - react-router-dom@^6.0.0-alpha.3: version "6.0.0-alpha.5" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.0.0-alpha.5.tgz#3c3e22226ee610eb91042a351741ce3f53596323" @@ -14468,6 +14637,14 @@ react-sizeme@^2.6.7: shallowequal "^1.1.0" throttle-debounce "^2.1.0" +react-split@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/react-split/-/react-split-2.0.9.tgz#8267b198c6b186857671da94c3feed828e6b2c66" + integrity sha512-IxKtxxmcbNUmWMSd5vlNnlE0jwbgQS1HyQYxt7h8qFgPskSkUTNzMbO838xapmmNf9D+u9B/bdtFnVjt+JC2JA== + dependencies: + prop-types "^15.5.7" + split.js "^1.6.0" + react-spring@^8.0.27: version "8.0.27" resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-8.0.27.tgz#97d4dee677f41e0b2adcb696f3839680a3aa356a" @@ -14487,6 +14664,17 @@ react-syntax-highlighter@^12.2.1: prismjs "^1.8.4" refractor "^2.4.1" +react-syntax-highlighter@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-13.0.0.tgz#7f29b0fae2aef1a4b16fdd309994ee49b94dcc42" + integrity sha512-wwZlztFmZNmE7ZsXYApUpoPhZ/QU6ngWh8fYnh8QkoKnvCxRQ6ZQiNuX+EzFN+NSVpDxpAJjW+faT25XVodX5A== + dependencies: + "@babel/runtime" "^7.3.1" + highlight.js "^10.1.1" + lowlight "^1.14.0" + prismjs "^1.20.0" + refractor "^3.0.0" + react-textarea-autosize@^8.1.1: version "8.2.0" resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.2.0.tgz#fae38653f5ec172a855fd5fffb39e466d56aebdb" @@ -14496,37 +14684,30 @@ react-textarea-autosize@^8.1.1: use-composed-ref "^1.0.0" use-latest "^1.0.0" -react-top-loading-bar@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/react-top-loading-bar/-/react-top-loading-bar-1.2.0.tgz#fcbca3c2e462bee7b1c0d1850e8b584cd7bd5a26" - integrity sha512-5oNdy+DfD5JK06bcc/gsnnXHmml+d8eaBe3C8KQ3eLiH/BD8+FcwsgbAwqgOaRjuSeVQXdYN2JC2G1uVFtCLfA== - -react-universal-interface@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.0.tgz#b65cbf7d71a2f3f7dd9705d8e4f06748539bd465" - integrity sha512-PzApKKWfd7gvDi1sU/D07jUqnLvFxYqvJi+GEtLvBO5tXJjKr2Sa8ETVHkMA7Jcvdwt7ttbPq7Sed1JpFdNqBQ== - dependencies: - tslib "^1.9.3" +react-universal-interface@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b" + integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw== -react-use@^14.2.0: - version "14.3.0" - resolved "https://registry.yarnpkg.com/react-use/-/react-use-14.3.0.tgz#aa794db42108e15363be5c04db35a57acf8ecb6b" - integrity sha512-Jx7Zl0k8dHA0UKpTVwYUThC5/V+Dt6JzCGiMHPNIhsxJGkiKuB1AQ7J7pNq4zj3l37ABd/RF+jRGThw0czrJXA== +react-use@^15.3.3: + version "15.3.3" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-15.3.3.tgz#f16de7a16286c446388e8bd99680952fc3dc9a95" + integrity sha512-nYb94JbmDCaLZg3sOXmFW8HN+lXWxnl0caspXoYfZG1CON8JfLN9jMOyxRDUpm7dUq7WZ5mIept/ByqBQKJ0wQ== dependencies: "@types/js-cookie" "2.2.6" "@xobotyi/scrollbar-width" "1.9.5" copy-to-clipboard "^3.2.0" - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" fast-shallow-equal "^1.0.0" js-cookie "^2.2.1" nano-css "^5.2.1" - react-universal-interface "^0.6.0" + react-universal-interface "^0.6.2" resize-observer-polyfill "^1.5.1" screenfull "^5.0.0" set-harmonic-interval "^1.0.1" throttle-debounce "^2.1.0" ts-easing "^0.2.0" - tslib "^1.10.0" + tslib "^2.0.0" react@^16.13.1, react@^16.8.3: version "16.13.1" @@ -14673,6 +14854,15 @@ refractor@^2.4.1: parse-entities "^1.1.2" prismjs "~1.17.0" +refractor@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.0.0.tgz#7c8072eaf49dbc1b333e7acc64fb52a1c9b17c75" + integrity sha512-eCGK/oP4VuyW/ERqjMZRZHxl2QsztbkedkYy/SxqE/+Gh1gLaAF17tWIOcVJDiyGhar1NZy/0B9dFef7J0+FDw== + dependencies: + hastscript "^5.0.0" + parse-entities "^2.0.0" + prismjs "~1.20.0" + regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -14862,7 +15052,7 @@ request-promise-native@^1.0.5: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.83.0, request@^2.87.0, request@^2.88.0: +request@^2.87.0, request@^2.88.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -15211,11 +15401,11 @@ screenfull@^5.0.0: integrity sha512-cCF2b+L/mnEiORLN5xSAz6H3t18i2oHh9BA8+CQlAh5DRw2+NFAGQJOSYbcGw8B2k04g/lVvFcfZ83b3ysH5UQ== scroll-into-view-if-needed@^2.2.20: - version "2.2.24" - resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.24.tgz#12bca532990769bd509115a49edcfa755e92a0ea" - integrity sha512-vsC6SzyIZUyJG8o4nbUDCiIwsPdH6W/FVmjT2avR2hp/yzS53JjGmg/bKD20TkoNajbu5dAQN4xR7yes4qhwtQ== + version "2.2.25" + resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.25.tgz#117b7bc7c61bc7a2b7872a0984bc73a19bc6e961" + integrity sha512-C8RKJPq9lK7eubwGpLbUkw3lklcG3Ndjmea2PyauzrA0i4DPlzAmVMGxaZrBFqCrVLfvJmP80IyHnv4jxvg1OQ== dependencies: - compute-scroll-into-view "^1.0.13" + compute-scroll-into-view "^1.0.14" secure-keys@^1.0.0: version "1.0.0" @@ -15469,15 +15659,15 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -single-spa-react@^2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/single-spa-react/-/single-spa-react-2.14.0.tgz#4a55ea74a57db06adb41f0de3419a0c378745e1b" - integrity sha512-KQ2/y7/JBIquK0WUiwb1/Y7f4qTZITNotw+JwNPesj0WKeCi91u0LOZe2ps56QMJbyB4UrA5IzMBwbYWDr1pIw== +single-spa-react@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/single-spa-react/-/single-spa-react-3.0.1.tgz#7e67eaec47bc15b5716d6af6f83f5fbf1a1c05df" + integrity sha512-/toJWeG0UgJuN2CnvP5CewDR5IsH3GYXsFuOQgqkjY3BPUNDFYy4NiEAZE3OWpW+FjTbziGlrqy/u67WtE7tCw== -single-spa@^5.3.4: - version "5.5.1" - resolved "https://registry.yarnpkg.com/single-spa/-/single-spa-5.5.1.tgz#2d9bc2155e3698dab1a1ec97e94e33de193db53b" - integrity sha512-cGIQN5u4GQVVOSSbYdPM+a48HmZmbT64S/aLZ5tBM2keWHfZkKF6+NyfMXvW3ZQ3xs4Ugs5st+ZxgmAO3e/yCw== +single-spa@^5.5.5: + version "5.5.5" + resolved "https://registry.yarnpkg.com/single-spa/-/single-spa-5.5.5.tgz#77a947516874d50b1e1a43aa6c316b550605d4fb" + integrity sha512-10YHGwETkAT6k9dkOTClig2h/R6yA5qvLdvk6i7Z4/PK8A2iD/pVvYKIud0aZVA69OzO1lrQOHB4VgT4t8/kXw== sisteransi@^1.0.4: version "1.0.5" @@ -15690,6 +15880,11 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +split.js@^1.6.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/split.js/-/split.js-1.6.2.tgz#b8c63aeef2b15d84a003ead09e7def6ad166bb40" + integrity sha512-72C7zcQePzlmWqPOKkB2Ro0sUmnWSx+qEWXjLJKk6Qp4jAkFRz1hJgJb+ay6ZQyz/Aw9r8N/PZiCEKbPVpFoDQ== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -16094,14 +16289,6 @@ style-loader@^1.2.1: loader-utils "^2.0.0" schema-utils "^2.6.6" -style-value-types@^3.1.6, style-value-types@^3.1.7: - version "3.1.7" - resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-3.1.7.tgz#3d7d3cf9cb9f9ee86c00e19ba65d6a181a0db33a" - integrity sha512-jPaG5HcAPs3vetSwOJozrBXxuHo9tjZVnbRyBjxqb00c2saIoeuBJc1/2MtvB8eRZy41u/BBDH0CpfzWixftKg== - dependencies: - hey-listen "^1.0.8" - tslib "^1.10.0" - styled-components@^2.0.0: version "2.4.1" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.4.1.tgz#663bd0485d4b6ab46f946210dc03d2398d1ade74" @@ -16116,17 +16303,6 @@ styled-components@^2.0.0: stylis "^3.4.0" supports-color "^3.2.3" -stylefire@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/stylefire/-/stylefire-7.0.3.tgz#9120ecbb084111788e0ddaa04074799750f20d1d" - integrity sha512-Q0l7NSeFz/OkX+o6/7Zg3VZxSAZeQzQpYomWmIpOehFM/rJNMSLVX5fgg6Q48ut2ETNKwdhm97mPNU643EBCoQ== - dependencies: - "@popmotion/popcorn" "^0.4.4" - framesync "^4.0.0" - hey-listen "^1.0.8" - style-value-types "^3.1.7" - tslib "^1.10.0" - stylehacks@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" @@ -16557,7 +16733,7 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== -tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: version "1.13.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== @@ -16636,10 +16812,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.7.4: - version "3.9.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a" - integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ== +typescript@^3.9.7: + version "3.9.7" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== ua-parser-js@^0.7.18: version "0.7.21" @@ -16986,6 +17162,13 @@ v8flags@^3.0.1: dependencies: homedir-polyfill "^1.0.1" +v8flags@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" + integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== + dependencies: + homedir-polyfill "^1.0.1" + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"