From 9f36064095f66e475d790b5b2a91c2c0697f1e4d Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 5 Jul 2024 04:12:38 -0600 Subject: [PATCH] close fwdata file when leaving the editor (#925) * close fwdata project when the client disconnects, notify all clients in the group that the project is closed. * add button to navigate home from project view * setup client side notifications * only log auth warnings * fix bug where the home button wouldn't show correctly which was caused by dynamically rendering it with #if --- .../FwDataMiniLcmBridge/FwDataBridgeKernel.cs | 2 -- backend/FwDataMiniLcmBridge/FwDataFactory.cs | 22 +++++++++++-- backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs | 1 + backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs | 22 ++++++++++++- .../LocalWebApp/appsettings.Development.json | 3 +- frontend/viewer/src/App.svelte | 2 ++ frontend/viewer/src/FwDataProjectView.svelte | 6 ++++ frontend/viewer/src/ProjectView.svelte | 12 +++++-- .../Lexbox.ClientServer.Hubs.ts | 1 + .../TypedSignalR.Client/index.ts | 5 ++- .../notifications/NotificationOutlet.svelte | 32 +++++++++++++++++++ .../src/lib/notifications/notifications.ts | 21 ++++++++++++ .../lib/services/service-provider-signalr.ts | 6 ++-- 13 files changed, 124 insertions(+), 11 deletions(-) create mode 100644 frontend/viewer/src/lib/notifications/NotificationOutlet.svelte create mode 100644 frontend/viewer/src/lib/notifications/notifications.ts diff --git a/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs b/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs index 165ff881b..4e7ab74ff 100644 --- a/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs +++ b/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs @@ -13,8 +13,6 @@ public static IServiceCollection AddFwDataBridge(this IServiceCollection service services.AddLogging(); services.AddSingleton(); services.AddSingleton(); - //todo since this is scoped it gets created on each request (or hub method call), which opens the project file on each request - //this is not ideal since opening the project file can be slow. It should be done once per hub connection. services.AddKeyedScoped(FwDataApiKey, (provider, o) => provider.GetRequiredService().GetCurrentFwDataMiniLcmApi(true)); services.AddSingleton(); return services; diff --git a/backend/FwDataMiniLcmBridge/FwDataFactory.cs b/backend/FwDataMiniLcmBridge/FwDataFactory.cs index 80b872a8f..b9672dadf 100644 --- a/backend/FwDataMiniLcmBridge/FwDataFactory.cs +++ b/backend/FwDataMiniLcmBridge/FwDataFactory.cs @@ -64,9 +64,12 @@ private static void OnLcmProjectCacheEviction(object key, object? value, Evictio var (logger, projects) = ((ILogger, HashSet))state!; var name = lcmCache.ProjectId.Name; logger.LogInformation("Evicting project {ProjectFileName} from cache", name); - lcmCache.Dispose(); - logger.LogInformation("FW Data Project {ProjectFileName} disposed", name); projects.Remove((string)key); + if (!lcmCache.IsDisposed) + { + lcmCache.Dispose(); + logger.LogInformation("FW Data Project {ProjectFileName} disposed", name); + } } public void Dispose() @@ -90,4 +93,19 @@ public FwDataMiniLcmApi GetCurrentFwDataMiniLcmApi(bool saveOnDispose) } return GetFwDataMiniLcmApi(fwDataProject, true); } + + public void CloseCurrentProject() + { + var fwDataProject = context.Project; + if (fwDataProject is null) return; + CloseProject(fwDataProject); + } + + private void CloseProject(FwDataProject project) + { + var cacheKey = CacheKey(project); + var lcmCache = cache.Get(cacheKey); + if (lcmCache is null) return; + cache.Remove(cacheKey); + } } diff --git a/backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs b/backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs index 72c600fdb..3634d5f36 100644 --- a/backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs +++ b/backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs @@ -9,6 +9,7 @@ namespace LocalWebApp.Hubs; public interface ILexboxClient { Task OnEntryUpdated(Entry entry); + Task OnProjectClosed(); } public class CrdtMiniLcmApiHub( diff --git a/backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs b/backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs index 039f1b929..9d1e887fc 100644 --- a/backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs +++ b/backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs @@ -6,11 +6,31 @@ namespace LocalWebApp.Hubs; -public class FwDataMiniLcmHub([FromKeyedServices(FwDataBridgeKernel.FwDataApiKey)] ILexboxApi lexboxApi) : Hub +public class FwDataMiniLcmHub([FromKeyedServices(FwDataBridgeKernel.FwDataApiKey)] ILexboxApi lexboxApi, FwDataFactory fwDataFactory, + FwDataProjectContext context) : Hub { public const string ProjectRouteKey = "fwdata"; public override async Task OnConnectedAsync() { + var project = context.Project; + if (project is null) + { + throw new InvalidOperationException("No project is set in the context."); + } + await Groups.AddToGroupAsync(Context.ConnectionId, project.Name); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + //todo if multiple clients are connected, this will close the project for all of them. + fwDataFactory.CloseCurrentProject(); + var project = context.Project; + if (project is null) + { + throw new InvalidOperationException("No project is set in the context."); + } + await Clients.OthersInGroup(project.Name).OnProjectClosed(); + await Groups.RemoveFromGroupAsync(Context.ConnectionId, project.Name); } public async Task GetWritingSystems() diff --git a/backend/LocalWebApp/appsettings.Development.json b/backend/LocalWebApp/appsettings.Development.json index 960be5067..cd5854649 100644 --- a/backend/LocalWebApp/appsettings.Development.json +++ b/backend/LocalWebApp/appsettings.Development.json @@ -1,7 +1,8 @@ { "Logging": { "LogLevel": { - "Default": "Information" + "Default": "Information", + "LocalWebApp.Auth.LoggerAdapter": "Warning", } } } diff --git a/frontend/viewer/src/App.svelte b/frontend/viewer/src/App.svelte index ccfca6518..60aeae4f9 100644 --- a/frontend/viewer/src/App.svelte +++ b/frontend/viewer/src/App.svelte @@ -4,6 +4,7 @@ import TestProjectView from './TestProjectView.svelte'; import FwDataProjectView from './FwDataProjectView.svelte'; import HomeView from './HomeView.svelte'; + import NotificationOutlet from './lib/notifications/NotificationOutlet.svelte'; import Sandbox from './lib/sandbox/Sandbox.svelte'; export let url = ''; @@ -38,3 +39,4 @@ + diff --git a/frontend/viewer/src/FwDataProjectView.svelte b/frontend/viewer/src/FwDataProjectView.svelte index 3b7fff518..30f9232aa 100644 --- a/frontend/viewer/src/FwDataProjectView.svelte +++ b/frontend/viewer/src/FwDataProjectView.svelte @@ -4,6 +4,8 @@ import {onDestroy, setContext} from 'svelte'; import {SetupSignalR} from './lib/services/service-provider-signalr'; import ProjectView from './ProjectView.svelte'; + import {navigate} from 'svelte-routing'; + import {AppNotification} from './lib/notifications/notifications'; export let projectName: string; const connection = new HubConnectionBuilder() @@ -18,6 +20,10 @@ SetupSignalR(connection, { history: false, write: true, + }, + async () => { + navigate('/'); + AppNotification.display('Project closed on another tab', 'warning', 'long'); }); let connected = false; diff --git a/frontend/viewer/src/ProjectView.svelte b/frontend/viewer/src/ProjectView.svelte index 1ae31435d..cf0f7fd4b 100644 --- a/frontend/viewer/src/ProjectView.svelte +++ b/frontend/viewer/src/ProjectView.svelte @@ -1,7 +1,8 @@ +
+ {#each $notifications as notification} +
+ +
+ {#if notification.type === 'success'} + + {:else if notification.type === 'error'} + + {:else if notification.type === 'info'} + + {:else if notification.type === 'warning'} + + {/if} +
+
{notification.message}
+
+
+ {/each} +
diff --git a/frontend/viewer/src/lib/notifications/notifications.ts b/frontend/viewer/src/lib/notifications/notifications.ts new file mode 100644 index 000000000..e30db017b --- /dev/null +++ b/frontend/viewer/src/lib/notifications/notifications.ts @@ -0,0 +1,21 @@ +import {writable, type Writable, type Readable, readonly} from 'svelte/store'; + +export class AppNotification { + private static _notifications: Writable = writable([]); + public static get notifications(): Writable { + return this._notifications; + } + public static display(message: string, type: 'success' | 'error' | 'info' | 'warning', timeout: 'short' | 'long' | number = 'short') { + const notification = new AppNotification(message, type); + this._notifications.update(notifications => [...notifications, notification]); + if (timeout === -1) return; + if (typeof timeout === 'string') { + timeout = timeout === 'short' ? 5000 : 30000; + } + setTimeout(() => { + this._notifications.update(notifications => notifications.filter(n => n !== notification)); + }, timeout); + } + + private constructor(public message: string, public type: 'success' | 'error' | 'info' | 'warning') {} +} diff --git a/frontend/viewer/src/lib/services/service-provider-signalr.ts b/frontend/viewer/src/lib/services/service-provider-signalr.ts index 8e2b7812a..5c4319cb6 100644 --- a/frontend/viewer/src/lib/services/service-provider-signalr.ts +++ b/frontend/viewer/src/lib/services/service-provider-signalr.ts @@ -5,7 +5,8 @@ import type { HubConnection } from '@microsoft/signalr'; import type { LexboxApiFeatures, LexboxApiMetadata } from './lexbox-api'; import {LexboxService} from './service-provider'; -export function SetupSignalR(connection: HubConnection, features: LexboxApiFeatures) { +const noop = () => Promise.resolve(); +export function SetupSignalR(connection: HubConnection, features: LexboxApiFeatures, onProjectClosed: () => Promise = noop) { const hubFactory = getHubProxyFactory('ILexboxApiHub'); const hubProxy = hubFactory.createHubProxy(connection); @@ -17,7 +18,8 @@ export function SetupSignalR(connection: HubConnection, features: LexboxApiFeatu getReceiverRegister('ILexboxClient').register(connection, { OnEntryUpdated: async (entry: Entry) => { console.log('OnEntryUpdated', entry); - } + }, + OnProjectClosed: onProjectClosed }); window.lexbox.ServiceProvider.setService(LexboxService.LexboxApi, lexboxApiHubProxy); }