Browse Source

Started to improve webhooks.

pull/95/head
Sebastian Stehle 8 years ago
parent
commit
002a4f13b6
  1. 10
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaWebhookEntity.cs
  2. 11
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaWebhookRepository.cs
  3. 79
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoWebhookEventEntity.cs
  4. 109
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoWebhookEventRepository.cs
  5. 9
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs
  6. 3
      src/Squidex.Domain.Apps.Read/Schemas/ISchemaWebhookEntity.cs
  7. 27
      src/Squidex.Domain.Apps.Read/Schemas/IWebhookEventEntity.cs
  8. 2
      src/Squidex.Domain.Apps.Read/Schemas/Repositories/ISchemaWebhookRepository.cs
  9. 33
      src/Squidex.Domain.Apps.Read/Schemas/Repositories/IWebhookEventRepository.cs
  10. 159
      src/Squidex.Domain.Apps.Read/Schemas/WebhookDequeuer.cs
  11. 126
      src/Squidex.Domain.Apps.Read/Schemas/WebhookEnqueuer.cs
  12. 237
      src/Squidex.Domain.Apps.Read/Schemas/WebhookInvoker.cs
  13. 32
      src/Squidex.Domain.Apps.Read/Schemas/WebhookJob.cs
  14. 18
      src/Squidex.Domain.Apps.Read/Schemas/WebhookJobResult.cs
  15. 3
      src/Squidex.Domain.Apps.Read/Schemas/WebhookResult.cs
  16. 88
      src/Squidex.Domain.Apps.Read/Schemas/WebhookSender.cs
  17. 4
      src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs
  18. 2
      src/Squidex.Domain.Users/UserExtensions.cs
  19. 1
      src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs
  20. 2
      src/Squidex.Infrastructure/TypeNameRegistry.cs
  21. 28
      src/Squidex/Config/Domain/ReadModule.cs
  22. 30
      src/Squidex/Config/Domain/StoreMongoDbModule.cs
  23. 2
      src/Squidex/Controllers/Api/Assets/AssetsController.cs
  24. 4
      src/Squidex/Controllers/Api/Plans/AppPlansController.cs
  25. 9
      src/Squidex/Controllers/Api/Webhooks/Models/WebhookDto.cs
  26. 60
      src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventDto.cs
  27. 23
      src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventsDto.cs
  28. 44
      src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs
  29. 3
      src/Squidex/Controllers/ContentApi/ContentSwaggerController.cs
  30. 1
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  31. 3
      src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs
  32. 18
      src/Squidex/app/features/webhooks/module.ts
  33. 67
      src/Squidex/app/features/webhooks/pages/webhooks-page.component.html
  34. 8
      src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts
  35. 12
      src/Squidex/app/shared/services/webhooks.service.spec.ts
  36. 8
      src/Squidex/app/shared/services/webhooks.service.ts
  37. 8
      src/Squidex/app/theme/icomoon/demo-files/demo.css
  38. 1474
      src/Squidex/app/theme/icomoon/demo.html
  39. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.eot
  40. 7
      src/Squidex/app/theme/icomoon/fonts/icomoon.svg
  41. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.ttf
  42. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.woff
  43. 1326
      src/Squidex/app/theme/icomoon/selection.json
  44. 220
      src/Squidex/app/theme/icomoon/style.css

10
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaWebhookEntity.cs

@ -7,7 +7,6 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Domain.Apps.Read.Schemas;
@ -54,14 +53,5 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
[BsonRequired]
[BsonElement]
public long TotalRequestTime { get; set; }
[BsonRequired]
[BsonElement]
public List<string> LastDumps { get; set; } = new List<string>();
IEnumerable<string> ISchemaWebhookEntity.LastDumps
{
get { return LastDumps; }
}
}
}

11
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaWebhookRepository.cs

@ -26,7 +26,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
{
public partial class MongoSchemaWebhookRepository : MongoRepositoryBase<MongoSchemaWebhookEntity>, ISchemaWebhookRepository, IEventConsumer
{
private const int MaxDumps = 10;
private static readonly List<ShortInfo> EmptyWebhooks = new List<ShortInfo>();
private Dictionary<Guid, Dictionary<Guid, List<ShortInfo>>> inMemoryWebhooks;
private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1);
@ -67,7 +66,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
return inMemoryWebhooks.GetOrDefault(appId)?.GetOrDefault(schemaId)?.ToList() ?? EmptyWebhooks;
}
public async Task AddInvokationAsync(Guid webhookId, string dump, WebhookResult result, TimeSpan elapsed)
public async Task TraceSentAsync(Guid webhookId, WebhookResult result, TimeSpan elapsed)
{
var webhookEntity =
await Collection.Find(x => x.Id == webhookId)
@ -80,7 +79,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
case WebhookResult.Success:
webhookEntity.TotalSucceeded++;
break;
case WebhookResult.Fail:
case WebhookResult.Failed:
webhookEntity.TotalFailed++;
break;
case WebhookResult.Timeout:
@ -89,12 +88,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
}
webhookEntity.TotalRequestTime += (long)elapsed.TotalMilliseconds;
webhookEntity.LastDumps.Insert(0, dump);
while (webhookEntity.LastDumps.Count > MaxDumps)
{
webhookEntity.LastDumps.RemoveAt(webhookEntity.LastDumps.Count - 1);
}
await Collection.ReplaceOneAsync(x => x.Id == webhookId, webhookEntity);
}

79
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoWebhookEventEntity.cs

@ -0,0 +1,79 @@
// ==========================================================================
// MongoWebhookEventEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using MongoDB.Bson.Serialization.Attributes;
using NodaTime;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
{
public sealed class MongoWebhookEventEntity : MongoEntity, IWebhookEventEntity
{
private WebhookJob job;
[BsonRequired]
[BsonElement]
public Guid AppId { get; set; }
[BsonRequired]
[BsonElement]
public Guid WebhookId { get; set; }
[BsonRequired]
[BsonElement]
public Uri RequestUrl { get; set; }
[BsonRequired]
[BsonElement]
public string RequestBody { get; set; }
[BsonRequired]
[BsonElement]
public string RequestSignature { get; set; }
[BsonRequired]
[BsonElement]
public string EventName { get; set; }
[BsonRequired]
[BsonElement]
public string LastDump { get; set; }
[BsonRequired]
[BsonElement]
public Instant Expires { get; set; }
[BsonRequired]
[BsonElement]
public Instant? NextAttempt { get; set; }
[BsonRequired]
[BsonElement]
public int NumCalls { get; set; }
[BsonRequired]
[BsonElement]
public bool IsSending { get; set; }
[BsonRequired]
[BsonElement]
public WebhookResult Result { get; set; }
[BsonRequired]
[BsonElement]
public WebhookJobResult JobResult { get; set; }
public WebhookJob Job
{
get { return job ?? (job = SimpleMapper.Map(this, new WebhookJob())); }
}
}
}

109
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoWebhookEventRepository.cs

