Skip to content

Commit

Permalink
add alert/notification functionality (for server connection events an…
Browse files Browse the repository at this point in the history
…d messages)
RaidMax committed Jun 11, 2022
1 parent 3475da8 commit 5966541
Showing 16 changed files with 579 additions and 21 deletions.
55 changes: 55 additions & 0 deletions Application/Alerts/AlertExtensions.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 137 additions & 0 deletions Application/Alerts/AlertManager.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
6 changes: 5 additions & 1 deletion Application/ApplicationManager.cs
Original file line number Diff line number Diff line change
@@ -57,6 +57,7 @@ public class ApplicationManager : IManager
private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc;
readonly PenaltyService PenaltySvc;
private readonly IAlertManager _alertManager;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
@@ -82,13 +83,14 @@ public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionH
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService)
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>();
ClientSvc = clientService;
PenaltySvc = penaltyService;
_alertManager = alertManager;
ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow;
PageList = new PageList();
@@ -508,6 +510,7 @@ public async Task Init()
#endregion

_metaRegistration.Register();
await _alertManager.Initialize();

#region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
@@ -697,5 +700,6 @@ public void AddAdditionalCommand(IManagerCommand command)
}

public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
public IAlertManager AlertManager => _alertManager;
}
}
75 changes: 66 additions & 9 deletions Application/Commands/OfflineMessageCommand.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Misc;
using IW4MAdmin.Application.Alerts;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -16,19 +19,66 @@ public class OfflineMessageCommand : Command
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
private readonly IAlertManager _alertManager;
private const short MaxLength = 1024;

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";
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
Alias = "om";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;

_contextFactory = contextFactory;
_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)
@@ -38,30 +88,37 @@ public override async Task ExecuteAsync(GameEvent gameEvent)
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
return;
}

if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
return;
}

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;
}

await using var context = _contextFactory.CreateContext(enableTracking: false);
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());

var newMessage = new EFInboxMessage()
var newMessage = new EFInboxMessage
{
SourceClientId = gameEvent.Origin.ClientId,
DestinationClientId = gameEvent.Target.ClientId,
ServerId = server.Id,
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
{
context.Set<EFInboxMessage>().Add(newMessage);
@@ -75,4 +132,4 @@ public override async Task ExecuteAsync(GameEvent gameEvent)
}
}
}
}
}
22 changes: 20 additions & 2 deletions Application/IW4MServer.cs
Original file line number Diff line number Diff line change
@@ -24,8 +24,10 @@
using static SharedLibraryCore.Database.Models.EFClient;
using Data.Models;
using Data.Models.Server;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Commands;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Alerts;
using static Data.Models.Client.EFClient;

namespace IW4MAdmin
@@ -306,8 +308,16 @@ protected override async Task<bool> ProcessEvent(GameEvent E)
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{
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;
}

@@ -318,7 +328,15 @@ protected override async Task<bool> ProcessEvent(GameEvent E)

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))
2 changes: 2 additions & 0 deletions Application/Main.cs
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@
using Data.Abstractions;
using Data.Helpers;
using Integrations.Source.Extensions;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Localization;
using Microsoft.Extensions.Logging;
@@ -448,6 +449,7 @@ private static async Task<IServiceCollection> ConfigureServices(string[] args)
.AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton<IEventPublisher, EventPublisher>()
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>()
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);
33 changes: 33 additions & 0 deletions SharedLibraryCore/Alerts/Alert.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
4 changes: 4 additions & 0 deletions SharedLibraryCore/BaseController.cs
Original file line number Diff line number Diff line change
@@ -20,6 +20,8 @@ namespace SharedLibraryCore
{
public class BaseController : Controller
{
protected readonly IAlertManager AlertManager;

/// <summary>
/// life span in months
/// </summary>
@@ -34,6 +36,7 @@ public class BaseController : Controller

public BaseController(IManager manager)
{
AlertManager = manager.AlertManager;
Manager = manager;
Localization ??= Utilities.CurrentLocalization.LocalizationIndex;
AppConfig = Manager.GetApplicationSettings().Configuration();
@@ -169,6 +172,7 @@ public override void OnActionExecuting(ActionExecutingContext context)
ViewBag.ReportCount = Manager.GetServers().Sum(server =>
server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24)));
ViewBag.PermissionsSet = PermissionsSet;
ViewBag.Alerts = AlertManager.RetrieveAlerts(Client).ToList();

base.OnActionExecuting(context);
}
8 changes: 8 additions & 0 deletions SharedLibraryCore/Configuration/ApplicationConfiguration.cs
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Data.Models.Misc;
using Newtonsoft.Json;
using SharedLibraryCore.Configuration.Attributes;
using SharedLibraryCore.Interfaces;
@@ -154,6 +155,13 @@ public class ApplicationConfiguration : IBaseConfiguration
{ 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]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")]
public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new()
54 changes: 54 additions & 0 deletions SharedLibraryCore/Interfaces/IAlertManager.cs
Original file line number Diff line number Diff line change
@@ -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; }

}
2 changes: 2 additions & 0 deletions SharedLibraryCore/Interfaces/IManager.cs
Original file line number Diff line number Diff line change
@@ -102,5 +102,7 @@ public interface IManager
/// event executed when event has finished executing
/// </summary>
event EventHandler<GameEvent> OnGameEventExecuted;

