add alert/notification functionality (for server connection events and messages)
This commit is contained in:
parent
ffb0e5cac1
commit
a44b4e9475
55
Application/Alerts/AlertExtensions.cs
Normal file
55
Application/Alerts/AlertExtensions.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Alerts;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Alerts;
|
||||||
|
|
||||||
|
public static class AlertExtensions
|
||||||
|
{
|
||||||
|
public static Alert.AlertState BuildAlert(this EFClient client, Alert.AlertCategory? type = null)
|
||||||
|
{
|
||||||
|
return new Alert.AlertState
|
||||||
|
{
|
||||||
|
RecipientId = client.ClientId,
|
||||||
|
Category = type ?? Alert.AlertCategory.Information
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alert.AlertState WithCategory(this Alert.AlertState state, Alert.AlertCategory category)
|
||||||
|
{
|
||||||
|
state.Category = category;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alert.AlertState OfType(this Alert.AlertState state, string type)
|
||||||
|
{
|
||||||
|
state.Type = type;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alert.AlertState WithMessage(this Alert.AlertState state, string message)
|
||||||
|
{
|
||||||
|
state.Message = message;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alert.AlertState ExpiresIn(this Alert.AlertState state, TimeSpan expiration)
|
||||||
|
{
|
||||||
|
state.ExpiresAt = DateTime.Now.Add(expiration);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alert.AlertState FromSource(this Alert.AlertState state, string source)
|
||||||
|
{
|
||||||
|
state.Source = source;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alert.AlertState FromClient(this Alert.AlertState state, EFClient client)
|
||||||
|
{
|
||||||
|
state.Source = client.Name.StripColors();
|
||||||
|
state.SourceId = client.ClientId;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
137
Application/Alerts/AlertManager.cs
Normal file
137
Application/Alerts/AlertManager.cs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using SharedLibraryCore.Alerts;
|
||||||
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Alerts;
|
||||||
|
|
||||||
|
public class AlertManager : IAlertManager
|
||||||
|
{
|
||||||
|
private readonly ApplicationConfiguration _appConfig;
|
||||||
|
private readonly ConcurrentDictionary<int, List<Alert.AlertState>> _states = new();
|
||||||
|
private readonly List<Func<Task<IEnumerable<Alert.AlertState>>>> _staticSources = new();
|
||||||
|
|
||||||
|
public AlertManager(ApplicationConfiguration appConfig)
|
||||||
|
{
|
||||||
|
_appConfig = appConfig;
|
||||||
|
_states.TryAdd(0, new List<Alert.AlertState>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
|
||||||
|
|
||||||
|
public async Task Initialize()
|
||||||
|
{
|
||||||
|
foreach (var source in _staticSources)
|
||||||
|
{
|
||||||
|
var alerts = await source();
|
||||||
|
foreach (var alert in alerts)
|
||||||
|
{
|
||||||
|
AddAlert(alert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client)
|
||||||
|
{
|
||||||
|
lock (_states)
|
||||||
|
{
|
||||||
|
var alerts = Enumerable.Empty<Alert.AlertState>();
|
||||||
|
if (client.Level > Data.Models.Client.EFClient.Permission.Trusted)
|
||||||
|
{
|
||||||
|
alerts = alerts.Concat(_states[0].Where(alert =>
|
||||||
|
alert.MinimumPermission is null || alert.MinimumPermission <= client.Level));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_states.ContainsKey(client.ClientId))
|
||||||
|
{
|
||||||
|
alerts = alerts.Concat(_states[client.ClientId].AsReadOnly());
|
||||||
|
}
|
||||||
|
|
||||||
|
return alerts.OrderByDescending(alert => alert.OccuredAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkAlertAsRead(Guid alertId)
|
||||||
|
{
|
||||||
|
lock (_states)
|
||||||
|
{
|
||||||
|
foreach (var items in _states.Values)
|
||||||
|
{
|
||||||
|
var matchingEvent = items.FirstOrDefault(item => item.AlertId == alertId);
|
||||||
|
|
||||||
|
if (matchingEvent is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Remove(matchingEvent);
|
||||||
|
OnAlertConsumed?.Invoke(this, matchingEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkAllAlertsAsRead(int recipientId)
|
||||||
|
{
|
||||||
|
lock (_states)
|
||||||
|
{
|
||||||
|
foreach (var items in _states.Values)
|
||||||
|
{
|
||||||
|
items.RemoveAll(item =>
|
||||||
|
{
|
||||||
|
if (item.RecipientId != null && item.RecipientId != recipientId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnAlertConsumed?.Invoke(this, item);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddAlert(Alert.AlertState alert)
|
||||||
|
{
|
||||||
|
lock (_states)
|
||||||
|
{
|
||||||
|
if (alert.RecipientId is null)
|
||||||
|
{
|
||||||
|
_states[0].Add(alert);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_states.ContainsKey(alert.RecipientId.Value))
|
||||||
|
{
|
||||||
|
_states[alert.RecipientId.Value] = new List<Alert.AlertState>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_appConfig.MinimumAlertPermissions.ContainsKey(alert.Type))
|
||||||
|
{
|
||||||
|
alert.MinimumPermission = _appConfig.MinimumAlertPermissions[alert.Type];
|
||||||
|
}
|
||||||
|
|
||||||
|
_states[alert.RecipientId.Value].Add(alert);
|
||||||
|
|
||||||
|
PruneOldAlerts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource)
|
||||||
|
{
|
||||||
|
_staticSources.Add(alertSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void PruneOldAlerts()
|
||||||
|
{
|
||||||
|
foreach (var value in _states.Values)
|
||||||
|
{
|
||||||
|
value.RemoveAll(item => item.ExpiresAt < DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -57,6 +57,7 @@ namespace IW4MAdmin.Application
|
|||||||
private readonly List<MessageToken> MessageTokens;
|
private readonly List<MessageToken> MessageTokens;
|
||||||
private readonly ClientService ClientSvc;
|
private readonly ClientService ClientSvc;
|
||||||
readonly PenaltyService PenaltySvc;
|
readonly PenaltyService PenaltySvc;
|
||||||
|
private readonly IAlertManager _alertManager;
|
||||||
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
||||||
readonly IPageList PageList;
|
readonly IPageList PageList;
|
||||||
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
|
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
|
||||||
@ -82,13 +83,14 @@ namespace IW4MAdmin.Application
|
|||||||
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
|
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
|
||||||
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
|
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
|
||||||
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
|
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
|
||||||
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService)
|
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
|
||||||
{
|
{
|
||||||
MiddlewareActionHandler = actionHandler;
|
MiddlewareActionHandler = actionHandler;
|
||||||
_servers = new ConcurrentBag<Server>();
|
_servers = new ConcurrentBag<Server>();
|
||||||
MessageTokens = new List<MessageToken>();
|
MessageTokens = new List<MessageToken>();
|
||||||
ClientSvc = clientService;
|
ClientSvc = clientService;
|
||||||
PenaltySvc = penaltyService;
|
PenaltySvc = penaltyService;
|
||||||
|
_alertManager = alertManager;
|
||||||
ConfigHandler = appConfigHandler;
|
ConfigHandler = appConfigHandler;
|
||||||
StartTime = DateTime.UtcNow;
|
StartTime = DateTime.UtcNow;
|
||||||
PageList = new PageList();
|
PageList = new PageList();
|
||||||
@ -508,6 +510,7 @@ namespace IW4MAdmin.Application
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
_metaRegistration.Register();
|
_metaRegistration.Register();
|
||||||
|
await _alertManager.Initialize();
|
||||||
|
|
||||||
#region CUSTOM_EVENTS
|
#region CUSTOM_EVENTS
|
||||||
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
|
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
|
||||||
@ -697,5 +700,6 @@ namespace IW4MAdmin.Application
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
|
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
|
||||||
|
public IAlertManager AlertManager => _alertManager;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Data.Abstractions;
|
using Data.Abstractions;
|
||||||
using Data.Models.Client;
|
using Data.Models.Client;
|
||||||
using Data.Models.Misc;
|
using Data.Models.Misc;
|
||||||
|
using IW4MAdmin.Application.Alerts;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Alerts;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
@ -16,19 +19,66 @@ namespace IW4MAdmin.Application.Commands
|
|||||||
{
|
{
|
||||||
private readonly IDatabaseContextFactory _contextFactory;
|
private readonly IDatabaseContextFactory _contextFactory;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly IAlertManager _alertManager;
|
||||||
private const short MaxLength = 1024;
|
private const short MaxLength = 1024;
|
||||||
|
|
||||||
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
|
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
|
||||||
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger) : base(config, layout)
|
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger, IAlertManager alertManager)
|
||||||
|
: base(config, layout)
|
||||||
{
|
{
|
||||||
Name = "offlinemessage";
|
Name = "offlinemessage";
|
||||||
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
|
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
|
||||||
Alias = "om";
|
Alias = "om";
|
||||||
Permission = EFClient.Permission.Moderator;
|
Permission = EFClient.Permission.Moderator;
|
||||||
RequiresTarget = true;
|
RequiresTarget = true;
|
||||||
|
|
||||||
_contextFactory = contextFactory;
|
_contextFactory = contextFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_alertManager = alertManager;
|
||||||
|
|
||||||
|
_alertManager.RegisterStaticAlertSource(async () =>
|
||||||
|
{
|
||||||
|
var context = contextFactory.CreateContext(false);
|
||||||
|
return await context.InboxMessages.Where(message => !message.IsDelivered)
|
||||||
|
.Where(message => message.CreatedDateTime >= DateTime.UtcNow.AddDays(-7))
|
||||||
|
.Where(message => message.DestinationClient.Level > EFClient.Permission.User)
|
||||||
|
.Select(message => new Alert.AlertState
|
||||||
|
{
|
||||||
|
OccuredAt = message.CreatedDateTime,
|
||||||
|
Message = message.Message,
|
||||||
|
ExpiresAt = DateTime.UtcNow.AddDays(7),
|
||||||
|
Category = Alert.AlertCategory.Message,
|
||||||
|
Source = message.SourceClient.CurrentAlias.Name.StripColors(),
|
||||||
|
SourceId = message.SourceClientId,
|
||||||
|
RecipientId = message.DestinationClientId,
|
||||||
|
ReferenceId = message.InboxMessageId,
|
||||||
|
Type = nameof(EFInboxMessage)
|
||||||
|
}).ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
_alertManager.OnAlertConsumed += (_, state) =>
|
||||||
|
{
|
||||||
|
if (state.Category != Alert.AlertCategory.Message || state.ReferenceId is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var context = contextFactory.CreateContext(true);
|
||||||
|
foreach (var message in context.InboxMessages
|
||||||
|
.Where(message => message.InboxMessageId == state.ReferenceId.Value).ToList())
|
||||||
|
{
|
||||||
|
message.IsDelivered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.SaveChanges();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Could not update message state for alert {@Alert}", state);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task ExecuteAsync(GameEvent gameEvent)
|
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||||
@ -38,23 +88,24 @@ namespace IW4MAdmin.Application.Commands
|
|||||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
|
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
|
if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
|
||||||
{
|
{
|
||||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
|
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gameEvent.Target.IsIngame)
|
if (gameEvent.Target.IsIngame)
|
||||||
{
|
{
|
||||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"].FormatExt(gameEvent.Target.Name));
|
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"]
|
||||||
|
.FormatExt(gameEvent.Target.Name));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||||
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
|
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
|
||||||
|
|
||||||
var newMessage = new EFInboxMessage()
|
var newMessage = new EFInboxMessage
|
||||||
{
|
{
|
||||||
SourceClientId = gameEvent.Origin.ClientId,
|
SourceClientId = gameEvent.Origin.ClientId,
|
||||||
DestinationClientId = gameEvent.Target.ClientId,
|
DestinationClientId = gameEvent.Target.ClientId,
|
||||||
@ -62,6 +113,12 @@ namespace IW4MAdmin.Application.Commands
|
|||||||
Message = gameEvent.Data,
|
Message = gameEvent.Data,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_alertManager.AddAlert(gameEvent.Target.BuildAlert(Alert.AlertCategory.Message)
|
||||||
|
.WithMessage(gameEvent.Data.Trim())
|
||||||
|
.FromClient(gameEvent.Origin)
|
||||||
|
.OfType(nameof(EFInboxMessage))
|
||||||
|
.ExpiresIn(TimeSpan.FromDays(7)));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
context.Set<EFInboxMessage>().Add(newMessage);
|
context.Set<EFInboxMessage>().Add(newMessage);
|
||||||
@ -75,4 +132,4 @@ namespace IW4MAdmin.Application.Commands
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,10 @@ using Serilog.Context;
|
|||||||
using static SharedLibraryCore.Database.Models.EFClient;
|
using static SharedLibraryCore.Database.Models.EFClient;
|
||||||
using Data.Models;
|
using Data.Models;
|
||||||
using Data.Models.Server;
|
using Data.Models.Server;
|
||||||
|
using IW4MAdmin.Application.Alerts;
|
||||||
using IW4MAdmin.Application.Commands;
|
using IW4MAdmin.Application.Commands;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SharedLibraryCore.Alerts;
|
||||||
using static Data.Models.Client.EFClient;
|
using static Data.Models.Client.EFClient;
|
||||||
|
|
||||||
namespace IW4MAdmin
|
namespace IW4MAdmin
|
||||||
@ -306,8 +308,16 @@ namespace IW4MAdmin
|
|||||||
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
|
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
|
||||||
{
|
{
|
||||||
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
|
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
|
||||||
|
|
||||||
|
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
|
||||||
|
.WithCategory(Alert.AlertCategory.Error)
|
||||||
|
.FromSource("System")
|
||||||
|
.WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"))
|
||||||
|
.ExpiresIn(TimeSpan.FromDays(1));
|
||||||
|
|
||||||
|
Manager.AlertManager.AddAlert(alert);
|
||||||
}
|
}
|
||||||
|
|
||||||
Throttled = true;
|
Throttled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,7 +328,15 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
|
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
|
||||||
{
|
{
|
||||||
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
|
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"));
|
||||||
|
|
||||||
|
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
|
||||||
|
.WithCategory(Alert.AlertCategory.Information)
|
||||||
|
.FromSource("System")
|
||||||
|
.WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"))
|
||||||
|
.ExpiresIn(TimeSpan.FromDays(1));
|
||||||
|
|
||||||
|
Manager.AlertManager.AddAlert(alert);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(CustomSayName))
|
if (!string.IsNullOrEmpty(CustomSayName))
|
||||||
|
@ -27,6 +27,7 @@ using System.Threading.Tasks;
|
|||||||
using Data.Abstractions;
|
using Data.Abstractions;
|
||||||
using Data.Helpers;
|
using Data.Helpers;
|
||||||
using Integrations.Source.Extensions;
|
using Integrations.Source.Extensions;
|
||||||
|
using IW4MAdmin.Application.Alerts;
|
||||||
using IW4MAdmin.Application.Extensions;
|
using IW4MAdmin.Application.Extensions;
|
||||||
using IW4MAdmin.Application.Localization;
|
using IW4MAdmin.Application.Localization;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -448,6 +449,7 @@ namespace IW4MAdmin.Application
|
|||||||
.AddSingleton<IServerDataCollector, ServerDataCollector>()
|
.AddSingleton<IServerDataCollector, ServerDataCollector>()
|
||||||
.AddSingleton<IEventPublisher, EventPublisher>()
|
.AddSingleton<IEventPublisher, EventPublisher>()
|
||||||
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
|
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
|
||||||
|
.AddSingleton<IAlertManager, AlertManager>()
|
||||||
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
|
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
|
||||||
.AddSingleton(translationLookup)
|
.AddSingleton(translationLookup)
|
||||||
.AddDatabaseContextOptions(appConfig);
|
.AddDatabaseContextOptions(appConfig);
|
||||||
|
33
SharedLibraryCore/Alerts/Alert.cs
Normal file
33
SharedLibraryCore/Alerts/Alert.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using Data.Models.Client;
|
||||||
|
|
||||||
|
namespace SharedLibraryCore.Alerts;
|
||||||
|
|
||||||
|
public class Alert
|
||||||
|
{
|
||||||
|
public enum AlertCategory
|
||||||
|
{
|
||||||
|
Information,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
Message,
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AlertState
|
||||||
|
{
|
||||||
|
public Guid AlertId { get; } = Guid.NewGuid();
|
||||||
|
public AlertCategory Category { get; set; }
|
||||||
|
public DateTime OccuredAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime? ExpiresAt { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public string Source { get; set; }
|
||||||
|
public int? RecipientId { get; set; }
|
||||||
|
public int? SourceId { get; set; }
|
||||||
|
public int? ReferenceId { get; set; }
|
||||||
|
public bool? Delivered { get; set; }
|
||||||
|
public bool? Consumed { get; set; }
|
||||||
|
public EFClient.Permission? MinimumPermission { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
public static AlertState Build() => new();
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,8 @@ namespace SharedLibraryCore
|
|||||||
{
|
{
|
||||||
public class BaseController : Controller
|
public class BaseController : Controller
|
||||||
{
|
{
|
||||||
|
protected readonly IAlertManager AlertManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// life span in months
|
/// life span in months
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -34,6 +36,7 @@ namespace SharedLibraryCore
|
|||||||
|
|
||||||
public BaseController(IManager manager)
|
public BaseController(IManager manager)
|
||||||
{
|
{
|
||||||
|
AlertManager = manager.AlertManager;
|
||||||
Manager = manager;
|
Manager = manager;
|
||||||
Localization ??= Utilities.CurrentLocalization.LocalizationIndex;
|
Localization ??= Utilities.CurrentLocalization.LocalizationIndex;
|
||||||
AppConfig = Manager.GetApplicationSettings().Configuration();
|
AppConfig = Manager.GetApplicationSettings().Configuration();
|
||||||
@ -169,6 +172,7 @@ namespace SharedLibraryCore
|
|||||||
ViewBag.ReportCount = Manager.GetServers().Sum(server =>
|
ViewBag.ReportCount = Manager.GetServers().Sum(server =>
|
||||||
server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24)));
|
server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24)));
|
||||||
ViewBag.PermissionsSet = PermissionsSet;
|
ViewBag.PermissionsSet = PermissionsSet;
|
||||||
|
ViewBag.Alerts = AlertManager.RetrieveAlerts(Client).ToList();
|
||||||
|
|
||||||
base.OnActionExecuting(context);
|
base.OnActionExecuting(context);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Data.Models.Misc;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using SharedLibraryCore.Configuration.Attributes;
|
using SharedLibraryCore.Configuration.Attributes;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
@ -154,6 +155,13 @@ namespace SharedLibraryCore.Configuration
|
|||||||
{ Permission.Console.ToString(), new List<string> { "*" } }
|
{ Permission.Console.ToString(), new List<string> { "*" } }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public Dictionary<string, Permission> MinimumAlertPermissions { get; set; } = new()
|
||||||
|
{
|
||||||
|
{ nameof(EFInboxMessage), Permission.Trusted },
|
||||||
|
{ GameEvent.EventType.ConnectionLost.ToString(), Permission.Administrator },
|
||||||
|
{ GameEvent.EventType.ConnectionRestored.ToString(), Permission.Administrator }
|
||||||
|
};
|
||||||
|
|
||||||
[ConfigurationIgnore]
|
[ConfigurationIgnore]
|
||||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")]
|
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")]
|
||||||
public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new()
|
public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new()
|
||||||
|
54
SharedLibraryCore/Interfaces/IAlertManager.cs
Normal file
54
SharedLibraryCore/Interfaces/IAlertManager.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using SharedLibraryCore.Alerts;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
|
||||||
|
namespace SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
public interface IAlertManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the manager
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task Initialize();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all the alerts for given client
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">client to retrieve alerts for</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger a new alert
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="alert">Alert to trigger</param>
|
||||||
|
void AddAlert(Alert.AlertState alert);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks an alert as read and removes it from the manager
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="alertId">Id of the alert to mark as read</param>
|
||||||
|
void MarkAlertAsRead(Guid alertId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mark all alerts intended for the given recipientId as read
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="recipientId">Identifier of the recipient</param>
|
||||||
|
void MarkAllAlertsAsRead(int recipientId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a static (persistent) event source eg datastore that
|
||||||
|
/// gets initialized at startup
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="alertSource">Source action</param>
|
||||||
|
void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fires when an alert has been consumed (dimissed)
|
||||||
|
/// </summary>
|
||||||
|
EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
|
||||||
|
|
||||||
|
}
|
@ -102,5 +102,7 @@ namespace SharedLibraryCore.Interfaces
|
|||||||
/// event executed when event has finished executing
|
/// event executed when event has finished executing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<GameEvent> OnGameEventExecuted;
|
event EventHandler<GameEvent> OnGameEventExecuted;
|
||||||
|
|
||||||
|
IAlertManager AlertManager { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ namespace WebfrontCore.Controllers
|
|||||||
private readonly string _unbanCommandName;
|
private readonly string _unbanCommandName;
|
||||||
private readonly string _sayCommandName;
|
private readonly string _sayCommandName;
|
||||||
private readonly string _kickCommandName;
|
private readonly string _kickCommandName;
|
||||||
|
private readonly string _offlineMessageCommandName;
|
||||||
private readonly string _flagCommandName;
|
private readonly string _flagCommandName;
|
||||||
private readonly string _unflagCommandName;
|
private readonly string _unflagCommandName;
|
||||||
private readonly string _setLevelCommandName;
|
private readonly string _setLevelCommandName;
|
||||||
@ -64,6 +65,9 @@ namespace WebfrontCore.Controllers
|
|||||||
case nameof(SetLevelCommand):
|
case nameof(SetLevelCommand):
|
||||||
_setLevelCommandName = cmd.Name;
|
_setLevelCommandName = cmd.Name;
|
||||||
break;
|
break;
|
||||||
|
case "OfflineMessageCommand":
|
||||||
|
_offlineMessageCommandName = cmd.Name;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -213,7 +217,7 @@ namespace WebfrontCore.Controllers
|
|||||||
|
|
||||||
public async Task<IActionResult> Login(int clientId, string password)
|
public async Task<IActionResult> Login(int clientId, string password)
|
||||||
{
|
{
|
||||||
return await Task.FromResult(RedirectToAction("Login", "Account", new {clientId, password}));
|
return await Task.FromResult(RedirectToAction("Login", "Account", new { clientId, password }));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult EditForm()
|
public IActionResult EditForm()
|
||||||
@ -327,26 +331,27 @@ namespace WebfrontCore.Controllers
|
|||||||
public async Task<IActionResult> RecentClientsForm(PaginationRequest request)
|
public async Task<IActionResult> RecentClientsForm(PaginationRequest request)
|
||||||
{
|
{
|
||||||
ViewBag.First = request.Offset == 0;
|
ViewBag.First = request.Offset == 0;
|
||||||
|
|
||||||
if (request.Count > 20)
|
if (request.Count > 20)
|
||||||
{
|
{
|
||||||
request.Count = 20;
|
request.Count = 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
var clients = await Manager.GetClientService().GetRecentClients(request);
|
var clients = await Manager.GetClientService().GetRecentClients(request);
|
||||||
|
|
||||||
return request.Offset == 0
|
return request.Offset == 0
|
||||||
? View("~/Views/Shared/Components/Client/_RecentClientsContainer.cshtml", clients)
|
? View("~/Views/Shared/Components/Client/_RecentClientsContainer.cshtml", clients)
|
||||||
: View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients);
|
: View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult RecentReportsForm()
|
public IActionResult RecentReportsForm()
|
||||||
{
|
{
|
||||||
var serverInfo = Manager.GetServers().Select(server =>
|
var serverInfo = Manager.GetServers().Select(server =>
|
||||||
new ServerInfo
|
new ServerInfo
|
||||||
{
|
{
|
||||||
Name = server.Hostname,
|
Name = server.Hostname,
|
||||||
Reports = server.Reports.Where(report => (DateTime.UtcNow - report.ReportedOn).TotalHours <= 24).ToList()
|
Reports = server.Reports.Where(report => (DateTime.UtcNow - report.ReportedOn).TotalHours <= 24)
|
||||||
|
.ToList()
|
||||||
});
|
});
|
||||||
|
|
||||||
return View("Partials/_Reports", serverInfo);
|
return View("Partials/_Reports", serverInfo);
|
||||||
@ -473,6 +478,105 @@ namespace WebfrontCore.Controllers
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IActionResult DismissAlertForm(Guid id)
|
||||||
|
{
|
||||||
|
var info = new ActionInfo
|
||||||
|
{
|
||||||
|
ActionButtonLabel = "Dismiss",
|
||||||
|
Name = "Dismiss Alert?",
|
||||||
|
Inputs = new List<InputInfo>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "alertId",
|
||||||
|
Type = "hidden",
|
||||||
|
Value = id.ToString()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Action = nameof(DismissAlert),
|
||||||
|
ShouldRefresh = true
|
||||||
|
};
|
||||||
|
|
||||||
|
return View("_ActionForm", info);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult DismissAlert(Guid alertId)
|
||||||
|
{
|
||||||
|
AlertManager.MarkAlertAsRead(alertId);
|
||||||
|
return Json(new[]
|
||||||
|
{
|
||||||
|
new CommandResponseInfo
|
||||||
|
{
|
||||||
|
Response = "Alert dismissed"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult DismissAllAlertsForm()
|
||||||
|
{
|
||||||
|
var info = new ActionInfo
|
||||||
|
{
|
||||||
|
ActionButtonLabel = "Dismiss",
|
||||||
|
Name = "Dismiss All Alerts?",
|
||||||
|
Inputs = new List<InputInfo>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "targetId",
|
||||||
|
Type = "hidden",
|
||||||
|
Value = Client.ClientId.ToString()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Action = nameof(DismissAllAlerts),
|
||||||
|
ShouldRefresh = true
|
||||||
|
};
|
||||||
|
|
||||||
|
return View("_ActionForm", info);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult DismissAllAlerts(int targetId)
|
||||||
|
{
|
||||||
|
AlertManager.MarkAllAlertsAsRead(targetId);
|
||||||
|
return Json(new[]
|
||||||
|
{
|
||||||
|
new CommandResponseInfo
|
||||||
|
{
|
||||||
|
Response = "Alerts dismissed"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult OfflineMessageForm()
|
||||||
|
{
|
||||||
|
var info = new ActionInfo
|
||||||
|
{
|
||||||
|
ActionButtonLabel = "Send",
|
||||||
|
Name = "Compose Message",
|
||||||
|
Inputs = new List<InputInfo>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "message",
|
||||||
|
Label = "Message Content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action = "OfflineMessage",
|
||||||
|
ShouldRefresh = true
|
||||||
|
};
|
||||||
|
return View("_ActionForm", info);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OfflineMessage(int targetId, string message)
|
||||||
|
{
|
||||||
|
var server = Manager.GetServers().First();
|
||||||
|
return await Task.FromResult(RedirectToAction("Execute", "Console", new
|
||||||
|
{
|
||||||
|
serverId = server.EndPoint,
|
||||||
|
command =
|
||||||
|
$"{_appConfig.CommandPrefix}{_offlineMessageCommandName} @{targetId} {message.CapClientName(500)}"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
|
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
|
||||||
.Concat(_appConfig.GlobalRules)
|
.Concat(_appConfig.GlobalRules)
|
||||||
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))
|
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
Layout = null;
|
Layout = null;
|
||||||
}
|
}
|
||||||
<h5 class="modal-title mb-10">@Model.Name.Titleize()</h5>
|
<h5 class="modal-title mb-10">@Model.Name.Titleize()</h5>
|
||||||
@if (Model.Inputs.Any())
|
@if (Model.Inputs.Any(input => input.Type != "hidden"))
|
||||||
{
|
{
|
||||||
<hr class="mb-10"/>
|
<hr class="mb-10"/>
|
||||||
}
|
}
|
||||||
@ -55,12 +55,12 @@
|
|||||||
<input type="@inputType" name="@input.Name" value="@value" hidden="hidden">
|
<input type="@inputType" name="@input.Name" value="@value" hidden="hidden">
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (Model.Inputs.Any())
|
@if (Model.Inputs.Any(input => input.Type != "hidden"))
|
||||||
{
|
{
|
||||||
<hr class="mb-10"/>
|
<hr class="mb-10"/>
|
||||||
}
|
}
|
||||||
<div class="ml-auto">
|
<div class="ml-auto">
|
||||||
<button type="submit" class="btn btn-primary">@Model.ActionButtonLabel</button>
|
<button type="submit" class="btn btn-primary">@Model.ActionButtonLabel</button>
|
||||||
<a href="#" class="btn mr-5" role="button" onclick="halfmoon.toggleModal('actionModal');">Close</a>
|
<a href="#" class="btn mr-5 ml-5" role="button" onclick="halfmoon.toggleModal('actionModal');">Close</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -279,6 +279,18 @@
|
|||||||
EntityId = Model.ClientId
|
EntityId = Model.ClientId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ViewBag.Authorized)
|
||||||
|
{
|
||||||
|
menuItems.Items.Add(new SideContextMenuItem
|
||||||
|
{
|
||||||
|
Title = "Message",
|
||||||
|
IsButton = true,
|
||||||
|
Reference = "OfflineMessage",
|
||||||
|
Icon = "oi oi-envelope-closed",
|
||||||
|
EntityId = Model.ClientId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
menuItems.Items.Add(new SideContextMenuItem
|
menuItems.Items.Add(new SideContextMenuItem
|
||||||
{
|
{
|
||||||
@ -336,6 +348,7 @@
|
|||||||
EntityId = Model.ClientId
|
EntityId = Model.ClientId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
<partial name="_SideContextMenu" for="@menuItems"></partial>
|
<partial name="_SideContextMenu" for="@menuItems"></partial>
|
||||||
|
|
||||||
|
65
WebfrontCore/Views/Shared/Partials/_Notifications.cshtml
Normal file
65
WebfrontCore/Views/Shared/Partials/_Notifications.cshtml
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
@using SharedLibraryCore.Alerts
|
||||||
|
@using Humanizer
|
||||||
|
@model IEnumerable<SharedLibraryCore.Alerts.Alert.AlertState>
|
||||||
|
@{
|
||||||
|
Layout = null;
|
||||||
|
}
|
||||||
|
<div class="dropdown with-arrow" data-toggle="dropdown" id="alert-toggle" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<div data-toggle="tooltip" data-title="@(Model.Any() ? "View Alerts" : "No Alerts")" data-placement="bottom">
|
||||||
|
<i class="oi oi-bell mt-5"></i>
|
||||||
|
</div>
|
||||||
|
@if (Model.Any())
|
||||||
|
{
|
||||||
|
<div class="position-absolute bg-danger rounded-circle ml-10" style="width: 0.5em;height: 0.5em;top: 0;"></div>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right w-400" aria-labelledby="alert-toggle">
|
||||||
|
<div class="d-flex">
|
||||||
|
<h6 class="dropdown-header">@ViewBag.Alerts.Count Alerts</h6>
|
||||||
|
<i class="oi oi-circle-x font-size-12 ml-auto mr-10 text-danger align-self-center profile-action" data-action="DismissAllAlerts" data-action-id="@ViewBag.User.ClientId"></i>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
@foreach (var alert in Model)
|
||||||
|
{
|
||||||
|
<div class="d-flex p-5 pl-10 pr-10">
|
||||||
|
<div class="align-self-center">
|
||||||
|
@if (alert.Category == Alert.AlertCategory.Error)
|
||||||
|
{
|
||||||
|
<i class="oi oi-warning text-danger font-size-12 mr-5"></i>
|
||||||
|
}
|
||||||
|
@if (alert.Category == Alert.AlertCategory.Warning)
|
||||||
|
{
|
||||||
|
<i class="oi oi-warning text-secondary font-size-12 mr-5"></i>
|
||||||
|
}
|
||||||
|
@if (alert.Category == Alert.AlertCategory.Information)
|
||||||
|
{
|
||||||
|
<i class="oi oi-circle-check font-size-12 mr-5 text-primary"></i>
|
||||||
|
}
|
||||||
|
@if (alert.Category == Alert.AlertCategory.Message)
|
||||||
|
{
|
||||||
|
<i class="oi oi-envelope-closed font-size-12 mr-5 text-primary"></i>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="font-size-12 p-5">
|
||||||
|
<span>@alert.Message</span>
|
||||||
|
<div class="text-muted d-flex">
|
||||||
|
<span>@alert.OccuredAt.Humanize()</span>
|
||||||
|
@if (!string.IsNullOrEmpty(alert.Source))
|
||||||
|
{
|
||||||
|
<span class="ml-5 mr-5">•</span>
|
||||||
|
@if (alert.SourceId is null)
|
||||||
|
{
|
||||||
|
<div class="text-white font-weight-light">@alert.Source.StripColors()</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a asp-controller="Client" asp-action="Profile" asp-route-id="@alert.SourceId" class="no-decoration">@alert.Source</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i class="oi oi-circle-x font-size-12 ml-auto align-self-center profile-action" data-action="DismissAlert" data-action-id="@alert.AlertId"></i>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
@ -119,11 +119,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex ml-auto">
|
<div class="d-flex ml-auto">
|
||||||
|
<div class="align-self-center">
|
||||||
|
@await Html.PartialAsync("Partials/_Notifications", (object)ViewBag.Alerts)
|
||||||
|
</div>
|
||||||
<div class="btn btn-action mr-10 ml-10" onclick="halfmoon.toggleDarkMode()" data-toggle="tooltip" data-title="Toggle display mode" data-placement="bottom">
|
<div class="btn btn-action mr-10 ml-10" onclick="halfmoon.toggleDarkMode()" data-toggle="tooltip" data-title="Toggle display mode" data-placement="bottom">
|
||||||
<i class="oi oi-moon"></i>
|
<i class="oi oi-moon"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-none d-md-block ">
|
<div class="d-none d-md-block ">
|
||||||
|
|
||||||
<partial name="_SearchResourceForm"/>
|
<partial name="_SearchResourceForm"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex d-lg-none">
|
<div class="d-flex d-lg-none">
|
||||||
|
Loading…
Reference in New Issue
Block a user