feat(centralui): Topology 'Deploy all…' on site and area nodes with confirm dialog, Force and summary toast (Gitea #40)

This commit is contained in:
Joseph Doherty
2026-10-22 14:16:08 -04:00
parent d71f61e71b
commit bcd087efe8
6 changed files with 314 additions and 4 deletions
@@ -676,6 +676,9 @@
@onclick='() => NavigationManager.NavigateTo($"/deployment/instances/create?siteId={node.EntityId}")'>
Create Instance here
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item" @onclick="() => DeployAll(node)"
disabled="@_actionInProgress">Deploy all…</button>
break;
case TopoNodeKind.Area:
@@ -687,6 +690,9 @@
<button class="dropdown-item" @onclick="() => OpenMoveAreaDialog(node)">Move to Area…</button>
<button class="dropdown-item" @onclick="() => BeginRename(node)">Rename…</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item" @onclick="() => DeployAll(node)"
disabled="@_actionInProgress">Deploy all…</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger" @onclick="() => DeleteArea(node)" disabled="@_actionInProgress">Delete</button>
break;
@@ -1069,6 +1075,86 @@
_actionInProgress = false;
}
// ---- Deploy all (site / area subtree, Gitea #40) ----
private async Task DeployAll(TopoNode node)
{
if (_actionInProgress) return;
_actionInProgress = true;
try
{
var isArea = node.Kind == TopoNodeKind.Area;
var siteId = isArea ? node.SiteId : node.EntityId;
int? areaId = isArea ? node.EntityId : null;
var user = await GetSiteDeployerAsync(siteId);
if (user is null) return;
var preview = await DeploymentService.PreviewSubtreeDeployAsync(siteId, areaId);
if (!preview.IsSuccess)
{
_toast.ShowError($"Deploy all failed: {preview.Error}");
return;
}
if (preview.Value.Total == 0)
{
_toast.ShowInfo($"No instances under '{node.Label}' to deploy.");
return;
}
var scopeLabel = (isArea ? "area " : "site ") + node.Label;
var total = preview.Value.Total;
var disabled = preview.Value.Disabled;
var choice = await Dialog.ShowAsync<DeployAllDialog.DeployAllResult>(
"Deploy all",
ctx => @<DeployAllDialog Context="ctx" ScopeLabel="@scopeLabel" Total="total" Disabled="disabled" />);
if (choice is null) return;
var result = isArea
? await DeploymentService.DeployAreaAsync(node.EntityId, user, choice.Force)
: await DeploymentService.DeploySiteAsync(siteId, user, choice.Force);
if (!result.IsSuccess)
{
_toast.ShowError($"Deploy all failed: {result.Error}");
return;
}
var summary = result.Value;
var skipped = summary.SkippedInstances?.Count ?? 0;
var text = $"Deploy all under '{node.Label}': {summary.SuccessCount} deployed, {skipped} skipped, {summary.FailureCount} failed.";
if (summary.FailureCount > 0)
{
var failures = summary.InstanceResults.Where(r => !r.Success).Take(3)
.Select(r => $"{r.UniqueName}: {r.ErrorMessage}");
var more = summary.FailureCount > 3 ? $" (+{summary.FailureCount - 3} more)" : string.Empty;
_toast.ShowError($"{text} {string.Join("; ", failures)}{more}", "Deploy all");
}
else
{
_toast.ShowSuccess(text, "Deploy all");
}
await LoadDataAsync();
}
catch (Exception ex)
{
_toast.ShowError($"Deploy all failed: {ex.Message}");
}
finally
{
_actionInProgress = false;
}
}
// Deployer role + current site scope for a site-level (bulk) action.
private async Task<string?> GetSiteDeployerAsync(int siteId)
{
var user = await AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Deployer);
if (!await SiteScope.IsSiteAllowedAsync(siteId))
{
_toast.ShowError("This site is no longer available within your permitted sites.");
return null;
}
return user;
}
// ---- Diff modal ----
private async Task ShowDiff(Instance inst)
{