@ -0,0 +1,109 @@
// ==========================================================================
// MongoWebhookEventRepository.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using NodaTime;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
{
public sealed class MongoWebhookEventRepository : MongoRepositoryBase<MongoWebhookEventEntity>, IWebhookEventRepository
{
private readonly IClock clock;
public MongoWebhookEventRepository(IMongoDatabase database, IClock clock)
: base(database)
{
Guard.NotNull(clock, nameof(clock));
this.clock = clock;
}
protected override string CollectionName()
{
return "WebhookEvents";
}
protected override async Task SetupCollectionAsync(IMongoCollection<MongoWebhookEventEntity> collection)
{
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NextAttempt).Descending(x => x.IsSending));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Descending(x => x.Created));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero });
}
public Task QueryPendingAsync(Func<IWebhookEventEntity, Task> callback, CancellationToken cancellationToken = new CancellationToken())
{
var now = clock.GetCurrentInstant();
return Collection.Find(x => x.NextAttempt < now && !x.IsSending).ForEachAsync(callback, cancellationToken);
}
public async Task<IReadOnlyList<IWebhookEventEntity>> QueryByAppAsync(Guid appId, int skip = 0, int take = 20)
{
var entities = await Collection.Find(x => x.AppId == appId).Skip(skip).Limit(take).SortByDescending(x => x.Created).ToListAsync();
return entities;
}
public async Task<IWebhookEventEntity> FindAsync(Guid id)
{
var entity = await Collection.Find(x => x.Id == id).FirstOrDefaultAsync();
return entity;
}
public async Task<int> CountByAppAsync(Guid appId)
{
return (int)await Collection.CountAsync(x => x.AppId == appId);
}
public Task TraceSendingAsync(Guid jobId)
{
return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true));
}
public Task EnqueueAsync(WebhookJob job, Instant nextAttempt)
{
var entity = SimpleMapper.Map(job, new MongoWebhookEventEntity { NextAttempt = nextAttempt });
return Collection.InsertOneIfNotExistsAsync(entity);
}
public Task TraceSentAsync(Guid jobId, string dump, WebhookResult result, TimeSpan elapsed, Instant? nextAttempt)
{
WebhookJobResult jobResult;
if (result != WebhookResult.Success && nextAttempt == null)
{
jobResult = WebhookJobResult.Failed;
}
else if (result != WebhookResult.Success && nextAttempt.HasValue)
{
jobResult = WebhookJobResult.Retry;
}
else
{
jobResult = WebhookJobResult.Success;
}
return Collection.UpdateOneAsync(x => x.Id == jobId,
Update.Set(x => x.Result, result)
.Set(x => x.LastDump, dump)
.Set(x => x.JobResult, jobResult)
.Inc(x => x.NumCalls, 1));
}
}
}

9
src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs

@ -6,20 +6,19 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Assets.Repositories;
using Squidex.Domain.Apps.Read.Contents.Repositories;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Domain.Apps.Read.Utils;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using System;
using GraphQL;
using Squidex.Infrastructure.Tasks;
using Squidex.Domain.Apps.Events;
// ReSharper disable InvertIf

3
src/Squidex.Domain.Apps.Read/Schemas/ISchemaWebhookEntity.cs

@ -7,7 +7,6 @@
// ==========================================================================
using System;
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Read.Schemas
{
@ -22,7 +21,5 @@ namespace Squidex.Domain.Apps.Read.Schemas
long TotalTimedout { get; }
long TotalRequestTime { get; }
IEnumerable<string> LastDumps { get; }
}
}

27
src/Squidex.Domain.Apps.Read/Schemas/IWebhookEventEntity.cs

@ -0,0 +1,27 @@
// ==========================================================================
// IWebhookEventEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using NodaTime;
namespace Squidex.Domain.Apps.Read.Schemas
{
public interface IWebhookEventEntity
{
WebhookJob Job { get; }
Instant? NextAttempt { get; }
WebhookResult Result { get; }
WebhookJobResult JobResult { get; }
int NumCalls { get; }
string LastDump { get; }
}
}

2
src/Squidex.Domain.Apps.Read/Schemas/Repositories/ISchemaWebhookRepository.cs

@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Read.Schemas.Repositories
{
public interface ISchemaWebhookRepository
{
Task AddInvokationAsync(Guid webhookId, string dump, WebhookResult result, TimeSpan elapsed);
Task TraceSentAsync(Guid webhookId, WebhookResult result, TimeSpan elapsed);
Task<IReadOnlyList<ISchemaWebhookUrlEntity>> QueryUrlsBySchemaAsync(Guid appId, Guid schemaId);

33
src/Squidex.Domain.Apps.Read/Schemas/Repositories/IWebhookEventRepository.cs

@ -0,0 +1,33 @@
// ==========================================================================
// IWebhookEventRepository.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NodaTime;
namespace Squidex.Domain.Apps.Read.Schemas.Repositories
{
public interface IWebhookEventRepository
{
Task EnqueueAsync(WebhookJob job, Instant nextAttempt);
Task TraceSendingAsync(Guid jobId);
Task TraceSentAsync(Guid jobId, string dump, WebhookResult result, TimeSpan elapsed, Instant? nextCall);
Task QueryPendingAsync(Func<IWebhookEventEntity, Task> callback, CancellationToken cancellationToken = default(CancellationToken));
Task<int> CountByAppAsync(Guid appId);
Task<IReadOnlyList<IWebhookEventEntity>> QueryByAppAsync(Guid appId, int skip = 0, int take = 20);
Task<IWebhookEventEntity> FindAsync(Guid id);
}
}

159
src/Squidex.Domain.Apps.Read/Schemas/WebhookDequeuer.cs

@ -0,0 +1,159 @@
// ==========================================================================
// WebhookDequeuer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using NodaTime;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Timers;
// ReSharper disable SwitchStatementMissingSomeCases
// ReSharper disable MethodSupportsCancellation
// ReSharper disable InvertIf
namespace Squidex.Domain.Apps.Read.Schemas
{
public sealed class WebhookDequeuer : DisposableObjectBase, IExternalSystem
{
private readonly ActionBlock<IWebhookEventEntity> requestBlock;
private readonly TransformBlock<IWebhookEventEntity, IWebhookEventEntity> blockBlock;
private readonly IWebhookEventRepository webhookEventRepository;
private readonly ISchemaWebhookRepository webhookRepository;
private readonly WebhookSender webhookSender;
private readonly CompletionTimer timer;
private readonly ISemanticLog log;
private readonly IClock clock;
public WebhookDequeuer(WebhookSender webhookSender,
IWebhookEventRepository webhookEventRepository,
ISchemaWebhookRepository webhookRepository,
IClock clock,
ISemanticLog log)
{
Guard.NotNull(webhookEventRepository, nameof(webhookEventRepository));
Guard.NotNull(webhookRepository, nameof(webhookRepository));
Guard.NotNull(webhookSender, nameof(webhookSender));
Guard.NotNull(clock, nameof(clock));
Guard.NotNull(log, nameof(log));
this.webhookEventRepository = webhookEventRepository;
this.webhookRepository = webhookRepository;
this.webhookSender = webhookSender;
this.clock = clock;
this.log = log;
requestBlock =
new ActionBlock<IWebhookEventEntity>(MakeRequestAsync,
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 });
blockBlock =
new TransformBlock<IWebhookEventEntity, IWebhookEventEntity>(x => BlockAsync(x),
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1, BoundedCapacity = 1 });
blockBlock.LinkTo(requestBlock, new DataflowLinkOptions { PropagateCompletion = true });
timer = new CompletionTimer(5000, QueryAsync);
}
protected override void DisposeObject(bool disposing)
{
if (disposing)
{
timer.StopAsync().Wait();
blockBlock.Complete();
requestBlock.Completion.Wait();
}
}
public void Connect()
{
}
private async Task QueryAsync(CancellationToken cancellationToken)
{
try
{
await webhookEventRepository.QueryPendingAsync(blockBlock.SendAsync, cancellationToken);
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "QueueWebhookEvents")
.WriteProperty("status", "Failed"));
}
}
private async Task<IWebhookEventEntity> BlockAsync(IWebhookEventEntity @event)
{
try
{
await webhookEventRepository.TraceSendingAsync(@event.Job.Id);
return @event;
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "BlockWebhookEvent")
.WriteProperty("status", "Failed"));
throw;
}
}
private async Task MakeRequestAsync(IWebhookEventEntity @event)
{
try
{
var now = clock.GetCurrentInstant();
var response = await webhookSender.SendAsync(@event.Job);
Instant? nextCall = null;
if (response.Result != WebhookResult.Success)
{
switch (@event.NumCalls)
{
case 0:
nextCall = now.Plus(Duration.FromMinutes(5));
break;
case 1:
nextCall = now.Plus(Duration.FromHours(1));
break;
case 2:
nextCall = now.Plus(Duration.FromHours(5));
break;
case 3:
nextCall = now.Plus(Duration.FromHours(6));
break;
}
}
await Task.WhenAll(
webhookRepository.TraceSentAsync(@event.Job.WebhookId, response.Result, response.Elapsed),
webhookEventRepository.TraceSentAsync(@event.Job.Id, response.Dump, response.Result, response.Elapsed, nextCall));
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "SendWebhookEvent")
.WriteProperty("status", "Failed"));
throw;
}
}
}
}

