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 c06e31561..6e9135e8b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs @@ -8,6 +8,7 @@ using NodaTime; using Orleans; using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; @@ -34,16 +35,22 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner this.ruleService = ruleService; } - public async Task> SimulateAsync(IRuleEntity rule, + public Task> SimulateAsync(IRuleEntity rule, + CancellationToken ct = default) + { + return SimulateAsync(rule.AppId, rule.Id, rule.RuleDef, ct); + } + + public async Task> SimulateAsync(NamedId appId, DomainId ruleId, Rule rule, CancellationToken ct = default) { Guard.NotNull(rule); var context = new RuleContext { - AppId = rule.AppId, - Rule = rule.RuleDef, - RuleId = rule.Id, + AppId = appId, + Rule = rule, + RuleId = ruleId, IncludeSkipped = true, IncludeStale = true }; @@ -52,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner 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); @@ -75,6 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner EnrichedEvent = result.EnrichedEvent, Error = result.EnrichmentError?.Message, Event = @event.Payload, + EventId = @event.Headers.EventId(), EventName = eventName, SkipReason = result.SkipReason }); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs index d741f8cde..742f31304 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs @@ -5,12 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Rules.Runner { public interface IRuleRunnerService { + Task> SimulateAsync(NamedId appId, DomainId ruleId, Rule rule, + CancellationToken ct = default); + Task> SimulateAsync(IRuleEntity rule, CancellationToken ct = default); 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 8722d8ad1..023c2f7ca 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs @@ -6,11 +6,14 @@ // ========================================================================== using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Rules.Runner { public sealed record SimulatedRuleEvent { + public Guid EventId { get; init; } + public string EventName { get; init; } public object Event { get; init; } diff --git a/backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs b/backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs index dbe7ee45a..cedbb08b0 100644 --- a/backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs +++ b/backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs @@ -7,9 +7,9 @@ using System.Reflection; using System.Runtime.Serialization; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NJsonSchema.Converters; -using Squidex.Infrastructure; #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) { - 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) @@ -89,4 +99,4 @@ namespace Squidex.Web.Json return mapping.FirstOrDefault(x => x.Value == type).Key ?? type.Name; } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs index 7a0ad9354..0f72527a4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs @@ -7,6 +7,7 @@ using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; namespace Squidex.Areas.Api.Controllers.Apps.Models @@ -26,7 +27,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models public UpdateRole ToCommand(string name) { - return new UpdateRole { Name = name, Permissions = Permissions, Properties = Properties }; + return SimpleMapper.Map(this, new UpdateRole { Name = name }); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs index 92f1a4fda..369bb34b8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs @@ -27,16 +27,14 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models [JsonConverter(typeof(RuleActionConverter))] public RuleAction Action { get; set; } - public CreateRule ToCommand() + public Rule ToRule() { - var command = new CreateRule { Action = Action }; - - if (Trigger != null) - { - command.Trigger = Trigger.ToTrigger(); - } + return new Rule(Trigger.ToTrigger(), Action); + } - return command; + public CreateRule ToCommand() + { + return new CreateRule { Action = Action, Trigger = Trigger?.ToTrigger() }; } } } 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 f3fe4b1d5..853d60750 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs +++ b/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 { + /// + /// The unique event id. + /// + [Required] + public Guid EventId { get; init; } + /// /// The name of the event. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs index f203d1025..87379b1d1 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs +++ b/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 }); - if (Trigger != null) - { - command.Trigger = Trigger.ToTrigger(); - } + command.Trigger = Trigger?.ToTrigger(); return command; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index b7b3c5140..11792225c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/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.Scripting; using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.Repositories; @@ -282,6 +283,31 @@ namespace Squidex.Areas.Api.Controllers.Rules return NoContent(); } + /// + /// Simulate a rule. + /// + /// The name of the app. + /// The rule to simulate. + /// + /// 200 => Rule simulated. + /// 404 => Rule or app not found. + /// + [HttpPost] + [Route("apps/{app}/rules/simulate/")] + [ProducesResponseType(typeof(SimulatedRuleEventsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(Permissions.AppRulesEvents)] + [ApiCosts(5)] + public async Task 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); + } + /// /// Simulate a rule. /// diff --git a/frontend/src/app/features/administration/state/users.state.ts b/frontend/src/app/features/administration/state/users.state.ts index b326e8043..17e181e52 100644 --- a/frontend/src/app/features/administration/state/users.state.ts +++ b/frontend/src/app/features/administration/state/users.state.ts @@ -88,7 +88,9 @@ export class UsersState extends State { public load(isReload = false, update: Partial = {}): Observable { 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); diff --git a/frontend/src/app/features/rules/pages/messages.ts b/frontend/src/app/features/rules/pages/messages.ts new file mode 100644 index 000000000..5d8381891 --- /dev/null +++ b/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, + ) { + } +} diff --git a/frontend/src/app/features/rules/pages/rule/rule-page.component.ts b/frontend/src/app/features/rules/pages/rule/rule-page.component.ts index 5c955f068..4917f9f45 100644 --- a/frontend/src/app/features/rules/pages/rule/rule-page.component.ts +++ b/frontend/src/app/features/rules/pages/rule/rule-page.component.ts @@ -6,8 +6,11 @@ */ import { Component, OnInit } from '@angular/core'; +import { AbstractControl } from '@angular/forms'; 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 = { type: string; values: any; form: T }; @@ -17,13 +20,16 @@ type ComponentState = { type: string; values: any; form: T }; templateUrl: './rule-page.component.html', }) export class RulePageComponent extends ResourceOwner implements OnInit { - public supportedActions: { [name: string]: RuleElementDto } = {}; + private currentTriggerSubscription?: Subscription; + private currentActionSubscription?: Subscription; + public supportedTriggers = ALL_TRIGGERS; + public supportedActions: { [name: string]: RuleElementDto } = {}; public rule?: RuleDto | null; - public currentAction?: ComponentState; public currentTrigger?: ComponentState; + public currentAction?: ComponentState; public isEnabled = false; public isEditable = false; @@ -44,6 +50,7 @@ export class RulePageComponent extends ResourceOwner implements OnInit { public readonly rulesState: RulesState, public readonly rulesService: RulesService, public readonly schemasState: SchemasState, + private readonly messageBus: MessageBus, private readonly route: ActivatedRoute, private readonly router: Router, ) { @@ -91,6 +98,8 @@ export class RulePageComponent extends ResourceOwner implements OnInit { this.currentAction = { form, type, values }; this.currentAction.form.setEnabled(this.isEditable); + this.currentActionSubscription?.unsubscribe(); + this.currentActionSubscription = this.subscribe(form.form); } this.currentAction!.form.load(values); @@ -102,11 +111,17 @@ export class RulePageComponent extends ResourceOwner implements OnInit { this.currentTrigger = { form, type, values }; this.currentTrigger.form.setEnabled(this.isEditable); + this.currentTriggerSubscription?.unsubscribe(); + this.currentTriggerSubscription = this.subscribe(form.form); } this.currentTrigger.form.load(values); } + private subscribe(form: AbstractControl) { + return value$(form).pipe(debounceTime(100)).subscribe(() => this.publishState()); + } + public resetAction() { 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() { this.currentAction?.form.submitCompleted({ noReset: true }); this.currentTrigger?.form.submitCompleted({ noReset: true }); diff --git a/frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.html b/frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.html index 727e12734..a57edefb3 100644 --- a/frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.html +++ b/frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.html @@ -25,9 +25,9 @@ - diff --git a/frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.ts b/frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.ts index 633a76299..dce788290 100644 --- a/frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.ts +++ b/frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.ts @@ -7,7 +7,8 @@ import { Component, OnInit } from '@angular/core'; 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({ selector: 'sqx-simulator-events-page', @@ -15,16 +16,23 @@ import { ResourceOwner, RuleSimulatorState, SimulatedRuleEventDto } from '@app/s templateUrl: './rule-simulator-page.component.html', }) export class RuleSimulatorPageComponent extends ResourceOwner implements OnInit { - public selectedRuleEvent?: SimulatedRuleEventDto | null; + public selectedRuleEvent?: string | null; constructor( - private route: ActivatedRoute, public readonly ruleSimulatorState: RuleSimulatorState, + private readonly route: ActivatedRoute, + private readonly messageBus: MessageBus, ) { super(); } public ngOnInit() { + this.own( + this.messageBus.of(RuleConfigured) + .subscribe(message => { + this.ruleSimulatorState.setRule(message.trigger, message.action); + })); + this.own( this.route.queryParams .subscribe(query => { @@ -33,14 +41,18 @@ export class RuleSimulatorPageComponent extends ResourceOwner implements OnInit } public simulate() { - this.ruleSimulatorState.load(); + this.ruleSimulatorState.load(true); } public selectEvent(event: SimulatedRuleEventDto) { - if (this.selectedRuleEvent === event) { + if (this.selectedRuleEvent === event.eventId) { this.selectedRuleEvent = null; } else { - this.selectedRuleEvent = event; + this.selectedRuleEvent = event.eventId; } } + + public trackByEvent(_index: number, event: SimulatedRuleEventDto) { + return event.eventId; + } } diff --git a/frontend/src/app/framework/angular/forms/model.ts b/frontend/src/app/framework/angular/forms/model.ts index b10a93ca2..12d6bac1c 100644 --- a/frontend/src/app/framework/angular/forms/model.ts +++ b/frontend/src/app/framework/angular/forms/model.ts @@ -80,6 +80,10 @@ export class Form { return value; } + public getValue() { + return this.transformSubmit(this.form.value); + } + public load(value: Partial | undefined) { this.state.resetState(); diff --git a/frontend/src/app/shared/services/rules.service.spec.ts b/frontend/src/app/shared/services/rules.service.spec.ts index da0ffe492..eaa81261f 100644 --- a/frontend/src/app/shared/services/rules.service.spec.ts +++ b/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', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { const resource: Resource = { @@ -417,12 +444,12 @@ describe('RulesService', () => { return { id: `id${id}`, created: `${id % 1000 + 2000}-12-12T10:10:00Z`, + description: `event-url${key}`, eventName: `event-name${key}`, - nextAttempt: `${id % 1000 + 2000}-11-11T10:10`, jobResult: `Failed${key}`, lastDump: `event-dump${key}`, + nextAttempt: `${id % 1000 + 2000}-11-11T10:10`, numCalls: id, - description: `event-url${key}`, result: `Failed${key}`, _links: { update: { method: 'PUT', href: `/rules/events/${id}` }, @@ -434,6 +461,7 @@ describe('RulesService', () => { const key = `${id}${suffix}`; return { + eventId: `id${key}`, eventName: `name${key}`, event: { value: 'simple' }, enrichedEvent: { value: 'enriched' }, @@ -499,6 +527,7 @@ export function createSimulatedRuleEvent(id: number, suffix = '') { const key = `${id}${suffix}`; return new SimulatedRuleEventDto({}, + `id${key}`, `name${key}`, { value: 'simple' }, { value: 'enriched' }, diff --git a/frontend/src/app/shared/services/rules.service.ts b/frontend/src/app/shared/services/rules.service.ts index 70c064fad..7ebde192a 100644 --- a/frontend/src/app/shared/services/rules.service.ts +++ b/frontend/src/app/shared/services/rules.service.ts @@ -198,6 +198,7 @@ export class SimulatedRuleEventDto { public readonly _links: ResourceLinks; constructor(links: ResourceLinks, + public readonly eventId: string, public readonly eventName: string, public readonly event: any, public readonly enrichedEvent: any | undefined, @@ -360,6 +361,16 @@ export class RulesService { pretifyError('i18n:rules.ruleEvents.loadFailed')); } + public postSimulatedEvents(appName: string, trigger: any, action: any): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/simulate`); + + return this.http.post(url, { trigger, action }).pipe( + map(body => { + return parseSimulatedEvents(body); + }), + pretifyError('i18n:rules.ruleEvents.loadFailed')); + } + public enqueueEvent(appName: string, resource: Resource): Observable { const link = resource._links['update']; @@ -471,6 +482,7 @@ function parseRuleEvent(response: any) { function parseSimulatedRuleEvent(response: any) { return new SimulatedRuleEventDto(response._links, + response.eventId, response.eventName, response.event, response.enrichedEvent, diff --git a/frontend/src/app/shared/state/rule-events.state.ts b/frontend/src/app/shared/state/rule-events.state.ts index 28a5db6bb..4922f2c16 100644 --- a/frontend/src/app/shared/state/rule-events.state.ts +++ b/frontend/src/app/shared/state/rule-events.state.ts @@ -67,7 +67,9 @@ export class RuleEventsState extends State { public load(isReload = false, update: Partial = {}): Observable { 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); diff --git a/frontend/src/app/shared/state/rule-simulator.state.spec.ts b/frontend/src/app/shared/state/rule-simulator.state.spec.ts index ad1457d4f..9cdcdc36a 100644 --- a/frontend/src/app/shared/state/rule-simulator.state.spec.ts +++ b/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()); }); + 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', () => { rulesService.setup(x => x.getSimulatedEvents(app, '12')) .returns(() => throwError(() => 'Service Error')); diff --git a/frontend/src/app/shared/state/rule-simulator.state.ts b/frontend/src/app/shared/state/rule-simulator.state.ts index 7f517c861..c4ca96e22 100644 --- a/frontend/src/app/shared/state/rule-simulator.state.ts +++ b/frontend/src/app/shared/state/rule-simulator.state.ts @@ -18,6 +18,12 @@ interface Snapshot extends ListState { // The current rule id. ruleId?: string; + + // The rule trigger. + trigger?: any; + + // The rule action. + action?: any; } @Injectable() @@ -57,22 +63,29 @@ export class RuleSimulatorState extends State { public load(isReload = false): Observable { 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); } private loadInternal(isReload: boolean): Observable { - if (!this.snapshot.ruleId) { + const { action, ruleId, trigger } = this.snapshot; + + if (!ruleId && !trigger && !action) { return EMPTY; } 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 }) => { if (isReload) { this.dialogs.notifyInfo('i18n:rules.ruleEvents.reloaded'); @@ -94,4 +107,8 @@ export class RuleSimulatorState extends State { public selectRule(ruleId?: string) { this.resetState({ ruleId }, 'Select Rule'); } + + public setRule(trigger: any, action: any) { + this.next({ trigger, action }, 'Set Rule'); + } } diff --git a/frontend/src/app/shared/state/rules.state.ts b/frontend/src/app/shared/state/rules.state.ts index 0c1b4938a..1c9cdc77c 100644 --- a/frontend/src/app/shared/state/rules.state.ts +++ b/frontend/src/app/shared/state/rules.state.ts @@ -95,7 +95,9 @@ export class RulesState extends State { public load(isReload = false): Observable { if (!isReload) { - this.resetState({ selectedRule: this.snapshot.selectedRule }, 'Loading Initial'); + const { selectedRule } = this.snapshot; + + this.resetState({ selectedRule }, 'Loading Initial'); } return this.loadInternal(isReload);