Browse Source

Closes #57, Collect Statistics about Webhooks

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
b08534d827
  1. 1
      Squidex.sln.DotSettings
  2. 2
      src/Squidex.Core/ContentValidator.cs
  3. 2
      src/Squidex.Infrastructure/CQRS/Commands/CommandContext.cs
  4. 96
      src/Squidex.Infrastructure/Http/DumpFormatter.cs
  5. 6
      src/Squidex.Infrastructure/PropertyValue.cs
  6. 4
      src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs
  7. 30
      src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookEntity.cs
  8. 87
      src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookRepository.cs
  9. 68
      src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookRepository_EventHandling.cs
  10. 13
      src/Squidex.Read/Schemas/ISchemaWebhookEntity.cs
  11. 21
      src/Squidex.Read/Schemas/ISchemaWebhookUrlEntity.cs
  12. 4
      src/Squidex.Read/Schemas/Repositories/ISchemaWebhookRepository.cs
  13. 2
      src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs
  14. 101
      src/Squidex.Read/Schemas/WebhookInvoker.cs
  15. 17
      src/Squidex.Read/Schemas/WebhookResult.cs
  16. 27
      src/Squidex/Controllers/Api/Webhooks/Models/WebhookCreatedDto.cs
  17. 27
      src/Squidex/Controllers/Api/Webhooks/Models/WebhookDto.cs
  18. 12
      src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs
  19. 44
      src/Squidex/app/features/webhooks/pages/webhooks-page.component.html
  20. 30
      src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss
  21. 15
      src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts
  22. 30
      src/Squidex/app/shared/services/webhooks.service.spec.ts
  23. 30
      src/Squidex/app/shared/services/webhooks.service.ts
  24. 440
      src/Squidex/app/theme/icomoon/demo.html
  25. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.eot
  26. 3
      src/Squidex/app/theme/icomoon/fonts/icomoon.svg
  27. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.ttf
  28. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.woff
  29. 912
      src/Squidex/app/theme/icomoon/selection.json
  30. 73
      src/Squidex/app/theme/icomoon/style.css

1
Squidex.sln.DotSettings

@ -14,6 +14,7 @@
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ets/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_005Ftest_002Doutput_002A/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_005Ftest_002Doutput_002A/@EntryIndexRemoved">True</s:Boolean>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeAccessorOwnerBody/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=AutoPropertyCanBeMadeGetOnly_002EGlobal/@EntryIndexedValue"></s:String>
<s:Boolean x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=AutoPropertyCanBeMadeGetOnly_002EGlobal/@EntryIndexRemoved">True</s:Boolean>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ClassNeverInstantiated_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>

2
src/Squidex.Core/ContentValidator.cs

@ -109,7 +109,7 @@ namespace Squidex.Core
foreach (var partitionValues in fieldData)
{
if (!partition.TryGetItem(partitionValues.Key, out var partitionItem))
if (!partition.TryGetItem(partitionValues.Key, out var _))
{
errors.AddError($"<FIELD> has an unsupported {partitioning.Key} value '{partitionValues.Key}'", field);
}

2
src/Squidex.Infrastructure/CQRS/Commands/CommandContext.cs

@ -76,7 +76,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
public T Result<T>()
{
return result != null ? (T)result.Item1 : default(T);
return (T)result?.Item1;
}
}
}

96
src/Squidex.Infrastructure/Http/DumpFormatter.cs

@ -0,0 +1,96 @@
// ==========================================================================
// DumpFormatter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
// ReSharper disable InvertIf
namespace Squidex.Infrastructure.Http
{
public static class DumpFormatter
{
public static string BuildDump(HttpRequestMessage request, HttpResponseMessage response, string requestBody, string responseBody, TimeSpan elapsed)
{
var writer = new StringBuilder();
writer.AppendLine("Request:");
writer.AppendRequest(request, requestBody);
writer.AppendLine();
writer.AppendLine();
writer.AppendLine("Response:");
writer.AppendResponse(response, responseBody, elapsed);
return writer.ToString();
}
private static void AppendRequest(this StringBuilder writer, HttpRequestMessage request, string requestBody)
{
var method = request.Method.ToString().ToUpperInvariant();
writer.AppendLine($"{method}: {request.RequestUri} HTTP/{request.Version}");
writer.AppendHeaders(request.Headers);
writer.AppendHeaders(request.Content?.Headers);
if (!string.IsNullOrWhiteSpace(requestBody))
{
writer.AppendLine();
writer.AppendLine(requestBody);
}
}
private static void AppendResponse(this StringBuilder writer, HttpResponseMessage response, string responseBody, TimeSpan elapsed)
{
if (response != null)
{
var responseCode = (int)response.StatusCode;
var responseText = Enum.GetName(typeof(HttpStatusCode), response.StatusCode);
writer.AppendLine($"HTTP/{response.Version} {responseCode} {responseText}");
writer.AppendHeaders(response.Headers);
writer.AppendHeaders(response.Content?.Headers);
if (!string.IsNullOrWhiteSpace(responseBody))
{
writer.AppendLine();
writer.AppendLine(responseBody);
}
writer.AppendLine();
writer.AppendLine($"Elapsed: {elapsed}");
}
else
{
writer.AppendLine($"Timeout after {elapsed}");
}
}
private static void AppendHeaders(this StringBuilder writer, HttpHeaders headers)
{
if (headers == null)
{
return;
}
foreach (var header in headers)
{
writer.Append(header.Key);
writer.Append(": ");
writer.Append(string.Join("; ", header.Value));
writer.AppendLine();
}
}
}
}

