Browse Source

Started with cancelling events.

pull/336/head
Sebastian Stehle 7 years ago
parent
commit
451a5f24f8
  1. 13
      src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs
  2. 4
      src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs
  3. 3
      src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs
  4. 30
      src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  5. 9
      src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs
  6. 15
      src/Squidex/Pipeline/Extensions.cs
  7. 2
      src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs
  8. 14
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.html
  9. 4
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts
  10. 2
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html
  11. 2
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html
  12. 4
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html
  13. 2
      src/Squidex/app/features/settings/pages/more/more-page.component.html
  14. 13
      src/Squidex/app/shared/services/rules.service.spec.ts
  15. 14
      src/Squidex/app/shared/services/rules.service.ts
  16. 11
      src/Squidex/app/shared/state/rule-events.state.spec.ts
  17. 17
      src/Squidex/app/shared/state/rule-events.state.ts

13
src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs

@ -66,11 +66,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
return ruleEvent; return ruleEvent;
} }
public Task RemoveAsync(Guid appId)
{
return Collection.DeleteManyAsync(x => x.AppId == appId);
}
public async Task<int> CountByAppAsync(Guid appId) public async Task<int> CountByAppAsync(Guid appId)
{ {
return (int)await Collection.CountDocumentsAsync(x => x.AppId == appId); return (int)await Collection.CountDocumentsAsync(x => x.AppId == appId);
@ -88,6 +83,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
return Collection.InsertOneIfNotExistsAsync(entity); return Collection.InsertOneIfNotExistsAsync(entity);
} }
public Task CancelAsync(Guid id)
{
return Collection.UpdateOneAsync(x => x.Id == id,
Update
.Set(x => x.NextAttempt, null)
.Set(x => x.JobResult, RuleJobResult.Cancelled));
}
public Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextAttempt) public Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextAttempt)
{ {
return Collection.UpdateOneAsync(x => x.Id == jobId, return Collection.UpdateOneAsync(x => x.Id == jobId,

4
src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs

@ -21,12 +21,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Repositories
Task EnqueueAsync(Guid id, Instant nextAttempt); Task EnqueueAsync(Guid id, Instant nextAttempt);
Task CancelAsync(Guid id);
Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextCall); Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextCall);
Task QueryPendingAsync(Instant now, Func<IRuleEventEntity, Task> callback, CancellationToken ct = default(CancellationToken)); Task QueryPendingAsync(Instant now, Func<IRuleEventEntity, Task> callback, CancellationToken ct = default(CancellationToken));
Task RemoveAsync(Guid appId);
Task<int> CountByAppAsync(Guid appId); Task<int> CountByAppAsync(Guid appId);
Task<IReadOnlyList<IRuleEventEntity>> QueryByAppAsync(Guid appId, int skip = 0, int take = 20); Task<IReadOnlyList<IRuleEventEntity>> QueryByAppAsync(Guid appId, int skip = 0, int take = 20);

3
src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs

@ -12,6 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
Pending, Pending,
Success, Success,
Retry, Retry,
Failed Failed,
Cancelled
} }
} }

30
src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -250,7 +250,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
} }
/// <summary> /// <summary>
/// Enqueue the event to be send. /// Enqueue the event to retry it immediate
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The event to enqueue.</param> /// <param name="id">The event to enqueue.</param>
@ -274,5 +274,31 @@ namespace Squidex.Areas.Api.Controllers.Rules
return NoContent(); return NoContent();
} }
/// <summary>
/// Cancels the event to not retry it again.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The event to enqueue.</param>
/// <returns>
/// 200 => Rule deqeued.
/// 404 => App or rule event not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/rules/events/{id}/")]
[ApiCosts(0)]
public async Task<IActionResult> DeleteEvent(string app, Guid id)
{
var entity = await ruleEventsRepository.FindAsync(id);
if (entity == null)
{
return NotFound();
}
await ruleEventsRepository.CancelAsync(id);
return NoContent();
}
} }
} }

9
src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs

