use flavors for integration types

This commit is contained in:
2021-12-23 09:07:34 +01:00
parent 99c7b258e9
commit 41e4765a31
14 changed files with 55 additions and 429 deletions
@@ -116,7 +116,7 @@ public class IntegrationsController : ZeroApiController
//[ZeroAuthorize(CountryPermissions.Update)]
public virtual async Task<ActionResult<Result>> Update(string alias, Integration updateModel, [FromQuery] string changeToken = null)
{
if (alias != updateModel.TypeAlias)
if (alias != updateModel.Flavor)
{
return BadRequest(Result.Fail(nameof(alias), "@integration.errors.noaliasmatch"));
}
+1 -1
View File
@@ -34,7 +34,7 @@ public class ZeroBackofficePlugin : ZeroPlugin
services.AddSingleton<IIconService, IconService>();
services.AddSingleton<IResourceService, ResourceService>();
services.AddSingleton<ISectionService, SectionService>();
services.AddScoped<ISectionService, SectionService>();
services.AddSingleton<IBackofficeAssetFileSystem, BackofficeAssetFileSystem>(svc =>
{
@@ -5,7 +5,7 @@ namespace zero.Backoffice.Services;
public class SectionService : ISectionService
{
protected IOptions<BackofficeOptions> Options { get; set; }
protected IZeroOptions Options { get; set; }
protected IBackofficeAssetFileSystem FileSystem { get; set; }
@@ -16,7 +16,7 @@ public class SectionService : ISectionService
protected IEnumerable<ISettingsGroup> SettingsGroups { get; set; }
public SectionService(IOptions<BackofficeOptions> options, IBackofficeAssetFileSystem fileSystem, ILogger<ISectionService> logger, IEnumerable<IBackofficeSection> sections, IEnumerable<ISettingsGroup> settingsGroups)
public SectionService(IZeroOptions options, IBackofficeAssetFileSystem fileSystem, ILogger<ISectionService> logger, IEnumerable<IBackofficeSection> sections, IEnumerable<ISettingsGroup> settingsGroups)
{
Options = options;
FileSystem = fileSystem;
@@ -88,7 +88,7 @@ public class SectionService : ISectionService
List<BackofficeSettingGroupPresentation> groups = new();
//bool hasIntegrations = Options.For<IntegrationOptions>().Any();
bool hasIntegrations = Options.For<FlavorOptions>().GetAll<Integration>().Any();
foreach (SettingsGroup group in SettingsGroups)
{
@@ -100,10 +100,11 @@ public class SectionService : ISectionService
//{
// continue;
//}
//if (area.Alias == Constants.Settings.Integrations && !hasIntegrations)
//{
// continue;
//}
if (area.Alias == Constants.Settings.Integrations && !hasIntegrations)
{
continue;
}
//bool isPlugin = !(area is InternalSettingsArea);
@@ -23,7 +23,13 @@ public class ConfigurationModule : ZeroModule
services.AddScoped<IIntegrationStore, IntegrationStore>();
services.AddOptions<FeatureOptions>().Bind(configuration.GetSection("Zero:Features"));
services.AddOptions<IntegrationOptions>();
services.ConfigureOptions<ConfigureIntegrationJsonOptions>();
services.Configure<FlavorOptions>(opts =>
{
opts.Configure<Integration>(cfg =>
{
cfg.CanUseWithoutFlavors = false;
});
});
}
}
@@ -1,20 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace zero.Configuration;
public class ConfigureIntegrationJsonOptions : IConfigureOptions<JsonOptions>
{
private readonly IZeroOptions zeroOptions;
public ConfigureIntegrationJsonOptions(IZeroOptions options)
{
zeroOptions = options;
}
public void Configure(JsonOptions options)
{
options.JsonSerializerOptions.Converters.Add(new JsonIntegrationTypeVariantConverter(zeroOptions));
}
}
@@ -0,0 +1,21 @@
using FluentValidation;
namespace zero.Configuration;
public static class FlavorOptionsExtensions
{
public static void AddIntegration<T>(this FlavorOptions options, string alias, string name, string description, string editorAlias = null, List<string> tags = default, string imagePath = null, IValidator validator = null) where T : Integration, new()
{
options.Add<Integration, T>(new IntegrationType(typeof(T))
{
Alias = alias,
Name = name,
Description = description,
ImagePath = imagePath,
Tags = tags,
Validator = validator,
Construct = cfg => new T(),
EditorAlias = editorAlias
});
}
}
@@ -9,6 +9,4 @@ namespace zero.Configuration;
[RavenCollection("Integrations")]
public class Integration : ZeroEntity
{
/// <inheritdoc />
public string TypeAlias { get; set; }
}
@@ -1,42 +0,0 @@
using FluentValidation;
namespace zero.Configuration;
public class IntegrationOptions : List<IntegrationType>
{
public void Add<T>(string alias, string name, string description, string editorAlias = null, List<string> tags = default, string imagePath = null, IValidator validator = null) where T : Integration , new()
{
Add(new IntegrationType(typeof(T))
{
Alias = alias,
Name = name,
Description = description,
ImagePath = imagePath,
Tags = tags,
Validator = validator,
Construct = cfg => new T(),
EditorAlias = editorAlias
});
}
public void Add(Type type, string alias, string name, string description, string editorAlias = null, List<string> tags = default, string imagePath = null, IValidator validator = null)
{
if (!typeof(Integration).IsAssignableFrom(type))
{
throw new ArgumentException("Type has to inherit the Integration base model", nameof(type));
}
Add(new IntegrationType(type)
{
Alias = alias,
Name = name,
Description = description,
ImagePath = imagePath,
Tags = tags,
Validator = validator,
Construct = cfg => Activator.CreateInstance(type) as Integration,
EditorAlias = editorAlias
});
}
}
@@ -17,24 +17,16 @@ public class IntegrationStore : IIntegrationStore
/// <inheritdoc />
public virtual Task<Integration> Empty(string alias)
public virtual async Task<Integration> Empty(string alias)
{
IntegrationType type = IntegrationTypes.GetByAlias(alias);
Integration integration = type?.Construct(type);
if (integration != null)
{
integration.TypeAlias = alias;
}
return Task.FromResult(integration);
return await Operations.Empty<Integration>(alias);
}
/// <inheritdoc />
public virtual async Task<Integration> Load(string alias)
{
Paged<Integration> result = await Operations.Load<Integration>(1, 1, q => q.Where(x => x.TypeAlias == alias));
Paged<Integration> result = await Operations.Load<Integration>(1, 1, q => q.Where(x => x.Flavor == alias));
return result.Items.FirstOrDefault();
}
@@ -104,19 +96,19 @@ public class IntegrationStore : IIntegrationStore
return Result<Integration>.Fail("@integration.errors.notfound");
}
IntegrationType type = IntegrationTypes.GetByAlias(model.TypeAlias);
IntegrationType type = IntegrationTypes.GetByAlias(model.Flavor);
if (type == null)
{
return Result<Integration>.Fail("@integration.errors.typenotfound");
}
if (create && await Operations.Any<Integration>(q => q.Where(x => x.TypeAlias == model.TypeAlias)))
if (create && await Operations.Any<Integration>(q => q.Where(x => x.Flavor == model.Flavor)))
{
return Result<Integration>.Fail("@integration.errors.multiplenotallowed");
}
if (!create && await Operations.Any<Integration>(q => q.Where(x => x.TypeAlias == model.TypeAlias && x.Id != model.Id)))
if (!create && await Operations.Any<Integration>(q => q.Where(x => x.Flavor == model.Flavor && x.Id != model.Id)))
{
return Result<Integration>.Fail("@integration.errors.alreadycreated");
}
@@ -7,39 +7,20 @@ namespace zero.Configuration;
/// An integration is an application part which has a public configuration per app.
/// It's up to the user to provide functionality.
/// </summary>
public class IntegrationType
public class IntegrationType : FlavorConfig
{
/// <summary>
/// Type of the associated entity
/// </summary>
[JsonIgnore]
public Type ModelType { get; private set; }
public IntegrationType(Type type) : base(type) { }
/// <summary>
/// Alias to find the editor schema
/// </summary>
public string EditorAlias { get; set; }
/// <summary>
/// Alias for querying
/// </summary>
public string Alias { get; set; }
/// <summary>
/// Name of the flavor
/// </summary>
public string Name { get; set; }
/// <summary>
/// Group integrations by tags
/// </summary>
public List<string> Tags { get; set; } = new();
/// <summary>
/// Optional description
/// </summary>
public string Description { get; set; }
/// <summary>
/// Image of the integration
/// </summary>
@@ -50,12 +31,4 @@ public class IntegrationType
/// </summary>
[JsonIgnore]
public IValidator Validator { get; set; }
[JsonIgnore]
public Func<IntegrationType, Integration> Construct { get; set; }
public IntegrationType(Type type)
{
ModelType = type;
}
}
@@ -6,36 +6,35 @@ public class IntegrationTypeService : IIntegrationTypeService
protected IHandlerHolder Handler { get; private set; }
protected IntegrationOptions Types { get; set; }
protected FlavorOptions Flavors { get; set; }
public IntegrationTypeService(IZeroContext context, IZeroOptions options, IHandlerHolder handler)
{
Context = context;
Handler = handler;
Types = options.For<IntegrationOptions>();
Flavors = options.For<FlavorOptions>();
}
/// <inheritdoc />
public IEnumerable<IntegrationType> GetAll()
{
return Types;
return Flavors.GetAll<Integration>().Select(x => (IntegrationType)x);
}
/// <inheritdoc />
public IntegrationType GetByAlias(string integrationTypeAlias)
{
return Types.FirstOrDefault(x => x.Alias == integrationTypeAlias);
return Flavors.Get<Integration>(integrationTypeAlias) as IntegrationType;
}
/// <inheritdoc />
public IntegrationType GetByType<T>() where T : Integration
{
Type type = typeof(T);
return Types.FirstOrDefault(x => x.ModelType == type);
return Flavors.Get<Integration, T>() as IntegrationType;
}
}
@@ -1,281 +0,0 @@
//using Microsoft.Extensions.Logging;
//using Raven.Client.Documents;
//using Raven.Client.Documents.Linq;
//namespace zero.Core.Configuration;
//public class IntegrationsCollection : EntityStore<Integration>, IIntegrationsCollection
//{
// /// <inheritdoc />
// public IReadOnlyCollection<IntegrationType> RegisteredTypes { get; private set; }
// protected ILogger<IntegrationsCollection> Logger { get; private set; }
// public IntegrationsCollection(IStoreContext<Integration> context, ILogger<IntegrationsCollection> logger) : base(context)
// {
// Options = new(true);
// RegisteredTypes = Context.Options.Integrations.GetAllItems();
// Logger = logger;
// }
// /// <inheritdoc />
// public Integration GetEmpty(string alias)
// {
// IntegrationType type = RegisteredTypes.FirstOrDefault(x => x.Alias.Equals(alias, StringComparison.InvariantCultureIgnoreCase));
// if (type == null)
// {
// return null;
// }
// try
// {
// Integration model = Activator.CreateInstance(type.ContentType) as Integration;
// model.TypeAlias = type.Alias;
// return model;
// }
// catch
// {
// Logger.LogWarning("Could not create integration with type {alias}", alias);
// }
// return null;
// }
// /// <inheritdoc />
// public async Task<Integration> GetByAlias(string alias)
// {
// IntegrationType type = RegisteredTypes.FirstOrDefault(x => x.Alias.Equals(alias, StringComparison.InvariantCultureIgnoreCase));
// return await Load<Integration>(type);
// }
// /// <inheritdoc />
// public async Task<T> Get<T>(string alias = null) where T : Integration, new()
// {
// IntegrationType type = RegisteredTypes.FirstOrDefault(x => x.ContentType == typeof(T) && (alias.IsNullOrEmpty() || x.Alias.Equals(alias, StringComparison.InvariantCultureIgnoreCase)));
// return await Load<T>(type);
// }
// /// <inheritdoc />
// public async Task<IList<Integration>> GetByTag(string tag)
// {
// IEnumerable<IntegrationType> types = RegisteredTypes.Where(x => x.Tags.Contains(tag, StringComparer.InvariantCultureIgnoreCase));
// if (!types.Any())
// {
// return new List<Integration>();
// }
// string[] aliases = types.Select(x => x.Alias).ToArray();
// return await Session.Query<Integration>().Where(x => x.TypeAlias.In(aliases)).ToListAsync();
// }
// /// <inheritdoc />
// public async Task<bool> Any(string tag)
// {
// return (await GetByTag(tag)).Any(x => x.IsActive);
// }
// /// <inheritdoc />
// public override async Task<Paged<Integration>> Load(ListQuery<Integration> query)
// {
// List<Integration> result = new();
// List<Integration> models = await Session.Query<Integration>().ToListAsync();
// foreach (IntegrationType type in RegisteredTypes)
// {
// Integration model = models.FirstOrDefault(x => x.TypeAlias == type.Alias);
// if (model != null)
// {
// model.Name = type.Name;
// result.Add(model);
// }
// }
// return result.ToQueriedList(query);
// }
// /// <inheritdoc />
// public async Task<IList<IntegrationTypeWithStatus>> GetTypesWithStatus()
// {
// List<IntegrationTypeWithStatus> result = new();
// List<Integration> models = await Session.Query<Integration>().ToListAsync();
// foreach (IntegrationType type in RegisteredTypes)
// {
// Integration model = models.FirstOrDefault(x => x.TypeAlias == type.Alias);
// result.Add(new()
// {
// Type = type,
// Id = model?.Id,
// IsActive = model?.IsActive ?? false,
// IsConfigured = model != null
// });
// }
// return result.OrderByDescending(x => x.IsActive).ThenByDescending(x => x.IsConfigured).ThenByDescending(x => x.Type.Name).ToList();
// }
// /// <inheritdoc />
// public override async Task<Result<Integration>> Save(Integration model)
// {
// if (model == null)
// {
// return Result<Integration>.Fail("@integration.errors.notfound");
// }
// IntegrationType type = RegisteredTypes.FirstOrDefault(x => x.Alias.Equals(model.TypeAlias, StringComparison.InvariantCultureIgnoreCase));
// if (type == null)
// {
// return Result<Integration>.Fail("@integration.errors.typenotfound");
// }
// string existingId = await Session.Query<Integration>().Where(x => x.TypeAlias == type.Alias).Select(x => x.Id).FirstOrDefaultAsync();
// if (!existingId.IsNullOrEmpty() && existingId != model.Id)
// {
// return Result<Integration>.Fail("@integration.errors.multiplenotallowed");
// }
// model.Alias = type.Alias;
// model.Name = null;
// Result<Integration> result = await base.Save(model);
// if (result.IsSuccess)
// {
// result.Model.Name = type.Name;
// }
// return result;
// }
// /// <inheritdoc />
// public async Task<Result<Integration>> Activate(string alias)
// {
// Integration model = await GetByAlias(alias);
// if (model != null)
// {
// model.IsActive = true;
// }
// return await Save(model);
// }
// /// <inheritdoc />
// public async Task<Result<Integration>> Deactivate(string alias)
// {
// Integration model = await GetByAlias(alias);
// if (model != null)
// {
// model.IsActive = false;
// }
// return await Save(model);
// }
// /// <inheritdoc />
// public async Task<Result<Integration>> DeleteByAlias(string alias)
// {
// return await base.Delete(await GetByAlias(alias));
// }
// /// <summary>
// /// Get integration data from database
// /// </summary>
// protected async Task<T> Load<T>(IntegrationType type) where T : Integration, new()
// {
// if (type == null)
// {
// return default;
// }
// T entity = await Session.Query<T>().FirstOrDefaultAsync(x => x.TypeAlias == type.Alias);
// if (entity != null)
// {
// entity.Name = type.Name;
// entity.TypeAlias = type.Alias;
// }
// return WhenActive(entity) as T;
// }
//}
//public interface IIntegrationsCollection
//{
// /// <summary>
// /// Get all registered integration types
// /// </summary>
// IReadOnlyCollection<IntegrationType> RegisteredTypes { get; }
// /// <summary>
// /// Get new integration model for the specified integration type alias
// /// </summary>
// Integration GetEmpty(string alias);
// /// <summary>
// /// Get integration by an alias
// /// </summary>
// Task<Integration> GetByAlias(string alias);
// /// <summary>
// /// Get an integration by type and optional alias
// /// </summary>
// Task<T> Get<T>(string alias = null) where T : Integration, new();
// /// <summary>
// /// Get all integrations by a certain tag
// /// </summary>
// Task<IList<Integration>> GetByTag(string tag);
// /// <summary>
// /// Check if any integrations of certain tag are activated
// /// </summary>
// Task<bool> Any(string tag);
// /// <summary>
// /// Get all integrations with the specified query
// /// </summary>
// Task<Paged<Integration>> Load(ListQuery<Integration> query);
// /// <summary>
// /// Get all integration types with their configuration status
// /// </summary>
// Task<IList<IntegrationTypeWithStatus>> GetTypesWithStatus();
// /// <summary>
// /// Saves an integration
// /// </summary>
// Task<Result<Integration>> Save(Integration model);
// /// <summary>
// /// Activates a configured integration
// /// </summary>
// Task<Result<Integration>> Activate(string alias);
// /// <summary>
// /// Disables a configured integration
// /// </summary>
// Task<Result<Integration>> Deactivate(string alias);
// /// <summary>
// /// Deletes configuration of an integration and disables it
// /// </summary>
// Task<Result<Integration>> DeleteByAlias(string alias);
//}
@@ -1,18 +0,0 @@
namespace zero.Configuration;
public class JsonIntegrationTypeVariantConverter : JsonDiscriminatorConverter<ZeroEntity>
{
protected IntegrationOptions Integrations { get; private set; }
public JsonIntegrationTypeVariantConverter(IZeroOptions options) : base("typeAlias")
{
Integrations = options.For<IntegrationOptions>();
}
protected override Type GetTypeFromDiscriminator(Type requestedType, string discriminator)
{
IntegrationType type = Integrations.FirstOrDefault(x => x.Alias == discriminator);
return type?.ModelType ?? requestedType;
}
}
+3 -6
View File
@@ -29,10 +29,13 @@ builder.Services.Configure<ZeroDevOptions>(opts =>
builder.Services.Configure<FlavorOptions>(opts =>
{
opts.AddSpaceList<TeamMember>("team", "Team", "Members of our team", "fth-users", "spaces.team");
opts.AddIntegration<FathomAnalyticsIntegration>("analytics.fathom", "Fathom Analytics", "Connect your website to Fathom and track page views", tags: new() { "analytics" }, imagePath: "/assets/fathom.png");
opts.AddIntegration<GoogleAnalyticsIntegration>("analytics.google", "Google Analytics", "Connect your website to google analytics", tags: new() { "analytics" }, imagePath: "/assets/googleanalytics.png");
opts.Configure<Country>(x => x.CanUseWithoutFlavors = false);
opts.Add<Country, EuropeanCountry>("eu_country", "EU country", "A country within the European Union", "fth-globe");
opts.Add<Country, AmericanCountry>("usa_country", "USA", "A country in the United States", "fth-flag");
});
builder.Services.Configure<BlueprintOptions>(opts =>
@@ -42,12 +45,6 @@ builder.Services.Configure<BlueprintOptions>(opts =>
opts.Add<Translation>();
});
builder.Services.Configure<IntegrationOptions>(opts =>
{
opts.Add<FathomAnalyticsIntegration>("analytics.fathom", "Fathom Analytics", "Connect your website to Fathom and track page views", tags: new() { "analytics" }, imagePath: "/assets/fathom.png");
opts.Add<GoogleAnalyticsIntegration>("analytics.google", "Google Analytics", "Connect your website to google analytics", tags: new() { "analytics" }, imagePath: "/assets/googleanalytics.png");
});
var app = builder.Build();
// Configure the HTTP request pipeline.