6
src/Squidex.Infrastructure/PropertyValue.cs

@ -192,13 +192,13 @@ namespace Squidex.Infrastructure
}
catch (OverflowException)
{
string message = $"The property has type '{valueType}' and cannot be casted to '{requestedType}' because it is either too small or large.";
var message = $"The property has type '{valueType}' and cannot be casted to '{requestedType}' because it is either too small or large.";
throw new InvalidCastException(message);
}
catch (InvalidCastException)
{
string message = $"The property has type '{valueType}' and cannot be casted to '{requestedType}'.";
var message = $"The property has type '{valueType}' and cannot be casted to '{requestedType}'.";
throw new InvalidCastException(message);
}
@ -214,7 +214,7 @@ namespace Squidex.Infrastructure
}
catch (Exception ex)
{
string message = $"The property has type '{valueType}' and cannot be casted to '{requestedType}'.";
var message = $"The property has type '{valueType}' and cannot be casted to '{requestedType}'.";
throw new InvalidCastException(message, ex);
}

4
src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs

@ -26,7 +26,7 @@ namespace Squidex.Infrastructure.Reflection
}
else
{
getMethod = x => { throw new NotSupportedException(); };
getMethod = x => throw new NotSupportedException();
}
if (propertyInfo.CanWrite)
@ -35,7 +35,7 @@ namespace Squidex.Infrastructure.Reflection
}
else
{
setMethod = (x, y) => { throw new NotSupportedException(); };
setMethod = (x, y) => throw new NotSupportedException();
}
}

30
src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookEntity.cs

@ -7,6 +7,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Read.Schemas;
@ -24,16 +25,41 @@ namespace Squidex.Read.MongoDb.Schemas
[BsonElement]
public Uri Url { get; set; }
[BsonRequired]
[BsonElement]
public Guid AppId { get; set; }
[BsonRequired]
[BsonElement]
public Guid SchemaId { get; set; }
[BsonRequired]
[BsonElement]
public string SharedSecret { get; set; }
[BsonRequired]
[BsonElement]
public Guid AppId { get; set; }
public long TotalSucceeded { get; set; }
[BsonRequired]
[BsonElement]
public Guid SchemaId { get; set; }
public long TotalFailed { get; set; }
[BsonRequired]
[BsonElement]
public long TotalTimedout { get; set; }
[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; }
}
}
}

87
src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookRepository.cs

