Browse Source

Better rule simulator.

pull/867/head
Sebastian 4 years ago
parent
commit
54e79fe6d8
  1. 18
      backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs
  2. 4
      backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs
  3. 3
      backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs
  4. 14
      backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs
  5. 3
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs
  6. 14
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs
  7. 6
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs
  8. 5
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs
  9. 26
      backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  10. 4
      frontend/src/app/features/administration/state/users.state.ts
  11. 14
      frontend/src/app/features/rules/pages/messages.ts
  12. 35
      frontend/src/app/features/rules/pages/rule/rule-page.component.ts
  13. 4
      frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.html
  14. 24
      frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.ts
  15. 4
      frontend/src/app/framework/angular/forms/model.ts
  16. 33
      frontend/src/app/shared/services/rules.service.spec.ts
  17. 12
      frontend/src/app/shared/services/rules.service.ts
  18. 4
      frontend/src/app/shared/state/rule-events.state.ts
  19. 15
      frontend/src/app/shared/state/rule-simulator.state.spec.ts
  20. 25
      frontend/src/app/shared/state/rule-simulator.state.ts
  21. 4
      frontend/src/app/shared/state/rules.state.ts

18
backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs

@ -8,6 +8,7 @@
using NodaTime; using NodaTime;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -34,16 +35,22 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
this.ruleService = ruleService; this.ruleService = ruleService;
} }
public async Task<List<SimulatedRuleEvent>> SimulateAsync(IRuleEntity rule, public Task<List<SimulatedRuleEvent>> SimulateAsync(IRuleEntity rule,
CancellationToken ct = default)
{
return SimulateAsync(rule.AppId, rule.Id, rule.RuleDef, ct);
}
public async Task<List<SimulatedRuleEvent>> SimulateAsync(NamedId<DomainId> appId, DomainId ruleId, Rule rule,
CancellationToken ct = default) CancellationToken ct = default)
{ {
Guard.NotNull(rule); Guard.NotNull(rule);
var context = new RuleContext var context = new RuleContext
{ {
AppId = rule.AppId, AppId = appId,
Rule = rule.RuleDef, Rule = rule,
RuleId = rule.Id, RuleId = ruleId,
IncludeSkipped = true, IncludeSkipped = true,
IncludeStale = true IncludeStale = true
}; };
@ -52,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
var fromNow = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromDays(7)); var fromNow = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromDays(7));
await foreach (var storedEvent in eventStore.QueryAllReverseAsync($"^([a-zA-Z0-9]+)\\-{rule.AppId.Id}", fromNow, MaxSimulatedEvents, ct)) await foreach (var storedEvent in eventStore.QueryAllReverseAsync($"^([a-zA-Z0-9]+)\\-{appId.Id}", fromNow, MaxSimulatedEvents, ct))
{ {
var @event = eventDataFormatter.ParseIfKnown(storedEvent); var @event = eventDataFormatter.ParseIfKnown(storedEvent);
@ -75,6 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
EnrichedEvent = result.EnrichedEvent, EnrichedEvent = result.EnrichedEvent,
Error = result.EnrichmentError?.Message, Error = result.EnrichmentError?.Message,
Event = @event.Payload, Event = @event.Payload,
EventId = @event.Headers.EventId(),
EventName = eventName, EventName = eventName,
SkipReason = result.SkipReason SkipReason = result.SkipReason
}); });

4
backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs

@ -5,12 +5,16 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Rules.Runner namespace Squidex.Domain.Apps.Entities.Rules.Runner
{ {
public interface IRuleRunnerService public interface IRuleRunnerService
{ {
Task<List<SimulatedRuleEvent>> SimulateAsync(NamedId<DomainId> appId, DomainId ruleId, Rule rule,
CancellationToken ct = default);
Task<List<SimulatedRuleEvent>> SimulateAsync(IRuleEntity rule, Task<List<SimulatedRuleEvent>> SimulateAsync(IRuleEntity rule,
CancellationToken ct = default); CancellationToken ct = default);

3
backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs

@ -6,11 +6,14 @@
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Rules.Runner namespace Squidex.Domain.Apps.Entities.Rules.Runner
{ {
public sealed record SimulatedRuleEvent public sealed record SimulatedRuleEvent
{ {
public Guid EventId { get; init; }
public string EventName { get; init; } public string EventName { get; init; }
public object Event { get; init; } public object Event { get; init; }

14
backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs

@ -7,9 +7,9 @@
using System.Reflection; using System.Reflection;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NJsonSchema.Converters; using NJsonSchema.Converters;
using Squidex.Infrastructure;
#pragma warning disable RECS0108 // Warns about static fields in generic types #pragma warning disable RECS0108 // Warns about static fields in generic types
@ -81,7 +81,17 @@ namespace Squidex.Web.Json
protected override Type GetDiscriminatorType(JObject jObject, Type objectType, string discriminatorValue) protected override Type GetDiscriminatorType(JObject jObject, Type objectType, string discriminatorValue)
{ {
return mapping.GetOrDefault(discriminatorValue) ?? throw new InvalidOperationException($"Could not find subtype of '{objectType.Name}' with discriminator '{discriminatorValue}'."); if (discriminatorValue == null)
{
throw new JsonException("Cannot find discriminator.");
}
if (!mapping.TryGetValue(discriminatorValue, out var type))
{
throw new JsonException($"Could not find subtype of '{objectType.Name}' with discriminator '{discriminatorValue}'.");
}
return type;
} }
public override string GetDiscriminatorValue(Type type) public override string GetDiscriminatorValue(Type type)

3
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs

@ -7,6 +7,7 @@
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -26,7 +27,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public UpdateRole ToCommand(string name) public UpdateRole ToCommand(string name)
{ {
return new UpdateRole { Name = name, Permissions = Permissions, Properties = Properties }; return SimpleMapper.Map(this, new UpdateRole { Name = name });
} }
} }
} }

14
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs

@ -27,16 +27,14 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
[JsonConverter(typeof(RuleActionConverter))] [JsonConverter(typeof(RuleActionConverter))]
public RuleAction Action { get; set; } public RuleAction Action { get; set; }
public CreateRule ToCommand() public Rule ToRule()
{ {
var command = new CreateRule { Action = Action }; return new Rule(Trigger.ToTrigger(), Action);
}
if (Trigger != null)
{
command.Trigger = Trigger.ToTrigger();
}
return command; public CreateRule ToCommand()
{
return new CreateRule { Action = Action, Trigger = Trigger?.ToTrigger() };
} }
} }
} }

6
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs

