feat(centralui): Topology 'Deploy all…' on site and area nodes with confirm dialog, Force and summary toast (Gitea #40)
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
@*
|
||||
Body component for the Topology "Deploy all…" confirmation (Gitea #40), hosted by
|
||||
IDialogService.ShowAsync. Shows how many instances the site/area subtree holds and
|
||||
how many of those are Disabled (always skipped), and offers the Force switch that
|
||||
also redeploys up-to-date instances. Closes with DeployAllResult; Cancel resolves
|
||||
to null, so a cancel is distinguishable from a confirm (the MoveTemplateDialog /
|
||||
TemplatePickerDialog contract).
|
||||
*@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
|
||||
<div data-test="deploy-all-body">
|
||||
<p class="mb-2">
|
||||
Deploy all instances under <strong data-test="deploy-all-scope">@ScopeLabel</strong>?
|
||||
</p>
|
||||
<p class="mb-2 small" data-test="deploy-all-counts">
|
||||
<span class="fw-semibold">@Total</span> instance@(Total == 1 ? "" : "s")
|
||||
@if (Disabled > 0)
|
||||
{
|
||||
<span class="text-muted"> (@Disabled disabled — will be skipped)</span>
|
||||
}
|
||||
. Instances already running the current configuration are skipped.
|
||||
</p>
|
||||
<div class="form-check mb-1">
|
||||
<input type="checkbox" class="form-check-input" id="deploy-all-force" data-test="deploy-all-force"
|
||||
@bind="_force" />
|
||||
<label class="form-check-label small" for="deploy-all-force">Force redeploy up-to-date instances</label>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
A redeploy resets an instance's static attribute overrides and clears its native alarm state.
|
||||
Disabled instances are never deployed or re-enabled.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer px-0 pb-0 mt-3">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-secondary" type="button" data-test="deploy-all-cancel"
|
||||
@onclick="() => Context.Cancel()">Cancel</button>
|
||||
<button class="btn btn-primary" type="button" data-test="deploy-all-confirm"
|
||||
@onclick="() => Context.Close(new DeployAllResult(_force))">Deploy all</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>Supplied by <c>IDialogService.ShowAsync</c>; closes/cancels the modal.</summary>
|
||||
[Parameter] public DialogContext<DeployAllResult> Context { get; set; } = default!;
|
||||
|
||||
/// <summary>Human label of the subtree being deployed, e.g. "site Plant" or "area Line A".</summary>
|
||||
[Parameter] public string ScopeLabel { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Instances in the subtree.</summary>
|
||||
[Parameter] public int Total { get; set; }
|
||||
|
||||
/// <summary>Of those, the Disabled ones a bulk deploy always skips.</summary>
|
||||
[Parameter] public int Disabled { get; set; }
|
||||
|
||||
private bool _force;
|
||||
|
||||
/// <summary>Operator's confirmed choice; <c>null</c> from ShowAsync means Cancel.</summary>
|
||||
/// <param name="Force">Also redeploy instances that are already up to date.</param>
|
||||
public sealed record DeployAllResult(bool Force);
|
||||
}
|
||||
Reference in New Issue
Block a user