@ -13,31 +13,31 @@ using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
using Squidex.Read.Schemas;
using Squidex.Read.Schemas.Repositories;
// ReSharper disable SwitchStatementMissingSomeCases
namespace Squidex.Read.MongoDb.Schemas
{
public class MongoSchemaWebhookRepository : MongoRepositoryBase<MongoSchemaWebhookEntity>, ISchemaWebhookRepository, IEventConsumer
public partial class MongoSchemaWebhookRepository : MongoRepositoryBase<MongoSchemaWebhookEntity>, ISchemaWebhookRepository, IEventConsumer
{
private static readonly List<ISchemaWebhookEntity> EmptyWebhooks = new List<ISchemaWebhookEntity>();
private Dictionary<Guid, List<MongoSchemaWebhookEntity>> inMemoryWebhooks;
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);
public string Name
public sealed class ShortInfo : ISchemaWebhookUrlEntity
{
get { return GetType().Name; }
}
public Guid Id { get; set; }
public string EventsFilter
{
get { return "^schema-"; }
public Uri Url { get; set; }
public string SharedSecret { get; set; }
}
public MongoSchemaWebhookRepository(IMongoDatabase database)
@ -55,45 +55,47 @@ namespace Squidex.Read.MongoDb.Schemas
return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.SchemaId));
}
public Task On(Envelope<IEvent> @event)
{
return this.DispatchActionAsync(@event.Payload, @event.Headers);
}
protected async Task On(WebhookAdded @event, EnvelopeHeaders headers)
public async Task<IReadOnlyList<ISchemaWebhookEntity>> QueryByAppAsync(Guid appId)
{
await EnsureWebooksLoadedAsync();
var webhook = SimpleMapper.Map(@event, new MongoSchemaWebhookEntity { AppId = @event.AppId.Id, SchemaId = @event.SchemaId.Id });
inMemoryWebhooks.GetOrAddNew(webhook.AppId).Add(webhook);
await Collection.InsertOneAsync(webhook);
return await Collection.Find(x => x.AppId == appId).ToListAsync();
}
protected async Task On(WebhookDeleted @event, EnvelopeHeaders headers)
public async Task<IReadOnlyList<ISchemaWebhookUrlEntity>> QueryUrlsBySchemaAsync(Guid appId, Guid schemaId)
{
await EnsureWebooksLoadedAsync();
inMemoryWebhooks.GetOrDefault(@event.AppId.Id)?.RemoveAll(w => w.Id == @event.Id);
await Collection.DeleteManyAsync(x => x.Id == @event.Id);
return inMemoryWebhooks.GetOrDefault(appId)?.GetOrDefault(schemaId)?.ToList() ?? EmptyWebhooks;
}
protected async Task On(SchemaDeleted @event, EnvelopeHeaders headers)
public async Task AddInvokationAsync(Guid webhookId, string dump, WebhookResult result, TimeSpan elapsed)
{
await EnsureWebooksLoadedAsync();
var webhookEntity = await Collection.Find(x => x.Id == webhookId).FirstOrDefaultAsync();
inMemoryWebhooks.GetOrDefault(@event.AppId.Id)?.RemoveAll(w => w.SchemaId == @event.SchemaId.Id);
if (webhookEntity != null)
{
switch (result)
{
case WebhookResult.Success:
webhookEntity.TotalSucceeded++;
break;
case WebhookResult.Fail:
webhookEntity.TotalFailed++;
break;
case WebhookResult.Timeout:
webhookEntity.TotalTimedout++;
break;
}
await Collection.DeleteManyAsync(x => x.SchemaId == @event.SchemaId.Id);
}
webhookEntity.TotalRequestTime += (long)elapsed.TotalMilliseconds;
webhookEntity.LastDumps.Insert(0, dump);
public async Task<IReadOnlyList<ISchemaWebhookEntity>> QueryByAppAsync(Guid appId)
{
await EnsureWebooksLoadedAsync();
while (webhookEntity.LastDumps.Count > MaxDumps)
{
webhookEntity.LastDumps.RemoveAt(webhookEntity.LastDumps.Count - 1);
}
return inMemoryWebhooks.GetOrDefault(appId)?.OfType<ISchemaWebhookEntity>()?.ToList() ?? EmptyWebhooks;
await Collection.ReplaceOneAsync(x => x.Id == webhookId, webhookEntity);
}
}
private async Task EnsureWebooksLoadedAsync()
@ -106,9 +108,18 @@ namespace Squidex.Read.MongoDb.Schemas
if (inMemoryWebhooks == null)
{
var result = new Dictionary<Guid, Dictionary<Guid, List<ShortInfo>>>();
var webhooks = await Collection.Find(new BsonDocument()).ToListAsync();
inMemoryWebhooks = webhooks.GroupBy(x => x.AppId).ToDictionary(x => x.Key, x => x.ToList());
foreach (var webhook in webhooks)
{
var list = result.GetOrAddNew(webhook.AppId).GetOrAddNew(webhook.SchemaId);
list.Add(SimpleMapper.Map(webhook, new ShortInfo()));
}
inMemoryWebhooks = result;
}
}
finally

68
src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookRepository_EventHandling.cs

@ -0,0 +1,68 @@
// ==========================================================================
// MongoSchemaWebhookRepository_EventHandling.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Read.MongoDb.Schemas
{
public partial class MongoSchemaWebhookRepository
{
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return "^schema-"; }
}
public Task On(Envelope<IEvent> @event)
{
return this.DispatchActionAsync(@event.Payload, @event.Headers);
}
protected async Task On(WebhookAdded @event, EnvelopeHeaders headers)
{
await EnsureWebooksLoadedAsync();
var theAppId = @event.AppId.Id;
var theSchemaId = @event.SchemaId.Id;
var webhook = SimpleMapper.Map(@event, new MongoSchemaWebhookEntity { AppId = theAppId, SchemaId = theSchemaId });
inMemoryWebhooks.GetOrAddNew(theAppId).GetOrAddNew(theSchemaId).Add(SimpleMapper.Map(@event, new ShortInfo()));
await Collection.InsertOneAsync(webhook);
}
protected async Task On(WebhookDeleted @event, EnvelopeHeaders headers)
{
await EnsureWebooksLoadedAsync();
inMemoryWebhooks.GetOrDefault(@event.AppId.Id)?.Remove(@event.SchemaId.Id);
await Collection.DeleteManyAsync(x => x.Id == @event.Id);
}
protected async Task On(SchemaDeleted @event, EnvelopeHeaders headers)
{
await EnsureWebooksLoadedAsync();
inMemoryWebhooks.GetOrDefault(@event.AppId.Id)?.Remove(@event.SchemaId.Id);
await Collection.DeleteManyAsync(x => x.SchemaId == @event.SchemaId.Id);
}
}
}

