Browse Source

Some progress with the UI.

pull/98/head
Sebastian Stehle 9 years ago
parent
commit
de83838fc2
  1. 1
      src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs
  2. 2
      src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs
  3. 2
      src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs
  4. 5
      src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs
  5. 9
      src/Squidex/Config/Domain/WriteModule.cs
  6. 8
      src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs
  7. 1
      src/Squidex/app/features/webhooks/declarations.ts
  8. 2
      src/Squidex/app/features/webhooks/module.ts
  9. 86
      src/Squidex/app/features/webhooks/pages/webhooks-page.component.html
  10. 11
      src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss
  11. 36
      src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts
  12. 3
      src/Squidex/app/shared/services/webhooks.service.spec.ts
  13. 85
      src/Squidex/app/shared/services/webhooks.service.ts
  14. 3
      src/Squidex/app/theme/_bootstrap.scss

1
src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs

@ -13,5 +13,6 @@ namespace Squidex.Domain.Apps.Events.Webhooks
[TypeName("WebhookCreatedEvent")] [TypeName("WebhookCreatedEvent")]
public sealed class WebhookCreated : WebhookEditEvent public sealed class WebhookCreated : WebhookEditEvent
{ {
public string SharedSecret { get; set; }
} }
} }

2
src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs

@ -10,7 +10,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Webhooks namespace Squidex.Domain.Apps.Events.Webhooks
{ {
[TypeName("WebhookDeletedEvent")] [TypeName("WebhookDeletedEventV2")]
public sealed class WebhookDeleted : WebhookEvent public sealed class WebhookDeleted : WebhookEvent
{ {
} }

2
src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs

@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Write.Webhooks
{ {
await ValidateAsync(command, () => "Failed to create webhook"); await ValidateAsync(command, () => "Failed to create webhook");
await handler.UpdateAsync<WebhookDomainObject>(context, c => c.Create(command)); await handler.CreateAsync<WebhookDomainObject>(context, c => c.Create(command));
} }
protected async Task On(UpdateWebhook command, CommandContext context) protected async Task On(UpdateWebhook command, CommandContext context)

5
src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs

@ -27,6 +27,11 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
var factoryFunction = (DomainObjectFactoryFunction<T>)serviceProvider.GetService(typeof(DomainObjectFactoryFunction<T>)); var factoryFunction = (DomainObjectFactoryFunction<T>)serviceProvider.GetService(typeof(DomainObjectFactoryFunction<T>));
if (factoryFunction == null)
{
throw new InvalidOperationException($"No factory registered for {typeof(T)}");
}
var domainObject = factoryFunction.Invoke(id); var domainObject = factoryFunction.Invoke(id);
if (domainObject.Version != -1) if (domainObject.Version != -1)

9
src/Squidex/Config/Domain/WriteModule.cs

@ -13,6 +13,7 @@ using Squidex.Domain.Apps.Write.Apps;
using Squidex.Domain.Apps.Write.Assets; using Squidex.Domain.Apps.Write.Assets;
using Squidex.Domain.Apps.Write.Contents; using Squidex.Domain.Apps.Write.Contents;
using Squidex.Domain.Apps.Write.Schemas; using Squidex.Domain.Apps.Write.Schemas;
using Squidex.Domain.Apps.Write.Webhooks;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Pipeline.CommandHandlers; using Squidex.Pipeline.CommandHandlers;
@ -71,6 +72,10 @@ namespace Squidex.Config.Domain
.As<ICommandMiddleware>() .As<ICommandMiddleware>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<WebhookCommandMiddleware>()
.As<ICommandMiddleware>()
.SingleInstance();
builder.RegisterType<ETagCommandMiddleware>() builder.RegisterType<ETagCommandMiddleware>()
.As<ICommandMiddleware>() .As<ICommandMiddleware>()
.SingleInstance(); .SingleInstance();
@ -87,6 +92,10 @@ namespace Squidex.Config.Domain
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();
builder.Register<DomainObjectFactoryFunction<WebhookDomainObject>>(c => (id => new WebhookDomainObject(id, -1)))
.AsSelf()
.SingleInstance();
builder.Register<DomainObjectFactoryFunction<SchemaDomainObject>>(c => builder.Register<DomainObjectFactoryFunction<SchemaDomainObject>>(c =>
{ {
var fieldRegistry = c.Resolve<FieldRegistry>(); var fieldRegistry = c.Resolve<FieldRegistry>();

8
src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs

@ -99,7 +99,7 @@ namespace Squidex.Controllers.Api.Webhooks
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<Guid>>(); var result = context.Result<EntityCreatedResult<Guid>>();
var response = new EntityCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version }; var response = new WebhookCreatedDto { Id = result.IdOrValue, SharedSecret = command.SharedSecret, Version = result.Version };
return CreatedAtAction(nameof(GetWebhooks), new { app }, response); return CreatedAtAction(nameof(GetWebhooks), new { app }, response);
} }
@ -113,7 +113,7 @@ namespace Squidex.Controllers.Api.Webhooks
/// <returns> /// <returns>
/// 203 => Webhook updated. /// 203 => Webhook updated.
/// 400 => Webhook is not valid. /// 400 => Webhook is not valid.
/// 404 => App or webhook not found. /// 404 => Webhook or app not found.
/// </returns> /// </returns>
/// <remarks> /// <remarks>
/// All events for the specified schemas will be sent to the url. The timeout is 2 seconds. /// All events for the specified schemas will be sent to the url. The timeout is 2 seconds.
@ -140,10 +140,10 @@ namespace Squidex.Controllers.Api.Webhooks
/// <param name="id">The id of the webhook to delete.</param> /// <param name="id">The id of the webhook to delete.</param>
/// <returns> /// <returns>
/// 204 => Webhook has been deleted. /// 204 => Webhook has been deleted.
/// 404 => Webhook or shema or app not found. /// 404 => Webhook or app not found.
/// </returns> /// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}//webhooks/{id}")] [Route("apps/{app}/webhooks/{id}")]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteWebhook(string app, Guid id) public async Task<IActionResult> DeleteWebhook(string app, Guid id)
{ {

1
src/Squidex/app/features/webhooks/declarations.ts

@ -6,4 +6,5 @@
*/ */
export * from './pages/webhook-events-page.component'; export * from './pages/webhook-events-page.component';
export * from './pages/webhook.component';
export * from './pages/webhooks-page.component'; export * from './pages/webhooks-page.component';

2
src/Squidex/app/features/webhooks/module.ts

@ -15,6 +15,7 @@ import {
} from 'shared'; } from 'shared';
import { import {
WebhookComponent,
WebhookEventsPageComponent, WebhookEventsPageComponent,
WebhooksPageComponent WebhooksPageComponent
} from './declarations'; } from './declarations';
@ -46,6 +47,7 @@ const routes: Routes = [
RouterModule.forChild(routes) RouterModule.forChild(routes)
], ],
declarations: [ declarations: [
WebhookComponent,
WebhookEventsPageComponent, WebhookEventsPageComponent,
WebhooksPageComponent WebhooksPageComponent
] ]

86
src/Squidex/app/features/webhooks/pages/webhooks-page.component.html

@ -26,96 +26,18 @@
</div> </div>
<div *ngIf="webhooks"> <div *ngIf="webhooks">
<div *ngFor="let w of webhooks"> <sqx-webhook *ngFor="let webhook of webhooks" [webhook]="webhook" [allSchemas]="schemas"
<div class="table-items-row"> (updating)="deleteWebhook(webhook)"
<table class="table table-middle table-sm table-borderless table-fixed"> (deleting)="deleteWebhook(webhook)"></sqx-webhook>
<colgroup>
<col style="width: 120px; text-align: right;" />
<col style="width: 100%" />
<col style="width: 40px" />
</colgroup>
<tr>
<td colspan="2">
<h3 class="client-name">
Schema: {{w.schema.name}}
</h3>
</td>
<td class="client-delete">
<button type="button" class="btn btn-link btn-danger"
(sqxConfirmClick)="deleteWebhook(w)"
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 readonly class="form-control" [attr.value]="w.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]="w.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>
<div class="webhook-stats" *ngIf="w.webhook.averageRequestTimeMs > 0">
<div class="row">
<div class="col-3">
<span title="Succeeded Requests" [class.success]="w.webhook.totalSucceeded > 0">
<i class="icon-checkmark"></i> {{w.webhook.totalSucceeded}}
</span>
</div>
<div class="col-3">
<span title="Failed Requests" [class.failed]="w.webhook.totalFailed > 0">
<i class="icon-bug"></i> {{w.webhook.totalFailed}}
</span>
</div>
<div class="col-3">
<span title="Timeout Requests" [class.failed]="w.webhook.totalTimedout > 0">
<i class="icon-timeout"></i> {{w.webhook.totalTimedout}}
</span>
</div>
<div class="col-3">
<span title="Average Response Time">
<i class="icon-elapsed"></i> {{w.webhook.averageRequestTimeMs}} ms
</span>
</div>
</div>
</div>
</div>
</div>
<div class="table-items-footer"> <div class="table-items-footer">
<form class="form-inline" [formGroup]="addWebhookForm" (ngSubmit)="addWebhook()"> <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"> <div class="form-group mr-1">
<sqx-control-errors for="url" [submitted]="addWebhookFormSubmitted"></sqx-control-errors> <sqx-control-errors for="url" [submitted]="addWebhookFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" formControlName="url" placeholder="Enter webhook url" autocomplete="off" /> <input type="text" class="form-control url-control" formControlName="url" placeholder="Enter webhook url" autocomplete="off" />
</div> </div>
<button type="submit" class="btn btn-success" [disabled]="!hasUrl">Add Webhook</button> <button type="submit" class="btn btn-success" [disabled]="!hasUrl">Add Webhook</button>
<button type="reset" class="btn btn-link" (click)="cancelAddWebhook()" [disabled]="addWebhookFormSubmitted">Cancel</button> <button type="reset" class="btn btn-link" (click)="cancelAddWebhook()" [disabled]="addWebhookFormSubmitted">Cancel</button>
</form> </form>

11
src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss

@ -1,8 +1,8 @@
@import '_vars'; @import '_vars';
@import '_mixins'; @import '_mixins';
.schemas-control { .url-control {
width: 10rem; width: 24rem;
} }
.failed { .failed {
@ -11,11 +11,4 @@
.success { .success {
color: $color-theme-green; color: $color-theme-green;
}
.webhook-stats {
border-top: 1px solid $color-border;
margin-left: -1.25rem;
margin-right: -1.25rem;
padding: 1rem 1rem 0;
} }

36
src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts

@ -11,7 +11,9 @@ import { FormBuilder, Validators } from '@angular/forms';
import { import {
AppComponentBase, AppComponentBase,
AppsStoreService, AppsStoreService,
AuthService,
CreateWebhookDto, CreateWebhookDto,
DateTime,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
SchemaDto, SchemaDto,
@ -21,27 +23,18 @@ import {
WebhooksService WebhooksService
} from 'shared'; } from 'shared';
interface WebhookWithSchema { webhook: WebhookDto; schema: SchemaDto; };
@Component({ @Component({
selector: 'sqx-webhooks-page', selector: 'sqx-webhooks-page',
styleUrls: ['./webhooks-page.component.scss'], styleUrls: ['./webhooks-page.component.scss'],
templateUrl: './webhooks-page.component.html' templateUrl: './webhooks-page.component.html'
}) })
export class WebhooksPageComponent extends AppComponentBase implements OnInit { export class WebhooksPageComponent extends AppComponentBase implements OnInit {
private readonly version = new Version(); public webhooks: ImmutableArray<WebhookDto>;
public webhooks: ImmutableArray<WebhookWithSchema>;
public schemas: SchemaDto[]; public schemas: SchemaDto[];
public addWebhookFormSubmitted = false; public addWebhookFormSubmitted = false;
public addWebhookForm = public addWebhookForm =
this.formBuilder.group({ this.formBuilder.group({
schemaId: ['',
[
Validators.required
]],
url: ['', url: ['',
[ [
Validators.required Validators.required
@ -53,6 +46,7 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
} }
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService,
private readonly authService: AuthService,
private readonly schemasService: SchemasService, private readonly schemasService: SchemasService,
private readonly webhooksService: WebhooksService, private readonly webhooksService: WebhooksService,
private readonly formBuilder: FormBuilder private readonly formBuilder: FormBuilder
@ -72,14 +66,7 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
(s, w) => { return { webhooks: w, schemas: s }; })) (s, w) => { return { webhooks: w, schemas: s }; }))
.subscribe(dtos => { .subscribe(dtos => {
this.schemas = dtos.schemas; this.schemas = dtos.schemas;
this.webhooks = ImmutableArray.of(dtos.webhooks);
this.webhooks =
ImmutableArray.of(
dtos.webhooks.map(w => {
return { webhook: w, schema: dtos.schemas.find(s => s.id === w.schemaId), showDetails: false };
}).filter(w => !!w.schema));
this.addWebhookForm.controls['schemaId'].setValue(this.schemas.map(x => x.id)[0]);
if (showInfo) { if (showInfo) {
this.notifyInfo('Webhooks reloaded.'); this.notifyInfo('Webhooks reloaded.');
@ -89,9 +76,9 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
}); });
} }
public deleteWebhook(webhook: WebhookWithSchema) { public deleteWebhook(webhook: WebhookDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.webhooksService.deleteWebhook(app, webhook.schema.name, webhook.webhook.id, this.version)) .switchMap(app => this.webhooksService.deleteWebhook(app, webhook.id, webhook.version))
.subscribe(dto => { .subscribe(dto => {
this.webhooks = this.webhooks.remove(webhook); this.webhooks = this.webhooks.remove(webhook);
}, error => { }, error => {
@ -105,15 +92,14 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
if (this.addWebhookForm.valid) { if (this.addWebhookForm.valid) {
this.addWebhookForm.disable(); this.addWebhookForm.disable();
const requestDto = new CreateWebhookDto(this.addWebhookForm.controls['url'].value); const requestDto = new CreateWebhookDto(this.addWebhookForm.controls['url'].value, []);
const schemaId = this.addWebhookForm.controls['schemaId'].value; const me = this.authService.user!.token;
const schema = this.schemas.find(s => s.id === schemaId);
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.webhooksService.postWebhook(app, schema.name, requestDto, this.version)) .switchMap(app => this.webhooksService.postWebhook(app, requestDto, me, DateTime.now(), new Version()))
.subscribe(dto => { .subscribe(dto => {
this.webhooks = this.webhooks.push({ webhook: dto, schema: schema }); this.webhooks = this.webhooks.push(dto);
this.resetWebhookForm(); this.resetWebhookForm();
}, error => { }, error => {

3
src/Squidex/app/shared/services/webhooks.service.spec.ts

@ -5,6 +5,7 @@
* Copyright (c) Sebastian Stehle. All rights reserved * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
/*
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing'; import { inject, TestBed } from '@angular/core/testing';
@ -165,4 +166,4 @@ describe('WebhooksService', () => {
expect(req.request.method).toEqual('PUT'); expect(req.request.method).toEqual('PUT');
})); }));
}); });*/

85
src/Squidex/app/shared/services/webhooks.service.ts

@ -21,8 +21,13 @@ import {
export class WebhookDto { export class WebhookDto {
constructor( constructor(
public readonly id: string, public readonly id: string,
public readonly schemaId: string,
public readonly sharedSecret: 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 url: string,
public readonly totalSucceeded: number, public readonly totalSucceeded: number,
public readonly totalFailed: number, public readonly totalFailed: number,
@ -30,6 +35,33 @@ export class WebhookDto {
public readonly averageRequestTimeMs: number public readonly averageRequestTimeMs: number
) { ) {
} }
public update(update: UpdateWebhookDto, user: string, now?: DateTime): WebhookDto {
return new WebhookDto(
this.id,
this.sharedSecret,
this.createdBy, user,
this.created, now || DateTime.now(),
this.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,
public readonly sendUnpublish: boolean
) {
}
} }
export class WebhookEventDto { export class WebhookEventDto {
@ -57,7 +89,16 @@ export class WebhookEventsDto {
export class CreateWebhookDto { export class CreateWebhookDto {
constructor( constructor(
public readonly url: string public readonly url: string,
public readonly schemas: WebhookSchemaDto[]
) {
}
}
export class UpdateWebhookDto {
constructor(
public readonly url: string,
public readonly schemas: WebhookSchemaDto[]
) { ) {
} }
} }
@ -78,10 +119,24 @@ export class WebhooksService {
const items: any[] = response; const items: any[] = response;
return items.map(item => { return items.map(item => {
const schemas = item.schemas.map((schema: any) =>
new WebhookSchemaDto(
schema.schemaId,
schema.sendCreate,
schema.sendUpdate,
schema.sendDelete,
schema.sendPublish,
schema.sendUnpublish));
return new WebhookDto( return new WebhookDto(
item.id, item.id,
item.schemaId,
item.sharedSecret, 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.url,
item.totalSucceeded, item.totalSucceeded,
item.totalFailed, item.totalFailed,
@ -92,23 +147,35 @@ export class WebhooksService {
.pretifyError('Failed to load webhooks. Please reload.'); .pretifyError('Failed to load webhooks. Please reload.');
} }
public postWebhook(appName: string, schemaName: string, dto: CreateWebhookDto, version?: Version): Observable<WebhookDto> { public postWebhook(appName: string, dto: CreateWebhookDto, user: string, now?: DateTime, version?: Version): Observable<WebhookDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/webhooks`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`);
return HTTP.postVersioned(this.http, url, dto, version) return HTTP.postVersioned(this.http, url, dto)
.map(response => { .map(response => {
return new WebhookDto( return new WebhookDto(
response.id, response.id,
response.schemaId,
response.sharedSecret, response.sharedSecret,
user,
user,
now,
now,
version,
dto.schemas,
dto.url, dto.url,
0, 0, 0, 0); 0, 0, 0, 0);
}) })
.pretifyError('Failed to create webhook. Please reload.'); .pretifyError('Failed to create webhook. Please reload.');
} }
public deleteWebhook(appName: string, schemaName: string, id: string, version?: Version): Observable<any> { public putWebhook(appName: string, id: string, dto: UpdateWebhookDto, version?: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/webhooks/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`);
return HTTP.putVersioned(this.http, url, dto, version)
.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) return HTTP.deleteVersioned(this.http, url, version)
.pretifyError('Failed to delete webhook. Please reload.'); .pretifyError('Failed to delete webhook. Please reload.');

3
src/Squidex/app/theme/_bootstrap.scss

@ -410,7 +410,8 @@ a {
// Remove all borders. // Remove all borders.
&-borderless { &-borderless {
td { td,
th {
border: 0; border: 0;
} }
} }

Loading…
Cancel
Save