@ -16,6 +16,7 @@ using Squidex.Config;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Pipeline;
namespace Squidex.Areas.IdentityServer.Config namespace Squidex.Areas.IdentityServer.Config
{ {
@ -43,16 +44,16 @@ namespace Squidex.Areas.IdentityServer.Config
return client; return client;
} }
var token = clientId.Split(':', '~'); var (appName, appClientId) = clientId.GetClientParts();
if (token.Length != 2) if (appName == null)
{ {
return null; return null;
} }
var app = await appProvider.GetAppAsync(token[0]); var app = await appProvider.GetAppAsync(appName);
var appClient = app?.Clients.GetOrDefault(token[1]); var appClient = app?.Clients.GetOrDefault(appClientId);
if (appClient == null) if (appClient == null)
{ {

15
src/Squidex/Pipeline/Extensions.cs

@ -22,16 +22,19 @@ namespace Squidex.Pipeline
{ {
var clientId = principal.FindFirst(OpenIdClaims.ClientId)?.Value; var clientId = principal.FindFirst(OpenIdClaims.ClientId)?.Value;
var clientIdParts = clientId?.Split(':'); return clientId?.GetClientParts().ClientId;
}
public static (string App, string ClientId) GetClientParts(this string clientId)
{
var parts = clientId.Split(':', '~');
if (clientIdParts?.Length != 2) if (parts.Length != 2)
{ {
return null; return (null, null);
} }
clientId = clientIdParts[1]; return (parts[0], parts[1]);
return clientId;
} }
} }
} }

2
src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Pipeline.Robots namespace Squidex.Pipeline.Robots
{ {
public class RobotsTxtMiddleware : IMiddleware public sealed class RobotsTxtMiddleware : IMiddleware
{ {
private readonly RobotsTxtOptions robotsTxtOptions; private readonly RobotsTxtOptions robotsTxtOptions;

14
src/Squidex/app/features/rules/pages/events/rule-events-page.component.html

@ -60,23 +60,27 @@
</div> </div>
<div class="row event-stats"> <div class="row event-stats">
<div class="col-3"> <div class="col col-4">
<span class="badge badge-pill badge-{{event.result | sqxRuleEventBadgeClass}}">{{event.result}}</span> <span class="badge badge-pill badge-{{event.result | sqxRuleEventBadgeClass}}">{{event.result}}</span>
</div> </div>
<div class="col-3"> <div class="col col-4">
Attempts: {{event.numCalls}} Attempts: {{event.numCalls}}
</div> </div>
<div class="col-3"> <div class="col col-4">
Next: <ng-container *ngIf="event.nextAttempt">{{event.nextAttempt | sqxFromNow}}</ng-container> Next: <ng-container *ngIf="event.nextAttempt">{{event.nextAttempt | sqxFromNow}}</ng-container>
</div> </div>
<div class="col-3 text-right"> <div class="col col-auto text-right">
<button class="btn btn-success btn-sm" (click)="enqueue(event)">
Enqueue
</button>
<button class="btn btn-success btn-sm" (click)="enqueue(event)"> <button class="btn btn-success btn-sm" (click)="enqueue(event)">
Enqueue Enqueue
</button> </button>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col col-12">
<textarea class="event-dump form-control" readonly>{{event.lastDump}}</textarea> <textarea class="event-dump form-control" readonly>{{event.lastDump}}</textarea>
</div> </div>
</div> </div>

4
src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts

@ -48,6 +48,10 @@ export class RuleEventsPageComponent implements OnInit {
this.ruleEventsState.enqueue(event).pipe(onErrorResumeNext()).subscribe(); this.ruleEventsState.enqueue(event).pipe(onErrorResumeNext()).subscribe();
} }
public cancel(event: RuleEventDto) {
this.ruleEventsState.cancel(event).pipe(onErrorResumeNext()).subscribe();
}
public selectEvent(id: string) { public selectEvent(id: string) {
this.selectedEventId = this.selectedEventId !== id ? id : null; this.selectedEventId = this.selectedEventId !== id ? id : null;
} }

2
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html

@ -22,7 +22,7 @@
<ng-container content> <ng-container content>
<div class="row no-gutters" *ngIf="step === 1"> <div class="row no-gutters" *ngIf="step === 1">
<div *ngFor="let triggerType of ruleTriggers | sqxKeys" class="col-12 col-md-6"> <div *ngFor="let triggerType of ruleTriggers | sqxKeys" class="col col-12 col-md-6">
<div class="rule-element" (click)="selectTriggerType(triggerType)"> <div class="rule-element" (click)="selectTriggerType(triggerType)">
<sqx-rule-element [type]="triggerType" [isSmall]="false" [element]="ruleTriggers[triggerType]"></sqx-rule-element> <sqx-rule-element [type]="triggerType" [isSmall]="false" [element]="ruleTriggers[triggerType]"></sqx-rule-element>
</div> </div>

2
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html

@ -16,7 +16,7 @@
<div class="form-group"> <div class="form-group">
<div class="row"> <div class="row">
<div class="col-4 type" *ngFor="let fieldType of fieldTypes"> <div class="col col-4 type" *ngFor="let fieldType of fieldTypes">
<label> <label>
<input type="radio" class="radio-input" formControlName="type" value="{{fieldType.type}}" /> <input type="radio" class="radio-input" formControlName="type" value="{{fieldType.type}}" />

4
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html

@ -29,7 +29,7 @@
<div class="form-group"> <div class="form-group">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-6 type"> <div class="col col-6 type">
<label> <label>
<input type="radio" class="radio-input" formControlName="singleton" [value]="false" /> <input type="radio" class="radio-input" formControlName="singleton" [value]="false" />
@ -47,7 +47,7 @@
</label> </label>
</div> </div>
<div class="col-6 type"> <div class="col col-6 type">
<label> <label>
<input type="radio" class="radio-input" formControlName="singleton" [value]="true" /> <input type="radio" class="radio-input" formControlName="singleton" [value]="true" />

2
src/Squidex/app/features/settings/pages/more/more-page.component.html

@ -16,7 +16,7 @@
<div>Once you archive an app, there is no going back. Please be certain.</div> <div>Once you archive an app, there is no going back. Please be certain.</div>
</div> </div>
<div class="col-auto"> <div class="col col-auto">
<button class="btn btn-danger" <button class="btn btn-danger"
(sqxConfirmClick)="archiveApp()" (sqxConfirmClick)="archiveApp()"
confirmTitle="Archive App" confirmTitle="Archive App"

13
src/Squidex/app/shared/services/rules.service.spec.ts

@ -346,4 +346,17 @@ describe('RulesService', () => {
req.flush({}); req.flush({});
})); }));
it('should make delete request to cancel rule event',
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => {
rulesService.cancelEvent('my-app', '123').subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/events/123');
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
}); });

