Browse Source

Feature/simulator2 (#756)

* SimulatorV2

* Improvements to rule simulator.

* Increase color contrast.
pull/758/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
b980d55d37
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      backend/i18n/frontend_en.json
  2. 15
      backend/i18n/frontend_it.json
  3. 15
      backend/i18n/frontend_nl.json
  4. 15
      backend/i18n/frontend_zh.json
  5. 15
      backend/i18n/source/frontend_en.json
  6. 71
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs
  7. 18
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  8. 6
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  9. 30
      backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs
  10. 20
      backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs
  11. 22
      backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs
  12. 11
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs
  13. 81
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs
  14. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs
  15. 2
      frontend/app/features/rules/pages/simulator/rule-simulator-page.component.html
  16. 67
      frontend/app/features/rules/pages/simulator/simulated-rule-event.component.html
  17. 64
      frontend/app/features/rules/pages/simulator/simulated-rule-event.component.scss
  18. 4
      frontend/app/features/rules/pages/simulator/simulated-rule-event.component.ts
  19. 7
      frontend/app/framework/angular/forms/editors/code-editor.component.ts
  20. 4
      frontend/app/shared/services/rules.service.spec.ts
  21. 4
      frontend/app/shared/services/rules.service.ts

15
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?",

15
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?",

15
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?",

15
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": "你真的要触发规则吗?",

15
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?",

71
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
{
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
{
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);
Job = job,
EnrichedEvent = enrichedEvent,
Exception = exception,
SkipReason = SkipReason.Failed
};
}
}
}

18
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<JobResult> jobs, Envelope<IEvent> @event, RuleContext context, CancellationToken ct)
private async Task AddJobsAsync(List<JobResult> jobs, Envelope<IEvent> @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);
}
}

6
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);
}
}
}

30
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<SimulatedRuleEvent>(MaxSimulatedEvents);
var simulatedEvents = new List<SimulatedRuleEvent>(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)

20
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);
}
}
}

22
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; }
}
}

11
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; }
/// <summary>
/// The source event.
/// </summary>
[Required]
public object Event { get; set; }
/// <summary>
/// The enriched event.
/// </summary>
public object? EnrichedEvent { get; set; }
/// <summary>
/// The data for the action.
/// </summary>

