mirror of https://github.com/Squidex/squidex.git
54 changed files with 1457 additions and 1084 deletions
@ -0,0 +1,13 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
export * from './pages/rules/actions/webhook-action.component'; |
|||
export * from './pages/rules/triggers/content-changed-trigger.component'; |
|||
export * from './pages/rules/rule-wizard.component'; |
|||
export * from './pages/rules/rules-page.component'; |
|||
|
|||
export * from './pages/events/rule-events-page.component'; |
|||
@ -0,0 +1,29 @@ |
|||
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()"> |
|||
<div class="form-group row"> |
|||
<label class="col col-2 col-form-label" for="url">Url</label> |
|||
|
|||
<div class="col col-10"> |
|||
<sqx-control-errors for="url" [submitted]="actionFormSubmitted"></sqx-control-errors> |
|||
|
|||
<input type="url" class="form-control" id="url" formControlName="url" /> |
|||
|
|||
<small class="form-text text-muted"> |
|||
The url where the events will be sent to. |
|||
</small> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col col-2 col-form-label" for="sharedSecret">Secret</label> |
|||
|
|||
<div class="col col-10"> |
|||
<sqx-control-errors for="sharedSecret" [submitted]="actionFormSubmitted"></sqx-control-errors> |
|||
|
|||
<input type="text" class="form-control" id="sharedSecret" formControlName="sharedSecret" /> |
|||
|
|||
<small class="form-text text-muted"> |
|||
The shared secret will be used to add a header X-Signature=Sha256(RequestBody + Secret) |
|||
</small> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,2 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
@ -0,0 +1,55 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; |
|||
import { FormBuilder, Validators } from '@angular/forms'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-webhook-action', |
|||
styleUrls: ['./webhook-action.component.scss'], |
|||
templateUrl: './webhook-action.component.html' |
|||
}) |
|||
export class WebhookActionComponent implements OnInit { |
|||
@Input() |
|||
public action: any; |
|||
|
|||
@Output() |
|||
public actionChanged = new EventEmitter<object>(); |
|||
|
|||
public actionFormSubmitted = false; |
|||
public actionForm = |
|||
this.formBuilder.group({ |
|||
url: ['', |
|||
[ |
|||
Validators.required |
|||
]], |
|||
sharedSecret: [''] |
|||
}); |
|||
|
|||
constructor( |
|||
private readonly formBuilder: FormBuilder |
|||
) { |
|||
} |
|||
|
|||
public ngOnInit() { |
|||
this.action = Object.assign({}, { url: '', sharedSecret: '' }, this.action || {}); |
|||
|
|||
this.actionFormSubmitted = false; |
|||
this.actionForm.reset(); |
|||
this.actionForm.setValue(this.action); |
|||
} |
|||
|
|||
public save() { |
|||
this.actionFormSubmitted = true; |
|||
|
|||
if (this.actionForm.valid) { |
|||
const action = this.actionForm.value; |
|||
|
|||
this.actionChanged.emit(action); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
<div class="modal-dialog modal-lg"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h4 class="modal-title" *ngIf="step === 1"> |
|||
Step 1 of 4: Select Trigger |
|||
</h4> |
|||
<h4 class="modal-title" *ngIf="step === 2"> |
|||
Step 2 of 4: Configure Trigger |
|||
</h4> |
|||
<h4 class="modal-title" *ngIf="step === 3"> |
|||
Step 3 of 4: Select Action |
|||
</h4> |
|||
<h4 class="modal-title" *ngIf="step === 4"> |
|||
Step 4 of 4: Configure Action |
|||
</h4> |
|||
|
|||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="cancel()"> |
|||
<span aria-hidden="true">×</span> |
|||
</button> |
|||
</div> |
|||
|
|||
<div class="modal-body"> |
|||
<div *ngIf="step === 1"> |
|||
<span *ngFor="let trigger of ruleTriggers | sqxKeys" class="rule-element rule-element-{{trigger}}" (click)="selectTriggerType(trigger)"> |
|||
<span class="rule-element-icon"> |
|||
<i class="icon-trigger-{{trigger}}"></i> |
|||
</span> |
|||
<span class="rule-element-text"> |
|||
{{ruleTriggers[trigger]}} |
|||
</span> |
|||
</span> |
|||
</div> |
|||
|
|||
<div *ngIf="step === 2 && schemas" class="modal-form"> |
|||
<div [ngSwitch]="triggerType"> |
|||
<div *ngSwitchCase="'ContentChanged'"> |
|||
<sqx-content-changed-trigger #triggerControl |
|||
[schemas]="schemas" |
|||
[trigger]="trigger" |
|||
(triggerChanged)="selectTrigger($event)"> |
|||
</sqx-content-changed-trigger> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div *ngIf="step === 3"> |
|||
<span *ngFor="let action of ruleActions | sqxKeys" class="rule-element rule-element-{{action}}" (click)="selectActionType(action)"> |
|||
<span class="rule-element-icon"> |
|||
<i class="icon-action-{{action}}"></i> |
|||
</span> |
|||
<span class="rule-element-text"> |
|||
{{ruleActions[action]}} |
|||
</span> |
|||
</span> |
|||
</div> |
|||
|
|||
<div *ngIf="step === 4" class="modal-form"> |
|||
<div [ngSwitch]="actionType"> |
|||
<div *ngSwitchCase="'Webhook'"> |
|||
<sqx-webhook-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-webhook-action> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="modal-footer" *ngIf="step === 2 || step === 4"> |
|||
<div class="clearfix" *ngIf="step === 2"> |
|||
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button> |
|||
<button type="submit" class="float-right btn btn-primary" (click)="triggerControl.save()">Next</button> |
|||
</div> |
|||
<div class="clearfix" *ngIf="step === 4"> |
|||
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button> |
|||
<button type="submit" class="float-right btn btn-primary" (click)="actionControl.save()">Save</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,38 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
.modal { |
|||
&-dialog { |
|||
height: 70%; |
|||
} |
|||
|
|||
&-content { |
|||
min-height: 100%; |
|||
max-height: 100%; |
|||
} |
|||
|
|||
&-body { |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
&-header { |
|||
@include flex-shrink(0); |
|||
} |
|||
|
|||
&-footer { |
|||
@include flex-shrink(0); |
|||
} |
|||
|
|||
&-form { |
|||
padding-top: 2em; |
|||
padding-bottom: 0; |
|||
} |
|||
} |
|||
|
|||
.clearfix { |
|||
width: 100%; |
|||
} |
|||
|
|||
.rule-element { |
|||
margin-right: .5rem; |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; |
|||
|
|||
import { |
|||
AppComponentBase, |
|||
AppsStoreService, |
|||
AuthService, |
|||
CreateRuleDto, |
|||
DateTime, |
|||
DialogService, |
|||
fadeAnimation, |
|||
ruleActions, |
|||
ruleTriggers, |
|||
RuleDto, |
|||
RulesService, |
|||
SchemaDto |
|||
} from 'shared'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-rule-wizard', |
|||
styleUrls: ['./rule-wizard.component.scss'], |
|||
templateUrl: './rule-wizard.component.html', |
|||
animations: [ |
|||
fadeAnimation |
|||
] |
|||
}) |
|||
export class RuleWizardComponent extends AppComponentBase { |
|||
public ruleActions = ruleActions; |
|||
public ruleTriggers = ruleTriggers; |
|||
|
|||
public triggerType: string; |
|||
public trigger: any = {}; |
|||
public actionType: string; |
|||
public action: any = {}; |
|||
public step = 1; |
|||
|
|||
@ViewChild('triggerControl') |
|||
public triggerControl: any; |
|||
|
|||
@ViewChild('actionControl') |
|||
public actionControl: any; |
|||
|
|||
@Output() |
|||
public cancelled = new EventEmitter(); |
|||
|
|||
@Output() |
|||
public created = new EventEmitter<RuleDto>(); |
|||
|
|||
@Input() |
|||
public schemas: SchemaDto[]; |
|||
|
|||
constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, |
|||
private readonly rulesService: RulesService |
|||
) { |
|||
super(dialogs, apps, authService); |
|||
} |
|||
|
|||
public selectTriggerType(type: string) { |
|||
this.triggerType = type; |
|||
this.step++; |
|||
} |
|||
|
|||
public selectTrigger(value: any) { |
|||
this.trigger = Object.assign({}, value, { triggerType: this.triggerType }); |
|||
this.step++; |
|||
} |
|||
|
|||
public selectActionType(type: string) { |
|||
this.actionType = type; |
|||
this.step++; |
|||
} |
|||
|
|||
public selectAction(value: any) { |
|||
this.action = Object.assign({}, value, { actionType: this.actionType }); |
|||
|
|||
const requestDto = new CreateRuleDto(this.trigger, this.action); |
|||
|
|||
this.appNameOnce() |
|||
.switchMap(app => this.rulesService.postRule(app, requestDto, this.authService.user!.id, DateTime.now())) |
|||
.subscribe(dto => { |
|||
this.created.emit(dto); |
|||
}, error => { |
|||
this.notifyError(error); |
|||
}); |
|||
} |
|||
|
|||
public cancel() { |
|||
this.cancelled.emit(); |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
<sqx-title message="{app} | Rules" parameter1="app" value1="{{appName() | async}}"></sqx-title> |
|||
|
|||
<sqx-panel panelWidth="50rem"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<div class="float-right"> |
|||
<button class="btn btn-link btn-decent" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
|
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut> |
|||
|
|||
<button class="btn btn-success" #buttonNew (click)="addRuleDialog.show()" title="New Rule (CTRL + M)"> |
|||
<i class="icon-plus"></i> New |
|||
</button> |
|||
</div> |
|||
|
|||
<h3 class="panel-title">Rules</h3> |
|||
</div> |
|||
|
|||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content panel-content-scroll"> |
|||
<div class="table-items-row table-items-row-empty" *ngIf="rules && rules.length === 0"> |
|||
No Rule created yet. |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
<div class="panel-sidebar"> |
|||
<a class="panel-link" routerLink="events" routerLinkActive="active" #linkHistory> |
|||
<i class="icon-time"></i> |
|||
</a> |
|||
</div> |
|||
</sqx-panel> |
|||
|
|||
<div class="modal" *sqxModalView="addRuleDialog;onRoot:true;closeAuto:false" @fade> |
|||
<div class="modal-backdrop"></div> |
|||
|
|||
<sqx-rule-wizard [schemas]="schemas" |
|||
(cancelled)="addRuleDialog.hide()"> |
|||
</sqx-rule-wizard> |
|||
</div> |
|||
|
|||
<router-outlet></router-outlet> |
|||
@ -0,0 +1,66 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Component, OnInit } from '@angular/core'; |
|||
|
|||
import { |
|||
AppComponentBase, |
|||
AppsStoreService, |
|||
AuthService, |
|||
DialogService, |
|||
fadeAnimation, |
|||
ImmutableArray, |
|||
ModalView, |
|||
RuleDto, |
|||
RulesService, |
|||
SchemaDto, |
|||
SchemasService |
|||
} from 'shared'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-rules-page', |
|||
styleUrls: ['./rules-page.component.scss'], |
|||
templateUrl: './rules-page.component.html', |
|||
animations: [ |
|||
fadeAnimation |
|||
] |
|||
}) |
|||
export class RulesPageComponent extends AppComponentBase implements OnInit { |
|||
public addRuleDialog = new ModalView(true, false); |
|||
|
|||
public rules: ImmutableArray<RuleDto>; |
|||
public schemas: SchemaDto[]; |
|||
|
|||
constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, |
|||
private readonly schemasService: SchemasService, |
|||
private readonly rulesService: RulesService |
|||
) { |
|||
super(dialogs, apps, authService); |
|||
} |
|||
|
|||
public ngOnInit() { |
|||
this.load(); |
|||
} |
|||
|
|||
public load(showInfo = false) { |
|||
this.appNameOnce() |
|||
.switchMap(app => |
|||
this.schemasService.getSchemas(app) |
|||
.combineLatest(this.rulesService.getRules(app), |
|||
(s, w) => { return { rules: w, schemas: s }; })) |
|||
.subscribe(dtos => { |
|||
this.schemas = dtos.schemas; |
|||
this.rules = ImmutableArray.of(dtos.rules); |
|||
|
|||
if (showInfo) { |
|||
this.notifyInfo('Rules reloaded.'); |
|||
} |
|||
}, error => { |
|||
this.notifyError(error); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
<table class="table table-middle table-sm table-fixed table-borderless"> |
|||
<colgroup> |
|||
<col style="width: 100%" /> |
|||
<col style="width: 40px" /> |
|||
<col style="width: 40px" /> |
|||
<col style="width: 40px" /> |
|||
<col style="width: 40px" /> |
|||
<col style="width: 40px" /> |
|||
<col style="width: 40px" /> |
|||
</colgroup> |
|||
|
|||
<tr> |
|||
<th> |
|||
Schema |
|||
</th> |
|||
<th class="text-center" title="All"> |
|||
All |
|||
</th> |
|||
<th class="text-center" title="Created"> |
|||
C |
|||
</th> |
|||
<th class="text-center" title="Updated"> |
|||
U |
|||
</th> |
|||
<th class="text-center" title="Deleted"> |
|||
D |
|||
</th> |
|||
<th class="text-center" title="Changed"> |
|||
C |
|||
</th> |
|||
<th></th> |
|||
</tr> |
|||
|
|||
<tr *ngFor="let schema of triggerSchemas"> |
|||
<td> |
|||
<span class="truncate">{{schema.schema.name}}</span> |
|||
</td> |
|||
<td class="text-center" title="Created"> |
|||
<input type="checkbox" [ngModel]="schema.sendAll" (ngModelChange)="toggleAll(schema)" /> |
|||
</td> |
|||
<td class="text-center" title="Created"> |
|||
<input type="checkbox" [ngModel]="schema.sendCreate" (ngModelChange)="toggle(schema, 'sendCreate')" /> |
|||
</td> |
|||
<td class="text-center" title="Updated"> |
|||
<input type="checkbox" [ngModel]="schema.sendUpdate" (ngModelChange)="toggle(schema, 'sendUpdate')" /> |
|||
</td> |
|||
<td class="text-center" title="Deleted"> |
|||
<input type="checkbox" [ngModel]="schema.sendDelete" (ngModelChange)="toggle(schema, 'sendDelete')" /> |
|||
</td> |
|||
<td class="text-center" title="Published"> |
|||
<input type="checkbox" [ngModel]="schema.sendChange" (ngModelChange)="toggle(schema, 'sendChange')" /> |
|||
</td> |
|||
<td class="text-center"> |
|||
<button type="button" class="btn btn-link btn-secondary" (click)="removeSchema(schema)"> |
|||
<i class="icon-close"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
|
|||
<div class="section" *ngIf="schemasToAdd.length > 0"> |
|||
<form class="form-inline" (ngSubmit)="addSchema()"> |
|||
<div class="form-group mr-1"> |
|||
<select class="form-control schemas-control" [(ngModel)]="schemaToAdd" name="schema"> |
|||
<option *ngFor="let schema of schemasToAdd" [ngValue]="schema">{{schema.name}}</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<button type="submit" class="btn btn-success" [disabled]="!hasSchema">Add Schema</button> |
|||
</form> |
|||
</div> |
|||
@ -0,0 +1,8 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
.section { |
|||
border-top: 1px solid $color-border; |
|||
padding-top: 1rem; |
|||
padding-bottom: 0; |
|||
} |
|||
@ -0,0 +1,147 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; |
|||
|
|||
import { |
|||
ImmutableArray, |
|||
SchemaDto |
|||
} from 'shared'; |
|||
|
|||
export interface TriggerSchemaForm { |
|||
schema: SchemaDto; |
|||
sendAll: boolean; |
|||
sendCreate: boolean; |
|||
sendUpdate: boolean; |
|||
sendDelete: boolean; |
|||
sendChange: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'sqx-content-changed-trigger', |
|||
styleUrls: ['./content-changed-trigger.component.scss'], |
|||
templateUrl: './content-changed-trigger.component.html' |
|||
}) |
|||
export class ContentChangedTriggerComponent implements OnInit { |
|||
@Input() |
|||
public schemas: SchemaDto[]; |
|||
|
|||
@Input() |
|||
public trigger: any; |
|||
|
|||
@Output() |
|||
public triggerChanged = new EventEmitter<object>(); |
|||
|
|||
public triggerSchemas: ImmutableArray<TriggerSchemaForm>; |
|||
|
|||
public schemaToAdd: SchemaDto; |
|||
public schemasToAdd: ImmutableArray<SchemaDto>; |
|||
|
|||
public get hasSchema() { |
|||
return !!this.schemaToAdd; |
|||
} |
|||
|
|||
public ngOnInit() { |
|||
const triggerSchemas: any[] = (this.trigger.schemas = this.trigger.schemas || []); |
|||
|
|||
this.triggerSchemas = |
|||
ImmutableArray.of( |
|||
triggerSchemas.map(triggerSchema => { |
|||
const schema = this.schemas.find(s => s.id === triggerSchema.schemaId); |
|||
|
|||
if (schema) { |
|||
return this.updateSendAll({ |
|||
schema: schema, |
|||
sendAll: false, |
|||
sendCreate: triggerSchema.sendCreate, |
|||
sendUpdate: triggerSchema.sendUpdate, |
|||
sendDelete: triggerSchema.sendDelete, |
|||
sendChange: triggerSchema.sendChange |
|||
}); |
|||
} else { |
|||
return null; |
|||
} |
|||
}).filter(s => s !== null).map(s => s!)).sortByStringAsc(s => s.schema.name); |
|||
|
|||
this.schemasToAdd = |
|||
ImmutableArray.of( |
|||
this.schemas.filter(schema => |
|||
!triggerSchemas.find(s => s.schemaId === schema.id))) |
|||
.sortByStringAsc(x => x.name); |
|||
|
|||
this.schemaToAdd = this.schemasToAdd.values[0]; |
|||
} |
|||
|
|||
public save() { |
|||
const schemas = |
|||
this.triggerSchemas.values.map(s => { |
|||
return { |
|||
schemaId: s.schema.id, |
|||
sendCreate: s.sendCreate, |
|||
sendUpdate: s.sendUpdate, |
|||
sendDelete: s.sendDelete, |
|||
sendChange: s.sendChange |
|||
}; |
|||
}); |
|||
|
|||
this.triggerChanged.emit({ schemas }); |
|||
} |
|||
|
|||
public removeSchema(schemaForm: TriggerSchemaForm) { |
|||
this.triggerSchemas = this.triggerSchemas.remove(schemaForm); |
|||
|
|||
this.schemasToAdd = this.schemasToAdd.push(schemaForm.schema).sortByStringAsc(x => x.name); |
|||
this.schemaToAdd = this.schemasToAdd.values[0]; |
|||
} |
|||
|
|||
public addSchema() { |
|||
this.triggerSchemas = |
|||
this.triggerSchemas.push( |
|||
this.updateSendAll({ |
|||
schema: this.schemaToAdd, |
|||
sendAll: false, |
|||
sendCreate: false, |
|||
sendUpdate: false, |
|||
sendDelete: false, |
|||
sendChange: false |
|||
})).sortByStringAsc(x => x.schema.name); |
|||
|
|||
this.schemasToAdd = this.schemasToAdd.remove(this.schemaToAdd).sortByStringAsc(x => x.name); |
|||
this.schemaToAdd = this.schemasToAdd.values[0]; |
|||
} |
|||
|
|||
public toggle(schemaForm: TriggerSchemaForm, property: string) { |
|||
const newSchema = this.updateSendAll(Object.assign({}, schemaForm, { [property]: !schemaForm[property] })); |
|||
|
|||
this.triggerSchemas = this.triggerSchemas.replace(schemaForm, newSchema); |
|||
} |
|||
|
|||
public toggleAll(schemaForm: TriggerSchemaForm) { |
|||
const newSchema = this.updateAll(<any>{ schema: schemaForm.schema }, !schemaForm.sendAll); |
|||
|
|||
this.triggerSchemas = this.triggerSchemas.replace(schemaForm, newSchema); |
|||
} |
|||
|
|||
private updateAll(schemaForm: TriggerSchemaForm, value: boolean): TriggerSchemaForm { |
|||
schemaForm.sendAll = value; |
|||
schemaForm.sendCreate = value; |
|||
schemaForm.sendUpdate = value; |
|||
schemaForm.sendDelete = value; |
|||
schemaForm.sendChange = value; |
|||
return schemaForm; |
|||
} |
|||
|
|||
private updateSendAll(schemaForm: TriggerSchemaForm): TriggerSchemaForm { |
|||
schemaForm.sendAll = |
|||
schemaForm.sendCreate && |
|||
schemaForm.sendUpdate && |
|||
schemaForm.sendDelete && |
|||
schemaForm.sendChange; |
|||
|
|||
return schemaForm; |
|||
} |
|||
} |
|||
@ -1,10 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
export * from './pages/webhook-events-page.component'; |
|||
export * from './pages/webhook.component'; |
|||
export * from './pages/webhooks-page.component'; |
|||
@ -1,149 +0,0 @@ |
|||
<div class="table-items-row"> |
|||
<form [formGroup]="webhookForm" (ngSubmit)="save()"> |
|||
<table class="table table-middle table-sm table-borderless table-fixed"> |
|||
<colgroup> |
|||
<col style="width: 120px; text-align: right;" /> |
|||
<col style="width: 100%" /> |
|||
<col style="width: 40px" /> |
|||
</colgroup> |
|||
|
|||
<tr> |
|||
<td class="text-right" colspan="2"> |
|||
<button type="submit" class="btn btn-primary" [disabled]="!hasUrl">Save</button> |
|||
</td> |
|||
<td class="client-delete"> |
|||
<button type="button" class="btn btn-link btn-danger" |
|||
(sqxConfirmClick)="deleting.emit()" |
|||
confirmTitle="Delete webhook" |
|||
confirmText="Do you really want to delete the webhook?"> |
|||
<i class="icon-bin2"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
<tr> |
|||
<td>Url:</td> |
|||
<td> |
|||
<input type="url" class="form-control" formControlName="url" [attr.value]="webhook.url" #inputUrl /> |
|||
</td> |
|||
<td> |
|||
<button type="button" class="btn btn-primary btn-link" [sqxCopy]="inputUrl"> |
|||
<i class="icon-copy"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
<tr> |
|||
<td>Secret:</td> |
|||
<td> |
|||
<input readonly class="form-control" [attr.value]="webhook.sharedSecret" #inputSecret /> |
|||
</td> |
|||
<td> |
|||
<button type="button" class="btn btn-primary btn-link" [sqxCopy]="inputSecret"> |
|||
<i class="icon-copy"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</form> |
|||
|
|||
<div class="webhook-section" *ngIf="schemas.length > 0"> |
|||
<table class="table table-middle table-sm table-fixed table-borderless"> |
|||
<colgroup> |
|||
<col style="width: 100%" /> |
|||
<col style="width: 40px" /> |
|||
<col style="width: 40px" /> |
|||
<col style="width: 40px" /> |
|||
<col style="width: 40px" /> |
|||
<col style="width: 40px" /> |
|||
<col style="width: 40px" /> |
|||
</colgroup> |
|||
|
|||
<tr> |
|||
<th> |
|||
Schema |
|||
</th> |
|||
<th class="text-center" title="All"> |
|||
All |
|||
</th> |
|||
<th class="text-center" title="Created"> |
|||
C |
|||
</th> |
|||
<th class="text-center" title="Updated"> |
|||
U |
|||
</th> |
|||
<th class="text-center" title="Deleted"> |
|||
D |
|||
</th> |
|||
<th class="text-center" title="Published"> |
|||
P |
|||
</th> |
|||
<th></th> |
|||
</tr> |
|||
|
|||
<tr *ngFor="let schema of schemas"> |
|||
<td> |
|||
<span class="truncate">{{schema.schema.name}}</span> |
|||
</td> |
|||
<td class="text-center" title="Created"> |
|||
<input type="checkbox" [ngModel]="schema.sendAll" (ngModelChange)="toggleAll(schema)" /> |
|||
</td> |
|||
<td class="text-center" title="Created"> |
|||
<input type="checkbox" [ngModel]="schema.sendCreate" (ngModelChange)="toggle(schema, 'sendCreate')" /> |
|||
</td> |
|||
<td class="text-center" title="Updated"> |
|||
<input type="checkbox" [ngModel]="schema.sendUpdate" (ngModelChange)="toggle(schema, 'sendUpdate')" /> |
|||
</td> |
|||
<td class="text-center" title="Deleted"> |
|||
<input type="checkbox" [ngModel]="schema.sendDelete" (ngModelChange)="toggle(schema, 'sendDelete')" /> |
|||
</td> |
|||
<td class="text-center" title="Published"> |
|||
<input type="checkbox" [ngModel]="schema.sendPublish" (ngModelChange)="toggle(schema, 'sendPublish')" /> |
|||
</td> |
|||
<td class="text-center"> |
|||
<button type="button" class="btn btn-link btn-secondary" |
|||
(sqxConfirmClick)="removeSchema(schema)" |
|||
confirmTitle="Remove schema" |
|||
confirmText="Do you really want to remove the schema?"> |
|||
<i class="icon-close"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</div> |
|||
|
|||
<div class="webhook-section" *ngIf="schemasToAdd.length > 0"> |
|||
<form class="form-inline" (ngSubmit)="addSchema()"> |
|||
<div class="form-group mr-1"> |
|||
<select class="form-control schemas-control" [(ngModel)]="schemaToAdd" name="schema"> |
|||
<option *ngFor="let schema of schemasToAdd" [ngValue]="schema">{{schema.name}}</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<button type="submit" class="btn btn-success" [disabled]="!hasSchema">Add Schema</button> |
|||
</form> |
|||
</div> |
|||
|
|||
<div class="webhook-section" *ngIf="webhook.averageRequestTimeMs > 0"> |
|||
<div class="row"> |
|||
<div class="col-3"> |
|||
<span title="Succeeded Requests" [class.success]="webhook.totalSucceeded > 0"> |
|||
<i class="icon-checkmark"></i> {{webhook.totalSucceeded}} |
|||
</span> |
|||
</div> |
|||
<div class="col-3"> |
|||
<span title="Failed Requests" [class.failed]="webhook.totalFailed > 0"> |
|||
<i class="icon-bug"></i> {{webhook.totalFailed}} |
|||
</span> |
|||
</div> |
|||
<div class="col-3"> |
|||
<span title="Timeout Requests" [class.failed]="webhook.totalTimedout > 0"> |
|||
<i class="icon-timeout"></i> {{webhook.totalTimedout}} |
|||
</span> |
|||
</div> |
|||
<div class="col-3"> |
|||
<span title="Average Response Time"> |
|||
<i class="icon-elapsed"></i> {{webhook.averageRequestTimeMs}} ms |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -1,13 +0,0 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
.schemas-control { |
|||
width: 18rem; |
|||
} |
|||
|
|||
.webhook-section { |
|||
border-top: 1px solid $color-border; |
|||
margin-left: -1.25rem; |
|||
margin-right: -1.25rem; |
|||
padding: 1rem 1.25rem 0; |
|||
} |
|||
@ -1,174 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; |
|||
import { FormBuilder, Validators } from '@angular/forms'; |
|||
|
|||
import { |
|||
ImmutableArray, |
|||
SchemaDto, |
|||
UpdateWebhookDto, |
|||
WebhookDto, |
|||
WebhookSchemaDto |
|||
} from 'shared'; |
|||
|
|||
export interface WebhookSchemaForm { |
|||
schema: SchemaDto; |
|||
sendAll: boolean; |
|||
sendCreate: boolean; |
|||
sendUpdate: boolean; |
|||
sendDelete: boolean; |
|||
sendPublish: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'sqx-webhook', |
|||
styleUrls: ['./webhook.component.scss'], |
|||
templateUrl: './webhook.component.html' |
|||
}) |
|||
export class WebhookComponent implements OnInit { |
|||
@Output() |
|||
public deleting = new EventEmitter(); |
|||
|
|||
@Output() |
|||
public updating = new EventEmitter<UpdateWebhookDto>(); |
|||
|
|||
@Input() |
|||
public allSchemas: SchemaDto[]; |
|||
|
|||
@Input() |
|||
public webhook: WebhookDto; |
|||
|
|||
public schemas: ImmutableArray<WebhookSchemaForm>; |
|||
|
|||
public schemaToAdd: SchemaDto; |
|||
public schemasToAdd: ImmutableArray<SchemaDto>; |
|||
|
|||
public webhookForm = |
|||
this.formBuilder.group({ |
|||
url: ['', |
|||
[ |
|||
Validators.required |
|||
]] |
|||
}); |
|||
|
|||
public get hasUrl() { |
|||
return this.webhookForm.controls['url'].value && this.webhookForm.controls['url'].value.length > 0; |
|||
} |
|||
|
|||
public get hasSchema() { |
|||
return !!this.schemaToAdd; |
|||
} |
|||
|
|||
constructor( |
|||
private readonly formBuilder: FormBuilder |
|||
) { |
|||
} |
|||
|
|||
public ngOnInit() { |
|||
this.webhookForm.controls['url'].setValue(this.webhook.url); |
|||
|
|||
this.schemas = |
|||
ImmutableArray.of( |
|||
this.webhook.schemas.map(webhookSchema => { |
|||
const schema = this.allSchemas.find(s => s.id === webhookSchema.schemaId); |
|||
|
|||
if (schema) { |
|||
return this.updateSendAll({ |
|||
schema: schema, |
|||
sendAll: false, |
|||
sendCreate: webhookSchema.sendCreate, |
|||
sendUpdate: webhookSchema.sendUpdate, |
|||
sendDelete: webhookSchema.sendDelete, |
|||
sendPublish: webhookSchema.sendPublish |
|||
}); |
|||
} else { |
|||
return null; |
|||
} |
|||
}).filter(w => w !== null).map(w => w!)).sortByStringAsc(x => x.schema.name); |
|||
|
|||
this.schemasToAdd = |
|||
ImmutableArray.of( |
|||
this.allSchemas.filter(schema => |
|||
!this.webhook.schemas.find(w => w.schemaId === schema.id))) |
|||
.sortByStringAsc(x => x.name); |
|||
this.schemaToAdd = this.schemasToAdd.values[0]; |
|||
} |
|||
|
|||
public removeSchema(schemaForm: WebhookSchemaForm) { |
|||
this.schemas = this.schemas.remove(schemaForm); |
|||
|
|||
this.schemasToAdd = this.schemasToAdd.push(schemaForm.schema).sortByStringAsc(x => x.name); |
|||
this.schemaToAdd = this.schemasToAdd.values[0]; |
|||
} |
|||
|
|||
public addSchema() { |
|||
this.schemas = |
|||
this.schemas.push( |
|||
this.updateSendAll({ |
|||
schema: this.schemaToAdd, |
|||
sendAll: false, |
|||
sendCreate: false, |
|||
sendUpdate: false, |
|||
sendDelete: false, |
|||
sendPublish: false |
|||
})).sortByStringAsc(x => x.schema.name); |
|||
|
|||
this.schemasToAdd = this.schemasToAdd.remove(this.schemaToAdd).sortByStringAsc(x => x.name); |
|||
this.schemaToAdd = this.schemasToAdd.values[0]; |
|||
} |
|||
|
|||
public save() { |
|||
const requestDto = |
|||
new UpdateWebhookDto( |
|||
this.webhookForm.controls['url'].value, |
|||
this.schemas.values.map(schema => |
|||
new WebhookSchemaDto( |
|||
schema.schema.id, |
|||
schema.sendCreate, |
|||
schema.sendUpdate, |
|||
schema.sendDelete, |
|||
schema.sendPublish))); |
|||
|
|||
this.emitUpdating(requestDto); |
|||
} |
|||
|
|||
public toggle(schemaForm: WebhookSchemaForm, property: string) { |
|||
const newSchema = this.updateSendAll(Object.assign({}, schemaForm, { [property]: !schemaForm[property] })); |
|||
|
|||
this.schemas = this.schemas.replace(schemaForm, newSchema); |
|||
} |
|||
|
|||
public toggleAll(schemaForm: WebhookSchemaForm) { |
|||
const newSchema = this.updateAll(<any>{ schema: schemaForm.schema }, !schemaForm.sendAll); |
|||
|
|||
this.schemas = this.schemas.replace(schemaForm, newSchema); |
|||
} |
|||
|
|||
private emitUpdating(dto: UpdateWebhookDto) { |
|||
this.updating.emit(dto); |
|||
} |
|||
|
|||
private updateAll(schemaForm: WebhookSchemaForm, value: boolean): WebhookSchemaForm { |
|||
schemaForm.sendAll = value; |
|||
schemaForm.sendCreate = value; |
|||
schemaForm.sendUpdate = value; |
|||
schemaForm.sendDelete = value; |
|||
schemaForm.sendPublish = value; |
|||
return schemaForm; |
|||
} |
|||
|
|||
private updateSendAll(schemaForm: WebhookSchemaForm): WebhookSchemaForm { |
|||
schemaForm.sendAll = |
|||
schemaForm.sendCreate && |
|||
schemaForm.sendUpdate && |
|||
schemaForm.sendDelete && |
|||
schemaForm.sendPublish; |
|||
|
|||
return schemaForm; |
|||
} |
|||
} |
|||
@ -1,72 +0,0 @@ |
|||
<sqx-title message="{app} | Webhooks" parameter1="app" value1="{{appName() | async}}"></sqx-title> |
|||
|
|||
<sqx-panel panelWidth="50rem"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<div class="float-right"> |
|||
<button class="btn btn-link btn-decent" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
|
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
</div> |
|||
|
|||
<h3 class="panel-title">Webhooks</h3> |
|||
</div> |
|||
|
|||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content panel-content-scroll"> |
|||
<div class="table-items-row table-items-row-empty" *ngIf="webhooks && webhooks.length === 0"> |
|||
No Webhook created yet. |
|||
</div> |
|||
|
|||
<div *ngIf="webhooks"> |
|||
<sqx-webhook *ngFor="let webhook of webhooks" [webhook]="webhook" [allSchemas]="schemas" |
|||
(updating)="updateWebhook(webhook, $event)" |
|||
(deleting)="deleteWebhook(webhook)"></sqx-webhook> |
|||
|
|||
<div class="table-items-footer"> |
|||
<form [formGroup]="addWebhookForm" (ngSubmit)="addWebhook()"> |
|||
<div class="row no-gutters"> |
|||
<div class="col"> |
|||
<sqx-control-errors for="url" [submitted]="addWebhookFormSubmitted"></sqx-control-errors> |
|||
|
|||
<input type="text" class="form-control url-control" formControlName="url" placeholder="Enter webhook url" autocomplete="off" /> |
|||
</div> |
|||
<div class="col col-auto pl-1"> |
|||
<button type="submit" class="btn btn-success" [disabled]="!hasUrl">Add Webhook</button> |
|||
</div> |
|||
<div class="col col-auto pl-1"> |
|||
<button type="reset" class="btn btn-secondary" (click)="cancelAddWebhook()" [disabled]="addWebhookFormSubmitted">Cancel</button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="panel-sidebar"> |
|||
<a class="panel-link" routerLink="events" routerLinkActive="active" #linkHistory> |
|||
<i class="icon-time"></i> |
|||
</a> |
|||
<a class="panel-link" routerLink="help" routerLinkActive="active" #linkHelp> |
|||
<i class="icon-help"></i> |
|||
</a> |
|||
|
|||
<sqx-onboarding-tooltip id="history" [for]="linkHistory" position="leftTop" after="120000"> |
|||
The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time. |
|||
</sqx-onboarding-tooltip> |
|||
|
|||
<sqx-onboarding-tooltip id="help" [for]="linkHelp" position="leftTop" after="180000"> |
|||
Click the help icon to show a context specific help page. Go to <a href="https://docs.squidex.io" target="_blank">https://docs.squidex.io</a> for the full documentation. |
|||
</sqx-onboarding-tooltip> |
|||
</div> |
|||
</div> |
|||
</sqx-panel> |
|||
|
|||
<router-outlet></router-outlet> |
|||
@ -1,136 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { FormBuilder, Validators } from '@angular/forms'; |
|||
|
|||
import { |
|||
AppComponentBase, |
|||
AppsStoreService, |
|||
AuthService, |
|||
CreateWebhookDto, |
|||
DateTime, |
|||
DialogService, |
|||
ImmutableArray, |
|||
SchemaDto, |
|||
SchemasService, |
|||
WebhookDto, |
|||
WebhooksService, |
|||
UpdateWebhookDto |
|||
} from 'shared'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-webhooks-page', |
|||
styleUrls: ['./webhooks-page.component.scss'], |
|||
templateUrl: './webhooks-page.component.html' |
|||
}) |
|||
export class WebhooksPageComponent extends AppComponentBase implements OnInit { |
|||
public webhooks: ImmutableArray<WebhookDto>; |
|||
public schemas: SchemaDto[]; |
|||
|
|||
public addWebhookFormSubmitted = false; |
|||
public addWebhookForm = |
|||
this.formBuilder.group({ |
|||
url: ['', |
|||
[ |
|||
Validators.required |
|||
]] |
|||
}); |
|||
|
|||
public get hasUrl() { |
|||
return this.addWebhookForm.controls['url'].value && this.addWebhookForm.controls['url'].value.length > 0; |
|||
} |
|||
|
|||
constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, |
|||
private readonly schemasService: SchemasService, |
|||
private readonly webhooksService: WebhooksService, |
|||
private readonly formBuilder: FormBuilder |
|||
) { |
|||
super(dialogs, apps, authService); |
|||
} |
|||
|
|||
public ngOnInit() { |
|||
this.load(); |
|||
} |
|||
|
|||
public load(showInfo = false) { |
|||
this.appNameOnce() |
|||
.switchMap(app => |
|||
this.schemasService.getSchemas(app) |
|||
.combineLatest(this.webhooksService.getWebhooks(app), |
|||
(s, w) => { return { webhooks: w, schemas: s }; })) |
|||
.subscribe(dtos => { |
|||
this.schemas = dtos.schemas; |
|||
this.webhooks = ImmutableArray.of(dtos.webhooks); |
|||
|
|||
if (showInfo) { |
|||
this.notifyInfo('Webhooks reloaded.'); |
|||
} |
|||
}, error => { |
|||
this.notifyError(error); |
|||
}); |
|||
} |
|||
|
|||
public deleteWebhook(webhook: WebhookDto) { |
|||
this.appNameOnce() |
|||
.switchMap(app => this.webhooksService.deleteWebhook(app, webhook.id, webhook.version)) |
|||
.subscribe(dto => { |
|||
this.webhooks = this.webhooks.remove(webhook); |
|||
}, error => { |
|||
this.notifyError(error); |
|||
}); |
|||
} |
|||
|
|||
public updateWebhook(webhook: WebhookDto, requestDto: UpdateWebhookDto) { |
|||
this.appNameOnce() |
|||
.switchMap(app => this.webhooksService.putWebhook(app, webhook.id, requestDto, webhook.version)) |
|||
.subscribe(dto => { |
|||
this.webhooks = this.webhooks.replace(webhook, webhook.update(requestDto, this.userToken, dto.version)); |
|||
|
|||
this.notifyInfo('Webhook saved.'); |
|||
}, error => { |
|||
this.notifyError(error); |
|||
}); |
|||
} |
|||
|
|||
public addWebhook() { |
|||
this.addWebhookFormSubmitted = true; |
|||
|
|||
if (this.addWebhookForm.valid) { |
|||
this.addWebhookForm.disable(); |
|||
|
|||
const requestDto = new CreateWebhookDto(this.addWebhookForm.controls['url'].value, []); |
|||
|
|||
const me = this.userToken; |
|||
|
|||
this.appNameOnce() |
|||
.switchMap(app => this.webhooksService.postWebhook(app, requestDto, me, DateTime.now())) |
|||
.subscribe(dto => { |
|||
this.webhooks = this.webhooks.push(dto); |
|||
|
|||
this.resetWebhookForm(); |
|||
}, error => { |
|||
this.notifyError(error); |
|||
this.enableWebhookForm(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public cancelAddWebhook() { |
|||
this.resetWebhookForm(); |
|||
} |
|||
|
|||
private enableWebhookForm() { |
|||
this.addWebhookForm.enable(); |
|||
} |
|||
|
|||
private resetWebhookForm() { |
|||
this.addWebhookFormSubmitted = false; |
|||
this.addWebhookForm.enable(); |
|||
this.addWebhookForm.reset(); |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { |
|||
KeysPipe |
|||
} from './keys.pipe'; |
|||
|
|||
describe('KeysPipe', () => { |
|||
it('should return keys', () => { |
|||
const value = { |
|||
key1: 1, |
|||
key2: 2 |
|||
}; |
|||
|
|||
const pipe = new KeysPipe(); |
|||
|
|||
const actual = pipe.transform(value); |
|||
const expected = ['key1', 'key2']; |
|||
|
|||
expect(actual).toBe(expected); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,18 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Pipe, PipeTransform } from '@angular/core'; |
|||
|
|||
@Pipe({ |
|||
name: 'sqxKeys', |
|||
pure: true |
|||
}) |
|||
export class KeysPipe implements PipeTransform { |
|||
public transform(value: any, args: any[] = null): any { |
|||
return Object.keys(value); |
|||
} |
|||
} |
|||
@ -0,0 +1,310 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
|
|||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; |
|||
import { inject, TestBed } from '@angular/core/testing'; |
|||
|
|||
import { |
|||
AnalyticsService, |
|||
ApiUrlConfig, |
|||
CreateRuleDto, |
|||
DateTime, |
|||
UpdateRuleDto, |
|||
Version, |
|||
RuleDto, |
|||
RuleEventDto, |
|||
RuleEventsDto, |
|||
RulesService |
|||
} from './../'; |
|||
|
|||
describe('RuleDto', () => { |
|||
const creation = DateTime.today(); |
|||
const creator = 'not-me'; |
|||
const modified = DateTime.now(); |
|||
const modifier = 'me'; |
|||
const version = new Version('1'); |
|||
const newVersion = new Version('2'); |
|||
|
|||
it('should update trigger and action', () => { |
|||
const update = new UpdateRuleDto({ param1: 1, triggerType: 'NewType' }, { param2: 2, actionType: 'NewType' }); |
|||
|
|||
const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); |
|||
const rule_2 = rule_1.update(update, modifier, newVersion, modified); |
|||
|
|||
expect(rule_2.trigger).toEqual(update.trigger); |
|||
expect(rule_2.triggerType).toEqual(update.trigger.triggerType); |
|||
expect(rule_2.action).toEqual(update.action); |
|||
expect(rule_2.actionType).toEqual(update.action.actionType); |
|||
expect(rule_2.lastModified).toEqual(modified); |
|||
expect(rule_2.lastModifiedBy).toEqual(modifier); |
|||
expect(rule_2.version).toEqual(newVersion); |
|||
}); |
|||
|
|||
it('should enable', () => { |
|||
const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); |
|||
const rule_2 = rule_1.enable(modifier, newVersion, modified); |
|||
|
|||
expect(rule_2.isEnabled).toBeTruthy(); |
|||
expect(rule_2.lastModified).toEqual(modified); |
|||
expect(rule_2.lastModifiedBy).toEqual(modifier); |
|||
expect(rule_2.version).toEqual(newVersion); |
|||
}); |
|||
|
|||
it('should disable', () => { |
|||
const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); |
|||
const rule_2 = rule_1.disable(modifier, newVersion, modified); |
|||
|
|||
expect(rule_2.isEnabled).toBeFalsy(); |
|||
expect(rule_2.lastModified).toEqual(modified); |
|||
expect(rule_2.lastModifiedBy).toEqual(modifier); |
|||
expect(rule_2.version).toEqual(newVersion); |
|||
}); |
|||
}); |
|||
|
|||
describe('RulesService', () => { |
|||
const now = DateTime.now(); |
|||
const user = 'me'; |
|||
const version = new Version('1'); |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [ |
|||
HttpClientTestingModule |
|||
], |
|||
providers: [ |
|||
RulesService, |
|||
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, |
|||
{ provide: AnalyticsService, useValue: new AnalyticsService() } |
|||
] |
|||
}); |
|||
}); |
|||
|
|||
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { |
|||
httpMock.verify(); |
|||
})); |
|||
|
|||
it('should make get request to get app rules', |
|||
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { |
|||
|
|||
let rules: RuleDto[] | null = null; |
|||
|
|||
rulesService.getRules('my-app').subscribe(result => { |
|||
rules = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules'); |
|||
|
|||
expect(req.request.method).toEqual('GET'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush([ |
|||
{ |
|||
id: 'id1', |
|||
created: '2016-12-12T10:10', |
|||
createdBy: 'CreatedBy1', |
|||
lastModified: '2017-12-12T10:10', |
|||
lastModifiedBy: 'LastModifiedBy1', |
|||
url: 'http://squidex.io/hook', |
|||
version: '1', |
|||
trigger: { |
|||
param1: 1, |
|||
param2: 2, |
|||
triggerType: 'ContentChanged' |
|||
}, |
|||
action: { |
|||
param3: 3, |
|||
param4: 4, |
|||
actionType: 'Webhook' |
|||
}, |
|||
isEnabled: true |
|||
} |
|||
]); |
|||
|
|||
expect(rules).toEqual([ |
|||
new RuleDto('id1', 'CreatedBy1', 'LastModifiedBy1', |
|||
DateTime.parseISO_UTC('2016-12-12T10:10'), |
|||
DateTime.parseISO_UTC('2017-12-12T10:10'), |
|||
version, |
|||
true, |
|||
{ |
|||
param1: 1, |
|||
param2: 2 |
|||
}, |
|||
'ContentChanged', |
|||
{ |
|||
param3: 3, |
|||
param4: 4 |
|||
}, |
|||
'Webhook') |
|||
]); |
|||
})); |
|||
|
|||
it('should make post request to create rule', |
|||
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { |
|||
|
|||
const dto = new CreateRuleDto({ |
|||
param1: 1, |
|||
param2: 2, |
|||
triggerType: 'ContentChanged' |
|||
}, { |
|||
param3: 3, |
|||
param4: 4, |
|||
actionType: 'Webhook' |
|||
}); |
|||
|
|||
let rule: RuleDto | null = null; |
|||
|
|||
rulesService.postRule('my-app', dto, user, now).subscribe(result => { |
|||
rule = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules'); |
|||
|
|||
expect(req.request.method).toEqual('POST'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush({ id: 'id1', sharedSecret: 'token1', schemaId: 'schema1' }, { |
|||
headers: { |
|||
etag: '1' |
|||
} |
|||
}); |
|||
|
|||
expect(rule).toEqual( |
|||
new RuleDto('id1', 'CreatedBy1', 'LastModifiedBy1', |
|||
DateTime.parseISO_UTC('2016-12-12T10:10'), |
|||
DateTime.parseISO_UTC('2017-12-12T10:10'), |
|||
version, |
|||
true, |
|||
{ |
|||
param1: 1, |
|||
param2: 2 |
|||
}, |
|||
'ContentChanged', |
|||
{ |
|||
param3: 3, |
|||
param4: 4 |
|||
}, |
|||
'Webhook')); |
|||
})); |
|||
|
|||
it('should make put request to update rule', |
|||
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { |
|||
|
|||
const dto = new UpdateRuleDto({ param1: 1 }, { param2: 2 }); |
|||
|
|||
rulesService.putRule('my-app', '123', dto, version).subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123'); |
|||
|
|||
expect(req.request.method).toEqual('PUT'); |
|||
expect(req.request.headers.get('If-Match')).toEqual(version.value); |
|||
|
|||
req.flush({}); |
|||
})); |
|||
|
|||
it('should make put request to enable rule', |
|||
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { |
|||
|
|||
rulesService.enableRule('my-app', '123', version).subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123/enable'); |
|||
|
|||
expect(req.request.method).toEqual('PUT'); |
|||
expect(req.request.headers.get('If-Match')).toEqual(version.value); |
|||
|
|||
req.flush({}); |
|||
})); |
|||
|
|||
it('should make put request to disable rule', |
|||
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { |
|||
|
|||
rulesService.disableRule('my-app', '123', version).subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123/disable'); |
|||
|
|||
expect(req.request.method).toEqual('PUT'); |
|||
expect(req.request.headers.get('If-Match')).toEqual(version.value); |
|||
|
|||
req.flush({}); |
|||
})); |
|||
|
|||
it('should make delete request to delete rule', |
|||
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { |
|||
|
|||
rulesService.deleteRule('my-app', '123', version).subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123'); |
|||
|
|||
expect(req.request.method).toEqual('DELETE'); |
|||
expect(req.request.headers.get('If-Match')).toEqual(version.value); |
|||
})); |
|||
|
|||
it('should make get request to get app rule events', |
|||
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { |
|||
|
|||
let rules: RuleEventsDto | null = null; |
|||
|
|||
rulesService.getEvents('my-app', 10, 20).subscribe(result => { |
|||
rules = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/events?take=10&skip=20'); |
|||
|
|||
expect(req.request.method).toEqual('GET'); |
|||
|
|||
req.flush({ |
|||
total: 20, |
|||
items: [ |
|||
{ |
|||
id: 'id1', |
|||
created: '2017-12-12T10:10', |
|||
eventName: 'event1', |
|||
nextAttempt: '2017-12-12T12:10', |
|||
jobResult: 'Failed', |
|||
lastDump: 'dump1', |
|||
numCalls: 1, |
|||
description: 'url1', |
|||
result: 'Failed' |
|||
}, |
|||
{ |
|||
id: 'id2', |
|||
created: '2017-12-13T10:10', |
|||
eventName: 'event2', |
|||
nextAttempt: '2017-12-13T12:10', |
|||
jobResult: 'Failed', |
|||
lastDump: 'dump2', |
|||
numCalls: 2, |
|||
description: 'url2', |
|||
result: 'Failed' |
|||
} |
|||
] |
|||
}); |
|||
|
|||
expect(rules).toEqual( |
|||
new RuleEventsDto(20, [ |
|||
new RuleEventDto('id1', |
|||
DateTime.parseISO_UTC('2017-12-12T10:10'), |
|||
DateTime.parseISO_UTC('2017-12-12T12:10'), |
|||
'event1', 'url1', 'dump1', 'Failed', 'Failed', 1), |
|||
new RuleEventDto('id2', |
|||
DateTime.parseISO_UTC('2017-12-13T10:10'), |
|||
DateTime.parseISO_UTC('2017-12-13T12:10'), |
|||
'event2', 'url2', 'dump2', 'Failed', 'Failed', 2) |
|||
])); |
|||
})); |
|||
|
|||
it('should make put request to enqueue rule event', |
|||
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { |
|||
|
|||
rulesService.enqueueEvent('my-app', '123').subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/events/123'); |
|||
|
|||
expect(req.request.method).toEqual('PUT'); |
|||
})); |
|||
}); |
|||
@ -0,0 +1,260 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { Injectable } from '@angular/core'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
import 'framework/angular/http-extensions'; |
|||
|
|||
import { |
|||
AnalyticsService, |
|||
ApiUrlConfig, |
|||
DateTime, |
|||
HTTP, |
|||
Version, |
|||
Versioned |
|||
} from 'framework'; |
|||
|
|||
export const ruleTriggers: any = { |
|||
'ContentChanged': 'Content changed' |
|||
}; |
|||
|
|||
export const ruleActions: any = { |
|||
'Webhook': 'Send Webhooks' |
|||
}; |
|||
|
|||
export class RuleDto { |
|||
constructor( |
|||
public readonly id: string, |
|||
public readonly createdBy: string, |
|||
public readonly lastModifiedBy: string, |
|||
public readonly created: DateTime, |
|||
public readonly lastModified: DateTime, |
|||
public readonly version: Version, |
|||
public readonly isEnabled: boolean, |
|||
public readonly trigger: any, |
|||
public readonly triggerType: string, |
|||
public readonly action: any, |
|||
public readonly actionType: string |
|||
) { |
|||
} |
|||
|
|||
public update(update: UpdateRuleDto, user: string, version: Version, now?: DateTime): RuleDto { |
|||
return new RuleDto( |
|||
this.id, |
|||
this.createdBy, user, |
|||
this.created, now || DateTime.now(), |
|||
version, |
|||
this.isEnabled, |
|||
update.trigger, |
|||
update.trigger['triggerType'], |
|||
update.action, |
|||
update.action['actionType']); |
|||
} |
|||
|
|||
public enable(user: string, version: Version, now?: DateTime): RuleDto { |
|||
return new RuleDto( |
|||
this.id, |
|||
this.createdBy, user, |
|||
this.created, now || DateTime.now(), |
|||
version, |
|||
true, |
|||
this.trigger, |
|||
this.triggerType, |
|||
this.action, |
|||
this.actionType); |
|||
} |
|||
|
|||
public disable(user: string, version: Version, now?: DateTime): RuleDto { |
|||
return new RuleDto( |
|||
this.id, |
|||
this.createdBy, user, |
|||
this.created, now || DateTime.now(), |
|||
version, |
|||
true, |
|||
this.trigger, |
|||
this.triggerType, |
|||
this.action, |
|||
this.actionType); |
|||
} |
|||
} |
|||
|
|||
export class RuleEventDto { |
|||
constructor( |
|||
public readonly id: string, |
|||
public readonly created: DateTime, |
|||
public readonly nextAttempt: DateTime | null, |
|||
public readonly eventName: string, |
|||
public readonly description: string, |
|||
public readonly lastDump: string, |
|||
public readonly result: string, |
|||
public readonly jobResult: string, |
|||
public readonly numCalls: number |
|||
) { |
|||
} |
|||
} |
|||
|
|||
export class RuleEventsDto { |
|||
constructor( |
|||
public readonly total: number, |
|||
public readonly items: RuleEventDto[] |
|||
) { |
|||
} |
|||
} |
|||
|
|||
export class CreateRuleDto { |
|||
constructor( |
|||
public readonly trigger: any, |
|||
public readonly action: any |
|||
) { |
|||
} |
|||
} |
|||
|
|||
export class UpdateRuleDto { |
|||
constructor( |
|||
public readonly trigger: any, |
|||
public readonly action: any |
|||
) { |
|||
} |
|||
} |
|||
|
|||
@Injectable() |
|||
export class RulesService { |
|||
constructor( |
|||
private readonly http: HttpClient, |
|||
private readonly apiUrl: ApiUrlConfig, |
|||
private readonly analytics: AnalyticsService |
|||
) { |
|||
} |
|||
|
|||
public getRules(appName: string): Observable<RuleDto[]> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); |
|||
|
|||
return HTTP.getVersioned<any>(this.http, url) |
|||
.map(response => { |
|||
const items: any[] = response.payload.body; |
|||
|
|||
return items.map(item => { |
|||
return new RuleDto( |
|||
item.id, |
|||
item.createdBy, |
|||
item.lastModifiedBy, |
|||
DateTime.parseISO_UTC(item.created), |
|||
DateTime.parseISO_UTC(item.lastModified), |
|||
new Version(item.version.toString()), |
|||
item.isEnabled, |
|||
item.trigger, |
|||
item.trigger.triggerType, |
|||
item.action, |
|||
item.action.actionType); |
|||
}); |
|||
}) |
|||
.pretifyError('Failed to load Rules. Please reload.'); |
|||
} |
|||
|
|||
public postRule(appName: string, dto: CreateRuleDto, user: string, now: DateTime): Observable<RuleDto> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); |
|||
|
|||
return HTTP.postVersioned<any>(this.http, url, dto) |
|||
.map(response => { |
|||
const body = response.payload.body; |
|||
|
|||
return new RuleDto( |
|||
body.id, |
|||
user, |
|||
user, |
|||
now, |
|||
now, |
|||
response.version, |
|||
true, |
|||
dto.trigger, |
|||
dto.trigger.triggerType, |
|||
dto.action, |
|||
dto.action.actionType); |
|||
}) |
|||
.do(() => { |
|||
this.analytics.trackEvent('Rule', 'Created', appName); |
|||
}) |
|||
.pretifyError('Failed to create rule. Please reload.'); |
|||
} |
|||
|
|||
public putRule(appName: string, id: string, dto: UpdateRuleDto, version: Version): Observable<Versioned<any>> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`); |
|||
|
|||
return HTTP.putVersioned(this.http, url, dto, version) |
|||
.do(() => { |
|||
this.analytics.trackEvent('Rule', 'Updated', appName); |
|||
}) |
|||
.pretifyError('Failed to update rule. Please reload.'); |
|||
} |
|||
|
|||
public enableRule(appName: string, id: string, version: Version): Observable<Versioned<any>> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/enable`); |
|||
|
|||
return HTTP.putVersioned(this.http, url, {}, version) |
|||
.do(() => { |
|||
this.analytics.trackEvent('Rule', 'Updated', appName); |
|||
}) |
|||
.pretifyError('Failed to enable rule. Please reload.'); |
|||
} |
|||
|
|||
public disableRule(appName: string, id: string, version: Version): Observable<Versioned<any>> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/disable`); |
|||
|
|||
return HTTP.putVersioned(this.http, url, {}, version) |
|||
.do(() => { |
|||
this.analytics.trackEvent('Rule', 'Updated', appName); |
|||
}) |
|||
.pretifyError('Failed to disable rule. Please reload.'); |
|||
} |
|||
|
|||
public deleteRule(appName: string, id: string, version: Version): Observable<any> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`); |
|||
|
|||
return HTTP.deleteVersioned(this.http, url, version) |
|||
.do(() => { |
|||
this.analytics.trackEvent('Rule', 'Deleted', appName); |
|||
}) |
|||
.pretifyError('Failed to delete rule. Please reload.'); |
|||
} |
|||
|
|||
public getEvents(appName: string, take: number, skip: number): Observable<RuleEventsDto> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events?take=${take}&skip=${skip}`); |
|||
|
|||
return HTTP.getVersioned<any>(this.http, url) |
|||
.map(response => { |
|||
const body = response.payload.body; |
|||
|
|||
const items: any[] = body.items; |
|||
|
|||
return new RuleEventsDto(body.total, items.map(item => { |
|||
return new RuleEventDto( |
|||
item.id, |
|||
DateTime.parseISO_UTC(item.created), |
|||
item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, |
|||
item.eventName, |
|||
item.description, |
|||
item.lastDump, |
|||
item.result, |
|||
item.jobResult, |
|||
item.numCalls); |
|||
})); |
|||
}) |
|||
.pretifyError('Failed to load events. Please reload.'); |
|||
} |
|||
|
|||
public enqueueEvent(appName: string, id: string): Observable<any> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events/${id}`); |
|||
|
|||
return HTTP.putVersioned(this.http, url, {}) |
|||
.do(() => { |
|||
this.analytics.trackEvent('Rule', 'EventEnqueued', appName); |
|||
}) |
|||
.pretifyError('Failed to enqueue rule event. Please reload.'); |
|||
} |
|||
} |
|||
@ -1,245 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
|
|||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; |
|||
import { inject, TestBed } from '@angular/core/testing'; |
|||
|
|||
import { |
|||
AnalyticsService, |
|||
ApiUrlConfig, |
|||
CreateWebhookDto, |
|||
DateTime, |
|||
UpdateWebhookDto, |
|||
Version, |
|||
WebhookDto, |
|||
WebhookEventDto, |
|||
WebhookEventsDto, |
|||
WebhookSchemaDto, |
|||
WebhooksService |
|||
} from './../'; |
|||
|
|||
describe('WebhookDto', () => { |
|||
const creation = DateTime.today(); |
|||
const creator = 'not-me'; |
|||
const modified = DateTime.now(); |
|||
const modifier = 'me'; |
|||
const version = new Version('1'); |
|||
const newVersion = new Version('2'); |
|||
|
|||
it('should update url and schemas', () => { |
|||
const webhook_1 = new WebhookDto('id1', 'token1', creator, creator, creation, creation, version, [], 'http://squidex.io/hook', 1, 2, 3, 4); |
|||
const webhook_2 = |
|||
webhook_1.update(new UpdateWebhookDto('http://squidex.io/hook2', |
|||
[ |
|||
new WebhookSchemaDto('1', true, true, true, true), |
|||
new WebhookSchemaDto('2', true, true, true, true) |
|||
]), modifier, newVersion, modified); |
|||
|
|||
expect(webhook_2.url).toEqual('http://squidex.io/hook2'); |
|||
expect(webhook_2.schemas.length).toEqual(2); |
|||
expect(webhook_2.lastModified).toEqual(modified); |
|||
expect(webhook_2.lastModifiedBy).toEqual(modifier); |
|||
expect(webhook_2.version).toEqual(newVersion); |
|||
}); |
|||
}); |
|||
|
|||
describe('WebhooksService', () => { |
|||
const now = DateTime.now(); |
|||
const user = 'me'; |
|||
const version = new Version('1'); |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [ |
|||
HttpClientTestingModule |
|||
], |
|||
providers: [ |
|||
WebhooksService, |
|||
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, |
|||
{ provide: AnalyticsService, useValue: new AnalyticsService() } |
|||
] |
|||
}); |
|||
}); |
|||
|
|||
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { |
|||
httpMock.verify(); |
|||
})); |
|||
|
|||
it('should make get request to get app webhooks', |
|||
inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { |
|||
|
|||
let webhooks: WebhookDto[] | null = null; |
|||
|
|||
webhooksService.getWebhooks('my-app').subscribe(result => { |
|||
webhooks = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks'); |
|||
|
|||
expect(req.request.method).toEqual('GET'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush([ |
|||
{ |
|||
id: 'id1', |
|||
sharedSecret: 'token1', |
|||
created: '2016-12-12T10:10', |
|||
createdBy: 'CreatedBy1', |
|||
lastModified: '2017-12-12T10:10', |
|||
lastModifiedBy: 'LastModifiedBy1', |
|||
url: 'http://squidex.io/hook', |
|||
version: '1', |
|||
totalSucceeded: 1, |
|||
totalFailed: 2, |
|||
totalTimedout: 3, |
|||
averageRequestTimeMs: 4, |
|||
schemas: [{ |
|||
schemaId: '1', |
|||
sendCreate: true, |
|||
sendUpdate: true, |
|||
sendDelete: true, |
|||
sendPublish: true |
|||
}, { |
|||
schemaId: '2', |
|||
sendCreate: true, |
|||
sendUpdate: true, |
|||
sendDelete: true, |
|||
sendPublish: true |
|||
}] |
|||
} |
|||
]); |
|||
|
|||
expect(webhooks).toEqual([ |
|||
new WebhookDto('id1', 'token1', 'CreatedBy1', 'LastModifiedBy1', |
|||
DateTime.parseISO_UTC('2016-12-12T10:10'), |
|||
DateTime.parseISO_UTC('2017-12-12T10:10'), |
|||
version, |
|||
[ |
|||
new WebhookSchemaDto('1', true, true, true, true), |
|||
new WebhookSchemaDto('2', true, true, true, true) |
|||
], |
|||
'http://squidex.io/hook', 1, 2, 3, 4) |
|||
]); |
|||
})); |
|||
|
|||
it('should make post request to create webhook', |
|||
inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { |
|||
|
|||
const dto = new CreateWebhookDto('http://squidex.io/hook', []); |
|||
|
|||
let webhook: WebhookDto | null = null; |
|||
|
|||
webhooksService.postWebhook('my-app', dto, user, now).subscribe(result => { |
|||
webhook = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks'); |
|||
|
|||
expect(req.request.method).toEqual('POST'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush({ id: 'id1', sharedSecret: 'token1', schemaId: 'schema1' }, { |
|||
headers: { |
|||
etag: '1' |
|||
} |
|||
}); |
|||
|
|||
expect(webhook).toEqual( |
|||
new WebhookDto('id1', 'token1', user, user, now, now, version, [], 'http://squidex.io/hook', 0, 0, 0, 0)); |
|||
})); |
|||
|
|||
it('should make put request to update webhook', |
|||
inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { |
|||
|
|||
const dto = new UpdateWebhookDto('http://squidex.io/hook', []); |
|||
|
|||
webhooksService.putWebhook('my-app', '123', dto, version).subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/123'); |
|||
|
|||
expect(req.request.method).toEqual('PUT'); |
|||
expect(req.request.headers.get('If-Match')).toEqual(version.value); |
|||
|
|||
req.flush({}); |
|||
})); |
|||
|
|||
it('should make delete request to delete webhook', |
|||
inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { |
|||
|
|||
webhooksService.deleteWebhook('my-app', '123', version).subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/123'); |
|||
|
|||
expect(req.request.method).toEqual('DELETE'); |
|||
expect(req.request.headers.get('If-Match')).toEqual(version.value); |
|||
})); |
|||
|
|||
it('should make get request to get app webhook events', |
|||
inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { |
|||
|
|||
let webhooks: WebhookEventsDto | null = null; |
|||
|
|||
webhooksService.getEvents('my-app', 10, 20).subscribe(result => { |
|||
webhooks = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/events?take=10&skip=20'); |
|||
|
|||
expect(req.request.method).toEqual('GET'); |
|||
|
|||
req.flush({ |
|||
total: 20, |
|||
items: [ |
|||
{ |
|||
id: 'id1', |
|||
created: '2017-12-12T10:10', |
|||
eventName: 'event1', |
|||
nextAttempt: '2017-12-12T12:10', |
|||
jobResult: 'Failed', |
|||
lastDump: 'dump1', |
|||
numCalls: 1, |
|||
requestUrl: 'url1', |
|||
result: 'Failed' |
|||
}, |
|||
{ |
|||
id: 'id2', |
|||
created: '2017-12-13T10:10', |
|||
eventName: 'event2', |
|||
nextAttempt: '2017-12-13T12:10', |
|||
jobResult: 'Failed', |
|||
lastDump: 'dump2', |
|||
numCalls: 2, |
|||
requestUrl: 'url2', |
|||
result: 'Failed' |
|||
} |
|||
] |
|||
}); |
|||
|
|||
expect(webhooks).toEqual( |
|||
new WebhookEventsDto(20, [ |
|||
new WebhookEventDto('id1', |
|||
DateTime.parseISO_UTC('2017-12-12T10:10'), |
|||
DateTime.parseISO_UTC('2017-12-12T12:10'), |
|||
'event1', 'url1', 'dump1', 'Failed', 'Failed', 1), |
|||
new WebhookEventDto('id2', |
|||
DateTime.parseISO_UTC('2017-12-13T10:10'), |
|||
DateTime.parseISO_UTC('2017-12-13T12:10'), |
|||
'event2', 'url2', 'dump2', 'Failed', 'Failed', 2) |
|||
])); |
|||
})); |
|||
|
|||
it('should make put request to enqueue webhook event', |
|||
inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { |
|||
|
|||
webhooksService.enqueueEvent('my-app', '123').subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/events/123'); |
|||
|
|||
expect(req.request.method).toEqual('PUT'); |
|||
})); |
|||
}); |
|||
@ -1,230 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { Injectable } from '@angular/core'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
import 'framework/angular/http-extensions'; |
|||
|
|||
import { |
|||
AnalyticsService, |
|||
ApiUrlConfig, |
|||
DateTime, |
|||
HTTP, |
|||
Version, |
|||
Versioned |
|||
} from 'framework'; |
|||
|
|||
export class WebhookDto { |
|||
constructor( |
|||
public readonly id: string, |
|||
public readonly sharedSecret: string, |
|||
public readonly createdBy: string, |
|||
public readonly lastModifiedBy: string, |
|||
public readonly created: DateTime, |
|||
public readonly lastModified: DateTime, |
|||
public readonly version: Version, |
|||
public readonly schemas: WebhookSchemaDto[], |
|||
public readonly url: string, |
|||
public readonly totalSucceeded: number, |
|||
public readonly totalFailed: number, |
|||
public readonly totalTimedout: number, |
|||
public readonly averageRequestTimeMs: number |
|||
) { |
|||
} |
|||
|
|||
public update(update: UpdateWebhookDto, user: string, version: Version, now?: DateTime): WebhookDto { |
|||
return new WebhookDto( |
|||
this.id, |
|||
this.sharedSecret, |
|||
this.createdBy, user, |
|||
this.created, now || DateTime.now(), |
|||
version, |
|||
update.schemas, |
|||
update.url, |
|||
this.totalSucceeded, |
|||
this.totalFailed, |
|||
this.totalTimedout, |
|||
this.averageRequestTimeMs); |
|||
} |
|||
} |
|||
|
|||
export class WebhookSchemaDto { |
|||
constructor( |
|||
public readonly schemaId: string, |
|||
public readonly sendCreate: boolean, |
|||
public readonly sendUpdate: boolean, |
|||
public readonly sendDelete: boolean, |
|||
public readonly sendPublish: boolean |
|||
) { |
|||
} |
|||
} |
|||
|
|||
export class WebhookEventDto { |
|||
constructor( |
|||
public readonly id: string, |
|||
public readonly created: DateTime, |
|||
public readonly nextAttempt: DateTime | null, |
|||
public readonly eventName: string, |
|||
public readonly requestUrl: string, |
|||
public readonly lastDump: string, |
|||
public readonly result: string, |
|||
public readonly jobResult: string, |
|||
public readonly numCalls: number |
|||
) { |
|||
} |
|||
} |
|||
|
|||
export class WebhookEventsDto { |
|||
constructor( |
|||
public readonly total: number, |
|||
public readonly items: WebhookEventDto[] |
|||
) { |
|||
} |
|||
} |
|||
|
|||
export class CreateWebhookDto { |
|||
constructor( |
|||
public readonly url: string, |
|||
public readonly schemas: WebhookSchemaDto[] |
|||
) { |
|||
} |
|||
} |
|||
|
|||
export class UpdateWebhookDto { |
|||
constructor( |
|||
public readonly url: string, |
|||
public readonly schemas: WebhookSchemaDto[] |
|||
) { |
|||
} |
|||
} |
|||
|
|||
@Injectable() |
|||
export class WebhooksService { |
|||
constructor( |
|||
private readonly http: HttpClient, |
|||
private readonly apiUrl: ApiUrlConfig, |
|||
private readonly analytics: AnalyticsService |
|||
) { |
|||
} |
|||
|
|||
public getWebhooks(appName: string): Observable<WebhookDto[]> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); |
|||
|
|||
return HTTP.getVersioned<any>(this.http, url) |
|||
.map(response => { |
|||
const items: any[] = response.payload.body; |
|||
|
|||
return items.map(item => { |
|||
const schemas = item.schemas.map((schema: any) => |
|||
new WebhookSchemaDto( |
|||
schema.schemaId, |
|||
schema.sendCreate, |
|||
schema.sendUpdate, |
|||
schema.sendDelete, |
|||
schema.sendPublish)); |
|||
|
|||
return new WebhookDto( |
|||
item.id, |
|||
item.sharedSecret, |
|||
item.createdBy, |
|||
item.lastModifiedBy, |
|||
DateTime.parseISO_UTC(item.created), |
|||
DateTime.parseISO_UTC(item.lastModified), |
|||
new Version(item.version.toString()), |
|||
schemas, |
|||
item.url, |
|||
item.totalSucceeded, |
|||
item.totalFailed, |
|||
item.totalTimedout, |
|||
item.averageRequestTimeMs); |
|||
}); |
|||
}) |
|||
.pretifyError('Failed to load webhooks. Please reload.'); |
|||
} |
|||
|
|||
public postWebhook(appName: string, dto: CreateWebhookDto, user: string, now: DateTime): Observable<WebhookDto> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); |
|||
|
|||
return HTTP.postVersioned<any>(this.http, url, dto) |
|||
.map(response => { |
|||
const body = response.payload.body; |
|||
|
|||
return new WebhookDto( |
|||
body.id, |
|||
body.sharedSecret, |
|||
user, |
|||
user, |
|||
now, |
|||
now, |
|||
response.version, |
|||
dto.schemas, |
|||
dto.url, |
|||
0, 0, 0, 0); |
|||
}) |
|||
.do(() => { |
|||
this.analytics.trackEvent('Webhook', 'Created', appName); |
|||
}) |
|||
.pretifyError('Failed to create webhook. Please reload.'); |
|||
} |
|||
|
|||
public putWebhook(appName: string, id: string, dto: UpdateWebhookDto, version: Version): Observable<Versioned<any>> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`); |
|||
|
|||
return HTTP.putVersioned(this.http, url, dto, version) |
|||
.do(() => { |
|||
this.analytics.trackEvent('Webhook', 'Updated', appName); |
|||
}) |
|||
.pretifyError('Failed to update webhook. Please reload.'); |
|||
} |
|||
|
|||
public deleteWebhook(appName: string, id: string, version: Version): Observable<any> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`); |
|||
|
|||
return HTTP.deleteVersioned(this.http, url, version) |
|||
.do(() => { |
|||
this.analytics.trackEvent('Webhook', 'Deleted', appName); |
|||
}) |
|||
.pretifyError('Failed to delete webhook. Please reload.'); |
|||
} |
|||
|
|||
public getEvents(appName: string, take: number, skip: number): Observable<WebhookEventsDto> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/events?take=${take}&skip=${skip}`); |
|||
|
|||
return HTTP.getVersioned<any>(this.http, url) |
|||
.map(response => { |
|||
const body = response.payload.body; |
|||
|
|||
const items: any[] = body.items; |
|||
|
|||
return new WebhookEventsDto(body.total, items.map(item => { |
|||
return new WebhookEventDto( |
|||
item.id, |
|||
DateTime.parseISO_UTC(item.created), |
|||
item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, |
|||
item.eventName, |
|||
item.requestUrl, |
|||
item.lastDump, |
|||
item.result, |
|||
item.jobResult, |
|||
item.numCalls); |
|||
})); |
|||
}) |
|||
.pretifyError('Failed to load events. Please reload.'); |
|||
} |
|||
|
|||
public enqueueEvent(appName: string, id: string): Observable<any> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/events/${id}`); |
|||
|
|||
return HTTP.putVersioned(this.http, url, {}) |
|||
.do(() => { |
|||
this.analytics.trackEvent('Webhook', 'EventEnqueued', appName); |
|||
}) |
|||
.pretifyError('Failed to enqueue webhook event. Please reload.'); |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
@import '_mixins'; |
|||
@import '_vars'; |
|||
|
|||
$trigger-content-changed-text: #3389ff; |
|||
$trigger-content-changed-icon: #297ff6; |
|||
|
|||
$action-webhook-text: #4bb958; |
|||
$action-webhook-icon: #47b353; |
|||
|
|||
@mixin build-element($text-color, $icon-color) { |
|||
& { |
|||
background: $text-color; |
|||
} |
|||
|
|||
&:hover { |
|||
background: $icon-color; |
|||
} |
|||
|
|||
.rule-element-icon { |
|||
background: $icon-color; |
|||
} |
|||
} |
|||
|
|||
.rule-element { |
|||
& { |
|||
@include truncate; |
|||
@include transition(background-color .4s ease); |
|||
color: $color-dark-foreground; |
|||
cursor: pointer; |
|||
display: inline-block; |
|||
line-height: 2.8rem; |
|||
font-size: 1rem; |
|||
font-weight: normal; |
|||
padding-right: .8rem; |
|||
width: 15rem; |
|||
} |
|||
|
|||
&-icon { |
|||
height: 3rem; |
|||
display: inline-block; |
|||
line-height: 3rem; |
|||
font-size: 1.2rem; |
|||
font-weight: normal; |
|||
margin: 0; |
|||
margin-right: .5rem; |
|||
padding: 0 .8rem; |
|||
} |
|||
} |
|||
|
|||
.rule-element-ContentChanged { |
|||
@include build-element($trigger-content-changed-text, $trigger-content-changed-icon) |
|||
} |
|||
|
|||
.rule-element-Webhook { |
|||
@include build-element($action-webhook-text, $action-webhook-icon) |
|||
} |
|||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue