refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)

Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
Joseph Doherty
2026-05-28 09:37:45 -04:00
parent 6d87ee3c3b
commit 7b0b9c7365
1531 changed files with 11180 additions and 11054 deletions
@@ -0,0 +1,525 @@
@page "/design/templates"
@using ZB.MOM.WW.ScadaBridge.Security
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
@using ZB.MOM.WW.ScadaBridge.TemplateEngine
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
@inject ITemplateEngineRepository TemplateEngineRepository
@inject TemplateService TemplateService
@inject TemplateFolderService TemplateFolderService
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager
@inject IDialogService Dialog
<div class="container-fluid mt-3">
<ToastNotification @ref="_toast" />
<RenameFolderDialog @bind-IsVisible="_showRenameFolderDialog"
FolderId="_renameFolderId"
InitialName="@_renameFolderInitialName"
ErrorMessage="@_renameFolderError"
OnSubmit="SubmitRenameFolder" />
<MoveTemplateDialog @bind-IsVisible="_showMoveTemplateDialog"
TemplateId="_moveTemplateId"
TemplateName="@_moveTemplateName"
FolderOptions="EnumerateFolderOptions()"
ErrorMessage="@_moveTemplateError"
OnSubmit="SubmitMoveTemplate" />
<MoveFolderDialog @bind-IsVisible="_showMoveFolderDialog"
FolderId="_moveFolderId"
FolderName="@_moveFolderName"
FolderOptions="EnumerateFolderOptionsExcluding(_moveFolderId)"
ErrorMessage="@_moveFolderError"
OnSubmit="SubmitMoveFolder" />
<ComposeIntoDialog @bind-IsVisible="_showComposeDialog"
SourceTemplateId="_composeSourceId"
SourceName="@_composeSourceName"
ParentOptions="EnumerateComposableParents(_composeSourceId)"
ErrorMessage="@_composeError"
OnSubmit="SubmitCompose" />
@if (_loading)
{
<LoadingSpinner IsLoading="true" />
}
else if (_errorMessage != null)
{
<div class="alert alert-danger">@_errorMessage</div>
}
else
{
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">Templates</h4>
<div class="d-flex gap-2">
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm dropdown-toggle"
data-bs-toggle="dropdown" aria-expanded="false">
Bulk actions
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button class="dropdown-item" @onclick="() => _tree.ExpandAll()">Expand all folders</button>
</li>
<li>
<button class="dropdown-item" @onclick="() => _tree.CollapseAll()">Collapse all folders</button>
</li>
</ul>
</div>
<button class="btn btn-outline-secondary btn-sm"
title="New folder at root"
@onclick="() => OpenNewFolderDialog(null)">+ Folder</button>
<button class="btn btn-primary btn-sm"
title="New template at root"
@onclick='() => NavigationManager.NavigateTo("/design/templates/create")'>+ Template</button>
</div>
</div>
<div style="max-height: calc(100vh - 160px); overflow-y: auto; padding: 4px;">
<TemplateFolderTree @ref="_tree"
Folders="_folders"
Templates="_templates"
SelectionMode="TreeViewSelectionMode.Single"
ExtraTemplateChildren="BuildCompositionLeavesFor"
StorageKey="templates-tree">
<NodeContent Context="node">
@RenderNodeLabel(node)
</NodeContent>
<ContextMenu Context="node">
@RenderNodeContextMenu(node)
</ContextMenu>
<EmptyContent>
<span class="text-muted fst-italic">No templates yet. Use the buttons above to create a folder or template.</span>
</EmptyContent>
</TemplateFolderTree>
</div>
}
</div>
@code {
// CentralUI-024: delegates to the shared helper so the claim type stays
// resolved through JwtTokenService rather than a duplicated magic string.
private Task<string> GetCurrentUserAsync()
=> AuthStateProvider.GetCurrentUsernameAsync();
private List<Template> _templates = new();
private List<TemplateFolder> _folders = new();
private bool _loading = true;
private string? _errorMessage;
private ToastNotification _toast = default!;
protected override async Task OnInitializedAsync()
{
await LoadTemplatesAsync();
}
private async Task LoadTemplatesAsync()
{
_loading = true;
try
{
_templates = (await TemplateEngineRepository.GetAllTemplatesAsync()).ToList();
_folders = (await TemplateEngineRepository.GetAllFoldersAsync()).ToList();
_templatesById = _templates.ToDictionary(t => t.Id);
_compositionsById = _templates
.SelectMany(t => t.Compositions)
.GroupBy(c => c.Id)
.ToDictionary(g => g.Key, g => g.First());
}
catch (Exception ex)
{
_errorMessage = $"Failed to load templates: {ex.Message}";
}
_loading = false;
}
// ID lookups so RenderNodeLabel / RenderNodeContextMenu can resolve the
// entity behind a TemplateTreeNode (whose payload is just Id+Kind+Name).
private Dictionary<int, Template> _templatesById = new();
private Dictionary<int, TemplateComposition> _compositionsById = new();
// Composition-leaf builder for TemplateFolderTree's ExtraTemplateChildren
// hook: walks each template's compositions recursively so cascaded slots
// appear as nested children. The Transport Export wizard intentionally
// does NOT supply this hook — compositions aren't independently exportable.
private IReadOnlyList<TemplateTreeNode> BuildCompositionLeavesFor(Template owner)
{
var result = new List<TemplateTreeNode>();
foreach (var c in owner.Compositions.OrderBy(c => c.InstanceName, StringComparer.OrdinalIgnoreCase))
{
var node = new TemplateTreeNode
{
Kind = TemplateTreeNodeKind.Composition,
Id = c.Id,
Name = c.InstanceName,
};
if (_templatesById.TryGetValue(c.ComposedTemplateId, out var composed))
{
foreach (var nested in BuildCompositionLeavesFor(composed))
{
node.Children.Add(nested);
}
}
result.Add(node);
}
return result;
}
private TemplateFolderTree _tree = default!;
private void OpenTemplate(int templateId) =>
NavigationManager.NavigateTo($"/design/templates/{templateId}");
private RenderFragment RenderNodeLabel(TemplateTreeNode node) => __builder =>
{
switch (node.Kind)
{
case TemplateTreeNodeKind.Folder:
<span class="tv-glyph"><i class="bi bi-folder"></i></span>
<span class="tv-label @(node.Children.Count > 0 ? "fw-semibold" : "")"
title="@node.Name">@node.Name</span>
@if (node.Children.Count > 0)
{
<span class="tv-meta">
<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis">@node.Children.Count</span>
</span>
}
break;
case TemplateTreeNodeKind.Template:
<span class="tv-glyph"><i class="bi bi-file-earmark-text"></i></span>
<span class="tv-label @(node.Children.Count > 0 ? "fw-semibold" : "")"
title="@node.Name"
@ondblclick="() => OpenTemplate(node.Id)">@node.Name</span>
break;
case TemplateTreeNodeKind.Composition:
var composedId = _compositionsById.TryGetValue(node.Id, out var comp) ? comp.ComposedTemplateId : 0;
<span class="tv-glyph"><i class="bi bi-arrow-return-right"></i></span>
<span class="tv-label" title="@node.Name"
@ondblclick="() => OpenTemplate(composedId)">@node.Name</span>
break;
}
};
private RenderFragment RenderNodeContextMenu(TemplateTreeNode node) => __builder =>
{
switch (node.Kind)
{
case TemplateTreeNodeKind.Folder:
<button class="dropdown-item" @onclick="() => OpenNewFolderDialog(node.Id)">New Folder</button>
<button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/create?folderId={node.Id}")'>New Template</button>
<button class="dropdown-item" @onclick="() => OpenRenameFolderDialog(node.Id, node.Name)">Rename</button>
<button class="dropdown-item" @onclick="() => OpenMoveFolderDialog(node.Id, node.Name)">Move to Folder…</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger" @onclick="() => DeleteFolder(node.Id, node.Name)">Delete</button>
break;
case TemplateTreeNodeKind.Template:
var tmpl = _templatesById.TryGetValue(node.Id, out var t) ? t : null;
<button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/create?parentId={node.Id}")'>New Inheriting Template</button>
@if (tmpl != null)
{
<button class="dropdown-item" @onclick="() => OpenComposeDialog(tmpl)">Compose into…</button>
}
<button class="dropdown-item" @onclick="() => OpenMoveTemplateDialog(node.Id, node.Name)">Move to Folder…</button>
<div class="dropdown-divider"></div>
@if (tmpl != null)
{
<button class="dropdown-item text-danger" @onclick="() => DeleteTemplate(tmpl)">Delete</button>
}
break;
case TemplateTreeNodeKind.Composition:
if (_compositionsById.TryGetValue(node.Id, out var ctx))
{
<button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/{ctx.ComposedTemplateId}")'>Open composed template</button>
<button class="dropdown-item" @onclick="() => RenameComposition(ctx)">Rename…</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger" @onclick="() => DeleteComposition(ctx)">Delete</button>
}
break;
}
};
// New-folder dialog: replaced the dedicated <NewFolderDialog> component with
// IDialogService.PromptAsync. Validation failures surface via toast instead of
// inline error text — the prompt UI doesn't have a slot for an error message.
private async Task OpenNewFolderDialog(int? parentFolderId)
{
var name = await Dialog.PromptAsync("New folder", "Folder name", placeholder: "Folder name");
if (string.IsNullOrWhiteSpace(name)) return;
var user = await GetCurrentUserAsync();
var result = await TemplateFolderService.CreateFolderAsync(name.Trim(), parentFolderId, user);
if (result.IsSuccess)
{
_toast.ShowSuccess($"Folder '{result.Value.Name}' created.");
await LoadTemplatesAsync();
}
else
{
_toast.ShowError(result.Error);
}
}
// Move-template dialog state
private bool _showMoveTemplateDialog;
private int _moveTemplateId;
private string _moveTemplateName = string.Empty;
private string? _moveTemplateError;
private void OpenMoveTemplateDialog(int templateId, string label)
{
_moveTemplateId = templateId;
_moveTemplateName = label;
_moveTemplateError = null;
_showMoveTemplateDialog = true;
}
private async Task SubmitMoveTemplate((int TemplateId, int? NewFolderId) req)
{
_moveTemplateError = null;
var user = await GetCurrentUserAsync();
var result = await TemplateService.MoveTemplateAsync(req.TemplateId, req.NewFolderId, user);
if (result.IsSuccess)
{
_showMoveTemplateDialog = false;
_toast.ShowSuccess($"Template '{_moveTemplateName}' moved.");
await LoadTemplatesAsync();
}
else
{
_moveTemplateError = result.Error;
}
}
// Flat list of folders with indentation labels, for the picker.
private IEnumerable<(int? Id, string Label)> EnumerateFolderOptions()
{
yield return (null, "(Root)");
foreach (var f in WalkFolderHierarchy(_folders.Where(f => f.ParentFolderId == null), 0, excludeFolderId: null))
yield return f;
}
// Same as EnumerateFolderOptions, but prunes the given folder and all its descendants
// so the move dialog can't surface an obvious cycle target (server still validates).
private IEnumerable<(int? Id, string Label)> EnumerateFolderOptionsExcluding(int excludeFolderId)
{
yield return (null, "(Root)");
foreach (var f in WalkFolderHierarchy(_folders.Where(f => f.ParentFolderId == null), 0, excludeFolderId))
yield return f;
}
private IEnumerable<(int? Id, string Label)> WalkFolderHierarchy(IEnumerable<TemplateFolder> level, int depth, int? excludeFolderId)
{
foreach (var f in level.OrderBy(f => f.Name, StringComparer.OrdinalIgnoreCase))
{
if (excludeFolderId.HasValue && f.Id == excludeFolderId.Value) continue;
yield return ((int?)f.Id, new string(' ', depth * 2) + f.Name);
foreach (var sub in WalkFolderHierarchy(_folders.Where(c => c.ParentFolderId == f.Id), depth + 1, excludeFolderId))
yield return sub;
}
}
// Move-folder dialog state
private bool _showMoveFolderDialog;
private int _moveFolderId;
private string _moveFolderName = string.Empty;
private string? _moveFolderError;
private void OpenMoveFolderDialog(int folderId, string label)
{
_moveFolderId = folderId;
_moveFolderName = label;
_moveFolderError = null;
_showMoveFolderDialog = true;
}
private async Task SubmitMoveFolder((int FolderId, int? NewParentId) req)
{
_moveFolderError = null;
var user = await GetCurrentUserAsync();
var result = await TemplateFolderService.MoveFolderAsync(req.FolderId, req.NewParentId, user);
if (result.IsSuccess)
{
_showMoveFolderDialog = false;
_toast.ShowSuccess($"Folder '{_moveFolderName}' moved.");
await LoadTemplatesAsync();
}
else
{
_moveFolderError = result.Error;
}
}
// Rename folder dialog state
private bool _showRenameFolderDialog;
private int _renameFolderId;
private string _renameFolderInitialName = string.Empty;
private string? _renameFolderError;
private void OpenRenameFolderDialog(int folderId, string currentName)
{
_renameFolderId = folderId;
_renameFolderInitialName = currentName;
_renameFolderError = null;
_showRenameFolderDialog = true;
}
private async Task SubmitRenameFolder((int FolderId, string NewName) req)
{
_renameFolderError = null;
var user = await GetCurrentUserAsync();
var result = await TemplateFolderService.RenameFolderAsync(req.FolderId, req.NewName, user);
if (result.IsSuccess)
{
_showRenameFolderDialog = false;
_toast.ShowSuccess("Folder renamed.");
await LoadTemplatesAsync();
}
else
{
_renameFolderError = result.Error;
}
}
private async Task DeleteFolder(int folderId, string label)
{
var confirmed = await Dialog.ConfirmAsync("Delete Folder", $"Delete folder '{label}'?", danger: true);
if (!confirmed) return;
var user = await GetCurrentUserAsync();
var result = await TemplateFolderService.DeleteFolderAsync(folderId, user);
if (result.IsSuccess)
{
_toast.ShowSuccess($"Folder '{label}' deleted.");
await LoadTemplatesAsync();
}
else
{
_toast.ShowError(result.Error);
}
}
private async Task DeleteTemplate(Template template)
{
var confirmed = await Dialog.ConfirmAsync(
"Delete Template",
$"Delete template '{template.Name}'? This will fail if instances or child templates reference it.",
danger: true);
if (!confirmed) return;
try
{
var user = await GetCurrentUserAsync();
var result = await TemplateService.DeleteTemplateAsync(template.Id, user);
if (result.IsSuccess)
{
_toast.ShowSuccess($"Template '{template.Name}' deleted.");
await LoadTemplatesAsync();
}
else
{
_toast.ShowError(result.Error);
}
}
catch (Exception ex)
{
_toast.ShowError($"Delete failed: {ex.Message}");
}
}
// ---- Compose-into dialog ----
private bool _showComposeDialog;
private int _composeSourceId;
private string _composeSourceName = string.Empty;
private string? _composeError;
private void OpenComposeDialog(Template source)
{
_composeSourceId = source.Id;
_composeSourceName = source.Name;
_composeError = null;
_showComposeDialog = true;
}
// Possible parents for a compose: every non-derived template except the source itself.
// Server still validates cycles + collisions; the picker just trims obvious bad choices.
private IEnumerable<(int Id, string Label)> EnumerateComposableParents(int sourceId)
{
return _templates
.Where(t => !t.IsDerived && t.Id != sourceId)
.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
.Select(t => (t.Id, t.Name));
}
private async Task SubmitCompose((int SourceTemplateId, int ParentTemplateId, string SlotName) req)
{
_composeError = null;
var user = await GetCurrentUserAsync();
var result = await TemplateService.AddCompositionAsync(req.ParentTemplateId, req.SourceTemplateId, req.SlotName, user);
if (result.IsSuccess)
{
_showComposeDialog = false;
_toast.ShowSuccess($"Composed '{_composeSourceName}' as '{req.SlotName}'.");
await LoadTemplatesAsync();
}
else
{
_composeError = result.Error;
}
}
// ---- Composition leaf: rename + delete ----
private async Task RenameComposition(TemplateComposition composition)
{
var newName = await Dialog.PromptAsync(
"Rename slot",
$"New name for slot '{composition.InstanceName}':",
initialValue: composition.InstanceName,
placeholder: "Slot name");
if (string.IsNullOrWhiteSpace(newName) || newName.Trim() == composition.InstanceName) return;
var user = await GetCurrentUserAsync();
var result = await TemplateService.RenameCompositionAsync(composition.Id, newName.Trim(), user);
if (result.IsSuccess)
{
_toast.ShowSuccess($"Slot renamed to '{newName.Trim()}'.");
await LoadTemplatesAsync();
}
else
{
_toast.ShowError(result.Error);
}
}
private async Task DeleteComposition(TemplateComposition composition)
{
var confirmed = await Dialog.ConfirmAsync(
"Delete composition",
$"Delete slot '{composition.InstanceName}'? This removes the derived template and any overrides on it.",
danger: true);
if (!confirmed) return;
var user = await GetCurrentUserAsync();
var result = await TemplateService.DeleteCompositionAsync(composition.Id, user);
if (result.IsSuccess)
{
_toast.ShowSuccess($"Composition '{composition.InstanceName}' removed.");
await LoadTemplatesAsync();
}
else
{
_toast.ShowError(result.Error);
}
}
}