From 0b28d05ea14b29ecbdbc938dc21ce4cd50cf53df Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Mon, 12 Feb 2024 13:57:08 -0300 Subject: [PATCH 01/13] start dashboard if agent mode is set as dashboard --- agent/config/flags.go | 5 ++-- agent/runner/runner.go | 40 +++++++++++++++++++++++++++ agent/runner/runstrategy_dashboard.go | 19 +++++++++++++ agent/runner/runstrategy_desktop.go | 30 +------------------- 4 files changed, 63 insertions(+), 31 deletions(-) create mode 100644 agent/runner/runstrategy_dashboard.go diff --git a/agent/config/flags.go b/agent/config/flags.go index 068de3a81f..0ff39b7289 100644 --- a/agent/config/flags.go +++ b/agent/config/flags.go @@ -3,8 +3,9 @@ package config type Mode string const ( - Mode_Desktop Mode = "desktop" - Mode_Verbose Mode = "verbose" + Mode_Dashboard Mode = "dashboard" + Mode_Desktop Mode = "desktop" + Mode_Verbose Mode = "verbose" ) type Flags struct { diff --git a/agent/runner/runner.go b/agent/runner/runner.go index 6b753efd10..f8c3d83c9a 100644 --- a/agent/runner/runner.go +++ b/agent/runner/runner.go @@ -2,9 +2,11 @@ package runner import ( "context" + "errors" "fmt" "os" + "github.com/golang-jwt/jwt" agentConfig "github.com/kubeshop/tracetest/agent/config" "github.com/kubeshop/tracetest/agent/ui" @@ -100,6 +102,10 @@ func (s *Runner) StartAgent(ctx context.Context, endpoint, agentApiKey, uiEndpoi cfg.APIKey = agentApiKey } + if s.mode == agentConfig.Mode_Dashboard { + return s.RunDashboardStrategy(ctx, cfg, uiEndpoint) + } + if s.mode == agentConfig.Mode_Desktop { return s.RunDesktopStrategy(ctx, cfg, uiEndpoint) } @@ -110,3 +116,37 @@ func (s *Runner) StartAgent(ctx context.Context, endpoint, agentApiKey, uiEndpoi func enableLogging(logLevel string) bool { return os.Getenv("TRACETEST_DEV") == "true" && logLevel == "debug" } + +func (s *Runner) authenticate(ctx context.Context, cfg agentConfig.Config) (*Session, jwt.MapClaims, error) { + isStarted := false + session := &Session{} + + var err error + + for !isStarted { + session, err = StartSession(ctx, cfg, nil, s.logger) + if err != nil && errors.Is(err, ErrOtlpServerStart) { + s.ui.Error("Tracetest Agent binds to the OpenTelemetry ports 4317 and 4318 which are used to receive trace information from your system. The agent tried to bind to these ports, but failed.") + shouldRetry := s.ui.Enter("Please stop the process currently listening on these ports and press enter to try again.") + + if !shouldRetry { + s.ui.Finish() + return nil, nil, err + } + + continue + } + + if err != nil { + return nil, nil, err + } + + isStarted = true + } + + claims, err := config.GetTokenClaims(session.Token) + if err != nil { + return nil, nil, err + } + return session, claims, nil +} diff --git a/agent/runner/runstrategy_dashboard.go b/agent/runner/runstrategy_dashboard.go new file mode 100644 index 0000000000..55ab90ec63 --- /dev/null +++ b/agent/runner/runstrategy_dashboard.go @@ -0,0 +1,19 @@ +package runner + +import ( + "context" + + agentConfig "github.com/kubeshop/tracetest/agent/config" + "github.com/kubeshop/tracetest/agent/ui/dashboard" +) + +func (s *Runner) RunDashboardStrategy(ctx context.Context, cfg agentConfig.Config, uiEndpoint string) error { + session, _, err := s.authenticate(ctx, cfg) + if err != nil { + return err + } + + defer session.Close() + + return dashboard.StartDashboard(ctx) +} diff --git a/agent/runner/runstrategy_desktop.go b/agent/runner/runstrategy_desktop.go index 9805850680..139c9a013c 100644 --- a/agent/runner/runstrategy_desktop.go +++ b/agent/runner/runstrategy_desktop.go @@ -2,11 +2,9 @@ package runner import ( "context" - "errors" "fmt" agentConfig "github.com/kubeshop/tracetest/agent/config" - "github.com/kubeshop/tracetest/cli/config" consoleUI "github.com/kubeshop/tracetest/agent/ui" ) @@ -14,33 +12,7 @@ import ( func (s *Runner) RunDesktopStrategy(ctx context.Context, cfg agentConfig.Config, uiEndpoint string) error { s.ui.Infof("Starting Agent with name %s...", cfg.Name) - isStarted := false - session := &Session{} - - var err error - - for !isStarted { - session, err = StartSession(ctx, cfg, nil, s.logger) - if err != nil && errors.Is(err, ErrOtlpServerStart) { - s.ui.Error("Tracetest Agent binds to the OpenTelemetry ports 4317 and 4318 which are used to receive trace information from your system. The agent tried to bind to these ports, but failed.") - shouldRetry := s.ui.Enter("Please stop the process currently listening on these ports and press enter to try again.") - - if !shouldRetry { - s.ui.Finish() - return err - } - - continue - } - - if err != nil { - return err - } - - isStarted = true - } - - claims, err := config.GetTokenClaims(session.Token) + session, claims, err := s.authenticate(ctx, cfg) if err != nil { return err } From 525a4705fc94af7f947bcc67c12de1eb03d2280c Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Mon, 12 Feb 2024 16:13:15 -0300 Subject: [PATCH 02/13] update environment and collector statistics --- agent/collector/collector.go | 23 +++++++++- agent/collector/ingester.go | 17 +++++++ agent/config/config.go | 6 +++ agent/runner/runner.go | 1 + agent/runner/runstrategy_dashboard.go | 18 +++++++- agent/ui/dashboard/components/header.go | 58 +++++++++++++++++++----- agent/ui/dashboard/dashboard.go | 4 +- agent/ui/dashboard/events/events.go | 5 ++ agent/ui/dashboard/main/main.go | 8 +++- agent/ui/dashboard/models/environment.go | 7 +++ agent/ui/dashboard/sensors/sensor.go | 8 ++++ 11 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 agent/ui/dashboard/models/environment.go diff --git a/agent/collector/collector.go b/agent/collector/collector.go index 7989a728d3..2faecd7af0 100644 --- a/agent/collector/collector.go +++ b/agent/collector/collector.go @@ -9,11 +9,14 @@ import ( "time" "github.com/kubeshop/tracetest/agent/event" + "github.com/kubeshop/tracetest/agent/ui/dashboard/sensors" "github.com/kubeshop/tracetest/server/otlp" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) +var activeCollector Collector + type Config struct { HTTPPort int GRPCPort int @@ -48,6 +51,12 @@ func WithObserver(observer event.Observer) CollectorOption { } } +func WithSensor(sensor sensors.Sensor) CollectorOption { + return func(ric *remoteIngesterConfig) { + ric.sensor = sensor + } +} + type collector struct { grpcServer stoppable httpServer stoppable @@ -60,6 +69,8 @@ type Collector interface { Statistics() Statistics ResetStatistics() + + SetSensor(sensors.Sensor) } // Stop implements stoppable. @@ -76,12 +87,21 @@ func (c *collector) ResetStatistics() { c.ingester.ResetStatistics() } +func (c *collector) SetSensor(sensor sensors.Sensor) { + c.ingester.SetSensor(sensor) +} + +func GetActiveCollector() Collector { + return activeCollector +} + func Start(ctx context.Context, config Config, tracer trace.Tracer, opts ...CollectorOption) (Collector, error) { ingesterConfig := remoteIngesterConfig{ URL: config.RemoteServerURL, Token: config.RemoteServerToken, logger: zap.NewNop(), observer: event.NewNopObserver(), + sensor: sensors.NewSensor(), } for _, opt := range opts { @@ -115,7 +135,8 @@ func Start(ctx context.Context, config Config, tracer trace.Tracer, opts ...Coll return nil, fmt.Errorf("could not start HTTP OTLP listener: %w", err) } - return &collector{grpcServer: grpcServer, httpServer: httpServer, ingester: ingester}, nil + activeCollector = &collector{grpcServer: grpcServer, httpServer: httpServer, ingester: ingester} + return activeCollector, nil } func onProcessTermination(callback func()) { diff --git a/agent/collector/ingester.go b/agent/collector/ingester.go index 56b98e8704..5a7e5ca287 100644 --- a/agent/collector/ingester.go +++ b/agent/collector/ingester.go @@ -8,6 +8,8 @@ import ( "time" "github.com/kubeshop/tracetest/agent/event" + "github.com/kubeshop/tracetest/agent/ui/dashboard/events" + "github.com/kubeshop/tracetest/agent/ui/dashboard/sensors" "github.com/kubeshop/tracetest/server/otlp" "go.opencensus.io/trace" pb "go.opentelemetry.io/proto/otlp/collector/trace/v1" @@ -27,6 +29,8 @@ type ingester interface { Statistics() Statistics ResetStatistics() + + SetSensor(sensors.Sensor) } func newForwardIngester(ctx context.Context, batchTimeout time.Duration, cfg remoteIngesterConfig, startRemoteServer bool) (ingester, error) { @@ -34,9 +38,11 @@ func newForwardIngester(ctx context.Context, batchTimeout time.Duration, cfg rem BatchTimeout: batchTimeout, RemoteIngester: cfg, buffer: &buffer{}, + traceIDs: make(map[string]bool, 0), done: make(chan bool), traceCache: cfg.traceCache, logger: cfg.logger, + sensor: cfg.sensor, } if startRemoteServer { @@ -63,9 +69,11 @@ type forwardIngester struct { RemoteIngester remoteIngesterConfig client pb.TraceServiceClient buffer *buffer + traceIDs map[string]bool done chan bool traceCache TraceCache logger *zap.Logger + sensor sensors.Sensor statistics Statistics } @@ -77,6 +85,7 @@ type remoteIngesterConfig struct { startRemoteServer bool logger *zap.Logger observer event.Observer + sensor sensors.Sensor } type buffer struct { @@ -92,6 +101,10 @@ func (i *forwardIngester) ResetStatistics() { i.statistics = Statistics{} } +func (i *forwardIngester) SetSensor(sensor sensors.Sensor) { + i.sensor = sensor +} + func (i *forwardIngester) Ingest(ctx context.Context, request *pb.ExportTraceServiceRequest, requestType otlp.RequestType) (*pb.ExportTraceServiceResponse, error) { spanCount := countSpans(request) i.buffer.mutex.Lock() @@ -100,6 +113,8 @@ func (i *forwardIngester) Ingest(ctx context.Context, request *pb.ExportTraceSer i.statistics.SpanCount += int64(spanCount) i.statistics.LastSpanTimestamp = time.Now() + i.sensor.Emit(events.SpanCountUpdated, i.statistics.SpanCount) + i.buffer.mutex.Unlock() i.logger.Debug("received spans", zap.Int("count", spanCount)) @@ -108,6 +123,7 @@ func (i *forwardIngester) Ingest(ctx context.Context, request *pb.ExportTraceSer // In case of OTLP datastore, those spans will be polled from this cache instead // of a real datastore i.cacheTestSpans(request.ResourceSpans) + i.sensor.Emit(events.TraceCountUpdated, len(i.traceIDs)) } return &pb.ExportTraceServiceResponse{ @@ -208,6 +224,7 @@ func (i *forwardIngester) cacheTestSpans(resourceSpans []*v1.ResourceSpans) { i.logger.Debug("caching test spans", zap.Int("count", len(spans))) for traceID, spans := range spans { + i.traceIDs[traceID] = true if _, ok := i.traceCache.Get(traceID); !ok { i.logger.Debug("traceID is not part of a test", zap.String("traceID", traceID)) // traceID is not part of a test diff --git a/agent/config/config.go b/agent/config/config.go index d6873d3311..751acabe1a 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path" + "regexp" "strings" "github.com/spf13/viper" @@ -17,6 +18,11 @@ type Config struct { OTLPServer OtlpServer `mapstructure:"otlp_server"` } +func (c Config) APIEndpoint() string { + regex := regexp.MustCompile(":[0-9]+$") + return string(regex.ReplaceAll([]byte(c.ServerURL), []byte(""))) +} + type OtlpServer struct { GRPCPort int `mapstructure:"grpc_port"` HTTPPort int `mapstructure:"http_port"` diff --git a/agent/runner/runner.go b/agent/runner/runner.go index f8c3d83c9a..1ba4a175f7 100644 --- a/agent/runner/runner.go +++ b/agent/runner/runner.go @@ -102,6 +102,7 @@ func (s *Runner) StartAgent(ctx context.Context, endpoint, agentApiKey, uiEndpoi cfg.APIKey = agentApiKey } + s.mode = agentConfig.Mode_Dashboard if s.mode == agentConfig.Mode_Dashboard { return s.RunDashboardStrategy(ctx, cfg, uiEndpoint) } diff --git a/agent/runner/runstrategy_dashboard.go b/agent/runner/runstrategy_dashboard.go index 55ab90ec63..85755ad94e 100644 --- a/agent/runner/runstrategy_dashboard.go +++ b/agent/runner/runstrategy_dashboard.go @@ -3,17 +3,31 @@ package runner import ( "context" + "github.com/kubeshop/tracetest/agent/collector" agentConfig "github.com/kubeshop/tracetest/agent/config" "github.com/kubeshop/tracetest/agent/ui/dashboard" + "github.com/kubeshop/tracetest/agent/ui/dashboard/models" + "github.com/kubeshop/tracetest/agent/ui/dashboard/sensors" + "github.com/kubeshop/tracetest/server/version" ) func (s *Runner) RunDashboardStrategy(ctx context.Context, cfg agentConfig.Config, uiEndpoint string) error { - session, _, err := s.authenticate(ctx, cfg) + session, claims, err := s.authenticate(ctx, cfg) if err != nil { return err } defer session.Close() - return dashboard.StartDashboard(ctx) + sensor := sensors.NewSensor() + if collector := collector.GetActiveCollector(); collector != nil { + collector.SetSensor(sensor) + } + + // TODO: convert ids into names + return dashboard.StartDashboard(ctx, models.EnvironmentInformation{ + OrganizationName: claims["organization_id"].(string), + EnvironmentName: claims["environment_id"].(string), + AgentVersion: version.Version, + }, sensor) } diff --git a/agent/ui/dashboard/components/header.go b/agent/ui/dashboard/components/header.go index 476aa43dc9..a905eaf3f6 100644 --- a/agent/ui/dashboard/components/header.go +++ b/agent/ui/dashboard/components/header.go @@ -5,6 +5,7 @@ import ( "time" "github.com/kubeshop/tracetest/agent/ui/dashboard/events" + "github.com/kubeshop/tracetest/agent/ui/dashboard/models" "github.com/kubeshop/tracetest/agent/ui/dashboard/sensors" "github.com/kubeshop/tracetest/agent/ui/dashboard/styles" "github.com/rivo/tview" @@ -41,7 +42,12 @@ type Header struct { sensor sensors.Sensor data HeaderData - messageBanner *MessageBanner + messageBanner *MessageBanner + + organizationTableCell *tview.TableCell + environmentTableCell *tview.TableCell + agentVersionTableCell *tview.TableCell + uptimeTableCell *tview.TableCell runsTableCell *tview.TableCell tracesTableCell *tview.TableCell @@ -50,14 +56,17 @@ type Header struct { func NewHeader(renderScheduler RenderScheduler, sensor sensors.Sensor) *Header { h := &Header{ - Flex: tview.NewFlex(), - renderScheduler: renderScheduler, - sensor: sensor, - messageBanner: NewMessageBanner(renderScheduler), - uptimeTableCell: tview.NewTableCell("0s").SetStyle(styles.MetricValueStyle), - runsTableCell: tview.NewTableCell("0").SetStyle(styles.MetricValueStyle), - tracesTableCell: tview.NewTableCell("0").SetStyle(styles.MetricValueStyle), - spansTableCell: tview.NewTableCell("0").SetStyle(styles.MetricValueStyle), + Flex: tview.NewFlex(), + renderScheduler: renderScheduler, + sensor: sensor, + messageBanner: NewMessageBanner(renderScheduler), + organizationTableCell: tview.NewTableCell("").SetStyle(styles.MetricValueStyle), + environmentTableCell: tview.NewTableCell("").SetStyle(styles.MetricValueStyle), + agentVersionTableCell: tview.NewTableCell("").SetStyle(styles.MetricValueStyle), + uptimeTableCell: tview.NewTableCell("0s").SetStyle(styles.MetricValueStyle), + runsTableCell: tview.NewTableCell("0").SetStyle(styles.MetricValueStyle), + tracesTableCell: tview.NewTableCell("0").SetStyle(styles.MetricValueStyle), + spansTableCell: tview.NewTableCell("0").SetStyle(styles.MetricValueStyle), } h.draw() @@ -99,13 +108,13 @@ func (h *Header) getEnvironmentInformationTable() tview.Primitive { table.SetBackgroundColor(styles.HeaderBackgroundColor) table.SetBorder(true).SetTitle("Environment").SetTitleColor(styles.HighlighColor) table.SetCell(0, 0, tview.NewTableCell("Organization: ").SetStyle(styles.MetricNameStyle)) - table.SetCell(0, 1, tview.NewTableCell("my-company").SetStyle(styles.MetricValueStyle)) + table.SetCell(0, 1, h.organizationTableCell) table.SetCell(1, 0, tview.NewTableCell("Environment: ").SetStyle(styles.MetricNameStyle)) - table.SetCell(1, 1, tview.NewTableCell("steve-dev").SetStyle(styles.MetricValueStyle)) + table.SetCell(1, 1, h.environmentTableCell) table.SetCell(2, 0, tview.NewTableCell("Last Tracing Backend: ").SetStyle(styles.MetricNameStyle)) table.SetCell(2, 1, tview.NewTableCell("Jaeger").SetStyle(styles.MetricValueStyle)) table.SetCell(3, 0, tview.NewTableCell("Version: ").SetStyle(styles.MetricNameStyle)) - table.SetCell(3, 1, tview.NewTableCell("v0.15.5").SetStyle(styles.MetricValueStyle)) + table.SetCell(3, 1, h.agentVersionTableCell) table.SetBorderPadding(1, 1, 2, 1) return table @@ -144,4 +153,29 @@ func (h *Header) setupSensors() { h.data.Metrics.Uptime = uptime h.onDataChange() }) + + h.sensor.On(events.EnvironmentStart, func(e sensors.Event) { + var environment models.EnvironmentInformation + e.Unmarshal(&environment) + + h.environmentTableCell.SetText(environment.EnvironmentName) + h.organizationTableCell.SetText(environment.OrganizationName) + h.agentVersionTableCell.SetText(environment.AgentVersion) + }) + + h.sensor.On(events.SpanCountUpdated, func(e sensors.Event) { + var count int64 + e.Unmarshal(&count) + + h.data.Metrics.Spans = count + h.onDataChange() + }) + + h.sensor.On(events.TraceCountUpdated, func(e sensors.Event) { + var count int + e.Unmarshal(&count) + + h.data.Metrics.Traces = int64(count) + h.onDataChange() + }) } diff --git a/agent/ui/dashboard/dashboard.go b/agent/ui/dashboard/dashboard.go index 8c0966fff6..e408d8c885 100644 --- a/agent/ui/dashboard/dashboard.go +++ b/agent/ui/dashboard/dashboard.go @@ -32,11 +32,11 @@ func startUptimeCounter(sensor sensors.Sensor) { }() } -func StartDashboard(ctx context.Context) error { +func StartDashboard(ctx context.Context, environment models.EnvironmentInformation, sensor sensors.Sensor) error { app := tview.NewApplication() tview.Styles.PrimitiveBackgroundColor = styles.HeaderBackgroundColor renderScheduler := components.NewRenderScheduler(app) - sensor := sensors.NewSensor() + sensor.Emit(events.EnvironmentStart, environment) startUptimeCounter(sensor) diff --git a/agent/ui/dashboard/events/events.go b/agent/ui/dashboard/events/events.go index 53ae7255e1..751ae04cc0 100644 --- a/agent/ui/dashboard/events/events.go +++ b/agent/ui/dashboard/events/events.go @@ -4,4 +4,9 @@ var ( UptimeChanged = "uptime_changed" NewTestRun = "new_test_run" UpdatedTestRun = "updated_test_run" + + EnvironmentStart = "environment_start" + + SpanCountUpdated = "span_count_updated" + TraceCountUpdated = "trace_count_updated" ) diff --git a/agent/ui/dashboard/main/main.go b/agent/ui/dashboard/main/main.go index a0565b2c6c..224080cb8c 100644 --- a/agent/ui/dashboard/main/main.go +++ b/agent/ui/dashboard/main/main.go @@ -5,10 +5,16 @@ import ( "fmt" "github.com/kubeshop/tracetest/agent/ui/dashboard" + "github.com/kubeshop/tracetest/agent/ui/dashboard/models" + "github.com/kubeshop/tracetest/agent/ui/dashboard/sensors" ) func main() { - err := dashboard.StartDashboard(context.Background()) + err := dashboard.StartDashboard(context.Background(), models.EnvironmentInformation{ + OrganizationName: "Ana", + EnvironmentName: "Empregada", + AgentVersion: "0.15.5", + }, sensors.NewSensor()) if err != nil { fmt.Println(err.Error()) } diff --git a/agent/ui/dashboard/models/environment.go b/agent/ui/dashboard/models/environment.go new file mode 100644 index 0000000000..d9ae06ae6e --- /dev/null +++ b/agent/ui/dashboard/models/environment.go @@ -0,0 +1,7 @@ +package models + +type EnvironmentInformation struct { + OrganizationName string + EnvironmentName string + AgentVersion string +} diff --git a/agent/ui/dashboard/sensors/sensor.go b/agent/ui/dashboard/sensors/sensor.go index 4a9771192d..6be6d6db8c 100644 --- a/agent/ui/dashboard/sensors/sensor.go +++ b/agent/ui/dashboard/sensors/sensor.go @@ -27,11 +27,13 @@ func (e *Event) Unmarshal(target interface{}) error { type sensor struct { listeners map[string][]func(Event) + lastEvent map[string]Event } func NewSensor() Sensor { return &sensor{ listeners: make(map[string][]func(Event)), + lastEvent: make(map[string]Event), } } @@ -43,6 +45,10 @@ func (r *sensor) On(eventName string, cb func(Event)) { slice = make([]func(Event), 0) } r.listeners[eventName] = append(slice, cb) + + if event, ok := r.lastEvent[eventName]; ok { + cb(event) + } } func (r *sensor) Emit(eventName string, event interface{}) { @@ -52,6 +58,8 @@ func (r *sensor) Emit(eventName string, event interface{}) { data: event, } + r.lastEvent[eventName] = e + for _, listener := range listeners { listener(e) } From 27c2fdc065fef7989e7a3a2df123342fcab6e665 Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Mon, 12 Feb 2024 17:57:56 -0300 Subject: [PATCH 03/13] feat: update list of tests based on test runs received by the agent --- agent/runner/runner.go | 5 +- agent/runner/runstrategy_dashboard.go | 114 +++++++++++++++++- agent/runner/runstrategy_desktop.go | 2 +- agent/ui/dashboard/components/header.go | 2 +- .../ui/dashboard/components/test_run_list.go | 20 ++- agent/ui/dashboard/dashboard.go | 5 +- agent/ui/dashboard/events/events.go | 2 +- agent/ui/dashboard/models/test_runs.go | 1 + agent/ui/dashboard/pages/test_runs_page.go | 2 +- agent/workers/trigger.go | 8 ++ 10 files changed, 144 insertions(+), 17 deletions(-) diff --git a/agent/runner/runner.go b/agent/runner/runner.go index 1ba4a175f7..f1ce015e29 100644 --- a/agent/runner/runner.go +++ b/agent/runner/runner.go @@ -8,6 +8,7 @@ import ( "github.com/golang-jwt/jwt" agentConfig "github.com/kubeshop/tracetest/agent/config" + "github.com/kubeshop/tracetest/agent/event" "github.com/kubeshop/tracetest/agent/ui" "github.com/kubeshop/tracetest/cli/config" @@ -118,14 +119,14 @@ func enableLogging(logLevel string) bool { return os.Getenv("TRACETEST_DEV") == "true" && logLevel == "debug" } -func (s *Runner) authenticate(ctx context.Context, cfg agentConfig.Config) (*Session, jwt.MapClaims, error) { +func (s *Runner) authenticate(ctx context.Context, cfg agentConfig.Config, observer event.Observer) (*Session, jwt.MapClaims, error) { isStarted := false session := &Session{} var err error for !isStarted { - session, err = StartSession(ctx, cfg, nil, s.logger) + session, err = StartSession(ctx, cfg, observer, s.logger) if err != nil && errors.Is(err, ErrOtlpServerStart) { s.ui.Error("Tracetest Agent binds to the OpenTelemetry ports 4317 and 4318 which are used to receive trace information from your system. The agent tried to bind to these ports, but failed.") shouldRetry := s.ui.Enter("Please stop the process currently listening on these ports and press enter to try again.") diff --git a/agent/runner/runstrategy_dashboard.go b/agent/runner/runstrategy_dashboard.go index 85755ad94e..75032ad569 100644 --- a/agent/runner/runstrategy_dashboard.go +++ b/agent/runner/runstrategy_dashboard.go @@ -2,28 +2,35 @@ package runner import ( "context" + "fmt" + "time" "github.com/kubeshop/tracetest/agent/collector" agentConfig "github.com/kubeshop/tracetest/agent/config" + "github.com/kubeshop/tracetest/agent/event" + "github.com/kubeshop/tracetest/agent/proto" "github.com/kubeshop/tracetest/agent/ui/dashboard" + "github.com/kubeshop/tracetest/agent/ui/dashboard/events" "github.com/kubeshop/tracetest/agent/ui/dashboard/models" "github.com/kubeshop/tracetest/agent/ui/dashboard/sensors" "github.com/kubeshop/tracetest/server/version" + v1 "go.opentelemetry.io/proto/otlp/trace/v1" ) func (s *Runner) RunDashboardStrategy(ctx context.Context, cfg agentConfig.Config, uiEndpoint string) error { - session, claims, err := s.authenticate(ctx, cfg) + sensor := sensors.NewSensor() + if collector := collector.GetActiveCollector(); collector != nil { + collector.SetSensor(sensor) + } + + observer := newDashboardObserver(sensor) + session, claims, err := s.authenticate(ctx, cfg, observer) if err != nil { return err } defer session.Close() - sensor := sensors.NewSensor() - if collector := collector.GetActiveCollector(); collector != nil { - collector.SetSensor(sensor) - } - // TODO: convert ids into names return dashboard.StartDashboard(ctx, models.EnvironmentInformation{ OrganizationName: claims["organization_id"].(string), @@ -31,3 +38,98 @@ func (s *Runner) RunDashboardStrategy(ctx context.Context, cfg agentConfig.Confi AgentVersion: version.Version, }, sensor) } + +type dashboardObserver struct { + runs map[string]models.TestRun + sensor sensors.Sensor +} + +func (o *dashboardObserver) EndDataStoreConnection(*proto.DataStoreConnectionTestRequest, error) { + +} + +func (o *dashboardObserver) EndSpanReceive([]*v1.Span, error) { + +} + +func (o *dashboardObserver) EndStopRequest(*proto.StopRequest, error) { + +} + +func (o *dashboardObserver) EndTracePoll(*proto.PollingRequest, error) { + +} + +func (o *dashboardObserver) EndTriggerExecution(*proto.TriggerRequest, error) { + +} + +func (o *dashboardObserver) Error(error) { +} + +func (o *dashboardObserver) StartDataStoreConnection(*proto.DataStoreConnectionTestRequest) { +} + +func (o *dashboardObserver) StartSpanReceive([]*v1.Span) { +} + +func (o *dashboardObserver) StartStopRequest(request *proto.StopRequest) { + model := o.getRun(request.TestID, request.RunID) + model.Status = "Stopped by user" + + o.setRun(model) + o.sensor.Emit(events.UpdatedTestRun, model) +} + +func (o *dashboardObserver) StartTracePoll(request *proto.PollingRequest) { + model := o.getRun(request.TestID, request.RunID) + model.Status = "Awaiting Trace" + + o.setRun(model) + o.sensor.Emit(events.UpdatedTestRun, model) +} + +func (o *dashboardObserver) StartTriggerExecution(request *proto.TriggerRequest) { + model := o.getRun(request.TestID, request.RunID) + model.TestID = request.TestID + model.RunID = fmt.Sprintf("%d", request.RunID) + model.Type = request.Trigger.Type + model.Endpoint = getEndpoint(request) + model.Name = "" + model.Status = "Triggering" + model.Started = time.Now() + + o.setRun(model) + o.sensor.Emit(events.NewTestRun, model) +} + +func (o *dashboardObserver) getRun(testID string, runID int32) models.TestRun { + if model, ok := o.runs[fmt.Sprintf("%s-%d", testID, runID)]; ok { + return model + } + + return models.TestRun{TestID: testID, RunID: fmt.Sprintf("%d", runID)} +} + +func (o *dashboardObserver) setRun(model models.TestRun) { + o.runs[fmt.Sprintf("%s-%s", model.TestID, model.RunID)] = model +} + +func getEndpoint(request *proto.TriggerRequest) string { + switch request.Trigger.Type { + case "http": + return request.Trigger.Http.Url + case "grpc": + return fmt.Sprintf("%s/%s", request.Trigger.Grpc.Address, request.Trigger.Grpc.Service) + case "kafka": + return request.Trigger.Kafka.Topic + case "traceID": + return request.Trigger.TraceID.Id + default: + return "" + } +} + +func newDashboardObserver(sensor sensors.Sensor) event.Observer { + return &dashboardObserver{sensor: sensor, runs: make(map[string]models.TestRun)} +} diff --git a/agent/runner/runstrategy_desktop.go b/agent/runner/runstrategy_desktop.go index 139c9a013c..050cf8ce3a 100644 --- a/agent/runner/runstrategy_desktop.go +++ b/agent/runner/runstrategy_desktop.go @@ -12,7 +12,7 @@ import ( func (s *Runner) RunDesktopStrategy(ctx context.Context, cfg agentConfig.Config, uiEndpoint string) error { s.ui.Infof("Starting Agent with name %s...", cfg.Name) - session, claims, err := s.authenticate(ctx, cfg) + session, claims, err := s.authenticate(ctx, cfg, nil) if err != nil { return err } diff --git a/agent/ui/dashboard/components/header.go b/agent/ui/dashboard/components/header.go index a905eaf3f6..a5e2eb91e5 100644 --- a/agent/ui/dashboard/components/header.go +++ b/agent/ui/dashboard/components/header.go @@ -146,7 +146,7 @@ func (h *Header) hideMessageBanner() { } func (h *Header) setupSensors() { - h.sensor.On(events.UptimeChanged, func(e sensors.Event) { + h.sensor.On(events.TimeChanged, func(e sensors.Event) { var uptime time.Duration e.Unmarshal(&uptime) diff --git a/agent/ui/dashboard/components/test_run_list.go b/agent/ui/dashboard/components/test_run_list.go index ce72f8a3f4..cdf35717ac 100644 --- a/agent/ui/dashboard/components/test_run_list.go +++ b/agent/ui/dashboard/components/test_run_list.go @@ -3,8 +3,11 @@ package components import ( "fmt" "strings" + "time" + "github.com/kubeshop/tracetest/agent/ui/dashboard/events" "github.com/kubeshop/tracetest/agent/ui/dashboard/models" + "github.com/kubeshop/tracetest/agent/ui/dashboard/sensors" "github.com/kubeshop/tracetest/agent/ui/dashboard/styles" "github.com/rivo/tview" ) @@ -22,13 +25,15 @@ type TestRunList struct { testRuns []models.TestRun + sensor sensors.Sensor renderScheduler RenderScheduler } -func NewTestRunList(renderScheduler RenderScheduler) *TestRunList { +func NewTestRunList(renderScheduler RenderScheduler, sensor sensors.Sensor) *TestRunList { list := &TestRunList{ Table: tview.NewTable(), renderScheduler: renderScheduler, + sensor: sensor, } for i, header := range headers { @@ -48,6 +53,8 @@ func NewTestRunList(renderScheduler RenderScheduler) *TestRunList { fmt.Println(row, column) }) + list.setupSensors() + return list } @@ -58,6 +65,17 @@ func (l *TestRunList) SetTestRuns(runs []models.TestRun) { }) } +func (l *TestRunList) setupSensors() { + l.sensor.On(events.TimeChanged, func(e sensors.Event) { + for i, run := range l.testRuns { + run.When = time.Since(run.Started).Round(time.Second) + l.testRuns[i] = run + } + + l.renderRuns() + }) +} + func (l *TestRunList) renderRuns() { for i, run := range l.testRuns { l.Table.SetCell(i+1, 0, tview.NewTableCell(run.Name).SetStyle(styles.MetricValueStyle).SetExpansion(1).SetAlign(tview.AlignLeft)) diff --git a/agent/ui/dashboard/dashboard.go b/agent/ui/dashboard/dashboard.go index e408d8c885..27c2950efb 100644 --- a/agent/ui/dashboard/dashboard.go +++ b/agent/ui/dashboard/dashboard.go @@ -18,15 +18,12 @@ type Dashboard struct{} func startUptimeCounter(sensor sensors.Sensor) { ticker := time.NewTicker(time.Second) - fastTicker := time.NewTicker(50 * time.Millisecond) start := time.Now() go func() { for { select { case <-ticker.C: - sensor.Emit(events.UptimeChanged, time.Since(start).Round(time.Second)) - case <-fastTicker.C: - sensor.Emit(events.NewTestRun, models.TestRun{TestID: "1", RunID: "1", Name: "my test", Type: "HTTP", Endpoint: "http://localhost:11633/api/tests", Status: "Awaiting Traces", When: time.Since(start).Round(time.Second)}) + sensor.Emit(events.TimeChanged, time.Since(start).Round(time.Second)) } } }() diff --git a/agent/ui/dashboard/events/events.go b/agent/ui/dashboard/events/events.go index 751ae04cc0..11373af3b6 100644 --- a/agent/ui/dashboard/events/events.go +++ b/agent/ui/dashboard/events/events.go @@ -1,7 +1,7 @@ package events var ( - UptimeChanged = "uptime_changed" + TimeChanged = "time_changed" NewTestRun = "new_test_run" UpdatedTestRun = "updated_test_run" diff --git a/agent/ui/dashboard/models/test_runs.go b/agent/ui/dashboard/models/test_runs.go index 4ee6a27acd..9664b39f45 100644 --- a/agent/ui/dashboard/models/test_runs.go +++ b/agent/ui/dashboard/models/test_runs.go @@ -9,5 +9,6 @@ type TestRun struct { Type string Endpoint string Status string + Started time.Time When time.Duration } diff --git a/agent/ui/dashboard/pages/test_runs_page.go b/agent/ui/dashboard/pages/test_runs_page.go index c725f0a36a..d9998607ee 100644 --- a/agent/ui/dashboard/pages/test_runs_page.go +++ b/agent/ui/dashboard/pages/test_runs_page.go @@ -30,7 +30,7 @@ func NewTestRunPage(renderScheduler components.RenderScheduler, sensor sensors.S } p.header = components.NewHeader(renderScheduler, sensor) - p.testRunList = components.NewTestRunList(renderScheduler) + p.testRunList = components.NewTestRunList(renderScheduler, sensor) p.Grid. // We gonna use 4 lines (it could be 2, but there's a limitation in tview that only allow diff --git a/agent/workers/trigger.go b/agent/workers/trigger.go index 7496442406..7ef4d2236b 100644 --- a/agent/workers/trigger.go +++ b/agent/workers/trigger.go @@ -8,6 +8,7 @@ import ( "github.com/kubeshop/tracetest/agent/collector" "github.com/kubeshop/tracetest/agent/event" "github.com/kubeshop/tracetest/agent/proto" + "github.com/kubeshop/tracetest/agent/ui/dashboard/sensors" agentTrigger "github.com/kubeshop/tracetest/agent/workers/trigger" "github.com/kubeshop/tracetest/server/executor" "github.com/kubeshop/tracetest/server/pkg/id" @@ -23,6 +24,7 @@ type TriggerWorker struct { registry *agentTrigger.Registry traceCache collector.TraceCache observer event.Observer + sensor sensors.Sensor stoppableProcessRunner StoppableProcessRunner } @@ -46,6 +48,12 @@ func WithTriggerObserver(observer event.Observer) TriggerOption { } } +func WithSensor(sensor sensors.Sensor) TriggerOption { + return func(tw *TriggerWorker) { + tw.sensor = sensor + } +} + func NewTriggerWorker(client *client.Client, opts ...TriggerOption) *TriggerWorker { // TODO: use a real tracer tracer := trace.NewNoopTracerProvider().Tracer("noop") From 5ff36b3eda23b087ad1873eefadb9b2b3bc6c5bd Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Mon, 12 Feb 2024 18:26:28 -0300 Subject: [PATCH 04/13] update test run counter --- agent/ui/dashboard/components/header.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/agent/ui/dashboard/components/header.go b/agent/ui/dashboard/components/header.go index a5e2eb91e5..b49c88d234 100644 --- a/agent/ui/dashboard/components/header.go +++ b/agent/ui/dashboard/components/header.go @@ -178,4 +178,10 @@ func (h *Header) setupSensors() { h.data.Metrics.Traces = int64(count) h.onDataChange() }) + + h.sensor.On(events.NewTestRun, func(e sensors.Event) { + h.data.Metrics.TestRuns++ + + h.onDataChange() + }) } From 7f0f0e5079925d3e798cb0aceae9a236ace4837d Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Tue, 13 Feb 2024 13:17:12 -0300 Subject: [PATCH 05/13] make dashboard as part of the desktop option list --- agent/runner/runner.go | 11 ++++--- agent/runner/runstrategy_dashboard.go | 9 ++---- agent/runner/runstrategy_desktop.go | 31 ++++++++++++------- agent/ui/dashboard/components/header.go | 2 +- .../ui/dashboard/components/test_run_list.go | 2 +- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/agent/runner/runner.go b/agent/runner/runner.go index f1ce015e29..099fb3060f 100644 --- a/agent/runner/runner.go +++ b/agent/runner/runner.go @@ -23,6 +23,7 @@ type Runner struct { ui ui.ConsoleUI mode agentConfig.Mode logger *zap.Logger + claims jwt.MapClaims } func NewRunner(configurator config.Configurator, resources *resourcemanager.Registry, ui ui.ConsoleUI) *Runner { @@ -103,11 +104,6 @@ func (s *Runner) StartAgent(ctx context.Context, endpoint, agentApiKey, uiEndpoi cfg.APIKey = agentApiKey } - s.mode = agentConfig.Mode_Dashboard - if s.mode == agentConfig.Mode_Dashboard { - return s.RunDashboardStrategy(ctx, cfg, uiEndpoint) - } - if s.mode == agentConfig.Mode_Desktop { return s.RunDesktopStrategy(ctx, cfg, uiEndpoint) } @@ -150,5 +146,10 @@ func (s *Runner) authenticate(ctx context.Context, cfg agentConfig.Config, obser if err != nil { return nil, nil, err } + s.claims = claims return session, claims, nil } + +func (s *Runner) getCurrentSessionClaims() jwt.MapClaims { + return s.claims +} diff --git a/agent/runner/runstrategy_dashboard.go b/agent/runner/runstrategy_dashboard.go index 75032ad569..1627ae9bc8 100644 --- a/agent/runner/runstrategy_dashboard.go +++ b/agent/runner/runstrategy_dashboard.go @@ -23,14 +23,11 @@ func (s *Runner) RunDashboardStrategy(ctx context.Context, cfg agentConfig.Confi collector.SetSensor(sensor) } - observer := newDashboardObserver(sensor) - session, claims, err := s.authenticate(ctx, cfg, observer) - if err != nil { - return err + claims := s.getCurrentSessionClaims() + if claims == nil { + return fmt.Errorf("not authenticated") } - defer session.Close() - // TODO: convert ids into names return dashboard.StartDashboard(ctx, models.EnvironmentInformation{ OrganizationName: claims["organization_id"].(string), diff --git a/agent/runner/runstrategy_desktop.go b/agent/runner/runstrategy_desktop.go index 050cf8ce3a..9ac1cb87a3 100644 --- a/agent/runner/runstrategy_desktop.go +++ b/agent/runner/runstrategy_desktop.go @@ -20,19 +20,28 @@ func (s *Runner) RunDesktopStrategy(ctx context.Context, cfg agentConfig.Config, isOpen := true message := `Agent is started! Leave the terminal open so tests can be run and traces gathered from this environment. You can` - options := []consoleUI.Option{{ - Text: "Open Tracetest in a browser to this environment", - Fn: func(_ consoleUI.ConsoleUI) { - s.ui.OpenBrowser(fmt.Sprintf("%sorganizations/%s/environments/%s", uiEndpoint, claims["organization_id"], claims["environment_id"])) + options := []consoleUI.Option{ + { + Text: "Open Tracetest in a browser to this environment", + Fn: func(_ consoleUI.ConsoleUI) { + s.ui.OpenBrowser(fmt.Sprintf("%sorganizations/%s/environments/%s", uiEndpoint, claims["organization_id"], claims["environment_id"])) + }, }, - }, { - Text: "Stop this agent", - Fn: func(_ consoleUI.ConsoleUI) { - isOpen = false - session.Close() - s.ui.Finish() + { + Text: "(Experimental) Open Dashboard", + Fn: func(ui consoleUI.ConsoleUI) { + s.RunDashboardStrategy(ctx, cfg, uiEndpoint) + }, }, - }} + { + Text: "Stop this agent", + Fn: func(_ consoleUI.ConsoleUI) { + isOpen = false + session.Close() + s.claims = nil + s.ui.Finish() + }, + }} for isOpen { selected := s.ui.Select(message, options, 0) diff --git a/agent/ui/dashboard/components/header.go b/agent/ui/dashboard/components/header.go index b49c88d234..710f8f34eb 100644 --- a/agent/ui/dashboard/components/header.go +++ b/agent/ui/dashboard/components/header.go @@ -112,7 +112,7 @@ func (h *Header) getEnvironmentInformationTable() tview.Primitive { table.SetCell(1, 0, tview.NewTableCell("Environment: ").SetStyle(styles.MetricNameStyle)) table.SetCell(1, 1, h.environmentTableCell) table.SetCell(2, 0, tview.NewTableCell("Last Tracing Backend: ").SetStyle(styles.MetricNameStyle)) - table.SetCell(2, 1, tview.NewTableCell("Jaeger").SetStyle(styles.MetricValueStyle)) + table.SetCell(2, 1, tview.NewTableCell("").SetStyle(styles.MetricValueStyle)) table.SetCell(3, 0, tview.NewTableCell("Version: ").SetStyle(styles.MetricNameStyle)) table.SetCell(3, 1, h.agentVersionTableCell) table.SetBorderPadding(1, 1, 2, 1) diff --git a/agent/ui/dashboard/components/test_run_list.go b/agent/ui/dashboard/components/test_run_list.go index cdf35717ac..063f3e9f52 100644 --- a/agent/ui/dashboard/components/test_run_list.go +++ b/agent/ui/dashboard/components/test_run_list.go @@ -17,7 +17,7 @@ var headers = []string{ "Type", "Endpoint", "Status", - "When", + "Age", } type TestRunList struct { From e5cc70cd2c3960ebe48ed2f6a49e99ec0f266a7b Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Tue, 13 Feb 2024 13:32:01 -0300 Subject: [PATCH 06/13] open test run in browser --- agent/runner/runstrategy_dashboard.go | 10 ++++----- agent/runner/runstrategy_desktop.go | 7 ++++-- agent/ui/dashboard/components/header.go | 4 ++-- .../ui/dashboard/components/test_run_list.go | 6 +++-- agent/ui/dashboard/events/events.go | 2 ++ agent/ui/dashboard/main/main.go | 6 ++--- agent/ui/dashboard/models/environment.go | 7 +++--- agent/ui/dashboard/pages/test_runs_page.go | 22 +++++++++++++++++++ 8 files changed, 47 insertions(+), 17 deletions(-) diff --git a/agent/runner/runstrategy_dashboard.go b/agent/runner/runstrategy_dashboard.go index 1627ae9bc8..bfdca6b4a2 100644 --- a/agent/runner/runstrategy_dashboard.go +++ b/agent/runner/runstrategy_dashboard.go @@ -17,8 +17,7 @@ import ( v1 "go.opentelemetry.io/proto/otlp/trace/v1" ) -func (s *Runner) RunDashboardStrategy(ctx context.Context, cfg agentConfig.Config, uiEndpoint string) error { - sensor := sensors.NewSensor() +func (s *Runner) RunDashboardStrategy(ctx context.Context, cfg agentConfig.Config, uiEndpoint string, sensor sensors.Sensor) error { if collector := collector.GetActiveCollector(); collector != nil { collector.SetSensor(sensor) } @@ -30,9 +29,10 @@ func (s *Runner) RunDashboardStrategy(ctx context.Context, cfg agentConfig.Confi // TODO: convert ids into names return dashboard.StartDashboard(ctx, models.EnvironmentInformation{ - OrganizationName: claims["organization_id"].(string), - EnvironmentName: claims["environment_id"].(string), - AgentVersion: version.Version, + OrganizationID: claims["organization_id"].(string), + EnvironmentID: claims["environment_id"].(string), + AgentVersion: version.Version, + ServerEndpoint: uiEndpoint, }, sensor) } diff --git a/agent/runner/runstrategy_desktop.go b/agent/runner/runstrategy_desktop.go index 9ac1cb87a3..866a44fc95 100644 --- a/agent/runner/runstrategy_desktop.go +++ b/agent/runner/runstrategy_desktop.go @@ -5,6 +5,7 @@ import ( "fmt" agentConfig "github.com/kubeshop/tracetest/agent/config" + "github.com/kubeshop/tracetest/agent/ui/dashboard/sensors" consoleUI "github.com/kubeshop/tracetest/agent/ui" ) @@ -12,7 +13,9 @@ import ( func (s *Runner) RunDesktopStrategy(ctx context.Context, cfg agentConfig.Config, uiEndpoint string) error { s.ui.Infof("Starting Agent with name %s...", cfg.Name) - session, claims, err := s.authenticate(ctx, cfg, nil) + sensor := sensors.NewSensor() + dashboardObserver := newDashboardObserver(sensor) + session, claims, err := s.authenticate(ctx, cfg, dashboardObserver) if err != nil { return err } @@ -30,7 +33,7 @@ You can` { Text: "(Experimental) Open Dashboard", Fn: func(ui consoleUI.ConsoleUI) { - s.RunDashboardStrategy(ctx, cfg, uiEndpoint) + s.RunDashboardStrategy(ctx, cfg, uiEndpoint, sensor) }, }, { diff --git a/agent/ui/dashboard/components/header.go b/agent/ui/dashboard/components/header.go index 710f8f34eb..240c06c3f0 100644 --- a/agent/ui/dashboard/components/header.go +++ b/agent/ui/dashboard/components/header.go @@ -158,8 +158,8 @@ func (h *Header) setupSensors() { var environment models.EnvironmentInformation e.Unmarshal(&environment) - h.environmentTableCell.SetText(environment.EnvironmentName) - h.organizationTableCell.SetText(environment.OrganizationName) + h.environmentTableCell.SetText(environment.EnvironmentID) + h.organizationTableCell.SetText(environment.OrganizationID) h.agentVersionTableCell.SetText(environment.AgentVersion) }) diff --git a/agent/ui/dashboard/components/test_run_list.go b/agent/ui/dashboard/components/test_run_list.go index 063f3e9f52..b49983f7e2 100644 --- a/agent/ui/dashboard/components/test_run_list.go +++ b/agent/ui/dashboard/components/test_run_list.go @@ -1,7 +1,6 @@ package components import ( - "fmt" "strings" "time" @@ -50,7 +49,10 @@ func NewTestRunList(renderScheduler RenderScheduler, sensor sensors.Sensor) *Tes list.SetSelectable(true, false) list.Select(0, 0) list.SetSelectedFunc(func(row, column int) { - fmt.Println(row, column) + // ignore the header which is the first row + selectedRow := row - 1 + run := list.testRuns[selectedRow] + list.sensor.Emit(events.SelectedTestRun, run) }) list.setupSensors() diff --git a/agent/ui/dashboard/events/events.go b/agent/ui/dashboard/events/events.go index 11373af3b6..a4e8942d16 100644 --- a/agent/ui/dashboard/events/events.go +++ b/agent/ui/dashboard/events/events.go @@ -9,4 +9,6 @@ var ( SpanCountUpdated = "span_count_updated" TraceCountUpdated = "trace_count_updated" + + SelectedTestRun = "selected_test_run" ) diff --git a/agent/ui/dashboard/main/main.go b/agent/ui/dashboard/main/main.go index 224080cb8c..ffcd82679b 100644 --- a/agent/ui/dashboard/main/main.go +++ b/agent/ui/dashboard/main/main.go @@ -11,9 +11,9 @@ import ( func main() { err := dashboard.StartDashboard(context.Background(), models.EnvironmentInformation{ - OrganizationName: "Ana", - EnvironmentName: "Empregada", - AgentVersion: "0.15.5", + OrganizationID: "Ana", + EnvironmentID: "Empregada", + AgentVersion: "0.15.5", }, sensors.NewSensor()) if err != nil { fmt.Println(err.Error()) diff --git a/agent/ui/dashboard/models/environment.go b/agent/ui/dashboard/models/environment.go index d9ae06ae6e..8ed7e879a7 100644 --- a/agent/ui/dashboard/models/environment.go +++ b/agent/ui/dashboard/models/environment.go @@ -1,7 +1,8 @@ package models type EnvironmentInformation struct { - OrganizationName string - EnvironmentName string - AgentVersion string + OrganizationID string + EnvironmentID string + AgentVersion string + ServerEndpoint string } diff --git a/agent/ui/dashboard/pages/test_runs_page.go b/agent/ui/dashboard/pages/test_runs_page.go index d9998607ee..10d019a567 100644 --- a/agent/ui/dashboard/pages/test_runs_page.go +++ b/agent/ui/dashboard/pages/test_runs_page.go @@ -3,6 +3,7 @@ package pages import ( "fmt" + "github.com/kubeshop/tracetest/agent/ui" "github.com/kubeshop/tracetest/agent/ui/dashboard/components" "github.com/kubeshop/tracetest/agent/ui/dashboard/events" "github.com/kubeshop/tracetest/agent/ui/dashboard/models" @@ -78,6 +79,27 @@ func NewTestRunPage(renderScheduler components.RenderScheduler, sensor sensors.S p.testRunList.SetTestRuns(p.testRuns) }) + sensor.On(events.EnvironmentStart, func(e sensors.Event) { + var environment models.EnvironmentInformation + e.Unmarshal(&environment) + + sensor.On(events.SelectedTestRun, func(e sensors.Event) { + var run models.TestRun + e.Unmarshal(&run) + + endpoint := fmt.Sprintf( + "%s/organizations/%s/environments/%s/test/%s/run/%s", + environment.ServerEndpoint, + environment.OrganizationID, + environment.EnvironmentID, + run.TestID, + run.RunID, + ) + + ui.DefaultUI.OpenBrowser(endpoint) + }) + }) + return p } From 5ead8eb050da435fd7f9d4e2a050ff86e460acad Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Tue, 13 Feb 2024 13:33:14 -0300 Subject: [PATCH 07/13] fix panic --- agent/ui/dashboard/components/test_run_list.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/agent/ui/dashboard/components/test_run_list.go b/agent/ui/dashboard/components/test_run_list.go index b49983f7e2..8c30f584af 100644 --- a/agent/ui/dashboard/components/test_run_list.go +++ b/agent/ui/dashboard/components/test_run_list.go @@ -50,6 +50,10 @@ func NewTestRunList(renderScheduler RenderScheduler, sensor sensors.Sensor) *Tes list.Select(0, 0) list.SetSelectedFunc(func(row, column int) { // ignore the header which is the first row + if row == 0 { + return + } + selectedRow := row - 1 run := list.testRuns[selectedRow] list.sensor.Emit(events.SelectedTestRun, run) From c1ee6f175e2c2608a69cf62a001e29d15294192c Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Tue, 13 Feb 2024 14:28:30 -0300 Subject: [PATCH 08/13] make it possible to exit and go back to dashboard --- agent/runner/runstrategy_desktop.go | 6 +++++- agent/ui/dashboard/dashboard.go | 9 +++++++++ agent/ui/dashboard/sensors/sensor.go | 6 ++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/agent/runner/runstrategy_desktop.go b/agent/runner/runstrategy_desktop.go index 866a44fc95..be64e6e7c2 100644 --- a/agent/runner/runstrategy_desktop.go +++ b/agent/runner/runstrategy_desktop.go @@ -33,7 +33,11 @@ You can` { Text: "(Experimental) Open Dashboard", Fn: func(ui consoleUI.ConsoleUI) { - s.RunDashboardStrategy(ctx, cfg, uiEndpoint, sensor) + sensor.Reset() + err := s.RunDashboardStrategy(ctx, cfg, uiEndpoint, sensor) + if err != nil { + fmt.Println(err.Error()) + } }, }, { diff --git a/agent/ui/dashboard/dashboard.go b/agent/ui/dashboard/dashboard.go index 27c2950efb..1701a2bac2 100644 --- a/agent/ui/dashboard/dashboard.go +++ b/agent/ui/dashboard/dashboard.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/gdamore/tcell/v2" "github.com/kubeshop/tracetest/agent/ui/dashboard/components" "github.com/kubeshop/tracetest/agent/ui/dashboard/events" "github.com/kubeshop/tracetest/agent/ui/dashboard/models" @@ -40,6 +41,14 @@ func StartDashboard(ctx context.Context, environment models.EnvironmentInformati router := NewRouter() router.AddAndSwitchToPage("home", pages.NewTestRunPage(renderScheduler, sensor)) + app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyCtrlC, tcell.KeyEsc: + app.Stop() + } + return event + }) + if err := app.SetRoot(router, true).SetFocus(router).Run(); err != nil { return fmt.Errorf("failed to start dashboard: %w", err) } diff --git a/agent/ui/dashboard/sensors/sensor.go b/agent/ui/dashboard/sensors/sensor.go index 6be6d6db8c..ccceafbf1d 100644 --- a/agent/ui/dashboard/sensors/sensor.go +++ b/agent/ui/dashboard/sensors/sensor.go @@ -9,6 +9,7 @@ import ( type Sensor interface { On(string, func(Event)) Emit(string, interface{}) + Reset() } type Event struct { @@ -37,6 +38,11 @@ func NewSensor() Sensor { } } +func (r *sensor) Reset() { + r.listeners = make(map[string][]func(Event)) + r.lastEvent = make(map[string]Event) +} + func (r *sensor) On(eventName string, cb func(Event)) { var slice []func(Event) if existingSlice, ok := r.listeners[eventName]; ok { From 5e8124709d87bc4d7b39cd4279e366dc5e2a1ccd Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Tue, 13 Feb 2024 14:32:57 -0300 Subject: [PATCH 09/13] remove dashboard mode from CLI --- agent/config/flags.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/agent/config/flags.go b/agent/config/flags.go index 0ff39b7289..068de3a81f 100644 --- a/agent/config/flags.go +++ b/agent/config/flags.go @@ -3,9 +3,8 @@ package config type Mode string const ( - Mode_Dashboard Mode = "dashboard" - Mode_Desktop Mode = "desktop" - Mode_Verbose Mode = "verbose" + Mode_Desktop Mode = "desktop" + Mode_Verbose Mode = "verbose" ) type Flags struct { From 5350e7a052296423c4fbbca016e02ad807048675 Mon Sep 17 00:00:00 2001 From: Oscar Reyes Date: Tue, 13 Feb 2024 11:41:33 -0600 Subject: [PATCH 10/13] Chore performance improvements (#3616) * chore(frontend): indentifying performance improvements * chore(frontend): identify performance improvements * feat(frontend): hide dag view for big traces (#3606) * Chore performance improvements tech debt 1 (#3607) * adding async functionality * chore(frontend): DAG improvements * chore(frontend): DAG improvements * chore(frontend): fixing spinner styles * chore(frontend): fixing spinner styles * feat(frontend): fixing tests * chore(frontend): fixing spinner styles * chore(frontend): FE Improvements for the Test View (#3613) * adding async functionality * chore(frontend): DAG improvements * chore(frontend): DAG improvements * chore(frontend): fixing spinner styles * chore(frontend): fixing spinner styles * feat(frontend): fixing tests * chore(frontend): fixing spinner styles * chore(frontend): FE Improvements for the Test View * chore(frontend): reverting editor changes * chore(frontend): Adding memoization for useComplete hook * chore(frontend): Adding Search Service usage * chore(frontend): cleanup * feat(frontend): implement virtual list for timeline view (#3617) * feat(frontend): implement virtual list for timeline view * remove prop * add header and collapse * feat: add timeline view connectors (#3623) * feat(frontend): Implemeting new Timeline for the Test Page (#3627) * feat(frontend): Implemeting new Timeline for the Test Page * feat(frontend): Fixing tests * feat(frontend): Fixing tests * Chore performance improvements span search (#3629) * feat(frontend): Implemeting new Timeline for the Test Page * feat(frontend): Implementing span search for analyzer and test views * feat(frontend): read improvements * feat(frontend): adding single line input component * feat(frontend): updating analyzer styles * feat(frontend): Fixing tests * feat(frontend): Fixing tests --------- Co-authored-by: Jorge Padilla --- .../e2e/TestRunDetail/CreateAssertion.spec.ts | 8 - web/cypress/support/commands.ts | 1 - web/docker-compose.yaml | 127 +++++++++++++++ web/package-lock.json | 42 +++++ web/package.json | 5 + .../AnalyzerResult/AnalyzerResult.styled.ts | 16 +- .../AnalyzerResult/AnalyzerResult.tsx | 10 +- web/src/components/AnalyzerResult/Plugins.tsx | 11 +- web/src/components/AnalyzerResult/Rule.tsx | 125 +++++---------- .../components/AnalyzerResult/RuleResult.tsx | 64 ++++++++ .../components/Fields/Auth/AuthApiKeyBase.tsx | 7 +- web/src/components/Fields/Auth/AuthBasic.tsx | 8 +- web/src/components/Fields/Auth/AuthBearer.tsx | 5 +- web/src/components/Fields/Headers/Headers.tsx | 7 +- .../Fields/KeyValueList/KeyValueList.tsx | 7 +- .../components/Fields/Metadata/Metadata.tsx | 7 +- .../components/Fields/MultiURL/MultiURL.tsx | 5 +- .../components/Fields/PlainAuth/Fields.tsx | 7 +- web/src/components/Fields/URL/URL.tsx | 6 +- .../Fields/VariableName/VariableName.tsx | 5 +- .../Expression/hooks/useAutoComplete.ts | 16 +- .../Inputs/Editor/Selector/Selector.tsx | 11 +- .../Editor/Selector/hooks/useAutoComplete.ts | 16 +- .../Inputs/SingleLine/SingleLine.tsx | 12 ++ web/src/components/Inputs/SingleLine/index.ts | 2 + .../LoadingSpinner/LoadingSpinner.styled.ts | 8 + web/src/components/LoadingSpinner/index.ts | 4 + .../components/RunDetailLayout/HeaderLeft.tsx | 21 +-- web/src/components/RunDetailTest/TestDAG.tsx | 62 ++++++++ .../components/RunDetailTest/TestPanel.tsx | 14 +- .../RunDetailTest/Visualization.tsx | 72 ++------- .../RunDetailTrace/AnalyzerPanel.tsx | 7 +- .../RunDetailTrace/RunDetailTrace.tsx | 5 + web/src/components/RunDetailTrace/Search.tsx | 27 +--- .../components/RunDetailTrace/TraceDAG.tsx | 61 +++++++ .../components/RunDetailTrace/TracePanel.tsx | 12 +- .../RunDetailTrace/Visualization.tsx | 68 +++----- .../TestPlugins/Forms/Kafka/Kafka.tsx | 5 +- .../components/TestResults/TestResults.tsx | 2 +- .../components/TestSpec/TestSpec.styled.ts | 1 + web/src/components/TestSpecDetail/Content.tsx | 85 +++++----- .../components/TestSpecDetail/ResultCard.tsx | 65 ++++++++ web/src/components/TestSpecDetail/Search.tsx | 70 ++++++++ .../components/TestSpecDetail/SpanHeader.tsx | 30 ++-- .../TestSpecDetail/TestSpecDetail.styled.ts | 25 ++- .../TestSpecDetail/TestSpecDetail.tsx | 59 +++---- .../components/TestSpecs/TestSpecs.styled.ts | 6 - web/src/components/TestSpecs/TestSpecs.tsx | 41 +++-- .../Visualization/components/DAG/DAG.tsx | 5 +- .../components/Navigation/Navigation.tsx | 1 + .../components/Switch/Switch.styled.ts | 7 +- .../components/Switch/Switch.tsx | 21 ++- .../Timeline/BaseSpanNode/BaseSpanNodeV2.tsx | 62 ++++++++ .../Timeline/BaseSpanNode/ConnectorV2.tsx | 56 +++++++ .../components/Timeline/Header.tsx | 22 +++ .../components/Timeline/ListWrapper.tsx | 33 ++++ .../components/Timeline/NavigationWrapper.tsx | 10 ++ .../components/Timeline/SpanNodeFactoryV2.tsx | 30 ++++ .../components/Timeline/Ticks/Ticks.styled.ts | 37 +++++ .../components/Timeline/Ticks/Ticks.tsx | 40 +++++ .../components/Timeline/Timeline.provider.tsx | 127 +++++++++++++++ .../components/Timeline/TimelineV2.styled.ts | 150 ++++++++++++++++++ .../components/Timeline/TimelineV2.tsx | 37 +++++ .../TraceSpanNode/TraceSpanNodeV2.tsx | 19 +++ web/src/constants/Timeline.constants.ts | 1 + web/src/constants/Visualization.constants.ts | 2 + web/src/hooks/useSpanData.ts | 2 + web/src/models/DAG.model.ts | 3 + web/src/models/SearchSpansResult.model.ts | 22 +++ web/src/models/Span.model.ts | 11 +- web/src/models/TestRun.model.ts | 4 +- web/src/models/Trace.model.ts | 25 ++- .../models/__tests__/TestRun.model.test.ts | 2 - .../providers/TestRun/TestRun.provider.tsx | 5 +- .../TestSpecs/TestSpecs.provider.tsx | 10 +- web/src/redux/actions/Router.actions.ts | 9 +- web/src/redux/actions/TestSpecs.actions.ts | 1 + .../Tracetest/endpoints/TestRun.endpoint.ts | 9 ++ web/src/redux/apis/Tracetest/index.ts | 2 + web/src/redux/slices/DAG.slice.ts | 15 +- web/src/redux/slices/Trace.slice.ts | 16 +- web/src/selectors/Assertion.selectors.ts | 2 +- web/src/selectors/Editor.selectors.ts | 26 +++ web/src/selectors/Span.selectors.ts | 4 +- web/src/selectors/TestRun.selectors.ts | 2 +- web/src/services/Analyzer.service.ts | 13 +- web/src/services/Assertion.service.ts | 3 +- web/src/services/DAG.service.ts | 17 +- web/src/services/Span.service.ts | 1 + web/src/services/TestRun.service.ts | 12 +- web/src/services/Timeline.service.ts | 18 +++ web/src/utils/Common.ts | 9 ++ web/src/utils/Date.ts | 44 +++++ 93 files changed, 1726 insertions(+), 501 deletions(-) create mode 100644 web/docker-compose.yaml create mode 100644 web/src/components/AnalyzerResult/RuleResult.tsx create mode 100644 web/src/components/Inputs/SingleLine/SingleLine.tsx create mode 100644 web/src/components/Inputs/SingleLine/index.ts create mode 100644 web/src/components/LoadingSpinner/LoadingSpinner.styled.ts create mode 100644 web/src/components/RunDetailTest/TestDAG.tsx create mode 100644 web/src/components/RunDetailTrace/TraceDAG.tsx create mode 100644 web/src/components/TestSpecDetail/ResultCard.tsx create mode 100644 web/src/components/TestSpecDetail/Search.tsx create mode 100644 web/src/components/Visualization/components/Timeline/BaseSpanNode/BaseSpanNodeV2.tsx create mode 100644 web/src/components/Visualization/components/Timeline/BaseSpanNode/ConnectorV2.tsx create mode 100644 web/src/components/Visualization/components/Timeline/Header.tsx create mode 100644 web/src/components/Visualization/components/Timeline/ListWrapper.tsx create mode 100644 web/src/components/Visualization/components/Timeline/NavigationWrapper.tsx create mode 100644 web/src/components/Visualization/components/Timeline/SpanNodeFactoryV2.tsx create mode 100644 web/src/components/Visualization/components/Timeline/Ticks/Ticks.styled.ts create mode 100644 web/src/components/Visualization/components/Timeline/Ticks/Ticks.tsx create mode 100644 web/src/components/Visualization/components/Timeline/Timeline.provider.tsx create mode 100644 web/src/components/Visualization/components/Timeline/TimelineV2.styled.ts create mode 100644 web/src/components/Visualization/components/Timeline/TimelineV2.tsx create mode 100644 web/src/components/Visualization/components/Timeline/TraceSpanNode/TraceSpanNodeV2.tsx create mode 100644 web/src/models/SearchSpansResult.model.ts create mode 100644 web/src/selectors/Editor.selectors.ts diff --git a/web/cypress/e2e/TestRunDetail/CreateAssertion.spec.ts b/web/cypress/e2e/TestRunDetail/CreateAssertion.spec.ts index d4ac6ae833..404641d870 100644 --- a/web/cypress/e2e/TestRunDetail/CreateAssertion.spec.ts +++ b/web/cypress/e2e/TestRunDetail/CreateAssertion.spec.ts @@ -48,7 +48,6 @@ describe('Create Assertion', () => { cy.get('[data-cy=assertion-form-submit-button]').click(); - cy.get('[data-cy=test-specs-container]').should('be.visible'); cy.get('[data-cy=test-spec-container]').should('have.lengthOf', 1); }); @@ -74,7 +73,6 @@ describe('Create Assertion', () => { cy.get('[data-cy=assertion-form-submit-button]').click(); - cy.get('[data-cy=test-specs-container]').should('be.visible'); cy.get('[data-cy=test-spec-container]').should('have.lengthOf', 1); }); @@ -98,8 +96,6 @@ describe('Create Assertion', () => { cy.selectOperator(1); cy.get('[data-cy=assertion-form-submit-button]').click(); - - cy.get('[data-cy=test-specs-container]').should('be.visible'); cy.get('[data-cy=test-spec-container]').should('have.lengthOf', 1); }); @@ -124,8 +120,6 @@ describe('Create Assertion', () => { cy.selectOperator(1); cy.get('[data-cy=assertion-form-submit-button]').click(); - - cy.get('[data-cy=test-specs-container]').should('be.visible'); cy.get('[data-cy=test-spec-container]').should('have.lengthOf', 1); }); @@ -151,9 +145,7 @@ describe('Create Assertion', () => { cy.get('[data-cy=assertion-form-submit-button]').click(); - cy.get('[data-cy=test-specs-container]').should('exist'); cy.get('[data-cy=test-spec-container]').should('have.lengthOf', 2); - cy.get('[data-cy=trace-actions-revert-all').click(); cy.get('[data-cy=test-spec-container]').should('have.lengthOf', 0); }); diff --git a/web/cypress/support/commands.ts b/web/cypress/support/commands.ts index 62a6d24621..8e14833ba4 100644 --- a/web/cypress/support/commands.ts +++ b/web/cypress/support/commands.ts @@ -224,7 +224,6 @@ Cypress.Commands.add('createAssertion', () => { cy.get('[data-cy=assertion-check-operator]').click({force: true}); cy.get('[data-cy=assertion-form-submit-button]').click(); - cy.get('[data-cy=test-specs-container]').should('be.visible'); cy.get('[data-cy=test-spec-container]').should('have.lengthOf', 1); }); diff --git a/web/docker-compose.yaml b/web/docker-compose.yaml new file mode 100644 index 0000000000..e9cfa7ef85 --- /dev/null +++ b/web/docker-compose.yaml @@ -0,0 +1,127 @@ +version: '3.2' +services: + tracetest: + restart: unless-stopped + image: kubeshop/tracetest:${TAG:-latest} + extra_hosts: + - 'host.docker.internal:host-gateway' + build: + context: . + volumes: + - type: bind + source: ../local-config/tracetest.config.yaml + target: /app/tracetest.yaml + - type: bind + source: ../local-config/tracetest.provision.yaml + target: /app/provisioning.yaml + ports: + - 11633:11633 + command: --provisioning-file /app/provisioning.yaml + healthcheck: + test: ['CMD', 'wget', '--spider', 'localhost:11633'] + interval: 1s + timeout: 3s + retries: 60 + depends_on: + postgres: + condition: service_healthy + environment: + TRACETEST_DEV: ${TRACETEST_DEV} + TRACETEST_TESTPIPELINES_TRIGGEREXECUTE_ENABLED: ${TRACETEST_TESTPIPELINES_TRIGGEREXECUTE_ENABLED} + TRACETEST_TESTPIPELINES_TRACEFETCH_ENABLED: ${TRACETEST_TESTPIPELINES_TRACEFETCH_ENABLED} + TRACETEST_DATASTOREPIPELINES_TESTCONNECTION_ENABLED: ${TRACETEST_DATASTOREPIPELINES_TESTCONNECTION_ENABLED} + + postgres: + image: postgres:15.2 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + ports: + - 5432:5432 + healthcheck: + test: pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" + interval: 1s + timeout: 5s + retries: 60 + + otel-collector: + image: otel/opentelemetry-collector-contrib:0.59.0 + extra_hosts: + - 'host.docker.internal:host-gateway' + ports: + - '4317:4317' + command: + - '--config' + - '/otel-local-config.yaml' + volumes: + - ../local-config/collector.config.yaml:/otel-local-config.yaml + depends_on: + - tracetest + + cache: + image: redis:6 + ports: + - 6379:6379 + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 1s + timeout: 3s + retries: 60 + + queue: + image: rabbitmq:3.12 + restart: unless-stopped + ports: + - 5672:5672 + - 15672:15672 + healthcheck: + test: rabbitmq-diagnostics -q check_running + interval: 1s + timeout: 5s + retries: 60 + + demo-api: + image: kubeshop/demo-pokemon-api:latest + restart: unless-stopped + pull_policy: always + environment: + REDIS_URL: cache + DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres?schema=public + RABBITMQ_HOST: queue + POKE_API_BASE_URL: https://pokeapi.co/api/v2 + COLLECTOR_ENDPOINT: http://otel-collector:4317 + NPM_RUN_COMMAND: api + healthcheck: + test: ['CMD', 'wget', '--spider', 'localhost:8081'] + interval: 1s + timeout: 3s + retries: 60 + ports: + - 8081:8081 + depends_on: + postgres: + condition: service_healthy + cache: + condition: service_healthy + queue: + condition: service_healthy + + worker: + image: kubeshop/demo-pokemon-api:latest + restart: unless-stopped + pull_policy: always + environment: + REDIS_URL: cache + DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres?schema=public + RABBITMQ_HOST: queue + POKE_API_BASE_URL: https://pokeapi.co/api/v2 + COLLECTOR_ENDPOINT: http://otel-collector:4317 + NPM_RUN_COMMAND: worker + depends_on: + postgres: + condition: service_healthy + cache: + condition: service_healthy + queue: + condition: service_healthy + diff --git a/web/package-lock.json b/web/package-lock.json index 2bcb1b0ac0..d356e453f8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -60,6 +60,8 @@ "react-scripts": "5.0.1", "react-spaces": "0.3.8", "react-syntax-highlighter": "15.5.0", + "react-virtualized-auto-sizer": "1.0.22", + "react-window": "1.8.10", "redux-first-history": "5.0.12", "styled-components": "5.3.3", "typescript": "5.0.2" @@ -77,6 +79,7 @@ "@types/lodash": "4.14.181", "@types/postman-collection": "3.5.7", "@types/react-syntax-highlighter": "15.5.7", + "@types/react-window": "1.8.8", "@types/styled-components": "5.1.21", "concurrently": "7.2.1", "cypress": "13.2.0", @@ -7535,6 +7538,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "license": "MIT", @@ -21170,6 +21182,36 @@ "react": ">= 0.14.0" } }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.22.tgz", + "integrity": "sha512-2CGT/4rZ6jvVkKqzJGnZlyQxj4rWPKAwZR80vMlmpYToN18xaB0yIODOoBltWZLbSgpHBpIk0Ae1nrVO9hVClA==", + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-window": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-window/node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/readable-stream": { "version": "3.6.0", "license": "MIT", diff --git a/web/package.json b/web/package.json index b641c4f128..7050ec53a1 100644 --- a/web/package.json +++ b/web/package.json @@ -56,6 +56,8 @@ "react-scripts": "5.0.1", "react-spaces": "0.3.8", "react-syntax-highlighter": "15.5.0", + "react-virtualized-auto-sizer": "1.0.22", + "react-window": "1.8.10", "redux-first-history": "5.0.12", "styled-components": "5.3.3", "typescript": "5.0.2" @@ -81,6 +83,8 @@ "cy:open": "cypress open", "cy:run": "cypress run", "cy:ci": "cypress run --parallel --record --key $CYPRESS_RECORD_KEY", + "cy:local:run": "POKEMON_HTTP_ENDPOINT=http://demo-api:8081 cypress run", + "cy:local:open": "POKEMON_HTTP_ENDPOINT=http://demo-api:8081 cypress open", "prettier": "prettier --write ./src", "less": "lessc --js src/antd-theme/antd-customized.less src/antd-theme/antd-customized.css" }, @@ -122,6 +126,7 @@ "@types/lodash": "4.14.181", "@types/postman-collection": "3.5.7", "@types/react-syntax-highlighter": "15.5.7", + "@types/react-window": "1.8.8", "@types/styled-components": "5.1.21", "concurrently": "7.2.1", "cypress": "13.2.0", diff --git a/web/src/components/AnalyzerResult/AnalyzerResult.styled.ts b/web/src/components/AnalyzerResult/AnalyzerResult.styled.ts index a04f33a243..cf3d442393 100644 --- a/web/src/components/AnalyzerResult/AnalyzerResult.styled.ts +++ b/web/src/components/AnalyzerResult/AnalyzerResult.styled.ts @@ -47,27 +47,19 @@ export const GlobalScoreContainer = styled.div` justify-content: center; `; -export const RuleContainer = styled.div` - border-bottom: ${({theme}) => `1px dashed ${theme.color.borderLight}`}; - padding-bottom: 16px; - margin-bottom: 16px; - margin-left: 43px; -`; - export const RuleHeader = styled.div` display: flex; flex-direction: row; justify-content: space-between; `; -export const Column = styled.div` - display: flex; - flex-direction: column; - margin-bottom: 8px; +export const Column = styled(RuleHeader)` + width: 95%; `; -export const RuleBody = styled(Column)` +export const RuleBody = styled(Column)<{$resultCount: number}>` padding-left: 20px; + height: ${({$resultCount}) => ($resultCount > 10 ? '100vh' : `${$resultCount * 32}px`)}; `; export const Subtitle = styled(Typography.Title)` diff --git a/web/src/components/AnalyzerResult/AnalyzerResult.tsx b/web/src/components/AnalyzerResult/AnalyzerResult.tsx index 08e66870de..45f21a36ed 100644 --- a/web/src/components/AnalyzerResult/AnalyzerResult.tsx +++ b/web/src/components/AnalyzerResult/AnalyzerResult.tsx @@ -2,7 +2,6 @@ import BetaBadge from 'components/BetaBadge/BetaBadge'; import Link from 'components/Link'; import {COMMUNITY_SLACK_URL, OCTOLIINT_ISSUE_URL} from 'constants/Common.constants'; import LinterResult from 'models/LinterResult.model'; -import Trace from 'models/Trace.model'; import {useSettingsValues} from 'providers/SettingsValues/SettingsValues.provider'; import * as S from './AnalyzerResult.styled'; import Empty from './Empty'; @@ -11,10 +10,9 @@ import Plugins from './Plugins'; interface IProps { result: LinterResult; - trace: Trace; } -const AnalyzerResult = ({result: {score, minimumScore, plugins = [], passed}, trace}: IProps) => { +const AnalyzerResult = ({result: {score, minimumScore, plugins = [], passed}}: IProps) => { const {linter} = useSettingsValues(); return ( @@ -31,13 +29,13 @@ const AnalyzerResult = ({result: {score, minimumScore, plugins = [], passed}, tr It can be globally disabled for all tests in the settings page.{' '} )} - We value your feedback on this beta release. Share your thoughts on Slack or add - them to this Issue. + We value your feedback on this beta release. Share your thoughts on Slack or + add them to this Issue. {plugins.length ? ( <> - + ) : ( diff --git a/web/src/components/AnalyzerResult/Plugins.tsx b/web/src/components/AnalyzerResult/Plugins.tsx index 4f6a347d21..a88d9e30e0 100644 --- a/web/src/components/AnalyzerResult/Plugins.tsx +++ b/web/src/components/AnalyzerResult/Plugins.tsx @@ -1,7 +1,8 @@ import {Space, Switch, Typography} from 'antd'; import {useState} from 'react'; import {LinterResultPlugin} from 'models/LinterResult.model'; -import Trace from 'models/Trace.model'; +import {useAppSelector} from 'redux/hooks'; +import TraceSelectors from 'selectors/Trace.selectors'; import TraceAnalyzerAnalytics from 'services/Analytics/TraceAnalyzer.service'; import AnalyzerService from 'services/Analyzer.service'; import * as S from './AnalyzerResult.styled'; @@ -11,12 +12,12 @@ import Collapse, {CollapsePanel} from '../Collapse'; interface IProps { plugins: LinterResultPlugin[]; - trace: Trace; } -const Plugins = ({plugins: rawPlugins, trace}: IProps) => { +const Plugins = ({plugins: rawPlugins}: IProps) => { const [onlyErrors, setOnlyErrors] = useState(false); - const plugins = AnalyzerService.getPlugins(rawPlugins, onlyErrors); + const matchedSpans = useAppSelector(TraceSelectors.selectMatchedSpans); + const plugins = AnalyzerService.getPlugins(rawPlugins, onlyErrors, matchedSpans); return ( <> @@ -38,7 +39,7 @@ const Plugins = ({plugins: rawPlugins, trace}: IProps) => { key={plugin.name} > {plugin.rules.map(rule => ( - + ))} ))} diff --git a/web/src/components/AnalyzerResult/Rule.tsx b/web/src/components/AnalyzerResult/Rule.tsx index 9480e0f04e..dbf81025ba 100644 --- a/web/src/components/AnalyzerResult/Rule.tsx +++ b/web/src/components/AnalyzerResult/Rule.tsx @@ -1,103 +1,54 @@ -import {useCallback} from 'react'; -import {CaretUpFilled} from '@ant-design/icons'; +import {FixedSizeList as List} from 'react-window'; +import AutoSizer, {Size} from 'react-virtualized-auto-sizer'; import {Space, Tooltip, Typography} from 'antd'; +import {PercentageOutlined} from '@ant-design/icons'; import {LinterResultPluginRule} from 'models/LinterResult.model'; -import Trace from 'models/Trace.model'; -import Span from 'models/Span.model'; import {LinterRuleErrorLevel} from 'models/Linter.model'; -import {useAppDispatch} from 'redux/hooks'; -import {selectSpan} from 'redux/slices/Trace.slice'; -import TraceAnalyzerAnalytics from 'services/Analytics/TraceAnalyzer.service'; import * as S from './AnalyzerResult.styled'; import RuleIcon from './RuleIcon'; -import RuleLink from './RuleLink'; +import RuleResult from './RuleResult'; +import Collapse, {CollapsePanel} from '../Collapse'; interface IProps { rule: LinterResultPluginRule; - trace: Trace; } -function getSpanName(spans: Span[], spanId: string) { - const span = spans.find(s => s.id === spanId); - return span?.name ?? ''; -} - -const Rule = ({ - rule: {id, tips, passed, description, name, errorDescription, results = [], level, weight = 0}, - trace, -}: IProps) => { - const dispatch = useAppDispatch(); - - const onSpanResultClick = useCallback( - (spanId: string) => { - TraceAnalyzerAnalytics.onSpanNameClick(); - dispatch(selectSpan({spanId})); - }, - [dispatch] - ); - +const Rule = ({rule: {tips, id, passed, description, name, level, results, weight = 0}, rule}: IProps) => { return ( - - - - - - - {name} - - - - - {description} - - {level === LinterRuleErrorLevel.ERROR && ( - - Weight: {weight} - - )} - - - - {results?.map((result, resultIndex) => ( - // eslint-disable-next-line react/no-array-index-key -
- } - onClick={() => onSpanResultClick(result.spanId)} - type="link" - $error={!result.passed} - > - {getSpanName(trace.spans, result.spanId)} - - - {!result.passed && result.errors.length > 1 && ( - <> -
- {errorDescription} -
- - {result.errors.map(error => ( -
  • - - {error.value} - -
  • - ))} -
    - + + + + + + + {name} + + {description} + + + {level === LinterRuleErrorLevel.ERROR && ( + + {weight} + + )} - - {!result.passed && result.errors.length === 1 && ( -
    - {result.errors[0].description} -
    + + } + key={id} + > + + + {({height, width}: Size) => ( + + {RuleResult} + )} - - {!result.passed && } -
    - ))} -
    -
    + + + + ); }; diff --git a/web/src/components/AnalyzerResult/RuleResult.tsx b/web/src/components/AnalyzerResult/RuleResult.tsx new file mode 100644 index 0000000000..89a42f8bd1 --- /dev/null +++ b/web/src/components/AnalyzerResult/RuleResult.tsx @@ -0,0 +1,64 @@ +import {Tooltip, Typography} from 'antd'; +import {CaretUpFilled} from '@ant-design/icons'; +import {useCallback, useMemo} from 'react'; +import {LinterResultPluginRule} from 'models/LinterResult.model'; +import {useAppDispatch} from 'redux/hooks'; +import {selectSpan} from 'redux/slices/Trace.slice'; +import {useTestRun} from 'providers/TestRun/TestRun.provider'; +import TraceAnalyzerAnalytics from 'services/Analytics/TraceAnalyzer.service'; +import * as S from './AnalyzerResult.styled'; +import RuleLink from './RuleLink'; + +interface IProps { + index: number; + data: LinterResultPluginRule; + style: React.CSSProperties; +} + +const RuleResult = ({index, data: {results, id, errorDescription}, style}: IProps) => { + const {spanId, passed, errors} = useMemo(() => results[index], [results, index]); + const dispatch = useAppDispatch(); + const { + run: {trace}, + } = useTestRun(); + + const onClick = useCallback(() => { + TraceAnalyzerAnalytics.onSpanNameClick(); + dispatch(selectSpan({spanId})); + }, [dispatch, spanId]); + + return ( +
    + } onClick={onClick} type="link" $error={!passed}> + {trace.flat[spanId].name ?? ''} + + + {!passed && errors.length > 1 && ( + <> +
    + {errorDescription} +
    + + {errors.map(error => ( +
  • + + {error.value} + +
  • + ))} +
    + + )} + + {!passed && errors.length === 1 && ( +
    + {errors[0].description} +
    + )} + + {!passed && } +
    + ); +}; + +export default RuleResult; diff --git a/web/src/components/Fields/Auth/AuthApiKeyBase.tsx b/web/src/components/Fields/Auth/AuthApiKeyBase.tsx index feb938f702..46d045fc76 100644 --- a/web/src/components/Fields/Auth/AuthApiKeyBase.tsx +++ b/web/src/components/Fields/Auth/AuthApiKeyBase.tsx @@ -1,7 +1,6 @@ import {Form} from 'antd'; -import {Editor} from 'components/Inputs'; -import {SupportedEditors} from 'constants/Editor.constants'; import * as S from './Auth.styled'; +import SingleLine from '../../Inputs/SingleLine'; interface IProps { baseName: string[]; @@ -17,7 +16,7 @@ const AuthApiKeyBase = ({baseName}: IProps) => ( label="Key" rules={[{required: true}]} > - + ( label="Value" rules={[{required: true}]} > - + diff --git a/web/src/components/Fields/Auth/AuthBasic.tsx b/web/src/components/Fields/Auth/AuthBasic.tsx index d8444a84ef..c98d055873 100644 --- a/web/src/components/Fields/Auth/AuthBasic.tsx +++ b/web/src/components/Fields/Auth/AuthBasic.tsx @@ -1,8 +1,6 @@ import {Form} from 'antd'; -import React from 'react'; -import {Editor} from 'components/Inputs'; -import {SupportedEditors} from 'constants/Editor.constants'; import * as S from './Auth.styled'; +import SingleLine from '../../Inputs/SingleLine'; interface IProps { baseName: string[]; @@ -18,7 +16,7 @@ const AuthBasic = ({baseName}: IProps) => ( label="Username" rules={[{required: true}]} > - + ( data-cy="basic-password" rules={[{required: true}]} > - + diff --git a/web/src/components/Fields/Auth/AuthBearer.tsx b/web/src/components/Fields/Auth/AuthBearer.tsx index 7f22363f58..37a6e8f10f 100644 --- a/web/src/components/Fields/Auth/AuthBearer.tsx +++ b/web/src/components/Fields/Auth/AuthBearer.tsx @@ -1,6 +1,5 @@ import {Form} from 'antd'; -import {Editor} from 'components/Inputs'; -import {SupportedEditors} from 'constants/Editor.constants'; +import SingleLine from '../../Inputs/SingleLine'; interface IProps { baseName: string[]; @@ -8,7 +7,7 @@ interface IProps { const AuthBearer = ({baseName}: IProps) => ( - + ); diff --git a/web/src/components/Fields/Headers/Headers.tsx b/web/src/components/Fields/Headers/Headers.tsx index de8462e793..e1fa486ff7 100644 --- a/web/src/components/Fields/Headers/Headers.tsx +++ b/web/src/components/Fields/Headers/Headers.tsx @@ -1,9 +1,8 @@ import {PlusOutlined} from '@ant-design/icons'; import {Button, Form} from 'antd'; import {DEFAULT_HEADERS, IKeyValue} from 'constants/Test.constants'; -import {Editor} from 'components/Inputs'; -import {SupportedEditors} from 'constants/Editor.constants'; import * as S from './Headers.styled'; +import SingleLine from '../../Inputs/SingleLine'; interface IProps { initialValue?: IKeyValue[]; @@ -26,11 +25,11 @@ const Headers = ({ {fields.map((field, index) => ( - + - + diff --git a/web/src/components/Fields/KeyValueList/KeyValueList.tsx b/web/src/components/Fields/KeyValueList/KeyValueList.tsx index 4d0efbf6f4..809d719b15 100644 --- a/web/src/components/Fields/KeyValueList/KeyValueList.tsx +++ b/web/src/components/Fields/KeyValueList/KeyValueList.tsx @@ -1,9 +1,8 @@ import {PlusOutlined} from '@ant-design/icons'; import {Button, Form} from 'antd'; -import {Editor} from 'components/Inputs'; -import {SupportedEditors} from 'constants/Editor.constants'; import {IKeyValue} from 'constants/Test.constants'; import * as S from './KeyValueList.styled'; +import SingleLine from '../../Inputs/SingleLine'; interface IProps { name?: string; @@ -31,13 +30,13 @@ const KeyValueList = ({ - + - + diff --git a/web/src/components/Fields/Metadata/Metadata.tsx b/web/src/components/Fields/Metadata/Metadata.tsx index af006b56d9..44019ca0c2 100644 --- a/web/src/components/Fields/Metadata/Metadata.tsx +++ b/web/src/components/Fields/Metadata/Metadata.tsx @@ -1,8 +1,7 @@ import {PlusOutlined} from '@ant-design/icons'; import {Button, Form} from 'antd'; -import {SupportedEditors} from 'constants/Editor.constants'; -import {Editor} from 'components/Inputs'; import * as S from './Metadata.styled'; +import SingleLine from '../../Inputs/SingleLine'; const Metadata = () => ( @@ -13,13 +12,13 @@ const Metadata = () => ( - + - + diff --git a/web/src/components/Fields/MultiURL/MultiURL.tsx b/web/src/components/Fields/MultiURL/MultiURL.tsx index 8648829153..436e31b4bd 100644 --- a/web/src/components/Fields/MultiURL/MultiURL.tsx +++ b/web/src/components/Fields/MultiURL/MultiURL.tsx @@ -1,8 +1,7 @@ import {PlusOutlined} from '@ant-design/icons'; import {Button, Form} from 'antd'; -import {SupportedEditors} from 'constants/Editor.constants'; -import {Editor} from 'components/Inputs'; import * as S from './MultiURL.styled'; +import SingleLine from '../../Inputs/SingleLine'; interface IProps { name?: string[]; @@ -24,7 +23,7 @@ const MultiURL = ({name = ['brokerUrls']}: IProps) => ( {fields.map((field, index) => ( - + {!isFirstItem(index) && ( diff --git a/web/src/components/Fields/PlainAuth/Fields.tsx b/web/src/components/Fields/PlainAuth/Fields.tsx index fce6259126..c1842f392e 100644 --- a/web/src/components/Fields/PlainAuth/Fields.tsx +++ b/web/src/components/Fields/PlainAuth/Fields.tsx @@ -1,7 +1,6 @@ import {Form} from 'antd'; -import {Editor} from 'components/Inputs'; -import {SupportedEditors} from 'constants/Editor.constants'; import * as S from './PlainAuth.styled'; +import SingleLine from '../../Inputs/SingleLine'; interface IProps { baseName: string[]; @@ -17,7 +16,7 @@ const Fields = ({baseName}: IProps) => ( rules={[{required: true}]} style={{flexBasis: '50%', overflow: 'hidden'}} > - + ( rules={[{required: true}]} style={{flexBasis: '50%', overflow: 'hidden'}} > - + diff --git a/web/src/components/Fields/URL/URL.tsx b/web/src/components/Fields/URL/URL.tsx index 69546d47c5..aee4ef6b65 100644 --- a/web/src/components/Fields/URL/URL.tsx +++ b/web/src/components/Fields/URL/URL.tsx @@ -1,7 +1,7 @@ import {Col, Form, Row, Select} from 'antd'; import {HTTP_METHOD} from 'constants/Common.constants'; -import {SupportedEditors} from 'constants/Editor.constants'; -import {Editor, DockerTip} from 'components/Inputs'; +import {DockerTip} from 'components/Inputs'; +import SingleLine from '../../Inputs/SingleLine'; interface IProps { showMethodSelector?: boolean; @@ -37,7 +37,7 @@ const URL = ({showMethodSelector = true}: IProps) => ( rules={[{required: true, message: 'Please enter a valid URL'}]} style={{marginBottom: 0}} > - + diff --git a/web/src/components/Fields/VariableName/VariableName.tsx b/web/src/components/Fields/VariableName/VariableName.tsx index cc9dc10b55..8f23aa6433 100644 --- a/web/src/components/Fields/VariableName/VariableName.tsx +++ b/web/src/components/Fields/VariableName/VariableName.tsx @@ -1,6 +1,5 @@ import {Form} from 'antd'; -import {Editor} from 'components/Inputs'; -import {SupportedEditors} from 'constants/Editor.constants'; +import SingleLine from '../../Inputs/SingleLine'; const VariableName = () => ( ( rules={[{required: true, message: 'Please enter a valid variable name'}]} style={{marginBottom: 0}} > - + ); diff --git a/web/src/components/Inputs/Editor/Expression/hooks/useAutoComplete.ts b/web/src/components/Inputs/Editor/Expression/hooks/useAutoComplete.ts index 981aef66b5..ea25aaebb0 100644 --- a/web/src/components/Inputs/Editor/Expression/hooks/useAutoComplete.ts +++ b/web/src/components/Inputs/Editor/Expression/hooks/useAutoComplete.ts @@ -1,10 +1,9 @@ import {useCallback} from 'react'; -import {noop, uniqBy} from 'lodash'; +import {noop} from 'lodash'; import {Completion, CompletionContext} from '@codemirror/autocomplete'; import {useAppStore} from 'redux/hooks'; -import AssertionSelectors from 'selectors/Assertion.selectors'; import VariableSetSelectors from 'selectors/VariableSet.selectors'; -import SpanSelectors from 'selectors/Span.selectors'; +import {selectExpressionAttributeList} from 'selectors/Editor.selectors'; import EditorService from 'services/Editor.service'; import {SupportedEditors} from 'constants/Editor.constants'; @@ -18,13 +17,10 @@ interface IProps { const useAutoComplete = ({testId, runId, onSelect = noop, autocompleteCustomValues}: IProps) => { const {getState} = useAppStore(); - const getAttributeList = useCallback(() => { - const state = getState(); - const spanIdList = SpanSelectors.selectMatchedSpans(state); - const attributeList = AssertionSelectors.selectAttributeList(state, testId, runId, spanIdList); - - return uniqBy(attributeList, 'key'); - }, [getState, runId, testId]); + const getAttributeList = useCallback( + () => selectExpressionAttributeList(getState(), testId, runId), + [getState, runId, testId] + ); const getSelectedVariableSetEntryList = useCallback(() => { const state = getState(); diff --git a/web/src/components/Inputs/Editor/Selector/Selector.tsx b/web/src/components/Inputs/Editor/Selector/Selector.tsx index 960c52a277..fd29cab398 100644 --- a/web/src/components/Inputs/Editor/Selector/Selector.tsx +++ b/web/src/components/Inputs/Editor/Selector/Selector.tsx @@ -1,5 +1,6 @@ import {autocompletion} from '@codemirror/autocomplete'; import {linter} from '@codemirror/lint'; +import {EditorState} from '@codemirror/state'; import {EditorView} from '@codemirror/view'; import CodeMirror from '@uiw/react-codemirror'; import {useMemo} from 'react'; @@ -31,7 +32,13 @@ const Selector = ({ const editorTheme = useEditorTheme(); const extensionList = useMemo( - () => [autocompletion({override: [completionFn]}), linter(lintFn), selectorQL(), EditorView.lineWrapping], + () => [ + autocompletion({override: [completionFn]}), + linter(lintFn), + selectorQL(), + EditorView.lineWrapping, + EditorState.transactionFilter.of(tr => (tr.newDoc.lines > 1 ? [] : tr)), + ], [completionFn, lintFn] ); @@ -39,7 +46,7 @@ const Selector = ({ { const {getState} = useAppStore(); - const getAttributeList = useCallback(() => { - const state = getState(); - const defaultList = AssertionSelectors.selectAllAttributeList(state, testId, runId); - - return defaultList; - }, [getState, runId, testId]); + const getAttributeList = useCallback( + () => selectSelectorAttributeList(getState(), testId, runId), + [getState, runId, testId] + ); return useCallback( async (context: CompletionContext) => { @@ -55,7 +53,9 @@ const useAutoComplete = ({testId, runId}: IProps) => { const uniqueList = uniqBy(attributeList, 'key'); const identifierText = state.doc.sliceString(nodeBefore.from, nodeBefore.to); const isIdentifier = nodeBefore.name === Tokens.Identifier; - const list = isIdentifier ? uniqueList.filter(({key}) => key.toLowerCase().includes(identifierText.toLowerCase())) : uniqueList; + const list = isIdentifier + ? uniqueList.filter(({key}) => key.toLowerCase().includes(identifierText.toLowerCase())) + : uniqueList; return { from: isIdentifier ? nodeBefore.from : word.from, diff --git a/web/src/components/Inputs/SingleLine/SingleLine.tsx b/web/src/components/Inputs/SingleLine/SingleLine.tsx new file mode 100644 index 0000000000..c6c540a982 --- /dev/null +++ b/web/src/components/Inputs/SingleLine/SingleLine.tsx @@ -0,0 +1,12 @@ +import {EditorState} from '@codemirror/state'; +import {Editor} from 'components/Inputs'; +import {SupportedEditors} from 'constants/Editor.constants'; +import {IEditorProps} from '../Editor/Editor'; + +const extensions = [EditorState.transactionFilter.of(tr => (tr.newDoc.lines > 1 ? [] : tr))]; + +const SingleLine = (props: IEditorProps) => ( + +); + +export default SingleLine; diff --git a/web/src/components/Inputs/SingleLine/index.ts b/web/src/components/Inputs/SingleLine/index.ts new file mode 100644 index 0000000000..42ff2f26a5 --- /dev/null +++ b/web/src/components/Inputs/SingleLine/index.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export {default} from './SingleLine'; diff --git a/web/src/components/LoadingSpinner/LoadingSpinner.styled.ts b/web/src/components/LoadingSpinner/LoadingSpinner.styled.ts new file mode 100644 index 0000000000..04c78890e0 --- /dev/null +++ b/web/src/components/LoadingSpinner/LoadingSpinner.styled.ts @@ -0,0 +1,8 @@ +import styled from 'styled-components'; + +export const SpinnerContainer = styled.div` + height: 100%; + display: flex; + align-items: center; + justify-content: center; +`; diff --git a/web/src/components/LoadingSpinner/index.ts b/web/src/components/LoadingSpinner/index.ts index 6e7f1055ec..b2e5482e70 100644 --- a/web/src/components/LoadingSpinner/index.ts +++ b/web/src/components/LoadingSpinner/index.ts @@ -1,2 +1,6 @@ +import {SpinnerContainer} from './LoadingSpinner.styled'; + +export {SpinnerContainer}; + // eslint-disable-next-line no-restricted-exports export {default} from './LoadingSpinner'; diff --git a/web/src/components/RunDetailLayout/HeaderLeft.tsx b/web/src/components/RunDetailLayout/HeaderLeft.tsx index e719fd5c3b..a52e6f039b 100644 --- a/web/src/components/RunDetailLayout/HeaderLeft.tsx +++ b/web/src/components/RunDetailLayout/HeaderLeft.tsx @@ -3,13 +3,13 @@ import {useDashboard} from 'providers/Dashboard/Dashboard.provider'; import {useTest} from 'providers/Test/Test.provider'; import {useTestRun} from 'providers/TestRun/TestRun.provider'; import {useMemo} from 'react'; -import Date from 'utils/Date'; import {isRunStateFinished} from 'models/TestRun.model'; import {TDraftTest} from 'types/Test.types'; import TestService from 'services/Test.service'; import HeaderForm from './HeaderForm'; import Info from './Info'; import * as S from './RunDetailLayout.styled'; +import TestRunService from '../../services/TestRun.service'; interface IProps { name: string; @@ -18,21 +18,8 @@ interface IProps { } const HeaderLeft = ({name, triggerType, origin}: IProps) => { - const { - run: { - createdAt, - testSuiteId, - testSuiteRunId, - executionTime, - trace, - traceId, - testVersion, - metadata: {source} = {}, - } = {}, - run, - } = useTestRun(); + const {run: {createdAt, testSuiteId, testSuiteRunId, executionTime, trace, traceId} = {}, run} = useTestRun(); const {onEdit, isEditLoading: isLoading, test} = useTest(); - const createdTimeAgo = Date.getTimeAgo(createdAt ?? ''); const {navigate} = useDashboard(); const stateIsFinished = isRunStateFinished(run.state); @@ -44,7 +31,7 @@ const HeaderLeft = ({name, triggerType, origin}: IProps) => { const description = useMemo(() => { return ( <> - v{testVersion} • {triggerType} • Ran {createdTimeAgo} {source && <>• Run via {source.toUpperCase()}} + {TestRunService.getHeaderInfo(run, triggerType)} {testSuiteId && !!testSuiteRunId && ( <> {' '} @@ -56,7 +43,7 @@ const HeaderLeft = ({name, triggerType, origin}: IProps) => { )} ); - }, [testVersion, triggerType, createdTimeAgo, source, testSuiteId, testSuiteRunId]); + }, [run, triggerType, testSuiteId, testSuiteRunId]); return ( diff --git a/web/src/components/RunDetailTest/TestDAG.tsx b/web/src/components/RunDetailTest/TestDAG.tsx new file mode 100644 index 0000000000..3597fe4893 --- /dev/null +++ b/web/src/components/RunDetailTest/TestDAG.tsx @@ -0,0 +1,62 @@ +import {useCallback, useEffect} from 'react'; +import {Node, NodeChange} from 'react-flow-renderer'; + +import DAG from 'components/Visualization/components/DAG'; +import {useSpan} from 'providers/Span/Span.provider'; +import {useAppDispatch, useAppSelector} from 'redux/hooks'; +import {initNodes, onNodesChange as onNodesChangeAction} from 'redux/slices/DAG.slice'; +import DAGSelectors from 'selectors/DAG.selectors'; +import TraceDiagramAnalyticsService from 'services/Analytics/TraceDiagramAnalytics.service'; +import Trace from 'models/Trace.model'; +import {useTestSpecForm} from '../TestSpecForm/TestSpecForm.provider'; +import LoadingSpinner, {SpinnerContainer} from '../LoadingSpinner'; + +export interface IProps { + trace: Trace; + onNavigateToSpan(spanId: string): void; +} + +const TestDAG = ({trace: {spans}, onNavigateToSpan}: IProps) => { + const dispatch = useAppDispatch(); + const edges = useAppSelector(DAGSelectors.selectEdges); + const nodes = useAppSelector(DAGSelectors.selectNodes); + const {onSelectSpan, matchedSpans, focusedSpan} = useSpan(); + const {isOpen} = useTestSpecForm(); + + useEffect(() => { + dispatch(initNodes({spans})); + }, [dispatch, spans]); + + const onNodesChange = useCallback((changes: NodeChange[]) => dispatch(onNodesChangeAction({changes})), [dispatch]); + + const onNodeClick = useCallback( + (event, {id}: Node) => { + TraceDiagramAnalyticsService.onClickSpan(id); + onSelectSpan(id); + }, + [onSelectSpan] + ); + + if (spans.length && !nodes.length) { + return ( + + + + ); + } + + return ( + 0 || isOpen} + matchedSpans={matchedSpans} + nodes={nodes} + onNavigateToSpan={onNavigateToSpan} + onNodesChange={onNodesChange} + onNodeClick={onNodeClick} + selectedSpan={focusedSpan} + /> + ); +}; + +export default TestDAG; diff --git a/web/src/components/RunDetailTest/TestPanel.tsx b/web/src/components/RunDetailTest/TestPanel.tsx index 2c7ba26238..e7b3b19dd8 100644 --- a/web/src/components/RunDetailTest/TestPanel.tsx +++ b/web/src/components/RunDetailTest/TestPanel.tsx @@ -1,7 +1,7 @@ import {Tabs} from 'antd'; import {useCallback, useState} from 'react'; import {useSearchParams} from 'react-router-dom'; -import {VisualizationType} from 'components/RunDetailTrace/RunDetailTrace'; +import {VisualizationType, getIsDAGDisabled} from 'components/RunDetailTrace/RunDetailTrace'; import TestOutputs from 'components/TestOutputs'; import TestOutputForm from 'components/TestOutputForm/TestOutputForm'; import TestResults from 'components/TestResults'; @@ -56,7 +56,11 @@ const TestPanel = ({run, testId, runEvents}: IProps) => { const { test: {skipTraceCollection}, } = useTest(); - const [visualizationType, setVisualizationType] = useState(VisualizationType.Dag); + + const isDAGDisabled = getIsDAGDisabled(run?.trace?.spans?.length); + const [visualizationType, setVisualizationType] = useState(() => + isDAGDisabled ? VisualizationType.Timeline : VisualizationType.Dag + ); const handleClose = useCallback(() => { onSetFocusedSpan(''); @@ -111,20 +115,23 @@ const TestPanel = ({run, testId, runEvents}: IProps) => { {run.state === TestState.FINISHED && ( { TestRunAnalytics.onSwitchDiagramView(type); setVisualizationType(type); }} type={visualizationType} + totalSpans={run?.trace?.spans?.length} /> )} {skipTraceCollection && } @@ -217,7 +224,6 @@ const TestPanel = ({run, testId, runEvents}: IProps) => { onDelete={handleDelete} onEdit={handleEdit} onRevert={handleRevert} - onSelectSpan={handleSelectSpan} selectedSpan={selectedSpan?.id} testSpec={selectedTestSpec} /> diff --git a/web/src/components/RunDetailTest/Visualization.tsx b/web/src/components/RunDetailTest/Visualization.tsx index 7e41e954f4..6ea97b2b23 100644 --- a/web/src/components/RunDetailTest/Visualization.tsx +++ b/web/src/components/RunDetailTest/Visualization.tsx @@ -1,66 +1,32 @@ import {useCallback, useEffect} from 'react'; -import {Node, NodeChange} from 'react-flow-renderer'; import {VisualizationType} from 'components/RunDetailTrace/RunDetailTrace'; import RunEvents from 'components/RunEvents'; -import {useTestSpecForm} from 'components/TestSpecForm/TestSpecForm.provider'; -import DAG from 'components/Visualization/components/DAG'; -import Timeline from 'components/Visualization/components/Timeline'; +import TimelineV2 from 'components/Visualization/components/Timeline/TimelineV2'; import {TestRunStage} from 'constants/TestRunEvents.constants'; import {NodeTypesEnum} from 'constants/Visualization.constants'; -import Span from 'models/Span.model'; import TestRunEvent from 'models/TestRunEvent.model'; import {useSpan} from 'providers/Span/Span.provider'; -import {useAppDispatch, useAppSelector} from 'redux/hooks'; -import {initNodes, onNodesChange as onNodesChangeAction} from 'redux/slices/DAG.slice'; -import DAGSelectors from 'selectors/DAG.selectors'; -import TraceAnalyticsService from 'services/Analytics/TestRunAnalytics.service'; -import TraceDiagramAnalyticsService from 'services/Analytics/TraceDiagramAnalytics.service'; +import Trace from 'models/Trace.model'; import TestRunService from 'services/TestRun.service'; import {TTestRunState} from 'types/TestRun.types'; +import TestDAG from './TestDAG'; export interface IProps { + isDAGDisabled: boolean; runEvents: TestRunEvent[]; runState: TTestRunState; - spans: Span[]; type: VisualizationType; + trace: Trace; } -const Visualization = ({runEvents, runState, spans, type}: IProps) => { - const dispatch = useAppDispatch(); - const edges = useAppSelector(DAGSelectors.selectEdges); - const nodes = useAppSelector(DAGSelectors.selectNodes); - const {onSelectSpan, matchedSpans, onSetFocusedSpan, focusedSpan, selectedSpan} = useSpan(); - - const {isOpen} = useTestSpecForm(); - - useEffect(() => { - dispatch(initNodes({spans})); - }, [dispatch, spans]); +const Visualization = ({isDAGDisabled, runEvents, runState, trace, trace: {spans, rootSpan}, type}: IProps) => { + const {onSelectSpan, matchedSpans, onSetFocusedSpan, selectedSpan} = useSpan(); useEffect(() => { if (selectedSpan) return; - const firstSpan = spans.find(span => !span.parentId); - onSelectSpan(firstSpan?.id ?? ''); - }, [onSelectSpan, selectedSpan, spans]); - - const onNodesChange = useCallback((changes: NodeChange[]) => dispatch(onNodesChangeAction({changes})), [dispatch]); - - const onNodeClick = useCallback( - (event, {id}: Node) => { - TraceDiagramAnalyticsService.onClickSpan(id); - onSelectSpan(id); - }, - [onSelectSpan] - ); - - const onNodeClickTimeline = useCallback( - (spanId: string) => { - TraceAnalyticsService.onTimelineSpanClick(spanId); - onSelectSpan(spanId); - }, - [onSelectSpan] - ); + onSelectSpan(rootSpan.id); + }, [onSelectSpan, rootSpan, selectedSpan, spans]); const onNavigateToSpan = useCallback( (spanId: string) => { @@ -74,24 +40,14 @@ const Visualization = ({runEvents, runState, spans, type}: IProps) => { return ; } - return type === VisualizationType.Dag ? ( - 0 || isOpen} - matchedSpans={matchedSpans} - nodes={nodes} - onNavigateToSpan={onNavigateToSpan} - onNodesChange={onNodesChange} - onNodeClick={onNodeClick} - selectedSpan={focusedSpan} - /> + return type === VisualizationType.Dag && !isDAGDisabled ? ( + ) : ( - 0 || isOpen} + diff --git a/web/src/components/RunDetailTrace/AnalyzerPanel.tsx b/web/src/components/RunDetailTrace/AnalyzerPanel.tsx index e8da58fa9a..af25b789d4 100644 --- a/web/src/components/RunDetailTrace/AnalyzerPanel.tsx +++ b/web/src/components/RunDetailTrace/AnalyzerPanel.tsx @@ -1,5 +1,4 @@ import TestRun, {isRunStateFinished} from 'models/TestRun.model'; -import Trace from 'models/Trace.model'; import AnalyzerResult from '../AnalyzerResult/AnalyzerResult'; import SkeletonResponse from '../RunDetailTriggerResponse/SkeletonResponse'; import {RightPanel, PanelContainer} from '../ResizablePanels'; @@ -19,11 +18,7 @@ const AnalyzerPanel = ({run}: IProps) => ( {size => ( - {isRunStateFinished(run.state) ? ( - - ) : ( - - )} + {isRunStateFinished(run.state) ? : } )} diff --git a/web/src/components/RunDetailTrace/RunDetailTrace.tsx b/web/src/components/RunDetailTrace/RunDetailTrace.tsx index 3333e8ec22..5953b75b25 100644 --- a/web/src/components/RunDetailTrace/RunDetailTrace.tsx +++ b/web/src/components/RunDetailTrace/RunDetailTrace.tsx @@ -1,4 +1,5 @@ import ResizablePanels from 'components/ResizablePanels'; +import {MAX_DAG_NODES} from 'constants/Visualization.constants'; import TestRun from 'models/TestRun.model'; import TestRunEvent from 'models/TestRunEvent.model'; import * as S from './RunDetailTrace.styled'; @@ -19,6 +20,10 @@ export enum VisualizationType { Timeline, } +export function getIsDAGDisabled(totalSpans: number = 0): boolean { + return totalSpans > MAX_DAG_NODES; +} + const RunDetailTrace = ({run, runEvents, testId, skipTraceCollection}: IProps) => { return ( diff --git a/web/src/components/RunDetailTrace/Search.tsx b/web/src/components/RunDetailTrace/Search.tsx index 0f33e2cb83..5e04dfd2a6 100644 --- a/web/src/components/RunDetailTrace/Search.tsx +++ b/web/src/components/RunDetailTrace/Search.tsx @@ -5,16 +5,13 @@ import {useCallback, useMemo, useState} from 'react'; import {Editor} from 'components/Inputs'; import {SupportedEditors} from 'constants/Editor.constants'; -import {useTestRun} from 'providers/TestRun/TestRun.provider'; import TracetestAPI from 'redux/apis/Tracetest'; import {useAppDispatch, useAppSelector} from 'redux/hooks'; import {matchSpans, selectSpan, setSearchText} from 'redux/slices/Trace.slice'; import TraceSelectors from 'selectors/Trace.selectors'; -import SpanService from 'services/Span.service'; -import EditorService from 'services/Editor.service'; import * as S from './RunDetailTrace.styled'; -const {useLazyGetSelectedSpansQuery} = TracetestAPI.instance; +const {useGetSearchedSpansMutation} = TracetestAPI.instance; interface IProps { runId: number; @@ -25,35 +22,25 @@ const Search = ({runId, testId}: IProps) => { const [search, setSearch] = useState(''); const dispatch = useAppDispatch(); const matchedSpans = useAppSelector(TraceSelectors.selectMatchedSpans); - const { - run: {trace: {spans = []} = {}}, - } = useTestRun(); - const [getSelectedSpans] = useLazyGetSelectedSpansQuery(); + const [getSearchedSpans] = useGetSearchedSpansMutation(); const handleSearch = useCallback( async (query: string) => { - const isValidSelector = EditorService.getIsQueryValid(SupportedEditors.Selector, query || ''); if (!query) { dispatch(matchSpans({spanIds: []})); dispatch(selectSpan({spanId: ''})); return; } - let spanIds = []; - if (isValidSelector) { - const selectedSpansData = await getSelectedSpans({query, runId, testId}).unwrap(); - spanIds = selectedSpansData.spanIds; - } else { - dispatch(setSearchText({searchText: query})); - spanIds = SpanService.searchSpanList(spans, query); - } - + const {spanIds} = await getSearchedSpans({query, runId, testId}).unwrap(); + dispatch(setSearchText({searchText: query})); dispatch(matchSpans({spanIds})); + if (spanIds.length) { dispatch(selectSpan({spanId: spanIds[0]})); } }, - [dispatch, getSelectedSpans, runId, spans, testId] + [dispatch, getSearchedSpans, runId, testId] ); const onSearch = useMemo(() => debounce(handleSearch, 500), [handleSearch]); @@ -67,7 +54,7 @@ const Search = ({runId, testId}: IProps) => { { onSearch(query); setSearch(query); diff --git a/web/src/components/RunDetailTrace/TraceDAG.tsx b/web/src/components/RunDetailTrace/TraceDAG.tsx new file mode 100644 index 0000000000..a874852440 --- /dev/null +++ b/web/src/components/RunDetailTrace/TraceDAG.tsx @@ -0,0 +1,61 @@ +import {useAppDispatch, useAppSelector} from 'redux/hooks'; +import TraceSelectors from 'selectors/Trace.selectors'; +import {Node, NodeChange} from 'react-flow-renderer'; +import {changeNodes, initNodes, selectSpan} from 'redux/slices/Trace.slice'; +import TraceDiagramAnalyticsService from 'services/Analytics/TraceDiagramAnalytics.service'; +import {useCallback, useEffect} from 'react'; +import Trace from 'models/Trace.model'; +import DAG from '../Visualization/components/DAG'; +import LoadingSpinner, {SpinnerContainer} from '../LoadingSpinner'; + +interface IProps { + trace: Trace; + onNavigateToSpan(spanId: string): void; + matchedSpans: string[]; + selectedSpan: string; +} + +const TraceDAG = ({trace: {spans}, matchedSpans, selectedSpan, onNavigateToSpan}: IProps) => { + const nodes = useAppSelector(TraceSelectors.selectNodes); + const edges = useAppSelector(TraceSelectors.selectEdges); + const isMatchedMode = Boolean(matchedSpans.length); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(initNodes({spans})); + }, [dispatch, spans]); + + const onNodesChange = useCallback((changes: NodeChange[]) => dispatch(changeNodes({changes})), [dispatch]); + + const onNodeClick = useCallback( + (event: React.MouseEvent, {id}: Node) => { + event.stopPropagation(); + TraceDiagramAnalyticsService.onClickSpan(id); + dispatch(selectSpan({spanId: id})); + }, + [dispatch] + ); + + if (spans.length && !nodes.length) { + return ( + + + + ); + } + + return ( + + ); +}; + +export default TraceDAG; diff --git a/web/src/components/RunDetailTrace/TracePanel.tsx b/web/src/components/RunDetailTrace/TracePanel.tsx index 53cc6edb8f..ed958e3f09 100644 --- a/web/src/components/RunDetailTrace/TracePanel.tsx +++ b/web/src/components/RunDetailTrace/TracePanel.tsx @@ -4,7 +4,7 @@ import TraceAnalyticsService from 'services/Analytics/TestRunAnalytics.service'; import {TestState} from 'constants/TestRun.constants'; import TestRunEvent from 'models/TestRunEvent.model'; import Search from './Search'; -import {VisualizationType} from './RunDetailTrace'; +import {VisualizationType, getIsDAGDisabled} from './RunDetailTrace'; import * as S from './RunDetailTrace.styled'; import Switch from '../Visualization/components/Switch/Switch'; import Visualization from './Visualization'; @@ -19,7 +19,10 @@ type TProps = { }; const TracePanel = ({run, testId, runEvents, skipTraceCollection}: TProps) => { - const [visualizationType, setVisualizationType] = useState(VisualizationType.Dag); + const isDAGDisabled = getIsDAGDisabled(run?.trace?.spans?.length); + const [visualizationType, setVisualizationType] = useState(() => + isDAGDisabled ? VisualizationType.Timeline : VisualizationType.Dag + ); return ( @@ -34,18 +37,21 @@ const TracePanel = ({run, testId, runEvents, skipTraceCollection}: TProps) => { {run.state === TestState.FINISHED && ( { TraceAnalyticsService.onSwitchDiagramView(type); setVisualizationType(type); }} type={visualizationType} + totalSpans={run?.trace?.spans?.length} /> )} diff --git a/web/src/components/RunDetailTrace/Visualization.tsx b/web/src/components/RunDetailTrace/Visualization.tsx index 3ec49e140a..b2a52f9135 100644 --- a/web/src/components/RunDetailTrace/Visualization.tsx +++ b/web/src/components/RunDetailTrace/Visualization.tsx @@ -3,64 +3,43 @@ import {TestRunStage} from 'constants/TestRunEvents.constants'; import {NodeTypesEnum} from 'constants/Visualization.constants'; import TestRunEvent from 'models/TestRunEvent.model'; import {useCallback, useEffect} from 'react'; -import {Node, NodeChange} from 'react-flow-renderer'; import {useAppDispatch, useAppSelector} from 'redux/hooks'; -import {changeNodes, initNodes, selectSpan} from 'redux/slices/Trace.slice'; +import {selectSpan} from 'redux/slices/Trace.slice'; import TraceSelectors from 'selectors/Trace.selectors'; -import TraceAnalyticsService from 'services/Analytics/TestRunAnalytics.service'; -import TraceDiagramAnalyticsService from 'services/Analytics/TraceDiagramAnalytics.service'; import TestRunService from 'services/TestRun.service'; +import Trace from 'models/Trace.model'; import {TTestRunState} from 'types/TestRun.types'; -import Span from 'models/Span.model'; -import DAG from '../Visualization/components/DAG'; -import Timeline from '../Visualization/components/Timeline'; +import TimelineV2 from 'components/Visualization/components/Timeline/TimelineV2'; import {VisualizationType} from './RunDetailTrace'; +import TraceDAG from './TraceDAG'; interface IProps { + isDAGDisabled: boolean; runEvents: TestRunEvent[]; runState: TTestRunState; - spans: Span[]; + trace: Trace; type: VisualizationType; } -const Visualization = ({runEvents, runState, spans, type}: IProps) => { +const Visualization = ({isDAGDisabled, runEvents, runState, trace, trace: {spans, rootSpan}, type}: IProps) => { const dispatch = useAppDispatch(); - const edges = useAppSelector(TraceSelectors.selectEdges); - const matchedSpans = useAppSelector(TraceSelectors.selectMatchedSpans); - const nodes = useAppSelector(TraceSelectors.selectNodes); const selectedSpan = useAppSelector(TraceSelectors.selectSelectedSpan); - const isMatchedMode = Boolean(matchedSpans.length); - - useEffect(() => { - dispatch(initNodes({spans})); - }, [dispatch, spans]); + const matchedSpans = useAppSelector(TraceSelectors.selectMatchedSpans); useEffect(() => { if (selectedSpan) return; - const firstSpan = spans.find(span => !span.parentId); - dispatch(selectSpan({spanId: firstSpan?.id ?? ''})); - }, [dispatch, selectedSpan, spans]); - - const onNodesChange = useCallback((changes: NodeChange[]) => dispatch(changeNodes({changes})), [dispatch]); - const onNodeClick = useCallback( - (event: React.MouseEvent, {id}: Node) => { - event.stopPropagation(); - TraceDiagramAnalyticsService.onClickSpan(id); - dispatch(selectSpan({spanId: id})); - }, - [dispatch] - ); + dispatch(selectSpan({spanId: rootSpan.id ?? ''})); + }, [dispatch, rootSpan.id, selectedSpan, spans]); - const onNodeClickTimeline = useCallback( + const onNavigateToSpan = useCallback( (spanId: string) => { - TraceAnalyticsService.onTimelineSpanClick(spanId); dispatch(selectSpan({spanId})); }, [dispatch] ); - const onNavigateToSpan = useCallback( + const onNodeClickTimeline = useCallback( (spanId: string) => { dispatch(selectSpan({spanId})); }, @@ -71,26 +50,21 @@ const Visualization = ({runEvents, runState, spans, type}: IProps) => { return ; } - return type === VisualizationType.Dag ? ( - ) : ( - ); }; diff --git a/web/src/components/TestPlugins/Forms/Kafka/Kafka.tsx b/web/src/components/TestPlugins/Forms/Kafka/Kafka.tsx index 951ad2da29..83b3be1d64 100644 --- a/web/src/components/TestPlugins/Forms/Kafka/Kafka.tsx +++ b/web/src/components/TestPlugins/Forms/Kafka/Kafka.tsx @@ -6,6 +6,7 @@ import useQueryTabs from 'hooks/useQueryTabs'; import {SupportedEditors} from 'constants/Editor.constants'; import {TDraftTest} from 'types/Test.types'; import * as S from './Kafka.styled'; +import SingleLine from '../../../Inputs/SingleLine'; const Kafka = () => { const [activeKey, setActiveKey] = useQueryTabs('auth', 'triggerTab'); @@ -25,7 +26,7 @@ const Kafka = () => { } key="message"> - + { } key="topic"> - + diff --git a/web/src/components/TestResults/TestResults.tsx b/web/src/components/TestResults/TestResults.tsx index 77968585b9..715d768982 100644 --- a/web/src/components/TestResults/TestResults.tsx +++ b/web/src/components/TestResults/TestResults.tsx @@ -31,7 +31,7 @@ const TestResults = ({onDelete, onEdit, onRevert}: IProps) => { onSelectSpan(testSpec?.spanIds[0] || ''); setSelectedSpec(testSpec?.selector); }, - [assertionResults?.resultList, onSelectSpan, onSetFocusedSpan, setSelectedSpec] + [assertionResults, onSelectSpan, onSetFocusedSpan, setSelectedSpec] ); return ( diff --git a/web/src/components/TestSpec/TestSpec.styled.ts b/web/src/components/TestSpec/TestSpec.styled.ts index 28b70e3683..dd5a301bef 100644 --- a/web/src/components/TestSpec/TestSpec.styled.ts +++ b/web/src/components/TestSpec/TestSpec.styled.ts @@ -17,6 +17,7 @@ export const Container = styled.div<{$isDeleted: boolean}>` display: flex; gap: 12px; padding: 16px; + margin-bottom: 16px; > div:first-child { opacity: ${({$isDeleted}) => ($isDeleted ? 0.5 : 1)}; diff --git a/web/src/components/TestSpecDetail/Content.tsx b/web/src/components/TestSpecDetail/Content.tsx index 90dea1340c..4ad20d8bcf 100644 --- a/web/src/components/TestSpecDetail/Content.tsx +++ b/web/src/components/TestSpecDetail/Content.tsx @@ -1,24 +1,21 @@ -import {useEffect, useMemo} from 'react'; +import {useEffect, useMemo, useRef} from 'react'; +import {FixedSizeList as List} from 'react-window'; +import AutoSizer, {Size} from 'react-virtualized-auto-sizer'; -import {SemanticGroupNames} from 'constants/SemanticGroupNames.constants'; -import {useTestRun} from 'providers/TestRun/TestRun.provider'; import {useAppSelector} from 'redux/hooks'; import TestSpecsSelectors from 'selectors/TestSpecs.selectors'; import AssertionService from 'services/Assertion.service'; +import TraceSelectors from 'selectors/Trace.selectors'; import {TAssertionResultEntry} from 'models/AssertionResults.model'; -import {useTest} from 'providers/Test/Test.provider'; -import useScrollTo from 'hooks/useScrollTo'; -import Assertion from './Assertion'; import Header from './Header'; -import SpanHeader from './SpanHeader'; -import * as S from './TestSpecDetail.styled'; +import ResultCard from './ResultCard'; +import Search from './Search'; interface IProps { onClose(): void; onDelete(selector: string): void; onEdit(assertionResult: TAssertionResultEntry, name: string): void; onRevert(originalSelector: string): void; - onSelectSpan(spanId: string): void; selectedSpan?: string; testSpec: TAssertionResultEntry; } @@ -28,17 +25,10 @@ const Content = ({ onDelete, onEdit, onRevert, - onSelectSpan, selectedSpan, testSpec, testSpec: {resultList, selector, spanIds}, }: IProps) => { - const { - run: {trace, id: runId}, - } = useTestRun(); - const { - test: {id: testId}, - } = useTest(); const { isDeleted = false, isDraft = false, @@ -46,12 +36,28 @@ const Content = ({ name = '', } = useAppSelector(state => TestSpecsSelectors.selectSpecBySelector(state, selector)) || {}; const totalPassedChecks = useMemo(() => AssertionService.getTotalPassedChecks(resultList), [resultList]); - const results = useMemo(() => AssertionService.getResultsHashedBySpanId(resultList), [resultList]); - const scrollTo = useScrollTo(); + const matchedSpans = useAppSelector(TraceSelectors.selectMatchedSpans); + const results = useMemo( + () => Object.entries(AssertionService.getResultsHashedBySpanId(resultList, matchedSpans)), + [matchedSpans, resultList] + ); + + const listRef = useRef(null); useEffect(() => { - scrollTo(`assertion-result-${selectedSpan}`); - }, [scrollTo, selectedSpan]); + if (listRef.current) { + const index = results.findIndex(([spanId]) => spanId === selectedSpan); + if (index !== -1) { + listRef?.current?.scrollToItem(index, 'smart'); + } + } + }, [results, selectedSpan]); + + const itemSize = useMemo(() => { + const [, checkResults = []] = results[0]; + + return checkResults.length * 72.59 + 40 + 16; + }, [results]); return ( <> @@ -76,33 +82,22 @@ const Content = ({ title={!selector && !name ? 'All Spans' : name} /> - {Object.entries(results).map(([spanId, checkResults]) => { - const span = trace?.spans.find(({id}) => id === spanId); + - return ( - } - type="inner" - $isSelected={spanId === selectedSpan} - $type={span?.type ?? SemanticGroupNames.General} - id={`assertion-result-${spanId}`} + + {({height, width}: Size) => ( + - onSelectSpan(span?.id ?? '')}> - {checkResults.map(checkResult => ( - - ))} - - - ); - })} + {ResultCard} + + )} + ); }; diff --git a/web/src/components/TestSpecDetail/ResultCard.tsx b/web/src/components/TestSpecDetail/ResultCard.tsx new file mode 100644 index 0000000000..95f4b33876 --- /dev/null +++ b/web/src/components/TestSpecDetail/ResultCard.tsx @@ -0,0 +1,65 @@ +import {useCallback, useMemo} from 'react'; +import {useSpan} from 'providers/Span/Span.provider'; +import {useTest} from 'providers/Test/Test.provider'; +import {ICheckResult} from 'types/Assertion.types'; +import {SemanticGroupNames} from 'constants/SemanticGroupNames.constants'; +import {useTestRun} from 'providers/TestRun/TestRun.provider'; +import * as S from './TestSpecDetail.styled'; +import Assertion from './Assertion'; +import SpanHeader from './SpanHeader'; + +interface IProps { + index: number; + data: [string, ICheckResult[]][]; + style: React.CSSProperties; +} + +const ResultCard = ({index, data, style}: IProps) => { + const [spanId, checkResults] = useMemo(() => data[index], [data, index]); + const { + run: {trace, id: runId}, + } = useTestRun(); + const { + test: {id: testId}, + } = useTest(); + const {selectedSpan, onSetFocusedSpan, onSelectSpan} = useSpan(); + + const onFocusAndSelect = useCallback(() => { + onSelectSpan(spanId); + onSetFocusedSpan(spanId); + }, [onSelectSpan, onSetFocusedSpan, spanId]); + + const span = trace?.flat[spanId]; + + return ( + } + type="inner" + $isSelected={spanId === selectedSpan?.id} + $type={span?.type ?? SemanticGroupNames.General} + id={`assertion-result-${spanId}`} + onClick={() => onSelectSpan(span?.id ?? '')} + > + + {checkResults.map(checkResult => ( + + ))} + + + ); +}; + +export default ResultCard; diff --git a/web/src/components/TestSpecDetail/Search.tsx b/web/src/components/TestSpecDetail/Search.tsx new file mode 100644 index 0000000000..539d9333e5 --- /dev/null +++ b/web/src/components/TestSpecDetail/Search.tsx @@ -0,0 +1,70 @@ +import {Col} from 'antd'; +import {debounce} from 'lodash'; +import {useCallback, useMemo, useState} from 'react'; + +import {Editor} from 'components/Inputs'; +import {SupportedEditors} from 'constants/Editor.constants'; +import TracetestAPI from 'redux/apis/Tracetest'; +import {useTestRun} from 'providers/TestRun/TestRun.provider'; +import {useTest} from 'providers/Test/Test.provider'; +import {useAppDispatch} from 'redux/hooks'; +import {matchSpans, selectSpan, setSearchText} from 'redux/slices/Trace.slice'; +import * as S from './TestSpecDetail.styled'; + +const {useGetSearchedSpansMutation} = TracetestAPI.instance; + +const Search = () => { + const [search, setSearch] = useState(''); + const dispatch = useAppDispatch(); + const [getSearchedSpans] = useGetSearchedSpansMutation(); + const { + run: {id: runId}, + } = useTestRun(); + const { + test: {id: testId}, + } = useTest(); + + const handleSearch = useCallback( + async (query: string) => { + if (!query) { + dispatch(matchSpans({spanIds: []})); + dispatch(selectSpan({spanId: ''})); + return; + } + + const {spanIds} = await getSearchedSpans({query, runId, testId}).unwrap(); + dispatch(setSearchText({searchText: query})); + dispatch(matchSpans({spanIds})); + + if (spanIds.length) { + dispatch(selectSpan({spanId: spanIds[0]})); + } + }, + [dispatch, getSearchedSpans, runId, testId] + ); + + const onSearch = useMemo(() => debounce(handleSearch, 500), [handleSearch]); + const onClear = useCallback(() => { + onSearch(''); + setSearch(''); + }, [onSearch]); + + return ( + + + { + onSearch(query); + setSearch(query); + }} + value={search} + /> + {!!search && } + + + ); +}; + +export default Search; diff --git a/web/src/components/TestSpecDetail/SpanHeader.tsx b/web/src/components/TestSpecDetail/SpanHeader.tsx index 03ed113014..4817ca3d92 100644 --- a/web/src/components/TestSpecDetail/SpanHeader.tsx +++ b/web/src/components/TestSpecDetail/SpanHeader.tsx @@ -1,5 +1,6 @@ import {SettingOutlined, ToolOutlined} from '@ant-design/icons'; +import {Typography} from 'antd'; import * as SSpanNode from 'components/Visualization/components/DAG/BaseSpanNode/BaseSpanNode.styled'; import {SemanticGroupNamesToText} from 'constants/SemanticGroupNames.constants'; import {SpanKindToText} from 'constants/Span.constants'; @@ -16,20 +17,25 @@ const SpanHeader = ({onSelectSpan, span}: IProps) => { const {kind, name, service, system, type} = SpanService.getSpanInfo(span); return ( - onSelectSpan(span?.id ?? '')}> - - {name} - - - {`${service} ${SpanKindToText[kind]}`} - - {Boolean(system) && ( + + onSelectSpan(span?.id ?? '')}> + + {name} - - {system} + + {`${service} ${SpanKindToText[kind]}`} - )} - + {Boolean(system) && ( + + + {system} + + )} + + + {span?.id} + + ); }; diff --git a/web/src/components/TestSpecDetail/TestSpecDetail.styled.ts b/web/src/components/TestSpecDetail/TestSpecDetail.styled.ts index a1b2841500..24b00e82fc 100644 --- a/web/src/components/TestSpecDetail/TestSpecDetail.styled.ts +++ b/web/src/components/TestSpecDetail/TestSpecDetail.styled.ts @@ -1,4 +1,4 @@ -import {CheckCircleFilled, InfoCircleFilled, MinusCircleFilled} from '@ant-design/icons'; +import {CheckCircleFilled, CloseCircleFilled, InfoCircleFilled, MinusCircleFilled} from '@ant-design/icons'; import {Card, Drawer, Typography} from 'antd'; import styled from 'styled-components'; @@ -18,10 +18,6 @@ export const CardContainer = styled(Card)<{$isSelected: boolean; $type: Semantic border: ${({$isSelected, theme}) => $isSelected ? `1px solid ${theme.color.interactive}` : `1px solid ${theme.color.borderLight}`}; - :not(:last-child) { - margin-bottom: 16px; - } - .ant-card-head { border-bottom: ${({theme}) => `1px solid ${theme.color.borderLight}`}; border-top: ${({$type}) => `4px solid ${SemanticGroupNamesToColor[$type]}`}; @@ -108,5 +104,24 @@ export const SpanHeaderContainer = styled.div` cursor: pointer; display: flex; gap: 8px; +`; + +export const Wrapper = styled.div` + align-items: center; + cursor: pointer; + justify-content: space-between; + display: flex; padding: 8px 12px; `; + +export const ClearSearchIcon = styled(CloseCircleFilled)` + position: absolute; + right: 8px; + top: 8px; + color: ${({theme}) => theme.color.textLight}; + cursor: pointer; +`; + +export const SearchContainer = styled(Row)` + margin-bottom: 16px; +`; diff --git a/web/src/components/TestSpecDetail/TestSpecDetail.tsx b/web/src/components/TestSpecDetail/TestSpecDetail.tsx index 8141068bd8..4a7adc3f40 100644 --- a/web/src/components/TestSpecDetail/TestSpecDetail.tsx +++ b/web/src/components/TestSpecDetail/TestSpecDetail.tsx @@ -8,45 +8,32 @@ interface IProps { onDelete(selector: string): void; onEdit(assertionResult: TAssertionResultEntry, name: string): void; onRevert(originalSelector: string): void; - onSelectSpan(spanId: string): void; selectedSpan?: string; testSpec?: TAssertionResultEntry; } -const TestSpecDetail = ({ - isOpen, - onClose, - onDelete, - onEdit, - onRevert, - onSelectSpan, - selectedSpan, - testSpec, -}: IProps) => { - return ( - - {testSpec && ( - - )} - - ); -}; +const TestSpecDetail = ({isOpen, onClose, onDelete, onEdit, onRevert, selectedSpan, testSpec}: IProps) => ( + + {testSpec && ( + + )} + +); export default TestSpecDetail; diff --git a/web/src/components/TestSpecs/TestSpecs.styled.ts b/web/src/components/TestSpecs/TestSpecs.styled.ts index 17cebb4d5b..25433c3f84 100644 --- a/web/src/components/TestSpecs/TestSpecs.styled.ts +++ b/web/src/components/TestSpecs/TestSpecs.styled.ts @@ -3,12 +3,6 @@ import styled from 'styled-components'; import noResultsIcon from 'assets/SpanAssertionsEmptyState.svg'; -export const Container = styled.div` - display: flex; - flex-direction: column; - gap: 16px; -`; - export const EmptyContainer = styled.div` align-items: center; display: flex; diff --git a/web/src/components/TestSpecs/TestSpecs.tsx b/web/src/components/TestSpecs/TestSpecs.tsx index 034b972e38..83aa70b175 100644 --- a/web/src/components/TestSpecs/TestSpecs.tsx +++ b/web/src/components/TestSpecs/TestSpecs.tsx @@ -1,7 +1,8 @@ import TestSpec from 'components/TestSpec'; +import AutoSizer, {Size} from 'react-virtualized-auto-sizer'; +import {FixedSizeList as List} from 'react-window'; import AssertionResults, {TAssertionResultEntry} from 'models/AssertionResults.model'; import Empty from './Empty'; -import * as S from './TestSpecs.styled'; interface IProps { assertionResults?: AssertionResults; @@ -17,20 +18,32 @@ const TestSpecs = ({assertionResults, onDelete, onEdit, onOpen, onRevert}: IProp } return ( - - {assertionResults?.resultList?.map(specResult => - specResult.resultList.length ? ( - - ) : null + + {({height, width}: Size) => ( + + {({index, data}) => { + const specResult = data[index]; + + return specResult.resultList.length ? ( + + ) : null; + }} + )} - + ); }; diff --git a/web/src/components/Visualization/components/DAG/DAG.tsx b/web/src/components/Visualization/components/DAG/DAG.tsx index f7a4fe77a9..c719c23643 100644 --- a/web/src/components/Visualization/components/DAG/DAG.tsx +++ b/web/src/components/Visualization/components/DAG/DAG.tsx @@ -5,6 +5,7 @@ import Actions from './Actions'; import * as S from './DAG.styled'; import TestSpanNode from './TestSpanNode/TestSpanNode'; import TraceSpanNode from './TraceSpanNode/TraceSpanNode'; +import {MAX_DAG_NODES} from '../../../../constants/Visualization.constants'; /** Important to define the nodeTypes outside the component to prevent re-renderings */ const nodeTypes = {traceSpan: TraceSpanNode, testSpan: TestSpanNode}; @@ -46,15 +47,17 @@ const DAG = ({ edges={edges} nodes={nodes} deleteKeyCode={null} - fitView minZoom={0.1} multiSelectionKeyCode={null} nodesConnectable={false} nodeTypes={nodeTypes} + onInit={() => nodes.length >= MAX_DAG_NODES && onNavigateToSpan(nodes[0]?.id)} onNodeClick={onNodeClick} onNodeDragStop={onNodeClick} onNodesChange={onNodesChange} + onlyRenderVisibleElements selectionKeyCode={null} + fitView={nodes.length <= MAX_DAG_NODES} > {isMiniMapActive && } diff --git a/web/src/components/Visualization/components/Navigation/Navigation.tsx b/web/src/components/Visualization/components/Navigation/Navigation.tsx index 8517c217e8..c4ba60e0f4 100644 --- a/web/src/components/Visualization/components/Navigation/Navigation.tsx +++ b/web/src/components/Visualization/components/Navigation/Navigation.tsx @@ -13,6 +13,7 @@ interface IProps { } const Navigation = ({matchedSpans, onNavigateToSpan, selectedSpan}: IProps) => { + // TODO: save matched spans in a different data structure const index = matchedSpans.findIndex(spanId => spanId === selectedSpan) + 1; const navigate = useCallback( diff --git a/web/src/components/Visualization/components/Switch/Switch.styled.ts b/web/src/components/Visualization/components/Switch/Switch.styled.ts index 4cc51f89ba..6e4067bf0a 100644 --- a/web/src/components/Visualization/components/Switch/Switch.styled.ts +++ b/web/src/components/Visualization/components/Switch/Switch.styled.ts @@ -11,10 +11,13 @@ export const Container = styled.div` padding: 7px; `; -export const DAGIcon = styled(ClusterOutlined)<{$isSelected?: boolean}>` +export const DAGIcon = styled(ClusterOutlined)<{$isDisabled?: boolean; $isSelected?: boolean}>` color: ${({$isSelected = false, theme}) => ($isSelected ? theme.color.primary : theme.color.textSecondary)}; - cursor: pointer; font-size: ${({theme}) => theme.size.xl}; + + && { + cursor: ${({$isDisabled}) => ($isDisabled ? 'not-allowed' : 'pointer')}; + } `; export const TimelineIcon = styled(BarsOutlined)<{$isSelected?: boolean}>` diff --git a/web/src/components/Visualization/components/Switch/Switch.tsx b/web/src/components/Visualization/components/Switch/Switch.tsx index 4e4fc4e68c..26458eb11f 100644 --- a/web/src/components/Visualization/components/Switch/Switch.tsx +++ b/web/src/components/Visualization/components/Switch/Switch.tsx @@ -1,17 +1,30 @@ import {Tooltip} from 'antd'; - import {VisualizationType} from 'components/RunDetailTrace/RunDetailTrace'; +import {MAX_DAG_NODES} from 'constants/Visualization.constants'; import * as S from './Switch.styled'; interface IProps { + isDAGDisabled: boolean; onChange(type: VisualizationType): void; type: VisualizationType; + totalSpans?: number; } -const Switch = ({onChange, type}: IProps) => ( +const Switch = ({isDAGDisabled, onChange, type, totalSpans = 0}: IProps) => ( - - onChange(VisualizationType.Dag)} /> + + !isDAGDisabled && onChange(VisualizationType.Dag)} + /> 1 - viewEnd ? 'left' : 'right'; +} + +interface IProps extends IPropsComponent { + span: Span; +} + +const BaseSpanNode = ({index, node, span, style}: IProps) => { + const {collapsedSpans, getScale, matchedSpans, onSpanCollapse, onSpanClick, selectedSpan} = useTimeline(); + const {start: viewStart, end: viewEnd} = getScale(span.startTime, span.endTime); + const hintSide = getHintSide(viewStart, viewEnd); + const isSelected = selectedSpan === node.data.id; + const isMatched = matchedSpans.includes(node.data.id); + const isCollapsed = collapsedSpans.includes(node.data.id); + + return ( +
    + onSpanClick(node.data.id)} + $isEven={index % 2 === 0} + $isMatched={isMatched} + $isSelected={isSelected} + > + + + + + {span.name} + + + + + + + + {span.duration} + + + +
    + ); +}; + +export default BaseSpanNode; diff --git a/web/src/components/Visualization/components/Timeline/BaseSpanNode/ConnectorV2.tsx b/web/src/components/Visualization/components/Timeline/BaseSpanNode/ConnectorV2.tsx new file mode 100644 index 0000000000..cb332a3f84 --- /dev/null +++ b/web/src/components/Visualization/components/Timeline/BaseSpanNode/ConnectorV2.tsx @@ -0,0 +1,56 @@ +import {BaseLeftPaddingV2} from 'constants/Timeline.constants'; +import * as S from '../TimelineV2.styled'; + +interface IProps { + hasParent: boolean; + id: string; + isCollapsed: boolean; + nodeDepth: number; + onCollapse(id: string): void; + totalChildren: number; +} + +const Connector = ({hasParent, id, isCollapsed, nodeDepth, onCollapse, totalChildren}: IProps) => { + const leftPadding = nodeDepth * BaseLeftPaddingV2; + + return ( + + {hasParent && ( + <> + + + + )} + + {totalChildren > 0 ? ( + <> + {!isCollapsed && } + + + {totalChildren} + + { + event.stopPropagation(); + onCollapse(id); + }} + /> + + ) : ( + + )} + + {new Array(nodeDepth).fill(0).map((_, index) => { + return ; + })} + + ); +}; + +export default Connector; diff --git a/web/src/components/Visualization/components/Timeline/Header.tsx b/web/src/components/Visualization/components/Timeline/Header.tsx new file mode 100644 index 0000000000..d5ff7cdc41 --- /dev/null +++ b/web/src/components/Visualization/components/Timeline/Header.tsx @@ -0,0 +1,22 @@ +import Ticks from './Ticks/Ticks'; +import * as S from './TimelineV2.styled'; + +const NUM_TICKS = 5; + +interface IProps { + duration: number; +} + +const Header = ({duration}: IProps) => ( + + + + Span + + + + + +); + +export default Header; diff --git a/web/src/components/Visualization/components/Timeline/ListWrapper.tsx b/web/src/components/Visualization/components/Timeline/ListWrapper.tsx new file mode 100644 index 0000000000..cda954bfb7 --- /dev/null +++ b/web/src/components/Visualization/components/Timeline/ListWrapper.tsx @@ -0,0 +1,33 @@ +import {FixedSizeList as List} from 'react-window'; +import Header from './Header'; +import SpanNodeFactory from './SpanNodeFactoryV2'; +import * as S from './TimelineV2.styled'; +import {useTimeline} from './Timeline.provider'; + +const HEADER_HEIGHT = 242; + +interface IProps { + listRef: React.RefObject; +} + +const ListWrapper = ({listRef}: IProps) => { + const {spans, viewEnd, viewStart} = useTimeline(); + + return ( + +
    + + {SpanNodeFactory} + + + ); +}; + +export default ListWrapper; diff --git a/web/src/components/Visualization/components/Timeline/NavigationWrapper.tsx b/web/src/components/Visualization/components/Timeline/NavigationWrapper.tsx new file mode 100644 index 0000000000..9f3c92ca47 --- /dev/null +++ b/web/src/components/Visualization/components/Timeline/NavigationWrapper.tsx @@ -0,0 +1,10 @@ +import Navigation from '../Navigation'; +import {useTimeline} from './Timeline.provider'; + +const NavigationWrapper = () => { + const {matchedSpans, onSpanNavigation, selectedSpan} = useTimeline(); + + return ; +}; + +export default NavigationWrapper; diff --git a/web/src/components/Visualization/components/Timeline/SpanNodeFactoryV2.tsx b/web/src/components/Visualization/components/Timeline/SpanNodeFactoryV2.tsx new file mode 100644 index 0000000000..9e6aed970c --- /dev/null +++ b/web/src/components/Visualization/components/Timeline/SpanNodeFactoryV2.tsx @@ -0,0 +1,30 @@ +import {NodeTypesEnum} from 'constants/Visualization.constants'; +import {TNode} from 'types/Timeline.types'; +// import TestSpanNode from './TestSpanNode/TestSpanNode'; +import TraceSpanNode from './TraceSpanNode/TraceSpanNodeV2'; + +export interface IPropsComponent { + index: number; + node: TNode; + style: React.CSSProperties; +} + +const ComponentMap: Record React.ReactElement> = { + [NodeTypesEnum.TestSpan]: TraceSpanNode, + [NodeTypesEnum.TraceSpan]: TraceSpanNode, +}; + +interface IProps { + data: TNode[]; + index: number; + style: React.CSSProperties; +} + +const SpanNodeFactory = ({data, ...props}: IProps) => { + const node = data[props.index]; + const Component = ComponentMap[node.type]; + + return ; +}; + +export default SpanNodeFactory; diff --git a/web/src/components/Visualization/components/Timeline/Ticks/Ticks.styled.ts b/web/src/components/Visualization/components/Timeline/Ticks/Ticks.styled.ts new file mode 100644 index 0000000000..0af2ed4f32 --- /dev/null +++ b/web/src/components/Visualization/components/Timeline/Ticks/Ticks.styled.ts @@ -0,0 +1,37 @@ +import {Typography} from 'antd'; +import styled, {css} from 'styled-components'; + +export const Ticks = styled.div` + pointer-events: none; + position: relative; +`; + +export const Tick = styled.div` + align-items: center; + background: ${({theme}) => theme.color.borderLight}; + display: flex; + height: 100%; + position: absolute; + width: 1px; + + :first-child, + :last-child { + width: 0; + } +`; + +export const TickLabel = styled(Typography.Text)<{$isEndAnchor: boolean}>` + color: ${({theme}) => theme.color.text}; + font-size: ${({theme}) => theme.size.sm}; + font-weight: 400; + left: 0.25rem; + position: absolute; + white-space: nowrap; + + ${({$isEndAnchor}) => + $isEndAnchor && + css` + left: initial; + right: 0.25rem; + `}; +`; diff --git a/web/src/components/Visualization/components/Timeline/Ticks/Ticks.tsx b/web/src/components/Visualization/components/Timeline/Ticks/Ticks.tsx new file mode 100644 index 0000000000..5387421b91 --- /dev/null +++ b/web/src/components/Visualization/components/Timeline/Ticks/Ticks.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import Date, {ONE_MILLISECOND} from 'utils/Date'; +import * as S from './Ticks.styled'; + +function getLabels(numTicks: number, startTime: number, endTime: number) { + const viewingDuration = endTime - startTime; + const labels = []; + + for (let i = 0; i < numTicks; i += 1) { + const durationAtTick = startTime + (i / (numTicks - 1)) * viewingDuration; + labels.push(Date.formatDuration(durationAtTick * ONE_MILLISECOND)); + } + + return labels; +} + +interface IProps { + endTime?: number; + numTicks: number; + startTime?: number; +} + +const Ticks = ({endTime = 0, numTicks, startTime = 0}: IProps) => { + const labels = getLabels(numTicks, startTime, endTime); + + return ( + + {labels.map((label, index) => { + const portion = index / (numTicks - 1); + return ( + + = 1}>{label} + + ); + })} + + ); +}; + +export default Ticks; diff --git a/web/src/components/Visualization/components/Timeline/Timeline.provider.tsx b/web/src/components/Visualization/components/Timeline/Timeline.provider.tsx new file mode 100644 index 0000000000..26257e2e6b --- /dev/null +++ b/web/src/components/Visualization/components/Timeline/Timeline.provider.tsx @@ -0,0 +1,127 @@ +import {NodeTypesEnum} from 'constants/Visualization.constants'; +import noop from 'lodash/noop'; +import without from 'lodash/without'; +import Span from 'models/Span.model'; +import TimelineModel from 'models/Timeline.model'; +import {createContext, useCallback, useContext, useMemo, useState} from 'react'; +import {FixedSizeList as List} from 'react-window'; +import TraceAnalyticsService from 'services/Analytics/TestRunAnalytics.service'; +import TimelineService, {TScaleFunction} from 'services/Timeline.service'; +import {TNode} from 'types/Timeline.types'; + +interface IContext { + collapsedSpans: string[]; + getScale: TScaleFunction; + matchedSpans: string[]; + onSpanClick(spanId: string): void; + onSpanCollapse(spanId: string): void; + onSpanNavigation(spanId: string): void; + selectedSpan: string; + spans: TNode[]; + viewEnd: number; + viewStart: number; +} + +export const Context = createContext({ + collapsedSpans: [], + getScale: () => ({start: 0, end: 0}), + matchedSpans: [], + onSpanClick: noop, + onSpanCollapse: noop, + onSpanNavigation: noop, + selectedSpan: '', + spans: [], + viewEnd: 0, + viewStart: 0, +}); + +interface IProps { + children: React.ReactNode; + listRef: React.RefObject; + nodeType: NodeTypesEnum; + spans: Span[]; + onNavigate(spanId: string): void; + onClick(spanId: string): void; + matchedSpans: string[]; + selectedSpan: string; +} + +export const useTimeline = () => useContext(Context); + +const TimelineProvider = ({ + children, + listRef, + nodeType, + spans, + onClick, + onNavigate, + matchedSpans, + selectedSpan, +}: IProps) => { + const [collapsedSpans, setCollapsedSpans] = useState([]); + + const nodes = useMemo(() => TimelineModel(spans, nodeType), [spans, nodeType]); + const filteredNodes = useMemo(() => TimelineService.getFilteredNodes(nodes, collapsedSpans), [collapsedSpans, nodes]); + const [min, max] = useMemo(() => TimelineService.getMinMax(nodes), [nodes]); + const getScale = useCallback(() => TimelineService.createScaleFunc({min, max}), [max, min]); + + const onSpanClick = useCallback( + (spanId: string) => { + TraceAnalyticsService.onTimelineSpanClick(spanId); + onClick(spanId); + }, + [onClick] + ); + + const onSpanNavigation = useCallback( + (spanId: string) => { + onNavigate(spanId); + // TODO: Improve the method to search for the index + const index = filteredNodes.findIndex(node => node.data.id === spanId); + if (index !== -1) { + listRef?.current?.scrollToItem(index, 'start'); + } + }, + [filteredNodes, listRef, onNavigate] + ); + + const onSpanCollapse = useCallback((spanId: string) => { + setCollapsedSpans(prevCollapsed => { + if (prevCollapsed.includes(spanId)) { + return without(prevCollapsed, spanId); + } + return [...prevCollapsed, spanId]; + }); + }, []); + + const value = useMemo( + () => ({ + collapsedSpans, + getScale: getScale(), + matchedSpans, + onSpanClick, + onSpanCollapse, + onSpanNavigation, + selectedSpan, + spans: filteredNodes, + viewEnd: max, + viewStart: min, + }), + [ + collapsedSpans, + filteredNodes, + getScale, + matchedSpans, + max, + min, + onSpanClick, + onSpanCollapse, + onSpanNavigation, + selectedSpan, + ] + ); + + return {children}; +}; + +export default TimelineProvider; diff --git a/web/src/components/Visualization/components/Timeline/TimelineV2.styled.ts b/web/src/components/Visualization/components/Timeline/TimelineV2.styled.ts new file mode 100644 index 0000000000..827d7b88a3 --- /dev/null +++ b/web/src/components/Visualization/components/Timeline/TimelineV2.styled.ts @@ -0,0 +1,150 @@ +import {Typography} from 'antd'; +import {SemanticGroupNames, SemanticGroupNamesToColor} from 'constants/SemanticGroupNames.constants'; +import styled, {css} from 'styled-components'; + +export const Container = styled.div` + padding: 50px 24px 0 24px; +`; + +export const Row = styled.div<{$isEven: boolean; $isMatched: boolean; $isSelected: boolean}>` + background-color: ${({theme, $isEven}) => ($isEven ? theme.color.background : theme.color.white)}; + display: grid; + grid-template-columns: 300px 1fr; + grid-template-rows: 32px; + padding: 0px 16px; + + :hover { + background-color: ${({theme}) => theme.color.backgroundInteractive}; + } + + ${({$isMatched}) => + $isMatched && + css` + background-color: ${({theme}) => theme.color.alertYellow}; + `}; + + ${({$isSelected}) => + $isSelected && + css` + background: rgba(97, 23, 94, 0.1); + + :hover { + background: rgba(97, 23, 94, 0.1); + } + `}; +`; + +export const Col = styled.div` + display: grid; + grid-template-columns: 1fr 8px; +`; + +export const ColDuration = styled.div` + overflow: hidden; + position: relative; +`; + +export const Header = styled.div` + align-items: center; + display: flex; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const NameContainer = styled.div` + overflow: hidden; + text-overflow: ellipsis; +`; + +export const Separator = styled.div` + border-left: 1px solid rgb(222, 227, 236); + cursor: ew-resize; + height: 32px; + padding: 0px 3px; + width: 1px; +`; + +export const Title = styled(Typography.Text)` + color: ${({theme}) => theme.color.text}; + font-size: ${({theme}) => theme.size.sm}; + font-weight: 400; +`; + +export const Connector = styled.svg` + flex-shrink: 0; + overflow: hidden; + overflow-clip-margin: content-box; +`; + +export const SpanBar = styled.div<{$type: SemanticGroupNames}>` + background-color: ${({$type}) => SemanticGroupNamesToColor[$type]}; + border-radius: 3px; + height: 18px; + min-width: 2px; + position: absolute; + top: 7px; +`; + +export const SpanBarLabel = styled.div<{$side: 'left' | 'right'}>` + color: ${({theme}) => theme.color.textSecondary}; + font-size: ${({theme}) => theme.size.xs}; + padding: 1px 4px 0 4px; + position: absolute; + + ${({$side}) => + $side === 'left' + ? css` + right: 100%; + ` + : css` + left: 100%; + `}; +`; + +export const TextConnector = styled.text<{$isActive?: boolean}>` + fill: ${({theme, $isActive}) => ($isActive ? theme.color.white : theme.color.text)}; + font-size: ${({theme}) => theme.size.xs}; +`; + +export const CircleDot = styled.circle` + fill: ${({theme}) => theme.color.textSecondary}; + stroke-width: 2; + stroke: ${({theme}) => theme.color.white}; +`; + +export const LineBase = styled.line` + stroke: ${({theme}) => theme.color.borderLight}; +`; + +export const RectBase = styled.rect<{$isActive?: boolean}>` + fill: ${({theme, $isActive}) => ($isActive ? theme.color.primary : theme.color.white)}; + stroke: ${({theme}) => theme.color.textSecondary}; +`; + +export const RectBaseTransparent = styled(RectBase)` + cursor: pointer; + fill: transparent; +`; + +export const HeaderRow = styled.div` + background-color: ${({theme}) => theme.color.white}; + display: grid; + grid-template-columns: 300px 1fr; + grid-template-rows: 32px; + padding: 0px 16px; +`; + +export const HeaderContent = styled.div` + align-items: center; + display: flex; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const HeaderTitle = styled(Typography.Title)` + && { + margin: 0; + } +`; diff --git a/web/src/components/Visualization/components/Timeline/TimelineV2.tsx b/web/src/components/Visualization/components/Timeline/TimelineV2.tsx new file mode 100644 index 0000000000..b3c72c9078 --- /dev/null +++ b/web/src/components/Visualization/components/Timeline/TimelineV2.tsx @@ -0,0 +1,37 @@ +import {NodeTypesEnum} from 'constants/Visualization.constants'; +import Span from 'models/Span.model'; +import {useRef} from 'react'; +import {FixedSizeList as List} from 'react-window'; +import NavigationWrapper from './NavigationWrapper'; +import TimelineProvider from './Timeline.provider'; +import ListWrapper from './ListWrapper'; + +export interface IProps { + nodeType: NodeTypesEnum; + spans: Span[]; + onNavigate(spanId: string): void; + onClick(spanId: string): void; + matchedSpans: string[]; + selectedSpan: string; +} + +const Timeline = ({nodeType, spans, onClick, onNavigate, matchedSpans, selectedSpan}: IProps) => { + const listRef = useRef(null); + + return ( + + + + + ); +}; + +export default Timeline; diff --git a/web/src/components/Visualization/components/Timeline/TraceSpanNode/TraceSpanNodeV2.tsx b/web/src/components/Visualization/components/Timeline/TraceSpanNode/TraceSpanNodeV2.tsx new file mode 100644 index 0000000000..539eda5ae2 --- /dev/null +++ b/web/src/components/Visualization/components/Timeline/TraceSpanNode/TraceSpanNodeV2.tsx @@ -0,0 +1,19 @@ +import useSpanData from 'hooks/useSpanData'; +// import Header from './Header'; +import BaseSpanNode from '../BaseSpanNode/BaseSpanNodeV2'; +import {IPropsComponent} from '../SpanNodeFactoryV2'; + +const TraceSpanNode = (props: IPropsComponent) => { + const {node} = props; + const {span} = useSpanData(node.data.id); + + return ( + } + span={span} + /> + ); +}; + +export default TraceSpanNode; diff --git a/web/src/constants/Timeline.constants.ts b/web/src/constants/Timeline.constants.ts index a8c3a1c480..4a4677dafd 100644 --- a/web/src/constants/Timeline.constants.ts +++ b/web/src/constants/Timeline.constants.ts @@ -3,3 +3,4 @@ export const AxisOffset = 100; export const NodeHeight = 66; export const NodeOverlayHeight = NodeHeight - 2; export const BaseLeftPadding = 10; +export const BaseLeftPaddingV2 = 16; diff --git a/web/src/constants/Visualization.constants.ts b/web/src/constants/Visualization.constants.ts index b1030ac6e9..9146f4a20d 100644 --- a/web/src/constants/Visualization.constants.ts +++ b/web/src/constants/Visualization.constants.ts @@ -2,3 +2,5 @@ export enum NodeTypesEnum { TraceSpan = 'traceSpan', TestSpan = 'testSpan', } + +export const MAX_DAG_NODES = 200; diff --git a/web/src/hooks/useSpanData.ts b/web/src/hooks/useSpanData.ts index 799bce5a07..a8ff64654d 100644 --- a/web/src/hooks/useSpanData.ts +++ b/web/src/hooks/useSpanData.ts @@ -28,6 +28,8 @@ const useSpanData = (id: string): IUseSpanData => { const span = useAppSelector(state => selectSpanById(state, {testId, runId, spanId: id})); + // TODO: should we get analyzerErrors, testSpecs and testOutputs as part of the trace struct from the BE? + // Right now we are getting them from the testRun struct for each span by spanId const analyzerErrors = useAppSelector(state => selectAnalyzerErrorsBySpanId(state, {testId, runId, spanId: id})); const testSpecs = useAppSelector(state => selectTestSpecsBySpanId(state, {testId, runId, spanId: id})); diff --git a/web/src/models/DAG.model.ts b/web/src/models/DAG.model.ts index 38248f6d71..d1d4ea3598 100644 --- a/web/src/models/DAG.model.ts +++ b/web/src/models/DAG.model.ts @@ -19,7 +19,10 @@ function DAG(spans: Span[], type: NodeTypesEnum) { if (b.id > a.id) return 1; return 0; }); + return DAGService.getEdgesAndNodes(nodesDatum); } +export const getShouldShowDAG = (spanCount: number): boolean => spanCount <= 200; + export default DAG; diff --git a/web/src/models/SearchSpansResult.model.ts b/web/src/models/SearchSpansResult.model.ts new file mode 100644 index 0000000000..affc074b76 --- /dev/null +++ b/web/src/models/SearchSpansResult.model.ts @@ -0,0 +1,22 @@ +import {Model, TTestSchemas} from '../types/Common.types'; + +export type TRawSearchSpansResult = TTestSchemas['SearchSpansResult']; +type SearchSpansResult = Model< + TRawSearchSpansResult, + { + spanIds: string[]; + spansIds?: undefined; + } +>; + +const defaultSearchSpansResult: TRawSearchSpansResult = { + spansIds: [], +}; + +function SearchSpansResult({spansIds = []} = defaultSearchSpansResult): SearchSpansResult { + return { + spanIds: spansIds, + }; +} + +export default SearchSpansResult; diff --git a/web/src/models/Span.model.ts b/web/src/models/Span.model.ts index 37f9c4cbae..580f156b82 100644 --- a/web/src/models/Span.model.ts +++ b/web/src/models/Span.model.ts @@ -48,7 +48,16 @@ const getSpanSignature = ( }, []); }; -const Span = ({id = '', attributes = {}, startTime = 0, endTime = 0, parentId = '', name = ''}: TRawSpan): Span => { +const defaultSpan: TRawSpan = { + id: '', + parentId: '', + name: '', + attributes: {}, + startTime: 0, + endTime: 0, +}; + +const Span = ({id = '', attributes = {}, startTime = 0, endTime = 0, parentId = '', name = ''} = defaultSpan): Span => { const mappedAttributeList: TSpanFlatAttribute[] = [{key: 'name', value: name}]; const attributeList = Object.entries(attributes) .map(([key, value]) => ({ diff --git a/web/src/models/TestRun.model.ts b/web/src/models/TestRun.model.ts index 4e39fedef8..e4dbcc3678 100644 --- a/web/src/models/TestRun.model.ts +++ b/web/src/models/TestRun.model.ts @@ -20,7 +20,7 @@ type TestRun = Model< TRawTestRun, { result: AssertionResults; - trace?: Trace; + trace: Trace; totalAssertionCount: number; failedAssertionCount: number; passedAssertionCount: number; @@ -138,7 +138,7 @@ const TestRun = ({ spanId, state, testVersion, - trace: trace ? Trace(trace) : undefined, + trace: trace ? Trace(trace) : Trace(), totalAssertionCount: getTestResultCount(result), failedAssertionCount: getTestResultCount(result, 'failed'), passedAssertionCount: getTestResultCount(result, 'passed'), diff --git a/web/src/models/Trace.model.ts b/web/src/models/Trace.model.ts index 5fabfab841..b0cdc8f036 100644 --- a/web/src/models/Trace.model.ts +++ b/web/src/models/Trace.model.ts @@ -1,16 +1,35 @@ -import { TTraceSchemas } from 'types/Common.types'; +import {TTraceSchemas} from 'types/Common.types'; import Span from './Span.model'; export type TRawTrace = TTraceSchemas['Trace']; +export type TSpanMap = Record; type Trace = { + flat: TSpanMap; spans: Span[]; traceId: string; + rootSpan: Span; }; -const Trace = ({traceId = '', flat = {}}: TRawTrace): Trace => { +const defaultTrace: TRawTrace = { + traceId: '', + flat: {}, + tree: {}, +}; + +const Trace = ({traceId = '', flat: rawFlat = {}, tree = {}} = defaultTrace): Trace => { + const flat: TSpanMap = {}; + const spans = Object.values(rawFlat).map(raw => { + const span = Span(raw); + flat[span.id || ''] = span; + + return span; + }); + return { traceId, - spans: Object.values(flat).map(rawSpan => Span(rawSpan)), + rootSpan: Span(tree), + flat, + spans, }; }; diff --git a/web/src/models/__tests__/TestRun.model.test.ts b/web/src/models/__tests__/TestRun.model.test.ts index 91c806fb08..2aa1012335 100644 --- a/web/src/models/__tests__/TestRun.model.test.ts +++ b/web/src/models/__tests__/TestRun.model.test.ts @@ -7,7 +7,6 @@ describe('Test Run', () => { const testRunResult = TestRun(rawTestRunResult); expect(testRunResult.id).toEqual(rawTestRunResult.id); - expect(testRunResult.trace).not.toEqual(undefined); expect(testRunResult.totalAssertionCount).toEqual(0); expect(testRunResult.passedAssertionCount).toEqual(0); expect(testRunResult.failedAssertionCount).toEqual(0); @@ -21,7 +20,6 @@ describe('Test Run', () => { const testRunResult = TestRun(rawTestRunResult); - expect(testRunResult.trace).toEqual(undefined); expect(testRunResult.executionTime).toEqual(0); }); }); diff --git a/web/src/providers/TestRun/TestRun.provider.tsx b/web/src/providers/TestRun/TestRun.provider.tsx index 57a378658d..169d8d5801 100644 --- a/web/src/providers/TestRun/TestRun.provider.tsx +++ b/web/src/providers/TestRun/TestRun.provider.tsx @@ -5,6 +5,7 @@ import TestRun, {isRunStateFinished} from 'models/TestRun.model'; import TestRunEvent from 'models/TestRunEvent.model'; import TracetestAPI from 'redux/apis/Tracetest'; import TestProvider from '../Test'; +import LoadingSpinner, { SpinnerContainer } from '../../components/LoadingSpinner'; const {useGetRunByIdQuery, useGetRunEventsQuery, useStopRunMutation, useSkipPollingMutation} = TracetestAPI.instance; @@ -76,7 +77,9 @@ const TestRunProvider = ({children, testId, runId = 0}: IProps) => { ) : ( -
    + + + ); }; diff --git a/web/src/providers/TestSpecs/TestSpecs.provider.tsx b/web/src/providers/TestSpecs/TestSpecs.provider.tsx index 42e8fe4420..ac4179c0e2 100644 --- a/web/src/providers/TestSpecs/TestSpecs.provider.tsx +++ b/web/src/providers/TestSpecs/TestSpecs.provider.tsx @@ -60,11 +60,11 @@ const TestSpecsProvider = ({children, testId, runId}: IProps) => { const {test} = useTest(); const {run} = useTestRun(); - const assertionResults = useAppSelector(state => TestSpecsSelectors.selectAssertionResults(state)); - const specs = useAppSelector(state => TestSpecsSelectors.selectSpecs(state)); - const isDraftMode = useAppSelector(state => TestSpecsSelectors.selectIsDraftMode(state)); - const isLoading = useAppSelector(state => TestSpecsSelectors.selectIsLoading(state)); - const isInitialized = useAppSelector(state => TestSpecsSelectors.selectIsInitialized(state)); + const assertionResults = useAppSelector(TestSpecsSelectors.selectAssertionResults); + const specs = useAppSelector(TestSpecsSelectors.selectSpecs); + const isDraftMode = useAppSelector(TestSpecsSelectors.selectIsDraftMode); + const isLoading = useAppSelector(TestSpecsSelectors.selectIsLoading); + const isInitialized = useAppSelector(TestSpecsSelectors.selectIsInitialized); const selectedSpec = useAppSelector(TestSpecsSelectors.selectSelectedSpec); const selectedTestSpec = useAppSelector(state => TestSpecsSelectors.selectAssertionBySelector(state, selectedSpec!)); diff --git a/web/src/redux/actions/Router.actions.ts b/web/src/redux/actions/Router.actions.ts index a7b195d36b..f404f4d99c 100644 --- a/web/src/redux/actions/Router.actions.ts +++ b/web/src/redux/actions/Router.actions.ts @@ -4,7 +4,7 @@ import {Params} from 'react-router-dom'; import {push} from 'redux-first-history'; import {RouterSearchFields} from 'constants/Common.constants'; import TestSpecsSelectors from 'selectors/TestSpecs.selectors'; -import DAGSelectors from 'selectors/DAG.selectors'; +// import DAGSelectors from 'selectors/DAG.selectors'; import SpanSelectors from 'selectors/Span.selectors'; import {setSelectedSpan} from 'redux/slices/Span.slice'; import {setSelectedSpec} from 'redux/slices/TestSpecs.slice'; @@ -31,10 +31,13 @@ const RouterActions = () => ({ getState() as RootState, Number(positionIndex) ); - const isDagReady = DAGSelectors.selectNodes(getState() as RootState).length > 0; + + // TODO: the default view for big traces is no longer the DAG, so this check is no longer valid + // move the view to the state and check depending on the type + // const isViewReady = DAGSelectors.selectNodes(getState() as RootState).length > 0; if (selectedSpec === assertionResult?.selector) return; - if (assertionResult && isDagReady) dispatch(setSelectedSpec(assertionResult)); + if (assertionResult) dispatch(setSelectedSpec(assertionResult)); } ), updateSelectedSpan: createAsyncThunk( diff --git a/web/src/redux/actions/TestSpecs.actions.ts b/web/src/redux/actions/TestSpecs.actions.ts index 7d3dea5399..f6583b91e5 100644 --- a/web/src/redux/actions/TestSpecs.actions.ts +++ b/web/src/redux/actions/TestSpecs.actions.ts @@ -26,6 +26,7 @@ const TestSpecsActions = () => ({ const specs = TestSpecsSelectors.selectSpecs(getState() as RootState).filter(def => !def.isDeleted); const outputs = selectTestOutputs(getState() as RootState); const rawTest = await TestService.getUpdatedRawTest(test, {definition: {specs}, outputs}); + await dispatch(TestGateway.edit(rawTest, testId)); return dispatch(TestRunGateway.reRun(testId, runId)).unwrap(); } diff --git a/web/src/redux/apis/Tracetest/endpoints/TestRun.endpoint.ts b/web/src/redux/apis/Tracetest/endpoints/TestRun.endpoint.ts index e19a9ff977..61621b1f0b 100644 --- a/web/src/redux/apis/Tracetest/endpoints/TestRun.endpoint.ts +++ b/web/src/redux/apis/Tracetest/endpoints/TestRun.endpoint.ts @@ -8,6 +8,7 @@ import SelectedSpans, {TRawSelectedSpans} from 'models/SelectedSpans.model'; import Test from 'models/Test.model'; import TestRun, {TRawTestRun} from 'models/TestRun.model'; import TestRunEvent, {TRawTestRunEvent} from 'models/TestRunEvent.model'; +import SearchSpansResult, {TRawSearchSpansResult} from 'models/SearchSpansResult.model'; import {KnownSources} from 'models/RunMetadata.model'; import {TRawTestSpecs} from 'models/TestSpecs.model'; import {TTestApiEndpointBuilder} from '../Tracetest.api'; @@ -113,6 +114,14 @@ export const testRunEndpoints = (builder: TTestApiEndpointBuilder) => ({ providesTags: (result, error, {query}) => (result ? [{type: TracetestApiTags.SPAN, id: `${query}-LIST`}] : []), transformResponse: (rawSpanList: TRawSelectedSpans) => SelectedSpans(rawSpanList), }), + getSearchedSpans: builder.mutation({ + query: ({query, testId, runId}) => ({ + url: `/tests/${testId}/run/${runId}/search-spans`, + method: HTTP_METHOD.POST, + body: JSON.stringify({query}), + }), + transformResponse: (raw: TRawSearchSpansResult) => SearchSpansResult(raw), + }), getRunEvents: builder.query({ query: ({runId, testId}) => `/tests/${testId}/run/${runId}/events`, diff --git a/web/src/redux/apis/Tracetest/index.ts b/web/src/redux/apis/Tracetest/index.ts index f81596602d..c0c7935bab 100644 --- a/web/src/redux/apis/Tracetest/index.ts +++ b/web/src/redux/apis/Tracetest/index.ts @@ -68,6 +68,7 @@ const { useLazyTestOtlpConnectionQuery, useTestOtlpConnectionQuery, useResetTestOtlpConnectionMutation, + useGetSearchedSpansMutation, endpoints, } = TracetestAPI.instance; @@ -129,5 +130,6 @@ export { useLazyTestOtlpConnectionQuery, useTestOtlpConnectionQuery, useResetTestOtlpConnectionMutation, + useGetSearchedSpansMutation, endpoints, }; diff --git a/web/src/redux/slices/DAG.slice.ts b/web/src/redux/slices/DAG.slice.ts index db55e0aa55..cea4c45145 100644 --- a/web/src/redux/slices/DAG.slice.ts +++ b/web/src/redux/slices/DAG.slice.ts @@ -1,4 +1,4 @@ -import {createSlice, PayloadAction} from '@reduxjs/toolkit'; +import {createAsyncThunk, createSlice, PayloadAction} from '@reduxjs/toolkit'; import {applyNodeChanges, Edge, MarkerType, Node, NodeChange} from 'react-flow-renderer'; import {theme} from 'constants/Theme.constants'; @@ -23,8 +23,7 @@ const dagSlice = createSlice({ name: 'dag', initialState, reducers: { - initNodes(state, {payload}: PayloadAction<{spans: Span[]}>) { - const {edges, nodes} = DAGModel(payload.spans, NodeTypesEnum.TestSpan); + initNodes(state, {payload: {edges, nodes}}: PayloadAction<{edges: Edge[]; nodes: Node[]}>) { state.edges = edges; state.nodes = nodes; }, @@ -78,5 +77,13 @@ const dagSlice = createSlice({ }, }); -export const {initNodes, onNodesChange} = dagSlice.actions; +export const initNodes = createAsyncThunk( + 'dag/generateDagLayout', + async ({spans}, {dispatch}) => { + const {edges, nodes} = await DAGModel(spans, NodeTypesEnum.TestSpan); + dispatch(dagSlice.actions.initNodes({edges, nodes})); + } +); + +export const {onNodesChange} = dagSlice.actions; export default dagSlice.reducer; diff --git a/web/src/redux/slices/Trace.slice.ts b/web/src/redux/slices/Trace.slice.ts index d258bd9032..542c3f7d10 100644 --- a/web/src/redux/slices/Trace.slice.ts +++ b/web/src/redux/slices/Trace.slice.ts @@ -1,4 +1,4 @@ -import {createSlice, PayloadAction} from '@reduxjs/toolkit'; +import {createAsyncThunk, createSlice, PayloadAction} from '@reduxjs/toolkit'; import {applyNodeChanges, Edge, MarkerType, Node, NodeChange} from 'react-flow-renderer'; import {theme} from 'constants/Theme.constants'; @@ -26,10 +26,10 @@ const traceSlice = createSlice({ name: 'trace', initialState, reducers: { - initNodes(state, {payload}: PayloadAction<{spans: Span[]}>) { - const {edges, nodes} = DAGModel(payload.spans, NodeTypesEnum.TraceSpan); + initNodes(state, {payload: {edges, nodes}}: PayloadAction<{edges: Edge[]; nodes: Node[]}>) { state.edges = edges; state.nodes = nodes; + // Clear state state.matchedSpans = []; state.searchText = ''; @@ -70,5 +70,13 @@ const traceSlice = createSlice({ }, }); -export const {initNodes, changeNodes, selectSpan, matchSpans, setSearchText} = traceSlice.actions; +export const initNodes = createAsyncThunk( + 'trace/generateDagLayout', + async ({spans}, {dispatch}) => { + const {edges, nodes} = await DAGModel(spans, NodeTypesEnum.TraceSpan); + dispatch(traceSlice.actions.initNodes({edges, nodes})); + } +); + +export const {changeNodes, selectSpan, matchSpans, setSearchText} = traceSlice.actions; export default traceSlice.reducer; diff --git a/web/src/selectors/Assertion.selectors.ts b/web/src/selectors/Assertion.selectors.ts index 7351d3a2c8..8635b74055 100644 --- a/web/src/selectors/Assertion.selectors.ts +++ b/web/src/selectors/Assertion.selectors.ts @@ -29,7 +29,7 @@ const selectMatchedSpanList = createSelector(stateSelector, paramsSelector, (sta const {data: {trace} = {}} = TracetestAPI.instance.endpoints.getRunById.select({testId, runId})(state); if (!spanIdList.length) return trace?.spans || []; - return trace?.spans.filter(({id}) => spanIdList.includes(id)) || []; + return spanIdList.map((spanId) => trace!.flat[spanId]); }); const AssertionSelectors = () => { diff --git a/web/src/selectors/Editor.selectors.ts b/web/src/selectors/Editor.selectors.ts new file mode 100644 index 0000000000..9f511e96c2 --- /dev/null +++ b/web/src/selectors/Editor.selectors.ts @@ -0,0 +1,26 @@ +import {uniqBy} from 'lodash'; +import {createSelector} from '@reduxjs/toolkit'; +import {RootState} from 'redux/store'; +import AssertionSelectors from './Assertion.selectors'; +import SpanSelectors from './Span.selectors'; + +const stateSelector = (state: RootState) => state; +const paramsSelector = (state: RootState, testId: string, runId: number) => ({ + testId, + runId, +}); + +export const selectSelectorAttributeList = createSelector(stateSelector, paramsSelector, (state, {testId, runId}) => + AssertionSelectors.selectAllAttributeList(state, testId, runId) +); + +export const selectExpressionAttributeList = createSelector( + stateSelector, + paramsSelector, + SpanSelectors.selectMatchedSpans, + (state, {testId, runId}, spanIds) => { + const attributeList = AssertionSelectors.selectAttributeList(state, testId, runId, spanIds); + + return uniqBy(attributeList, 'key'); + } +); diff --git a/web/src/selectors/Span.selectors.ts b/web/src/selectors/Span.selectors.ts index d1dc43f3ad..9e6ecc8cc0 100644 --- a/web/src/selectors/Span.selectors.ts +++ b/web/src/selectors/Span.selectors.ts @@ -23,9 +23,7 @@ const SpanSelectors = () => ({ selectSpanById: createSelector(stateSelector, paramsSelector, (state, {spanId, testId, runId}) => { const {data: {trace} = {}} = TracetestAPI.instance.endpoints.getRunById.select({testId, runId})(state); - const spanList = trace?.spans || []; - - return spanList.find(span => span.id === spanId); + return trace?.flat[spanId]; }), selectSelectedSpan: createSelector(spansStateSelector, ({selectedSpan}) => selectedSpan), selectFocusedSpan: createSelector(spansStateSelector, ({focusedSpan}) => focusedSpan), diff --git a/web/src/selectors/TestRun.selectors.ts b/web/src/selectors/TestRun.selectors.ts index bcc50c2c24..9638ed067f 100644 --- a/web/src/selectors/TestRun.selectors.ts +++ b/web/src/selectors/TestRun.selectors.ts @@ -15,7 +15,7 @@ const selectTestRun = (state: RootState, params: {testId: string; runId: number; export const selectSpanById = createSelector([selectTestRun, selectParams], (testRun, params) => { const {trace} = testRun; - return trace?.spans?.find(span => span.id === params.spanId) ?? Span({id: params.spanId}); + return trace.flat[params.spanId] || Span({id: params.spanId}); }); const selectAnalyzerErrors = createSelector([selectTestRun], testRun => { diff --git a/web/src/services/Analyzer.service.ts b/web/src/services/Analyzer.service.ts index 12600cc319..a91bc39574 100644 --- a/web/src/services/Analyzer.service.ts +++ b/web/src/services/Analyzer.service.ts @@ -3,14 +3,23 @@ import LinterResult from 'models/LinterResult.model'; const MAX_PLUGIN_SCORE = 100; const AnalyzerService = () => ({ - getPlugins(plugins: LinterResult['plugins'], showOnlyErrors: boolean): LinterResult['plugins'] { + getPlugins( + plugins: LinterResult['plugins'], + showOnlyErrors: boolean, + spanIds: string[] = [] + ): LinterResult['plugins'] { return plugins .filter(plugin => !showOnlyErrors || plugin.score < MAX_PLUGIN_SCORE) .map(plugin => ({ ...plugin, rules: plugin.rules .filter(rule => !showOnlyErrors || !rule.passed) - .map(rule => ({...rule, results: rule?.results?.filter(result => !showOnlyErrors || !result.passed)})), + .map(rule => ({ + ...rule, + results: rule.results.filter( + result => (!spanIds.length || spanIds.includes(result.spanId)) && (!showOnlyErrors || !result.passed) + ), + })), })); }, }); diff --git a/web/src/services/Assertion.service.ts b/web/src/services/Assertion.service.ts index dcbbd21d88..cf616dc3da 100644 --- a/web/src/services/Assertion.service.ts +++ b/web/src/services/Assertion.service.ts @@ -53,9 +53,10 @@ const AssertionService = () => ({ .some(result => !!result); }, - getResultsHashedBySpanId(resultList: AssertionResult[]) { + getResultsHashedBySpanId(resultList: AssertionResult[], spanIds: string[] = []) { return resultList .flatMap(({assertion, spanResults}) => spanResults.map(spanResult => ({result: spanResult, assertion}))) + .filter(({result}) => !spanIds.length || spanIds.includes(result.spanId)) .reduce((prev: Record, curr) => { const items = prev[curr.result.spanId] || []; items.push(curr); diff --git a/web/src/services/DAG.service.ts b/web/src/services/DAG.service.ts index c8ec9358e6..e1fb1a7050 100644 --- a/web/src/services/DAG.service.ts +++ b/web/src/services/DAG.service.ts @@ -1,10 +1,11 @@ import {coordCenter, Dag, dagStratify, layeringSimplex, sugiyama} from 'd3-dag'; -import {MarkerType} from 'react-flow-renderer'; +import {Edge, MarkerType, Node} from 'react-flow-renderer'; import {theme} from 'constants/Theme.constants'; import {INodeDatum} from 'types/DAG.types'; +import {withLowPriority} from '../utils/Common'; -function getDagLayout(nodesDatum: INodeDatum[]) { +function getDagLayout(nodesDatum: INodeDatum[]): Dag, undefined> { const stratify = dagStratify(); const dag = stratify(nodesDatum); @@ -18,7 +19,7 @@ function getDagLayout(nodesDatum: INodeDatum[]) { return dag; } -function getNodes(dagLayout: Dag, undefined>) { +function getNodes(dagLayout: Dag, undefined>): Node[] { return dagLayout.descendants().map(({data: {id, data, type}, x, y}) => ({ data, id, @@ -27,7 +28,7 @@ function getNodes(dagLayout: Dag, undefined>) { })); } -function getEdges(dagLayout: Dag, undefined>) { +function getEdges(dagLayout: Dag, undefined>): Edge[] { return dagLayout.links().map(({source, target}) => ({ animated: false, id: `${source.data.id}-${target.data.id}`, @@ -39,12 +40,12 @@ function getEdges(dagLayout: Dag, undefined>) { } const DAGService = () => ({ - getEdgesAndNodes(nodesDatum: INodeDatum[]) { + async getEdgesAndNodes(nodesDatum: INodeDatum[]): Promise<{edges: Edge[]; nodes: Node[]}> { if (!nodesDatum.length) return {edges: [], nodes: []}; - const dagLayout = getDagLayout(nodesDatum); - const edges = getEdges(dagLayout); - const nodes = getNodes(dagLayout); + const dagLayout = await withLowPriority(getDagLayout)(nodesDatum); + const edges = await withLowPriority(getEdges)(dagLayout); + const nodes = await withLowPriority(getNodes)(dagLayout); return {edges, nodes}; }, diff --git a/web/src/services/Span.service.ts b/web/src/services/Span.service.ts index 1325e2d3e2..c4a439f851 100644 --- a/web/src/services/Span.service.ts +++ b/web/src/services/Span.service.ts @@ -45,6 +45,7 @@ const SpanService = () => ({ ).trim()}]`; }, + // TODO: this is very costly, we might need to move this to the backend searchSpanList(spanList: Span[], searchText: string) { if (!searchText.trim()) return []; diff --git a/web/src/services/TestRun.service.ts b/web/src/services/TestRun.service.ts index e0ad41303f..503b3b8aad 100644 --- a/web/src/services/TestRun.service.ts +++ b/web/src/services/TestRun.service.ts @@ -2,10 +2,12 @@ import {filter, findLastIndex, flow} from 'lodash'; import {TestRunStage, TraceEventType} from 'constants/TestRunEvents.constants'; import AssertionResults from 'models/AssertionResults.model'; import LinterResult from 'models/LinterResult.model'; -import {isRunStateAnalyzingError, isRunStateStopped, isRunStateSucceeded} from 'models/TestRun.model'; +import TestRun, {isRunStateAnalyzingError, isRunStateStopped, isRunStateSucceeded} from 'models/TestRun.model'; import TestRunEvent from 'models/TestRunEvent.model'; import TestRunOutput from 'models/TestRunOutput.model'; import {TAnalyzerErrorsBySpan, TTestOutputsBySpan, TTestRunState, TTestSpecsBySpan} from 'types/TestRun.types'; +import Date from 'utils/Date'; +import {singularOrPlural} from 'utils/Common'; const TestRunService = () => ({ shouldDisplayTraceEvents(state: TTestRunState, numberOfSpans: number) { @@ -96,6 +98,14 @@ const TestRunService = () => ({ return {...prev, [curr.spanId]: [...value, curr]}; }, {}); }, + + getHeaderInfo({createdAt, testVersion, metadata: {source = ''}, trace}: TestRun, triggerType: string) { + const createdTimeAgo = Date.getTimeAgo(createdAt ?? ''); + + return `v${testVersion} • ${triggerType} • Ran ${createdTimeAgo} • ${ + !!trace?.spans.length && `${trace.spans.length} ${singularOrPlural('span', trace?.spans.length)}` + } ${source && `• Run via ${source.toUpperCase()}`}`; + }, }); export default TestRunService(); diff --git a/web/src/services/Timeline.service.ts b/web/src/services/Timeline.service.ts index 610f389dab..8b06096135 100644 --- a/web/src/services/Timeline.service.ts +++ b/web/src/services/Timeline.service.ts @@ -2,6 +2,8 @@ import {stratify} from '@visx/hierarchy'; import {NodeTypesEnum} from 'constants/Visualization.constants'; import {INodeDataSpan, TNode} from 'types/Timeline.types'; +export type TScaleFunction = (start: number, end: number) => {start: number; end: number}; + function getHierarchyNodes(nodesData: INodeDataSpan[]) { return stratify() .id(d => d.id) @@ -48,6 +50,22 @@ const TimelineService = () => ({ const endTimes = nodes.map(node => node.data.endTime); return [Math.min(...startTimes), Math.max(...endTimes)]; }, + + createScaleFunc(viewRange: {min: number; max: number}): TScaleFunction { + const {min, max} = viewRange; + const viewWindow = max - min; + + /** + * Scale function + * @param {number} start The start of the sub-range. + * @param {number} end The end of the sub-range. + * @return {Object} The resultant range. + */ + return (start: number, end: number) => ({ + start: (start - min) / viewWindow, + end: (end - min) / viewWindow, + }); + }, }); export default TimelineService(); diff --git a/web/src/utils/Common.ts b/web/src/utils/Common.ts index 6cdc3dab52..ef2c86c1e9 100644 --- a/web/src/utils/Common.ts +++ b/web/src/utils/Common.ts @@ -87,3 +87,12 @@ export const getParsedURL = (rawUrl: string): URL => { return new URL(rawUrl); }; + +export const withLowPriority = + any>(fn: T): ((...args: Parameters) => Promise>) => + (...args: Parameters): Promise> => + new Promise(resolve => { + setTimeout(() => { + resolve(fn(...args)); + }, 0); + }); diff --git a/web/src/utils/Date.ts b/web/src/utils/Date.ts index d534d4dbe3..19c73d2433 100644 --- a/web/src/utils/Date.ts +++ b/web/src/utils/Date.ts @@ -1,4 +1,21 @@ import {format, formatDistanceToNowStrict, isValid, parseISO} from 'date-fns'; +import dropWhile from 'lodash/dropWhile'; +import round from 'lodash/round'; + +export const ONE_MILLISECOND = 1000 * 1; +const ONE_SECOND = 1000 * ONE_MILLISECOND; +const ONE_MINUTE = 60 * ONE_SECOND; +const ONE_HOUR = 60 * ONE_MINUTE; +const ONE_DAY = 24 * ONE_HOUR; + +const UNIT_STEPS: {unit: string; microseconds: number; ofPrevious: number}[] = [ + {unit: 'd', microseconds: ONE_DAY, ofPrevious: 24}, + {unit: 'h', microseconds: ONE_HOUR, ofPrevious: 60}, + {unit: 'm', microseconds: ONE_MINUTE, ofPrevious: 60}, + {unit: 's', microseconds: ONE_SECOND, ofPrevious: 1000}, + {unit: 'ms', microseconds: ONE_MILLISECOND, ofPrevious: 1000}, + {unit: 'μs', microseconds: 1, ofPrevious: 1000}, +]; const Date = { format(date: string, dateFormat = "EEEE, yyyy/MM/dd 'at' HH:mm:ss") { @@ -8,6 +25,7 @@ const Date = { } return format(isoDate, dateFormat); }, + getTimeAgo(date: string) { const isoDate = parseISO(date); if (!isValid(isoDate)) { @@ -15,9 +33,35 @@ const Date = { } return formatDistanceToNowStrict(isoDate, {addSuffix: true}); }, + isDefaultDate(date: string) { return date === '0001-01-01T00:00:00Z'; }, + + /** + * Format duration for display. + * + * @param {number} duration - microseconds + * @return {string} formatted duration + */ + formatDuration(duration: number): string { + // Drop all units that are too large except the last one + const [primaryUnit, secondaryUnit] = dropWhile( + UNIT_STEPS, + ({microseconds}, index) => index < UNIT_STEPS.length - 1 && microseconds > duration + ); + + if (primaryUnit.ofPrevious === 1000) { + // If the unit is decimal based, display as a decimal + return `${round(duration / primaryUnit.microseconds, 2)}${primaryUnit.unit}`; + } + + const primaryValue = Math.floor(duration / primaryUnit.microseconds); + const primaryUnitString = `${primaryValue}${primaryUnit.unit}`; + const secondaryValue = Math.round((duration / secondaryUnit.microseconds) % primaryUnit.ofPrevious); + const secondaryUnitString = `${secondaryValue}${secondaryUnit.unit}`; + return secondaryValue === 0 ? primaryUnitString : `${primaryUnitString} ${secondaryUnitString}`; + }, }; export default Date; From a7384bcd23f842dcc9bd99a062944dea3a1143bd Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Tue, 13 Feb 2024 15:26:22 -0300 Subject: [PATCH 11/13] disable logger in dashboard mode --- agent/runner/runner.go | 13 +++++++++++++ agent/runner/runstrategy_dashboard.go | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/agent/runner/runner.go b/agent/runner/runner.go index 099fb3060f..b04e60b5de 100644 --- a/agent/runner/runner.go +++ b/agent/runner/runner.go @@ -15,6 +15,7 @@ import ( "github.com/kubeshop/tracetest/cli/pkg/resourcemanager" "go.uber.org/zap" + "go.uber.org/zap/zapcore" ) type Runner struct { @@ -23,6 +24,7 @@ type Runner struct { ui ui.ConsoleUI mode agentConfig.Mode logger *zap.Logger + loggerLevel *zap.AtomicLevel claims jwt.MapClaims } @@ -53,10 +55,21 @@ Once started, Tracetest Agent exposes OTLP ports 4317 and 4318 to ingest traces if enableLogging(flags.LogLevel) { var err error + atom := zap.NewAtomicLevel() logger, err = zap.NewDevelopment() if err != nil { return fmt.Errorf("could not create logger: %w", err) } + + logger = logger.WithOptions(zap.WrapCore(func(c zapcore.Core) zapcore.Core { + return zapcore.NewCore( + zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig()), + zapcore.Lock(os.Stdout), + atom, + ) + })) + + s.loggerLevel = &atom } s.logger = logger diff --git a/agent/runner/runstrategy_dashboard.go b/agent/runner/runstrategy_dashboard.go index bfdca6b4a2..63cccedde3 100644 --- a/agent/runner/runstrategy_dashboard.go +++ b/agent/runner/runstrategy_dashboard.go @@ -15,9 +15,13 @@ import ( "github.com/kubeshop/tracetest/agent/ui/dashboard/sensors" "github.com/kubeshop/tracetest/server/version" v1 "go.opentelemetry.io/proto/otlp/trace/v1" + "go.uber.org/zap" ) func (s *Runner) RunDashboardStrategy(ctx context.Context, cfg agentConfig.Config, uiEndpoint string, sensor sensors.Sensor) error { + enableLogger := s.disableLogger() + defer enableLogger() + if collector := collector.GetActiveCollector(); collector != nil { collector.SetSensor(sensor) } @@ -36,6 +40,14 @@ func (s *Runner) RunDashboardStrategy(ctx context.Context, cfg agentConfig.Confi }, sensor) } +func (s *Runner) disableLogger() func() { + s.loggerLevel.SetLevel(zap.PanicLevel) + + return func() { + s.loggerLevel.SetLevel(zap.DebugLevel) + } +} + type dashboardObserver struct { runs map[string]models.TestRun sensor sensors.Sensor From 914b40465b65ca561156f34234c4cad14f076e2c Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Tue, 13 Feb 2024 15:27:25 -0300 Subject: [PATCH 12/13] keep old log level --- agent/runner/runstrategy_dashboard.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agent/runner/runstrategy_dashboard.go b/agent/runner/runstrategy_dashboard.go index 63cccedde3..cd8f131b3e 100644 --- a/agent/runner/runstrategy_dashboard.go +++ b/agent/runner/runstrategy_dashboard.go @@ -41,10 +41,11 @@ func (s *Runner) RunDashboardStrategy(ctx context.Context, cfg agentConfig.Confi } func (s *Runner) disableLogger() func() { + oldLevel := s.loggerLevel.Level() s.loggerLevel.SetLevel(zap.PanicLevel) return func() { - s.loggerLevel.SetLevel(zap.DebugLevel) + s.loggerLevel.SetLevel(oldLevel) } } From e44e29bdd963fe570ed83493da16bd1e71f79df3 Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Tue, 13 Feb 2024 15:29:50 -0300 Subject: [PATCH 13/13] add comment about why we are disabling logs in dashboard mode --- agent/runner/runstrategy_dashboard.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agent/runner/runstrategy_dashboard.go b/agent/runner/runstrategy_dashboard.go index cd8f131b3e..dbca28e206 100644 --- a/agent/runner/runstrategy_dashboard.go +++ b/agent/runner/runstrategy_dashboard.go @@ -19,6 +19,9 @@ import ( ) func (s *Runner) RunDashboardStrategy(ctx context.Context, cfg agentConfig.Config, uiEndpoint string, sensor sensors.Sensor) error { + // This prevents the agent logger from printing lots of messages + // and override the dashboard UI. + // By calling enableLogger() at the end of this function, the logger behavior is restored enableLogger := s.disableLogger() defer enableLogger()