Skip to content

Commit c3e89ed

Browse files
committed
use SignalR for AlertDropdown
1 parent 15116a8 commit c3e89ed

File tree

10 files changed

+411
-27
lines changed

10 files changed

+411
-27
lines changed

Signum.React.Extensions/Alerts/AlertController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using Signum.React.Filters;
55
using Signum.Entities.Alerts;
66

7-
namespace Signum.React.Authorization;
7+
namespace Signum.React.Alerts;
88

99
[ValidateModelFilter]
1010
public class AlertController : ControllerBase

Signum.React.Extensions/Alerts/AlertDropdown.tsx

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import * as React from 'react'
2-
import { isRtl } from '@framework/AppContext'
32
import * as Operations from '@framework/Operations'
4-
import { getTypeInfo, symbolNiceName } from '@framework/Reflection'
53
import * as Finder from '@framework/Finder'
64
import { is, JavascriptMessage, toLite } from '@framework/Signum.Entities'
7-
import { Toast, NavItem, Button, ButtonGroup } from 'react-bootstrap'
5+
import { Toast, Button, ButtonGroup } from 'react-bootstrap'
86
import { DateTime } from 'luxon'
9-
import { useAPI, useAPIWithReload, useDocumentEvent, useForceUpdate, useInterval, usePrevious, useThrottle, useUpdatedRef } from '@framework/Hooks';
10-
import { LinkContainer } from '@framework/Components'
7+
import { useAPIWithReload, useForceUpdate, useUpdatedRef } from '@framework/Hooks';
118
import * as AuthClient from '../Authorization/AuthClient'
129
import * as Navigator from '@framework/Navigator'
1310
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@@ -17,27 +14,33 @@ import "./AlertDropdown.css"
1714
import { Link } from 'react-router-dom';
1815
import { classes, Dic } from '@framework/Globals'
1916
import MessageModal from '@framework/Modals/MessageModal'
20-
import { EntityLink } from '@framework/Search'
17+
import { useSignalRCallback, useSignalRConnection } from './useSignalR'
2118

