diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97d028dc77..34f20d3806 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,8 +14,6 @@ Although TiDB Dashboard can also be integrated into [PD], this form is not conve ### Step 1. Start a TiDB cluster -#### Solution A. Use TiUP (Recommended) - [TiUP] is the offical component manager for [TiDB]. It can help you set up a local TiDB cluster in a few minutes. Download and install TiUP: @@ -40,73 +38,7 @@ Start a local TiDB cluster: tiup playground nightly ``` -> Note: you might notice that there is already a TiDB Dashboard integrated into the PD started by TiUP. For development purpose, we will not use the that TiDB Dashboard. Please keep following the rest of the steps in this document. - -#### Solution B. Download and Run Binary Manually - -
- -Alternatively, you can deploy a cluster with binary files manually. - -1. Download binaries - - Linux: - - ```bash - mkdir tidb_cluster - cd tidb_cluster - wget https://download.pingcap.org/tidb-nightly-linux-amd64.tar.gz - tar -xzf tidb-nightly-linux-amd64.tar.gz - cd tidb-nightly-linux-amd64 - ``` - - MacOS: - - ```bash - mkdir tidb_cluster - cd tidb_cluster - wget https://download.pingcap.org/tidb-nightly-darwin-amd64.tar.gz - wget https://download.pingcap.org/tikv-nightly-darwin-amd64.tar.gz - wget https://download.pingcap.org/pd-nightly-darwin-amd64.tar.gz - mkdir tidb-nightly-darwin-amd64 - tar -xzf tidb-nightly-darwin-amd64.tar.gz -C tidb-nightly-darwin-amd64 --strip-components=1 - tar -xzf tikv-nightly-darwin-amd64.tar.gz -C tidb-nightly-darwin-amd64 --strip-components=1 - tar -xzf pd-nightly-darwin-amd64.tar.gz -C tidb-nightly-darwin-amd64 --strip-components=1 - cd tidb-nightly-darwin-amd64 - ``` - -2. Start a PD server - - ```bash - ./bin/pd-server --name=pd --data-dir=pd --client-urls=http://127.0.0.1:2379 --log-file=pd.log - # Now pd-server is listen on port 2379 - ``` - -3. Start a TiKV server - - Open a new terminal: - - ```bash - ./bin/tikv-server --addr="127.0.0.1:20160" --pd-endpoints="127.0.0.1:2379" --data-dir=tikv --log-file=./tikv.log - # Now tikv-server is listen on port 20160 - ``` - -4. Start a TiDB server - - Open a new terminal: - - ```bash - ./bin/tidb-server --store=tikv --path="127.0.0.1:2379" --log-file=tidb.log - # Now tidb-server is listen on port 4000 - ``` - -5. Use mysql-client to check everything works fine: - - ```bash - mysql -h 127.0.0.1 -P 4000 -uroot - ``` - -
+You might notice that there is already a TiDB Dashboard integrated into the PD started by TiUP. For development purpose, it will not be used intentionally. ### Step 2. Prepare Prerequisites @@ -124,7 +56,7 @@ The followings are required for developing TiDB Dashboard: 1. Clone the repository: ```bash - git clone https://github.com/pingcap-incubator/tidb-dashboard.git + git clone https://github.com/pingcap/tidb-dashboard.git cd tidb-dashboard ``` @@ -144,17 +76,11 @@ The followings are required for developing TiDB Dashboard: yarn start ``` -1. That's it! You can access TiDB Dashboard now: - - TiDB Dashboard UI: http://127.0.0.1:3001 - - Swagger UI for TiDB Dashboard APIs: http://localhost:12333/dashboard/api/swagger +1. That's it! You can access TiDB Dashboard now: http://127.0.0.1:3001 ### Step 4. Run E2E Tests (optional) -Now we have only a few e2e tests in the `ui/tests` folder, you can contribute more for it. - -After finishing the above steps, we can run the tests by following commands: +When back-end server and front-end server are both started, E2E tests can be run by: ```bash cd ui/tests @@ -162,16 +88,30 @@ yarn yarn test ``` -### Step 5. Run Storybook Playground (optional) +> Now we have only a few e2e tests. Contributions are welcome! + +## Additional Guides + +### Swagger UI + +We use [Swagger] to generate the API server and corresponding clients. Swagger provides a web UI in which you can +see all TiDB Dashboard API endpoints and specifications, or even send API requests. + +Swagger UI is available at http://localhost:12333/dashboard/api/swagger after the above Step 3 is finished. + +### Storybook + +We expose some UI components in a playground provided by [React Storybook]. In the playground you can see what +components look like and how to use them. -After finishing the above steps, we can run the storybook playground by following commands: +Storybook can be started using the following commands: ```bash cd ui yarn storybook ``` -You can add more stories for your components to the playground. +> We have not yet make all components available in the Storybook. Contributions are welcome! ## Contribution flow @@ -256,3 +196,5 @@ The body of the commit message should describe why the change was made and at a [tidb]: https://github.com/pingcap/tidb [tikv]: https://github.com/tikv/tikv [tiup]: https://tiup.io +[Swagger]: https://swagger.io +[React Storybook]: https://storybook.js.org \ No newline at end of file diff --git a/go.mod b/go.mod index 84184fdcbc..1fad317dda 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( go.uber.org/zap v1.13.0 golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 // indirect + golang.org/x/sync v0.0.0-20190423024810-112230192c58 google.golang.org/grpc v1.25.1 gopkg.in/oleiade/reflections.v1 v1.0.0 ) diff --git a/go.sum b/go.sum index 36b1c7ab53..647834bb0d 100644 --- a/go.sum +++ b/go.sum @@ -424,6 +424,7 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 3b23d5e37f..b519f5f33c 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -126,18 +126,18 @@ func (s *Service) Start(ctx context.Context) error { ), fx.Populate(&s.apiHandlerEngine), fx.Invoke( - user.Register, - info.Register, - clusterinfo.Register, - profiling.Register, - logsearch.Register, - slowquery.Register, - statement.Register, - diagnose.Register, - keyvisual.Register, - metrics.Register, - queryeditor.Register, - configuration.Register, + user.RegisterRouter, + info.RegisterRouter, + clusterinfo.RegisterRouter, + profiling.RegisterRouter, + logsearch.RegisterRouter, + slowquery.RegisterRouter, + statement.RegisterRouter, + diagnose.RegisterRouter, + keyvisual.RegisterRouter, + metrics.RegisterRouter, + queryeditor.RegisterRouter, + configuration.RegisterRouter, // Must be at the end s.status.Register, ), diff --git a/pkg/apiserver/clusterinfo/service.go b/pkg/apiserver/clusterinfo/service.go index 6b9eb70591..312a479eff 100644 --- a/pkg/apiserver/clusterinfo/service.go +++ b/pkg/apiserver/clusterinfo/service.go @@ -60,7 +60,7 @@ func NewService(lc fx.Lifecycle, p ServiceParams) *Service { return s } -func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { +func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/topology") endpoint.Use(auth.MWAuthRequired()) endpoint.GET("/tidb", s.getTiDBTopology) diff --git a/pkg/apiserver/configuration/router.go b/pkg/apiserver/configuration/router.go index 00bf34670c..380586ec7e 100644 --- a/pkg/apiserver/configuration/router.go +++ b/pkg/apiserver/configuration/router.go @@ -22,7 +22,7 @@ import ( "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" ) -func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { +func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/configuration") endpoint.Use(auth.MWAuthRequired()) endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient)) diff --git a/pkg/apiserver/diagnose/diagnose.go b/pkg/apiserver/diagnose/diagnose.go index e339a99815..149caf83ae 100644 --- a/pkg/apiserver/diagnose/diagnose.go +++ b/pkg/apiserver/diagnose/diagnose.go @@ -57,7 +57,7 @@ func NewService(config *config.Config, tidbClient *tidb.Client, db *dbstore.DB, } } -func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { +func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/diagnose") endpoint.GET("/reports", auth.MWAuthRequired(), diff --git a/pkg/apiserver/info/info.go b/pkg/apiserver/info/info.go index 45b405210b..c298ca256c 100644 --- a/pkg/apiserver/info/info.go +++ b/pkg/apiserver/info/info.go @@ -44,7 +44,7 @@ func NewService(p ServiceParams) *Service { return &Service{params: p} } -func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { +func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/info") endpoint.GET("/info", s.infoHandler) endpoint.Use(auth.MWAuthRequired()) diff --git a/pkg/apiserver/logsearch/service.go b/pkg/apiserver/logsearch/service.go index ea48b169ee..d73c113531 100644 --- a/pkg/apiserver/logsearch/service.go +++ b/pkg/apiserver/logsearch/service.go @@ -72,7 +72,7 @@ func NewService(lc fx.Lifecycle, config *config.Config, db *dbstore.DB) *Service return service } -func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { +func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/logs") { endpoint.GET("/download", s.DownloadLogs) diff --git a/pkg/apiserver/metrics/metrics.go b/pkg/apiserver/metrics/metrics.go deleted file mode 100644 index d41c9da3f1..0000000000 --- a/pkg/apiserver/metrics/metrics.go +++ /dev/null @@ -1,131 +0,0 @@ -package metrics - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/gin-gonic/gin" - "github.com/joomcode/errorx" - "go.etcd.io/etcd/clientv3" - "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" -) - -var ( - ErrNS = errorx.NewNamespace("error.api.metrics") - ErrPrometheusNotFound = ErrNS.NewType("prometheus_not_found") - 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 -} - -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 - return nil - }, - }) - - return s -} - -func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { - endpoint := r.Group("/metrics") - endpoint.Use(auth.MWAuthRequired()) - endpoint.GET("/query", s.queryHandler) -} - -type QueryRequest struct { - StartTimeSec int `json:"start_time_sec" form:"start_time_sec"` - EndTimeSec int `json:"end_time_sec" form:"end_time_sec"` - StepSec int `json:"step_sec" form:"step_sec"` - Query string `json:"query" form:"query"` -} - -type QueryResponse struct { - Status string `json:"status"` - Data map[string]interface{} `json:"data"` -} - -// @Summary Query metrics -// @Description Query metrics in the given range -// @Param q query QueryRequest true "Query" -// @Success 200 {object} QueryResponse -// @Failure 401 {object} utils.APIError "Unauthorized failure" -// @Security JwtAuth -// @Router /metrics/query [get] -func (s *Service) queryHandler(c *gin.Context) { - var req QueryRequest - if err := c.ShouldBindQuery(&req); err != nil { - utils.MakeInvalidRequestErrorFromError(c, err) - return - } - - pi, err := topology.FetchPrometheusTopology(s.lifecycleCtx, s.params.EtcdClient) - if err != nil { - _ = c.Error(err) - return - } - if pi == nil { - _ = c.Error(ErrPrometheusNotFound.NewWithNoMessage()) - return - } - - params := url.Values{} - params.Add("query", req.Query) - params.Add("start", strconv.Itoa(req.StartTimeSec)) - params.Add("end", strconv.Itoa(req.EndTimeSec)) - params.Add("step", strconv.Itoa(req.StepSec)) - - uri := fmt.Sprintf("http://%s:%d/api/v1/query_range?%s", pi.IP, pi.Port, params.Encode()) - promReq, err := http.NewRequestWithContext(s.lifecycleCtx, http.MethodGet, uri, nil) - if err != nil { - _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to build Prometheus request")) - return - } - - promResp, err := s.params.HTTPClient.WithTimeout(defaultPromQueryTimeout).Do(promReq) - if err != nil { - _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to send requests to Prometheus")) - return - } - - defer promResp.Body.Close() - if promResp.StatusCode != http.StatusOK { - _ = c.Error(ErrPrometheusQueryFailed.New("failed to query Prometheus")) - return - } - - body, err := ioutil.ReadAll(promResp.Body) - if err != nil { - _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to read Prometheus query result")) - return - } - - c.Data(promResp.StatusCode, promResp.Header.Get("content-type"), body) -} diff --git a/pkg/apiserver/metrics/prom_resolve.go b/pkg/apiserver/metrics/prom_resolve.go new file mode 100644 index 0000000000..5c65b1762b --- /dev/null +++ b/pkg/apiserver/metrics/prom_resolve.go @@ -0,0 +1,188 @@ +// 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 metrics + +import ( + "bytes" + "encoding/json" + "fmt" + "net/url" + "strings" + "time" + + "github.com/pingcap-incubator/tidb-dashboard/pkg/utils/topology" +) + +const ( + promCacheTTL = time.Second * 5 +) + +type promAddressCacheEntity struct { + address string + cacheAt time.Time +} + +type pdServerConfig struct { + MetricStorage string `json:"metric-storage"` +} + +type pdConfig struct { + PdServer pdServerConfig `json:"pd-server"` +} + +// Check and normalize a Prometheus address supplied by user. +func normalizeCustomizedPromAddress(addr string) (string, error) { + if !strings.HasPrefix(addr, "http://") && !strings.HasPrefix(addr, "https://") { + addr = "http://" + addr + } + u, err := url.Parse(addr) + if err != nil { + return "", fmt.Errorf("invalid Prometheus address format: %v", err) + } + if len(u.Host) == 0 || len(u.Scheme) == 0 { + return "", fmt.Errorf("invalid Prometheus address format") + } + // Normalize the address, remove unnecessary parts. + addr = fmt.Sprintf("%s://%s", u.Scheme, u.Host) + return addr, nil +} + +// Resolve the customized Prometheus address in PD config. If it is not configured, empty address will be returned. +// The returned address must be valid. If an invalid Prometheus address is configured, errors will be returned. +func (s *Service) resolveCustomizedPromAddress(acceptInvalidAddr bool) (string, error) { + // Lookup "metric-storage" cluster config in PD. + data, err := s.params.PDClient.SendGetRequest("/config") + if err != nil { + return "", err + } + var config pdConfig + if err := json.Unmarshal(data, &config); err != nil { + return "", err + } + addr := config.PdServer.MetricStorage + if len(addr) > 0 { + if acceptInvalidAddr { + return addr, nil + } + // Verify whether address is valid. If not valid, throw error. + addr, err = normalizeCustomizedPromAddress(addr) + if err != nil { + return "", err + } + return addr, nil + } + return "", nil +} + +// Resolve the Prometheus address recorded by deployment tools in the `/topology` etcd namespace. +// If the address is not recorded (for example, when Prometheus is not deployed), empty address will be returned. +func (s *Service) resolveDeployedPromAddress() (string, error) { + pi, err := topology.FetchPrometheusTopology(s.lifecycleCtx, s.params.EtcdClient) + if err != nil { + return "", err + } + if pi == nil { + return "", nil + } + return fmt.Sprintf("http://%s:%d", pi.IP, pi.Port), nil +} + +// Resolve the final Prometheus address. When user has customized an address, this address is returned. Otherwise, +// address recorded by deployment tools will be returned. +// If neither custom address nor deployed address is available, empty address will be returned. +func (s *Service) resolveFinalPromAddress() (string, error) { + addr, err := s.resolveCustomizedPromAddress(false) + if err != nil { + return "", err + } + if addr != "" { + return addr, nil + } + addr, err = s.resolveDeployedPromAddress() + if err != nil { + return "", err + } + if addr != "" { + return addr, nil + } + return "", nil +} + +// Get the final Prometheus address from cache. If cache item is not valid, the address will be resolved from PD +// or etcd and then the cache will be updated. +func (s *Service) getPromAddressFromCache() (string, error) { + fn := func() (string, error) { + // Check whether cache is valid, and use the cache if possible. + if v := s.promAddressCache.Load(); v != nil { + entity := v.(*promAddressCacheEntity) + if entity.cacheAt.Add(promCacheTTL).After(time.Now()) { + return entity.address, nil + } + } + + // Cache is not valid, read from PD and etcd. + addr, err := s.resolveFinalPromAddress() + + if err != nil { + return "", err + } + + s.promAddressCache.Store(&promAddressCacheEntity{ + address: addr, + cacheAt: time.Now(), + }) + + return addr, nil + } + + resolveResult, err, _ := s.promRequestGroup.Do("any_key", func() (interface{}, error) { + return fn() + }) + if err != nil { + return "", err + } + return resolveResult.(string), nil +} + +// Set the customized Prometheus address. Address can be empty or a valid address like `http://host:port`. +// If address is set to empty, address from deployment tools will be used later. +func (s *Service) setCustomPromAddress(addr string) (string, error) { + var err error + if len(addr) > 0 { + addr, err = normalizeCustomizedPromAddress(addr) + if err != nil { + return "", err + } + } + + body := make(map[string]interface{}) + body["metric-storage"] = addr + bodyJSON, err := json.Marshal(&body) + if err != nil { + return "", err + } + + _, err = s.params.PDClient.SendPostRequest("/config", bytes.NewBuffer(bodyJSON)) + if err != nil { + return "", err + } + + // Invalidate cache immediately. + s.promAddressCache.Value.Store(&promAddressCacheEntity{ + address: addr, + cacheAt: time.Time{}, + }) + + return addr, nil +} diff --git a/pkg/apiserver/metrics/router.go b/pkg/apiserver/metrics/router.go new file mode 100644 index 0000000000..59a33611f3 --- /dev/null +++ b/pkg/apiserver/metrics/router.go @@ -0,0 +1,164 @@ +// 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 metrics + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user" + "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils" +) + +type QueryRequest struct { + StartTimeSec int `json:"start_time_sec" form:"start_time_sec"` + EndTimeSec int `json:"end_time_sec" form:"end_time_sec"` + StepSec int `json:"step_sec" form:"step_sec"` + Query string `json:"query" form:"query"` +} + +type QueryResponse struct { + Status string `json:"status"` + Data map[string]interface{} `json:"data"` +} + +func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { + endpoint := r.Group("/metrics") + endpoint.Use(auth.MWAuthRequired()) + endpoint.GET("/query", s.queryMetrics) + endpoint.GET("/prom_address", s.getPromAddressConfig) + endpoint.PUT("/prom_address", s.putCustomPromAddress) +} + +// @Summary Query metrics +// @Description Query metrics in the given range +// @Param q query QueryRequest true "Query" +// @Success 200 {object} QueryResponse +// @Failure 401 {object} utils.APIError "Unauthorized failure" +// @Security JwtAuth +// @Router /metrics/query [get] +func (s *Service) queryMetrics(c *gin.Context) { + var req QueryRequest + if err := c.ShouldBindQuery(&req); err != nil { + utils.MakeInvalidRequestErrorFromError(c, err) + return + } + + addr, err := s.getPromAddressFromCache() + if err != nil { + _ = c.Error(ErrLoadPrometheusAddressFailed.Wrap(err, "Load prometheus address failed")) + return + } + if addr == "" { + _ = c.Error(ErrPrometheusNotFound.New("Prometheus is not deployed in the cluster")) + return + } + + params := url.Values{} + params.Add("query", req.Query) + params.Add("start", strconv.Itoa(req.StartTimeSec)) + params.Add("end", strconv.Itoa(req.EndTimeSec)) + params.Add("step", strconv.Itoa(req.StepSec)) + + uri := fmt.Sprintf("%s/api/v1/query_range?%s", addr, params.Encode()) + promReq, err := http.NewRequestWithContext(s.lifecycleCtx, http.MethodGet, uri, nil) + if err != nil { + _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to build Prometheus request")) + return + } + + promResp, err := s.params.HTTPClient.WithTimeout(defaultPromQueryTimeout).Do(promReq) + if err != nil { + _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to send requests to Prometheus")) + return + } + + defer promResp.Body.Close() + if promResp.StatusCode != http.StatusOK { + _ = c.Error(ErrPrometheusQueryFailed.New("failed to query Prometheus")) + return + } + + body, err := ioutil.ReadAll(promResp.Body) + if err != nil { + _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to read Prometheus query result")) + return + } + + c.Data(promResp.StatusCode, promResp.Header.Get("content-type"), body) +} + +type GetPromAddressConfigResponse struct { + CustomizedAddr string `json:"customized_addr"` + DeployedAddr string `json:"deployed_addr"` +} + +// @ID metricsGetPromAddress +// @Summary Get the Prometheus address cluster config +// @Success 200 {object} GetPromAddressConfigResponse +// @Failure 401 {object} utils.APIError "Unauthorized failure" +// @Security JwtAuth +// @Router /metrics/prom_address [get] +func (s *Service) getPromAddressConfig(c *gin.Context) { + cAddr, err := s.resolveCustomizedPromAddress(true) + if err != nil { + _ = c.Error(err) + return + } + dAddr, err := s.resolveDeployedPromAddress() + if err != nil { + _ = c.Error(err) + return + } + c.JSON(http.StatusOK, GetPromAddressConfigResponse{ + CustomizedAddr: cAddr, + DeployedAddr: dAddr, + }) +} + +type PutCustomPromAddressRequest struct { + Addr string `json:"address"` +} + +type PutCustomPromAddressResponse struct { + NormalizedAddr string `json:"normalized_address"` +} + +// @ID metricsSetCustomPromAddress +// @Summary Set or clear the customized Prometheus address +// @Param request body PutCustomPromAddressRequest true "Request body" +// @Success 200 {object} PutCustomPromAddressResponse +// @Failure 401 {object} utils.APIError "Unauthorized failure" +// @Security JwtAuth +// @Router /metrics/prom_address [put] +func (s *Service) putCustomPromAddress(c *gin.Context) { + var req PutCustomPromAddressRequest + if err := c.ShouldBindJSON(&req); err != nil { + utils.MakeInvalidRequestErrorFromError(c, err) + return + } + addr, err := s.setCustomPromAddress(req.Addr) + if err != nil { + _ = c.Error(err) + return + } + c.JSON(http.StatusOK, PutCustomPromAddressResponse{ + NormalizedAddr: addr, + }) +} diff --git a/pkg/apiserver/metrics/service.go b/pkg/apiserver/metrics/service.go new file mode 100644 index 0000000000..91bedfd578 --- /dev/null +++ b/pkg/apiserver/metrics/service.go @@ -0,0 +1,67 @@ +// 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 metrics + +import ( + "context" + "time" + + "github.com/joomcode/errorx" + "go.etcd.io/etcd/clientv3" + "go.uber.org/atomic" + "go.uber.org/fx" + "golang.org/x/sync/singleflight" + + "github.com/pingcap-incubator/tidb-dashboard/pkg/httpc" + "github.com/pingcap-incubator/tidb-dashboard/pkg/pd" +) + +var ( + ErrNS = errorx.NewNamespace("error.api.metrics") + ErrLoadPrometheusAddressFailed = ErrNS.NewType("load_prom_address_failed") + ErrPrometheusNotFound = ErrNS.NewType("prom_not_found") + ErrPrometheusQueryFailed = ErrNS.NewType("prom_query_failed") +) + +const ( + defaultPromQueryTimeout = time.Second * 30 +) + +type ServiceParams struct { + fx.In + HTTPClient *httpc.Client + EtcdClient *clientv3.Client + PDClient *pd.Client +} + +type Service struct { + params ServiceParams + lifecycleCtx context.Context + + promRequestGroup singleflight.Group + promAddressCache atomic.Value +} + +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 + return nil + }, + }) + + return s +} diff --git a/pkg/apiserver/profiling/router.go b/pkg/apiserver/profiling/router.go index 97bb1db488..a09eca5b1c 100644 --- a/pkg/apiserver/profiling/router.go +++ b/pkg/apiserver/profiling/router.go @@ -30,7 +30,7 @@ import ( ) // Register register the handlers to the service. -func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { +func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/profiling") endpoint.GET("/group/list", auth.MWAuthRequired(), s.getGroupList) endpoint.POST("/group/start", auth.MWAuthRequired(), s.handleStartGroup) diff --git a/pkg/apiserver/queryeditor/service.go b/pkg/apiserver/queryeditor/service.go index 6c3e3c6db0..8665fb9a09 100644 --- a/pkg/apiserver/queryeditor/service.go +++ b/pkg/apiserver/queryeditor/service.go @@ -53,7 +53,7 @@ func NewService(lc fx.Lifecycle, p ServiceParams) *Service { return service } -func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { +func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/query_editor") endpoint.Use(auth.MWAuthRequired()) endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient)) diff --git a/pkg/apiserver/slowquery/service.go b/pkg/apiserver/slowquery/service.go index 6df8d5d6c2..624d57de9d 100644 --- a/pkg/apiserver/slowquery/service.go +++ b/pkg/apiserver/slowquery/service.go @@ -47,7 +47,7 @@ func NewService(p ServiceParams) *Service { return &Service{params: p} } -func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { +func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/slow_query") { endpoint.GET("/download", s.downloadHandler) diff --git a/pkg/apiserver/statement/service.go b/pkg/apiserver/statement/service.go index 7b25b3b16e..fce356bc85 100644 --- a/pkg/apiserver/statement/service.go +++ b/pkg/apiserver/statement/service.go @@ -48,7 +48,7 @@ func NewService(p ServiceParams) *Service { return &Service{params: p} } -func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { +func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/statements") { endpoint.GET("/download", s.downloadHandler) diff --git a/pkg/apiserver/user/auth.go b/pkg/apiserver/user/auth.go index 64ca155d3f..39bf30757b 100644 --- a/pkg/apiserver/user/auth.go +++ b/pkg/apiserver/user/auth.go @@ -245,7 +245,7 @@ func (s *AuthService) authSharingCodeForm(f *authenticateForm) (*utils.SessionUs return session, nil } -func Register(r *gin.RouterGroup, s *AuthService) { +func RegisterRouter(r *gin.RouterGroup, s *AuthService) { endpoint := r.Group("/user") endpoint.POST("/login", s.loginHandler) endpoint.POST("/share", s.MWAuthRequired(), s.shareSessionHandler) diff --git a/pkg/keyvisual/service.go b/pkg/keyvisual/service.go index ca8005bda0..aeb4e4fb53 100644 --- a/pkg/keyvisual/service.go +++ b/pkg/keyvisual/service.go @@ -113,7 +113,7 @@ func NewService( return s } -func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { +func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/keyvisual") endpoint.Use(auth.MWAuthRequired()) diff --git a/ui/dashboardApp/layout/main/Sider/index.tsx b/ui/dashboardApp/layout/main/Sider/index.tsx index b436e6f486..c00ec4523f 100644 --- a/ui/dashboardApp/layout/main/Sider/index.tsx +++ b/ui/dashboardApp/layout/main/Sider/index.tsx @@ -89,9 +89,9 @@ function Sider({ const menuItems = [ useAppMenuItem(registry, 'overview'), useAppMenuItem(registry, 'cluster_info'), - useAppMenuItem(registry, 'keyviz'), useAppMenuItem(registry, 'statement'), useAppMenuItem(registry, 'slow_query'), + useAppMenuItem(registry, 'keyviz'), useAppMenuItem(registry, 'system_report'), useAppMenuItem(registry, 'diagnose'), useAppMenuItem(registry, 'search_logs'), diff --git a/ui/dashboardApp/layout/signin/index.tsx b/ui/dashboardApp/layout/signin/index.tsx index 9e291de79d..c4bfe6208c 100644 --- a/ui/dashboardApp/layout/signin/index.tsx +++ b/ui/dashboardApp/layout/signin/index.tsx @@ -215,7 +215,7 @@ function TiDBSignInForm({ successRoute, onClickAlternative }) { } disabled /> + + + + )} + + + ) +} + function App() { const { t, i18n } = useTranslation() @@ -198,87 +335,95 @@ function App() { return ( - - - - - - - -
- - - -
-
- - - {error && } - {info && ( - - - - - - } - > - {info.version?.internal_version} - - - - - - } - > - {info.version?.build_git_hash} - - - } - > - {info.version?.build_time} - - - } - > - {info.version?.standalone} - - - - - - } + + + + + + + + + + + +
+ + + +
+
+ + + {error && } + {info && ( + + + + + + } + > + {info.version?.internal_version} + + + + + + } + > + {info.version?.build_git_hash} + + + } + > + {info.version?.build_time} + + + } + > + {info.version?.standalone} + + + + + + } + > + {info.version?.pd_version} + + + )} + + +
) } diff --git a/ui/lib/apps/UserProfile/translations/en.yaml b/ui/lib/apps/UserProfile/translations/en.yaml index 4f764c0592..efb6bd94ab 100644 --- a/ui/lib/apps/UserProfile/translations/en.yaml +++ b/ui/lib/apps/UserProfile/translations/en.yaml @@ -1,4 +1,16 @@ user_profile: + service_endpoints: + title: Service Endpoints + prometheus: + title: Prometheus Data Source + form: + deployed: Use deployed address + not_deployed: Prometheus is not deployed + custom: Use customized address + update: Update + cancel: Cancel + custom_form: + address: Customize Prometheus Address i18n: title: Language & Localization language: Language diff --git a/ui/lib/apps/UserProfile/translations/zh.yaml b/ui/lib/apps/UserProfile/translations/zh.yaml index 2a7687dcba..951f967c97 100644 --- a/ui/lib/apps/UserProfile/translations/zh.yaml +++ b/ui/lib/apps/UserProfile/translations/zh.yaml @@ -1,4 +1,16 @@ user_profile: + service_endpoints: + title: 服务端点 + prometheus: + title: Prometheus 数据源 + form: + deployed: 使用已部署的组件地址 + not_deployed: 未部署 Prometheus 组件 + custom: 使用自定义地址 + update: 更新 + cancel: 取消 + custom_form: + address: 自定义 Prometheus 数据源地址 i18n: title: 语言和本地化 language: 语言 diff --git a/ui/lib/components/AnimatedSkeleton/index.tsx b/ui/lib/components/AnimatedSkeleton/index.tsx index bb812dccf9..6b1b990c94 100644 --- a/ui/lib/components/AnimatedSkeleton/index.tsx +++ b/ui/lib/components/AnimatedSkeleton/index.tsx @@ -9,11 +9,13 @@ import styles from './index.module.less' export interface IAnimatedSkeletonProps extends SkeletonProps { showSkeleton?: boolean children?: React.ReactNode + style?: React.CSSProperties } function AnimatedSkeleton({ showSkeleton, children, + style, ...restProps }: IAnimatedSkeletonProps) { const [skeletonAppears, setSkeletonAppears] = useState(0) @@ -25,7 +27,7 @@ function AnimatedSkeleton({ }, [showSkeleton]) return ( -
+
{showSkeleton && (
{ + activeId: string +} + +export default function Blink({ + activeId, + children, + className, + ...restProps +}: IBlinkProps) { + const { blink } = useQueryParams() + + return ( +
+ {children} +
+ ) +} diff --git a/ui/lib/components/InstanceSelect/DropOverlay.tsx b/ui/lib/components/InstanceSelect/DropOverlay.tsx index b5071df6c5..68805b30f7 100644 --- a/ui/lib/components/InstanceSelect/DropOverlay.tsx +++ b/ui/lib/components/InstanceSelect/DropOverlay.tsx @@ -12,13 +12,12 @@ const groupProps = { onRenderHeader: (props) => , } -const containerStyle = { fontSize: '0.8rem' } - export interface IDropOverlayProps { selection: ISelection columns: IColumn[] items: IInstanceTableItem[] filterTableRef?: React.Ref + containerProps?: React.HTMLAttributes } function DropOverlay({ @@ -26,6 +25,7 @@ function DropOverlay({ columns, items, filterTableRef, + containerProps, }: IDropOverlayProps) { const { t } = useTranslation() const [keyword, setKeyword] = useState('') @@ -34,6 +34,18 @@ function DropOverlay({ return filterInstanceTable(items, keyword) }, [items, keyword]) + const { style: containerStyle, ...restContainerProps } = containerProps ?? {} + const finalContainerProps = useMemo(() => { + const style: React.CSSProperties = { + fontSize: '0.8rem', + ...containerStyle, + } + return { + style, + ...restContainerProps, + } as React.HTMLAttributes & Record + }, [containerStyle, restContainerProps]) + return ( ) diff --git a/ui/lib/components/InstanceSelect/TableWithFilter.tsx b/ui/lib/components/InstanceSelect/TableWithFilter.tsx index 66e3596b46..d6b4157048 100644 --- a/ui/lib/components/InstanceSelect/TableWithFilter.tsx +++ b/ui/lib/components/InstanceSelect/TableWithFilter.tsx @@ -21,8 +21,7 @@ export interface ITableWithFilterProps extends IDetailsListProps { onFilterChange?: (value: string) => void tableMaxHeight?: number tableWidth?: number - containerClassName?: string - containerStyle?: React.CSSProperties + containerProps?: React.HTMLAttributes } export interface ITableWithFilterRefProps { @@ -37,8 +36,7 @@ function TableWithFilter( onFilterChange, tableMaxHeight, tableWidth, - containerClassName, - containerStyle, + containerProps, ...restProps }: ITableWithFilterProps, ref: React.Ref @@ -73,11 +71,17 @@ function TableWithFilter( [containerState.height, tableMaxHeight, tableWidth] ) + const { + className: containerClassName, + style: containerStyle, + ...containerRestProps + } = containerProps ?? {} + return (
void enableTiFlash?: boolean defaultSelectAll?: boolean + dropContainerProps?: React.HTMLAttributes } export interface IInstanceSelectRefProps { @@ -85,6 +86,7 @@ function InstanceSelect( const { enableTiFlash, defaultSelectAll, + dropContainerProps, value, // only to exclude from restProps onChange, // only to exclude from restProps ...restProps @@ -243,9 +245,10 @@ function InstanceSelect( items={tableItems} selection={selection.current} filterTableRef={filterTableRef} + containerProps={dropContainerProps} /> ), - [columns, tableItems] + [columns, tableItems, dropContainerProps] ) const handleOpened = useCallback(() => { diff --git a/ui/lib/components/MetricChart/index.tsx b/ui/lib/components/MetricChart/index.tsx index 98f1df4949..4ce0e97bb0 100644 --- a/ui/lib/components/MetricChart/index.tsx +++ b/ui/lib/components/MetricChart/index.tsx @@ -19,9 +19,48 @@ import client from '@lib/client' import { AnimatedSkeleton, Card } from '@lib/components' import { useBatchClientRequest } from '@lib/utils/useClientRequest' import ErrorBar from '../ErrorBar' +import { addTranslationResource } from '@lib/utils/i18n' +import { Link } from 'react-router-dom' +import { useTranslation } from 'react-i18next' export type GraphType = 'bar' | 'line' +const translations = { + en: { + error: { + api: { + metrics: { + prom_not_found: + 'Prometheus is not deployed in the cluster. Metrics are unavailable.', + }, + }, + }, + components: { + metricChart: { + changePromButton: 'Change Prometheus Source', + }, + }, + }, + zh: { + error: { + api: { + metrics: { + prom_not_found: '集群中未部署 Prometheus 组件,监控不可用。', + }, + }, + }, + components: { + metricChart: { + changePromButton: '修改 Prometheus 源', + }, + }, + }, +} + +for (const key in translations) { + addTranslationResource(key, translations[key]) +} + export interface ISeries { query: string name: string @@ -70,6 +109,7 @@ export default function MetricChart({ type, }: IMetricChartProps) { const timeParams = useRef(getTimeParams()) + const { t } = useTranslation() const { isLoading, data, error, sendRequest } = useBatchClientRequest( series.map((s) => (reqConfig) => @@ -214,7 +254,7 @@ export default function MetricChart({ let inner if (showSkeleton) { - inner =
+ inner = null } else if ( _.every( _.zip(data, error), @@ -223,7 +263,12 @@ export default function MetricChart({ ) { inner = (
- + + + + {t('components.metricChart.changePromButton')} + +
) } else { @@ -249,7 +294,9 @@ export default function MetricChart({ return ( - {inner} + + {inner} + ) } diff --git a/ui/lib/components/MultiSelect/DropOverlay.tsx b/ui/lib/components/MultiSelect/DropOverlay.tsx index 3e1fb57374..49952292ba 100644 --- a/ui/lib/components/MultiSelect/DropOverlay.tsx +++ b/ui/lib/components/MultiSelect/DropOverlay.tsx @@ -6,7 +6,9 @@ import TableWithFilter, { } from '../InstanceSelect/TableWithFilter' import { IItem } from '.' -const containerStyle = { fontSize: '0.8rem' } +const containerProps: React.HTMLAttributes = { + style: { fontSize: '0.8rem' }, +} export interface IDropOverlayProps { selection: ISelection @@ -50,7 +52,7 @@ function DropOverlay({ tableWidth={250} columns={columns} items={filteredItems} - containerStyle={containerStyle} + containerProps={containerProps} ref={filterTableRef} /> ) diff --git a/ui/lib/components/index.ts b/ui/lib/components/index.ts index 51eb2676e8..d5b58766fd 100644 --- a/ui/lib/components/index.ts +++ b/ui/lib/components/index.ts @@ -52,6 +52,8 @@ export * from './ErrorBar' export { default as ErrorBar } from './ErrorBar' export * from './AppearAnimate' export { default as AppearAnimate } from './AppearAnimate' +export * from './Blink' +export { default as Blink } from './Blink' export { default as LanguageDropdown } from './LanguageDropdown' export { default as ParamsPageWrapper } from './ParamsPageWrapper' diff --git a/ui/lib/utils/useQueryParams.ts b/ui/lib/utils/useQueryParams.ts index 94d9be31ed..4718326ee7 100644 --- a/ui/lib/utils/useQueryParams.ts +++ b/ui/lib/utils/useQueryParams.ts @@ -2,6 +2,8 @@ import { useMemo } from 'react' import { useLocation } from 'react-router' export default function useQueryParams() { + // Note: seems that history.location can be outdated sometimes. + const { search } = useLocation() const params = useMemo(() => { diff --git a/ui/lib/utils/wdyr.ts b/ui/lib/utils/wdyr.ts index 3d79bfde63..50003057ed 100644 --- a/ui/lib/utils/wdyr.ts +++ b/ui/lib/utils/wdyr.ts @@ -3,8 +3,5 @@ import React from 'react' if (process.env.NODE_ENV === 'development') { console.log('Development mode, enable render trackers') const whyDidYouRender = require('@welldone-software/why-did-you-render') - whyDidYouRender(React, { - trackAllPureComponents: true, - logOwnerReasons: true, - }) + whyDidYouRender(React) } diff --git a/ui/package.json b/ui/package.json index 503655afe7..68c2ec3e5c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -21,6 +21,7 @@ "dayjs": "^1.8.31", "echarts": "^4.8.0", "echarts-for-react": "^2.0.16", + "history": "^5.0.0", "i18next": "^19.6.3", "i18next-browser-languagedetector": "^5.0.0", "lodash": "^4.17.19", diff --git a/ui/tests/e2e/_config.ts b/ui/tests/e2e/_config.ts new file mode 100644 index 0000000000..384c05c1aa --- /dev/null +++ b/ui/tests/e2e/_config.ts @@ -0,0 +1,4 @@ +export const SERVER_URL = + (process.env.SERVER_URL || 'http://localhost:3001/dashboard') + '#' +export const LOGIN_URL = SERVER_URL + '/signin' +export const OVERVIEW_URL = SERVER_URL + '/overview' diff --git a/ui/tests/e2e/_preset.js b/ui/tests/e2e/_preset.js new file mode 100644 index 0000000000..b65d085471 --- /dev/null +++ b/ui/tests/e2e/_preset.js @@ -0,0 +1,4 @@ +const ts_preset = require('ts-jest/jest-preset') +const puppeteer_preset = require('jest-puppeteer/jest-preset') + +module.exports = Object.assign(ts_preset, puppeteer_preset) diff --git a/ui/tests/e2e/_setup.js b/ui/tests/e2e/_setup.js new file mode 100644 index 0000000000..719a473b6e --- /dev/null +++ b/ui/tests/e2e/_setup.js @@ -0,0 +1 @@ +jest.setTimeout(10000) diff --git a/ui/tests/e2e/login.test.ts b/ui/tests/e2e/login.test.ts deleted file mode 100644 index 5d92870853..0000000000 --- a/ui/tests/e2e/login.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import puppeteer from 'puppeteer' -import ppExpect from 'expect-puppeteer' -import { LOGIN_URL, OVERVIEW_URL, PUPPETEER_CONFIG } from './test_config' - -describe('Login', () => { - let browser - beforeAll(async () => { - browser = await puppeteer.launch(PUPPETEER_CONFIG) - }) - - afterAll(() => { - browser.close() - }) - - it( - 'should login fail by incorrect password', - async () => { - const page = await browser.newPage() - await page.goto(LOGIN_URL) - - await ppExpect(page).toFill('input#tidb_signin_password', 'any') - await ppExpect(page).toClick('button#signin_btn') - - const failReason = await page.waitForSelector( - 'form#tidb_signin div[data-e2e="password"] div:last-child' - ) - const content = await failReason.evaluate((n) => n.innerText) - console.log('fail reason:', content) - expect(content).toContain('TiDB authentication failed') - }, - 10 * 1000 - ) - - it( - 'should login success by correct password', - async () => { - const page = await browser.newPage() - await page.goto(LOGIN_URL) - - const title = await page.title() - expect(title).toBe('TiDB Dashboard') - - const loginBtn = await page.waitForSelector('button#signin_btn') - await Promise.all([page.waitForNavigation(), loginBtn.click()]) - const url = await page.url() - expect(url).toBe(OVERVIEW_URL) - }, - 10 * 1000 - ) -}) diff --git a/ui/tests/e2e/search_log.test.ts b/ui/tests/e2e/search_log.test.ts index 8a81f03ca2..e8ab6635c8 100644 --- a/ui/tests/e2e/search_log.test.ts +++ b/ui/tests/e2e/search_log.test.ts @@ -1,92 +1,39 @@ -import puppeteer from 'puppeteer' -import ppExpect from 'expect-puppeteer' -import { LOGIN_URL, PUPPETEER_CONFIG } from './test_config' +import 'expect-puppeteer' +import { do_sign_in } from './utils/sign_in' describe('Search Logs', () => { - let browser - beforeAll(async () => { - browser = await puppeteer.launch(PUPPETEER_CONFIG) - }) - - afterAll(() => { - browser.close() - }) - it( 'should search correct logs', async () => { - const page = await browser.newPage() - - // login - await page.goto(LOGIN_URL) - await ppExpect(page).toClick('button#signin_btn') - - // jump to search logs page - await page.waitForSelector('a#search_logs') - const searchLogsLink = await page.$('a#search_logs') - await searchLogsLink.click() + await do_sign_in() - // this fails randomly and high possibility, says can't find "a#search_logs" element - // await ppExpect(page).toClick('a#search_logs') + await Promise.all([page.waitForNavigation(), page.click('a#search_logs')]) - // find search form - const searchForm = await page.waitForSelector('form#search_form') + // Fill keyword + await expect(page).toFill('[data-e2e="log_search_keywords"]', 'Welcome') - // choose time range - await ppExpect(searchForm).toClick( - 'button[data-e2e="timerange-selector"]' + // Deselect PD instance + await page.click('[data-e2e="log_search_instances"]') + await expect(page).toClick( + '[data-e2e="log_search_instances_drop"] .ms-GroupHeader-title', + { + text: 'PD', + } ) - const secondsOf1Hour = 60 * 60 - await ppExpect(page).toClick( - `div[data-e2e="common-timeranges"] div[data-e2e="timerange-${secondsOf1Hour}"]` - ) - // to hide dropdown - await ppExpect(searchForm).toClick( - 'button[data-e2e="timerange-selector"]' - ) - - // set log level to INFO - await ppExpect(searchForm).toClick('#logLevel') - await ppExpect(page).toClick('div[data-e2e="level_2"]') - - // select TiDB component - // https://stackoverflow.com/questions/59882543/how-to-wait-for-a-button-to-be-enabled-and-click-with-puppeteer - await page.waitForSelector('div#instances input:not([disabled])') - await ppExpect(searchForm).toClick('div#instances') - // components selector dropdown is a DOM node with absolute position - // and its parent is body, failed to add id or data-e2e to it - // cancel select PD and TiKV, and only remain TiDB - await ppExpect(page).toClick('div[data-e2e="table-with-filter"] span', { - text: 'PD', - }) - await ppExpect(page).toClick('div[data-e2e="table-with-filter"] span', { - text: 'TiKV', - }) - // 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') - - // check search result - let logsTable = await page.waitForSelector( - 'div[data-e2e="search-result"] div[role="presentation"]:first-child' - ) - const url = await page.url() - console.log('current url:', url) - let content = await logsTable.evaluate((node) => node.innerText) - console.log(content) - - logsTable = await page.waitForSelector( - 'div[data-e2e="search-result"] div[role="presentation"]:last-child' + await page.click('[data-e2e="log_search_instances"]') + + // Start search + await page.click('[data-e2e="log_search_submit"]') + + await page.waitForSelector('[data-e2e="log_search_result"]') + await page.waitForFunction( + `document + .querySelector('[data-e2e="log_search_result"]') + .innerText + .includes("Welcome to TiDB")`, + { timeout: 5000 } ) - content = await logsTable.evaluate((node) => node.innerText) - expect(content).toContain('Welcome to TiDB') - expect(content.includes('Welcome to TiKV')).toBe(false) }, - 25 * 1000 + 30 * 1000 ) }) diff --git a/ui/tests/e2e/sign_in.test.ts b/ui/tests/e2e/sign_in.test.ts new file mode 100644 index 0000000000..5e20fae316 --- /dev/null +++ b/ui/tests/e2e/sign_in.test.ts @@ -0,0 +1,28 @@ +import 'expect-puppeteer' +import { do_sign_in } from './utils/sign_in' +import { LOGIN_URL, OVERVIEW_URL } from './_config' + +describe('Sign In', () => { + it('should fail to sign in using incorrect password', async () => { + await page.goto(LOGIN_URL) + + await expect(page).toFill( + '[data-e2e="signin_password_input"]', + 'incorrect_password' + ) + await expect(page).toClick('[data-e2e="signin_submit"]') + await page.waitForFunction( + `document + .querySelector('[data-e2e="signin_password_form_item"]') + .innerText + .includes("TiDB authentication failed")`, + { timeout: 5000 } + ) + }) + + it('should sign in using correct password', async () => { + await do_sign_in() + const url = await page.url() + expect(url).toBe(OVERVIEW_URL) + }) +}) diff --git a/ui/tests/e2e/test_config.ts b/ui/tests/e2e/test_config.ts deleted file mode 100644 index 9b6259a084..0000000000 --- a/ui/tests/e2e/test_config.ts +++ /dev/null @@ -1,12 +0,0 @@ -export let SERVER_URL = `${ - process.env.SERVER_URL || 'http://localhost:3001/dashboard' -}#` -export const LOGIN_URL = SERVER_URL + '/signin' -export const OVERVIEW_URL = SERVER_URL + '/overview' - -export const PUPPETEER_CONFIG = process.env.CI - ? undefined - : { - headless: false, - slowMo: 80, - } diff --git a/ui/tests/e2e/utils/sign_in.ts b/ui/tests/e2e/utils/sign_in.ts new file mode 100644 index 0000000000..08cfe07a9b --- /dev/null +++ b/ui/tests/e2e/utils/sign_in.ts @@ -0,0 +1,10 @@ +import { LOGIN_URL } from '../_config' + +export async function do_sign_in() { + await page.goto(LOGIN_URL) + + await Promise.all([ + page.waitForNavigation(), + page.click('[data-e2e="signin_submit"]'), + ]) +} diff --git a/ui/tests/jest-puppeteer.config.js b/ui/tests/jest-puppeteer.config.js new file mode 100644 index 0000000000..c52e6f49c5 --- /dev/null +++ b/ui/tests/jest-puppeteer.config.js @@ -0,0 +1,5 @@ +module.exports = { + launch: { + headless: process.env.HEADLESS !== 'false', + }, +} diff --git a/ui/tests/jest.config.js b/ui/tests/jest.config.js new file mode 100644 index 0000000000..1fb0901893 --- /dev/null +++ b/ui/tests/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + preset: './e2e/_preset.js', + setupFilesAfterEnv: ['expect-puppeteer', './e2e/_setup.js'], +} diff --git a/ui/tests/jestconfig.json b/ui/tests/jestconfig.json deleted file mode 100644 index cf5b2377b4..0000000000 --- a/ui/tests/jestconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "transform": { - "^.+\\.(t|j)sx?$": "ts-jest" - }, - "setupFilesAfterEnv": ["expect-puppeteer"] -} diff --git a/ui/tests/package.json b/ui/tests/package.json index 1c98eeb3c2..f37afc0cef 100644 --- a/ui/tests/package.json +++ b/ui/tests/package.json @@ -3,14 +3,16 @@ "version": "1.0.0", "license": "MIT", "scripts": { - "test": "jest --config jestconfig.json" + "test": "jest --runInBand" }, "devDependencies": { "@types/expect-puppeteer": "^4.4.0", "@types/jest": "^25.1.4", + "@types/jest-environment-puppeteer": "^4.4.0", "@types/puppeteer": "^2.0.1", "expect-puppeteer": "^4.4.0", "jest": "^25.1.0", + "jest-puppeteer": "^4.4.0", "puppeteer": "^2.1.1", "ts-jest": "^25.2.1", "typescript": "^3.7.4" diff --git a/ui/tests/yarn.lock b/ui/tests/yarn.lock index 6fd9c984ad..07c428d3d2 100644 --- a/ui/tests/yarn.lock +++ b/ui/tests/yarn.lock @@ -209,6 +209,38 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@hapi/address@2.x.x": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" + integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== + +"@hapi/bourne@1.x.x": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-1.3.2.tgz#0a7095adea067243ce3283e1b56b8a8f453b242a" + integrity sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA== + +"@hapi/hoek@8.x.x", "@hapi/hoek@^8.3.0": + version "8.5.1" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" + integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow== + +"@hapi/joi@^15.0.3": + version "15.1.1" + resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7" + integrity sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ== + dependencies: + "@hapi/address" "2.x.x" + "@hapi/bourne" "1.x.x" + "@hapi/hoek" "8.x.x" + "@hapi/topo" "3.x.x" + +"@hapi/topo@3.x.x": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29" + integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ== + dependencies: + "@hapi/hoek" "^8.3.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b" @@ -277,6 +309,16 @@ "@jest/types" "^25.1.0" jest-mock "^25.1.0" +"@jest/environment@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" + integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== + dependencies: + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + "@jest/fake-timers@^25.1.0": version "25.1.0" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-25.1.0.tgz#a1e0eff51ffdbb13ee81f35b52e0c1c11a350ce8" @@ -288,6 +330,18 @@ jest-util "^25.1.0" lolex "^5.0.0" +"@jest/fake-timers@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" + integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== + dependencies: + "@jest/types" "^26.6.2" + "@sinonjs/fake-timers" "^6.0.1" + "@types/node" "*" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-util "^26.6.2" + "@jest/reporters@^25.1.0": version "25.1.0" resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-25.1.0.tgz#9178ecf136c48f125674ac328f82ddea46e482b0" @@ -373,6 +427,17 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" +"@jest/types@>=24 && <=26", "@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@jest/types@^25.1.0": version "25.1.0" resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.1.0.tgz#b26831916f0d7c381e11dbb5e103a72aed1b4395" @@ -390,6 +455,13 @@ dependencies: type-detect "4.0.8" +"@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@types/babel__core@^7.1.0": version "7.1.6" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.6.tgz#16ff42a5ae203c9af1c6e190ed1f30f83207b610" @@ -456,6 +528,22 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/istanbul-reports@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" + integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest-environment-puppeteer@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@types/jest-environment-puppeteer/-/jest-environment-puppeteer-4.4.0.tgz#8d343035934610accdbfd4582e765823b948aa94" + integrity sha512-BjJWUmaui6CZE449y/xGVPPvOcNwlHZXxWekv38kZqu1Pda+Jn90pKaxWtxM5NAC2HaUEabsCWlTeHiJvno/hg== + dependencies: + "@jest/types" ">=24 && <=26" + "@types/puppeteer" "*" + jest-environment-node ">=24 && <=26" + "@types/jest@*", "@types/jest@^25.1.4": version "25.1.4" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.1.4.tgz#9e9f1e59dda86d3fd56afce71d1ea1b331f6f760" @@ -486,6 +574,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/stack-utils@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" + integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -832,7 +925,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -849,6 +942,14 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" @@ -873,6 +974,17 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +clone-deep@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.2.4.tgz#4e73dd09e9fb971cc38670c5dced9c1896481cc6" + integrity sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY= + dependencies: + for-own "^0.1.3" + is-plain-object "^2.0.1" + kind-of "^3.0.2" + lazy-cache "^1.0.3" + shallow-clone "^0.1.2" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -922,6 +1034,16 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +commander@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" + integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== + +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -954,6 +1076,11 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +core-js@^2.6.5: + version "2.6.11" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" + integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -996,6 +1123,14 @@ cssstyle@^2.0.0: dependencies: cssom "~0.3.6" +cwd@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/cwd/-/cwd-0.10.0.tgz#172400694057c22a13b0cf16162c7e4b7a7fe567" + integrity sha1-FyQAaUBXwioTsM8WFix+S3p/5Wc= + dependencies: + find-pkg "^0.1.2" + fs-exists-sync "^0.1.0" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -1143,6 +1278,11 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escodegen@^1.11.1: version "1.14.1" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" @@ -1222,6 +1362,13 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +expand-tilde@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" + integrity sha1-C4HrqJflo9MdHD0QL48BRB5VlEk= + dependencies: + os-homedir "^1.0.1" + expect-puppeteer@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/expect-puppeteer/-/expect-puppeteer-4.4.0.tgz#1c948af08acdd6c8cbdb7f90e617f44d86888886" @@ -1339,6 +1486,30 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +find-file-up@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/find-file-up/-/find-file-up-0.1.3.tgz#cf68091bcf9f300a40da411b37da5cce5a2fbea0" + integrity sha1-z2gJG8+fMApA2kEbN9pczlovvqA= + dependencies: + fs-exists-sync "^0.1.0" + resolve-dir "^0.1.0" + +find-pkg@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/find-pkg/-/find-pkg-0.1.2.tgz#1bdc22c06e36365532e2a248046854b9788da557" + integrity sha1-G9wiwG42NlUy4qJIBGhUuXiNpVc= + dependencies: + find-file-up "^0.1.2" + +find-process@^1.4.3: + version "1.4.4" + resolved "https://registry.yarnpkg.com/find-process/-/find-process-1.4.4.tgz#52820561162fda0d1feef9aed5d56b3787f0fd6e" + integrity sha512-rRSuT1LE4b+BFK588D2V8/VG9liW0Ark1XJgroxZXI0LtwmQJOb490DvDYvbm+Hek9ETFzTutGfJ90gumITPhQ== + dependencies: + chalk "^4.0.0" + commander "^5.1.0" + debug "^4.1.1" + find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -1347,11 +1518,23 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -for-in@^1.0.2: +for-in@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" + integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE= + +for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= +for-own@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4= + dependencies: + for-in "^1.0.1" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -1373,6 +1556,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +fs-exists-sync@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" + integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0= + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1436,6 +1624,24 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +global-modules@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" + integrity sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0= + dependencies: + global-prefix "^0.1.4" + is-windows "^0.2.0" + +global-prefix@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f" + integrity sha1-jTvGuNo8qBEqFg2NSW/wRiv+948= + dependencies: + homedir-polyfill "^1.0.0" + ini "^1.3.4" + is-windows "^0.2.0" + which "^1.2.12" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -1446,6 +1652,11 @@ graceful-fs@^4.2.3: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== +graceful-fs@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -1517,6 +1728,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +homedir-polyfill@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + html-encoding-sniffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" @@ -1584,6 +1802,11 @@ inherits@2, inherits@^2.0.3, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ini@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -1603,7 +1826,7 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-buffer@^1.1.5: +is-buffer@^1.0.2, is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -1691,7 +1914,7 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-plain-object@^2.0.3, is-plain-object@^2.0.4: +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== @@ -1727,6 +1950,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-windows@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" + integrity sha1-3hqm1j6indJIc3tp8f+LgALSEIw= + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -1859,6 +2087,19 @@ jest-config@^25.1.0: pretty-format "^25.1.0" realpath-native "^1.1.0" +jest-dev-server@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/jest-dev-server/-/jest-dev-server-4.4.0.tgz#557113faae2877452162696aa94c1e44491ab011" + integrity sha512-STEHJ3iPSC8HbrQ3TME0ozGX2KT28lbT4XopPxUm2WimsX3fcB3YOptRh12YphQisMhfqNSNTZUmWyT3HEXS2A== + dependencies: + chalk "^3.0.0" + cwd "^0.10.0" + find-process "^1.4.3" + prompts "^2.3.0" + spawnd "^4.4.0" + tree-kill "^1.2.2" + wait-on "^3.3.0" + jest-diff@^25.1.0: version "25.1.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.1.0.tgz#58b827e63edea1bc80c1de952b80cec9ac50e1ad" @@ -1899,6 +2140,18 @@ jest-environment-jsdom@^25.1.0: jest-util "^25.1.0" jsdom "^15.1.1" +"jest-environment-node@>=24 && <=26": + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" + integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + jest-util "^26.6.2" + jest-environment-node@^25.1.0: version "25.1.0" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-25.1.0.tgz#797bd89b378cf0bd794dc8e3dca6ef21126776db" @@ -1910,6 +2163,16 @@ jest-environment-node@^25.1.0: jest-mock "^25.1.0" jest-util "^25.1.0" +jest-environment-puppeteer@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/jest-environment-puppeteer/-/jest-environment-puppeteer-4.4.0.tgz#d82a37e0e0c51b63cc6b15dea101d53967508860" + integrity sha512-iV8S8+6qkdTM6OBR/M9gKywEk8GDSOe05hspCs5D8qKSwtmlUfdtHfB4cakdc68lC6YfK3AUsLirpfgodCHjzQ== + dependencies: + chalk "^3.0.0" + cwd "^0.10.0" + jest-dev-server "^4.4.0" + merge-deep "^3.0.2" + jest-get-type@^25.1.0: version "25.1.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.1.0.tgz#1cfe5fc34f148dc3a8a3b7275f6b9ce9e2e8a876" @@ -1988,6 +2251,21 @@ jest-message-util@^25.1.0: slash "^3.0.0" stack-utils "^1.0.1" +jest-message-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" + integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" + micromatch "^4.0.2" + pretty-format "^26.6.2" + slash "^3.0.0" + stack-utils "^2.0.2" + jest-mock@^25.1.0: version "25.1.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.1.0.tgz#411d549e1b326b7350b2e97303a64715c28615fd" @@ -1995,11 +2273,27 @@ jest-mock@^25.1.0: dependencies: "@jest/types" "^25.1.0" +jest-mock@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" + integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-pnp-resolver@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== +jest-puppeteer@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/jest-puppeteer/-/jest-puppeteer-4.4.0.tgz#4b906e638a5e3782ed865e7b673c82047b85952e" + integrity sha512-ZaiCTlPZ07B9HW0erAWNX6cyzBqbXMM7d2ugai4epBDKpKvRDpItlRQC6XjERoJELKZsPziFGS0OhhUvTvQAXA== + dependencies: + expect-puppeteer "^4.4.0" + jest-environment-puppeteer "^4.4.0" + jest-regex-util@^25.1.0: version "25.1.0" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-25.1.0.tgz#efaf75914267741838e01de24da07b2192d16d87" @@ -2115,6 +2409,18 @@ jest-util@^25.1.0: is-ci "^2.0.0" mkdirp "^0.5.1" +jest-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" + integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + micromatch "^4.0.2" + jest-validate@^25.1.0: version "25.1.0" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-25.1.0.tgz#1469fa19f627bb0a9a98e289f3e9ab6a668c732a" @@ -2243,6 +2549,13 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +kind-of@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" + integrity sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU= + dependencies: + is-buffer "^1.0.2" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -2272,6 +2585,16 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +lazy-cache@^0.2.3: + version "0.2.7" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" + integrity sha1-f+3fLctu23fRHvHRF6tf/fCrG2U= + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -2345,6 +2668,15 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +merge-deep@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2" + integrity sha512-T7qC8kg4Zoti1cFd8Cr0M+qaZfOwjlPDEdZIIPPB2JZctjaPM4fX+i7HOId69tAti2fvO6X5ldfYUONDODsrkA== + dependencies: + arr-union "^3.1.0" + clone-deep "^0.2.4" + kind-of "^3.0.2" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -2424,6 +2756,14 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + mkdirp@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -2609,6 +2949,11 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" +os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + p-each-series@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" @@ -2643,6 +2988,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= + parse5@5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" @@ -2732,6 +3082,16 @@ pretty-format@^25.1.0: ansi-styles "^4.0.0" react-is "^16.12.0" +pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -2750,6 +3110,14 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.4" +prompts@^2.3.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" + integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + proxy-from-env@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" @@ -2799,6 +3167,11 @@ react-is@^16.12.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" + integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== + readable-stream@^2.2.2: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" @@ -2901,6 +3274,14 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-dir@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" + integrity sha1-shklmlYC+sXFxJatiUpujMQwJh4= + dependencies: + expand-tilde "^1.2.2" + global-modules "^0.2.3" + resolve-from@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" @@ -2947,6 +3328,11 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== +rx@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" + integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I= + safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" @@ -3021,6 +3407,16 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +shallow-clone@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" + integrity sha1-WQnodLp3EG1zrEFM/sH/yofZcGA= + dependencies: + is-extendable "^0.1.1" + kind-of "^2.0.1" + lazy-cache "^0.2.3" + mixin-object "^2.0.1" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -3055,7 +3451,7 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= -sisteransi@^1.0.4: +sisteransi@^1.0.4, sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== @@ -3134,6 +3530,16 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +spawnd@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/spawnd/-/spawnd-4.4.0.tgz#bb52c5b34a22e3225ae1d3acb873b2cd58af0886" + integrity sha512-jLPOfB6QOEgMOQY15Z6+lwZEhH3F5ncXxIaZ7WHPIapwNNLyjrs61okj3VJ3K6tmP5TZ6cO0VAu9rEY4MD4YQg== + dependencies: + exit "^0.1.2" + signal-exit "^3.0.2" + tree-kill "^1.2.2" + wait-port "^0.2.7" + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -3166,6 +3572,13 @@ stack-utils@^1.0.1: resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== +stack-utils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" + integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw== + dependencies: + escape-string-regexp "^2.0.0" + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -3363,6 +3776,11 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + ts-jest@^25.2.1: version "25.2.1" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.2.1.tgz#49bf05da26a8b7fbfbc36b4ae2fcdc2fef35c85d" @@ -3514,6 +3932,26 @@ w3c-xmlserializer@^1.1.2: webidl-conversions "^4.0.2" xml-name-validator "^3.0.0" +wait-on@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-3.3.0.tgz#9940981d047a72a9544a97b8b5fca45b2170a082" + integrity sha512-97dEuUapx4+Y12aknWZn7D25kkjMk16PbWoYzpSdA8bYpVfS6hpl2a2pOWZ3c+Tyt3/i4/pglyZctG3J4V1hWQ== + dependencies: + "@hapi/joi" "^15.0.3" + core-js "^2.6.5" + minimist "^1.2.0" + request "^2.88.0" + rx "^4.1.0" + +wait-port@^0.2.7: + version "0.2.9" + resolved "https://registry.yarnpkg.com/wait-port/-/wait-port-0.2.9.tgz#3905cf271b5dbe37a85c03b85b418b81cb24ee55" + integrity sha512-hQ/cVKsNqGZ/UbZB/oakOGFqic00YAMM5/PEj3Bt4vKarv2jWIWzDbqlwT94qMs/exAQAsvMOq99sZblV92zxQ== + dependencies: + chalk "^2.4.2" + commander "^3.0.2" + debug "^4.1.1" + walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" @@ -3552,7 +3990,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@^1.2.9, which@^1.3.1: +which@^1.2.12, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== diff --git a/ui/yarn.lock b/ui/yarn.lock index 109ffea9a8..ee61ac900b 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -9209,6 +9209,13 @@ history@5.0.0-beta.9: dependencies: "@babel/runtime" "^7.7.6" +history@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08" + integrity sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg== + dependencies: + "@babel/runtime" "^7.7.6" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"