13
src/Squidex.Read/Schemas/ISchemaWebhookEntity.cs

@ -7,17 +7,22 @@
// ==========================================================================
using System;
using System.Collections.Generic;
namespace Squidex.Read.Schemas
{
public interface ISchemaWebhookEntity
public interface ISchemaWebhookEntity : ISchemaWebhookUrlEntity
{
Guid SchemaId { get; }
Guid Id { get; }
long TotalSucceeded { get; }
Uri Url { get; }
long TotalFailed { get; }
string SharedSecret { get; }
long TotalTimedout { get; }
long TotalRequestTime { get; }
IEnumerable<string> LastDumps { get; }
}
}

21
src/Squidex.Read/Schemas/ISchemaWebhookUrlEntity.cs

@ -0,0 +1,21 @@
// ==========================================================================
// ISchemaWebhookUrlEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Read.Schemas
{
public interface ISchemaWebhookUrlEntity
{
Guid Id { get; }
Uri Url { get; }
string SharedSecret { get; }
}
}

4
src/Squidex.Read/Schemas/Repositories/ISchemaWebhookRepository.cs

@ -14,6 +14,10 @@ namespace Squidex.Read.Schemas.Repositories
{
public interface ISchemaWebhookRepository
{
Task AddInvokationAsync(Guid webhookId, string dump, WebhookResult result, TimeSpan elapsed);
Task<IReadOnlyList<ISchemaWebhookUrlEntity>> QueryUrlsBySchemaAsync(Guid appId, Guid schemaId);
Task<IReadOnlyList<ISchemaWebhookEntity>> QueryByAppAsync(Guid appId);
}
}

2
src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs

@ -69,7 +69,7 @@ namespace Squidex.Read.Schemas
{
if (@event.Payload is SchemaEvent schemaEvent)
{
string channel = $"schemas.{schemaEvent.SchemaId.Name}";
var channel = $"schemas.{schemaEvent.SchemaId.Name}";
var result = ForEvent(@event.Payload, channel).AddParameter("Name", schemaEvent.SchemaId.Name);

101
src/Squidex.Read/Schemas/WebhookInvoker.cs

@ -7,7 +7,7 @@
// ==========================================================================
using System;
using System.Linq;
using System.Diagnostics;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
@ -17,6 +17,7 @@ using NodaTime;
using Squidex.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Http;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
using Squidex.Read.Schemas.Repositories;
@ -62,15 +63,13 @@ namespace Squidex.Read.Schemas
{
if (@event.Payload is ContentEvent contentEvent)
{
var hooks = await webhookRepository.QueryByAppAsync(contentEvent.AppId.Id);
var schemaHooks = hooks.Where(x => x.SchemaId == contentEvent.SchemaId.Id).ToList();
if (schemaHooks.Count > 0)
var hooks = await webhookRepository.QueryUrlsBySchemaAsync(contentEvent.AppId.Id, contentEvent.SchemaId.Id);
if (hooks.Count > 0)
{
var payload = CreatePayload(@event);
foreach (var hook in schemaHooks)
foreach (var hook in hooks)
{
DispatchEventAsync(payload, hook, @event.Headers.Timestamp()).Forget();
}
@ -82,39 +81,70 @@ namespace Squidex.Read.Schemas
{
return new JObject(
new JProperty("type", @event.Payload.GetType().Name),
new JProperty("meta", JObject.FromObject(@event.Headers, webhookSerializer)),
new JProperty("data", JObject.FromObject(@event.Payload, webhookSerializer)));
new JProperty("payload", JObject.FromObject(@event.Payload, webhookSerializer)),
new JProperty("timestamp", @event.Headers.Timestamp().ToString()));
}
private async Task DispatchEventAsync(JObject payload, ISchemaWebhookEntity webhook, Instant instant)
private async Task DispatchEventAsync(JObject payload, ISchemaWebhookUrlEntity webhook, Instant instant)
{
try
{
using (log.MeasureInformation(w => w
.WriteProperty("Action", "SendToHook")
.WriteProperty("Status", "Invoked")))
{
var signature = $"{instant.ToUnixTimeSeconds()}{webhook.SharedSecret}".Sha256Base64();
payload = SignPayload(payload, webhook, instant);
payload["Signature"] = signature;
var requestString = payload.ToString(Formatting.Indented);
var responseString = string.Empty;
using (var client = new HttpClient())
{
client.Timeout = Timeout;
var request = BuildRequest(requestString, webhook);
var response = (HttpResponseMessage)null;
var message = new HttpRequestMessage(HttpMethod.Post, webhook.Url)
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 })
{
Content = new StringContent(payload.ToString(), Encoding.UTF8, "application/json")
};
response = await client.SendAsync(request);
}
}
}
catch (TimeoutException)
{
isTimeout = true;
}
catch (OperationCanceledException)
{
isTimeout = true;
}
finally
{
watch.Stop();
}
message.Headers.TryAddWithoutValidation("X-Signature", signature);
message.Headers.Add("User-Agent", "Squidex");
if (response != null)
{
responseString = await response.Content.ReadAsStringAsync();
}
var response = await client.SendAsync(message);
var dump = DumpFormatter.BuildDump(request, response, requestString, responseString, watch.Elapsed);
response.EnsureSuccessStatusCode();
}
var result = WebhookResult.Fail;
if (isTimeout)
{
result = WebhookResult.Timeout;
}
else if (response?.IsSuccessStatusCode == true)
{
result = WebhookResult.Success;
}
await webhookRepository.AddInvokationAsync(webhook.Id, dump, result, watch.Elapsed);
}
catch (Exception ex)
{
@ -123,5 +153,22 @@ namespace Squidex.Read.Schemas
.WriteProperty("Status", "Failed"));
}
}
private static JObject SignPayload(JObject payload, ISchemaWebhookUrlEntity webhook, Instant instant)
{
payload["signature"] = $"{instant.ToUnixTimeSeconds()}{webhook.SharedSecret}".Sha256Base64();
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;
}
}
}

17
src/Squidex.Read/Schemas/WebhookResult.cs

@ -0,0 +1,17 @@
// ==========================================================================
// WebhookResult.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Read.Schemas
{
public enum WebhookResult
{
Success,
Fail,
Timeout
}
}

27
src/Squidex/Controllers/Api/Webhooks/Models/WebhookCreatedDto.cs

@ -0,0 +1,27 @@
// ==========================================================================
// WebhookCreatedDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Webhooks.Models
{
public class WebhookCreatedDto
{
/// <summary>
/// The id of the webhook.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The shared secret that is used to calculate the signature.
/// </summary>
[Required]
public string SharedSecret { get; set; }
}
}

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

@ -7,6 +7,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Webhooks.Models
@ -23,6 +24,26 @@ namespace Squidex.Controllers.Api.Webhooks.Models
/// </summary>
public Guid SchemaId { get; set; }
/// <summary>
/// The number of succceeded calls.
/// </summary>
public long TotalSucceeded { get; set; }
/// <summary>
/// The number of failed calls.
/// </summary>
public long TotalFailed { get; set; }
/// <summary>
/// The number of timedout calls.
/// </summary>
public long TotalTimedout { get; set; }
/// <summary>
/// The average request time in milliseconds.
/// </summary>
public long AverageRequestTimeMs { get; set; }
/// <summary>
/// The url of the webhook.
/// </summary>
@ -34,5 +55,11 @@ 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; }
}
}

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

@ -56,7 +56,13 @@ namespace Squidex.Controllers.Api.Webhooks
Response.Headers["ETag"] = new StringValues(App.Version.ToString());
var response = webhooks.Select(w => SimpleMapper.Map(w, new WebhookDto()));
var response = webhooks.Select(w =>
{
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 Ok(response);
}
@ -78,7 +84,7 @@ namespace Squidex.Controllers.Api.Webhooks
/// </remarks>
[HttpPost]
[Route("apps/{app}/schemas/{name}/webhooks/")]
[ProducesResponseType(typeof(WebhookDto), 201)]
[ProducesResponseType(typeof(WebhookCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ApiCosts(1)]
@ -88,7 +94,7 @@ namespace Squidex.Controllers.Api.Webhooks
await CommandBus.PublishAsync(command);
return CreatedAtAction(nameof(GetWebhooks), new { app }, SimpleMapper.Map(command, new WebhookDto()));
return CreatedAtAction(nameof(GetWebhooks), new { app }, SimpleMapper.Map(command, new WebhookCreatedDto()));
}
/// <summary>

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

@ -26,7 +26,7 @@
</div>
<div *ngIf="webhooks">
<div *ngFor="let webhook of webhooks">
<div *ngFor="let w of webhooks">
<div class="table-items-row">
<table class="table table-middle table-sm table-borderless table-fixed">
<colgroup>
@ -38,7 +38,7 @@
<tr>
<td colspan="2">
<h3 class="client-name">
Schema: {{webhook.schema.name}}
Schema: {{w.schema.name}}
</h3>
</td>
<td class="client-delete">
@ -50,7 +50,7 @@
<tr>
<td>Url:</td>
<td>
<input readonly class="form-control" [attr.value]="webhook.webhook.url" #inputUrl />
<input readonly class="form-control" [attr.value]="w.webhook.url" #inputUrl />
</td>
<td>
<button type="button" class="btn btn-primary btn-link" [sqxCopy]="inputUrl">
@ -61,7 +61,7 @@
<tr>
<td>Secret:</td>
<td>
<input readonly class="form-control" [attr.value]="webhook.webhook.sharedSecret" #inputSecret />
<input readonly class="form-control" [attr.value]="w.webhook.sharedSecret" #inputSecret />
</td>
<td>
<button type="button" class="btn btn-primary btn-link" [sqxCopy]="inputSecret">
@ -70,6 +70,42 @@
</td>
</tr>
</table>
<div class="webhook-stats" *ngIf="w.webhook.lastDumps.length > 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>
<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>
</div>
<div *ngIf="w.showDetails" class="webhook-dumps">
<textarea class="form-control webhook-dump" readonly>{{w.webhook.lastDumps[0]}}</textarea>
</div>
</div>
</div>
</div>

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

@ -3,4 +3,34 @@
.schemas-control {
width: 10rem;
}
.failed {
color: $color-theme-error;
}
.success {
color: $color-theme-green;
}
.webhook {
&-stats {
border-top: 1px solid $color-border;
margin-left: -1.25rem;
margin-right: -1.25rem;
padding: 1rem 1rem 0;
}
&-detail-link {
color: $color-theme-blue !important;
cursor: pointer;
font-size: .9rem;
}
&-dump {
margin-top: 1rem;
font-size: .8rem;
font-weight: normal;
height: 20rem;
}
}

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

@ -21,7 +21,7 @@ import {
WebhooksService
} from 'shared';
interface WebhookWithSchema { webhook: WebhookDto; schema: SchemaDto; };
interface WebhookWithSchema { webhook: WebhookDto; schema: SchemaDto; showDetails: boolean; };
@Component({
selector: 'sqx-webhooks-page',
@ -68,7 +68,7 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app =>
this.schemasService.getSchemas(app)
.withLatestFrom(this.webhooksService.getWebhooks(app),
.combineLatest(this.webhooksService.getWebhooks(app),
(s, w) => { return { webhooks: w, schemas: s }; }))
.subscribe(dtos => {
this.schemas = dtos.schemas;
@ -76,7 +76,7 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
this.webhooks =
ImmutableArray.of(
dtos.webhooks.map(w => {
return { webhook: w, schema: dtos.schemas.find(s => s.id === w.schemaId) };
return { webhook: w, schema: dtos.schemas.find(s => s.id === w.schemaId), showDetails: false };
}).filter(w => !!w.schema));
if (showInfo) {
@ -86,7 +86,6 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
this.notifyError(error);
});
}
public resetWebhookForm() {
this.addWebhookFormSubmitted = false;
this.addWebhookForm.enable();
@ -105,7 +104,9 @@ 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({ schema, webhook: dto });
const webhook = new WebhookDto(dto.id, schemaId, dto.sharedSecret, requestDto.url, 0, 0, 0, 0, []);
this.webhooks = this.webhooks.push({ schema, webhook, showDetails: false });
this.resetWebhookForm();
}, error => {
this.notifyError(error);
@ -114,6 +115,10 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
}
}
public toggleDetails(webhook: WebhookWithSchema) {
this.webhooks = this.webhooks.replace(webhook, { webhook: webhook.webhook, schema: webhook.schema, showDetails: !webhook.showDetails });
}
public deleteWebhook(webhook: WebhookWithSchema) {
this.appNameOnce()
.switchMap(app => this.webhooksService.deleteWebhook(app, webhook.schema.name, webhook.webhook.id, this.version))

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

@ -14,6 +14,7 @@ import {
AuthService,
CreateWebhookDto,
Version,
WebhookCreatedDto,
WebhookDto,
WebhooksService
} from './../';
@ -37,12 +38,22 @@ describe('WebhooksService', () => {
id: 'id1',
schemaId: 'schemaId1',
sharedSecret: 'token1',
url: 'http://squidex.io/1'
url: 'http://squidex.io/1',
totalSucceeded: 1,
totalFailed: 2,
totalTimedout: 3,
averageRequestTimeMs: 4,
dumps: ['dump1']
}, {
id: 'id2',
schemaId: 'schemaId2',
sharedSecret: 'token2',
url: 'http://squidex.io/2'
url: 'http://squidex.io/2',
totalSucceeded: 5,
totalFailed: 6,
totalTimedout: 7,
averageRequestTimeMs: 8,
dumps: ['dump2']
}]
})
)
@ -56,8 +67,8 @@ describe('WebhooksService', () => {
}).unsubscribe();
expect(webhooks).toEqual([
new WebhookDto('id1', 'schemaId1', 'token1', 'http://squidex.io/1'),
new WebhookDto('id2', 'schemaId2', 'token2', 'http://squidex.io/2')
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'])
]);
authService.verifyAll();
@ -70,24 +81,19 @@ describe('WebhooksService', () => {
.returns(() => Observable.of(
new Response(
new ResponseOptions({
body: {
id: 'id1',
schemaId: 'schemaId1',
sharedSecret: 'token1',
url: 'http://squidex.io/1'
}
body: { id: 'id1', sharedSecret: 'token1' }
})
)
))
.verifiable(Times.once());
let webhook: WebhookDto | null = null;
let webhook: WebhookCreatedDto | 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'));
expect(webhook).toEqual(new WebhookCreatedDto('id1', 'token1'));
authService.verifyAll();
});

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

@ -18,7 +18,20 @@ export class WebhookDto {
public readonly id: string,
public readonly schemaId: string,
public readonly sharedSecret: string,
public readonly url: string
public readonly url: string,
public readonly totalSucceeded: number,
public readonly totalFailed: number,
public readonly totalTimedout: number,
public readonly averageRequestTimeMs: number,
public readonly lastDumps: string[]
) {
}
}
export class WebhookCreatedDto {
constructor(
public readonly id: string,
public readonly sharedSecret: string
) {
}
}
@ -51,23 +64,26 @@ export class WebhooksService {
item.id,
item.schemaId,
item.sharedSecret,
item.url);
item.url,
item.totalSucceeded,
item.totalFailed,
item.totalTimedout,
item.averageRequestTimeMs,
item.lastDumps);
});
})
.catchError('Failed to load webhooks. Please reload.');
}
public postWebhook(appName: string, schemaName: string, dto: CreateWebhookDto, version?: Version): Observable<WebhookDto> {
public postWebhook(appName: string, schemaName: string, dto: CreateWebhookDto, version?: Version): Observable<WebhookCreatedDto> {
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(
return new WebhookCreatedDto(
response.id,
response.schemaId,
response.sharedSecret,
response.url);
response.sharedSecret);
})
.catchError('Failed to create webhook. Please reload.');
}

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

