merge wt/wp2 into v3/batch2-raw-ui (Wave A)
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
<NavRailItem Href="/hosts" Text="Host status" />
|
<NavRailItem Href="/hosts" Text="Host status" />
|
||||||
<NavRailItem Href="/clusters" Text="Clusters" />
|
<NavRailItem Href="/clusters" Text="Clusters" />
|
||||||
<NavRailItem Href="/uns" Text="UNS" />
|
<NavRailItem Href="/uns" Text="UNS" />
|
||||||
|
<NavRailItem Href="/raw" Text="Raw" />
|
||||||
<NavRailItem Href="/reservations" Text="Reservations" />
|
<NavRailItem Href="/reservations" Text="Reservations" />
|
||||||
<NavRailItem Href="/certificates" Text="Certificates" />
|
<NavRailItem Href="/certificates" Text="Certificates" />
|
||||||
<NavRailItem Href="/role-grants" Text="Role grants" />
|
<NavRailItem Href="/role-grants" Text="Role grants" />
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
@page "/raw"
|
||||||
|
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||||
|
@rendermode RenderMode.InteractiveServer
|
||||||
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||||
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw
|
||||||
|
@inject IRawTreeService Svc
|
||||||
|
|
||||||
|
@* Peer of GlobalUns (/uns) for the v3 Raw project tree — the cluster-rooted
|
||||||
|
Enterprise → Cluster → Folder → Driver → Device → TagGroup → Tag hierarchy.
|
||||||
|
Loads the eager roots on init, then hands off to RawTree, which lazily expands
|
||||||
|
each container via IRawTreeService.LoadChildrenAsync and surfaces per-node
|
||||||
|
context menus (real create/configure/tag modals arrive in later Batch-2 waves). *@
|
||||||
|
|
||||||
|
<PageTitle>Raw</PageTitle>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Raw</h4>
|
||||||
|
<span class="text-muted small">Changes apply on the next deployment.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="panel rise" style="animation-delay:.02s">
|
||||||
|
<div class="panel-head d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<span>Project tree</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (_loading)
|
||||||
|
{
|
||||||
|
<div style="padding:1rem" class="text-muted">
|
||||||
|
<span class="spinner-border spinner-border-sm me-1"></span>Loading…
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_loadError is not null)
|
||||||
|
{
|
||||||
|
<div style="padding:1rem" class="text-danger">@_loadError</div>
|
||||||
|
}
|
||||||
|
else if (_roots.Count == 0)
|
||||||
|
{
|
||||||
|
<div style="padding:1rem" class="text-muted">No clusters yet.</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div style="padding:.5rem 1rem">
|
||||||
|
<RawTree Roots="_roots" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private IReadOnlyList<RawNode> _roots = Array.Empty<RawNode>();
|
||||||
|
private bool _loading = true;
|
||||||
|
private string? _loadError;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_roots = await Svc.LoadRootsAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_loadError = $"Failed to load the Raw tree: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
@* Placeholder modal for the /raw context-menu actions whose real dialogs
|
||||||
|
(Configure / Device / TagModal / CSV import-export / Browse device / Rename)
|
||||||
|
land in later Batch-2 waves (B/C). This wave wires every menu action to a
|
||||||
|
named handler that opens this "coming soon" placeholder, so the tree + lazy
|
||||||
|
expansion + menus can be live-verified without the real modals existing yet.
|
||||||
|
Wave B/C replaces each handler body with the real modal invocation and can
|
||||||
|
then delete this component. Markup mirrors the GlobalUns delete-confirm modal. *@
|
||||||
|
|
||||||
|
@if (Visible)
|
||||||
|
{
|
||||||
|
<div class="modal-backdrop fade show" style="display:block"></div>
|
||||||
|
<div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">@Title</h5>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" @onclick="OnClose"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>@Message</p>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(NodeName))
|
||||||
|
{
|
||||||
|
<p class="text-muted small mb-0">Target: <span class="mono">@NodeName</span></p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" @onclick="OnClose">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>Whether the placeholder is shown.</summary>
|
||||||
|
[Parameter] public bool Visible { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Modal title — the action name (e.g. "Configure driver").</summary>
|
||||||
|
[Parameter] public string Title { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>"Coming soon" body message.</summary>
|
||||||
|
[Parameter] public string Message { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>The target node's display name, shown as context.</summary>
|
||||||
|
[Parameter] public string? NodeName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Raised when the placeholder is dismissed.</summary>
|
||||||
|
[Parameter] public EventCallback OnClose { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
@* Recursive, lazily-expanding renderer for the v3 Raw project tree (/raw).
|
||||||
|
|
||||||
|
Unlike UnsTree (which eager-loads the whole structural tree and never touches
|
||||||
|
the RawNode lazy fields), THIS component actually exercises the lazy plumbing:
|
||||||
|
expanding an unloaded container calls IRawTreeService.LoadChildrenAsync, appends
|
||||||
|
the returned level into node.Children and flips Loaded/Loading, showing a spinner
|
||||||
|
in flight and an error row on failure. Per-node context menus (right-click surface
|
||||||
|
+ "⋯" fallback) are built per RawNodeKind; every action opens a placeholder
|
||||||
|
(RawStubModal) in this wave — Wave B/C replaces each named handler's body with the
|
||||||
|
real Configure / Device / TagModal / CSV / Browse modal. This component is a single
|
||||||
|
instance that recurses via the RenderNode RenderFragment, so it owns one stub modal
|
||||||
|
and one injected service for the whole tree. *@
|
||||||
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||||
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw
|
||||||
|
@inject IRawTreeService Svc
|
||||||
|
|
||||||
|
@foreach (var root in Roots)
|
||||||
|
{
|
||||||
|
@RenderNode(root, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
<RawStubModal Visible="_stubVisible" Title="@_stubTitle" Message="@_stubMessage"
|
||||||
|
NodeName="@_stubNodeName" OnClose="CloseStub" />
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>The top-level Raw nodes to render (the enterprise roots from <c>LoadRootsAsync</c>).</summary>
|
||||||
|
[Parameter, EditorRequired] public IReadOnlyList<RawNode> Roots { get; set; } = default!;
|
||||||
|
|
||||||
|
// --- Stub-placeholder state (one instance for the whole recursive tree) ---
|
||||||
|
private bool _stubVisible;
|
||||||
|
private string _stubTitle = "";
|
||||||
|
private string _stubMessage = "";
|
||||||
|
private string? _stubNodeName;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Row rendering
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private RenderFragment RenderNode(RawNode node, int depth) => __builder =>
|
||||||
|
{
|
||||||
|
var indent = $"padding-left:{depth * 18}px";
|
||||||
|
var hasExpander = node.HasLazyChildren || node.Children.Count > 0;
|
||||||
|
var muted = node.Enabled == false && node.Kind is RawNodeKind.Driver or RawNodeKind.Device;
|
||||||
|
var items = MenuFor(node);
|
||||||
|
|
||||||
|
<div @key="node.Key" class="d-flex align-items-center gap-1 py-1" style="@indent">
|
||||||
|
@if (hasExpander)
|
||||||
|
{
|
||||||
|
<button type="button" class="btn btn-sm btn-link p-0" style="width:18px"
|
||||||
|
@onclick="@(() => ToggleAsync(node))">
|
||||||
|
@(node.Expanded ? "▼" : "▶")
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span style="width:18px"></span>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (items.Count > 0)
|
||||||
|
{
|
||||||
|
<ContextMenu Items="items" ShowEllipsis="true">
|
||||||
|
@RenderRowBody(node, muted)
|
||||||
|
</ContextMenu>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@RenderRowBody(node, muted)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (node.Expanded && node.Loading)
|
||||||
|
{
|
||||||
|
<div class="small text-muted" style="@($"padding-left:{(depth + 1) * 18}px")">
|
||||||
|
<span class="spinner-border spinner-border-sm me-1"></span>Loading…
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (node.Expanded && node.Error is not null)
|
||||||
|
{
|
||||||
|
<div class="small text-danger" style="@($"padding-left:{(depth + 1) * 18}px")">
|
||||||
|
@node.Error
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (node.Expanded)
|
||||||
|
{
|
||||||
|
@foreach (var child in node.Children)
|
||||||
|
{
|
||||||
|
@RenderNode(child, depth + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The icon + label + badge shown for a node — the ContextMenu right-click surface.
|
||||||
|
private RenderFragment RenderRowBody(RawNode node, bool muted) => __builder =>
|
||||||
|
{
|
||||||
|
<span class="d-inline-flex align-items-center gap-1">
|
||||||
|
<span aria-hidden="true">@KindIcon(node.Kind)</span>
|
||||||
|
<span class="mono small @(muted ? "text-muted" : "")">@node.DisplayName</span>
|
||||||
|
@if (muted)
|
||||||
|
{
|
||||||
|
<span class="chip chip-idle ms-1" style="font-size:0.65rem">disabled</span>
|
||||||
|
}
|
||||||
|
@if (node.ChildCount > 0)
|
||||||
|
{
|
||||||
|
<span class="chip chip-idle ms-1" style="font-size:0.7rem">@node.ChildCount</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string KindIcon(RawNodeKind kind) => kind switch
|
||||||
|
{
|
||||||
|
RawNodeKind.Enterprise => "🏢",
|
||||||
|
RawNodeKind.Cluster => "🖥️",
|
||||||
|
RawNodeKind.Folder => "📁",
|
||||||
|
RawNodeKind.Driver => "🔌",
|
||||||
|
RawNodeKind.Device => "📟",
|
||||||
|
RawNodeKind.TagGroup => "🗂️",
|
||||||
|
RawNodeKind.Tag => "🏷️",
|
||||||
|
_ => "•",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Lazy expansion — the whole point of WP2 (UNS never did this).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggles a node's expansion. On first expand of an unloaded container this sets
|
||||||
|
/// <c>Loading</c>, awaits <see cref="IRawTreeService.LoadChildrenAsync"/>, appends the
|
||||||
|
/// returned level into <c>Children</c>, flips <c>Loaded</c>, and re-renders — showing a
|
||||||
|
/// spinner in flight and an error row on failure. A leaf (Tag) has no expander so never
|
||||||
|
/// reaches here.
|
||||||
|
/// </summary>
|
||||||
|
private async Task ToggleAsync(RawNode node)
|
||||||
|
{
|
||||||
|
if (node.Loading) { return; }
|
||||||
|
|
||||||
|
node.Expanded = !node.Expanded;
|
||||||
|
|
||||||
|
if (node.Expanded && node.HasLazyChildren && !node.Loaded)
|
||||||
|
{
|
||||||
|
node.Loading = true;
|
||||||
|
node.Error = null;
|
||||||
|
StateHasChanged();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var children = await Svc.LoadChildrenAsync(node);
|
||||||
|
node.Children.Clear();
|
||||||
|
node.Children.AddRange(children);
|
||||||
|
node.ChildCount = node.Children.Count;
|
||||||
|
node.Loaded = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
node.Error = ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
node.Loading = false;
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Per-kind context menus. Every OnClick opens a placeholder in this wave; the
|
||||||
|
// named handlers below are the seam Wave B/C replaces with the real modals.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private IReadOnlyList<ContextMenuItem> MenuFor(RawNode node) => node.Kind switch
|
||||||
|
{
|
||||||
|
RawNodeKind.Cluster => ClusterMenu(node),
|
||||||
|
RawNodeKind.Folder => FolderMenu(node),
|
||||||
|
RawNodeKind.Driver => DriverMenu(node),
|
||||||
|
RawNodeKind.Device => DeviceMenu(node),
|
||||||
|
RawNodeKind.TagGroup => TagGroupMenu(node),
|
||||||
|
RawNodeKind.Tag => TagMenu(node),
|
||||||
|
_ => Array.Empty<ContextMenuItem>(), // Enterprise: label only, no menu.
|
||||||
|
};
|
||||||
|
|
||||||
|
private List<ContextMenuItem> ClusterMenu(RawNode node) => new()
|
||||||
|
{
|
||||||
|
new() { Label = "New folder", Icon = "📁", OnClick = () => OnNewFolder(node) },
|
||||||
|
new() { Label = "New driver", Icon = "🔌", OnClick = () => OnNewDriver(node) },
|
||||||
|
};
|
||||||
|
|
||||||
|
private List<ContextMenuItem> FolderMenu(RawNode node) => new()
|
||||||
|
{
|
||||||
|
new() { Label = "New folder", Icon = "📁", OnClick = () => OnNewFolder(node) },
|
||||||
|
new() { Label = "New driver", Icon = "🔌", OnClick = () => OnNewDriver(node) },
|
||||||
|
ContextMenuItem.Separator(),
|
||||||
|
new() { Label = "Rename", Icon = "✏️", OnClick = () => OnRenameFolder(node) },
|
||||||
|
new() { Label = "Delete", Icon = "🗑️", OnClick = () => OnDeleteFolder(node) },
|
||||||
|
};
|
||||||
|
|
||||||
|
private List<ContextMenuItem> DriverMenu(RawNode node) => new()
|
||||||
|
{
|
||||||
|
new() { Label = "Configure", Icon = "⚙", OnClick = () => OnConfigureDriver(node) },
|
||||||
|
new() { Label = "Test connect", Icon = "🔎", OnClick = () => OnTestConnect(node) },
|
||||||
|
new() { Label = "New device", Icon = "📟", OnClick = () => OnNewDevice(node) },
|
||||||
|
ContextMenuItem.Separator(),
|
||||||
|
new() { Label = node.Enabled ? "Disable" : "Enable", Icon = node.Enabled ? "⏸" : "▶",
|
||||||
|
OnClick = () => OnToggleDriverEnabled(node) },
|
||||||
|
new() { Label = "Rename", Icon = "✏️", OnClick = () => OnRenameDriver(node) },
|
||||||
|
new() { Label = "Delete", Icon = "🗑️", OnClick = () => OnDeleteDriver(node) },
|
||||||
|
};
|
||||||
|
|
||||||
|
private List<ContextMenuItem> DeviceMenu(RawNode node) => new()
|
||||||
|
{
|
||||||
|
new() { Label = "Configure", Icon = "⚙", OnClick = () => OnConfigureDevice(node) },
|
||||||
|
new() { Label = "New tag-group", Icon = "🗂️", OnClick = () => OnNewTagGroup(node) },
|
||||||
|
ContextMenuItem.Separator(),
|
||||||
|
// "Add tags ▸" flattened — ContextMenu is single-level, so the submenu is a labelled group.
|
||||||
|
new() { Label = "Add tags", Disabled = true },
|
||||||
|
new() { Label = "Manual entry", Icon = "✍️", OnClick = () => OnAddManualTags(node) },
|
||||||
|
new() { Label = "Import CSV", Icon = "📥", OnClick = () => OnImportCsv(node) },
|
||||||
|
new() { Label = "Export CSV", Icon = "📤", OnClick = () => OnExportCsv(node) },
|
||||||
|
new() { Label = "Browse device…", Icon = "🔭", OnClick = () => OnBrowseDevice(node) },
|
||||||
|
ContextMenuItem.Separator(),
|
||||||
|
new() { Label = "Delete", Icon = "🗑️", OnClick = () => OnDeleteDevice(node) },
|
||||||
|
};
|
||||||
|
|
||||||
|
private List<ContextMenuItem> TagGroupMenu(RawNode node) => new()
|
||||||
|
{
|
||||||
|
new() { Label = "New group", Icon = "🗂️", OnClick = () => OnNewGroup(node) },
|
||||||
|
ContextMenuItem.Separator(),
|
||||||
|
new() { Label = "Add tags", Disabled = true },
|
||||||
|
new() { Label = "Manual entry", Icon = "✍️", OnClick = () => OnAddManualTags(node) },
|
||||||
|
new() { Label = "Import CSV", Icon = "📥", OnClick = () => OnImportCsv(node) },
|
||||||
|
new() { Label = "Export CSV", Icon = "📤", OnClick = () => OnExportCsv(node) },
|
||||||
|
new() { Label = "Browse device…", Icon = "🔭", OnClick = () => OnBrowseDevice(node) },
|
||||||
|
ContextMenuItem.Separator(),
|
||||||
|
new() { Label = "Rename", Icon = "✏️", OnClick = () => OnRenameTagGroup(node) },
|
||||||
|
new() { Label = "Delete", Icon = "🗑️", OnClick = () => OnDeleteTagGroup(node) },
|
||||||
|
};
|
||||||
|
|
||||||
|
private List<ContextMenuItem> TagMenu(RawNode node) => new()
|
||||||
|
{
|
||||||
|
new() { Label = "Edit", Icon = "✏️", OnClick = () => OnEditTag(node) },
|
||||||
|
new() { Label = "Delete", Icon = "🗑️", OnClick = () => OnDeleteTag(node) },
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Named action handlers — the Wave B/C replacement seam. Each opens a placeholder
|
||||||
|
// today; a later wave swaps the body for the real modal invocation.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Folder / Cluster
|
||||||
|
private Task OnNewFolder(RawNode node) => ShowStub("New folder", node);
|
||||||
|
private Task OnNewDriver(RawNode node) => ShowStub("New driver", node);
|
||||||
|
private Task OnRenameFolder(RawNode node) => ShowStub("Rename folder", node);
|
||||||
|
private Task OnDeleteFolder(RawNode node) => ShowStub("Delete folder", node);
|
||||||
|
|
||||||
|
// Driver
|
||||||
|
private Task OnConfigureDriver(RawNode node) => ShowStub("Configure driver", node);
|
||||||
|
private Task OnTestConnect(RawNode node) => ShowStub("Test connect", node);
|
||||||
|
private Task OnNewDevice(RawNode node) => ShowStub("New device", node);
|
||||||
|
private Task OnToggleDriverEnabled(RawNode node) => ShowStub(node.Enabled ? "Disable driver" : "Enable driver", node);
|
||||||
|
private Task OnRenameDriver(RawNode node) => ShowStub("Rename driver", node);
|
||||||
|
private Task OnDeleteDriver(RawNode node) => ShowStub("Delete driver", node);
|
||||||
|
|
||||||
|
// Device
|
||||||
|
private Task OnConfigureDevice(RawNode node) => ShowStub("Configure device", node);
|
||||||
|
private Task OnNewTagGroup(RawNode node) => ShowStub("New tag-group", node);
|
||||||
|
private Task OnDeleteDevice(RawNode node) => ShowStub("Delete device", node);
|
||||||
|
|
||||||
|
// Add-tags submenu (shared by Device + TagGroup)
|
||||||
|
private Task OnAddManualTags(RawNode node) => ShowStub("Add tags — manual entry", node);
|
||||||
|
private Task OnImportCsv(RawNode node) => ShowStub("Import tags CSV", node);
|
||||||
|
private Task OnExportCsv(RawNode node) => ShowStub("Export tags CSV", node);
|
||||||
|
private Task OnBrowseDevice(RawNode node) => ShowStub("Browse device", node);
|
||||||
|
|
||||||
|
// TagGroup
|
||||||
|
private Task OnNewGroup(RawNode node) => ShowStub("New group", node);
|
||||||
|
private Task OnRenameTagGroup(RawNode node) => ShowStub("Rename tag-group", node);
|
||||||
|
private Task OnDeleteTagGroup(RawNode node) => ShowStub("Delete tag-group", node);
|
||||||
|
|
||||||
|
// Tag
|
||||||
|
private Task OnEditTag(RawNode node) => ShowStub("Edit tag", node);
|
||||||
|
private Task OnDeleteTag(RawNode node) => ShowStub("Delete tag", node);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Placeholder plumbing (removed once every handler above has its real modal).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private Task ShowStub(string action, RawNode node)
|
||||||
|
{
|
||||||
|
_stubTitle = action;
|
||||||
|
_stubMessage = $"{action} — coming in a later wave.";
|
||||||
|
_stubNodeName = node.DisplayName;
|
||||||
|
_stubVisible = true;
|
||||||
|
StateHasChanged();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseStub()
|
||||||
|
{
|
||||||
|
_stubVisible = false;
|
||||||
|
_stubTitle = "";
|
||||||
|
_stubMessage = "";
|
||||||
|
_stubNodeName = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user