126
src/Squidex.Domain.Apps.Read/Schemas/WebhookEnqueuer.cs

@ -0,0 +1,126 @@
// ==========================================================================
// WebhookEnqueuer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NodaTime;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Read.Schemas
{
public sealed class WebhookEnqueuer : IEventConsumer
{
private const string ContentPrefix = "Content";
private static readonly Duration ExpirationTime = Duration.FromDays(2);
private readonly IWebhookEventRepository webhookEventRepository;
private readonly ISchemaWebhookRepository webhookRepository;
private readonly IClock clock;
private readonly TypeNameRegistry typeNameRegistry;
private readonly JsonSerializer webhookSerializer;
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return "^content-"; }
}
public WebhookEnqueuer(TypeNameRegistry typeNameRegistry,
IWebhookEventRepository webhookEventRepository,
ISchemaWebhookRepository webhookRepository,
IClock clock,
JsonSerializer webhookSerializer)
{
Guard.NotNull(webhookEventRepository, nameof(webhookEventRepository));
Guard.NotNull(webhookSerializer, nameof(webhookSerializer));
Guard.NotNull(webhookRepository, nameof(webhookRepository));
Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry));
Guard.NotNull(clock, nameof(clock));
this.webhookEventRepository = webhookEventRepository;
this.webhookSerializer = webhookSerializer;
this.webhookRepository = webhookRepository;
this.clock = clock;
this.typeNameRegistry = typeNameRegistry;
}
public Task ClearAsync()
{
return TaskHelper.Done;
}
public async Task On(Envelope<IEvent> @event)
{
if (@event.Payload is ContentEvent contentEvent)
{
var eventType = typeNameRegistry.GetName(@event.Payload.GetType());
var webhooks = await webhookRepository.QueryUrlsBySchemaAsync(contentEvent.AppId.Id, contentEvent.SchemaId.Id);
if (webhooks.Count > 0)
{
var now = clock.GetCurrentInstant();
var payload = CreatePayload(@event, eventType);
var eventName = $"{contentEvent.SchemaId.Name.ToPascalCase()}{CreateContentEventName(eventType)}";
foreach (var webhook in webhooks)
{
await EnqueueJobAsync(payload, webhook, contentEvent, eventName, now);
}
}
}
}
private async Task EnqueueJobAsync(string payload, ISchemaWebhookUrlEntity webhook, AppEvent contentEvent, string eventName, Instant now)
{
var signature = $"{payload}{webhook.SharedSecret}".Sha256Base64();
var job = new WebhookJob
{
Id = Guid.NewGuid(),
AppId = contentEvent.AppId.Id,
RequestUrl = webhook.Url,
RequestBody = payload,
RequestSignature = signature,
EventName = eventName,
Expires = now.Plus(ExpirationTime),
WebhookId = webhook.Id
};
await webhookEventRepository.EnqueueAsync(job, now);
}
private string CreatePayload(Envelope<IEvent> @event, string eventType)
{
return new JObject(
new JProperty("type", eventType),
new JProperty("payload", JObject.FromObject(@event.Payload, webhookSerializer)),
new JProperty("timestamp", @event.Headers.Timestamp().ToString()))
.ToString(Formatting.Indented);
}
private static string CreateContentEventName(string eventType)
{
return eventType.StartsWith(ContentPrefix) ? eventType.Substring(ContentPrefix.Length) : eventType;
}
}
}

237
src/Squidex.Domain.Apps.Read/Schemas/WebhookInvoker.cs

@ -1,237 +0,0 @@
// ==========================================================================
// WebhookInvoker.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NodaTime;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Http;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Read.Schemas
{
public sealed class WebhookInvoker : DisposableObjectBase, IEventConsumer
{
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2);
private readonly ISchemaWebhookRepository webhookRepository;
private readonly ISemanticLog log;
private readonly TypeNameRegistry typeNameRegistry;
private readonly JsonSerializer webhookSerializer;
private readonly TransformBlock<InvocationRequest, InvocationResponse> invokeBlock;
private readonly ActionBlock<InvocationResponse> dumpBlock;
private class WebhookData
{
public ISchemaWebhookUrlEntity Webhook;
}
private sealed class InvocationRequest : WebhookData
{
public JObject Payload;
}
private sealed class InvocationResponse : WebhookData
{
public string Dump;
public TimeSpan Elapsed;
public WebhookResult Result;
}
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return "^content-"; }
}
public WebhookInvoker(ISchemaWebhookRepository webhookRepository, JsonSerializer webhookSerializer, ISemanticLog log, TypeNameRegistry typeNameRegistry)
{
Guard.NotNull(webhookRepository, nameof(webhookRepository));
Guard.NotNull(webhookSerializer, nameof(webhookSerializer));
Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry));
Guard.NotNull(log, nameof(log));
this.webhookRepository = webhookRepository;
this.webhookSerializer = webhookSerializer;
this.log = log;
this.typeNameRegistry = typeNameRegistry;
invokeBlock =
new TransformBlock<InvocationRequest, InvocationResponse>(x => DispatchEventAsync(x),
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1, BoundedCapacity = 8 });
dumpBlock =
new ActionBlock<InvocationResponse>(DumpAsync,
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 64 });
invokeBlock.LinkTo(dumpBlock,
new DataflowLinkOptions { PropagateCompletion = true }, x => x != null);
}
protected override void DisposeObject(bool disposing)
{
invokeBlock.Complete();
dumpBlock.Completion.Wait();
}
public Task ClearAsync()
{
return TaskHelper.Done;
}
public async Task On(Envelope<IEvent> @event)
{
if (@event.Payload is ContentEvent contentEvent)
{
var webhooks = await webhookRepository.QueryUrlsBySchemaAsync(contentEvent.AppId.Id, contentEvent.SchemaId.Id);
if (webhooks.Count > 0)
{
var payload = CreatePayload(@event);
foreach (var webhook in webhooks)
{
await invokeBlock.SendAsync(new InvocationRequest { Webhook = webhook, Payload = payload });
}
}
}
}
private JObject CreatePayload(Envelope<IEvent> @event)
{
return new JObject(
new JProperty("type", typeNameRegistry.GetName(@event.Payload.GetType())),
new JProperty("payload", JObject.FromObject(@event.Payload, webhookSerializer)),
new JProperty("timestamp", @event.Headers.Timestamp().ToString()));
}
private async Task DumpAsync(InvocationResponse input)
{
try
{
await webhookRepository.AddInvokationAsync(input.Webhook.Id, input.Dump, input.Result, input.Elapsed);
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "DumpHook")
.WriteProperty("status", "Failed"));
}
}
private async Task<InvocationResponse> DispatchEventAsync(InvocationRequest input)
{
try
{
var payload = SignPayload(input.Payload, input.Webhook);
var requestString = payload.ToString(Formatting.Indented);
var responseString = string.Empty;
var request = BuildRequest(requestString, input.Webhook);
var response = (HttpResponseMessage)null;
var isTimeout = false;
var watch = Stopwatch.StartNew();
try
{
using (log.MeasureInformation(w => w
.WriteProperty("action", "SendToHook")
.WriteProperty("status", "Invoked")
.WriteProperty("requestUrl", request.RequestUri.ToString())))
{
using (var client = new HttpClient { Timeout = Timeout })
{
response = await client.SendAsync(request);
}
}
}
catch (TimeoutException)
{
isTimeout = true;
}
catch (OperationCanceledException)
{
isTimeout = true;
}
finally
{
watch.Stop();
}
if (response != null)
{
responseString = await response.Content.ReadAsStringAsync();
}
var dump = DumpFormatter.BuildDump(request, response, requestString, responseString, watch.Elapsed);
var result = WebhookResult.Fail;
if (isTimeout)
{
result = WebhookResult.Timeout;
}
else if (response?.IsSuccessStatusCode == true)
{
result = WebhookResult.Success;
}
return new InvocationResponse { Dump = dump, Result = result, Elapsed = watch.Elapsed, Webhook = input.Webhook };
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "SendToHook")
.WriteProperty("status", "Failed"));
return null;
}
}
private static JObject SignPayload(JObject payload, ISchemaWebhookUrlEntity webhook)
{
payload = new JObject(payload);
var eventTimestamp = SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds();
var eventSignature = $"{eventTimestamp}{webhook.SharedSecret}".Sha256Base64();
payload["signature"] = eventSignature;
return payload;
}
private static HttpRequestMessage BuildRequest(string requestBody, ISchemaWebhookUrlEntity webhook)
{
var request = new HttpRequestMessage(HttpMethod.Post, webhook.Url)
{
Content = new StringContent(requestBody, Encoding.UTF8, "application/json")
};
return request;
}
}
}