14
src/Squidex/app/shared/services/rules.service.ts

@ -77,6 +77,10 @@ export class RuleEventDto extends Model {
) { ) {
super(); super();
} }
public with(value: Partial<RuleEventDto>): RuleEventDto {
return this.clone(value);
}
} }
export class CreateRuleDto { export class CreateRuleDto {
@ -269,4 +273,14 @@ export class RulesService {
}), }),
pretifyError('Failed to enqueue rule event. Please reload.')); pretifyError('Failed to enqueue rule event. Please reload.'));
} }
public cancelEvent(appName: string, id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events/${id}`);
return HTTP.deleteVersioned(this.http, url).pipe(
tap(() => {
this.analytics.trackEvent('Rule', 'EventDequeued', appName);
}),
pretifyError('Failed to cancel rule event. Please reload.'));
}
} }

11
src/Squidex/app/shared/state/rule-events.state.spec.ts

@ -91,4 +91,15 @@ describe('RuleEventsState', () => {
rulesService.verify(x => x.enqueueEvent(app, oldRuleEvents[0].id), Times.once()); rulesService.verify(x => x.enqueueEvent(app, oldRuleEvents[0].id), Times.once());
}); });
it('should call service when cancelling event', () => {
rulesService.setup(x => x.cancelEvent(app, oldRuleEvents[0].id))
.returns(() => of({}));
ruleEventsState.cancel(oldRuleEvents[0]).subscribe();
expect().nothing();
rulesService.verify(x => x.cancelEvent(app, oldRuleEvents[0].id), Times.once());
});
}); });

17
src/Squidex/app/shared/state/rule-events.state.ts

@ -85,6 +85,18 @@ export class RuleEventsState extends State<Snapshot> {
notify(this.dialogs)); notify(this.dialogs));
} }
public cancel(event: RuleEventDto): Observable<any> {
return this.rulesService.cancelEvent(this.appsState.appName, event.id).pipe(
tap(() => {
return this.next(s => {
const ruleEvents = s.ruleEvents.replaceBy('id', setCancelled(event));
return { ...s, ruleEvents, isLoaded: true };
});
}),
notify(this.dialogs));
}
public goNext(): Observable<any> { public goNext(): Observable<any> {
this.next(s => ({ ...s, ruleEventsPager: s.ruleEventsPager.goNext() })); this.next(s => ({ ...s, ruleEventsPager: s.ruleEventsPager.goNext() }));
@ -100,4 +112,7 @@ export class RuleEventsState extends State<Snapshot> {
private get appName() { private get appName() {
return this.appsState.appName; return this.appsState.appName;
} }
} }
const setCancelled = (event: RuleEventDto) =>
event.with({ nextAttempt: null, jobResult: 'Cancelled' });
Loading…
Cancel
Save