@ -9,11 +9,206 @@
<link rel="stylesheet" href="style.css"></head>
<body>
<div class="bgc1 clearfix">
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;66)</small></h1>
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;69)</small></h1>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: Unknown</h1>
<h1 class="mvm mtn fgc1">Grid Size: 16</h1>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-elapsed">
</span>
<span class="mls"> icon-elapsed</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e943" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe943;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-timeout">
</span>
<span class="mls"> icon-timeout</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e944" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe944;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-checkmark">
</span>
<span class="mls"> icon-checkmark</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e942" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe942;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-microsoft">
</span>
<span class="mls"> icon-microsoft</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e940" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe940;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-google">
</span>
<span class="mls"> icon-google</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e93b" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe93b;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-unlocked">
</span>
<span class="mls"> icon-unlocked</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e933" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe933;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-lock">
</span>
<span class="mls"> icon-lock</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e934" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe934;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-reset">
</span>
<span class="mls"> icon-reset</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e92e" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe92e;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-pause">
</span>
<span class="mls"> icon-pause</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e92f" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe92f;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-play">
</span>
<span class="mls"> icon-play</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e930" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe930;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-settings2">
</span>
<span class="mls"> icon-settings2</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e92d" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe92d;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-bin2">
</span>
<span class="mls"> icon-bin2</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e902" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe902;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: Unknown</h1>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-webhook">
@ -29,7 +224,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-github">
@ -45,7 +240,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-activity">
@ -61,7 +256,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-history">
@ -77,7 +272,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-time">
@ -93,7 +288,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-add">
@ -109,7 +304,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-plus">
@ -125,7 +320,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-check-circle">
@ -141,7 +336,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-check-circle-filled">
@ -157,7 +352,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-close">
@ -173,7 +368,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-content">
@ -189,7 +384,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-Checkbox">
@ -205,7 +400,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-Dropdown">
@ -221,7 +416,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-Input">
@ -237,7 +432,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-Radio">
@ -253,7 +448,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-TextArea">
@ -269,7 +464,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-Toggle">
@ -285,7 +480,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-copy">
@ -301,7 +496,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-dashboard">
@ -317,7 +512,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-delete">
@ -333,7 +528,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-bin">
@ -349,7 +544,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-delete-filled">
@ -365,7 +560,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-document-delete">
@ -381,7 +576,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-document-disable">
@ -397,7 +592,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-document-publish">
@ -413,7 +608,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-drag">
@ -429,7 +624,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-filter">
@ -445,7 +640,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-help">
@ -461,7 +656,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-type-Json">
@ -477,7 +672,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-json">
@ -493,7 +688,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-location">
@ -509,7 +704,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-Map">
@ -525,7 +720,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-type-Geolocation">
@ -541,7 +736,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-logo">
@ -557,7 +752,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-media">
@ -573,7 +768,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-type-Assets">
@ -589,7 +784,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-more">
@ -605,7 +800,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-dots">
@ -621,7 +816,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-pencil">
@ -637,7 +832,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-reference">
@ -653,7 +848,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-schemas">
@ -669,7 +864,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-search">
@ -685,7 +880,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-settings">
@ -701,7 +896,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-type-Boolean">
@ -717,7 +912,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-type-DateTime">
@ -733,7 +928,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-type-Number">
@ -749,7 +944,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-type-String">
@ -765,7 +960,7 @@
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-user">
@ -782,153 +977,6 @@
</div>
</div>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 16</h1>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-microsoft">
</span>
<span class="mls"> icon-microsoft</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e940" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe940;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-google">
</span>
<span class="mls"> icon-google</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e93b" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe93b;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-unlocked">
</span>
<span class="mls"> icon-unlocked</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e933" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe933;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-lock">
</span>
<span class="mls"> icon-lock</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e934" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe934;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-reset">
</span>
<span class="mls"> icon-reset</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e92e" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe92e;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-pause">
</span>
<span class="mls"> icon-pause</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e92f" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe92f;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-play">
</span>
<span class="mls"> icon-play</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e930" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe930;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-settings2">
</span>
<span class="mls"> icon-settings2</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e92d" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe92d;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-bin2">
</span>
<span class="mls"> icon-bin2</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e902" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe902;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 24</h1>
<div class="glyph fs3">

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

