Skip to content

Commit

Permalink
close fwdata file when leaving the editor (#925)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
hahn-kev authored Jul 5, 2024
1 parent e5074b9 commit 9f36064
Show file tree
Hide file tree
Showing 13 changed files with 124 additions and 11 deletions.
2 changes: 0 additions & 2 deletions backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ public static IServiceCollection AddFwDataBridge(this IServiceCollection service
services.AddLogging();
services.AddSingleton<FwDataFactory>();
services.AddSingleton<IProjectLoader, ProjectLoader>();
//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<ILexboxApi>(FwDataApiKey, (provider, o) => provider.GetRequiredService<FwDataFactory>().GetCurrentFwDataMiniLcmApi(true));
services.AddSingleton<FwDataProjectContext>();
return services;
Expand Down
22 changes: 20 additions & 2 deletions backend/FwDataMiniLcmBridge/FwDataFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,12 @@ private static void OnLcmProjectCacheEviction(object key, object? value, Evictio
var (logger, projects) = ((ILogger<FwDataFactory>, HashSet<string>))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()
Expand All @@ -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<LcmCache>(cacheKey);
if (lcmCache is null) return;
cache.Remove(cacheKey);
}
}
1 change: 1 addition & 0 deletions backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace LocalWebApp.Hubs;
public interface ILexboxClient
{
Task OnEntryUpdated(Entry entry);
Task OnProjectClosed();
}

public class CrdtMiniLcmApiHub(
Expand Down
22 changes: 21 additions & 1 deletion backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,31 @@

namespace LocalWebApp.Hubs;

public class FwDataMiniLcmHub([FromKeyedServices(FwDataBridgeKernel.FwDataApiKey)] ILexboxApi lexboxApi) : Hub<ILexboxClient>
public class FwDataMiniLcmHub([FromKeyedServices(FwDataBridgeKernel.FwDataApiKey)] ILexboxApi lexboxApi, FwDataFactory fwDataFactory,
FwDataProjectContext context) : Hub<ILexboxClient>
{
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<WritingSystems> GetWritingSystems()
Expand Down
3 changes: 2 additions & 1 deletion backend/LocalWebApp/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information"
"Default": "Information",
"LocalWebApp.Auth.LoggerAdapter": "Warning",
}
}
}
2 changes: 2 additions & 0 deletions frontend/viewer/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down Expand Up @@ -38,3 +39,4 @@
</Route>
</div>
</Router>
<NotificationOutlet/>
6 changes: 6 additions & 0 deletions frontend/viewer/src/FwDataProjectView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -18,6 +20,10 @@
SetupSignalR(connection, {
history: false,
write: true,
},
async () => {
navigate('/');
AppNotification.display('Project closed on another tab', 'warning', 'long');
});
let connected = false;
</script>
Expand Down
12 changes: 10 additions & 2 deletions frontend/viewer/src/ProjectView.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
import {AppBar, Button, ProgressCircle} from 'svelte-ux';
import {mdiArrowCollapseLeft, mdiArrowCollapseRight, mdiArrowLeft, mdiEyeSettingsOutline} from '@mdi/js';
import {mdiArrowCollapseLeft, mdiArrowCollapseRight, mdiArrowLeft, mdiEyeSettingsOutline, mdiHome} from '@mdi/js';
import Editor from './lib/Editor.svelte';
import {navigate} from 'svelte-routing';
import {headword, pickBestAlternative} from './lib/utils';
import {views} from './lib/config-data';
import {useLexboxApi} from './lib/services/service-provider';
Expand Down Expand Up @@ -57,6 +58,7 @@
export let projectName: string;
export let isConnected: boolean;
export let showHomeButton = true;
$: connected.set(isConnected);
const connected = writable(false);
Expand Down Expand Up @@ -228,7 +230,13 @@
</div>
{:else}
<div class="project-view !flex flex-col PortalTarget" style="{spaceForEditorStyle}">
<AppBar title={projectName} class="bg-secondary min-h-12 shadow-md" menuIcon=''>
<AppBar title={projectName} class="bg-secondary min-h-12 shadow-md">
<Button
classes={{root: showHomeButton ? '' : 'hidden'}}
slot="menuIcon"
icon={mdiHome}
on:click={() => navigate('/')}
/>
<div class="flex-grow-0 flex-shrink-0 lg:hidden mx-2 sm:mr-0" class:invisible={!pickedEntry}>
<Button icon={mdiArrowLeft} size="sm" iconOnly rounded variant="outline" on:click={() => pickedEntry = false} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export type ILexboxClient = {
* @returns Transpiled from System.Threading.Tasks.Task
*/
OnEntryUpdated(entry: Entry): Promise<void>;
OnProjectClosed(): Promise<void>;
}

Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,14 @@ class ILexboxClient_Binder implements ReceiverRegister<ILexboxClient> {
public readonly register = (connection: HubConnection, receiver: ILexboxClient): Disposable => {

const __onEntryUpdated = (...args: [Entry]) => receiver.OnEntryUpdated(...args);
const __onProjectClosed = () => receiver.OnProjectClosed();

connection.on("OnEntryUpdated", __onEntryUpdated);
connection.on("OnProjectClosed", __onProjectClosed);

const methodList: ReceiverMethod[] = [
{ methodName: "OnEntryUpdated", method: __onEntryUpdated }
{ methodName: "OnEntryUpdated", method: __onEntryUpdated },
{ methodName: "OnProjectClosed", method: __onProjectClosed },
]

return new ReceiverMethodSubscription(connection, methodList);
Expand Down
32 changes: 32 additions & 0 deletions frontend/viewer/src/lib/notifications/NotificationOutlet.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts">
import {AppNotification} from './notifications';
import {Notification, Icon} from 'svelte-ux';
import {
mdiAlert,
mdiAlertCircleOutline,
mdiCheckCircleOutline,
mdiInformationOutline
} from '@mdi/js';
const notifications = AppNotification.notifications;
</script>
<div class="fixed bottom-0 z-50 flex flex-col gap-2 p-4 w-full overflow-y-auto">
{#each $notifications as notification}
<div class="w-[400px] mx-auto">
<Notification open closeIcon>
<div slot="icon">
{#if notification.type === 'success'}
<Icon path={mdiCheckCircleOutline} size="1.5rem" class="text-success"/>
{:else if notification.type === 'error'}
<Icon path={mdiAlert} size="1.5rem" class="text-danger"/>
{:else if notification.type === 'info'}
<Icon path={mdiInformationOutline} size="1.5rem" class="text-info"/>
{:else if notification.type === 'warning'}
<Icon path={mdiAlertCircleOutline} size="1.5rem" class="text-warning"/>
{/if}
</div>
<div slot="title">{notification.message}</div>
</Notification>
</div>
{/each}
</div>
21 changes: 21 additions & 0 deletions frontend/viewer/src/lib/notifications/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {writable, type Writable, type Readable, readonly} from 'svelte/store';

export class AppNotification {
private static _notifications: Writable<AppNotification[]> = writable([]);
public static get notifications(): Writable<AppNotification[]> {
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') {}
}
6 changes: 4 additions & 2 deletions frontend/viewer/src/lib/services/service-provider-signalr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> = noop) {
const hubFactory = getHubProxyFactory('ILexboxApiHub');
const hubProxy = hubFactory.createHubProxy(connection);

Expand All @@ -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);
}

0 comments on commit 9f36064

Please sign in to comment.