diff --git a/Dockerfile b/Dockerfile index e57b35d67..d5746a8eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,6 +55,9 @@ RUN apk update \ # Copy from build stage COPY --from=builder /out/alpine . +ARG SQUIDEX__VERSION +ENV SQUIDEX__VERSION ${SQUIDEX__VERSION:-dev} + EXPOSE 80 EXPOSE 11111 diff --git a/src/Squidex.Web/ExposedConfiguration.cs b/src/Squidex.Web/ExposedConfiguration.cs new file mode 100644 index 000000000..169cfc9fb --- /dev/null +++ b/src/Squidex.Web/ExposedConfiguration.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Web +{ + public sealed class ExposedConfiguration : Dictionary + { + } +} diff --git a/src/Squidex.Web/ExposedValues.cs b/src/Squidex.Web/ExposedValues.cs new file mode 100644 index 000000000..9c01a60c0 --- /dev/null +++ b/src/Squidex.Web/ExposedValues.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.Configuration; +using Squidex.Infrastructure; + +namespace Squidex.Web +{ + public sealed class ExposedValues : Dictionary + { + public ExposedValues() + { + } + + public ExposedValues(ExposedConfiguration configured, IConfiguration configuration) + { + Guard.NotNull(configured, nameof(configured)); + Guard.NotNull(configuration, nameof(configuration)); + + foreach (var kvp in configured) + { + var value = configuration.GetValue(kvp.Value); + + if (!string.IsNullOrWhiteSpace(value)) + { + this[kvp.Key] = value; + } + } + } + + public override string ToString() + { + var sb = new StringBuilder(); + + foreach (var kvp in this) + { + if (sb.Length > 0) + { + sb.Append(", "); + } + + sb.Append(kvp.Key); + sb.Append(": "); + sb.Append(kvp.Value); + } + + return sb.ToString(); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs b/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs index 22a0254ad..388026dc4 100644 --- a/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs +++ b/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs @@ -18,9 +18,26 @@ namespace Squidex.Areas.Api.Controllers.Ping [ApiExplorerSettings(GroupName = nameof(Ping))] public sealed class PingController : ApiController { - public PingController(ICommandBus commandBus) + private readonly ExposedValues exposedValues; + + public PingController(ICommandBus commandBus, ExposedValues exposedValues) : base(commandBus) { + this.exposedValues = exposedValues; + } + + /// + /// Get general info status of the API. + /// + /// + /// 200 => Infos returned. + /// + [HttpGet] + [ProducesResponseType(typeof(ExposedValues), 200)] + [Route("info/")] + public IActionResult Info() + { + return Ok(exposedValues); } /// diff --git a/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs b/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs index ca8bdcc5e..a7ed32357 100644 --- a/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs @@ -13,8 +13,12 @@ namespace Squidex.Areas.Api.Controllers.UI { public Dictionary RegexSuggestions { get; set; } + public Dictionary More { get; set; } = new Dictionary(); + public MapOptions Map { get; set; } + public bool ShowInfo { get; set; } + public bool HideNews { get; set; } public bool HideOnboarding { get; set; } diff --git a/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs b/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs index 518bb7ae9..cf283c1ba 100644 --- a/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs +++ b/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs @@ -9,9 +9,9 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using Squidex.Areas.Api.Controllers.UI; using Squidex.Infrastructure.Json; +using Squidex.Web; namespace Squidex.Areas.Frontend.Middlewares { @@ -45,6 +45,13 @@ namespace Squidex.Areas.Frontend.Middlewares if (uiOptions != null) { + var values = httpContext.RequestServices.GetService(); + + if (values != null) + { + uiOptions.More["info"] = values.ToString(); + } + var jsonSerializer = httpContext.RequestServices.GetRequiredService(); var jsonOptions = jsonSerializer.Serialize(uiOptions, false); diff --git a/src/Squidex/Config/Web/WebServices.cs b/src/Squidex/Config/Web/WebServices.cs index e4e867d85..6dfa72593 100644 --- a/src/Squidex/Config/Web/WebServices.cs +++ b/src/Squidex/Config/Web/WebServices.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Squidex.Config.Domain; using Squidex.Domain.Apps.Entities; using Squidex.Pipeline.Plugins; @@ -21,6 +22,9 @@ namespace Squidex.Config.Web { public static void AddMyMvcWithPlugins(this IServiceCollection services, IConfiguration config) { + services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService>().Value, config)) + .AsSelf(); + services.AddSingletonAs() .AsSelf(); diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index 741aa75b0..e85d489ed 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -95,6 +95,8 @@ namespace Squidex config.GetSection("usage")); services.Configure( config.GetSection("rebuild")); + services.Configure( + config.GetSection("exposedConfiguration")); services.Configure( config.GetSection("contentsController")); diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.html b/src/Squidex/app/features/apps/pages/apps-page.component.html index 38eb2d9d3..2a02152a8 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.html +++ b/src/Squidex/app/features/apps/pages/apps-page.component.html @@ -100,6 +100,8 @@ +
{{info}}
+ diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.scss b/src/Squidex/app/features/apps/pages/apps-page.component.scss index af1a2f69f..ca1de18bf 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.scss +++ b/src/Squidex/app/features/apps/pages/apps-page.component.scss @@ -89,4 +89,11 @@ .deeplinks { display: none; } +} + +.info { + color: $color-border-dark; + padding: 2rem; + padding-left: $size-sidebar-width + .25rem; + font-size: .8rem; } \ No newline at end of file diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.ts b/src/Squidex/app/features/apps/pages/apps-page.component.ts index f51a6a7d3..1c73760fd 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.ts +++ b/src/Squidex/app/features/apps/pages/apps-page.component.ts @@ -35,6 +35,8 @@ export class AppsPageComponent implements OnInit { public newsFeatures: FeatureDto[]; public newsDialog = new DialogModel(); + public info: string; + constructor( public readonly appsState: AppsState, public readonly authState: AuthService, @@ -44,6 +46,9 @@ export class AppsPageComponent implements OnInit { private readonly onboardingService: OnboardingService, private readonly uiOptions: UIOptions ) { + if (uiOptions.get('showInfo')) { + this.info = uiOptions.get('more.info'); + } } public ngOnInit() { diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 79b8006e5..773dffe65 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -78,7 +78,11 @@ /* * Hide all onboarding tooltips and dialogs. */ - "hideOnboarding": false + "hideOnboarding": false, + /* + * Show the exposed values as information on the apps overview page. + */ + "showInfo": false }, "email": { @@ -133,8 +137,8 @@ /* * The text for the robots.txt file */ - "text": "User-agent: *\nAllow: /api/assets/*" - }, + "text": "User-agent: *\nAllow: /api/assets/*" + }, "healthz": { "gc": { @@ -483,5 +487,12 @@ * Set to true to rebuild indexes. */ "indexes": false + }, + + /*" + * A list of configuration valeus that should be exposed from the info endpoint and in the UI. + */ + "exposedConfiguration": { + "version": "squidex:version" } } diff --git a/tests/Squidex.Web.Tests/ExposedValuesTests.cs b/tests/Squidex.Web.Tests/ExposedValuesTests.cs new file mode 100644 index 000000000..b3ba83ba4 --- /dev/null +++ b/tests/Squidex.Web.Tests/ExposedValuesTests.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Squidex.Web +{ + public class ExposedValuesTests + { + [Fact] + public void Should_create_from_configuration() + { + var source = new ExposedConfiguration + { + ["name1"] = "config1", + ["name2"] = "config2", + ["name3"] = "config3", + }; + + var configuration = A.Fake(); + + SetupConfiguration(configuration, "config1", "value1"); + SetupConfiguration(configuration, "config2", "value2"); + + var values = new ExposedValues(source, configuration); + + Assert.Equal(2, values.Count); + Assert.Equal("value1", values["name1"]); + Assert.Equal("value2", values["name2"]); + } + + [Fact] + public void Should_format_empty_values() + { + var values = new ExposedValues(); + + Assert.Empty(values.ToString()); + } + + [Fact] + public void Should_format_from_single_value() + { + var values = new ExposedValues + { + ["name1"] = "value1" + }; + + Assert.Equal("name1: value1", values.ToString()); + } + + [Fact] + public void Should_format_from_multiple_values() + { + var values = new ExposedValues + { + ["name1"] = "value1", + ["name2"] = "value2" + }; + + Assert.Equal("name1: value1, name2: value2", values.ToString()); + } + + private static void SetupConfiguration(IConfiguration configuration, string key, string value) + { + var configSection = A.Fake(); + + A.CallTo(() => configSection.Value).Returns(value); + A.CallTo(() => configuration.GetSection(key)).Returns(configSection); + } + } +}