mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-22 07:52:08 +10:00
Hosts page UX, draft/UI split, and managed hosts save summary
- Split effective-config session into Draft + Ui (IEffectiveConfigDraft, IEffectiveConfigUiState) - Hosts: managed overlay, edit mode from table, validation deferred to blur/Escape/Enter - Managed hosts save modal: line-level diff (add/remove/modify/order) - Hosts group headers: editable/read-only badges, empty-group row, Effective names help - hosts-ui.js for touch vs hover help; table inline bind oninput for live preview - HostsEntryEditValidator and ManagedHostsPendingChangeDiff with tests
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.OfType<PendingOptionChange>().FirstOrDefault(c =>
|
||||
var pending = Session?.Draft.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();
|
||||
|
||||
+25
-21
@@ -18,17 +18,17 @@
|
||||
var _allSectionsExpanded = _sectionViews.Count > 0 && _sectionViews.All(v => _openPanels.Contains(v.SectionId));
|
||||
}
|
||||
<EffectiveConfigToolbar ShowSearchBox="@ShowSearchBox" SearchTerm="@_searchTerm" SearchTermChanged="@OnSearchTermChanged"
|
||||
IsEditMode="@Session.IsEditMode" PendingChangesCount="@Session.PendingChanges.Count"
|
||||
ValidationSummary="@Session.GetValidationSummary()"
|
||||
IsEditMode="@Session.Ui.IsEditMode" PendingChangesCount="@Session.Draft.PendingChanges.Count"
|
||||
ValidationSummary="@Session.Draft.GetValidationSummary()"
|
||||
AllSectionsExpanded="@_allSectionsExpanded"
|
||||
OnExpandAll="ExpandAll" OnCollapseAll="CollapseAll" OnEnterEditMode="EnterEditMode" OnExitEditModeWithConfirm="ExitEditModeWithConfirmAsync" OnOpenSaveModal="OpenSaveModal" />
|
||||
<CascadingValue Value="@Session">
|
||||
<CascadingValue Value="@_uiContext">
|
||||
<EffectiveConfigPanels Status="@Status" Context="@Context" SearchTerm="@_searchTerm" OpenSectionIds="@_openPanels"
|
||||
OnSectionToggle="OnSectionToggle" IsEditMode="@Session.IsEditMode" ActiveFieldKey="@Session.ActiveFieldKey" />
|
||||
OnSectionToggle="OnSectionToggle" IsEditMode="@Session.Ui.IsEditMode" ActiveFieldKey="@Session.Ui.ActiveFieldKey" />
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
<EffectiveConfigSaveFlow ShowModal="@_showSaveModal" PendingChanges="@Session.PendingChanges" Status="@Status" Session="@Session"
|
||||
<EffectiveConfigSaveFlow ShowModal="@_showSaveModal" PendingChanges="@Session.Draft.PendingChanges" Status="@Status" Session="@Session"
|
||||
OnClose="CloseSaveModal" OnRevertedNotify="OnRevertedFromSaveModal" OnSaveCompleted="OnSaveCompleted" OnContinueEditingAfterSavedFailure="ContinueEditingAfterSavedFailureAsync" OnCloseAndEndEdit="CloseSaveModalAndEndEdit" />
|
||||
</div>
|
||||
}
|
||||
@@ -71,9 +71,9 @@
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
ActivateFieldAsync = fieldKey => { ActivateField(fieldKey); return Task.CompletedTask; },
|
||||
DeactivateFieldAsync = () => { Session.DeactivateField(); return InvokeAsync(StateHasChanged); },
|
||||
DeactivateFieldAsync = () => { Session.Ui.DeactivateField(); return InvokeAsync(StateHasChanged); },
|
||||
CommitFieldAsync = args => { HandleFieldEditCommitted(args); return Task.CompletedTask; },
|
||||
RevertFieldAsync = (sectionId, optionName) => { Session.RevertChange(sectionId, optionName); RunCrossOptionEvaluator(); return InvokeAsync(StateHasChanged); }
|
||||
RevertFieldAsync = (sectionId, optionName) => { Session.Draft.RevertChange(sectionId, optionName); RunCrossOptionEvaluator(); return InvokeAsync(StateHasChanged); }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,14 +154,14 @@
|
||||
|
||||
private void EnterEditMode()
|
||||
{
|
||||
Session.EnterEditMode();
|
||||
Session.Ui.EnterEditMode();
|
||||
RefreshCrossOptionAfterEnteringEditMode();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task ExitEditModeWithConfirmAsync()
|
||||
{
|
||||
if (Session.PendingChanges.Count > 0)
|
||||
if (Session.Draft.PendingChanges.Count > 0)
|
||||
{
|
||||
var module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/dialog.js");
|
||||
try
|
||||
@@ -174,41 +174,43 @@
|
||||
await module.DisposeAsync();
|
||||
}
|
||||
}
|
||||
Session.ExitEditModeDiscard();
|
||||
Session.Draft.DiscardAllDraft();
|
||||
Session.Ui.ExitEditMode();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void DeactivateActiveFieldIfClicked()
|
||||
{
|
||||
if (Session.IsEditMode && Session.ActiveFieldKey != null)
|
||||
if (Session.Ui.IsEditMode && Session.Ui.ActiveFieldKey != null)
|
||||
{
|
||||
Session.DeactivateField();
|
||||
Session.Ui.DeactivateField();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateField(string fieldKey)
|
||||
{
|
||||
Session.ActivateField(fieldKey);
|
||||
Session.Ui.ActivateField(fieldKey);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void HandleFieldEditCommitted(EffectiveConfigEditCommittedArgs args)
|
||||
{
|
||||
Session.TrackCommit(args);
|
||||
Session.Draft.TrackCommit(args);
|
||||
Session.Ui.DeactivateField();
|
||||
RunCrossOptionEvaluator();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void RunCrossOptionEvaluator()
|
||||
{
|
||||
var optionChanges = Session.PendingChanges.OfType<PendingOptionChange>().ToList();
|
||||
var optionChanges = Session.Draft.PendingChanges.OfType<PendingOptionChange>().ToList();
|
||||
var issues = CrossOptionValidation.Validate(Status, optionChanges);
|
||||
Session.SetCrossOptionIssues(issues);
|
||||
Session.Draft.SetCrossOptionIssues(issues);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IEffectiveConfigEditSession.EnterEditMode"/> clears cross-option issues; repopulate from disk + pending.
|
||||
/// After entering edit mode, repopulate cross-option issues from disk + pending.
|
||||
/// </summary>
|
||||
private void RefreshCrossOptionAfterEnteringEditMode()
|
||||
{
|
||||
@@ -224,8 +226,8 @@
|
||||
|
||||
private void OpenSaveModal()
|
||||
{
|
||||
var hasPending = Session.PendingChanges.Count > 0;
|
||||
var hasValidation = Session.GetValidationSummary().Count > 0;
|
||||
var hasPending = Session.Draft.PendingChanges.Count > 0;
|
||||
var hasValidation = Session.Draft.GetValidationSummary().Count > 0;
|
||||
if (!hasPending && !hasValidation)
|
||||
return;
|
||||
_showSaveModal = true;
|
||||
@@ -241,9 +243,10 @@
|
||||
private async Task ContinueEditingAfterSavedFailureAsync()
|
||||
{
|
||||
_showSaveModal = false;
|
||||
Session.ExitEditModeDiscard();
|
||||
Session.Draft.DiscardAllDraft();
|
||||
Session.Ui.ExitEditMode();
|
||||
await OnSaveCompleted.InvokeAsync();
|
||||
Session.EnterEditMode();
|
||||
Session.Ui.EnterEditMode();
|
||||
RefreshCrossOptionAfterEnteringEditMode();
|
||||
StateHasChanged();
|
||||
}
|
||||
@@ -251,7 +254,8 @@
|
||||
private void CloseSaveModalAndEndEdit()
|
||||
{
|
||||
_showSaveModal = false;
|
||||
Session.ExitEditModeDiscard();
|
||||
Session.Draft.DiscardAllDraft();
|
||||
Session.Ui.ExitEditMode();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -262,6 +262,18 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Multivalue display mode: editable entry row (no list number; align with list content) */
|
||||
::deep .ec-field-value-list li.ec-multi-list-entry-point {
|
||||
list-style: none;
|
||||
margin-left: -1.25rem;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
::deep .ec-field-value-list li.ec-multi-list-empty-hint {
|
||||
list-style: none;
|
||||
margin-left: -1.25rem;
|
||||
}
|
||||
|
||||
/* Long paths/URLs wrap; allow break anywhere so nothing is clipped */
|
||||
::deep .ec-field-value-item {
|
||||
display: inline;
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ else
|
||||
return;
|
||||
}
|
||||
|
||||
var pending = Session?.PendingChanges.OfType<PendingOptionChange>().FirstOrDefault(c =>
|
||||
var pending = Session?.Draft.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)
|
||||
|
||||
+4
-4
@@ -58,7 +58,7 @@ else if (Descriptor.IsMultiValue)
|
||||
</CascadingValue>
|
||||
</span>
|
||||
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@_multiValueRowSource" OptionName="@Descriptor.OptionName" Value="@_multiValueItems"
|
||||
IsInteractive="@(IsEditMode && !Descriptor.IsCapabilityDisabled)" ShowEditableBadge="@(IsEditMode && !Descriptor.IsCapabilityDisabled)" HasPendingChange="@_multiValueHasPendingOrDraft" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@(IsEditMode && IsActive && !Descriptor.IsCapabilityDisabled)"
|
||||
IsInteractive="@(IsEditMode && !Descriptor.IsCapabilityDisabled)" ShowEditableBadge="@(!Descriptor.IsCapabilityDisabled)" HasPendingChange="@_multiValueHasPendingOrDraft" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@(IsEditMode && IsActive && !Descriptor.IsCapabilityDisabled)"
|
||||
IsCapabilityDisabled="@Descriptor.IsCapabilityDisabled" CapabilityDisabledReason="@Descriptor.CapabilityDisabledReason"
|
||||
OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||
</li>
|
||||
@@ -68,7 +68,7 @@ else if (Descriptor.IsMultiValue)
|
||||
@* Multi-value state is computed in OnParametersSet and in HandleMultiValueChanged; we only read cached fields here. *@
|
||||
<EffectiveConfigMultiValueRow Descriptor="@Descriptor" Label="@Descriptor.OptionName" Items="@_multiValueItems" LabelTooltip="@_labelTooltip" OptionHelpKey="@_optionHelpKey" OptionHelpAnchorId="@_anchorId"
|
||||
IsActiveEditor="@(IsEditMode && IsActive && !Descriptor.IsCapabilityDisabled)" IsEditMode="@IsEditMode" ReadonlyBadgeAnchorIdBase="@_readonlyBadgeAnchorId"
|
||||
Source="@_multiValueRowSource" ShowEditableBadge="@(IsEditMode && !Descriptor.IsCapabilityDisabled)" HasPendingChange="@_multiValueHasPendingOrDraft" FieldKey="@FieldKey"
|
||||
Source="@_multiValueRowSource" ShowEditableBadge="@(!Descriptor.IsCapabilityDisabled)" HasPendingChange="@_multiValueHasPendingOrDraft" FieldKey="@FieldKey"
|
||||
ItemsChanged="@HandleMultiValueChanged" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||
}
|
||||
}
|
||||
@@ -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.OfType<PendingOptionChange>().FirstOrDefault(c =>
|
||||
var pending = Session?.Draft.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
|
||||
@@ -305,7 +305,7 @@ else
|
||||
{
|
||||
_draftValue = null;
|
||||
_hasDraftValue = false;
|
||||
Session?.RevertChange(Descriptor.SectionId, Descriptor.OptionName);
|
||||
Session?.Draft.RevertChange(Descriptor.SectionId, Descriptor.OptionName);
|
||||
_exitAction = ExitAction.Revert;
|
||||
if (Ui != null)
|
||||
await Ui.DeactivateFieldAsync();
|
||||
|
||||
+10
-7
@@ -2,12 +2,17 @@
|
||||
@using DnsmasqWebUI.Models.Dnsmasq
|
||||
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
||||
|
||||
<span class="ec-field-value-item" title="@(ReadonlyClickActivatesField ? ReadonlyTitleOverride : Source?.GetReadOnlyTooltip())">
|
||||
<span class="ec-field-value-item" title="@Source?.GetReadOnlyTooltip()">
|
||||
@Value
|
||||
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@OptionName" Value="@Value"
|
||||
IsInteractive="@(IsEditMode && !ReadonlyClickActivatesField && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))"
|
||||
ShowEditableBadge="@(Source?.IsReadOnly != true)" EditableAsButton="true"
|
||||
FieldKey="@FieldKey" ReadonlyClickActivatesField="@ReadonlyClickActivatesField" ReadonlyTitleOverride="@ReadonlyTitleOverride" />
|
||||
<OptionStateBadge
|
||||
AnchorId="@(ReadonlyBadgeAnchorId ?? "")"
|
||||
Source="@Source"
|
||||
OptionName="@OptionName"
|
||||
Value="@Value"
|
||||
IsInteractive="@(IsEditMode && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))"
|
||||
ShowEditableBadge="@(Source?.IsReadOnly != true)"
|
||||
EditableAsButton="true"
|
||||
FieldKey="@FieldKey" />
|
||||
</span>
|
||||
|
||||
@code {
|
||||
@@ -17,6 +22,4 @@
|
||||
[Parameter] public bool IsEditMode { get; set; }
|
||||
[Parameter] public string? ReadonlyBadgeAnchorId { get; set; }
|
||||
[Parameter] public string? FieldKey { get; set; }
|
||||
[Parameter] public bool ReadonlyClickActivatesField { get; set; }
|
||||
[Parameter] public string? ReadonlyTitleOverride { get; set; }
|
||||
}
|
||||
|
||||
+55
-13
@@ -25,22 +25,64 @@
|
||||
else
|
||||
{
|
||||
<div class="ec-field-value ec-field-value-multi">
|
||||
@if (Items?.Count > 0)
|
||||
{
|
||||
<ol class="ec-field-value-list">
|
||||
<ol class="ec-field-value-list">
|
||||
@if (Items?.Count > 0)
|
||||
{
|
||||
@foreach (var (item, index) in Items!.Select((item, index) => (item, index)))
|
||||
{
|
||||
var anchorId = ReadonlyBadgeAnchorIdBase != null ? ReadonlyBadgeAnchorIdBase + "-" + index : "";
|
||||
<li><EffectiveConfigMultiValueItem Value="@item.Value" Source="@item.Source" OptionName="@Label" IsEditMode="@IsEditMode" ReadonlyBadgeAnchorId="@anchorId" FieldKey="@FieldKey" ReadonlyClickActivatesField="true" ReadonlyTitleOverride="New values can still be added. Click to edit." /></li>
|
||||
var anchorId = ReadonlyBadgeAnchorIdBase != null
|
||||
? $"{ReadonlyBadgeAnchorIdBase}-{index}"
|
||||
: "";
|
||||
<li>
|
||||
<EffectiveConfigMultiValueItem
|
||||
Value="@item.Value"
|
||||
Source="@item.Source"
|
||||
OptionName="@Label"
|
||||
IsEditMode="@IsEditMode"
|
||||
ReadonlyBadgeAnchorId="@anchorId"
|
||||
FieldKey="@FieldKey" />
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">(none)</span>
|
||||
<OptionStateBadge OptionName="@Label" ShowEditableBadge="@(!(Descriptor?.IsCapabilityDisabled == true))" EditableAsButton="true"
|
||||
FieldKey="@FieldKey" IsActiveEditor="false" IsInteractive="true" IsCapabilityDisabled="@(Descriptor?.IsCapabilityDisabled == true)" CapabilityDisabledReason="@Descriptor?.CapabilityDisabledReason" />
|
||||
}
|
||||
}
|
||||
@if (ShowEditableBadge && (Items?.Count ?? 0) > 0)
|
||||
{
|
||||
<li class="ec-multi-list-entry-point">
|
||||
<OptionStateBadge
|
||||
OptionName="@Label"
|
||||
ShowEditableBadge="true"
|
||||
EditableAsButton="true"
|
||||
FieldKey="@FieldKey"
|
||||
IsActiveEditor="false"
|
||||
IsInteractive="true"
|
||||
HasPendingChange="@HasPendingChange"
|
||||
IsCapabilityDisabled="@(Descriptor?.IsCapabilityDisabled == true)"
|
||||
CapabilityDisabledReason="@Descriptor?.CapabilityDisabledReason" />
|
||||
</li>
|
||||
}
|
||||
else if ((Items?.Count ?? 0) == 0)
|
||||
{
|
||||
<li class="ec-multi-list-empty-hint">
|
||||
@if (ShowEditableBadge)
|
||||
{
|
||||
<span class="text-muted">(none)</span>
|
||||
<OptionStateBadge
|
||||
OptionName="@Label"
|
||||
ShowEditableBadge="true"
|
||||
EditableAsButton="true"
|
||||
FieldKey="@FieldKey"
|
||||
IsActiveEditor="false"
|
||||
IsInteractive="true"
|
||||
HasPendingChange="@HasPendingChange"
|
||||
IsCapabilityDisabled="@(Descriptor?.IsCapabilityDisabled == true)"
|
||||
CapabilityDisabledReason="@Descriptor?.CapabilityDisabledReason" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">(none)</span>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
|
||||
private async Task HandleSave()
|
||||
{
|
||||
if (Session.PendingChanges.Count == 0) return;
|
||||
if (Session.HasBlockingValidationErrors())
|
||||
if (Session.Draft.PendingChanges.Count == 0) return;
|
||||
if (Session.Draft.HasBlockingValidationErrors())
|
||||
{
|
||||
NotificationService.ShowError("Fix validation errors before saving.");
|
||||
return;
|
||||
@@ -81,14 +81,16 @@
|
||||
_restoreResult = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
var result = await Session.ApplyAsync();
|
||||
var result = await Session.Draft.ApplyAsync();
|
||||
|
||||
_saveResult = result;
|
||||
if (result.Saved)
|
||||
{
|
||||
// Changes were written to disk, so clear them from session state.
|
||||
// Whether to stay in edit mode depends on whether restart succeeded.
|
||||
Session.AcceptAppliedChanges(stayInEditMode: result.IsValidateFailed || !result.Restarted);
|
||||
Session.Draft.AcceptAppliedChanges();
|
||||
if (!result.IsValidateFailed && result.Restarted)
|
||||
Session.Ui.ExitEditMode();
|
||||
else
|
||||
Session.Ui.EnterEditMode();
|
||||
}
|
||||
|
||||
if (result.Saved && result.Restarted)
|
||||
@@ -127,8 +129,8 @@
|
||||
_restoreResult = result;
|
||||
if (result.Restored)
|
||||
{
|
||||
// Restore wrote to disk, so clear any pending changes from session.
|
||||
Session.AcceptAppliedChanges(stayInEditMode: false);
|
||||
Session.Draft.AcceptAppliedChanges();
|
||||
Session.Ui.ExitEditMode();
|
||||
}
|
||||
|
||||
if (result.Restored && result.Restarted)
|
||||
|
||||
@@ -228,13 +228,39 @@
|
||||
}
|
||||
else if (c is PendingManagedHostsChange hostChange)
|
||||
{
|
||||
var hostLineDiffs = ManagedHostsPendingChangeDiff.Build(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">
|
||||
<div class="d-flex align-items-center flex-wrap 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>
|
||||
<span class="ec-save-modal-diff text-muted">@hostChange.OldEntries.Count → @hostChange.NewEntries.Count entries</span>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">@System.IO.Path.GetFileName(hostChange.ManagedHostsFilePath)</div>
|
||||
@if (hostLineDiffs.Count > 0)
|
||||
{
|
||||
<div class="ec-save-modal-managed-hosts-diff small mt-2 ps-2 border-start border-secondary border-opacity-25">
|
||||
@foreach (var d in hostLineDiffs)
|
||||
{
|
||||
@if (d.Kind == ManagedHostsLineDiffKind.Added)
|
||||
{
|
||||
<div class="text-success font-monospace text-break">+ @d.AfterLine</div>
|
||||
}
|
||||
else if (d.Kind == ManagedHostsLineDiffKind.Removed)
|
||||
{
|
||||
<div class="text-muted text-decoration-line-through font-monospace text-break">− @d.BeforeLine</div>
|
||||
}
|
||||
else if (d.Kind == ManagedHostsLineDiffKind.Modified)
|
||||
{
|
||||
<div class="text-muted text-decoration-line-through font-monospace text-break">~ @d.BeforeLine</div>
|
||||
<div class="text-success font-monospace text-break ms-1">→ @d.AfterLine</div>
|
||||
}
|
||||
else if (d.Kind == ManagedHostsLineDiffKind.OrderOnly)
|
||||
{
|
||||
<div class="text-muted fst-italic">Line order changed (same entries).</div>
|
||||
}
|
||||
}
|
||||
</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
|
||||
@@ -304,9 +330,9 @@
|
||||
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();
|
||||
private bool HasBlockingValidationErrors => Session.HasBlockingValidationErrors();
|
||||
private IReadOnlyList<FieldIssue> ValidationErrors => Session.Draft.GetValidationSummary().Where(i => i.Severity == FieldIssueSeverity.Error).ToList();
|
||||
private IReadOnlyList<FieldIssue> ValidationWarnings => Session.Draft.GetValidationSummary().Where(i => i.Severity == FieldIssueSeverity.Warning).ToList();
|
||||
private bool HasBlockingValidationErrors => Session.Draft.HasBlockingValidationErrors();
|
||||
|
||||
private static string GetValidationFailedOutput(EffectiveConfigSaveResult result)
|
||||
{
|
||||
@@ -371,13 +397,13 @@
|
||||
|
||||
private async Task RevertOption(PendingOptionChange change)
|
||||
{
|
||||
Session.RevertChange(change.SectionId, change.OptionName);
|
||||
Session.Draft.RevertChange(change.SectionId, change.OptionName);
|
||||
await OnReverted.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task RevertManagedHosts()
|
||||
{
|
||||
Session.RevertManagedHostsChange();
|
||||
Session.Draft.RevertManagedHostsDraft();
|
||||
await OnReverted.InvokeAsync();
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,15 @@
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.ec-save-modal-managed-hosts-diff {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.ec-save-modal-managed-hosts-diff > div + div {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.ec-save-modal-override {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
@@ -11,15 +11,8 @@
|
||||
}
|
||||
else if (Source?.IsReadOnly == true)
|
||||
{
|
||||
var readonlyTitle = !string.IsNullOrEmpty(ReadonlyTitleOverride) ? ReadonlyTitleOverride : Source?.GetReadOnlyTooltip();
|
||||
if (ReadonlyClickActivatesField && Ui != null && !string.IsNullOrEmpty(FieldKey))
|
||||
{
|
||||
<StatusBadge Kind="StatusBadgeKind.ReadOnly" Text="readonly" Clickable="true"
|
||||
Id="@AnchorId" Title="@readonlyTitle"
|
||||
OnClick="OnEditableClick"
|
||||
Class="status-badge-inline" />
|
||||
}
|
||||
else if (IsInteractive && Ui != null)
|
||||
var readonlyTitle = Source?.GetReadOnlyTooltip();
|
||||
if (IsInteractive && Ui != null)
|
||||
{
|
||||
<StatusBadge Kind="StatusBadgeKind.ReadOnly" Text="readonly" Clickable="true"
|
||||
Id="@AnchorId" Title="@readonlyTitle"
|
||||
@@ -88,10 +81,6 @@ else if (ShowEditableBadge)
|
||||
[Parameter] public string? FieldKey { get; set; }
|
||||
/// <summary>Optional: when set, editable-badge click invokes this instead of activating the field by key (e.g. list editor per-item edit).</summary>
|
||||
[Parameter] public EventCallback OnEditableClickRequested { get; set; }
|
||||
/// <summary>When true, readonly badge click activates the field for editing instead of showing the readonly popover (e.g. for multi-value where new items can still be added).</summary>
|
||||
[Parameter] public bool ReadonlyClickActivatesField { get; set; }
|
||||
/// <summary>Optional tooltip for readonly badge when <see cref="ReadonlyClickActivatesField"/> is true (e.g. "New values can still be added. Click to edit.").</summary>
|
||||
[Parameter] public string? ReadonlyTitleOverride { get; set; }
|
||||
/// <summary>When true, option is disabled because dnsmasq build does not support it (e.g. no DNSSEC).</summary>
|
||||
[Parameter] public bool IsCapabilityDisabled { get; set; }
|
||||
/// <summary>Tooltip shown when <see cref="IsCapabilityDisabled"/> is true (e.g. "This dnsmasq build does not support DNSSEC; option not written.").</summary>
|
||||
|
||||
Reference in New Issue
Block a user