81
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<Envelope<AppEvent>>._, A<RuleContext>._))
.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<Envelope<AppEvent>>._, A<RuleContext>._))
.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<Envelope<AppEvent>>._, A<RuleContext>._))
.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<Envelope<AppEvent>>._, A<RuleContext>._))
.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<Envelope<AppEvent>>._, A<RuleContext>._))
.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<Envelope<AppEvent>>._, A<RuleContext>._))
.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<Envelope<AppEvent>>._, A<RuleContext>._))
.MustNotHaveHappened();
@ -424,10 +424,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(@event), context, default))
.Returns(new List<EnrichedEvent> { 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<EnrichedEvent>());
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> { 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<Envelope<AppEvent>>.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);

6
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<RuleContext>.That.Matches(x => x.Rule == rule.RuleDef), default))
.Returns(new List<JobResult> { new JobResult(null) }.ToAsyncEnumerable());
.Returns(new List<JobResult> { 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<RuleContext>.That.Matches(x => x.Rule == rule.RuleDef), default))
.Returns(new List<JobResult> { new JobResult(job) }.ToAsyncEnumerable());
.Returns(new List<JobResult> { 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<IRuleEntity> { rule1, rule2 });
A.CallTo(() => ruleService.CreateJobsAsync(@event, A<RuleContext>.That.Matches(x => x.Rule == rule1.RuleDef), default))
.Returns(new List<JobResult> { new JobResult(job1) }.ToAsyncEnumerable());
.Returns(new List<JobResult> { new JobResult { Job = job1 } }.ToAsyncEnumerable());
A.CallTo(() => ruleService.CreateJobsAsync(@event, A<RuleContext>.That.Matches(x => x.Rule == rule2.RuleDef), default))
.Returns(new List<JobResult>().ToAsyncEnumerable());

2
frontend/app/features/rules/pages/simulator/rule-simulator-page.component.html

@ -1,6 +1,6 @@
<sqx-title message="i18n:rules.ruleSimulator.listPageTitle"></sqx-title>
<sqx-layout layout="simple" titleText="i18n:rules.simulator" [width]="50">
<sqx-layout layout="main" titleText="i18n:rules.simulator" [width]="50" [hideSidebar]="true">
<ng-container menu>
<button type="button" class="btn btn-text-secondary" (click)="simulate()" title="i18n:rules.simulateTooltip">
<i class="icon-play-line"></i> {{ 'rules.simulate' | sqxTranslate }}

67
frontend/app/features/rules/pages/simulator/simulated-rule-event.component.html

@ -9,7 +9,7 @@
<small class="truncate">{{event.skipReason}}</small>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-outline-secondary btn-expand" [class.expanded]="expanded" [disabled]="!event.error && !event.actionData" (click)="expandedChange.emit()">
<button type="button" class="btn btn-outline-secondary btn-expand" [class.expanded]="expanded" (click)="expandedChange.emit()">
<i class="icon-settings"></i>
</button>
</td>
@ -20,17 +20,72 @@
<h4>{{ 'common.details' | sqxTranslate }}</h4>
</div>
<div class="row event-dump">
<div class="form-group" *ngIf="event.actionData">
<div class="event-dump">
<div class="history">
<div class="history-start"></div>
<div>
<div class="history-transition">
{{ 'rules.simulation.eventQueried' | sqxTranslate }}
</div>
<div class="history-state" *ngIf="event.event">
<label>{{ 'common.event' | sqxTranslate }}</label>
<sqx-code-editor [ngModel]="event.event" valueMode="Json" [disabled]="true" [wordWrap]="false" height="auto" [maxLines]="20"></sqx-code-editor>
</div>
<ng-container *ngIf="event.enrichedEvent; else error1">
<div class="history-transition">
{{ 'rules.simulation.eventEnriched' | sqxTranslate }}
</div>
<div class="history-state" *ngIf="event.enrichedEvent">
<label>{{ 'rules.enrichedEvent' | sqxTranslate }}</label>
<sqx-code-editor [ngModel]="event.enrichedEvent" valueMode="Json" [disabled]="true" [wordWrap]="false" height="auto" [maxLines]="20"></sqx-code-editor>
</div>
<ng-container *ngIf="event.enrichedEvent; else error2">
<div class="history-transition">
{{ 'rules.simulation.conditionEvaluated' | sqxTranslate }}
</div>
<div class="history-transition">
{{ 'rules.simulation.actionCreated' | sqxTranslate }}
</div>
<div class="history-state" *ngIf="event.actionData">
<label>{{ 'rules.actionData' | sqxTranslate }}</label>
<sqx-code-editor [ngModel]="data" [disabled]="true" [wordWrap]="true" height="auto"></sqx-code-editor>
<sqx-code-editor [ngModel]="data" [disabled]="true" [wordWrap]="true" height="auto" [maxLines]="20"></sqx-code-editor>
</div>
<div class="history-transition">
{{ 'rules.simulation.actionExecuted' | sqxTranslate }}
</div>
</ng-container>
<ng-template #error2>
<div class="history-transition text-danger">
{{ errorText | sqxTranslate }}
</div>
</ng-template>
</ng-container>
<div class="form-group" *ngIf="event.error">
<ng-template #error1>
<div class="history-transition text-danger">
{{ errorText | sqxTranslate }}
</div>
</ng-template>
<div class="history-state" *ngIf="event.error">
<label>{{ 'common.error' | sqxTranslate }}</label>
<sqx-code-editor [ngModel]="event.error" [disabled]="true" [wordWrap]="true" height="auto"></sqx-code-editor>
<sqx-code-editor [ngModel]="event.error" [disabled]="true" [wordWrap]="true" height="auto" [maxLines]="20"></sqx-code-editor>
</div>
</div>
<div class="history-end"></div>
</div>
</div>
</td>

64
frontend/app/features/rules/pages/simulator/simulated-rule-event.component.scss

@ -36,3 +36,67 @@ td {
}
}
}
$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);
}
}

4
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';

7
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,
});
}
}

4
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}`,

4
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,

Loading…
Cancel
Save