@ -14,6 +14,12 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
{ {
public sealed record SimulatedRuleEventDto public sealed record SimulatedRuleEventDto
{ {
/// <summary>
/// The unique event id.
/// </summary>
[Required]
public Guid EventId { get; init; }
/// <summary> /// <summary>
/// The name of the event. /// The name of the event.
/// </summary> /// </summary>

5
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs

@ -40,10 +40,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
{ {
var command = SimpleMapper.Map(this, new UpdateRule { RuleId = id }); var command = SimpleMapper.Map(this, new UpdateRule { RuleId = id });
if (Trigger != null) command.Trigger = Trigger?.ToTrigger();
{
command.Trigger = Trigger.ToTrigger();
}
return command; return command;
} }

26
backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -14,6 +14,7 @@ using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Rules.Repositories;
@ -282,6 +283,31 @@ namespace Squidex.Areas.Api.Controllers.Rules
return NoContent(); return NoContent();
} }
/// <summary>
/// Simulate a rule.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">The rule to simulate.</param>
/// <returns>
/// 200 => Rule simulated.
/// 404 => Rule or app not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/rules/simulate/")]
[ProducesResponseType(typeof(SimulatedRuleEventsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppRulesEvents)]
[ApiCosts(5)]
public async Task<IActionResult> Simulate(string app, [FromBody] CreateRuleDto request)
{
var rule = request.ToRule();
var simulation = await ruleRunnerService.SimulateAsync(App.NamedId(), DomainId.Empty, rule, HttpContext.RequestAborted);
var response = SimulatedRuleEventsDto.FromDomain(simulation);
return Ok(response);
}
/// <summary> /// <summary>
/// Simulate a rule. /// Simulate a rule.
/// </summary> /// </summary>

4
frontend/src/app/features/administration/state/users.state.ts

@ -88,7 +88,9 @@ export class UsersState extends State<Snapshot> {
public load(isReload = false, update: Partial<Snapshot> = {}): Observable<any> { public load(isReload = false, update: Partial<Snapshot> = {}): Observable<any> {
if (!isReload) { if (!isReload) {
this.resetState({ selectedUser: this.snapshot.selectedUser, ...update }, 'Loading Initial'); const { selectedUser } = this.snapshot;
this.resetState({ selectedUser, ...update }, 'Loading Initial');
} }
return this.loadInternal(isReload); return this.loadInternal(isReload);

14
frontend/src/app/features/rules/pages/messages.ts

@ -0,0 +1,14 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export class RuleConfigured {
constructor(
public readonly trigger: any,
public readonly action: any,
) {
}
}

35
frontend/src/app/features/rules/pages/rule/rule-page.component.ts

@ -6,8 +6,11 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ActionForm, ALL_TRIGGERS, ResourceOwner, RuleDto, RuleElementDto, RulesService, RulesState, SchemasState, TriggerForm } from '@app/shared'; import { debounceTime, Subscription } from 'rxjs';
import { ActionForm, ALL_TRIGGERS, MessageBus, ResourceOwner, RuleDto, RuleElementDto, RulesService, RulesState, SchemasState, TriggerForm, value$ } from '@app/shared';
import { RuleConfigured } from '../messages';
type ComponentState<T> = { type: string; values: any; form: T }; type ComponentState<T> = { type: string; values: any; form: T };
@ -17,13 +20,16 @@ type ComponentState<T> = { type: string; values: any; form: T };
templateUrl: './rule-page.component.html', templateUrl: './rule-page.component.html',
}) })
export class RulePageComponent extends ResourceOwner implements OnInit { export class RulePageComponent extends ResourceOwner implements OnInit {
public supportedActions: { [name: string]: RuleElementDto } = {}; private currentTriggerSubscription?: Subscription;
private currentActionSubscription?: Subscription;
public supportedTriggers = ALL_TRIGGERS; public supportedTriggers = ALL_TRIGGERS;
public supportedActions: { [name: string]: RuleElementDto } = {};
public rule?: RuleDto | null; public rule?: RuleDto | null;
public currentAction?: ComponentState<ActionForm>;
public currentTrigger?: ComponentState<TriggerForm>; public currentTrigger?: ComponentState<TriggerForm>;
public currentAction?: ComponentState<ActionForm>;
public isEnabled = false; public isEnabled = false;
public isEditable = false; public isEditable = false;
@ -44,6 +50,7 @@ export class RulePageComponent extends ResourceOwner implements OnInit {
public readonly rulesState: RulesState, public readonly rulesState: RulesState,
public readonly rulesService: RulesService, public readonly rulesService: RulesService,
public readonly schemasState: SchemasState, public readonly schemasState: SchemasState,
private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly router: Router, private readonly router: Router,
) { ) {
@ -91,6 +98,8 @@ export class RulePageComponent extends ResourceOwner implements OnInit {
this.currentAction = { form, type, values }; this.currentAction = { form, type, values };
this.currentAction.form.setEnabled(this.isEditable); this.currentAction.form.setEnabled(this.isEditable);
this.currentActionSubscription?.unsubscribe();
this.currentActionSubscription = this.subscribe(form.form);
} }
this.currentAction!.form.load(values); this.currentAction!.form.load(values);
@ -102,11 +111,17 @@ export class RulePageComponent extends ResourceOwner implements OnInit {
this.currentTrigger = { form, type, values }; this.currentTrigger = { form, type, values };
this.currentTrigger.form.setEnabled(this.isEditable); this.currentTrigger.form.setEnabled(this.isEditable);
this.currentTriggerSubscription?.unsubscribe();
this.currentTriggerSubscription = this.subscribe(form.form);
} }
this.currentTrigger.form.load(values); this.currentTrigger.form.load(values);
} }
private subscribe(form: AbstractControl) {
return value$(form).pipe(debounceTime(100)).subscribe(() => this.publishState());
}
public resetAction() { public resetAction() {
this.currentAction = undefined; this.currentAction = undefined;
} }
@ -163,6 +178,20 @@ export class RulePageComponent extends ResourceOwner implements OnInit {
} }
} }
private publishState() {
if (!this.currentAction || !this.currentTrigger) {
return;
}
if (!this.currentAction.form.form.valid || !this.currentTrigger.form.form.valid) {
return;
}
this.messageBus.emit(new RuleConfigured(
this.currentTrigger.form.getValue(),
this.currentAction.form.getValue()));
}
private submitCompleted() { private submitCompleted() {
this.currentAction?.form.submitCompleted({ noReset: true }); this.currentAction?.form.submitCompleted({ noReset: true });
this.currentTrigger?.form.submitCompleted({ noReset: true }); this.currentTrigger?.form.submitCompleted({ noReset: true });

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

@ -25,9 +25,9 @@
</tr> </tr>
</thead> </thead>
<tbody *ngFor="let event of ruleSimulatorState.simulatedRuleEvents | async" <tbody *ngFor="let event of ruleSimulatorState.simulatedRuleEvents | async; trackBy: trackByEvent"
[sqxSimulatedRuleEvent]="event" [sqxSimulatedRuleEvent]="event"
[expanded]="event === selectedRuleEvent" [expanded]="event.eventId === selectedRuleEvent"
(expandedChange)="selectEvent(event)"> (expandedChange)="selectEvent(event)">
</tbody> </tbody>
</table> </table>

24
frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.ts

@ -7,7 +7,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { ResourceOwner, RuleSimulatorState, SimulatedRuleEventDto } from '@app/shared'; import { MessageBus, ResourceOwner, RuleSimulatorState, SimulatedRuleEventDto } from '@app/shared';
import { RuleConfigured } from '../messages';
@Component({ @Component({
selector: 'sqx-simulator-events-page', selector: 'sqx-simulator-events-page',
@ -15,16 +16,23 @@ import { ResourceOwner, RuleSimulatorState, SimulatedRuleEventDto } from '@app/s
templateUrl: './rule-simulator-page.component.html', templateUrl: './rule-simulator-page.component.html',
}) })
export class RuleSimulatorPageComponent extends ResourceOwner implements OnInit { export class RuleSimulatorPageComponent extends ResourceOwner implements OnInit {
public selectedRuleEvent?: SimulatedRuleEventDto | null; public selectedRuleEvent?: string | null;
constructor( constructor(
private route: ActivatedRoute,
public readonly ruleSimulatorState: RuleSimulatorState, public readonly ruleSimulatorState: RuleSimulatorState,
private readonly route: ActivatedRoute,
private readonly messageBus: MessageBus,
) { ) {
super(); super();
} }
public ngOnInit() { public ngOnInit() {
this.own(
this.messageBus.of(RuleConfigured)
.subscribe(message => {
this.ruleSimulatorState.setRule(message.trigger, message.action);
}));
this.own( this.own(
this.route.queryParams this.route.queryParams
.subscribe(query => { .subscribe(query => {
@ -33,14 +41,18 @@ export class RuleSimulatorPageComponent extends ResourceOwner implements OnInit
} }
public simulate() { public simulate() {
this.ruleSimulatorState.load(); this.ruleSimulatorState.load(true);
} }
public selectEvent(event: SimulatedRuleEventDto) { public selectEvent(event: SimulatedRuleEventDto) {
if (this.selectedRuleEvent === event) { if (this.selectedRuleEvent === event.eventId) {
this.selectedRuleEvent = null; this.selectedRuleEvent = null;
} else { } else {
this.selectedRuleEvent = event; this.selectedRuleEvent = event.eventId;
} }
} }
public trackByEvent(_index: number, event: SimulatedRuleEventDto) {
return event.eventId;
}
} }

4
frontend/src/app/framework/angular/forms/model.ts

@ -80,6 +80,10 @@ export class Form<T extends AbstractControl, TOut, TIn = TOut> {
return value; return value;
} }
public getValue() {
return this.transformSubmit(this.form.value);
}
public load(value: Partial<TIn> | undefined) { public load(value: Partial<TIn> | undefined) {
this.state.resetState(); this.state.resetState();

33
frontend/src/app/shared/services/rules.service.spec.ts

@ -326,6 +326,33 @@ describe('RulesService', () => {
])); ]));
})); }));
it('should make post request to get simulated rule events with action and trigger',
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => {
let rules: SimulatedRuleEventsDto;
rulesService.postSimulatedEvents('my-app', {}, {}).subscribe(result => {
rules = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/simulate');
expect(req.request.method).toEqual('POST');
req.flush({
total: 20,
items: [
simulatedRuleEventResponse(1),
simulatedRuleEventResponse(2),
],
});
expect(rules!).toEqual(
new SimulatedRuleEventsDto(20, [
createSimulatedRuleEvent(1),
createSimulatedRuleEvent(2),
]));
}));
it('should make put request to enqueue rule event', it('should make put request to enqueue rule event',
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => {
const resource: Resource = { const resource: Resource = {
@ -417,12 +444,12 @@ describe('RulesService', () => {
return { return {
id: `id${id}`, id: `id${id}`,
created: `${id % 1000 + 2000}-12-12T10:10:00Z`, created: `${id % 1000 + 2000}-12-12T10:10:00Z`,
description: `event-url${key}`,
eventName: `event-name${key}`, eventName: `event-name${key}`,
nextAttempt: `${id % 1000 + 2000}-11-11T10:10`,
jobResult: `Failed${key}`, jobResult: `Failed${key}`,
lastDump: `event-dump${key}`, lastDump: `event-dump${key}`,
nextAttempt: `${id % 1000 + 2000}-11-11T10:10`,
numCalls: id, numCalls: id,
description: `event-url${key}`,
result: `Failed${key}`, result: `Failed${key}`,
_links: { _links: {
update: { method: 'PUT', href: `/rules/events/${id}` }, update: { method: 'PUT', href: `/rules/events/${id}` },
@ -434,6 +461,7 @@ describe('RulesService', () => {
const key = `${id}${suffix}`; const key = `${id}${suffix}`;
return { return {
eventId: `id${key}`,
eventName: `name${key}`, eventName: `name${key}`,
event: { value: 'simple' }, event: { value: 'simple' },
enrichedEvent: { value: 'enriched' }, enrichedEvent: { value: 'enriched' },
@ -499,6 +527,7 @@ export function createSimulatedRuleEvent(id: number, suffix = '') {
const key = `${id}${suffix}`; const key = `${id}${suffix}`;
return new SimulatedRuleEventDto({}, return new SimulatedRuleEventDto({},
`id${key}`,
`name${key}`, `name${key}`,
{ value: 'simple' }, { value: 'simple' },
{ value: 'enriched' }, { value: 'enriched' },

12
frontend/src/app/shared/services/rules.service.ts

@ -198,6 +198,7 @@ export class SimulatedRuleEventDto {
public readonly _links: ResourceLinks; public readonly _links: ResourceLinks;
constructor(links: ResourceLinks, constructor(links: ResourceLinks,
public readonly eventId: string,
public readonly eventName: string, public readonly eventName: string,
public readonly event: any, public readonly event: any,
public readonly enrichedEvent: any | undefined, public readonly enrichedEvent: any | undefined,
@ -360,6 +361,16 @@ export class RulesService {
pretifyError('i18n:rules.ruleEvents.loadFailed')); pretifyError('i18n:rules.ruleEvents.loadFailed'));
} }
public postSimulatedEvents(appName: string, trigger: any, action: any): Observable<SimulatedRuleEventsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/simulate`);
return this.http.post<any>(url, { trigger, action }).pipe(
map(body => {
return parseSimulatedEvents(body);
}),
pretifyError('i18n:rules.ruleEvents.loadFailed'));
}
public enqueueEvent(appName: string, resource: Resource): Observable<any> { public enqueueEvent(appName: string, resource: Resource): Observable<any> {
const link = resource._links['update']; const link = resource._links['update'];
@ -471,6 +482,7 @@ function parseRuleEvent(response: any) {
function parseSimulatedRuleEvent(response: any) { function parseSimulatedRuleEvent(response: any) {
return new SimulatedRuleEventDto(response._links, return new SimulatedRuleEventDto(response._links,
response.eventId,
response.eventName, response.eventName,
response.event, response.event,
response.enrichedEvent, response.enrichedEvent,

4
frontend/src/app/shared/state/rule-events.state.ts

@ -67,7 +67,9 @@ export class RuleEventsState extends State<Snapshot> {
public load(isReload = false, update: Partial<Snapshot> = {}): Observable<any> { public load(isReload = false, update: Partial<Snapshot> = {}): Observable<any> {
if (!isReload) { if (!isReload) {
this.resetState({ ruleId: this.snapshot.ruleId, ...update }, 'Loading Initial'); const { ruleId } = this.snapshot;
this.resetState({ ruleId, ...update }, 'Loading Initial');
} }
return this.loadInternal(isReload); return this.loadInternal(isReload);

15
frontend/src/app/shared/state/rule-simulator.state.spec.ts

@ -52,6 +52,21 @@ describe('RuleSimulatorState', () => {
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never());
}); });
it('should load simulated rule events by action and trigger', () => {
rulesService.setup(x => x.postSimulatedEvents(app, It.isAny(), It.isAny()))
.returns(() => of(new SimulatedRuleEventsDto(200, oldSimulatedRuleEvents)));
ruleSimulatorState.setRule({}, {});
ruleSimulatorState.load().subscribe();
expect(ruleSimulatorState.snapshot.simulatedRuleEvents).toEqual(oldSimulatedRuleEvents);
expect(ruleSimulatorState.snapshot.isLoaded).toBeTruthy();
expect(ruleSimulatorState.snapshot.isLoading).toBeFalsy();
expect(ruleSimulatorState.snapshot.total).toEqual(200);
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never());
});
it('should reset loading state if loading failed', () => { it('should reset loading state if loading failed', () => {
rulesService.setup(x => x.getSimulatedEvents(app, '12')) rulesService.setup(x => x.getSimulatedEvents(app, '12'))
.returns(() => throwError(() => 'Service Error')); .returns(() => throwError(() => 'Service Error'));

25
frontend/src/app/shared/state/rule-simulator.state.ts

@ -18,6 +18,12 @@ interface Snapshot extends ListState {
// The current rule id. // The current rule id.
ruleId?: string; ruleId?: string;
// The rule trigger.
trigger?: any;
// The rule action.
action?: any;
} }
@Injectable() @Injectable()
@ -57,22 +63,29 @@ export class RuleSimulatorState extends State<Snapshot> {
public load(isReload = false): Observable<any> { public load(isReload = false): Observable<any> {
if (!isReload) { if (!isReload) {
this.resetState({ ruleId: this.snapshot.ruleId }, 'Loading Initial'); const { action, ruleId, trigger } = this.snapshot;
this.resetState({ action, ruleId, trigger }, 'Loading Initial');
} }
return this.loadInternal(isReload); return this.loadInternal(isReload);
} }
private loadInternal(isReload: boolean): Observable<any> { private loadInternal(isReload: boolean): Observable<any> {
if (!this.snapshot.ruleId) { const { action, ruleId, trigger } = this.snapshot;
if (!ruleId && !trigger && !action) {
return EMPTY; return EMPTY;
} }
this.next({ isLoading: true }, 'Loading Started'); this.next({ isLoading: true }, 'Loading Started');
const { ruleId } = this.snapshot; const request =
action && trigger ?
this.rulesService.postSimulatedEvents(this.appName, trigger, action) :
this.rulesService.getSimulatedEvents(this.appName, ruleId!);
return this.rulesService.getSimulatedEvents(this.appName, ruleId!).pipe( return request.pipe(
tap(({ total, items: simulatedRuleEvents }) => { tap(({ total, items: simulatedRuleEvents }) => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('i18n:rules.ruleEvents.reloaded'); this.dialogs.notifyInfo('i18n:rules.ruleEvents.reloaded');
@ -94,4 +107,8 @@ export class RuleSimulatorState extends State<Snapshot> {
public selectRule(ruleId?: string) { public selectRule(ruleId?: string) {
this.resetState({ ruleId }, 'Select Rule'); this.resetState({ ruleId }, 'Select Rule');
} }
public setRule(trigger: any, action: any) {
this.next({ trigger, action }, 'Set Rule');
}
} }

4
frontend/src/app/shared/state/rules.state.ts

@ -95,7 +95,9 @@ export class RulesState extends State<Snapshot> {
public load(isReload = false): Observable<any> { public load(isReload = false): Observable<any> {
if (!isReload) { if (!isReload) {
this.resetState({ selectedRule: this.snapshot.selectedRule }, 'Loading Initial'); const { selectedRule } = this.snapshot;
this.resetState({ selectedRule }, 'Loading Initial');
} }
return this.loadInternal(isReload); return this.loadInternal(isReload);

Loading…
Cancel
Save