Add real-time dashboard updates with SignalR
Integrate SignalR to provide real-time dashboard update notifications. - Added DashboardsHub and DashboardChangeNotifier on the backend. - Modified SqlDashboardStorage to trigger notifications on changes. - Registered SignalR services and mapped the hub endpoint. - Updated Blazor clients to connect to the hub and refresh dashboards on change. - Added SignalR client packages and necessary DI/configuration.
This commit is contained in:
19
DbFirst.API/Dashboards/DashboardChangeNotifier.cs
Normal file
19
DbFirst.API/Dashboards/DashboardChangeNotifier.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using DbFirst.API.Hubs;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace DbFirst.API.Dashboards;
|
||||||
|
|
||||||
|
public class DashboardChangeNotifier : IDashboardChangeNotifier
|
||||||
|
{
|
||||||
|
private readonly IHubContext<DashboardsHub> _hubContext;
|
||||||
|
|
||||||
|
public DashboardChangeNotifier(IHubContext<DashboardsHub> hubContext)
|
||||||
|
{
|
||||||
|
_hubContext = hubContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyChanged()
|
||||||
|
{
|
||||||
|
_ = _hubContext.Clients.All.SendAsync("DashboardsChanged");
|
||||||
|
}
|
||||||
|
}
|
||||||
6
DbFirst.API/Dashboards/IDashboardChangeNotifier.cs
Normal file
6
DbFirst.API/Dashboards/IDashboardChangeNotifier.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DbFirst.API.Dashboards;
|
||||||
|
|
||||||
|
public interface IDashboardChangeNotifier
|
||||||
|
{
|
||||||
|
void NotifyChanged();
|
||||||
|
}
|
||||||
@@ -11,12 +11,14 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
|
|||||||
private readonly string _connectionString;
|
private readonly string _connectionString;
|
||||||
private readonly string _tableName;
|
private readonly string _tableName;
|
||||||
private readonly Func<string?>? _userProvider;
|
private readonly Func<string?>? _userProvider;
|
||||||
|
private readonly IDashboardChangeNotifier? _notifier;
|
||||||
|
|
||||||
public SqlDashboardStorage(string connectionString, string tableName = "TBDD_SMF_CONFIG", Func<string?>? userProvider = null)
|
public SqlDashboardStorage(string connectionString, string tableName = "TBDD_SMF_CONFIG", Func<string?>? userProvider = null, IDashboardChangeNotifier? notifier = null)
|
||||||
{
|
{
|
||||||
_connectionString = connectionString;
|
_connectionString = connectionString;
|
||||||
_tableName = tableName;
|
_tableName = tableName;
|
||||||
_userProvider = userProvider;
|
_userProvider = userProvider;
|
||||||
|
_notifier = notifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<DashboardInfo> GetAvailableDashboardsInfo()
|
public IEnumerable<DashboardInfo> GetAvailableDashboardsInfo()
|
||||||
@@ -98,6 +100,7 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
|
|||||||
|
|
||||||
connection.Open();
|
connection.Open();
|
||||||
command.ExecuteNonQuery();
|
command.ExecuteNonQuery();
|
||||||
|
_notifier?.NotifyChanged();
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +121,8 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
|
|||||||
{
|
{
|
||||||
throw new ArgumentException($"Dashboard '{dashboardId}' not found.");
|
throw new ArgumentException($"Dashboard '{dashboardId}' not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_notifier?.NotifyChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteDashboard(string dashboardId)
|
public void DeleteDashboard(string dashboardId)
|
||||||
@@ -128,5 +133,6 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
|
|||||||
|
|
||||||
connection.Open();
|
connection.Open();
|
||||||
command.ExecuteNonQuery();
|
command.ExecuteNonQuery();
|
||||||
|
_notifier?.NotifyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
DbFirst.API/Hubs/DashboardsHub.cs
Normal file
7
DbFirst.API/Hubs/DashboardsHub.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace DbFirst.API.Hubs;
|
||||||
|
|
||||||
|
public class DashboardsHub : Hub
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using DbFirst.API.Middleware;
|
using DbFirst.API.Middleware;
|
||||||
using DbFirst.API.Dashboards;
|
using DbFirst.API.Dashboards;
|
||||||
|
using DbFirst.API.Hubs;
|
||||||
using DbFirst.Application;
|
using DbFirst.Application;
|
||||||
using DbFirst.Application.Repositories;
|
using DbFirst.Application.Repositories;
|
||||||
using DbFirst.Domain;
|
using DbFirst.Domain;
|
||||||
@@ -53,6 +54,8 @@ builder.Services.AddApplication();
|
|||||||
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
|
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
|
||||||
|
|
||||||
builder.Services.AddDevExpressControls();
|
builder.Services.AddDevExpressControls();
|
||||||
|
builder.Services.AddSignalR();
|
||||||
|
builder.Services.AddSingleton<IDashboardChangeNotifier, DashboardChangeNotifier>();
|
||||||
builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvider) => {
|
builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvider) => {
|
||||||
var dashboardsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "Dashboards");
|
var dashboardsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "Dashboards");
|
||||||
Directory.CreateDirectory(dashboardsPath);
|
Directory.CreateDirectory(dashboardsPath);
|
||||||
@@ -112,7 +115,8 @@ builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvi
|
|||||||
DashboardConfigurator configurator = new DashboardConfigurator();
|
DashboardConfigurator configurator = new DashboardConfigurator();
|
||||||
|
|
||||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty;
|
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty;
|
||||||
var dashboardStorage = new SqlDashboardStorage(connectionString, "TBDD_SMF_CONFIG");
|
var notifier = serviceProvider.GetRequiredService<IDashboardChangeNotifier>();
|
||||||
|
var dashboardStorage = new SqlDashboardStorage(connectionString, "TBDD_SMF_CONFIG", notifier: notifier);
|
||||||
configurator.SetDashboardStorage(dashboardStorage);
|
configurator.SetDashboardStorage(dashboardStorage);
|
||||||
|
|
||||||
DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage();
|
DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage();
|
||||||
@@ -155,7 +159,7 @@ app.UseCors();
|
|||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapDashboardRoute("api/dashboard", "DefaultDashboard");
|
app.MapDashboardRoute("api/dashboard", "DefaultDashboard");
|
||||||
|
app.MapHub<DashboardsHub>("/hubs/dashboards");
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.22" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.22" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.22" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.22" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/dashboard"
|
@page "/dashboard"
|
||||||
@page "/dashboards/{DashboardId?}"
|
@page "/dashboards/{DashboardId?}"
|
||||||
|
@implements IAsyncDisposable
|
||||||
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject DashboardApiClient DashboardApi
|
@inject DashboardApiClient DashboardApi
|
||||||
@@ -77,19 +78,38 @@
|
|||||||
[SupplyParameterFromQuery] public string? Mode { get; set; }
|
[SupplyParameterFromQuery] public string? Mode { get; set; }
|
||||||
|
|
||||||
private readonly List<DashboardInfoDto> dashboards = new();
|
private readonly List<DashboardInfoDto> dashboards = new();
|
||||||
|
private HubConnection? _hubConnection;
|
||||||
|
|
||||||
private bool IsDesigner => !string.Equals(Mode, "viewer", StringComparison.OrdinalIgnoreCase);
|
private bool IsDesigner => !string.Equals(Mode, "viewer", StringComparison.OrdinalIgnoreCase);
|
||||||
private WorkingMode CurrentMode => IsDesigner ? WorkingMode.Designer : WorkingMode.ViewerOnly;
|
private WorkingMode CurrentMode => IsDesigner ? WorkingMode.Designer : WorkingMode.ViewerOnly;
|
||||||
private string SelectedDashboardId { get; set; } = "";
|
private string SelectedDashboardId { get; set; } = string.Empty;
|
||||||
private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}";
|
private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}";
|
||||||
|
|
||||||
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
|
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
|
||||||
|
private string HubEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/hubs/dashboards";
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await RefreshDashboards();
|
||||||
|
|
||||||
|
_hubConnection = new HubConnectionBuilder()
|
||||||
|
.WithUrl(HubEndpoint)
|
||||||
|
.WithAutomaticReconnect()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_hubConnection.On("DashboardsChanged", async () =>
|
||||||
|
{
|
||||||
|
await RefreshDashboards();
|
||||||
|
});
|
||||||
|
|
||||||
|
await _hubConnection.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
if (dashboards.Count == 0)
|
if (dashboards.Count == 0)
|
||||||
{
|
{
|
||||||
dashboards.AddRange(await DashboardApi.GetAllAsync());
|
await RefreshDashboards();
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
|
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
|
||||||
@@ -119,4 +139,25 @@
|
|||||||
var targetMode = IsDesigner ? "viewer" : "designer";
|
var targetMode = IsDesigner ? "viewer" : "designer";
|
||||||
Navigation.NavigateTo($"dashboards/{SelectedDashboardId}?mode={targetMode}", replace: true);
|
Navigation.NavigateTo($"dashboards/{SelectedDashboardId}?mode={targetMode}", replace: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RefreshDashboards()
|
||||||
|
{
|
||||||
|
var latest = await DashboardApi.GetAllAsync();
|
||||||
|
if (latest.Count == dashboards.Count && latest.All(d => dashboards.Any(x => x.Id == d.Id && x.Name == d.Name)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboards.Clear();
|
||||||
|
dashboards.AddRange(latest);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_hubConnection != null)
|
||||||
|
{
|
||||||
|
await _hubConnection.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
|
@using Microsoft.AspNetCore.SignalR.Client
|
||||||
@using DbFirst.BlazorWasm
|
@using DbFirst.BlazorWasm
|
||||||
@using DbFirst.BlazorWasm.Layout
|
@using DbFirst.BlazorWasm.Layout
|
||||||
@using DbFirst.BlazorWasm.Models
|
@using DbFirst.BlazorWasm.Models
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/dashboard"
|
@page "/dashboard"
|
||||||
@page "/dashboards/{DashboardId?}"
|
@page "/dashboards/{DashboardId?}"
|
||||||
|
@implements IAsyncDisposable
|
||||||
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject DashboardApiClient DashboardApi
|
@inject DashboardApiClient DashboardApi
|
||||||
@@ -77,19 +78,38 @@
|
|||||||
[SupplyParameterFromQuery] public string? Mode { get; set; }
|
[SupplyParameterFromQuery] public string? Mode { get; set; }
|
||||||
|
|
||||||
private readonly List<DashboardInfoDto> dashboards = new();
|
private readonly List<DashboardInfoDto> dashboards = new();
|
||||||
|
private HubConnection? _hubConnection;
|
||||||
|
|
||||||
private bool IsDesigner => !string.Equals(Mode, "viewer", StringComparison.OrdinalIgnoreCase);
|
private bool IsDesigner => !string.Equals(Mode, "viewer", StringComparison.OrdinalIgnoreCase);
|
||||||
private WorkingMode CurrentMode => IsDesigner ? WorkingMode.Designer : WorkingMode.ViewerOnly;
|
private WorkingMode CurrentMode => IsDesigner ? WorkingMode.Designer : WorkingMode.ViewerOnly;
|
||||||
private string SelectedDashboardId { get; set; } = "";
|
private string SelectedDashboardId { get; set; } = string.Empty;
|
||||||
private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}";
|
private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}";
|
||||||
|
|
||||||
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
|
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
|
||||||
|
private string HubEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/hubs/dashboards";
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await RefreshDashboards();
|
||||||
|
|
||||||
|
_hubConnection = new HubConnectionBuilder()
|
||||||
|
.WithUrl(HubEndpoint)
|
||||||
|
.WithAutomaticReconnect()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_hubConnection.On("DashboardsChanged", async () =>
|
||||||
|
{
|
||||||
|
await RefreshDashboards();
|
||||||
|
});
|
||||||
|
|
||||||
|
await _hubConnection.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
if (dashboards.Count == 0)
|
if (dashboards.Count == 0)
|
||||||
{
|
{
|
||||||
dashboards.AddRange(await DashboardApi.GetAllAsync());
|
await RefreshDashboards();
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
|
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
|
||||||
@@ -119,4 +139,25 @@
|
|||||||
var targetMode = IsDesigner ? "viewer" : "designer";
|
var targetMode = IsDesigner ? "viewer" : "designer";
|
||||||
Navigation.NavigateTo($"dashboards/{SelectedDashboardId}?mode={targetMode}", replace: true);
|
Navigation.NavigateTo($"dashboards/{SelectedDashboardId}?mode={targetMode}", replace: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RefreshDashboards()
|
||||||
|
{
|
||||||
|
var latest = await DashboardApi.GetAllAsync();
|
||||||
|
if (latest.Count == dashboards.Count && latest.All(d => dashboards.Any(x => x.Id == d.Id && x.Name == d.Name)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboards.Clear();
|
||||||
|
dashboards.AddRange(latest);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_hubConnection != null)
|
||||||
|
{
|
||||||
|
await _hubConnection.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
|
@using Microsoft.AspNetCore.SignalR.Client
|
||||||
@using DbFirst.BlazorWebApp
|
@using DbFirst.BlazorWebApp
|
||||||
@using DbFirst.BlazorWebApp.Components
|
@using DbFirst.BlazorWebApp.Components
|
||||||
@using DbFirst.BlazorWebApp.Models
|
@using DbFirst.BlazorWebApp.Models
|
||||||
|
|||||||
@@ -12,5 +12,9 @@
|
|||||||
<PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user