+411
-27
lines changedFilter options
+411
-27
lines changed Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
4
4
using Signum.React.Filters;
5
5
using Signum.Entities.Alerts;
6
6
7
-
namespace Signum.React.Authorization;
7
+
namespace Signum.React.Alerts;
8
8
9
9
[ValidateModelFilter]
10
10
public class AlertController : ControllerBase
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
1
1
import * as React from 'react'
2
-
import { isRtl } from '@framework/AppContext'
3
2
import * as Operations from '@framework/Operations'
4
-
import { getTypeInfo, symbolNiceName } from '@framework/Reflection'
5
3
import * as Finder from '@framework/Finder'
6
4
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'
8
6
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';
11
8
import * as AuthClient from '../Authorization/AuthClient'
12
9
import * as Navigator from '@framework/Navigator'
13
10
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@@ -17,27 +14,33 @@ import "./AlertDropdown.css"
17
14
import { Link } from 'react-router-dom';
18
15
import { classes, Dic } from '@framework/Globals'
19
16
import MessageModal from '@framework/Modals/MessageModal'
20
-
import { EntityLink } from '@framework/Search'
17
+
import { useSignalRCallback, useSignalRConnection } from './useSignalR'
21
18
22
-
export default function AlertDropdown(props: { checkForChangesEvery?: number, keepRingingFor?: number }) {
19
+
export default function AlertDropdown(props: { keepRingingFor?: number }) {
23
20
24
21
if (!Navigator.isViewable(AlertEntity))
25
22
return null;
26
23
27
-
return <AlertDropdownImp checkForChangesEvery={props.checkForChangesEvery ?? 30 * 1000} keepRingingFor={props.keepRingingFor ?? 10 * 1000} />;
24
+
return <AlertDropdownImp keepRingingFor={props.keepRingingFor ?? 10 * 1000} />;
28
25
}
29
26
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
+
}, []);
31
36
32
37
const forceUpdate = useForceUpdate();
33
38
const [isOpen, setIsOpen] = React.useState<boolean>(false);
34
39
const [ringing, setRinging] = React.useState<boolean>(false);
35
40
const ringingRef = useUpdatedRef(ringing);
36
41
37
42
const [showAlerts, setShowAlert] = React.useState<number>(5);
38
-
39
-
var ticks = useInterval(props.checkForChangesEvery, 0, n => n + 1);
40
-
43
+
41
44
const isOpenRef = useUpdatedRef(isOpen);
42
45
43
46
var [countResult, reloadCount] = useAPIWithReload<AlertsClient.NumAlerts>((signal, oldResult) => AlertsClient.API.myAlertsCount().then(res => {
@@ -63,7 +66,7 @@ function AlertDropdownImp(props: { checkForChangesEvery: number, keepRingingFor:
63
66
}
64
67
65
68
return res;
66
-
}), [ticks], { avoidReset: true });
69
+
}), [], { avoidReset: true });
67
70
68
71
React.useEffect(() => {
69
72
if (ringing) {
@@ -75,10 +78,6 @@ function AlertDropdownImp(props: { checkForChangesEvery: number, keepRingingFor:
75
78
}
76
79
}, [ringing]);
77
80
78
-
useDocumentEvent("refresh-alerts", (e: Event) => {
79
-
reloadCount();
80
-
}, []);
81
-
82
81
const [alerts, setAlerts] = React.useState<AlertEntity[] | undefined>(undefined);
83
82
const [groupBy, setGroupBy] = React.useState<AlertDropDownGroup>("ByTypeAndUser");
84
83
@@ -108,8 +107,6 @@ function AlertDropdownImp(props: { checkForChangesEvery: number, keepRingingFor:
108
107
countResult.numAlerts -= toRemove.length;
109
108
forceUpdate();
110
109
111
-
112
-
113
110
Operations.API.executeMultiple(toRemove.map(a => toLite(a)), AlertOperation.Attend)
114
111
.then(res => {
115
112
Original file line number Diff line number Diff line change
@@ -60,11 +60,14 @@ export function start(options: { routes: JSX.Element[], showAlerts?: (typeName:
60
60
61
61
var cellFormatter = new Finder.CellFormatter((cell, ctx) => {
62
62
63
+
if (cell == null)
64
+
return undefined;
65
+
63
66
var alert: Partial<AlertEntity> = {
64
67
target: ctx.row.columns[ctx.columns.indexOf(AlertEntity.token(a => a.target).toString())],
65
68
textArguments: ctx.row.columns[ctx.columns.indexOf(AlertEntity.token(a => a.entity.textArguments).toString())]
66
69
};
67
-
return formatText(cell, alert);
70
+
return formatText(cell, alert);
68
71
});
69
72
70
73
Finder.registerPropertyFormatter(PropertyRoute.tryParse(AlertEntity, "Text"), cellFormatter);
@@ -120,10 +123,13 @@ export function getTitle(titleField: string | null, type: AlertTypeSymbol | null
120
123
if (titleField)
121
124
return titleField;
122
125
123
-
if (type!.key)
126
+
if (type == null)
127
+
return " - ";
128
+
129
+
if (type.key)
124
130
return symbolNiceName(type! as Entity & ISymbol);
125
131
126
-
return type!.name;
132
+
return type.name;
127
133
}
128
134
export function formatText(text: string, alert: Partial<AlertEntity>, onNavigated?: () => void): React.ReactElement {
129
135
var nodes: (string | React.ReactElement)[] = [];
Original file line number Diff line number Diff 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
+
Original file line number Diff line number Diff line change
@@ -1,12 +1,93 @@
1
1
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;
2
8
3
9
namespace Signum.React.Alerts;
4
10
5
11
public static class AlertsServer
6
12
{
13
+
internal static ConnectionMapping<Lite<IUserEntity>> Connections = null!;
14
+
15
+
public static IHubContext<AlertsHub, IAlertsClient> AlertsHub { get; private set; }
16
+
7
17
public static void Start(IApplicationBuilder app)
8
18
{
9
19
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
+
}
10
47
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
+
}
11
92
}
12
93
}
You can’t perform that action at this time.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4