32
src/Squidex.Domain.Apps.Read/Schemas/WebhookJob.cs

@ -0,0 +1,32 @@
// ==========================================================================
// WebhookJob.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using NodaTime;
namespace Squidex.Domain.Apps.Read.Schemas
{
public sealed class WebhookJob
{
public Guid Id { get; set; }
public Guid AppId { get; set; }
public Guid WebhookId { get; set; }
public Uri RequestUrl { get; set; }
public string RequestBody { get; set; }
public string RequestSignature { get; set; }
public string EventName { get; set; }
public Instant Expires { get; set; }
}
}

18
src/Squidex.Domain.Apps.Read/Schemas/WebhookJobResult.cs

@ -0,0 +1,18 @@
// ==========================================================================
// WebhookJobResult.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Read.Schemas
{
public enum WebhookJobResult
{
Pending,
Success,
Retry,
Failed
}
}

3
src/Squidex.Domain.Apps.Read/Schemas/WebhookResult.cs

@ -10,8 +10,9 @@ namespace Squidex.Domain.Apps.Read.Schemas
{
public enum WebhookResult
{
Pending,
Success,
Fail,
Failed,
Timeout
}
}

88
src/Squidex.Domain.Apps.Read/Schemas/WebhookSender.cs

@ -0,0 +1,88 @@
// ==========================================================================
// WebhookSender.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Squidex.Infrastructure.Http;
// ReSharper disable SuggestVarOrType_SimpleTypes
namespace Squidex.Domain.Apps.Read.Schemas
{
public class WebhookSender
{
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2);
public virtual async Task<(string Dump, WebhookResult Result, TimeSpan Elapsed)> SendAsync(WebhookJob job)
{
HttpRequestMessage request = BuildRequest(job);
HttpResponseMessage response = null;
var isTimeout = false;
var watch = Stopwatch.StartNew();
try
{
using (var client = new HttpClient { Timeout = Timeout })
{
response = await client.SendAsync(request);
}
}
catch (TimeoutException)
{
isTimeout = true;
}
catch (OperationCanceledException)
{
isTimeout = true;
}
finally
{
watch.Stop();
}
var responseString = string.Empty;
if (response != null)
{
responseString = await response.Content.ReadAsStringAsync();
}
var dump = DumpFormatter.BuildDump(request, response, job.RequestBody, responseString, watch.Elapsed);
var result = WebhookResult.Failed;
if (isTimeout)
{
result = WebhookResult.Timeout;
}
else if (response?.IsSuccessStatusCode == true)
{
result = WebhookResult.Success;
}
return (dump, result, watch.Elapsed);
}
private static HttpRequestMessage BuildRequest(WebhookJob job)
{
var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl)
{
Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json")
};
request.Headers.Add("X-Signature", job.RequestSignature);
return request;
}
}
}

4
src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs

@ -14,9 +14,7 @@ using MongoDB.Driver;
namespace Squidex.Domain.Users.MongoDb
{
public sealed class MongoRoleStore :
IRoleStore<IRole>,
IRoleFactory
public sealed class MongoRoleStore : IRoleStore<IRole>, IRoleFactory
{
private readonly RoleStore<WrappedIdentityRole> innerStore;

2
src/Squidex.Domain.Users/UserExtensions.cs

@ -57,7 +57,7 @@ namespace Squidex.Domain.Users
{
var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.SquidexPictureUrl)?.Value;
if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar"))
if (url != null && !string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar"))
{
if (url.Contains("?"))
{

1
src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs

@ -89,6 +89,7 @@ namespace Squidex.Infrastructure.Log
{
return writer.WriteObject(nameof(exception), inner =>
{
inner.WriteProperty("type", exception.GetType().FullName);
inner.WriteProperty("message", exception.Message);
inner.WriteProperty("stackTrace", exception.StackTrace);
});

2
src/Squidex.Infrastructure/TypeNameRegistry.cs

@ -78,7 +78,7 @@ namespace Squidex.Infrastructure
{
var typeNameAttribute = type.GetTypeInfo().GetCustomAttribute<TypeNameAttribute>();
if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName))
if (typeNameAttribute != null && !string.IsNullOrWhiteSpace(typeNameAttribute.TypeName))
{
Map(type, typeNameAttribute.TypeName);
}

28
src/Squidex/Config/Domain/ReadModule.cs

@ -22,6 +22,7 @@ using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Read.Schemas.Services.Implementations;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Pipeline;
@ -62,41 +63,56 @@ namespace Squidex.Config.Domain
builder.RegisterType<GraphQLUrlGenerator>()
.As<IGraphQLUrlGenerator>()
.AsSelf()
.SingleInstance();
builder.RegisterType<AssetUserPictureStore>()
.As<IUserPictureStore>()
.AsSelf()
.SingleInstance();
builder.RegisterType<AppHistoryEventsCreator>()
.As<IHistoryEventsCreator>()
.AsSelf()
.SingleInstance();
builder.RegisterType<ContentHistoryEventsCreator>()
.As<IHistoryEventsCreator>()
.AsSelf()
.SingleInstance();
builder.RegisterType<SchemaHistoryEventsCreator>()
.As<IHistoryEventsCreator>()
.AsSelf()
.SingleInstance();
builder.RegisterType<NoopAppPlanBillingManager>()
.As<IAppPlanBillingManager>()
.AsSelf()
.InstancePerDependency();
builder.RegisterType<CachingGraphQLService>()
.As<IGraphQLService>()
.AsSelf()
.InstancePerDependency();
builder.RegisterType<WebhookInvoker>()
builder.RegisterType<WebhookDequeuer>()
.As<IExternalSystem>()
.AsSelf()
.InstancePerDependency();
builder.RegisterType<WebhookEnqueuer>()
.As<IEventConsumer>()
.AsSelf()
.SingleInstance();
.InstancePerDependency();
builder.RegisterType<EdmModelBuilder>()
builder.RegisterType<WebhookSender>()
.AsSelf()
.SingleInstance();
builder.RegisterType<CachingGraphQLService>()
.As<IGraphQLService>()
builder.RegisterType<EdmModelBuilder>()
.AsSelf()
.InstancePerDependency();
.SingleInstance();
}
}
}

30
src/Squidex/Config/Domain/StoreMongoDbModule.cs

@ -88,28 +88,41 @@ namespace Squidex.Config.Domain
.As<IUserStore<IUser>>()
.As<IUserFactory>()
.As<IUserResolver>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoRoleStore>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IRoleStore<IRole>>()
.As<IRoleFactory>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoPersistedGrantStore>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IPersistedGrantStore>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoUsageStore>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IUsageStore>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoHistoryEventRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IHistoryEventRepository>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoEventConsumerInfoRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IEventConsumerInfoRepository>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
@ -120,32 +133,32 @@ namespace Squidex.Config.Domain
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoAppRepository>()
builder.RegisterType<MongoWebhookEventRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IAppRepository>()
.As<IWebhookEventRepository>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoAssetStatsRepository>()
builder.RegisterType<MongoAppRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IAssetStatsRepository>()
.As<IAppRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoAssetRepository>()
builder.RegisterType<MongoAssetStatsRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IAssetRepository>()
.As<IAssetStatsRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoHistoryEventRepository>()
builder.RegisterType<MongoAssetRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IHistoryEventRepository>()
.As<IAssetRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>()
.AsSelf()
@ -162,6 +175,7 @@ namespace Squidex.Config.Domain
builder.RegisterType<MongoSchemaRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<ISchemaRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();

2
src/Squidex/Controllers/Api/Assets/AssetsController.cs

@ -25,6 +25,8 @@ using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
// ReSharper disable PossibleNullReferenceException
namespace Squidex.Controllers.Api.Assets
{
/// <summary>

4
src/Squidex/Controllers/Api/Plans/AppPlansController.cs

@ -33,7 +33,9 @@ namespace Squidex.Controllers.Api.Plans
private readonly IAppPlansProvider appPlansProvider;
private readonly IAppPlanBillingManager appPlansBillingManager;
public AppPlansController(ICommandBus commandBus, IAppPlansProvider appPlansProvider, IAppPlanBillingManager appPlansBillingManager)
public AppPlansController(ICommandBus commandBus,
IAppPlansProvider appPlansProvider,
IAppPlanBillingManager appPlansBillingManager)
: base(commandBus)
{
this.appPlansProvider = appPlansProvider;

9
src/Squidex/Controllers/Api/Webhooks/Models/WebhookDto.cs

@ -7,7 +7,6 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Webhooks.Models
@ -40,7 +39,7 @@ namespace Squidex.Controllers.Api.Webhooks.Models
public long TotalTimedout { get; set; }
/// <summary>
/// The average request time in milliseconds.
/// The average response time in milliseconds.
/// </summary>
public long AverageRequestTimeMs { get; set; }
@ -55,11 +54,5 @@ namespace Squidex.Controllers.Api.Webhooks.Models
/// </summary>
[Required]
public string SharedSecret { get; set; }
/// <summary>
/// The last invokation dumps.
/// </summary>
[Required]
public List<string> LastDumps { get; set; }
}
}

60
src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventDto.cs

@ -0,0 +1,60 @@
// ==========================================================================
// WebhookEventDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Domain.Apps.Read.Schemas;
namespace Squidex.Controllers.Api.Webhooks.Models
{
public class WebhookEventDto
{
/// <summary>
/// The request url.
/// </summary>
[Required]
public Uri RequestUrl { get; set; }
/// <summary>
/// The name of the event.
/// </summary>
[Required]
public string EventName { get; set; }
/// <summary>
/// The webhook event id.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The last dump.
/// </summary>
public string LastDump { get; set; }
/// <summary>
/// The number of calls.
/// </summary>
public int NumCalls { get; set; }
/// <summary>
/// The next attempt.
/// </summary>
public Instant? NextAttempt { get; set; }
/// <summary>
/// The result of the event.
/// </summary>
public WebhookResult Result { get; set; }
/// <summary>
/// The result of the job.
/// </summary>
public WebhookJobResult JobResult { get; set; }
}
}

23
src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventsDto.cs

@ -0,0 +1,23 @@
// ==========================================================================
// WebhookEventsDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Controllers.Api.Webhooks.Models
{
public class WebhookEventsDto
{
/// <summary>
/// The total number of webhook events.
/// </summary>
public long Total { get; set; }
/// <summary>
/// The webhook events.
/// </summary>
public WebhookEventDto[] Items { get; set; }
}
}

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

@ -31,11 +31,15 @@ namespace Squidex.Controllers.Api.Webhooks
public class WebhooksController : ControllerBase
{
private readonly ISchemaWebhookRepository webhooksRepository;
private readonly IWebhookEventRepository webhookEventsRepository;
public WebhooksController(ICommandBus commandBus, ISchemaWebhookRepository webhooksRepository)
public WebhooksController(ICommandBus commandBus,
ISchemaWebhookRepository webhooksRepository,
IWebhookEventRepository webhookEventsRepository)
: base(commandBus)
{
this.webhooksRepository = webhooksRepository;
this.webhookEventsRepository = webhookEventsRepository;
}
/// <summary>
@ -61,7 +65,7 @@ namespace Squidex.Controllers.Api.Webhooks
var count = w.TotalTimedout + w.TotalSucceeded + w.TotalFailed;
var average = count == 0 ? 0 : w.TotalRequestTime / count;
return SimpleMapper.Map(w, new WebhookDto { AverageRequestTimeMs = average, LastDumps = w.LastDumps.ToList() });
return SimpleMapper.Map(w, new WebhookDto { AverageRequestTimeMs = average });
});
return Ok(response);
@ -116,5 +120,41 @@ namespace Squidex.Controllers.Api.Webhooks
return NoContent();
}
/// <summary>
/// Get webhook events.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Webhook events returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/webhooks/events")]
[ProducesResponseType(typeof(WebhookEventsDto), 200)]
[ApiCosts(0)]
public async Task<IActionResult> GetEvents(string app)
{
var taskForItems = webhookEventsRepository.QueryByAppAsync(App.Id);
var taskForCount = webhookEventsRepository.CountByAppAsync(App.Id);
await Task.WhenAll(taskForItems, taskForCount);
var response = new WebhookEventsDto
{
Total = taskForCount.Result,
Items = taskForItems.Result.Select(x =>
{
var itemModel = new WebhookEventDto();
SimpleMapper.Map(x, itemModel);
SimpleMapper.Map(x.Job, itemModel);
return itemModel;
}).ToArray()
};
return Ok(response);
}
}
}

3
src/Squidex/Controllers/ContentApi/ContentSwaggerController.cs

