Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add import from json file #61

Merged
merged 3 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public static class MediaType

public const string FeatureFlag = "application/vnd.microsoft.appconfig.ff+json";

public const string Json = "application/json";

public const string Keys = "application/vnd.microsoft.appconfig.keyset+json";

public const string Labels = "application/vnd.microsoft.appconfig.labelset+json";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<button class="@BackgroundClasses @BorderClasses @ColorClasses font-semibold h-[24px] px-5 rounded-sm" disabled="@IsDisabled" @onclick="OnClick">
<button class="@BackgroundClasses @BorderClasses @ColorClasses font-semibold h-[24px] px-5 rounded-sm" disabled="@IsDisabled" @onclick="OnClick" type="@Type">
@Label
</button>

Expand All @@ -11,6 +11,8 @@

[Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; }

[Parameter] public string? Type { get; set; }

private string BackgroundClasses => Appearance switch
{
AzureAppearance.Default => "bg-white hover:bg-concrete disabled:bg-concrete dark:bg-cod-grey dark:hover:bg-shark dark:disabled:bg-shark",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<InputFile @attributes="@AdditionalAttributes" class="px-2 pt-0.5 pb-1 w-full bg-white rounded-sm border cursor-pointer disabled:cursor-default border-storm-dust h-[24px] placeholder:text-storm-dust invalid:border-alizarin-crimson file:hidden dark:bg-cod-grey dark:border-star-dust dark:placeholder:text-star-dust dark:disabled:bg-shark dark:disabled:border-natural-grey dark:disabled:text-natural-grey disabled:bg-concrete disabled:border-star-dust disabled:text-star-dust" OnChange="@OnChange"/>

@code {
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object>? AdditionalAttributes { get; set; }

[Parameter] public EventCallback<InputFileChangeEventArgs> OnChange { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@typeparam TValue

<InputRadio @attributes="@AdditionalAttributes" class="hidden" Value="@Value"/>

@code {
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object>? AdditionalAttributes { get; set; }

[Parameter] public TValue? Value { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@typeparam TValue
@using System.Linq.Expressions

<InputRadioGroup @attributes="@AdditionalAttributes" Value="@Value" ValueChanged="@ValueChanged" ValueExpression="@ValueExpression">
<div class="flex flex-row gap-0.5 self-start p-0.5 rounded-full border border-storm-dust dark:border-star-dust" role="radiogroup">
@ChildContent
</div>
</InputRadioGroup>

@code {
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object>? AdditionalAttributes { get; set; }

[Parameter] public RenderFragment? ChildContent { get; set; }

[Parameter] public TValue? Value { get; set; }

[Parameter] public EventCallback<TValue> ValueChanged { get; set; }

[Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<label @attributes="@AdditionalAttributes" class="px-2.5 pt-0 pb-0.5 bg-white rounded-full cursor-pointer has-[:disabled]:cursor-default has-[:disabled]:bg-white has-[:disabled]:dark:bg-cod-grey has-[:checked]:bg-lochmara has-[:checked]:hover:bg-science-blue has-[:checked]:active:bg-venice-blue has-[:checked:disabled]:bg-concrete has-[:checked:disabled]:hover:bg-concrete has-[:checked:disabled]:active:bg-concrete has-[:checked]:dark:bg-lochmara has-[:checked]:dark:hover:bg-dodger-blue has-[:checked]:dark:active:bg-jordy-blue has-[:checked:disabled]:dark:bg-shark has-[:checked:disabled]:hover:dark:bg-shark has-[:checked:disabled]:dark:active:bg-shark text-mine-shaft has-[:disabled]:text-star-dust has-[:disabled]:dark:text-natural-grey has-[:checked]:text-white has-[:checked:disabled]:text-star-dust has-[:checked]:dark:text-mine-shaft has-[:checked:disabled]:dark:text-natural-grey dark:bg-cod-grey dark:hover:bg-shark dark:active:bg-tuatara dark:text-desert-storm hover:bg-concrete active:bg-gallery">
@ChildContent
</label>

@code {
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object>? AdditionalAttributes { get; set; }

[Parameter] public RenderFragment? ChildContent { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@typeparam TValue
@using System.Linq.Expressions

<InputSelect @attributes="@AdditionalAttributes" class="px-1 pt-0 pb-0.5 w-full bg-white rounded-sm border cursor-pointer disabled:cursor-default border-storm-dust h-[24px] invalid:border-alizarin-crimson dark:bg-cod-grey dark:border-star-dust dark:disabled:bg-shark dark:disabled:border-natural-grey dark:disabled:text-natural-grey disabled:bg-concrete disabled:border-star-dust disabled:text-star-dust" Value="@Value" ValueChanged="@ValueChanged" ValueExpression="@ValueExpression">
@ChildContent
</InputSelect>

@code {
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object>? AdditionalAttributes { get; set; }

[Parameter] public RenderFragment? ChildContent { get; set; }

[Parameter] public TValue? Value { get; set; }

[Parameter] public EventCallback<TValue> ValueChanged { get; set; }

[Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }
}
15 changes: 15 additions & 0 deletions src/AzureAppConfigurationEmulator/Components/AzureInputText.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@using System.Linq.Expressions

<InputText @attributes="@AdditionalAttributes" class="px-2 pt-1 pb-1.5 w-full bg-white rounded-sm border border-storm-dust h-[24px] placeholder:text-storm-dust invalid:border-alizarin-crimson dark:bg-cod-grey dark:border-star-dust dark:placeholder:text-star-dust dark:disabled:bg-shark dark:disabled:border-natural-grey dark:disabled:text-natural-grey disabled:bg-concrete disabled:border-star-dust disabled:text-star-dust" Value="@Value" ValueChanged="@ValueChanged" ValueExpression="@ValueExpression"/>

@code {
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object>? AdditionalAttributes { get; set; }

[Parameter] public RenderFragment? ChildContent { get; set; }

[Parameter] public string? Value { get; set; }

[Parameter] public EventCallback<string> ValueChanged { get; set; }

[Parameter] public Expression<Func<string>>? ValueExpression { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
<div class="flex flex-col gap-1 w-full">
<label class="flex flex-col gap-1 w-full">
@if (!string.IsNullOrEmpty(Label))
{
<label for="@Id">
<div class="mb-1">
@Label

@if (IsRequired)
{
<span class="text-alizarin-crimson"> *</span>
}
</label>
</div>
}

<input @bind:get="@Value" @bind:set="@(value => ValueChanged.InvokeAsync(value))" class="px-2 pt-1 pb-1.5 w-full bg-white rounded-sm border border-storm-dust h-[24px] placeholder:text-storm-dust invalid:border-alizarin-crimson dark:bg-cod-grey dark:border-star-dust dark:placeholder:text-star-dust dark:disabled:bg-shark dark:disabled:border-natural-grey dark:disabled:text-natural-grey disabled:bg-concrete disabled:border-star-dust disabled:text-star-dust" disabled="@IsDisabled" id="@Id" placeholder="@Placeholder" readonly="@IsReadOnly" required="@IsRequired" type="@Type"/>
</div>
<input @bind:get="@Value" @bind:set="@(value => ValueChanged.InvokeAsync(value))" class="px-2 pt-1 pb-1.5 w-full bg-white rounded-sm border border-storm-dust h-[24px] placeholder:text-storm-dust invalid:border-alizarin-crimson dark:bg-cod-grey dark:border-star-dust dark:placeholder:text-star-dust dark:disabled:bg-shark dark:disabled:border-natural-grey dark:disabled:text-natural-grey disabled:bg-concrete disabled:border-star-dust disabled:text-star-dust" disabled="@IsDisabled" placeholder="@Placeholder" readonly="@IsReadOnly" required="@IsRequired" type="@Type"/>
</label>

@code {
[Parameter] public string Id { get; set; } = Guid.NewGuid().ToString();

[Parameter] public bool IsDisabled { get; set; }

[Parameter] public bool IsReadOnly { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<NavItem Href="ff" Label="Feature manager"/>
@* <NavItem Href="kvrestore" Label="Restore"/> *@
@* <NavItem Href="kvc" Label="Compare"/> *@
@* <NavItem Href="kvdata" Label="Import/export"/> *@
<NavItem Href="kvdata" Label="Import/export"/>
</NavGroup>

<NavGroup Header="Settings">
Expand Down
245 changes: 245 additions & 0 deletions src/AzureAppConfigurationEmulator/Components/Pages/ImportExport.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
@inject IConfigurationSettingFactory ConfigurationSettingFactory
@inject IConfigurationSettingRepository ConfigurationSettingRepository
@page "/kvdata"
@using System.Security.Cryptography
@using System.Text
@using System.Text.Json
@using AzureAppConfigurationEmulator.Common.Abstractions
@using AzureAppConfigurationEmulator.Common.Constants

<PageTitle>Import/export</PageTitle>

<div class="flex flex-col">
<AzureToolbar>
<div class="h-[36px]"></div>
</AzureToolbar>

<div class="p-5">
<EditForm class="flex flex-col gap-4 items-stretch pb-4" Model="Model" OnSubmit="@HandleSubmit">
<AzureInputRadioGroup @bind-Value="@Model!.Operation" name="@nameof(Model.Operation)">
<AzureInputRadioLabel>
<AzureInputRadio checked="@(Model.Operation is Operation.Import)" Value="@Operation.Import"/>
<div>Import</div>
</AzureInputRadioLabel>

<AzureInputRadioLabel>
<AzureInputRadio checked="@(Model.Operation is Operation.Export)" disabled Value="@Operation.Export"/>
<div>Export</div>
</AzureInputRadioLabel>
</AzureInputRadioGroup>

<label class="flex flex-col gap-1">
<div>Source service</div>
<AzureInputSelect @bind-Value="@Model!.SourceService" name="@nameof(Model.SourceService)">
<option checked="@(Model.SourceService is null)" hidden value="">Please select a source service</option>
<option checked="@(Model.SourceService is SourceService.AzureAppConfiguration)" disabled value="@SourceService.AzureAppConfiguration">App Configuration</option>
<option checked="@(Model.SourceService is SourceService.AzureAppService)" disabled value="@SourceService.AzureAppService">App Service</option>
<option checked="@(Model.SourceService is SourceService.ConfigurationFile)" value="@SourceService.ConfigurationFile">Configuration file</option>
</AzureInputSelect>
</label>

@if (Model?.SourceService is not null)
{
<label class="flex flex-col gap-1">
<div>File type</div>
<AzureInputSelect @bind-Value="@Model.FileType" name="@nameof(Model.FileType)">
<option checked="@(Model.FileType is null)" hidden value="">Please select a file type</option>
<option checked="@(Model.FileType is ".json")" value=".json">Json</option>
<option checked="@(Model.FileType is ".yaml,.yml")" disabled value=".yaml,.yml">Yaml</option>
<option checked="@(Model.FileType is ".properties")" disabled value=".properties">Properties</option>
</AzureInputSelect>
</label>
}

@if (Model?.FileType is not null)
{
<label class="flex flex-col gap-1">
<div>Source file</div>
<AzureInputFile accept="@Model.FileType" name="@nameof(Model.SourceFile)" OnChange="args => Model.SourceFile = args.File"/>
</label>
}

@if (Model?.SourceFile is not null)
{
<label class="flex flex-col gap-1">
<div>Separator</div>
<AzureInputSelect @bind-Value="@Model.Separator" name="@nameof(Model.Separator)">
<option checked="@(Model.Separator is null)" value="">(No separator)</option>
<option checked="@(Model.Separator is ".")" value=".">&#46;</option>
<option checked="@(Model.Separator is ",")" value=",">&#44;</option>
<option checked="@(Model.Separator is ":")" value=":">&#58;</option>
<option checked="@(Model.Separator is ";")" value=";">&#59;</option>
<option checked="@(Model.Separator is "/")" value="/">&#47;</option>
<option checked="@(Model.Separator is "-")" value="-">&#45;</option>
<option checked="@(Model.Separator is "_")" value="_">&#95;</option>
<option checked="@(Model.Separator is "__")" value="__">&#95;&#95;</option>
</AzureInputSelect>
</label>

<label class="flex flex-col gap-1">
<div>Prefix</div>
<AzureInputText @bind-Value="Model.Prefix" name="@nameof(Model.Prefix)" placeholder="(No prefix)"/>
</label>

<label class="flex flex-col gap-1">
<div>Label</div>
<AzureInputText @bind-Value="Model.Label" name="@nameof(Model.Label)" placeholder="(No label)"/>
</label>

<label class="flex flex-col gap-1">
<div>Content type</div>
<AzureInputSelect @bind-Value="@Model.ContentType" name="@nameof(Model.ContentType)">
<option checked="@(Model.ContentType is null)" value="">(No content type)</option>
<option checked="@(Model.ContentType is MediaType.SecretReference)" value="@MediaType.SecretReference">Key Vault Reference (@MediaType.SecretReference)</option>
<option checked="@(Model.ContentType is MediaType.Json)" value="@MediaType.Json">JSON (@MediaType.Json)</option>
</AzureInputSelect>
</label>

<div>
<AzureButton Appearance="AzureButton.AzureAppearance.Primary" Label="@(Model.Operation switch { Operation.Export => "Export", Operation.Import => "Import", _ => throw new ArgumentOutOfRangeException() })" Type="submit"/>
</div>
}
</EditForm>
</div>
</div>

@code {
[SupplyParameterFromForm] public InputModel? Model { get; set; }

protected override void OnInitialized()
{
Model ??= new InputModel();
}

private static IEnumerable<KeyValuePair<string, object?>> FlattenJsonElement(JsonElement element, string? prefix = null, string? separator = null)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
foreach (var property in element.EnumerateObject())
{
foreach (var pair in FlattenJsonElement(property.Value, !string.IsNullOrEmpty(prefix) ? $"{prefix}{separator}{property.Name}" : property.Name, separator))
{
yield return pair;
}
}

break;
case JsonValueKind.Array:
var index = 0;

foreach (var inner in element.EnumerateArray())
{
foreach (var pair in FlattenJsonElement(inner, !string.IsNullOrEmpty(prefix) ? $"{prefix}{separator}{index}" : index.ToString(), separator))
{
yield return pair;
}

index += 1;
}

break;
case JsonValueKind.String:
if (!string.IsNullOrEmpty(prefix))
{
yield return new KeyValuePair<string, object?>(prefix, element.GetString());
}

break;
case JsonValueKind.Number:
if (!string.IsNullOrEmpty(prefix))
{
yield return new KeyValuePair<string, object?>(prefix, element.GetDouble());
}

break;
case JsonValueKind.True:
case JsonValueKind.False:
if (!string.IsNullOrEmpty(prefix))
{
yield return new KeyValuePair<string, object?>(prefix, element.GetBoolean());
}

break;
case JsonValueKind.Undefined:
case JsonValueKind.Null:
if (!string.IsNullOrEmpty(prefix))
{
yield return new KeyValuePair<string, object?>(prefix, null);
}

break;
default:
throw new ArgumentOutOfRangeException();
}
}

private async Task HandleSubmit()
{
if (Model is { SourceFile: { } file })
{
await using var stream = file.OpenReadStream();

using var document = await JsonDocument.ParseAsync(stream);

foreach (var (key, value) in FlattenJsonElement(document.RootElement, Model.Prefix, Model.Separator))
{
if (await ConfigurationSettingRepository.Get(key, Model.Label ?? LabelFilter.Null).SingleOrDefaultAsync() is { } setting)
{
var date = DateTimeOffset.UtcNow;

setting = setting with
{
Etag = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(date.UtcDateTime.ToString("yyyy-MM-dd HH:mm:ss")))),
ContentType = Model.ContentType,
Value = value?.ToString(),
LastModified = date
};

await ConfigurationSettingRepository.Update(setting);
}
else
{
setting = ConfigurationSettingFactory.Create(key, Model.Label, Model.ContentType, value?.ToString());

await ConfigurationSettingRepository.Add(setting);
}
}

Model = new InputModel();
}
}

public class InputModel
{
public string? ContentType { get; set; }

public string? FileType { get; set; }

public string? Label { get; set; }

public Operation Operation { get; set; } = Operation.Import;

public string? Prefix { get; set; }

public string? Separator { get; set; }

public IBrowserFile? SourceFile { get; set; }

public SourceService? SourceService { get; set; }
}

public enum Operation
{
Export,
Import
}

public enum SourceService
{
AzureAppConfiguration,
AzureAppService,
ConfigurationFile
}

}
Loading
Loading