IAlertManager AlertManager { get; }
}
}
114 changes: 109 additions & 5 deletions WebfrontCore/Controllers/ActionController.cs
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ public class ActionController : BaseController
private readonly string _unbanCommandName;
private readonly string _sayCommandName;
private readonly string _kickCommandName;
private readonly string _offlineMessageCommandName;
private readonly string _flagCommandName;
private readonly string _unflagCommandName;
private readonly string _setLevelCommandName;
@@ -64,6 +65,9 @@ public ActionController(IManager manager, IEnumerable<IManagerCommand> registere
case nameof(SetLevelCommand):
_setLevelCommandName = cmd.Name;
break;
case "OfflineMessageCommand":
_offlineMessageCommandName = cmd.Name;
break;
}
}
}
@@ -213,7 +217,7 @@ public IActionResult LoginForm()

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()
@@ -327,26 +331,27 @@ public async Task<IActionResult> ChatAsync(long id, string message)
public async Task<IActionResult> RecentClientsForm(PaginationRequest request)
{
ViewBag.First = request.Offset == 0;

if (request.Count > 20)
{
request.Count = 20;
}

var clients = await Manager.GetClientService().GetRecentClients(request);

return request.Offset == 0
? View("~/Views/Shared/Components/Client/_RecentClientsContainer.cshtml", clients)
: View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients);
}

public IActionResult RecentReportsForm()
{
var serverInfo = Manager.GetServers().Select(server =>
new ServerInfo
{
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);
@@ -473,6 +478,105 @@ public async Task<IActionResult> KickAsync(int targetId, string reason, string p
}));
}

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
.Concat(_appConfig.GlobalRules)
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))
6 changes: 3 additions & 3 deletions WebfrontCore/Views/Action/_ActionForm.cshtml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
Layout = null;
}
<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"/>
}
@@ -55,12 +55,12 @@
<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"/>
}
<div class="ml-auto">
<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>
</form>
13 changes: 13 additions & 0 deletions WebfrontCore/Views/Client/Profile/Index.cshtml
Original file line number Diff line number Diff line change
@@ -279,6 +279,18 @@
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
{
@@ -336,6 +348,7 @@
EntityId = Model.ClientId
});
}

}
<partial name="_SideContextMenu" for="@menuItems"></partial>

65 changes: 65 additions & 0 deletions WebfrontCore/Views/Shared/Partials/_Notifications.cshtml
Original file line number Diff line number Diff line change
@@ -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">&#8226;</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>
4 changes: 3 additions & 1 deletion WebfrontCore/Views/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
@@ -119,11 +119,13 @@
</div>

<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">
<i class="oi oi-moon"></i>
</div>
<div class="d-none d-md-block ">

<partial name="_SearchResourceForm"/>
</div>
<div class="d-flex d-lg-none">

0 comments on commit 5966541

Please sign in to comment.