@ -26,7 +26,8 @@ namespace Squidex.Controllers.ContentApi
private readonly IAppProvider appProvider;
private readonly SchemasSwaggerGenerator schemasSwaggerGenerator;
public ContentSwaggerController(ISchemaRepository schemaRepository, IAppProvider appProvider, SchemasSwaggerGenerator schemasSwaggerGenerator)
public ContentSwaggerController(ISchemaRepository schemaRepository, IAppProvider appProvider,
SchemasSwaggerGenerator schemasSwaggerGenerator)
{
this.appProvider = appProvider;

1
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -24,6 +24,7 @@ using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
// ReSharper disable PossibleNullReferenceException
// ReSharper disable RedundantIfElseBlock
namespace Squidex.Controllers.ContentApi

3
src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs

@ -40,7 +40,8 @@ namespace Squidex.Controllers.ContentApi.Generator
schemaQueryDescription = SwaggerHelper.LoadDocs("schemaquery");
}
public SchemaSwaggerGenerator(SwaggerDocument document, string path, Schema schema, Func<string, JsonSchema4, JsonSchema4> schemaResolver, PartitionResolver partitionResolver)
public SchemaSwaggerGenerator(SwaggerDocument document, string path, Schema schema,
Func<string, JsonSchema4, JsonSchema4> schemaResolver, PartitionResolver partitionResolver)
{
this.document = document;

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

@ -8,7 +8,11 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SqxFrameworkModule } from 'shared';
import {
HelpComponent,
SqxFrameworkModule,
SqxSharedModule
} from 'shared';
import {
WebhooksPageComponent
@ -17,13 +21,23 @@ import {
const routes: Routes = [
{
path: '',
component: WebhooksPageComponent
component: WebhooksPageComponent,
children: [
{
path: 'help',
component: HelpComponent,
data: {
helpPage: '05-integrated/webhooks'
}
}
]
}
];
@NgModule({
imports: [
SqxFrameworkModule,
SqxSharedModule,
RouterModule.forChild(routes)
],
declarations: [

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

@ -1,6 +1,6 @@
<sqx-title message="{app} | Webhooks" parameter1="app" value1="{{appName() | async}}"></sqx-title>
<sqx-panel panelWidth="46rem">
<sqx-panel panelWidth="50rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
@ -30,7 +30,7 @@
<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: 120px; text-align: right;" />
<col style="width: 100%" />
<col style="width: 40px" />
</colgroup>
@ -71,40 +71,28 @@
</tr>
</table>
<div class="webhook-stats" *ngIf="w.webhook.lastDumps.length > 0">
<div class="webhook-stats" *ngIf="w.webhook.averageRequestTimeMs > 0">
<div class="row">
<div class="col-8">
<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 Request Time">
<i class="icon-elapsed"></i> {{w.webhook.averageRequestTimeMs}} ms
</span>
</div>
</div>
<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-4 text-right">
<a *ngIf="!w.showDetails" class="webhook-detail-link" (click)="toggleDetails(w)">Show Last Request</a>
<a *ngIf="w.showDetails" class="webhook-detail-link" (click)="toggleDetails(w)">Hide Last Request</a>
<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 *ngIf="w.showDetails" class="webhook-dumps">
<h3>Last Request</h3>
<textarea class="form-control webhook-dump" readonly>{{w.webhook.lastDumps[0]}}</textarea>
</div>
</div>
</div>
@ -131,5 +119,16 @@
</div>
</div>
</div>
<div class="panel-sidebar">
<a class="panel-link" routerLink="events" routerLinkActive="active">
<i class="icon-time"></i>
</a>
<a class="panel-link" routerLink="help" routerLinkActive="active">
<i class="icon-help"></i>
</a>
</div>
</div>
</sqx-panel>
</sqx-panel>
<router-outlet></router-outlet>

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

@ -21,7 +21,7 @@ import {
WebhooksService
} from 'shared';
interface WebhookWithSchema { webhook: WebhookDto; schema: SchemaDto; showDetails: boolean; };
interface WebhookWithSchema { webhook: WebhookDto; schema: SchemaDto; };
@Component({
selector: 'sqx-webhooks-page',
@ -111,7 +111,7 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.webhooksService.postWebhook(app, schema.name, requestDto, this.version))
.subscribe(dto => {
this.webhooks = this.webhooks.push({ webhook: dto, schema: schema, showDetails: false });
this.webhooks = this.webhooks.push({ webhook: dto, schema: schema });
this.resetWebhookForm();
}, error => {
@ -125,10 +125,6 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
this.resetWebhookForm();
}
public toggleDetails(webhook: WebhookWithSchema) {
this.webhooks = this.webhooks.replace(webhook, { webhook: webhook.webhook, schema: webhook.schema, showDetails: !webhook.showDetails });
}
private enableWebhookForm() {
this.addWebhookForm.enable();
}

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

@ -58,8 +58,7 @@ describe('WebhooksService', () => {
totalSucceeded: 1,
totalFailed: 2,
totalTimedout: 3,
averageRequestTimeMs: 4,
lastDumps: ['dump1']
averageRequestTimeMs: 4
},
{
id: 'id2',
@ -69,14 +68,13 @@ describe('WebhooksService', () => {
totalSucceeded: 5,
totalFailed: 6,
totalTimedout: 7,
averageRequestTimeMs: 8,
lastDumps: ['dump2']
averageRequestTimeMs: 8
}
]);
expect(webhooks).toEqual([
new WebhookDto('id1', 'schemaId1', 'token1', 'http://squidex.io/1', 1, 2, 3, 4, ['dump1']),
new WebhookDto('id2', 'schemaId2', 'token2', 'http://squidex.io/2', 5, 6, 7, 8, ['dump2'])
new WebhookDto('id1', 'schemaId1', 'token1', 'http://squidex.io/1', 1, 2, 3, 4),
new WebhookDto('id2', 'schemaId2', 'token2', 'http://squidex.io/2', 5, 6, 7, 8)
]);
}));
@ -98,7 +96,7 @@ describe('WebhooksService', () => {
req.flush({ id: 'id1', sharedSecret: 'token1', schemaId: 'schema1' });
expect(webhook).toEqual(new WebhookDto('id1', 'schema1', 'token1', dto.url, 0, 0, 0, 0, []));
expect(webhook).toEqual(new WebhookDto('id1', 'schema1', 'token1', dto.url, 0, 0, 0, 0));
}));
it('should make delete request to delete webhook',

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

@ -26,8 +26,7 @@ export class WebhookDto {
public readonly totalSucceeded: number,
public readonly totalFailed: number,
public readonly totalTimedout: number,
public readonly averageRequestTimeMs: number,
public readonly lastDumps: string[]
public readonly averageRequestTimeMs: number
) {
}
}
@ -63,8 +62,7 @@ export class WebhooksService {
item.totalSucceeded,
item.totalFailed,
item.totalTimedout,
item.averageRequestTimeMs,
item.lastDumps);
item.averageRequestTimeMs);
});
})
.pretifyError('Failed to load webhooks. Please reload.');
@ -80,7 +78,7 @@ export class WebhooksService {
response.schemaId,
response.sharedSecret,
dto.url,
0, 0, 0, 0, []);
0, 0, 0, 0);
})
.pretifyError('Failed to create webhook. Please reload.');
}

8
src/Squidex/app/theme/icomoon/demo-files/demo.css

@ -153,15 +153,15 @@ p {
font-size: 32px;
}
.fs3 {
font-size: 24px;
font-size: 28px;
}
.fs4 {
font-size: 28px;
font-size: 32px;
}
.fs5 {
font-size: 20px;
font-size: 24px;
}
.fs6 {
font-size: 32px;
font-size: 20px;
}

