From b980d55d37c3b12148992afc14cbe412d0062506 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 6 Sep 2021 09:31:59 +0200 Subject: [PATCH] Feature/simulator2 (#756) * SimulatorV2 * Improvements to rule simulator. * Increase color contrast. --- backend/i18n/frontend_en.json | 15 ++++ backend/i18n/frontend_it.json | 15 ++++ backend/i18n/frontend_nl.json | 15 ++++ backend/i18n/frontend_zh.json | 15 ++++ backend/i18n/source/frontend_en.json | 15 ++++ .../HandleRules/JobResult.cs | 71 +++++++++++++--- .../HandleRules/RuleService.cs | 18 +++-- .../Rules/RuleEnqueuer.cs | 8 +- .../Rules/Runner/DefaultRuleRunnerService.cs | 30 +++---- .../Rules/Runner/RuleRunnerGrain.cs | 20 ++--- .../Rules/Runner/SimulatedRuleEvent.cs | 22 +++-- .../Rules/Models/SimulatedRuleEventDto.cs | 11 +++ .../HandleRules/RuleServiceTests.cs | 81 ++++++++++--------- .../Rules/RuleEnqueuerTests.cs | 6 +- .../rule-simulator-page.component.html | 2 +- .../simulated-rule-event.component.html | 75 ++++++++++++++--- .../simulated-rule-event.component.scss | 64 +++++++++++++++ .../simulated-rule-event.component.ts | 4 + .../forms/editors/code-editor.component.ts | 7 +- .../app/shared/services/rules.service.spec.ts | 4 + frontend/app/shared/services/rules.service.ts | 4 + 21 files changed, 395 insertions(+), 107 deletions(-) diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 401f02b6f..0b0b4f5bf 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -661,6 +661,7 @@ "rules.empty": "No rule created yet.", "rules.emptyAddRule": "Add Rule", "rules.enqueued": "Rule has been added to the queue.", + "rules.enrichedEvent": "Enriched Event", "rules.itemPageTitle": "Rule", "rules.listPageTitle": "Rules", "rules.loadFailed": "Failed to load Rules. Please reload.", @@ -694,6 +695,20 @@ "rules.runRuleConfirmTitle": "Run rule", "rules.simulate": "Simulate", "rules.simulateTooltip": "Simulate this rules using the last 100 events.", + "rules.simulation.actionCreated": "Job is created from the enriched event and action and added to a job queue.", + "rules.simulation.actionExecuted": "Job will be taken from the queue and executed.", + "rules.simulation.conditionEvaluated": "Event is evaluated, whether it matches the conditions in the tigger. ", + "rules.simulation.errorWrongConditionDoesNotMatch": "Condition does not match to the trigger.", + "rules.simulation.errorWrongDisabled": "Rule is dissabled.", + "rules.simulation.errorWrongEventForTrigger": "Event does not match to the trigger.", + "rules.simulation.errorWrongEventMismatch": "Event does not match to the trigger.", + "rules.simulation.errorWrongFailed": "Internal Error.", + "rules.simulation.errorWrongFromRule": "Event has been created from another rule and will be skipped to prevent endless loops.", + "rules.simulation.errorWrongNoAction": "Action type is obsolete and has been removed.", + "rules.simulation.errorWrongNoTrigger": "Trigger type is obsolete and has been removed.", + "rules.simulation.errorWrongTooOld": "Event is too old.", + "rules.simulation.eventEnriched": "Event is enriched with additional data", + "rules.simulation.eventQueried": "Event is queried from the database", "rules.simulator": "Simulator", "rules.stop": "Rule will stop soon.", "rules.triggerConfirmText": "Do you really want to trigger the rule?", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 579cda1d3..499f00d98 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -661,6 +661,7 @@ "rules.empty": "Nessuna regola è stato ancora creata.", "rules.emptyAddRule": "Aggiungi una regola", "rules.enqueued": "La regola è stata aggiunta alle code.", + "rules.enrichedEvent": "Enriched Event", "rules.itemPageTitle": "Rule", "rules.listPageTitle": "Regole", "rules.loadFailed": "Non è stato possibile caricare le regole. Per favore ricarica.", @@ -694,6 +695,20 @@ "rules.runRuleConfirmTitle": "Esegui la regola", "rules.simulate": "Simulate", "rules.simulateTooltip": "Simulate this rules using the last 100 events.", + "rules.simulation.actionCreated": "Job is created from the enriched event and action and added to a job queue.", + "rules.simulation.actionExecuted": "Job will be taken from the queue and executed.", + "rules.simulation.conditionEvaluated": "Event is evaluated, whether it matches the conditions in the tigger. ", + "rules.simulation.errorWrongConditionDoesNotMatch": "Condition does not match to the trigger.", + "rules.simulation.errorWrongDisabled": "Rule is dissabled.", + "rules.simulation.errorWrongEventForTrigger": "Event does not match to the trigger.", + "rules.simulation.errorWrongEventMismatch": "Event does not match to the trigger.", + "rules.simulation.errorWrongFailed": "Internal Error.", + "rules.simulation.errorWrongFromRule": "Event has been created from another rule and will be skipped to prevent endless loops.", + "rules.simulation.errorWrongNoAction": "Action type is obsolete and has been removed.", + "rules.simulation.errorWrongNoTrigger": "Trigger type is obsolete and has been removed.", + "rules.simulation.errorWrongTooOld": "Event is too old.", + "rules.simulation.eventEnriched": "Event is enriched with additional data", + "rules.simulation.eventQueried": "Event is queried from the database", "rules.simulator": "Simulator", "rules.stop": "La regola si fermerà al più presto.", "rules.triggerConfirmText": "Sei sicuro che voler attivare la regola?", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 81cbeff1b..ca9c5311c 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -661,6 +661,7 @@ "rules.empty": "Nog geen regel aangemaakt.", "rules.emptyAddRule": "Regel toevoegen", "rules.enqueued": "Regel is toegevoegd aan de wachtrij.", + "rules.enrichedEvent": "Enriched Event", "rules.itemPageTitle": "Rule", "rules.listPageTitle": "Regels", "rules.loadFailed": "Laden van regels is mislukt. Laad opnieuw.", @@ -694,6 +695,20 @@ "rules.runRuleConfirmTitle": "Regel uitvoeren", "rules.simulate": "Simulate", "rules.simulateTooltip": "Simulate this rules using the last 100 events.", + "rules.simulation.actionCreated": "Job is created from the enriched event and action and added to a job queue.", + "rules.simulation.actionExecuted": "Job will be taken from the queue and executed.", + "rules.simulation.conditionEvaluated": "Event is evaluated, whether it matches the conditions in the tigger. ", + "rules.simulation.errorWrongConditionDoesNotMatch": "Condition does not match to the trigger.", + "rules.simulation.errorWrongDisabled": "Rule is dissabled.", + "rules.simulation.errorWrongEventForTrigger": "Event does not match to the trigger.", + "rules.simulation.errorWrongEventMismatch": "Event does not match to the trigger.", + "rules.simulation.errorWrongFailed": "Internal Error.", + "rules.simulation.errorWrongFromRule": "Event has been created from another rule and will be skipped to prevent endless loops.", + "rules.simulation.errorWrongNoAction": "Action type is obsolete and has been removed.", + "rules.simulation.errorWrongNoTrigger": "Trigger type is obsolete and has been removed.", + "rules.simulation.errorWrongTooOld": "Event is too old.", + "rules.simulation.eventEnriched": "Event is enriched with additional data", + "rules.simulation.eventQueried": "Event is queried from the database", "rules.simulator": "Simulator", "rules.stop": "Regel stopt binnenkort.", "rules.triggerConfirmText": "Wil je echt de regel activeren?", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index 0d46e6c92..bb8fb1e63 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -661,6 +661,7 @@ "rules.empty": "尚未创建规则。", "rules.emptyAddRule": "添加规则", "rules.enqueued": "规则已加入队列。", + "rules.enrichedEvent": "Enriched Event", "rules.itemPageTitle": "规则", "rules.listPageTitle": "规则", "rules.loadFailed": "加载规则失败。请重新加载。", @@ -694,6 +695,20 @@ "rules.runRuleConfirmTitle": "运行规则", "rules.simulate": "模拟", "rules.simulateTooltip": "使用最近 100 个事件模拟此规则。", + "rules.simulation.actionCreated": "Job is created from the enriched event and action and added to a job queue.", + "rules.simulation.actionExecuted": "Job will be taken from the queue and executed.", + "rules.simulation.conditionEvaluated": "Event is evaluated, whether it matches the conditions in the tigger. ", + "rules.simulation.errorWrongConditionDoesNotMatch": "Condition does not match to the trigger.", + "rules.simulation.errorWrongDisabled": "Rule is dissabled.", + "rules.simulation.errorWrongEventForTrigger": "Event does not match to the trigger.", + "rules.simulation.errorWrongEventMismatch": "Event does not match to the trigger.", + "rules.simulation.errorWrongFailed": "Internal Error.", + "rules.simulation.errorWrongFromRule": "Event has been created from another rule and will be skipped to prevent endless loops.", + "rules.simulation.errorWrongNoAction": "Action type is obsolete and has been removed.", + "rules.simulation.errorWrongNoTrigger": "Trigger type is obsolete and has been removed.", + "rules.simulation.errorWrongTooOld": "Event is too old.", + "rules.simulation.eventEnriched": "Event is enriched with additional data", + "rules.simulation.eventQueried": "Event is queried from the database", "rules.simulator": "模拟器", "rules.stop": "规则很快就会停止。", "rules.triggerConfirmText": "你真的要触发规则吗?", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 401f02b6f..0b0b4f5bf 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -661,6 +661,7 @@ "rules.empty": "No rule created yet.", "rules.emptyAddRule": "Add Rule", "rules.enqueued": "Rule has been added to the queue.", + "rules.enrichedEvent": "Enriched Event", "rules.itemPageTitle": "Rule", "rules.listPageTitle": "Rules", "rules.loadFailed": "Failed to load Rules. Please reload.", @@ -694,6 +695,20 @@ "rules.runRuleConfirmTitle": "Run rule", "rules.simulate": "Simulate", "rules.simulateTooltip": "Simulate this rules using the last 100 events.", + "rules.simulation.actionCreated": "Job is created from the enriched event and action and added to a job queue.", + "rules.simulation.actionExecuted": "Job will be taken from the queue and executed.", + "rules.simulation.conditionEvaluated": "Event is evaluated, whether it matches the conditions in the tigger. ", + "rules.simulation.errorWrongConditionDoesNotMatch": "Condition does not match to the trigger.", + "rules.simulation.errorWrongDisabled": "Rule is dissabled.", + "rules.simulation.errorWrongEventForTrigger": "Event does not match to the trigger.", + "rules.simulation.errorWrongEventMismatch": "Event does not match to the trigger.", + "rules.simulation.errorWrongFailed": "Internal Error.", + "rules.simulation.errorWrongFromRule": "Event has been created from another rule and will be skipped to prevent endless loops.", + "rules.simulation.errorWrongNoAction": "Action type is obsolete and has been removed.", + "rules.simulation.errorWrongNoTrigger": "Trigger type is obsolete and has been removed.", + "rules.simulation.errorWrongTooOld": "Event is too old.", + "rules.simulation.eventEnriched": "Event is enriched with additional data", + "rules.simulation.eventQueried": "Event is queried from the database", "rules.simulator": "Simulator", "rules.stop": "Rule will stop soon.", "rules.triggerConfirmText": "Do you really want to trigger the rule?", diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs index 9fde0e823..588073f06 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs @@ -7,20 +7,69 @@ using System; using Squidex.Domain.Apps.Core.Rules; - -#pragma warning disable SA1313 // Parameter names should begin with lower-case letter +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; namespace Squidex.Domain.Apps.Core.HandleRules { - public sealed record JobResult(RuleJob? Job, Exception? Exception = null, SkipReason SkipReason = default) + public sealed record JobResult { - public static readonly JobResult ConditionDoesNotMatch = new JobResult(null, null, SkipReason.ConditionDoesNotMatch); - public static readonly JobResult Disabled = new JobResult(null, null, SkipReason.Disabled); - public static readonly JobResult EventMismatch = new JobResult(null, null, SkipReason.EventMismatch); - public static readonly JobResult FromRule = new JobResult(null, null, SkipReason.FromRule); - public static readonly JobResult NoAction = new JobResult(null, null, SkipReason.NoAction); - public static readonly JobResult NoTrigger = new JobResult(null, null, SkipReason.NoTrigger); - public static readonly JobResult TooOld = new JobResult(null, null, SkipReason.TooOld); - public static readonly JobResult WrongEventForTrigger = new JobResult(null, null, SkipReason.WrongEventForTrigger); + public static readonly JobResult ConditionDoesNotMatch = new JobResult + { + SkipReason = SkipReason.ConditionDoesNotMatch + }; + + public static readonly JobResult Disabled = new JobResult + { + SkipReason = SkipReason.Disabled + }; + + public static readonly JobResult EventMismatch = new JobResult + { + SkipReason = SkipReason.EventMismatch + }; + + public static readonly JobResult FromRule = new JobResult + { + SkipReason = SkipReason.FromRule + }; + + public static readonly JobResult NoAction = new JobResult + { + SkipReason = SkipReason.NoAction + }; + + public static readonly JobResult NoTrigger = new JobResult + { + SkipReason = SkipReason.NoTrigger + }; + + public static readonly JobResult TooOld = new JobResult + { + SkipReason = SkipReason.TooOld + }; + + public static readonly JobResult WrongEventForTrigger = new JobResult + { + SkipReason = SkipReason.WrongEventForTrigger + }; + + public RuleJob? Job { get; init; } + + public EnrichedEvent? EnrichedEvent { get; init; } + + public Exception? Exception { get; init; } + + public SkipReason SkipReason { get; init; } + + public static JobResult Failed(Exception exception, EnrichedEvent? enrichedEvent = null, RuleJob? job = null) + { + return new JobResult + { + Job = job, + EnrichedEvent = enrichedEvent, + Exception = exception, + SkipReason = SkipReason.Failed + }; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index 6059e1345..29aa64710 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -117,7 +117,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules } catch (Exception ex) { - job = new JobResult(null, ex); + job = JobResult.Failed(ex); } yield return job; @@ -144,7 +144,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules } } - private async Task AddJobsAsync(List jobs, Envelope @event, RuleContext context, CancellationToken ct) + private async Task AddJobsAsync(List jobs, Envelope @event, RuleContext context, + CancellationToken ct) { try { @@ -234,7 +235,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules { if (jobs.Count == 0) { - jobs.Add(new JobResult(null, ex, SkipReason.Failed)); + jobs.Add(new JobResult + { + EnrichedEvent = enrichedEvent, + Exception = ex, + SkipReason = SkipReason.Failed + }); } log.LogError(ex, w => w @@ -245,7 +251,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules } catch (Exception ex) { - jobs.Add(new JobResult(null, ex, SkipReason.Failed)); + jobs.Add(JobResult.Failed(ex)); log.LogError(ex, w => w .WriteProperty("action", "createRuleJob") @@ -282,13 +288,13 @@ namespace Squidex.Domain.Apps.Core.HandleRules job.ActionName = actionName; job.Description = description; - return new JobResult(job, null); + return new JobResult { Job = job, EnrichedEvent = enrichedEvent }; } catch (Exception ex) { job.Description = "Failed to create job"; - return new JobResult(job, ex); + return JobResult.Failed(ex, enrichedEvent, job); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index 48669a9e8..3cbaa119a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -59,11 +59,11 @@ namespace Squidex.Domain.Apps.Entities.Rules var jobs = ruleService.CreateJobsAsync(@event, ruleContext); - await foreach (var (job, ex, _) in jobs) + await foreach (var job in jobs) { - if (job != null) + if (job.Job != null) { - await ruleEventRepository.EnqueueAsync(job, ex); + await ruleEventRepository.EnqueueAsync(job.Job, job.Exception); } } } @@ -101,4 +101,4 @@ namespace Squidex.Domain.Apps.Entities.Rules }); } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs index 050415da9..02904c23e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner var context = GetContext(rule); - var result = new List(MaxSimulatedEvents); + var simulatedEvents = new List(MaxSimulatedEvents); var fromNow = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromDays(7)); @@ -53,28 +53,30 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner if (@event?.Payload is AppEvent appEvent) { - await foreach (var (job, exception, skip) in ruleService.CreateJobsAsync(@event, context, ct)) + await foreach (var result in ruleService.CreateJobsAsync(@event, context, ct)) { - var name = job?.EventName; + var eventName = result.Job?.EventName; - if (string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrWhiteSpace(eventName)) { - name = ruleService.GetName(appEvent); + eventName = ruleService.GetName(appEvent); } - var simulationResult = new SimulatedRuleEvent( - name, - job?.ActionName, - job?.ActionData, - exception?.Message, - skip); - - result.Add(simulationResult); + simulatedEvents.Add(new SimulatedRuleEvent + { + ActionData = result.Job?.ActionData, + ActionName = result.Job?.ActionName, + EnrichedEvent = result.EnrichedEvent, + Error = result.Exception?.Message, + Event = @event.Payload, + EventName = eventName, + SkipReason = result.SkipReason + }); } } } - return result; + return simulatedEvents; } public Task CancelAsync(DomainId appId) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs index 5d3597fad..fed5040c8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -217,22 +217,22 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner { var errors = 0; - await foreach (var (job, ex, _) in ruleService.CreateSnapshotJobsAsync(context, ct)) + await foreach (var job in ruleService.CreateSnapshotJobsAsync(context, ct)) { - if (job != null) + if (job.Job != null) { - await ruleEventRepository.EnqueueAsync(job, ex); + await ruleEventRepository.EnqueueAsync(job.Job, job.Exception); } - else if (ex != null) + else if (job.Exception != null) { errors++; if (errors >= MaxErrors) { - throw ex; + throw job.Exception; } - log.LogWarning(ex, w => w + log.LogWarning(job.Exception, w => w .WriteProperty("action", "runRule") .WriteProperty("status", "failedPartially")); } @@ -255,11 +255,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner { var jobs = ruleService.CreateJobsAsync(@event, context, ct); - await foreach (var (job, ex, _) in jobs) + await foreach (var job in jobs) { - if (job != null) + if (job.Job != null) { - await ruleEventRepository.EnqueueAsync(job, ex); + await ruleEventRepository.EnqueueAsync(job.Job, job.Exception); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs index 03ba31373..8722d8ad1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs @@ -7,16 +7,22 @@ using Squidex.Domain.Apps.Core.HandleRules; -#pragma warning disable SA1313 // Parameter names should begin with lower-case letter - namespace Squidex.Domain.Apps.Entities.Rules.Runner { - public sealed record SimulatedRuleEvent( - string EventName, - string? ActionName, - string? ActionData, - string? Error, - SkipReason SkipReason) + public sealed record SimulatedRuleEvent { + public string EventName { get; init; } + + public object Event { get; init; } + + public object? EnrichedEvent { get; init; } + + public string? ActionName { get; init; } + + public string? ActionData { get; init; } + + public string? Error { get; init; } + + public SkipReason SkipReason { get; init; } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs index df6d38f4e..614a737fd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs @@ -20,6 +20,17 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models [Required] public string EventName { get; set; } + /// + /// The source event. + /// + [Required] + public object Event { get; set; } + + /// + /// The enriched event. + /// + public object? EnrichedEvent { get; set; } + /// /// The data for the action. /// diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index 126eea198..c7953421b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -248,9 +248,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules new EnrichedContentEvent { AppId = appId } }.ToAsyncEnumerable()); - var result = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); - Assert.Equal(2, result.Count(x => x.Job != null && x.Exception == null)); + Assert.Equal(2, jobs.Count(x => x.Job != null && x.Exception == null)); } [Fact] @@ -271,9 +271,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules new EnrichedContentEvent { AppId = appId } }.ToAsyncEnumerable()); - var result = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); - Assert.Equal(2, result.Count(x => x.Job == null && x.Exception != null)); + Assert.Equal(2, jobs.Count(x => x.Job == null && x.Exception != null)); } [Fact] @@ -281,9 +281,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = Envelope.Create(new ContentCreated()); - var (_, _, reason) = await sut.CreateJobsAsync(@event, Rule(disable: true)).SingleAsync(); + var result = await sut.CreateJobsAsync(@event, Rule(disable: true)).SingleAsync(); - Assert.Equal(SkipReason.Disabled, reason); + Assert.Equal(SkipReason.Disabled, result.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); @@ -294,9 +294,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = Envelope.Create(new InvalidEvent()); - var (_, _, reason) = await sut.CreateJobsAsync(@event, Rule()).SingleAsync(); + var result = await sut.CreateJobsAsync(@event, Rule()).SingleAsync(); - Assert.Equal(SkipReason.EventMismatch, reason); + Assert.Equal(SkipReason.EventMismatch, result.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); @@ -307,9 +307,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = Envelope.Create(new ContentCreated()); - var (_, _, reason) = await sut.CreateJobsAsync(@event, RuleInvalidTrigger()).SingleAsync(); + var job = await sut.CreateJobsAsync(@event, RuleInvalidTrigger()).SingleAsync(); - Assert.Equal(SkipReason.NoTrigger, reason); + Assert.Equal(SkipReason.NoTrigger, job.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); @@ -320,9 +320,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = Envelope.Create(new ContentCreated()); - var (_, _, reason) = await sut.CreateJobsAsync(@event, Rule()).SingleAsync(); + var job = await sut.CreateJobsAsync(@event, Rule()).SingleAsync(); - Assert.Equal(SkipReason.WrongEventForTrigger, reason); + Assert.Equal(SkipReason.WrongEventForTrigger, job.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); @@ -336,9 +336,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) .Returns(true); - var (_, _, reason) = await sut.CreateJobsAsync(@event, RuleInvalidAction()).SingleAsync(); + var job = await sut.CreateJobsAsync(@event, RuleInvalidAction()).SingleAsync(); - Assert.Equal(SkipReason.NoAction, reason); + Assert.Equal(SkipReason.NoAction, job.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); @@ -354,9 +354,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) .Returns(true); - var (_, _, reason) = await sut.CreateJobsAsync(@event, Rule(ignoreStale: true)).SingleAsync(); + var job = await sut.CreateJobsAsync(@event, Rule(ignoreStale: true)).SingleAsync(); - Assert.Equal(SkipReason.TooOld, reason); + Assert.Equal(SkipReason.TooOld, job.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); @@ -395,9 +395,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var @event = Envelope.Create(new ContentCreated { FromRule = true }); - var (_, _, reason) = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); - Assert.Equal(SkipReason.FromRule, reason); + Assert.Equal(SkipReason.FromRule, job.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); @@ -424,10 +424,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(@event), context, default)) .Returns(new List { enrichedEvent }.ToAsyncEnumerable()); - var (job, _, reason) = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); - Assert.Equal(SkipReason.ConditionDoesNotMatch, reason); - Assert.NotNull(job); + Assert.Equal(SkipReason.ConditionDoesNotMatch, job.SkipReason); + Assert.Equal(enrichedEvent, job.EnrichedEvent); } [Fact] @@ -443,9 +443,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(@event), context)) .Throws(new InvalidOperationException()); - var (_, _, reason) = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); - Assert.Equal(SkipReason.Failed, reason); + Assert.Equal(SkipReason.Failed, job.SkipReason); } [Fact] @@ -464,9 +464,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(@event), context, default)) .Returns(AsyncEnumerable.Empty()); - var jobs = await sut.CreateJobsAsync(@event, context).ToListAsync(); + var job = await sut.CreateJobsAsync(@event, context).ToListAsync(); - Assert.Empty(jobs); + Assert.Empty(job); } [Fact] @@ -490,9 +490,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(@event), context, default)) .Returns(new List { enrichedEvent }.ToAsyncEnumerable()); - var (_, _, reason) = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); - Assert.Equal(SkipReason.ConditionDoesNotMatch, reason); + Assert.Equal(SkipReason.ConditionDoesNotMatch, job.SkipReason); + Assert.Equal(enrichedEvent, job.EnrichedEvent); } [Fact] @@ -515,9 +516,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(@event), context, default)) .Throws(new InvalidOperationException()); - var (_, _, reason) = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); - Assert.Equal(SkipReason.Failed, reason); + Assert.Equal(SkipReason.Failed, job.SkipReason); } [Fact] @@ -548,9 +549,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleActionHandler.CreateJobAsync(enrichedEvent, context.Rule.Action)) .Returns((actionDescription, new ValidData { Value = 10 })); - var (job, _, _) = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); - AssertJob(now, enrichedEvent, job!); + AssertJob(now, enrichedEvent, job); A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent, MatchPayload(@event))) .MustHaveHappened(); @@ -584,11 +585,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleActionHandler.CreateJobAsync(enrichedEvent, context.Rule.Action)) .Throws(new InvalidOperationException()); - var (job, ex, _) = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); - Assert.NotNull(ex); - Assert.NotNull(job?.ActionData); - Assert.NotNull(job?.Description); + Assert.NotNull(job.Exception); + Assert.NotNull(job.Job?.ActionData); + Assert.NotNull(job.Job?.Description); + Assert.Equal(enrichedEvent, job.EnrichedEvent); A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent, MatchPayload(@event))) .MustHaveHappened(); @@ -631,8 +633,8 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var jobs = await sut.CreateJobsAsync(@event, context, default).ToListAsync(); - AssertJob(now, enrichedEvent1, jobs[0].Job!); - AssertJob(now, enrichedEvent1, jobs[1].Job!); + AssertJob(now, enrichedEvent1, jobs[0]); + AssertJob(now, enrichedEvent2, jobs[1]); A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent1, MatchPayload(@event))) .MustHaveHappened(); @@ -741,8 +743,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules return A>.That.Matches(x => x.Payload == @event.Payload); } - private void AssertJob(Instant now, EnrichedContentEvent enrichedEvent, RuleJob job) + private void AssertJob(Instant now, EnrichedContentEvent enrichedEvent, JobResult result) { + var job = result.Job!; + + Assert.Equal(enrichedEvent, result.EnrichedEvent); Assert.Equal(enrichedEvent.AppId.Id, job.AppId); Assert.Equal(actionData, job.ActionData); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index 0002f5e13..b385127d5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Rules }; A.CallTo(() => ruleService.CreateJobsAsync(@event, A.That.Matches(x => x.Rule == rule.RuleDef), default)) - .Returns(new List { new JobResult(null) }.ToAsyncEnumerable()); + .Returns(new List { new JobResult() }.ToAsyncEnumerable()); await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); @@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Rules }; A.CallTo(() => ruleService.CreateJobsAsync(@event, A.That.Matches(x => x.Rule == rule.RuleDef), default)) - .Returns(new List { new JobResult(job) }.ToAsyncEnumerable()); + .Returns(new List { new JobResult { Job = job } }.ToAsyncEnumerable()); await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); @@ -159,7 +159,7 @@ namespace Squidex.Domain.Apps.Entities.Rules .Returns(new List { rule1, rule2 }); A.CallTo(() => ruleService.CreateJobsAsync(@event, A.That.Matches(x => x.Rule == rule1.RuleDef), default)) - .Returns(new List { new JobResult(job1) }.ToAsyncEnumerable()); + .Returns(new List { new JobResult { Job = job1 } }.ToAsyncEnumerable()); A.CallTo(() => ruleService.CreateJobsAsync(@event, A.That.Matches(x => x.Rule == rule2.RuleDef), default)) .Returns(new List().ToAsyncEnumerable()); diff --git a/frontend/app/features/rules/pages/simulator/rule-simulator-page.component.html b/frontend/app/features/rules/pages/simulator/rule-simulator-page.component.html index 212151364..727e12734 100644 --- a/frontend/app/features/rules/pages/simulator/rule-simulator-page.component.html +++ b/frontend/app/features/rules/pages/simulator/rule-simulator-page.component.html @@ -1,6 +1,6 @@ - + @@ -20,17 +20,72 @@

{{ 'common.details' | sqxTranslate }}

-
-
- - - -
+
+
+
+ +
+
+ {{ 'rules.simulation.eventQueried' | sqxTranslate }} +
+ +
+ + + +
+ + +
+ {{ 'rules.simulation.eventEnriched' | sqxTranslate }} +
-
- +
+ + + +
+ + +
+ {{ 'rules.simulation.conditionEvaluated' | sqxTranslate }} +
+ +
+ {{ 'rules.simulation.actionCreated' | sqxTranslate }} +
+ +
+ + + +
+ +
+ {{ 'rules.simulation.actionExecuted' | sqxTranslate }} +
+
+ +
+ {{ errorText | sqxTranslate }} +
+
+ + + +
+ {{ errorText | sqxTranslate }} +
+
- +
+ + + +
+
+ +
diff --git a/frontend/app/features/rules/pages/simulator/simulated-rule-event.component.scss b/frontend/app/features/rules/pages/simulator/simulated-rule-event.component.scss index ec5f5a4da..ce93f75d6 100644 --- a/frontend/app/features/rules/pages/simulator/simulated-rule-event.component.scss +++ b/frontend/app/features/rules/pages/simulator/simulated-rule-event.component.scss @@ -35,4 +35,68 @@ td { margin: 0; } } +} + +$history-dot-size: 10px; +$history-dot-offset-x: -($history-dot-size * .5 + 1px); +$history-dot-sm-size: 6px; +$history-dot-sm-offset-x: -($history-dot-sm-size * .5 + 1px); + +.history { + border-left: 2px dashed $color-border; + margin: .5rem 0; + padding: .5rem 1.5rem; + padding-right: 0; + position: relative; + + &-state { + position: relative; + + &::before { + @include circle($history-dot-size); + @include absolute(.5rem, null, null, -1.5rem); + background: $color-border; + content: ''; + margin: 0; + margin-left: $history-dot-offset-x; + } + } + + &-transition { + font-size: 85%; + font-weight: normal; + margin: 1rem 0; + position: relative; + + &::before { + @include circle($history-dot-sm-size); + @include absolute(.5rem, null, null, -1.5rem); + background: $color-border; + content: ''; + margin: 0; + margin-left: $history-dot-sm-offset-x; + } + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + + &-start, + &-end { + @include circle($history-dot-size); + background: $color-border; + } + + &-start { + @include absolute(-$history-dot-size * .5, null, null, $history-dot-offset-x); + } + + &-end { + @include absolute(null, null, -$history-dot-size * .5, $history-dot-offset-x); + } } \ No newline at end of file diff --git a/frontend/app/features/rules/pages/simulator/simulated-rule-event.component.ts b/frontend/app/features/rules/pages/simulator/simulated-rule-event.component.ts index 11640c9ec..e9978eaf4 100644 --- a/frontend/app/features/rules/pages/simulator/simulated-rule-event.component.ts +++ b/frontend/app/features/rules/pages/simulator/simulated-rule-event.component.ts @@ -38,6 +38,10 @@ export class SimulatedRuleEventComponent { return result; } + public get errorText() { + return `rules.simulation.error${this.event.skipReason}`; + } + public get status() { if (this.event.error) { return 'Failed'; diff --git a/frontend/app/framework/angular/forms/editors/code-editor.component.ts b/frontend/app/framework/angular/forms/editors/code-editor.component.ts index 7bbe95c63..f045e3bb9 100644 --- a/frontend/app/framework/angular/forms/editors/code-editor.component.ts +++ b/frontend/app/framework/angular/forms/editors/code-editor.component.ts @@ -49,6 +49,9 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im @Input() public valueMode: 'String' | 'Json' = 'String'; + @Input() + public maxLines: number | undefined; + @Input() public wordWrap: boolean; @@ -80,7 +83,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im this.setMode(); } - if (changes['height']) { + if (changes['height'] || changes['maxLines']) { this.setHeight(); } @@ -246,7 +249,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im } else if (this.height === 'auto') { this.aceEditor.setOptions({ minLines: 3, - maxLines: 500, + maxLines: this.maxLines || 500, }); } } diff --git a/frontend/app/shared/services/rules.service.spec.ts b/frontend/app/shared/services/rules.service.spec.ts index 18b63859f..ef8730beb 100644 --- a/frontend/app/shared/services/rules.service.spec.ts +++ b/frontend/app/shared/services/rules.service.spec.ts @@ -416,6 +416,8 @@ describe('RulesService', () => { return { eventName: `name${key}`, + event: { value: 'simple' }, + enrichedEvent: { value: 'enriched' }, actionName: `action-name${key}`, actionData: `action-data${key}`, error: `error${key}`, @@ -479,6 +481,8 @@ export function createSimulatedRuleEvent(id: number, suffix = '') { return new SimulatedRuleEventDto({}, `name${key}`, + { value: 'simple' }, + { value: 'enriched' }, `action-name${key}`, `action-data${key}`, `error${key}`, diff --git a/frontend/app/shared/services/rules.service.ts b/frontend/app/shared/services/rules.service.ts index ebd653cc2..46760e299 100644 --- a/frontend/app/shared/services/rules.service.ts +++ b/frontend/app/shared/services/rules.service.ts @@ -199,6 +199,8 @@ export class SimulatedRuleEventDto { constructor(links: ResourceLinks, public readonly eventName: string, + public readonly event: any, + public readonly enrichedEvent: any | undefined, public readonly actionName: string | undefined, public readonly actionData: string | undefined, public readonly error: string | undefined, @@ -451,6 +453,8 @@ function parseRuleEvent(response: any) { function parseSimulatedRuleEvent(response: any) { return new SimulatedRuleEventDto(response._links, response.eventName, + response.event, + response.enrichedEvent, response.actionName, response.actionData, response.error,