22-
export default function AlertDropdown(props: { checkForChangesEvery?: number, keepRingingFor?: number }) {
19+
export default function AlertDropdown(props: { keepRingingFor?: number }) {
2320

2421
if (!Navigator.isViewable(AlertEntity))
2522
return null;
2623

27-
return <AlertDropdownImp checkForChangesEvery={props.checkForChangesEvery ?? 30 * 1000} keepRingingFor={props.keepRingingFor ?? 10 * 1000} />;
24+
return <AlertDropdownImp keepRingingFor={props.keepRingingFor ?? 10 * 1000} />;
2825
}
2926

30-
function AlertDropdownImp(props: { checkForChangesEvery: number, keepRingingFor: number }) {
27+
function AlertDropdownImp(props: { keepRingingFor: number }) {
28+
29+
const conn = useSignalRConnection("~/api/alertshub", {
30+
accessTokenFactory: () => AuthClient.getAuthToken()!,
31+
});
32+
33+
useSignalRCallback(conn, "AlertsChanged", () => {
34+
reloadCount();
35+
}, []);
3136

3237
const forceUpdate = useForceUpdate();
3338
const [isOpen, setIsOpen] = React.useState<boolean>(false);
3439
const [ringing, setRinging] = React.useState<boolean>(false);
3540
const ringingRef = useUpdatedRef(ringing);
3641

3742
const [showAlerts, setShowAlert] = React.useState<number>(5);
38-
39-
var ticks = useInterval(props.checkForChangesEvery, 0, n => n + 1);
40-
43+
4144
const isOpenRef = useUpdatedRef(isOpen);
4245

4346
var [countResult, reloadCount] = useAPIWithReload<AlertsClient.NumAlerts>((signal, oldResult) => AlertsClient.API.myAlertsCount().then(res => {
@@ -63,7 +66,7 @@ function AlertDropdownImp(props: { checkForChangesEvery: number, keepRingingFor:
6366
}
6467

6568
return res;
66-
}), [ticks], { avoidReset: true });
69+
}), [], { avoidReset: true });
6770

6871
React.useEffect(() => {
6972
if (ringing) {
@@ -75,10 +78,6 @@ function AlertDropdownImp(props: { checkForChangesEvery: number, keepRingingFor:
7578
}
7679
}, [ringing]);
7780

78-
useDocumentEvent("refresh-alerts", (e: Event) => {
79-
reloadCount();
80-
}, []);
81-
8281
const [alerts, setAlerts] = React.useState<AlertEntity[] | undefined>(undefined);
8382
const [groupBy, setGroupBy] = React.useState<AlertDropDownGroup>("ByTypeAndUser");
8483

@@ -108,8 +107,6 @@ function AlertDropdownImp(props: { checkForChangesEvery: number, keepRingingFor:
108107
countResult.numAlerts -= toRemove.length;
109108
forceUpdate();
110109

111-
112-
113110
Operations.API.executeMultiple(toRemove.map(a => toLite(a)), AlertOperation.Attend)
114111
.then(res => {
115112

Signum.React.Extensions/Alerts/AlertsClient.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,14 @@ export function start(options: { routes: JSX.Element[], showAlerts?: (typeName:
6060

6161
var cellFormatter = new Finder.CellFormatter((cell, ctx) => {
6262

63+
if (cell == null)
64+
return undefined;
65+
6366
var alert: Partial<AlertEntity> = {
6467
target: ctx.row.columns[ctx.columns.indexOf(AlertEntity.token(a => a.target).toString())],
6568
textArguments: ctx.row.columns[ctx.columns.indexOf(AlertEntity.token(a => a.entity.textArguments).toString())]
6669
};
67-
return formatText(cell, alert);
70+
return formatText(cell, alert);
6871
});
6972

7073
Finder.registerPropertyFormatter(PropertyRoute.tryParse(AlertEntity, "Text"), cellFormatter);
@@ -120,10 +123,13 @@ export function getTitle(titleField: string | null, type: AlertTypeSymbol | null
120123
if (titleField)
121124
return titleField;
122125

123-
if (type!.key)
126+
if (type == null)
127+
return " - ";
128+
129+
if (type.key)
124130
return symbolNiceName(type! as Entity & ISymbol);
125131

126-
return type!.name;
132+
return type.name;
127133
}
128134
export function formatText(text: string, alert: Partial<AlertEntity>, onNavigated?: () => void): React.ReactElement {
129135
var nodes: (string | React.ReactElement)[] = [];
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.AspNetCore.SignalR;
3+
using Signum.Entities.Basics;
4+
using Signum.React.Alerts;
5+
using Signum.React.Authorization;
6+
using System.Threading.Tasks;
7+
8+
namespace Signum.React.Facades;
9+
10+
public interface IAlertsClient
11+
{
12+
Task AlertsChanged();
13+
}
14+
15+
public class AlertsHub : Hub<IAlertsClient>
16+
{
17+
public override Task OnConnectedAsync()
18+
{
19+
var user = GetUser(Context.GetHttpContext()!);
20+
21+
AlertsServer.Connections.Add(user.ToLite(), Context.ConnectionId);
22+
23+
return base.OnConnectedAsync();
24+
}
25+
26+
public override Task OnDisconnectedAsync(Exception? exception)
27+
{
28+
AlertsServer.Connections.Remove(Context.ConnectionId);
29+
return base.OnDisconnectedAsync(exception);
30+
}
31+
32+
IUserEntity GetUser(HttpContext httpContext)
33+
{
34+
var tokenString = httpContext.Request.Query["access_token"];
35+
if (tokenString.Count > 1)
36+
throw new InvalidOperationException($"{tokenString.Count} values in 'access_token' query string found");
37+
38+
if (tokenString.Count == 0)
39+
{
40+
tokenString = httpContext.Request.Headers["Authorization"];
41+
42+
if (tokenString.Count != 1)
43+
throw new InvalidOperationException($"{tokenString.Count} values in 'Authorization' header found");
44+
}
45+
46+
var token = AuthTokenServer.DeserializeToken(tokenString.SingleEx());
47+
48+
return token.User;
49+
}
50+
}
51+
52+
public class ConnectionMapping<T> where T : class
53+
{
54+
private readonly Dictionary<T, HashSet<string>> userToConnection = new Dictionary<T, HashSet<string>>();
55+
private readonly Dictionary<string, T> connectionToUser = new Dictionary<string, T>();
56+
57+
public int Count => userToConnection.Count;
58+
59+
public void Add(T key, string connectionId)
60+
{
61+
lock (this)
62+
{
63+
HashSet<string>? connections;
64+
if (!userToConnection.TryGetValue(key, out connections))
65+
{
66+
connections = new HashSet<string>();
67+
userToConnection.Add(key, connections);
68+
}
69+
70+
connections.Add(connectionId);
71+
72+
connectionToUser.Add(connectionId, key);
73+
}
74+
}
75+
76+
public IEnumerable<string> GetConnections(T key) => userToConnection.TryGetC(key) ?? Enumerable.Empty<string>();
77+
78+
public void Remove(string connectionId)
79+
{
80+
lock (this)
81+
{
82+
var user = connectionToUser.TryGetC(connectionId);
83+
if (user != null)
84+
{
85+
HashSet<string>? connections = userToConnection.TryGetC(user);
86+
if (connections != null)
87+
{
88+
connections.Remove(connectionId);
89+
if (connections.Count == 0)
90+
userToConnection.Remove(user);
91+
}
92+
93+
connectionToUser.Remove(connectionId);
94+
}
95+
}
96+
}
97+
}
98+
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,93 @@
11
using Microsoft.AspNetCore.Builder;
2+
using Microsoft.AspNetCore.Routing;
3+
using Microsoft.AspNetCore.SignalR;
4+
using Signum.Entities.Alerts;
5+
using Signum.Entities.Authorization;
6+
using Signum.Entities.Basics;
7+
using Signum.React.Facades;
28

39
namespace Signum.React.Alerts;
410

511
public static class AlertsServer
612
{
13+
internal static ConnectionMapping<Lite<IUserEntity>> Connections = null!;
14+
15+
public static IHubContext<AlertsHub, IAlertsClient> AlertsHub { get; private set; }
16+
717
public static void Start(IApplicationBuilder app)
818
{
919
SignumControllerFactory.RegisterArea(MethodInfo.GetCurrentMethod());
20+
}
21+
22+
public static void MapAlertsHub(IEndpointRouteBuilder endpoints)
23+
{
24+
endpoints.MapHub<AlertsHub>("/api/alertshub");
25+
Connections = new ConnectionMapping<Lite<IUserEntity>>();
26+
AlertsHub = (IHubContext<AlertsHub, IAlertsClient>)endpoints.ServiceProvider.GetService(typeof(IHubContext<AlertsHub, IAlertsClient>))!;
27+
28+
var alertEvents = Schema.Current.EntityEvents<AlertEntity>();
29+
30+
alertEvents.Saved += AlertEvents_Saved;
31+
alertEvents.PreUnsafeDelete += AlertEvents_PreUnsafeDelete;
32+
alertEvents.PreUnsafeUpdate += AlertEvents_PreUnsafeUpdate;
33+
alertEvents.PreUnsafeInsert += AlertEvents_PreUnsafeInsert;
34+
}
35+
36+
private static IDisposable? AlertEvents_PreUnsafeUpdate(IUpdateable update, IQueryable<AlertEntity> entityQuery)
37+
{
38+
NotifyOnCommitQuery(entityQuery);
39+
return null;
40+
}
41+
42+
private static LambdaExpression AlertEvents_PreUnsafeInsert(IQueryable query, LambdaExpression constructor, IQueryable<AlertEntity> entityQuery)
43+
{
44+
NotifyOnCommitQuery(entityQuery);
45+
return constructor;
46+
}
1047

48+
private static IDisposable? AlertEvents_PreUnsafeDelete(IQueryable<AlertEntity> entityQuery)
49+
{
50+
NotifyOnCommitQuery(entityQuery);
51+
return null;
52+
}
53+
54+
private static void AlertEvents_Saved(AlertEntity ident, SavedEventArgs args)
55+
{
56+
if (ident.Recipient != null)
57+
NotifyOnCommit(ident.Recipient);
58+
}
59+
60+
private static void NotifyOnCommitQuery(IQueryable<AlertEntity> alerts)
61+
{
62+
var recipients = alerts.Where(a => a.Recipient != null && a.State == AlertState.Saved).Select(a => a.Recipient!).Distinct().ToArray();
63+
if (recipients.Any())
64+
NotifyOnCommit(recipients);
65+
}
66+
private static void NotifyOnCommit(params Lite<IUserEntity>[] recipients)
67+
{
68+
var hs = (HashSet<Lite<IUserEntity>>)Transaction.UserData.GetOrCreate("AlertRecipients", new HashSet<Lite<IUserEntity>>());
69+
hs.AddRange(recipients);
70+
71+
Transaction.PostRealCommit -= Transaction_PostRealCommit;
72+
Transaction.PostRealCommit += Transaction_PostRealCommit;
73+
}
74+
75+
private static void Transaction_PostRealCommit(Dictionary<string, object> dic)
76+
{
77+
var hashSet = (HashSet<Lite<IUserEntity>>)dic["AlertRecipients"];
78+
foreach (var user in hashSet)
79+
{
80+
foreach (var connectionId in Connections.GetConnections(user))
81+
{
82+
try
83+
{
84+
AlertsServer.AlertsHub.Clients.Client(connectionId).AlertsChanged();
85+
}
86+
catch(Exception ex)
87+
{
88+
ex.LogException();
89+
}
90+
}
91+
}
1192
}
1293
}

0 commit comments

Comments
 (0)