1474
src/Squidex/app/theme/icomoon/demo.html

File diff suppressed because it is too large

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.eot

Binary file not shown.

7
src/Squidex/app/theme/icomoon/fonts/icomoon.svg

@ -73,7 +73,10 @@
<glyph unicode="&#xe93f;" glyph-name="webhook" d="M524.792 917.333c-133.751 0-243.208-108.454-243.208-241 0-72.755 34.012-137.024 85.833-181.292l-115.167-187.458c-3.016 0.305-5.946 0.875-9.042 0.875-49.392 0-89.625-39.845-89.625-88.792s40.233-88.792 89.625-88.792c49.392 0 89.583 39.845 89.583 88.792 0 17.952-5.505 34.638-14.792 48.625l152.208 247.708-32.458 19.833c-47.549 29.077-79.333 80.999-79.333 140.5 0 91.263 74.283 164.875 166.375 164.875s166.417-73.612 166.417-164.875c0-14.491-1.921-28.493-5.458-41.875l74.292-19.292c5.167 19.548 7.958 40.089 7.958 61.167 0 132.546-109.457 241-243.208 241zM524.792 765.125c-49.392 0-89.583-39.845-89.583-88.792s40.191-88.792 89.583-88.792c1.932 0 3.765 0.422 5.667 0.542l136.375-242.5 33.5 18.417c23.855 13.109 51.195 20.583 80.458 20.583 92.092 0 166.417-73.654 166.417-164.917s-74.324-164.875-166.417-164.875c-52.606 0-99.199 24.144-129.792 61.875l-59.917-47.708c44.569-54.969 113.29-90.292 189.708-90.292 133.751 0 243.208 108.454 243.208 241s-109.457 241-243.208 241c-28.895 0-55.96-6.577-81.792-15.833l-101.333 180.208c10.44 14.518 16.75 32.153 16.75 51.292 0 48.947-40.233 88.792-89.625 88.792zM182.333 453.042c-104.909-26.881-182.333-121.59-182.333-233.375 0-132.546 109.457-241 243.208-241 120.157 0 216.178 89.065 235.375 202.958h221.625c14.445-29.878 44.991-50.75 80.583-50.75 49.392 0 89.625 39.845 89.625 88.792s-40.233 88.792-89.625 88.792c-35.592 0-66.138-20.831-80.583-50.708h-290.625v-38.083c0-91.263-74.283-164.875-166.375-164.875s-166.417 73.612-166.417 164.875c0 76.963 53.266 141.381 124.792 159.708l-19.25 73.667z" />
<glyph unicode="&#xe940;" glyph-name="microsoft" d="M0.35 448l-0.35 312.074 384 52.144v-364.218zM448 821.518l511.872 74.482v-448h-511.872zM959.998 384l-0.126-448-511.872 72.016v375.984zM384 16.164l-383.688 52.594-0.020 315.242h383.708z" />
<glyph unicode="&#xe941;" glyph-name="github" d="M512 960c-282.88 0-512-229.248-512-512 0-226.24 146.688-418.112 350.080-485.76 25.6-4.8 35.008 11.008 35.008 24.64 0 12.16-0.448 44.352-0.64 87.040-142.464-30.912-172.48 68.672-172.48 68.672-23.296 59.136-56.96 74.88-56.96 74.88-46.4 31.744 3.584 31.104 3.584 31.104 51.392-3.584 78.4-52.736 78.4-52.736 45.696-78.272 119.872-55.68 149.12-42.56 4.608 33.088 17.792 55.68 32.448 68.48-113.728 12.8-233.216 56.832-233.216 252.992 0 55.872 19.84 101.568 52.672 137.408-5.76 12.928-23.040 64.96 4.48 135.488 0 0 42.88 13.76 140.8-52.48 40.96 11.392 84.48 17.024 128 17.28 43.52-0.256 87.040-5.888 128-17.28 97.28 66.24 140.16 52.48 140.16 52.48 27.52-70.528 10.24-122.56 5.12-135.488 32.64-35.84 52.48-81.536 52.48-137.408 0-196.672-119.68-240-233.6-252.608 17.92-15.36 34.56-46.72 34.56-94.72 0-68.48-0.64-123.52-0.64-140.16 0-13.44 8.96-29.44 35.2-24.32 204.864 67.136 351.424 259.136 351.424 485.056 0 282.752-229.248 512-512 512z" />
<glyph unicode="&#xe950;" glyph-name="alarm, timeout" d="M512 832c-247.424 0-448-200.576-448-448s200.576-448 448-448 448 200.576 448 448-200.576 448-448 448zM512 24c-198.824 0-360 161.178-360 360 0 198.824 161.176 360 360 360 198.822 0 360-161.176 360-360 0-198.822-161.178-360-360-360zM934.784 672.826c16.042 28.052 25.216 60.542 25.216 95.174 0 106.040-85.96 192-192 192-61.818 0-116.802-29.222-151.92-74.596 131.884-27.236 245.206-105.198 318.704-212.578v0zM407.92 885.404c-35.116 45.374-90.102 74.596-151.92 74.596-106.040 0-192-85.96-192-192 0-34.632 9.174-67.122 25.216-95.174 73.5 107.38 186.822 185.342 318.704 212.578zM512 384v256h-64v-320h256v64z" />
<glyph unicode="&#xe942;" glyph-name="checkmark" d="M927.936 687.008l-68.288 68.288c-12.608 12.576-32.96 12.576-45.536 0l-409.44-409.44-194.752 196.16c-12.576 12.576-32.928 12.576-45.536 0l-68.288-68.288c-12.576-12.608-12.576-32.96 0-45.536l285.568-287.488c12.576-12.576 32.96-12.576 45.536 0l500.736 500.768c12.576 12.544 12.576 32.96 0 45.536z" />
<glyph unicode="&#xe943;" glyph-name="elapsed" d="M512.002 766.788v65.212h128v64c0 35.346-28.654 64-64.002 64h-191.998c-35.346 0-64-28.654-64-64v-64h128v-65.212c-214.798-16.338-384-195.802-384-414.788 0-229.75 186.25-416 416-416s416 186.25 416 416c0 218.984-169.202 398.448-384 414.788zM706.276 125.726c-60.442-60.44-140.798-93.726-226.274-93.726s-165.834 33.286-226.274 93.726c-60.44 60.44-93.726 140.8-93.726 226.274s33.286 165.834 93.726 226.274c58.040 58.038 134.448 91.018 216.114 93.548l-21.678-314.020c-1.86-26.29 12.464-37.802 31.836-37.802s33.698 11.512 31.836 37.802l-21.676 314.022c81.666-2.532 158.076-35.512 216.116-93.55 60.44-60.44 93.726-140.8 93.726-226.274s-33.286-165.834-93.726-226.274z" />
<glyph unicode="&#xe944;" glyph-name="timeout" d="M512 832c-247.424 0-448-200.576-448-448s200.576-448 448-448 448 200.576 448 448-200.576 448-448 448zM512 24c-198.824 0-360 161.178-360 360 0 198.824 161.176 360 360 360 198.822 0 360-161.176 360-360 0-198.822-161.178-360-360-360zM934.784 672.826c16.042 28.052 25.216 60.542 25.216 95.174 0 106.040-85.96 192-192 192-61.818 0-116.802-29.222-151.92-74.596 131.884-27.236 245.206-105.198 318.704-212.578v0zM407.92 885.404c-35.116 45.374-90.102 74.596-151.92 74.596-106.040 0-192-85.96-192-192 0-34.632 9.174-67.122 25.216-95.174 73.5 107.38 186.822 185.342 318.704 212.578zM512 384v256h-64v-320h256v64z" />
<glyph unicode="&#xe9ca;" glyph-name="earth" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512-0.002c-62.958 0-122.872 13.012-177.23 36.452l233.148 262.29c5.206 5.858 8.082 13.422 8.082 21.26v96c0 17.674-14.326 32-32 32-112.99 0-232.204 117.462-233.374 118.626-6 6.002-14.14 9.374-22.626 9.374h-128c-17.672 0-32-14.328-32-32v-192c0-12.122 6.848-23.202 17.69-28.622l110.31-55.156v-187.886c-116.052 80.956-192 215.432-192 367.664 0 68.714 15.49 133.806 43.138 192h116.862c8.488 0 16.626 3.372 22.628 9.372l128 128c6 6.002 9.372 14.14 9.372 22.628v77.412c40.562 12.074 83.518 18.588 128 18.588 70.406 0 137.004-16.26 196.282-45.2-4.144-3.502-8.176-7.164-12.046-11.036-36.266-36.264-56.236-84.478-56.236-135.764s19.97-99.5 56.236-135.764c36.434-36.432 85.218-56.264 135.634-56.26 3.166 0 6.342 0.080 9.518 0.236 13.814-51.802 38.752-186.656-8.404-372.334-0.444-1.744-0.696-3.488-0.842-5.224-81.324-83.080-194.7-134.656-320.142-134.656z" />
<glyph unicode="&#xea10;" glyph-name="checkmark" d="M864 832l-480-480-224 224-160-160 384-384 640 640z" />
<glyph unicode="&#xf00a;" glyph-name="grid" d="M292.571 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857z" />
<glyph unicode="&#xf0c9;" glyph-name="list" horiz-adv-x="878" d="M877.714 182.857v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 475.428v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 768v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 62 KiB

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.ttf

