mirror of https://github.com/Squidex/squidex.git
34 changed files with 700 additions and 456 deletions
@ -0,0 +1,8 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
export * from './pages/webhooks-page.component'; |
||||
@ -0,0 +1,9 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
export * from './declarations'; |
||||
|
export * from './module'; |
||||
@ -0,0 +1,33 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { RouterModule, Routes } from '@angular/router'; |
||||
|
|
||||
|
import { SqxFrameworkModule } from 'shared'; |
||||
|
|
||||
|
import { |
||||
|
WebhooksPageComponent |
||||
|
} from './declarations'; |
||||
|
|
||||
|
const routes: Routes = [ |
||||
|
{ |
||||
|
path: '', |
||||
|
component: WebhooksPageComponent |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
SqxFrameworkModule, |
||||
|
RouterModule.forChild(routes) |
||||
|
], |
||||
|
declarations: [ |
||||
|
WebhooksPageComponent |
||||
|
] |
||||
|
}) |
||||
|
export class SqxFeatureWebhooksModule { } |
||||
@ -0,0 +1,98 @@ |
|||||
|
<sqx-title message="{app} | Webhooks" parameter1="app" value1="{{appName() | async}}"></sqx-title> |
||||
|
|
||||
|
<sqx-panel panelWidth="46rem"> |
||||
|
<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"> |
||||
|
<div *ngFor="let webhook of webhooks"> |
||||
|
<div class="table-items-row"> |
||||
|
<table class="table table-middle table-sm table-borderless table-fixed"> |
||||
|
<colgroup> |
||||
|
<col style="width: 160px; text-align: right;" /> |
||||
|
<col style="width: 100%" /> |
||||
|
<col style="width: 40px" /> |
||||
|
</colgroup> |
||||
|
|
||||
|
<tr> |
||||
|
<td colspan="2"> |
||||
|
<h3 class="client-name"> |
||||
|
Schema: {{webhook.schema.name}} |
||||
|
</h3> |
||||
|
</td> |
||||
|
<td class="client-delete"> |
||||
|
<button type="button" class="btn btn-link btn-danger" (click)="deleteWebhook(webhook)"> |
||||
|
<i class="icon-bin2"></i> |
||||
|
</button> |
||||
|
</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td>Url:</td> |
||||
|
<td> |
||||
|
<input readonly class="form-control" [attr.value]="webhook.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>Token:</td> |
||||
|
<td> |
||||
|
<input readonly class="form-control" [attr.value]="webhook.webhook.securityToken" #inputToken /> |
||||
|
</td> |
||||
|
<td> |
||||
|
<button type="button" class="btn btn-primary btn-link" [sqxCopy]="inputToken"> |
||||
|
<i class="icon-copy"></i> |
||||
|
</button> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="table-items-footer"> |
||||
|
<form class="form-inline" [formGroup]="addWebhookForm" (ngSubmit)="addWebhook()"> |
||||
|
<div class="form-group mr-1"> |
||||
|
<select class="form-control schemas-control" formControlName="schemaId"> |
||||
|
<option *ngFor="let schema of schemas" [ngValue]="schema.id">{{schema.name}}</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group mr-1"> |
||||
|
<sqx-control-errors for="url" [submitted]="addWebhookFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control" formControlName="url" placeholder="Enter webhook url" autocomplete="off" /> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
<button type="submit" class="btn btn-success" [disabled]="!hasUrl">Add Webhook</button> |
||||
|
<button type="reset" class="btn btn-link" (click)="resetWebhookForm()" [disabled]="addWebhookFormSubmitted">Cancel</button> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</sqx-panel> |
||||
@ -0,0 +1,6 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.schemas-control { |
||||
|
width: 10rem; |
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
|
||||
|
import { |
||||
|
AppComponentBase, |
||||
|
AppsStoreService, |
||||
|
CreateWebhookDto, |
||||
|
ImmutableArray, |
||||
|
NotificationService, |
||||
|
SchemaDto, |
||||
|
SchemasService, |
||||
|
Version, |
||||
|
WebhookDto, |
||||
|
WebhooksService |
||||
|
} from 'shared'; |
||||
|
|
||||
|
interface WebhookWithSchema { webhook: WebhookDto; schema: SchemaDto; }; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-webhooks-page', |
||||
|
styleUrls: ['./webhooks-page.component.scss'], |
||||
|
templateUrl: './webhooks-page.component.html' |
||||
|
}) |
||||
|
export class WebhooksPageComponent extends AppComponentBase implements OnInit { |
||||
|
private readonly version = new Version(); |
||||
|
|
||||
|
public webhooks: ImmutableArray<WebhookWithSchema>; |
||||
|
|
||||
|
public schemas: SchemaDto[]; |
||||
|
|
||||
|
public addWebhookFormSubmitted = false; |
||||
|
public addWebhookForm: FormGroup = |
||||
|
this.formBuilder.group({ |
||||
|
schemaId: ['', |
||||
|
[ |
||||
|
Validators.required |
||||
|
]], |
||||
|
url: ['', |
||||
|
[ |
||||
|
Validators.required |
||||
|
]] |
||||
|
}); |
||||
|
|
||||
|
public get hasUrl() { |
||||
|
return this.addWebhookForm.get('url').value && this.addWebhookForm.get('url').value.length > 0; |
||||
|
} |
||||
|
|
||||
|
constructor(apps: AppsStoreService, notifications: NotificationService, |
||||
|
private readonly schemasService: SchemasService, |
||||
|
private readonly webhooksService: WebhooksService, |
||||
|
private readonly formBuilder: FormBuilder |
||||
|
) { |
||||
|
super(notifications, apps); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.load(); |
||||
|
} |
||||
|
|
||||
|
public load(showInfo = false) { |
||||
|
this.appNameOnce() |
||||
|
.switchMap(app => |
||||
|
this.schemasService.getSchemas(app) |
||||
|
.withLatestFrom(this.webhooksService.getWebhooks(app), |
||||
|
(s, w) => { return { webhooks: w, schemas: s }; })) |
||||
|
.subscribe(dtos => { |
||||
|
this.schemas = dtos.schemas; |
||||
|
|
||||
|
this.webhooks = |
||||
|
ImmutableArray.of( |
||||
|
dtos.webhooks.map(w => { |
||||
|
return { webhook: w, schema: dtos.schemas.find(s => s.id === w.schemaId) }; |
||||
|
}).filter(w => !!w.schema)); |
||||
|
|
||||
|
if (showInfo) { |
||||
|
this.notifyInfo('Webhooks reloaded.'); |
||||
|
} |
||||
|
}, error => { |
||||
|
this.notifyError(error); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public resetWebhookForm() { |
||||
|
this.addWebhookFormSubmitted = false; |
||||
|
this.addWebhookForm.enable(); |
||||
|
this.addWebhookForm.reset(); |
||||
|
} |
||||
|
|
||||
|
public addWebhook() { |
||||
|
if (this.addWebhookForm.valid) { |
||||
|
this.addWebhookFormSubmitted = true; |
||||
|
this.addWebhookForm.disable(); |
||||
|
|
||||
|
const requestDto = new CreateWebhookDto(this.addWebhookForm.get('url')!.value); |
||||
|
const schemaId = this.addWebhookForm.get('schemaId')!.value; |
||||
|
const schema = this.schemas.find(s => s.id === schemaId); |
||||
|
|
||||
|
this.appNameOnce() |
||||
|
.switchMap(app => this.webhooksService.postWebhook(app, schema.name, requestDto, this.version)) |
||||
|
.subscribe(dto => { |
||||
|
this.webhooks = this.webhooks.push({ schema, webhook: dto }); |
||||
|
this.resetWebhookForm(); |
||||
|
}, error => { |
||||
|
this.notifyError(error); |
||||
|
this.resetWebhookForm(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public deleteWebhook(webhook: WebhookWithSchema) { |
||||
|
this.appNameOnce() |
||||
|
.switchMap(app => this.webhooksService.deleteWebhook(app, webhook.schema.name, webhook.webhook.id, this.version)) |
||||
|
.subscribe(dto => { |
||||
|
this.webhooks = this.webhooks.remove(webhook); |
||||
|
}, error => { |
||||
|
this.notifyError(error); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,108 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Response, ResponseOptions } from '@angular/http'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { IMock, Mock, Times } from 'typemoq'; |
||||
|
|
||||
|
import { |
||||
|
ApiUrlConfig, |
||||
|
AuthService, |
||||
|
CreateWebhookDto, |
||||
|
Version, |
||||
|
WebhookDto, |
||||
|
WebhooksService |
||||
|
} from './../'; |
||||
|
|
||||
|
describe('WebhooksService', () => { |
||||
|
let authService: IMock<AuthService>; |
||||
|
let webhooksService: WebhooksService; |
||||
|
let version = new Version('1'); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
authService = Mock.ofType(AuthService); |
||||
|
webhooksService = new WebhooksService(authService.object, new ApiUrlConfig('http://service/p/')); |
||||
|
}); |
||||
|
|
||||
|
it('should make get request to get app webhooks', () => { |
||||
|
authService.setup(x => x.authGet('http://service/p/api/apps/my-app/webhooks', version)) |
||||
|
.returns(() => Observable.of( |
||||
|
new Response( |
||||
|
new ResponseOptions({ |
||||
|
body: [{ |
||||
|
id: 'id1', |
||||
|
schemaId: 'schemaId1', |
||||
|
securityToken: 'token1', |
||||
|
url: 'http://squidex.io/1' |
||||
|
}, { |
||||
|
id: 'id2', |
||||
|
schemaId: 'schemaId2', |
||||
|
securityToken: 'token2', |
||||
|
url: 'http://squidex.io/2' |
||||
|
}] |
||||
|
}) |
||||
|
) |
||||
|
)) |
||||
|
.verifiable(Times.once()); |
||||
|
|
||||
|
let webhooks: WebhookDto[] | null = null; |
||||
|
|
||||
|
webhooksService.getWebhooks('my-app', version).subscribe(result => { |
||||
|
webhooks = result; |
||||
|
}).unsubscribe(); |
||||
|
|
||||
|
expect(webhooks).toEqual([ |
||||
|
new WebhookDto('id1', 'schemaId1', 'token1', 'http://squidex.io/1'), |
||||
|
new WebhookDto('id2', 'schemaId2', 'token2', 'http://squidex.io/2') |
||||
|
]); |
||||
|
|
||||
|
authService.verifyAll(); |
||||
|
}); |
||||
|
|
||||
|
it('should make post request to create webhook', () => { |
||||
|
const dto = new CreateWebhookDto('http://squidex.io/hook'); |
||||
|
|
||||
|
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas/my-schema/webhooks', dto, version)) |
||||
|
.returns(() => Observable.of( |
||||
|
new Response( |
||||
|
new ResponseOptions({ |
||||
|
body: { |
||||
|
id: 'id1', |
||||
|
schemaId: 'schemaId1', |
||||
|
securityToken: 'token1', |
||||
|
url: 'http://squidex.io/1' |
||||
|
} |
||||
|
}) |
||||
|
) |
||||
|
)) |
||||
|
.verifiable(Times.once()); |
||||
|
|
||||
|
let webhook: WebhookDto | null = null; |
||||
|
|
||||
|
webhooksService.postWebhook('my-app', 'my-schema', dto, version).subscribe(result => { |
||||
|
webhook = result; |
||||
|
}).unsubscribe(); |
||||
|
|
||||
|
expect(webhook).toEqual(new WebhookDto('id1', 'schemaId1', 'token1', 'http://squidex.io/1')); |
||||
|
|
||||
|
authService.verifyAll(); |
||||
|
}); |
||||
|
|
||||
|
it('should make delete request to delete webhook', () => { |
||||
|
authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/schemas/my-schema/webhooks/123', version)) |
||||
|
.returns(() => Observable.of( |
||||
|
new Response( |
||||
|
new ResponseOptions() |
||||
|
) |
||||
|
)) |
||||
|
.verifiable(Times.once()); |
||||
|
|
||||
|
webhooksService.deleteWebhook('my-app', 'my-schema', '123', version); |
||||
|
|
||||
|
authService.verifyAll(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,81 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
|
||||
|
import 'framework/angular/http-extensions'; |
||||
|
|
||||
|
import { ApiUrlConfig, Version } from 'framework'; |
||||
|
import { AuthService } from './auth.service'; |
||||
|
|
||||
|
export class WebhookDto { |
||||
|
constructor( |
||||
|
public readonly id: string, |
||||
|
public readonly schemaId: string, |
||||
|
public readonly securityToken: string, |
||||
|
public readonly url: string |
||||
|
) { |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class CreateWebhookDto { |
||||
|
constructor( |
||||
|
public readonly url: string |
||||
|
) { |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Injectable() |
||||
|
export class WebhooksService { |
||||
|
constructor( |
||||
|
private readonly authService: AuthService, |
||||
|
private readonly apiUrl: ApiUrlConfig |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public getWebhooks(appName: string, version?: Version): Observable<WebhookDto[]> { |
||||
|
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); |
||||
|
|
||||
|
return this.authService.authGet(url, version) |
||||
|
.map(response => response.json()) |
||||
|
.map(response => { |
||||
|
const items: any[] = response; |
||||
|
|
||||
|
return items.map(item => { |
||||
|
return new WebhookDto( |
||||
|
item.id, |
||||
|
item.schemaId, |
||||
|
item.securityToken, |
||||
|
item.url); |
||||
|
}); |
||||
|
}) |
||||
|
.catchError('Failed to load webhooks. Please reload.'); |
||||
|
} |
||||
|
|
||||
|
public postWebhook(appName: string, schemaName: string, dto: CreateWebhookDto, version?: Version): Observable<WebhookDto> { |
||||
|
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/webhooks`); |
||||
|
|
||||
|
return this.authService.authPost(url, dto, version) |
||||
|
.map(response => response.json()) |
||||
|
.map(response => { |
||||
|
return new WebhookDto( |
||||
|
response.id, |
||||
|
response.schemaId, |
||||
|
response.securityToken, |
||||
|
response.url); |
||||
|
}) |
||||
|
.catchError('Failed to create webhook. Please reload.'); |
||||
|
} |
||||
|
|
||||
|
public deleteWebhook(appName: string, schemaName: string, id: string, version?: Version): Observable<any> { |
||||
|
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/webhooks/${id}`); |
||||
|
|
||||
|
return this.authService.authDelete(url, version) |
||||
|
.catchError('Failed to delete webhook. Please reload.'); |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 57 KiB |
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue