From 47b2fafb6f7e8f61b7228e82006e81527e693b0f Mon Sep 17 00:00:00 2001 From: Breezewish Date: Mon, 16 Nov 2020 17:05:57 +0800 Subject: [PATCH 1/7] wip Signed-off-by: Breezewish --- go.mod | 1 + go.sum | 1 + pkg/apiserver/apiserver.go | 24 +-- pkg/apiserver/clusterinfo/service.go | 2 +- pkg/apiserver/configuration/router.go | 2 +- pkg/apiserver/diagnose/diagnose.go | 2 +- pkg/apiserver/info/info.go | 2 +- pkg/apiserver/logsearch/service.go | 2 +- pkg/apiserver/metrics/metrics.go | 131 ------------- pkg/apiserver/metrics/prom_resolve.go | 184 +++++++++++++++++++ pkg/apiserver/metrics/router.go | 152 +++++++++++++++ pkg/apiserver/metrics/service.go | 67 +++++++ pkg/apiserver/profiling/router.go | 2 +- pkg/apiserver/queryeditor/service.go | 2 +- pkg/apiserver/slowquery/service.go | 2 +- pkg/apiserver/statement/statement.go | 2 +- pkg/apiserver/user/auth.go | 2 +- pkg/keyvisual/service.go | 2 +- ui/lib/apps/UserProfile/index.tsx | 15 ++ ui/lib/apps/UserProfile/translations/en.yaml | 2 + ui/lib/apps/UserProfile/translations/zh.yaml | 2 + 21 files changed, 447 insertions(+), 154 deletions(-) delete mode 100644 pkg/apiserver/metrics/metrics.go create mode 100644 pkg/apiserver/metrics/prom_resolve.go create mode 100644 pkg/apiserver/metrics/router.go create mode 100644 pkg/apiserver/metrics/service.go 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..4bef89cf52 --- /dev/null +++ b/pkg/apiserver/metrics/prom_resolve.go @@ -0,0 +1,184 @@ +// 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" + "time" + + "github.com/pingcap/log" + "go.uber.org/zap" + + "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) { + u, err := url.Parse(addr) + if err != nil { + return "", fmt.Errorf("parse Prometheus address failed: %v", err) + } + // 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) { + log.Info("getPromAddressFromCache") + + fn := func() (string, error) { + log.Info("getPromAddressFromCache -> promRequestGroup.Do func") + + // 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()) { + log.Info("getPromAddressFromCache -> promRequestGroup.Do func -> Load Cache Success") + return entity.address, nil + } else { + log.Info("getPromAddressFromCache -> promRequestGroup.Do func -> Load Cache Success, but TTL passed") + } + } + + log.Info("getPromAddressFromCache -> promRequestGroup.Do func -> Load Cache Fail !!") + + // Cache is not valid, read from PD and etcd. + addr, err := s.resolveFinalPromAddress() + log.Info("resolvePromAddressDirect", zap.Any("addr", addr)) + + 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) 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)) + return err +} diff --git a/pkg/apiserver/metrics/router.go b/pkg/apiserver/metrics/router.go new file mode 100644 index 0000000000..a9224c41dd --- /dev/null +++ b/pkg/apiserver/metrics/router.go @@ -0,0 +1,152 @@ +// 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"` +} + +// @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"` +} + +// @Summary Set or clear the customized Prometheus address +// @Param request body PutCustomPromAddressRequest true "Request body" +// @Success 200 {object} utils.APIEmptyResponse +// @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 + } + + c.JSON(http.StatusOK, utils.APIEmptyResponse{}) +} 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 9f04fcd304..5449e442c7 100644 --- a/pkg/apiserver/slowquery/service.go +++ b/pkg/apiserver/slowquery/service.go @@ -37,7 +37,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.Use(auth.MWAuthRequired()) endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient)) diff --git a/pkg/apiserver/statement/statement.go b/pkg/apiserver/statement/statement.go index 951828a160..1800224a23 100644 --- a/pkg/apiserver/statement/statement.go +++ b/pkg/apiserver/statement/statement.go @@ -54,7 +54,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/lib/apps/UserProfile/index.tsx b/ui/lib/apps/UserProfile/index.tsx index cd15944b43..07616da192 100644 --- a/ui/lib/apps/UserProfile/index.tsx +++ b/ui/lib/apps/UserProfile/index.tsx @@ -206,6 +206,21 @@ function App() { + +
+ + {/* */} + +
+
diff --git a/ui/lib/apps/UserProfile/translations/en.yaml b/ui/lib/apps/UserProfile/translations/en.yaml index 4f764c0592..e02a671887 100644 --- a/ui/lib/apps/UserProfile/translations/en.yaml +++ b/ui/lib/apps/UserProfile/translations/en.yaml @@ -1,4 +1,6 @@ user_profile: + service_endpoints: + title: Service Endpoints 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..2fc3cfd051 100644 --- a/ui/lib/apps/UserProfile/translations/zh.yaml +++ b/ui/lib/apps/UserProfile/translations/zh.yaml @@ -1,4 +1,6 @@ user_profile: + service_endpoints: + title: 服务端点 i18n: title: 语言和本地化 language: 语言 From 76ff5e0f8de61ee89192d731118e1df51bc73508 Mon Sep 17 00:00:00 2001 From: Breezewish Date: Sat, 21 Nov 2020 17:35:13 +0800 Subject: [PATCH 2/7] Update form --- pkg/apiserver/metrics/prom_resolve.go | 20 +++- pkg/apiserver/metrics/router.go | 18 +++- ui/lib/apps/UserProfile/index.tsx | 130 +++++++++++++++++++++++--- ui/lib/utils/wdyr.ts | 5 +- 4 files changed, 146 insertions(+), 27 deletions(-) diff --git a/pkg/apiserver/metrics/prom_resolve.go b/pkg/apiserver/metrics/prom_resolve.go index 4bef89cf52..0e24968d4f 100644 --- a/pkg/apiserver/metrics/prom_resolve.go +++ b/pkg/apiserver/metrics/prom_resolve.go @@ -18,6 +18,7 @@ import ( "encoding/json" "fmt" "net/url" + "strings" "time" "github.com/pingcap/log" @@ -45,9 +46,15 @@ type pdConfig struct { // 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("parse Prometheus address failed: %v", err) + 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) @@ -163,12 +170,12 @@ func (s *Service) getPromAddressFromCache() (string, error) { // 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) error { +func (s *Service) setCustomPromAddress(addr string) (string, error) { var err error if len(addr) > 0 { addr, err = normalizeCustomizedPromAddress(addr) if err != nil { - return err + return "", err } } @@ -176,9 +183,12 @@ func (s *Service) setCustomPromAddress(addr string) error { body["metric-storage"] = addr bodyJSON, err := json.Marshal(&body) if err != nil { - return err + return "", err } _, err = s.params.PDClient.SendPostRequest("/config", bytes.NewBuffer(bodyJSON)) - return err + if err != nil { + return "", err + } + return addr, nil } diff --git a/pkg/apiserver/metrics/router.go b/pkg/apiserver/metrics/router.go index a9224c41dd..59a33611f3 100644 --- a/pkg/apiserver/metrics/router.go +++ b/pkg/apiserver/metrics/router.go @@ -109,6 +109,7 @@ type GetPromAddressConfigResponse struct { 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" @@ -135,9 +136,14 @@ 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} utils.APIEmptyResponse +// @Success 200 {object} PutCustomPromAddressResponse // @Failure 401 {object} utils.APIError "Unauthorized failure" // @Security JwtAuth // @Router /metrics/prom_address [put] @@ -147,6 +153,12 @@ func (s *Service) putCustomPromAddress(c *gin.Context) { utils.MakeInvalidRequestErrorFromError(c, err) return } - - c.JSON(http.StatusOK, utils.APIEmptyResponse{}) + addr, err := s.setCustomPromAddress(req.Addr) + if err != nil { + _ = c.Error(err) + return + } + c.JSON(http.StatusOK, PutCustomPromAddressResponse{ + NormalizedAddr: addr, + }) } diff --git a/ui/lib/apps/UserProfile/index.tsx b/ui/lib/apps/UserProfile/index.tsx index 07616da192..ef28248d57 100644 --- a/ui/lib/apps/UserProfile/index.tsx +++ b/ui/lib/apps/UserProfile/index.tsx @@ -7,8 +7,11 @@ import { Alert, Divider, Tooltip, + Radio, + Input, + Typography, } from 'antd' -import React, { useCallback, useState } from 'react' +import React, { useCallback, useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { CopyToClipboard } from 'react-copy-to-clipboard' import { @@ -36,6 +39,7 @@ import client from '@lib/client' import { getValueFormat } from '@baurine/grafana-value-formats' import ReactMarkdown from 'react-markdown' +const DEFAULT_FORM_ITEM_STYLE = { width: 200 } const SHARE_SESSION_EXPIRY_HOURS = [0.25, 0.5, 1, 2, 3, 6, 12, 24] function ShareSessionButton() { @@ -177,6 +181,111 @@ function ShareSessionButton() { ) } +function PrometheusAddressForm() { + const { t } = useTranslation() + const [isChanged, setIsChanged] = useState(false) + const [isPosting, setIsPosting] = useState(false) + const handleValuesChange = useCallback(() => setIsChanged(true), []) + const { error, isLoading, data } = useClientRequest((reqConfig) => + client.getInstance().metricsGetPromAddress(reqConfig) + ) + const isInitialLoad = useRef(true) + const [form] = Form.useForm() + + useEffect(() => { + if (data && isInitialLoad.current) { + isInitialLoad.current = false + form.setFieldsValue({ + sourceType: + (data.customized_addr?.length ?? 0) > 0 ? 'custom' : 'deployment', + customAddr: data.customized_addr, + }) + } + }, [data, form]) + + const handleFinish = useCallback( + async (values) => { + let address = '' + if (values.sourceType === 'custom') { + address = values.customAddr || '' + } + try { + setIsPosting(true) + const resp = await client.getInstance().metricsSetCustomPromAddress({ + address, + }) + form.setFieldsValue({ + customAddr: resp?.data?.normalized_address ?? '', + }) + setIsChanged(false) + } finally { + setIsPosting(false) + } + }, + [form] + ) + + return ( + + + + + + {error && } + + + Use deployed address + + {(data?.deployed_addr?.length ?? 0) > 0 && + `(${data!.deployed_addr})`} + {data && data.deployed_addr?.length === 0 && ( + + (Prometheus is not deployed) + + )} + + + + Custom + + + + + + {(f) => + f.getFieldValue('sourceType') === 'custom' && ( + + + + ) + } + + {isChanged && ( + + + + )} + + ) +} + function App() { const { t, i18n } = useTranslation() @@ -207,24 +316,15 @@ function App() {
-
- - {/* */} - -
+
- {_.map(ALL_LANGUAGES, (name, key) => { return ( 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) } From 8c47cb9c08b93ea0c4373b68ae52d8e54e3dcb0d Mon Sep 17 00:00:00 2001 From: Breezewish Date: Tue, 24 Nov 2020 01:33:56 +0800 Subject: [PATCH 3/7] Add blink effect --- ui/lib/apps/UserProfile/index.tsx | 131 ++++++++++++---------- ui/lib/components/Blink/index.module.less | 17 +++ ui/lib/components/Blink/index.tsx | 29 +++++ ui/lib/components/index.ts | 2 + ui/lib/utils/useQueryParams.ts | 13 ++- ui/package.json | 1 + ui/yarn.lock | 7 ++ 7 files changed, 138 insertions(+), 62 deletions(-) create mode 100644 ui/lib/components/Blink/index.module.less create mode 100644 ui/lib/components/Blink/index.tsx diff --git a/ui/lib/apps/UserProfile/index.tsx b/ui/lib/apps/UserProfile/index.tsx index ef28248d57..60fc6f74ea 100644 --- a/ui/lib/apps/UserProfile/index.tsx +++ b/ui/lib/apps/UserProfile/index.tsx @@ -30,6 +30,7 @@ import { TextWithInfo, Pre, ErrorBar, + Blink, } from '@lib/components' import * as auth from '@lib/utils/auth' import { ALL_LANGUAGES } from '@lib/utils/i18n' @@ -190,6 +191,7 @@ function PrometheusAddressForm() { client.getInstance().metricsGetPromAddress(reqConfig) ) const isInitialLoad = useRef(true) + const initialForm = useRef(null) // Used for "Cancel" behaviour const [form] = Form.useForm() useEffect(() => { @@ -200,6 +202,7 @@ function PrometheusAddressForm() { (data.customized_addr?.length ?? 0) > 0 ? 'custom' : 'deployment', customAddr: data.customized_addr, }) + initialForm.current = { ...form.getFieldsValue() } } }, [data, form]) @@ -214,9 +217,9 @@ function PrometheusAddressForm() { const resp = await client.getInstance().metricsSetCustomPromAddress({ address, }) - form.setFieldsValue({ - customAddr: resp?.data?.normalized_address ?? '', - }) + const customAddr = resp?.data?.normalized_address ?? '' + form.setFieldsValue({ customAddr }) + initialForm.current = { ...form.getFieldsValue() } setIsChanged(false) } finally { setIsPosting(false) @@ -225,64 +228,74 @@ function PrometheusAddressForm() { [form] ) + const handleCancel = useCallback(() => { + form.setFieldsValue({ ...initialForm.current }) + setIsChanged(false) + }, [form]) + return ( - - - - - - {error && } - - - Use deployed address - - {(data?.deployed_addr?.length ?? 0) > 0 && - `(${data!.deployed_addr})`} - {data && data.deployed_addr?.length === 0 && ( - - (Prometheus is not deployed) - - )} - - - - Custom - - - - - - {(f) => - f.getFieldValue('sourceType') === 'custom' && ( - - - - ) - } - - {isChanged && ( - - + + + + + + + {error && } + + + Use deployed address + + {(data?.deployed_addr?.length ?? 0) > 0 && + `(${data!.deployed_addr})`} + {data && data.deployed_addr?.length === 0 && ( + + (Prometheus is not deployed) + + )} + + + + Custom + + + + + + {(f) => + f.getFieldValue('sourceType') === 'custom' && ( + + + + ) + } - )} - + {isChanged && ( + + + + + + + )} + + ) } diff --git a/ui/lib/components/Blink/index.module.less b/ui/lib/components/Blink/index.module.less new file mode 100644 index 0000000000..e5c3382429 --- /dev/null +++ b/ui/lib/components/Blink/index.module.less @@ -0,0 +1,17 @@ +@import '~antd/lib/style/themes/default.less'; + +.blinkActive { + animation: blink 0.7s 2 ease-in-out; +} + +@keyframes blink { + 0% { + background-color: transparent; + } + 50% { + background-color: rgba(@gold-5, 0.4); + } + 100% { + background-color: transparent; + } +} diff --git a/ui/lib/components/Blink/index.tsx b/ui/lib/components/Blink/index.tsx new file mode 100644 index 0000000000..cd02933761 --- /dev/null +++ b/ui/lib/components/Blink/index.tsx @@ -0,0 +1,29 @@ +import useQueryParams from '@lib/utils/useQueryParams' +import React from 'react' +import cx from 'classnames' + +import styles from './index.module.less' + +export interface IBlinkProps extends React.HTMLAttributes { + activeId: string +} + +export default function Blink({ + activeId, + children, + className, + ...restProps +}: IBlinkProps) { + const { blink } = useQueryParams() + + return ( +
+ {children} +
+ ) +} 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..a1a43387ae 100644 --- a/ui/lib/utils/useQueryParams.ts +++ b/ui/lib/utils/useQueryParams.ts @@ -1,8 +1,15 @@ -import { useMemo } from 'react' -import { useLocation } from 'react-router' +import { useEffect, useMemo, useState } from 'react' +import { createHashHistory } from 'history'; + +const history = createHashHistory(); export default function useQueryParams() { - const { search } = useLocation() + // Instead of using `useLocation`, this hook access history directly, so that it can work + // without a `` in the tree. + const [search, setSearch] = useState(history.location.search) + useEffect(() => history.listen(({location}) => { + setSearch(location.search) + }), []); const params = useMemo(() => { const searchParams = new URLSearchParams(search) 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/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" From c7dcea0a7298cd328620fe54e767709fa90215c8 Mon Sep 17 00:00:00 2001 From: Breezewish Date: Tue, 24 Nov 2020 11:44:29 +0800 Subject: [PATCH 4/7] Translations Signed-off-by: Breezewish --- ui/lib/apps/UserProfile/index.tsx | 28 ++++++++--- ui/lib/apps/UserProfile/translations/en.yaml | 10 ++++ ui/lib/apps/UserProfile/translations/zh.yaml | 10 ++++ ui/lib/components/AnimatedSkeleton/index.tsx | 4 +- ui/lib/components/MetricChart/index.tsx | 53 ++++++++++++++++++-- 5 files changed, 94 insertions(+), 11 deletions(-) diff --git a/ui/lib/apps/UserProfile/index.tsx b/ui/lib/apps/UserProfile/index.tsx index 60fc6f74ea..7e9c4fa3ab 100644 --- a/ui/lib/apps/UserProfile/index.tsx +++ b/ui/lib/apps/UserProfile/index.tsx @@ -244,26 +244,36 @@ function PrometheusAddressForm() { {error && } - Use deployed address + + {t( + 'user_profile.service_endpoints.prometheus.form.deployed' + )} + {(data?.deployed_addr?.length ?? 0) > 0 && `(${data!.deployed_addr})`} {data && data.deployed_addr?.length === 0 && ( - (Prometheus is not deployed) + ( + {t( + 'user_profile.service_endpoints.prometheus.form.not_deployed' + )} + ) )} - Custom + + {t('user_profile.service_endpoints.prometheus.form.custom')} + @@ -273,7 +283,9 @@ function PrometheusAddressForm() { f.getFieldValue('sourceType') === 'custom' && ( + - )} diff --git a/ui/lib/apps/UserProfile/translations/en.yaml b/ui/lib/apps/UserProfile/translations/en.yaml index e02a671887..5a2f3c39fa 100644 --- a/ui/lib/apps/UserProfile/translations/en.yaml +++ b/ui/lib/apps/UserProfile/translations/en.yaml @@ -1,6 +1,16 @@ user_profile: service_endpoints: title: Service Endpoints + prometheus: + title: Prometheus Source + form: + deployed: Use deployed address + not_deployed: Prometheus is not deployed + custom: Custom + update: Update + cancel: Cancel + custom_form: + address: Custom 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 2fc3cfd051..c3726d1bf3 100644 --- a/ui/lib/apps/UserProfile/translations/zh.yaml +++ b/ui/lib/apps/UserProfile/translations/zh.yaml @@ -1,6 +1,16 @@ user_profile: service_endpoints: title: 服务端点 + prometheus: + title: Prometheus 数据源 + form: + deployed: 使用已部署的 Prometheus 地址 + 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 && (
(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} + ) } From e1a0163a6785a8c8a858b2d224ed3dcfb853e4f2 Mon Sep 17 00:00:00 2001 From: Breezewish Date: Tue, 24 Nov 2020 13:34:21 +0800 Subject: [PATCH 5/7] Fix some problems Signed-off-by: Breezewish --- pkg/apiserver/metrics/prom_resolve.go | 24 ++++++++------------- ui/dashboardApp/layout/main/Sider/index.tsx | 2 +- ui/lib/utils/useQueryParams.ts | 14 +++++++----- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/pkg/apiserver/metrics/prom_resolve.go b/pkg/apiserver/metrics/prom_resolve.go index 0e24968d4f..5c65b1762b 100644 --- a/pkg/apiserver/metrics/prom_resolve.go +++ b/pkg/apiserver/metrics/prom_resolve.go @@ -21,9 +21,6 @@ import ( "strings" "time" - "github.com/pingcap/log" - "go.uber.org/zap" - "github.com/pingcap-incubator/tidb-dashboard/pkg/utils/topology" ) @@ -51,10 +48,10 @@ func normalizeCustomizedPromAddress(addr string) (string, error) { } u, err := url.Parse(addr) if err != nil { - return "", fmt.Errorf("Invalid Prometheus address format: %v", err) + 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") + return "", fmt.Errorf("invalid Prometheus address format") } // Normalize the address, remove unnecessary parts. addr = fmt.Sprintf("%s://%s", u.Scheme, u.Host) @@ -125,27 +122,17 @@ func (s *Service) resolveFinalPromAddress() (string, error) { // 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) { - log.Info("getPromAddressFromCache") - fn := func() (string, error) { - log.Info("getPromAddressFromCache -> promRequestGroup.Do func") - // 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()) { - log.Info("getPromAddressFromCache -> promRequestGroup.Do func -> Load Cache Success") return entity.address, nil - } else { - log.Info("getPromAddressFromCache -> promRequestGroup.Do func -> Load Cache Success, but TTL passed") } } - log.Info("getPromAddressFromCache -> promRequestGroup.Do func -> Load Cache Fail !!") - // Cache is not valid, read from PD and etcd. addr, err := s.resolveFinalPromAddress() - log.Info("resolvePromAddressDirect", zap.Any("addr", addr)) if err != nil { return "", err @@ -190,5 +177,12 @@ func (s *Service) setCustomPromAddress(addr string) (string, error) { if err != nil { return "", err } + + // Invalidate cache immediately. + s.promAddressCache.Value.Store(&promAddressCacheEntity{ + address: addr, + cacheAt: time.Time{}, + }) + return addr, nil } 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/lib/utils/useQueryParams.ts b/ui/lib/utils/useQueryParams.ts index a1a43387ae..f614ac8ebb 100644 --- a/ui/lib/utils/useQueryParams.ts +++ b/ui/lib/utils/useQueryParams.ts @@ -1,15 +1,19 @@ import { useEffect, useMemo, useState } from 'react' -import { createHashHistory } from 'history'; +import { createHashHistory } from 'history' -const history = createHashHistory(); +const history = createHashHistory() export default function useQueryParams() { // Instead of using `useLocation`, this hook access history directly, so that it can work // without a `` in the tree. const [search, setSearch] = useState(history.location.search) - useEffect(() => history.listen(({location}) => { - setSearch(location.search) - }), []); + useEffect( + () => + history.listen(({ location }) => { + setSearch(location.search) + }), + [] + ) const params = useMemo(() => { const searchParams = new URLSearchParams(search) From 4ac99ccdf95bc925dc641e0710beb558d7691d6f Mon Sep 17 00:00:00 2001 From: Breezewish Date: Tue, 24 Nov 2020 13:42:08 +0800 Subject: [PATCH 6/7] Polish some translations Signed-off-by: Breezewish --- ui/lib/apps/UserProfile/translations/en.yaml | 6 +++--- ui/lib/apps/UserProfile/translations/zh.yaml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/lib/apps/UserProfile/translations/en.yaml b/ui/lib/apps/UserProfile/translations/en.yaml index 5a2f3c39fa..efb6bd94ab 100644 --- a/ui/lib/apps/UserProfile/translations/en.yaml +++ b/ui/lib/apps/UserProfile/translations/en.yaml @@ -2,15 +2,15 @@ user_profile: service_endpoints: title: Service Endpoints prometheus: - title: Prometheus Source + title: Prometheus Data Source form: deployed: Use deployed address not_deployed: Prometheus is not deployed - custom: Custom + custom: Use customized address update: Update cancel: Cancel custom_form: - address: Custom Prometheus Address + 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 c3726d1bf3..951f967c97 100644 --- a/ui/lib/apps/UserProfile/translations/zh.yaml +++ b/ui/lib/apps/UserProfile/translations/zh.yaml @@ -4,9 +4,9 @@ user_profile: prometheus: title: Prometheus 数据源 form: - deployed: 使用已部署的 Prometheus 地址 + deployed: 使用已部署的组件地址 not_deployed: 未部署 Prometheus 组件 - custom: 自定义地址 + custom: 使用自定义地址 update: 更新 cancel: 取消 custom_form: From a465adef4c7a3540cdcc52ffd032169fac64786c Mon Sep 17 00:00:00 2001 From: Breezewish Date: Tue, 24 Nov 2020 17:22:32 +0800 Subject: [PATCH 7/7] Revert useQueryParams change Signed-off-by: Breezewish --- CONTRIBUTING.md | 104 +--- ui/dashboardApp/layout/signin/index.tsx | 5 +- .../SearchLogs/components/SearchHeader.tsx | 9 +- .../SearchLogs/components/SearchResult.tsx | 2 +- ui/lib/apps/UserProfile/index.tsx | 175 +++---- .../components/InstanceSelect/DropOverlay.tsx | 18 +- .../InstanceSelect/TableWithFilter.tsx | 14 +- ui/lib/components/InstanceSelect/index.tsx | 5 +- ui/lib/components/MultiSelect/DropOverlay.tsx | 6 +- ui/lib/utils/useQueryParams.ts | 19 +- ui/tests/e2e/_config.ts | 4 + ui/tests/e2e/_preset.js | 4 + ui/tests/e2e/_setup.js | 1 + ui/tests/e2e/login.test.ts | 50 -- ui/tests/e2e/search_log.test.ts | 105 +--- ui/tests/e2e/sign_in.test.ts | 28 ++ ui/tests/e2e/test_config.ts | 12 - ui/tests/e2e/utils/sign_in.ts | 10 + ui/tests/jest-puppeteer.config.js | 5 + ui/tests/jest.config.js | 4 + ui/tests/jestconfig.json | 6 - ui/tests/package.json | 4 +- ui/tests/yarn.lock | 450 +++++++++++++++++- 23 files changed, 689 insertions(+), 351 deletions(-) create mode 100644 ui/tests/e2e/_config.ts create mode 100644 ui/tests/e2e/_preset.js create mode 100644 ui/tests/e2e/_setup.js delete mode 100644 ui/tests/e2e/login.test.ts create mode 100644 ui/tests/e2e/sign_in.test.ts delete mode 100644 ui/tests/e2e/test_config.ts create mode 100644 ui/tests/e2e/utils/sign_in.ts create mode 100644 ui/tests/jest-puppeteer.config.js create mode 100644 ui/tests/jest.config.js delete mode 100644 ui/tests/jestconfig.json 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/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 /> - - - - - - -
- - - -
-
- - - {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/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/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/utils/useQueryParams.ts b/ui/lib/utils/useQueryParams.ts index f614ac8ebb..4718326ee7 100644 --- a/ui/lib/utils/useQueryParams.ts +++ b/ui/lib/utils/useQueryParams.ts @@ -1,19 +1,10 @@ -import { useEffect, useMemo, useState } from 'react' -import { createHashHistory } from 'history' - -const history = createHashHistory() +import { useMemo } from 'react' +import { useLocation } from 'react-router' export default function useQueryParams() { - // Instead of using `useLocation`, this hook access history directly, so that it can work - // without a `` in the tree. - const [search, setSearch] = useState(history.location.search) - useEffect( - () => - history.listen(({ location }) => { - setSearch(location.search) - }), - [] - ) + // Note: seems that history.location can be outdated sometimes. + + const { search } = useLocation() const params = useMemo(() => { const searchParams = new URLSearchParams(search) 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==