Binary file not shown.

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.woff

Binary file not shown.

1326
src/Squidex/app/theme/icomoon/selection.json

File diff suppressed because it is too large

220
src/Squidex/app/theme/icomoon/style.css

@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?5btl8m');
src: url('fonts/icomoon.eot?5btl8m#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?5btl8m') format('truetype'),
url('fonts/icomoon.woff?5btl8m') format('woff'),
url('fonts/icomoon.svg?5btl8m#icomoon') format('svg');
src: url('fonts/icomoon.eot?7injjp');
src: url('fonts/icomoon.eot?7injjp#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?7injjp') format('truetype'),
url('fonts/icomoon.woff?7injjp') format('woff'),
url('fonts/icomoon.svg?7injjp#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
@ -24,17 +24,23 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-alarm:before {
content: "\e950";
.icon-checkmark:before {
content: "\e942";
}
.icon-timeout:before {
content: "\e950";
.icon-control-Stars:before {
content: "\e93a";
}
.icon-browser:before {
content: "\e935";
}
.icon-earth:before {
content: "\e9ca";
}
.icon-checkmark:before {
content: "\ea10";
.icon-elapsed:before {
content: "\e943";
}
.icon-timeout:before {
content: "\e944";
}
.icon-microsoft:before {
content: "\e940";
@ -63,6 +69,51 @@
.icon-bin2:before {
content: "\e902";
}
.icon-grid:before {
content: "\f00a";
}
.icon-list:before {
content: "\f0c9";
}
.icon-bug:before {
content: "\e93d";
}
.icon-control-Markdown:before {
content: "\e938";
}
.icon-control-Date:before {
content: "\e936";
}
.icon-control-DateTime:before {
content: "\e937";
}
.icon-angle-right:before {
content: "\e931";
}
.icon-user-o:before {
content: "\e932";
}
.icon-caret-right:before {
content: "\e929";
}
.icon-caret-left:before {
content: "\e92a";
}
.icon-caret-up:before {
content: "\e92b";
}
.icon-caret-down:before {
content: "\e92c";
}
.icon-angle-up:before {
content: "\e903";
}
.icon-angle-down:before {
content: "\e900";
}
.icon-angle-left:before {
content: "\e901";
}
.icon-webhook:before {
content: "\e93f";
}
@ -216,51 +267,138 @@
.icon-control-RichText:before {
content: "\e939";
}
.icon-bug:before {
content: "\e93d";
.icon-close:before {
content: "\e908";
}
.icon-control-Markdown:before {
content: "\e938";
.icon-content:before {
content: "\e909";
}
.icon-control-Date:before {
content: "\e936";
.icon-type-References:before {
content: "\e909";
}
.icon-control-DateTime:before {
content: "\e937";
.icon-control-Checkbox:before {
content: "\e90a";
}
.icon-angle-right:before {
content: "\e931";
.icon-control-Dropdown:before {
content: "\e90b";
}
.icon-user-o:before {
content: "\e932";
.icon-control-Input:before {
content: "\e90c";
}
.icon-caret-right:before {
content: "\e929";
.icon-control-Radio:before {
content: "\e90d";
}
.icon-caret-left:before {
content: "\e92a";
.icon-control-TextArea:before {
content: "\e90e";
}
.icon-caret-up:before {
content: "\e92b";
.icon-control-Toggle:before {
content: "\e90f";
}
.icon-caret-down:before {
content: "\e92c";
.icon-copy:before {
content: "\e910";
}
.icon-angle-up:before {
content: "\e903";
.icon-dashboard:before {
content: "\e911";
}
.icon-angle-down:before {
content: "\e900";
.icon-delete:before {
content: "\e912";
}
.icon-angle-left:before {
content: "\e901";
.icon-bin:before {
content: "\e912";
}
.icon-delete-filled:before {
content: "\e913";
}
.icon-document-delete:before {
content: "\e914";
}
.icon-document-disable:before {
content: "\e915";
}
.icon-document-publish:before {
content: "\e916";
}
.icon-drag:before {
content: "\e917";
}
.icon-filter:before {
content: "\e918";
}
.icon-help:before {
content: "\e919";
}
.icon-type-Json:before {
content: "\e91a";
}
.icon-json:before {
content: "\e91a";
}
.icon-location:before {
content: "\e91b";
}
.icon-control-Map:before {
content: "\e91b";
}
.icon-type-Geolocation:before {
content: "\e91b";
}
.icon-logo:before {
content: "\e91c";
}
.icon-media:before {
content: "\e91d";
}
.icon-type-Assets:before {
content: "\e91d";
}
.icon-more:before {
content: "\e91e";
}
.icon-dots:before {
content: "\e91e";
}
.icon-pencil:before {
content: "\e91f";
}
.icon-reference:before {
content: "\e920";
}
.icon-schemas:before {
content: "\e921";
}
.icon-search:before {
content: "\e922";
}
.icon-settings:before {
content: "\e923";
}
.icon-type-Boolean:before {
content: "\e924";
}
.icon-type-DateTime:before {
content: "\e925";
}
.icon-type-Number:before {
content: "\e926";
}
.icon-type-String:before {
content: "\e927";
}
.icon-user:before {
content: "\e928";
}
.icon-download:before {
content: "\e93e";
}
.icon-control-RichText:before {
content: "\e939";
}
.icon-info:before {
content: "\e93c";
}
.icon-control-Stars:before {
content: "\e93a";
.icon-info:before {
content: "\e93c";
}
.icon-browser:before {
content: "\e935";
.icon-info:before {
content: "\e93c";
}

Loading…
Cancel
Save