- Move CSS into wwwroot/css/ (theme.css, site.css); sidebar 218 -> 220px - Add hamburger + Bootstrap collapse for <lg viewports - Add Components/Shared/ with LoadingSpinner, ToastNotification, StatusBadge - Replace .page-title with flex + <h4 class="mb-0"> across 20 pages - Convert NewCluster + IdentificationFields forms to card + h6 subsection pattern
199 lines
7.4 KiB
Plaintext
199 lines
7.4 KiB
Plaintext
@page "/role-grants"
|
|
@attribute [Microsoft.AspNetCore.Authorization.Authorize(Policy = "CanPublish")]
|
|
@rendermode RenderMode.InteractiveServer
|
|
@using Microsoft.AspNetCore.Components.Web
|
|
@using Microsoft.AspNetCore.SignalR.Client
|
|
@using ZB.MOM.WW.OtOpcUa.Admin.Hubs
|
|
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Services
|
|
@inject ILdapGroupRoleMappingService RoleSvc
|
|
@inject ClusterService ClusterSvc
|
|
@inject AclChangeNotifier Notifier
|
|
@inject NavigationManager Nav
|
|
@inject AdminHubConnectionFactory HubFactory
|
|
@implements IAsyncDisposable
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h4 class="mb-0">LDAP group → Admin role grants</h4>
|
|
<button class="btn btn-primary btn-sm" @onclick="StartAdd">Add grant</button>
|
|
</div>
|
|
|
|
@if (_rows is null)
|
|
{
|
|
<p>Loading…</p>
|
|
}
|
|
else if (_rows.Count == 0)
|
|
{
|
|
<p class="text-muted">No role grants defined yet. Without at least one FleetAdmin grant,
|
|
only the bootstrap admin can publish drafts.</p>
|
|
}
|
|
else
|
|
{
|
|
<section class="panel rise" style="animation-delay:.08s">
|
|
<div class="panel-head">Grants</div>
|
|
<div class="table-wrap">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr><th>LDAP group</th><th>Role</th><th>Scope</th><th>Created</th><th>Notes</th><th></th></tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var r in _rows)
|
|
{
|
|
<tr>
|
|
<td><span class="mono">@r.LdapGroup</span></td>
|
|
<td><span class="chip chip-idle">@r.Role</span></td>
|
|
<td>@(r.IsSystemWide ? "Fleet-wide" : $"Cluster: {r.ClusterId}")</td>
|
|
<td class="small">@r.CreatedAtUtc.ToString("yyyy-MM-dd")</td>
|
|
<td class="small text-muted">@r.Notes</td>
|
|
<td><button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteAsync(r.Id)">Revoke</button></td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
@if (_showForm)
|
|
{
|
|
<section class="panel rise" style="animation-delay:.14s">
|
|
<div class="panel-head">New role grant</div>
|
|
<div class="p-3">
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">LDAP group (DN)</label>
|
|
<input class="form-control form-control-sm" @bind="_group" placeholder="cn=fleet-admin,ou=groups,dc=…"/>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Role</label>
|
|
<select class="form-select form-select-sm" @bind="_role">
|
|
@foreach (var r in Enum.GetValues<AdminRole>())
|
|
{
|
|
<option value="@r">@r</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2 pt-4">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="systemWide" @bind="_isSystemWide"/>
|
|
<label class="form-check-label" for="systemWide">Fleet-wide</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Cluster @(_isSystemWide ? "(disabled)" : "")</label>
|
|
<select class="form-select form-select-sm" @bind="_clusterId" disabled="@_isSystemWide">
|
|
<option value="">-- select --</option>
|
|
@if (_clusters is not null)
|
|
{
|
|
@foreach (var c in _clusters)
|
|
{
|
|
<option value="@c.ClusterId">@c.ClusterId</option>
|
|
}
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label">Notes (optional)</label>
|
|
<input class="form-control form-control-sm" @bind="_notes"/>
|
|
</div>
|
|
</div>
|
|
@if (_error is not null) { <section class="panel notice mt-3">@_error</section> }
|
|
<div class="mt-3">
|
|
<button class="btn btn-sm btn-primary" @onclick="SaveAsync">Save</button>
|
|
<button class="btn btn-sm btn-secondary ms-2" @onclick="() => _showForm = false">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
@code {
|
|
private IReadOnlyList<LdapGroupRoleMapping>? _rows;
|
|
private List<ServerCluster>? _clusters;
|
|
private bool _showForm;
|
|
private string _group = string.Empty;
|
|
private AdminRole _role = AdminRole.ConfigViewer;
|
|
private bool _isSystemWide;
|
|
private string _clusterId = string.Empty;
|
|
private string? _notes;
|
|
private string? _error;
|
|
|
|
protected override async Task OnInitializedAsync() => await ReloadAsync();
|
|
|
|
private async Task ReloadAsync()
|
|
{
|
|
_rows = await RoleSvc.ListAllAsync(CancellationToken.None);
|
|
_clusters = await ClusterSvc.ListAsync(CancellationToken.None);
|
|
}
|
|
|
|
private void StartAdd()
|
|
{
|
|
_group = string.Empty;
|
|
_role = AdminRole.ConfigViewer;
|
|
_isSystemWide = false;
|
|
_clusterId = string.Empty;
|
|
_notes = null;
|
|
_error = null;
|
|
_showForm = true;
|
|
}
|
|
|
|
private async Task SaveAsync()
|
|
{
|
|
_error = null;
|
|
try
|
|
{
|
|
var row = new LdapGroupRoleMapping
|
|
{
|
|
LdapGroup = _group.Trim(),
|
|
Role = _role,
|
|
IsSystemWide = _isSystemWide,
|
|
ClusterId = _isSystemWide ? null : (string.IsNullOrWhiteSpace(_clusterId) ? null : _clusterId),
|
|
Notes = string.IsNullOrWhiteSpace(_notes) ? null : _notes,
|
|
};
|
|
await RoleSvc.CreateAsync(row, CancellationToken.None);
|
|
await Notifier.NotifyRoleGrantsChangedAsync(CancellationToken.None);
|
|
_showForm = false;
|
|
await ReloadAsync();
|
|
}
|
|
catch (Exception ex) { _error = ex.Message; }
|
|
}
|
|
|
|
private async Task DeleteAsync(Guid id)
|
|
{
|
|
await RoleSvc.DeleteAsync(id, CancellationToken.None);
|
|
await Notifier.NotifyRoleGrantsChangedAsync(CancellationToken.None);
|
|
await ReloadAsync();
|
|
}
|
|
|
|
private HubConnection? _hub;
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (!firstRender || _hub is not null) return;
|
|
_hub = HubFactory.Create("/hubs/fleet");
|
|
_hub.On<RoleGrantsChangedMessage>("RoleGrantsChanged", async _ =>
|
|
{
|
|
await ReloadAsync();
|
|
await InvokeAsync(StateHasChanged);
|
|
});
|
|
// Best-effort: FleetStatusHub requires an authenticated caller, and the server-side
|
|
// HubConnection cannot forward the browser auth cookie — swallow connect failures so
|
|
// the page still renders. Live role-grant updates degrade.
|
|
try
|
|
{
|
|
await _hub.StartAsync();
|
|
await _hub.SendAsync("SubscribeFleet");
|
|
}
|
|
catch
|
|
{
|
|
// best-effort live updates — see comment above
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_hub is not null) { await _hub.DisposeAsync(); _hub = null; }
|
|
}
|
|
}
|