Binary file not shown.

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

@ -73,4 +73,7 @@
<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="&#xe942;" glyph-name="checkmark" d="M864 832l-480-480-224 224-160-160 384-384 640 640z" />
<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" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 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.

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

File diff suppressed because it is too large

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

@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?rfek6n');
src: url('fonts/icomoon.eot?rfek6n#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?rfek6n') format('truetype'),
url('fonts/icomoon.woff?rfek6n') format('woff'),
url('fonts/icomoon.svg?rfek6n#icomoon') format('svg');
src: url('fonts/icomoon.eot?5o132k');
src: url('fonts/icomoon.eot?5o132k#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?5o132k') format('truetype'),
url('fonts/icomoon.woff?5o132k') format('woff'),
url('fonts/icomoon.svg?5o132k#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
@ -24,6 +24,42 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-elapsed:before {
content: "\e943";
}
.icon-timeout:before {
content: "\e944";
}
.icon-checkmark:before {
content: "\e942";
}
.icon-microsoft:before {
content: "\e940";
}
.icon-google:before {
content: "\e93b";
}
.icon-unlocked:before {
content: "\e933";
}
.icon-lock:before {
content: "\e934";
}
.icon-reset:before {
content: "\e92e";
}
.icon-pause:before {
content: "\e92f";
}
.icon-play:before {
content: "\e930";
}
.icon-settings2:before {
content: "\e92d";
}
.icon-bin2:before {
content: "\e902";
}
.icon-webhook:before {
content: "\e93f";
}
@ -168,33 +204,6 @@
.icon-user:before {
content: "\e928";
}
.icon-microsoft:before {
content: "\e940";
}
.icon-google:before {
content: "\e93b";
}
.icon-unlocked:before {
content: "\e933";
}
.icon-lock:before {
content: "\e934";
}
.icon-reset:before {
content: "\e92e";
}
.icon-pause:before {
content: "\e92f";
}
.icon-play:before {
content: "\e930";
}
.icon-settings2:before {
content: "\e92d";
}
.icon-bin2:before {
content: "\e902";
}
.icon-download:before {
content: "\e93e";
}

Loading…
Cancel
Save