mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-22 13:57:50 +10:00
Effective config: unified save pipeline, descriptors, serialization, WriteFailed rollback
- EffectiveConfigFieldRef as value type; PendingDnsmasqChange/PendingOptionChange/PendingManagedHostsChange - Multi-file save plan (DnsmasqSaveWritePlan, DnsmasqManagedBackup) and backup/restore in save flow - Hosts integrated into effective-config save; Hosts.razor uses shared save modal - IEffectiveConfigDescriptorProvider + IEffectiveConfigFieldBuilder (DI); descriptors from EffectiveConfigSections - IEffectiveConfigPageEditor, shared descriptor resolution; addn-hosts/dhcp-host descriptor fix - DnsmasqConfText, IEffectiveConfigDirectiveSerializer; DnsmasqConfigService uses serializer for writes - IStructuredOptionValueHandler/Registry, DhcpHostOptionValueHandler; Dhcp.razor index-based DeleteRow fix - SaveService: roll back partial writes on failure; SaveModal: WriteFailed shows backups and Restore button
This commit is contained in:
+1
-1
@@ -31,7 +31,7 @@ public abstract class EffectiveConfigValueDisplayBase : ComponentBase
|
||||
/// <summary>Returns the value to display: pending change NewValue if this field has one, else descriptor value (not yet written to disk).</summary>
|
||||
protected object? GetEffectiveDisplayValue()
|
||||
{
|
||||
var pending = Session?.PendingChanges.FirstOrDefault(c =>
|
||||
var pending = Session?.PendingChanges.OfType<PendingOptionChange>().FirstOrDefault(c =>
|
||||
string.Equals(c.SectionId, Descriptor.SectionId, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(c.OptionName, Descriptor.OptionName, StringComparison.OrdinalIgnoreCase));
|
||||
return pending != null ? pending.NewValue : Descriptor.GetValue();
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
@namespace DnsmasqWebUI.Components.EffectiveConfig
|
||||
@using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Abstractions
|
||||
@using DnsmasqWebUI.Models.Dnsmasq
|
||||
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
||||
@inject IEffectiveConfigRenderFragmentRegistry RenderFragmentRegistry
|
||||
@inject IEffectiveConfigDescriptorProvider DescriptorProvider
|
||||
|
||||
<div class="ec-sections small">
|
||||
@{
|
||||
var views = EffectiveConfigViews.GetViewsForContext(Context);
|
||||
var descriptorsBySection = EffectiveConfigViews.GetDescriptorsBySection(Status, views, RenderFragmentRegistry);
|
||||
var descriptorsBySection = Status != null ? DescriptorProvider.GetBySection(Status, views) : new Dictionary<string, IReadOnlyList<EffectiveConfigFieldDescriptor>>();
|
||||
var hasSearch = !string.IsNullOrWhiteSpace(SearchTerm);
|
||||
var term = SearchTerm?.Trim() ?? "";
|
||||
}
|
||||
|
||||
+11
-1
@@ -5,6 +5,7 @@
|
||||
@using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation
|
||||
@inject IEffectiveConfigEditSession Session
|
||||
@inject IJSRuntime JSRuntime
|
||||
@implements IDisposable
|
||||
|
||||
@if (Status != null)
|
||||
{
|
||||
@@ -58,6 +59,7 @@
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Session.Changed += HandleSessionChanged;
|
||||
_uiContext = new EffectiveConfigUiContext
|
||||
{
|
||||
RequestOptionHelpAsync = HandleOptionHelpRequested,
|
||||
@@ -74,6 +76,13 @@
|
||||
};
|
||||
}
|
||||
|
||||
private void HandleSessionChanged() => _ = InvokeAsync(StateHasChanged);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Session.Changed -= HandleSessionChanged;
|
||||
}
|
||||
|
||||
private async Task OnSearchTermChanged(string value)
|
||||
{
|
||||
_searchTerm = value ?? "";
|
||||
@@ -191,7 +200,8 @@
|
||||
|
||||
private void RunCrossOptionEvaluator()
|
||||
{
|
||||
var issues = EffectiveConfigCrossOptionEvaluator.Evaluate(Status, Session.PendingChanges);
|
||||
var optionChanges = Session.PendingChanges.OfType<PendingOptionChange>().ToList();
|
||||
var issues = EffectiveConfigCrossOptionEvaluator.Evaluate(Status, optionChanges);
|
||||
Session.SetCrossOptionIssues(issues);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ else
|
||||
return;
|
||||
}
|
||||
|
||||
var pending = Session?.PendingChanges.FirstOrDefault(c =>
|
||||
var pending = Session?.PendingChanges.OfType<PendingOptionChange>().FirstOrDefault(c =>
|
||||
string.Equals(c.SectionId, Descriptor.SectionId, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(c.OptionName, Descriptor.OptionName, StringComparison.OrdinalIgnoreCase));
|
||||
if (pending?.OldValue is ExplicitToggleState previousState)
|
||||
|
||||
+1
-1
@@ -137,7 +137,7 @@ else
|
||||
_fragmentWithCallback = RenderFragmentRegistry.BuildFieldComponentFragment(Descriptor.SectionId, Descriptor.OptionName, EventCallback.Factory.Create<object?>(this, HandleValueChanged));
|
||||
_labelTooltip = DnsmasqOptionTooltips.Get(Descriptor.OptionName);
|
||||
_optionHelpKey = Descriptor.OptionName;
|
||||
var pending = Session?.PendingChanges.FirstOrDefault(c =>
|
||||
var pending = Session?.PendingChanges.OfType<PendingOptionChange>().FirstOrDefault(c =>
|
||||
string.Equals(c.SectionId, Descriptor.SectionId, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(c.OptionName, Descriptor.OptionName, StringComparison.OrdinalIgnoreCase));
|
||||
var descriptorValue = Descriptor.IsMultiValue
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
OnClose="@OnClose"
|
||||
OnContinueEditing="@(EventCallback.Factory.Create(this, HandleContinueEditing))"
|
||||
OnSave="@HandleSave"
|
||||
OnRestore="@HandleRestore"
|
||||
OnRestore="@HandleRestoreBackups"
|
||||
OnReverted="@HandleReverted"
|
||||
OnCloseAndEndEdit="@HandleCloseAndEndEdit" />
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
|
||||
[Parameter] public bool ShowModal { get; set; }
|
||||
[Parameter] public IReadOnlyList<PendingEffectiveConfigChange> PendingChanges { get; set; } = null!;
|
||||
[Parameter] public IReadOnlyList<PendingDnsmasqChange> PendingChanges { get; set; } = null!;
|
||||
[Parameter] public DnsmasqServiceStatus? Status { get; set; }
|
||||
[Parameter] public EventCallback OnClose { get; set; }
|
||||
/// <summary>Invoked when the modal reverts a change so the parent can re-run validation (e.g. cross-option evaluator).</summary>
|
||||
@@ -107,15 +107,15 @@
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task HandleRestore(string backupPath)
|
||||
private async Task HandleRestoreBackups(IReadOnlyList<DnsmasqManagedBackup> backups)
|
||||
{
|
||||
if (string.IsNullOrEmpty(backupPath)) return;
|
||||
if (backups == null || backups.Count == 0) return;
|
||||
|
||||
_saveState = SaveUiState.RestoreInProgress;
|
||||
_restoreResult = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
var result = await SaveService.RestoreAsync(backupPath);
|
||||
var result = await SaveService.RestoreAsync(backups);
|
||||
|
||||
_restoreResult = result;
|
||||
if (result.Restored && result.Restarted)
|
||||
|
||||
+146
-77
@@ -24,13 +24,19 @@
|
||||
<div class="alert alert-danger mb-3 d-flex align-items-center gap-2 ec-save-modal-alert">
|
||||
<i class="bi bi-exclamation-triangle-fill ec-save-modal-alert-icon" aria-hidden="true"></i>
|
||||
<div class="ec-save-modal-alert-text">
|
||||
<strong>Config was saved, but validation failed.</strong> Restart was not attempted. You can restore the backup or keep editing.
|
||||
<strong>Changes were saved, but validation failed.</strong> Restart was not attempted. You can restore the backups or keep editing.
|
||||
</div>
|
||||
</div>
|
||||
@if (SaveResult.BackupPath != null)
|
||||
@if (SaveResult.Backups.Count > 0)
|
||||
{
|
||||
<p class="ec-save-modal-backup-path mb-2">
|
||||
<span class="text-muted">Backup:</span> <code class="small ec-save-modal-backup-code">@SaveResult.BackupPath</code>
|
||||
<p class="ec-save-modal-backups mb-2">
|
||||
<span class="text-muted">Backups created:</span>
|
||||
<ul class="small ec-save-modal-backup-list mb-0 mt-1">
|
||||
@foreach (var b in SaveResult.Backups)
|
||||
{
|
||||
<li><code class="ec-save-modal-backup-code">@System.IO.Path.GetFileName(b.TargetPath)</code> → <code class="ec-save-modal-backup-code">@System.IO.Path.GetFileName(b.BackupPath)</code></li>
|
||||
}
|
||||
</ul>
|
||||
</p>
|
||||
}
|
||||
<div class="ec-save-modal-stderr mb-3">
|
||||
@@ -41,11 +47,25 @@
|
||||
}
|
||||
else if (SaveState == EffectiveConfigSaveFlow.SaveUiState.WriteFailed && SaveResult != null)
|
||||
{
|
||||
<div class="alert alert-danger mb-0">
|
||||
<strong>Save failed.</strong> @SaveResult.UserMessage
|
||||
@if (!string.IsNullOrEmpty(SaveResult.RestartStdErr))
|
||||
<div class="ec-save-modal-reload-failed">
|
||||
<div class="alert alert-danger mb-3 d-flex align-items-center gap-2 ec-save-modal-alert">
|
||||
<i class="bi bi-exclamation-triangle-fill ec-save-modal-alert-icon" aria-hidden="true"></i>
|
||||
<div class="ec-save-modal-alert-text">
|
||||
<strong>Save failed.</strong> @SaveResult.UserMessage
|
||||
@(SaveResult.Backups?.Count > 0 ? " You can restore the backups to revert any partial write, or continue editing." : "")
|
||||
</div>
|
||||
</div>
|
||||
@if (SaveResult.Backups?.Count > 0)
|
||||
{
|
||||
<pre class="ec-save-modal-pre border rounded p-2 bg-light small mt-2 mb-0">@SaveResult.RestartStdErr</pre>
|
||||
<p class="ec-save-modal-backups mb-2">
|
||||
<span class="text-muted">Backups created:</span>
|
||||
<ul class="small ec-save-modal-backup-list mb-0 mt-1">
|
||||
@foreach (var b in SaveResult.Backups)
|
||||
{
|
||||
<li><code class="ec-save-modal-backup-code">@System.IO.Path.GetFileName(b.TargetPath)</code> → <code class="ec-save-modal-backup-code">@System.IO.Path.GetFileName(b.BackupPath)</code></li>
|
||||
}
|
||||
</ul>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -55,13 +75,19 @@
|
||||
<div class="alert alert-danger mb-3 d-flex align-items-center gap-2 ec-save-modal-alert">
|
||||
<i class="bi bi-exclamation-triangle-fill ec-save-modal-alert-icon" aria-hidden="true"></i>
|
||||
<div class="ec-save-modal-alert-text">
|
||||
<strong>Config was saved, but the restart command failed.</strong> You can restore the backup or keep editing.
|
||||
<strong>Changes were saved, but the restart command failed.</strong> You can restore the backups or keep editing.
|
||||
</div>
|
||||
</div>
|
||||
@if (SaveResult.BackupPath != null)
|
||||
@if (SaveResult.Backups.Count > 0)
|
||||
{
|
||||
<p class="ec-save-modal-backup-path mb-2">
|
||||
<span class="text-muted">Backup:</span> <code class="small ec-save-modal-backup-code">@SaveResult.BackupPath</code>
|
||||
<p class="ec-save-modal-backups mb-2">
|
||||
<span class="text-muted">Backups created:</span>
|
||||
<ul class="small ec-save-modal-backup-list mb-0 mt-1">
|
||||
@foreach (var b in SaveResult.Backups)
|
||||
{
|
||||
<li><code class="ec-save-modal-backup-code">@System.IO.Path.GetFileName(b.TargetPath)</code> → <code class="ec-save-modal-backup-code">@System.IO.Path.GetFileName(b.BackupPath)</code></li>
|
||||
}
|
||||
</ul>
|
||||
</p>
|
||||
}
|
||||
<div class="ec-save-modal-stderr mb-3">
|
||||
@@ -115,10 +141,16 @@
|
||||
</div>
|
||||
}
|
||||
<p class="text-muted mb-3">These changes will be written to your config. Remove any you don't want to keep.</p>
|
||||
@if (ManagedFilePath != null)
|
||||
@if (TargetFileNames.Count > 0)
|
||||
{
|
||||
<p class="ec-save-modal-target mb-3">
|
||||
<span class="text-muted">Target file:</span> <code class="small">@ManagedFilePath</code>
|
||||
<p class="ec-save-modal-targets mb-3">
|
||||
<span class="text-muted">Targets:</span>
|
||||
<ul class="small mb-0 mt-1">
|
||||
@foreach (var name in TargetFileNames)
|
||||
{
|
||||
<li><code class="small">@name</code></li>
|
||||
}
|
||||
</ul>
|
||||
</p>
|
||||
}
|
||||
@if (PendingChanges.Count == 0)
|
||||
@@ -132,80 +164,97 @@
|
||||
<div class="ec-save-modal-changes list-group list-group-flush">
|
||||
@foreach (var c in PendingChanges)
|
||||
{
|
||||
var change = c;
|
||||
var oldList = AsStringList(change.OldValue);
|
||||
var newList = AsStringList(change.NewValue);
|
||||
var isMultiValue = oldList != null || newList != null;
|
||||
<div class="list-group-item ec-save-modal-item d-flex align-items-start gap-3 py-3 px-0 border-0 border-bottom">
|
||||
<div class="ec-save-modal-item-body flex-grow-1 min-width-0">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<StatusBadge Kind="StatusBadgeKind.NeutralPill" Text="@change.OptionName" Class="ec-save-modal-option me-2" />
|
||||
@if (!isMultiValue)
|
||||
{
|
||||
<span class="ec-save-modal-diff text-body">
|
||||
<span class="text-muted text-decoration-line-through">@FormatValue(change.OldValue, change.OptionName)</span>
|
||||
<span class="text-muted mx-1" aria-hidden="true">→</span>
|
||||
<span>@FormatValue(change.NewValue, change.OptionName)</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@if (isMultiValue)
|
||||
{
|
||||
var oldSeq = oldList ?? Array.Empty<string>();
|
||||
var newSeq = newList ?? Array.Empty<string>();
|
||||
var added = newSeq.Except(oldSeq).ToList();
|
||||
var removed = oldSeq.Except(newSeq).ToList();
|
||||
var orderOrDuplicatesChanged = added.Count == 0 && removed.Count == 0 && !oldSeq.SequenceEqual(newSeq, StringComparer.Ordinal);
|
||||
<div class="ec-save-modal-multidiff ps-3 mt-1 small">
|
||||
@if (orderOrDuplicatesChanged)
|
||||
@if (c is PendingOptionChange change)
|
||||
{
|
||||
var oldList = AsStringList(change.OldValue);
|
||||
var newList = AsStringList(change.NewValue);
|
||||
var isMultiValue = oldList != null || newList != null;
|
||||
<div class="list-group-item ec-save-modal-item d-flex align-items-start gap-3 py-3 px-0 border-0 border-bottom">
|
||||
<div class="ec-save-modal-item-body flex-grow-1 min-width-0">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<StatusBadge Kind="StatusBadgeKind.NeutralPill" Text="@change.OptionName" Class="ec-save-modal-option me-2" />
|
||||
@if (!isMultiValue)
|
||||
{
|
||||
<div class="text-muted">Order or duplicates changed:</div>
|
||||
<div class="text-muted text-decoration-line-through">Before: @string.Join(", ", oldSeq)</div>
|
||||
<div class="text-success">After: @string.Join(", ", newSeq)</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var val in removed)
|
||||
{
|
||||
<div class="text-muted text-decoration-line-through">− @val</div>
|
||||
}
|
||||
@foreach (var val in added)
|
||||
{
|
||||
<div class="text-success">+ @val</div>
|
||||
}
|
||||
<span class="ec-save-modal-diff text-body">
|
||||
<span class="text-muted text-decoration-line-through">@FormatValue(change.OldValue, change.OptionName)</span>
|
||||
<span class="text-muted mx-1" aria-hidden="true">→</span>
|
||||
<span>@FormatValue(change.NewValue, change.OptionName)</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (change.CurrentSourceFilePath != null)
|
||||
{
|
||||
<div class="ec-save-modal-override small text-muted mt-1">Overrides @System.IO.Path.GetFileName(change.CurrentSourceFilePath)</div>
|
||||
}
|
||||
@if (isMultiValue)
|
||||
{
|
||||
var oldSeq = oldList ?? Array.Empty<string>();
|
||||
var newSeq = newList ?? Array.Empty<string>();
|
||||
var added = newSeq.Except(oldSeq).ToList();
|
||||
var removed = oldSeq.Except(newSeq).ToList();
|
||||
var orderOrDuplicatesChanged = added.Count == 0 && removed.Count == 0 && !oldSeq.SequenceEqual(newSeq, StringComparer.Ordinal);
|
||||
<div class="ec-save-modal-multidiff ps-3 mt-1 small">
|
||||
@if (orderOrDuplicatesChanged)
|
||||
{
|
||||
<div class="text-muted">Order or duplicates changed:</div>
|
||||
<div class="text-muted text-decoration-line-through">Before: @string.Join(", ", oldSeq)</div>
|
||||
<div class="text-success">After: @string.Join(", ", newSeq)</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var val in removed)
|
||||
{
|
||||
<div class="text-muted text-decoration-line-through">− @val</div>
|
||||
}
|
||||
@foreach (var val in added)
|
||||
{
|
||||
<div class="text-success">+ @val</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (change.CurrentSourceFilePath != null)
|
||||
{
|
||||
<div class="ec-save-modal-override small text-muted mt-1">Overrides @System.IO.Path.GetFileName(change.CurrentSourceFilePath)</div>
|
||||
}
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger flex-shrink-0" @onclick="() => RevertOption(change)" title="Revert this change" aria-label="Revert @change.OptionName">
|
||||
<i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Revert
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger flex-shrink-0" @onclick="() => Revert(change)" title="Revert this change" aria-label="Revert @change.OptionName">
|
||||
<i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Revert
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
else if (c is PendingManagedHostsChange hostChange)
|
||||
{
|
||||
<div class="list-group-item ec-save-modal-item d-flex align-items-start gap-3 py-3 px-0 border-0 border-bottom">
|
||||
<div class="ec-save-modal-item-body flex-grow-1 min-width-0">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<StatusBadge Kind="StatusBadgeKind.NeutralPill" Text="Managed hosts" Class="ec-save-modal-option me-2" />
|
||||
<span class="ec-save-modal-diff text-body">@hostChange.OldEntries.Count entries → @hostChange.NewEntries.Count entries</span>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">@System.IO.Path.GetFileName(hostChange.ManagedHostsFilePath)</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger flex-shrink-0" @onclick="RevertManagedHosts" title="Revert managed hosts change" aria-label="Revert managed hosts">
|
||||
<i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Revert
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer ec-save-modal-footer">
|
||||
@if ((SaveState == EffectiveConfigSaveFlow.SaveUiState.ReloadFailed || SaveState == EffectiveConfigSaveFlow.SaveUiState.ValidateFailed) && SaveResult?.BackupPath != null)
|
||||
@if ((SaveState == EffectiveConfigSaveFlow.SaveUiState.ReloadFailed || SaveState == EffectiveConfigSaveFlow.SaveUiState.ValidateFailed || SaveState == EffectiveConfigSaveFlow.SaveUiState.WriteFailed) && SaveResult?.Backups?.Count > 0)
|
||||
{
|
||||
<button type="button" class="btn btn-primary" @onclick="() => Restore(SaveResult.BackupPath)"><i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Restore backup</button>
|
||||
<button type="button" class="btn btn-primary" @onclick="RestoreAll"><i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Restore backups</button>
|
||||
<button type="button" class="btn btn-outline-warning" @onclick="ContinueEditing"><i class="bi bi-pencil me-1" aria-hidden="true"></i>Continue editing</button>
|
||||
<button type="button" class="btn btn-danger" @onclick="CloseAndEndEdit"><i class="bi bi-x-circle me-1" aria-hidden="true"></i>Stop editing</button>
|
||||
}
|
||||
else if (SaveState == EffectiveConfigSaveFlow.SaveUiState.RestoreFailed)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-warning" @onclick="Close"><i class="bi bi-pencil me-1" aria-hidden="true"></i>Continue editing</button>
|
||||
<button type="button" class="btn btn-danger" @onclick="CloseAndEndEdit"><i class="bi bi-x-circle me-1" aria-hidden="true"></i>Stop editing</button>
|
||||
}
|
||||
else if (SaveState == EffectiveConfigSaveFlow.SaveUiState.WriteFailed)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-warning" @onclick="Close"><i class="bi bi-pencil me-1" aria-hidden="true"></i>Continue editing</button>
|
||||
}
|
||||
else if (SaveState == EffectiveConfigSaveFlow.SaveUiState.RestoreFailed)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-warning" @onclick="Close"><i class="bi bi-pencil me-1" aria-hidden="true"></i>Continue editing</button>
|
||||
<button type="button" class="btn btn-danger" @onclick="CloseAndEndEdit"><i class="bi bi-x-circle me-1" aria-hidden="true"></i>Stop editing</button>
|
||||
}
|
||||
else if (SaveState != EffectiveConfigSaveFlow.SaveUiState.Saving && SaveState != EffectiveConfigSaveFlow.SaveUiState.RestoreInProgress)
|
||||
{
|
||||
<button type="button" class="btn btn-secondary" @onclick="Close"><i class="bi bi-x me-1" aria-hidden="true"></i>Cancel</button>
|
||||
@@ -218,7 +267,7 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public IReadOnlyList<PendingEffectiveConfigChange> PendingChanges { get; set; } = null!;
|
||||
[Parameter] public IReadOnlyList<PendingDnsmasqChange> PendingChanges { get; set; } = null!;
|
||||
[Parameter] public DnsmasqServiceStatus? Status { get; set; }
|
||||
[Parameter] public IEffectiveConfigEditSession Session { get; set; } = null!;
|
||||
[Parameter] public EffectiveConfigSaveFlow.SaveUiState SaveState { get; set; }
|
||||
@@ -227,13 +276,26 @@
|
||||
[Parameter] public EventCallback OnClose { get; set; }
|
||||
[Parameter] public EventCallback OnContinueEditing { get; set; }
|
||||
[Parameter] public EventCallback OnSave { get; set; }
|
||||
[Parameter] public EventCallback<string> OnRestore { get; set; }
|
||||
[Parameter] public EventCallback<IReadOnlyList<DnsmasqManagedBackup>> OnRestore { get; set; }
|
||||
[Parameter] public EventCallback OnReverted { get; set; }
|
||||
[Parameter] public EventCallback OnCloseAndEndEdit { get; set; }
|
||||
|
||||
private bool AllowClose => SaveState != EffectiveConfigSaveFlow.SaveUiState.Saving && SaveState != EffectiveConfigSaveFlow.SaveUiState.RestoreInProgress;
|
||||
|
||||
private string? ManagedFilePath => Status?.ManagedFilePath;
|
||||
private IReadOnlyList<string> TargetFileNames => GetTargetFileNames();
|
||||
|
||||
private IReadOnlyList<string> GetTargetFileNames()
|
||||
{
|
||||
var list = new List<string>();
|
||||
if (Status == null) return list;
|
||||
var hasOptions = PendingChanges.OfType<PendingOptionChange>().Any();
|
||||
if (hasOptions && !string.IsNullOrEmpty(Status.ManagedFilePath))
|
||||
list.Add(System.IO.Path.GetFileName(Status.ManagedFilePath));
|
||||
var hostsChange = PendingChanges.OfType<PendingManagedHostsChange>().FirstOrDefault();
|
||||
if (hostsChange != null)
|
||||
list.Add(System.IO.Path.GetFileName(hostsChange.ManagedHostsFilePath));
|
||||
return list;
|
||||
}
|
||||
|
||||
private IReadOnlyList<FieldIssue> ValidationErrors => Session.GetValidationSummary().Where(i => i.Severity == FieldIssueSeverity.Error).ToList();
|
||||
private IReadOnlyList<FieldIssue> ValidationWarnings => Session.GetValidationSummary().Where(i => i.Severity == FieldIssueSeverity.Warning).ToList();
|
||||
@@ -300,12 +362,18 @@
|
||||
return s;
|
||||
}
|
||||
|
||||
private async Task Revert(PendingEffectiveConfigChange change)
|
||||
private async Task RevertOption(PendingOptionChange change)
|
||||
{
|
||||
Session.RevertChange(change.SectionId, change.OptionName);
|
||||
await OnReverted.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task RevertManagedHosts()
|
||||
{
|
||||
Session.RevertManagedHostsChange();
|
||||
await OnReverted.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task OnBackdropClick()
|
||||
{
|
||||
if (!AllowClose) return;
|
||||
@@ -329,9 +397,10 @@
|
||||
await OnSave.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task Restore(string backupPath)
|
||||
private async Task RestoreAll()
|
||||
{
|
||||
await OnRestore.InvokeAsync(backupPath);
|
||||
if (SaveResult == null || SaveResult.Backups.Count == 0) return;
|
||||
await OnRestore.InvokeAsync(SaveResult.Backups);
|
||||
}
|
||||
|
||||
private async Task CloseAndEndEdit()
|
||||
|
||||
Reference in New Issue
Block a user