improve server clientcount/activity graph on server overview
This commit is contained in:
parent
31123d9a33
commit
180a4911bc
@ -1028,12 +1028,19 @@ namespace IW4MAdmin
|
|||||||
{
|
{
|
||||||
var maxItems = Math.Ceiling(appConfig.MaxClientHistoryTime.TotalMinutes /
|
var maxItems = Math.Ceiling(appConfig.MaxClientHistoryTime.TotalMinutes /
|
||||||
appConfig.ServerDataCollectionInterval.TotalMinutes);
|
appConfig.ServerDataCollectionInterval.TotalMinutes);
|
||||||
while ( ClientHistory.Count > maxItems)
|
|
||||||
|
while (ClientHistory.ClientCounts.Count > maxItems)
|
||||||
{
|
{
|
||||||
ClientHistory.Dequeue();
|
ClientHistory.ClientCounts.RemoveAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientHistory.Enqueue(new PlayerHistory(ClientNum));
|
ClientHistory.ClientCounts.Add(new ClientCountSnapshot
|
||||||
|
{
|
||||||
|
ClientCount = ClientNum,
|
||||||
|
ConnectionInterrupted = Throttled,
|
||||||
|
Time = DateTime.UtcNow,
|
||||||
|
Map = CurrentMap.Name
|
||||||
|
});
|
||||||
playerCountStart = DateTime.Now;
|
playerCountStart = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +94,8 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
PeriodBlock = (int) (DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch).TotalMinutes,
|
PeriodBlock = (int) (DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch).TotalMinutes,
|
||||||
ServerId = await server.GetIdForServer(),
|
ServerId = await server.GetIdForServer(),
|
||||||
MapId = await GetOrCreateMap(server.CurrentMap.Name, (Reference.Game) server.GameName, token),
|
MapId = await GetOrCreateMap(server.CurrentMap.Name, (Reference.Game) server.GameName, token),
|
||||||
ClientCount = server.ClientNum
|
ClientCount = server.ClientNum,
|
||||||
|
ConnectionInterrupted = server.Throttled,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
@ -135,7 +135,9 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
{
|
{
|
||||||
snapshot.ServerId,
|
snapshot.ServerId,
|
||||||
snapshot.CapturedAt,
|
snapshot.CapturedAt,
|
||||||
snapshot.ClientCount
|
snapshot.ClientCount,
|
||||||
|
snapshot.ConnectionInterrupted,
|
||||||
|
MapName = snapshot.Map.Name,
|
||||||
})
|
})
|
||||||
.OrderBy(snapshot => snapshot.CapturedAt)
|
.OrderBy(snapshot => snapshot.CapturedAt)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
@ -143,8 +145,8 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
return history.GroupBy(snapshot => snapshot.ServerId).Select(byServer => new ClientHistoryInfo
|
return history.GroupBy(snapshot => snapshot.ServerId).Select(byServer => new ClientHistoryInfo
|
||||||
{
|
{
|
||||||
ServerId = byServer.Key,
|
ServerId = byServer.Key,
|
||||||
ClientCounts = byServer.Select(snapshot => new ClientCountSnapshot()
|
ClientCounts = byServer.Select(snapshot => new ClientCountSnapshot
|
||||||
{Time = snapshot.CapturedAt, ClientCount = snapshot.ClientCount}).ToList()
|
{ Time = snapshot.CapturedAt, ClientCount = snapshot.ClientCount, ConnectionInterrupted = snapshot.ConnectionInterrupted ?? false, Map = snapshot.MapName}).ToList()
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}, nameof(_clientHistoryCache), TimeSpan.MaxValue);
|
}, nameof(_clientHistoryCache), TimeSpan.MaxValue);
|
||||||
|
|
||||||
|
1626
Data/Migrations/MySql/20220329213440_AddConnectionInterruptedToEFServerSnapshot.Designer.cs
generated
Normal file
1626
Data/Migrations/MySql/20220329213440_AddConnectionInterruptedToEFServerSnapshot.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.MySql
|
||||||
|
{
|
||||||
|
public partial class AddConnectionInterruptedToEFServerSnapshot : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "ConnectionInterrupted",
|
||||||
|
table: "EFServerSnapshot",
|
||||||
|
type: "tinyint(1)",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ConnectionInterrupted",
|
||||||
|
table: "EFServerSnapshot");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1081,6 +1081,9 @@ namespace Data.Migrations.MySql
|
|||||||
b.Property<int>("ClientCount")
|
b.Property<int>("ClientCount")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool?>("ConnectionInterrupted")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
b.Property<int>("MapId")
|
b.Property<int>("MapId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
1683
Data/Migrations/Postgresql/20220329213521_AddConnectionInterruptedToEFServerSnapshot.Designer.cs
generated
Normal file
1683
Data/Migrations/Postgresql/20220329213521_AddConnectionInterruptedToEFServerSnapshot.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Postgresql
|
||||||
|
{
|
||||||
|
public partial class AddConnectionInterruptedToEFServerSnapshot : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "ConnectionInterrupted",
|
||||||
|
table: "EFServerSnapshot",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ConnectionInterrupted",
|
||||||
|
table: "EFServerSnapshot");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1134,6 +1134,9 @@ namespace Data.Migrations.Postgresql
|
|||||||
b.Property<int>("ClientCount")
|
b.Property<int>("ClientCount")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool?>("ConnectionInterrupted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<int>("MapId")
|
b.Property<int>("MapId")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
1624
Data/Migrations/Sqlite/20220329163928_AddConnectionInterruptedToEFServerSnapshot.Designer.cs
generated
Normal file
1624
Data/Migrations/Sqlite/20220329163928_AddConnectionInterruptedToEFServerSnapshot.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Sqlite
|
||||||
|
{
|
||||||
|
public partial class AddConnectionInterruptedToEFServerSnapshot : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "ConnectionInterrupted",
|
||||||
|
table: "EFServerSnapshot",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ConnectionInterrupted",
|
||||||
|
table: "EFServerSnapshot");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1079,6 +1079,9 @@ namespace Data.Migrations.Sqlite
|
|||||||
b.Property<int>("ClientCount")
|
b.Property<int>("ClientCount")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool?>("ConnectionInterrupted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("MapId")
|
b.Property<int>("MapId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
@ -32,5 +32,7 @@ namespace Data.Models.Server
|
|||||||
public EFMap Map { get; set; }
|
public EFMap Map { get; set; }
|
||||||
|
|
||||||
public int ClientCount { get; set; }
|
public int ClientCount { get; set; }
|
||||||
|
|
||||||
|
public bool? ConnectionInterrupted {get;set;}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,13 +6,19 @@ namespace SharedLibraryCore.Dtos
|
|||||||
public class ClientHistoryInfo
|
public class ClientHistoryInfo
|
||||||
{
|
{
|
||||||
public long ServerId { get; set; }
|
public long ServerId { get; set; }
|
||||||
public List<ClientCountSnapshot> ClientCounts { get; set; }
|
public List<ClientCountSnapshot> ClientCounts { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ClientCountSnapshot
|
public class ClientCountSnapshot
|
||||||
{
|
{
|
||||||
|
private const int UpdateInterval = 5;
|
||||||
public DateTime Time { get; set; }
|
public DateTime Time { get; set; }
|
||||||
public string TimeString => Time.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
public string TimeString => new DateTime(Time.Year, Time.Month, Time.Day, Time.Hour,
|
||||||
|
Math.Min(59, UpdateInterval * (int)Math.Round(Time.Minute / (float)UpdateInterval)), 0)
|
||||||
|
.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||||
public int ClientCount { get; set; }
|
public int ClientCount { get; set; }
|
||||||
|
public bool ConnectionInterrupted { get;set; }
|
||||||
|
public string Map { get; set; }
|
||||||
|
public string MapAlias { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,8 +15,7 @@ namespace SharedLibraryCore.Dtos
|
|||||||
public int MaxClients { get; set; }
|
public int MaxClients { get; set; }
|
||||||
public List<ChatInfo> ChatHistory { get; set; }
|
public List<ChatInfo> ChatHistory { get; set; }
|
||||||
public List<PlayerInfo> Players { get; set; }
|
public List<PlayerInfo> Players { get; set; }
|
||||||
public PlayerHistory[] PlayerHistory { get; set; }
|
public ClientHistoryInfo ClientHistory { get; set; }
|
||||||
public List<ClientCountSnapshot> ClientCountHistory { get; set; }
|
|
||||||
public long ID { get; set; }
|
public long ID { get; set; }
|
||||||
public bool Online { get; set; }
|
public bool Online { get; set; }
|
||||||
public string ConnectProtocolUrl { get; set; }
|
public string ConnectProtocolUrl { get; set; }
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
using System;
|
|
||||||
using SharedLibraryCore.Dtos;
|
|
||||||
|
|
||||||
namespace SharedLibraryCore.Helpers
|
|
||||||
{
|
|
||||||
public class PlayerHistory
|
|
||||||
{
|
|
||||||
// how many minutes between updates
|
|
||||||
public static readonly int UpdateInterval = 5;
|
|
||||||
|
|
||||||
private readonly DateTime When;
|
|
||||||
|
|
||||||
public PlayerHistory(int cNum)
|
|
||||||
{
|
|
||||||
var t = DateTime.UtcNow;
|
|
||||||
When = new DateTime(t.Year, t.Month, t.Day, t.Hour,
|
|
||||||
Math.Min(59, UpdateInterval * (int)Math.Round(t.Minute / (float)UpdateInterval)), 0);
|
|
||||||
y = cNum;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used by CanvasJS as a point on the x axis
|
|
||||||
/// </summary>
|
|
||||||
public string x => When.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used by CanvasJS as a point on the y axis
|
|
||||||
/// </summary>
|
|
||||||
public int y { get; }
|
|
||||||
|
|
||||||
public ClientCountSnapshot ToClientCountSnapshot()
|
|
||||||
{
|
|
||||||
return new ClientCountSnapshot
|
|
||||||
{
|
|
||||||
ClientCount = y,
|
|
||||||
Time = When
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -71,7 +71,7 @@ namespace SharedLibraryCore
|
|||||||
EventProcessing = new SemaphoreSlim(1, 1);
|
EventProcessing = new SemaphoreSlim(1, 1);
|
||||||
Clients = new List<EFClient>(new EFClient[64]);
|
Clients = new List<EFClient>(new EFClient[64]);
|
||||||
Reports = new List<Report>();
|
Reports = new List<Report>();
|
||||||
ClientHistory = new Queue<PlayerHistory>();
|
ClientHistory = new ClientHistoryInfo();
|
||||||
ChatHistory = new List<ChatInfo>();
|
ChatHistory = new List<ChatInfo>();
|
||||||
NextMessage = 0;
|
NextMessage = 0;
|
||||||
CustomSayEnabled = Manager.GetApplicationSettings().Configuration().EnableCustomSayName;
|
CustomSayEnabled = Manager.GetApplicationSettings().Configuration().EnableCustomSayName;
|
||||||
@ -97,7 +97,7 @@ namespace SharedLibraryCore
|
|||||||
public List<Map> Maps { get; protected set; } = new List<Map>();
|
public List<Map> Maps { get; protected set; } = new List<Map>();
|
||||||
public List<Report> Reports { get; set; }
|
public List<Report> Reports { get; set; }
|
||||||
public List<ChatInfo> ChatHistory { get; protected set; }
|
public List<ChatInfo> ChatHistory { get; protected set; }
|
||||||
public Queue<PlayerHistory> ClientHistory { get; }
|
public ClientHistoryInfo ClientHistory { get; }
|
||||||
public Game GameName { get; set; }
|
public Game GameName { get; set; }
|
||||||
|
|
||||||
public string Hostname
|
public string Hostname
|
||||||
|
@ -48,7 +48,7 @@ namespace WebfrontCore.Controllers
|
|||||||
.CLIENT_STATS_KEY)?.ZScore
|
.CLIENT_STATS_KEY)?.ZScore
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
ChatHistory = s.ChatHistory.ToList(),
|
ChatHistory = s.ChatHistory.ToList(),
|
||||||
PlayerHistory = s.ClientHistory.ToArray(),
|
ClientHistory = s.ClientHistory,
|
||||||
IsPasswordProtected = !string.IsNullOrEmpty(s.GamePassword)
|
IsPasswordProtected = !string.IsNullOrEmpty(s.GamePassword)
|
||||||
};
|
};
|
||||||
return PartialView("_ClientActivity", serverInfo);
|
return PartialView("_ClientActivity", serverInfo);
|
||||||
|
@ -19,16 +19,27 @@ namespace WebfrontCore.ViewComponents
|
|||||||
{
|
{
|
||||||
private readonly IServerDataViewer _serverDataViewer;
|
private readonly IServerDataViewer _serverDataViewer;
|
||||||
private readonly ApplicationConfiguration _appConfig;
|
private readonly ApplicationConfiguration _appConfig;
|
||||||
|
private readonly DefaultSettings _defaultSettings;
|
||||||
|
|
||||||
public ServerListViewComponent(IServerDataViewer serverDataViewer,
|
public ServerListViewComponent(IServerDataViewer serverDataViewer,
|
||||||
ApplicationConfiguration applicationConfiguration)
|
ApplicationConfiguration applicationConfiguration, DefaultSettings defaultSettings)
|
||||||
{
|
{
|
||||||
_serverDataViewer = serverDataViewer;
|
_serverDataViewer = serverDataViewer;
|
||||||
_appConfig = applicationConfiguration;
|
_appConfig = applicationConfiguration;
|
||||||
|
_defaultSettings = defaultSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IViewComponentResult Invoke(Game? game)
|
public IViewComponentResult Invoke(Game? game)
|
||||||
{
|
{
|
||||||
|
if (game.HasValue)
|
||||||
|
{
|
||||||
|
ViewBag.Maps = _defaultSettings.Maps.FirstOrDefault(map => map.Game == game);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ViewBag.Maps = _defaultSettings.Maps.SelectMany(maps => maps.Maps).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
var servers = Program.Manager.GetServers().Where(server => !game.HasValue || server.GameName == game);
|
var servers = Program.Manager.GetServers().Where(server => !game.HasValue || server.GameName == game);
|
||||||
|
|
||||||
var serverInfo = new List<ServerInfo>();
|
var serverInfo = new List<ServerInfo>();
|
||||||
@ -47,14 +58,14 @@ namespace WebfrontCore.ViewComponents
|
|||||||
|
|
||||||
var counts = clientHistory.ClientCounts?.AsEnumerable() ?? Enumerable.Empty<ClientCountSnapshot>();
|
var counts = clientHistory.ClientCounts?.AsEnumerable() ?? Enumerable.Empty<ClientCountSnapshot>();
|
||||||
|
|
||||||
if (server.ClientHistory.Count > 0)
|
if (server.ClientHistory.ClientCounts.Any())
|
||||||
{
|
{
|
||||||
counts = counts.Union(server.ClientHistory
|
counts = counts.Union(server.ClientHistory.ClientCounts.Where(history =>
|
||||||
.Select(history => history.ToClientCountSnapshot()).Where(history =>
|
history.Time > (clientHistory.ClientCounts?.LastOrDefault()?.Time ?? DateTime.MinValue)))
|
||||||
history.Time > (clientHistory.ClientCounts?.LastOrDefault()?.Time ?? DateTime.MinValue)));
|
.Where(history => history.Time >= DateTime.UtcNow - _appConfig.MaxClientHistoryTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
serverInfo.Add(new ServerInfo()
|
serverInfo.Add(new ServerInfo
|
||||||
{
|
{
|
||||||
Name = server.Hostname,
|
Name = server.Hostname,
|
||||||
ID = server.EndPoint,
|
ID = server.EndPoint,
|
||||||
@ -63,7 +74,11 @@ namespace WebfrontCore.ViewComponents
|
|||||||
ClientCount = server.Clients.Count(client => client != null),
|
ClientCount = server.Clients.Count(client => client != null),
|
||||||
MaxClients = server.MaxClients,
|
MaxClients = server.MaxClients,
|
||||||
GameType = server.GametypeName,
|
GameType = server.GametypeName,
|
||||||
PlayerHistory = server.ClientHistory.ToArray(),
|
ClientHistory = new ClientHistoryInfo
|
||||||
|
{
|
||||||
|
ServerId = server.EndPoint,
|
||||||
|
ClientCounts = counts.ToList()
|
||||||
|
},
|
||||||
Players = server.GetClientsAsList()
|
Players = server.GetClientsAsList()
|
||||||
.Select(p => new PlayerInfo()
|
.Select(p => new PlayerInfo()
|
||||||
{
|
{
|
||||||
@ -77,9 +92,6 @@ namespace WebfrontCore.ViewComponents
|
|||||||
.CLIENT_STATS_KEY)?.ZScore
|
.CLIENT_STATS_KEY)?.ZScore
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
ChatHistory = server.ChatHistory.ToList(),
|
ChatHistory = server.ChatHistory.ToList(),
|
||||||
ClientCountHistory =
|
|
||||||
counts.Where(history => history.Time >= DateTime.UtcNow - _appConfig.MaxClientHistoryTime)
|
|
||||||
.ToList(),
|
|
||||||
Online = !server.Throttled,
|
Online = !server.Throttled,
|
||||||
IPAddress =
|
IPAddress =
|
||||||
$"{(server.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : server.IP)}:{server.Port}",
|
$"{(server.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : server.IP)}:{server.Port}",
|
||||||
|
@ -1,6 +1,21 @@
|
|||||||
@model SharedLibraryCore.Dtos.ServerInfo
|
@model SharedLibraryCore.Dtos.ServerInfo
|
||||||
@{
|
@{
|
||||||
Layout = null;
|
Layout = null;
|
||||||
|
|
||||||
|
string GetMapName(string mapCode)
|
||||||
|
{
|
||||||
|
if (ViewBag.Maps?.Count == 0)
|
||||||
|
{
|
||||||
|
return mapCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ViewBag.Maps as List<Map>)?.FirstOrDefault(map => map.Name == mapCode)?.Alias ?? mapCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var snapshot in Model.ClientHistory.ClientCounts)
|
||||||
|
{
|
||||||
|
snapshot.MapAlias = GetMapName(snapshot.Map);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="row server-header pt-1 pb-1 bg-primary " id="server_header_@Model.ID">
|
<div class="row server-header pt-1 pb-1 bg-primary " id="server_header_@Model.ID">
|
||||||
@ -58,8 +73,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row server-history mb-4">
|
<div class="row server-history mb-4">
|
||||||
<div class="server-history-row" id="server_history_@Model.ID" data-serverid="@Model.ID"
|
<div class="server-history-row" style="position:relative; width: 80vw" id="server_history_@Model.ID" data-serverid="@Model.ID"
|
||||||
data-clienthistory='@Html.Raw(Json.Serialize(Model.PlayerHistory))'
|
data-clienthistory='@Html.Raw(Json.Serialize(Model.ClientHistory))'
|
||||||
data-clienthistory-ex='@Html.Raw(Json.Serialize(Model.ClientCountHistory))'
|
data-clienthistory-ex='@Html.Raw(Json.Serialize(Model.ClientHistory.ClientCounts))'
|
||||||
data-online="@Model.Online"></div>
|
data-online="@Model.Online">
|
||||||
|
<canvas id="server_history_canvas_@Model.ID" height="100"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,64 +1,163 @@
|
|||||||
function getPlayerHistoryChart(playerHistory, i, width, color, maxClients) {
|
function createDiagonalPattern(color = 'black') {
|
||||||
///////////////////////////////////////
|
let shape = document.createElement('canvas');
|
||||||
// thanks to canvasjs :(
|
shape.width = 10;
|
||||||
playerHistory.forEach(function (item, i) {
|
shape.height = 10;
|
||||||
playerHistory[i].x = new Date(playerHistory[i].timeString);
|
let c = shape.getContext('2d');
|
||||||
playerHistory[i].y = playerHistory[i].clientCount;
|
c.strokeStyle = color;
|
||||||
|
c.beginPath();
|
||||||
|
c.moveTo(2, 0);
|
||||||
|
c.lineTo(10, 8);
|
||||||
|
c.stroke();
|
||||||
|
c.beginPath();
|
||||||
|
c.moveTo(0, 8);
|
||||||
|
c.lineTo(2, 10);
|
||||||
|
c.stroke();
|
||||||
|
return c.createPattern(shape, 'repeat');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlayerHistoryChart(playerHistory, i, width, maxClients) {
|
||||||
|
const primaryColor = $('title').css('background-color');
|
||||||
|
const rgb = primaryColor.match(/\d+/g);
|
||||||
|
const fillColor = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.66)`;
|
||||||
|
const offlineFillColor = 'rgba(255, 96, 96, 0.55)';
|
||||||
|
|
||||||
|
const onlineTime = [];
|
||||||
|
const offlineTime = [];
|
||||||
|
const mapChange = [];
|
||||||
|
let lastMap = '';
|
||||||
|
|
||||||
|
playerHistory.forEach((elem, i) => {
|
||||||
|
if (elem.map !== lastMap) {
|
||||||
|
mapChange.push(i);
|
||||||
|
lastMap = elem.map;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elem.connectionInterrupted) {
|
||||||
|
offlineTime.push({
|
||||||
|
clientCount: maxClients,
|
||||||
|
timeString: elem.timeString
|
||||||
});
|
});
|
||||||
|
|
||||||
return new CanvasJS.Chart(`server_history_${i}`, {
|
onlineTime.push({
|
||||||
backgroundColor: '#191919',
|
clientCount: 0,
|
||||||
height: 100,
|
timeString: elem.timeString
|
||||||
width: width,
|
})
|
||||||
animationEnabled: true,
|
} else {
|
||||||
toolTip: {
|
offlineTime.push({
|
||||||
contentFormatter: function (e) {
|
clientCount: 0,
|
||||||
const date = moment.utc(e.entries[0].dataPoint.x);
|
timeString: elem.timeString
|
||||||
return date.local().calendar() + " - " + e.entries[0].dataPoint.y + " players";
|
});
|
||||||
|
|
||||||
|
onlineTime.push(elem)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let animationProgress = 0;
|
||||||
|
let initialAnimationComplete = false;
|
||||||
|
const originalLineDraw = Chart.controllers.line.prototype.draw;
|
||||||
|
Chart.helpers.extend(Chart.controllers.line.prototype, {
|
||||||
|
draw: function () {
|
||||||
|
originalLineDraw.apply(this, arguments);
|
||||||
|
|
||||||
|
const chart = this.chart;
|
||||||
|
const ctx = chart.chart.ctx;
|
||||||
|
|
||||||
|
chart.config.data.lineAtIndexes.forEach((elem, index) => {
|
||||||
|
const xScale = chart.scales['x-axis-0'];
|
||||||
|
const yScale = chart.scales['y-axis-0'];
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(xScale.getPixelForValue(undefined, elem), yScale.getPixelForValue(playerHistory[elem].clientCount) / (initialAnimationComplete ? 1 : animationProgress));
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
||||||
|
ctx.lineTo(xScale.getPixelForValue(undefined, elem), yScale.bottom);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const canvas = document.getElementById(`server_history_canvas_${i}`);
|
||||||
|
canvas.setAttribute('width', width);
|
||||||
|
|
||||||
|
return new Chart(document.getElementById(`server_history_canvas_${i}`), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: playerHistory.map(history => history.timeString),
|
||||||
|
datasets: [{
|
||||||
|
data: onlineTime.map(history => history.clientCount),
|
||||||
|
backgroundColor: fillColor,
|
||||||
|
borderColor: primaryColor,
|
||||||
|
borderWidth: 2,
|
||||||
|
hoverBorderColor: 'white',
|
||||||
|
hoverBorderWidth: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: offlineTime.map(history => history.clientCount),
|
||||||
|
backgroundColor: createDiagonalPattern(offlineFillColor),
|
||||||
|
borderColor: offlineFillColor,
|
||||||
|
borderWidth: 2,
|
||||||
|
hoverBorderColor: 'white',
|
||||||
|
hoverBorderWidth: 2
|
||||||
|
}],
|
||||||
|
lineAtIndexes: mapChange,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
onResize: function(chart, size) {
|
||||||
|
console.log(size);
|
||||||
|
},
|
||||||
|
legend: false,
|
||||||
|
defaultFontFamily: '-apple-system, BlinkMacSystemFont, "Open Sans", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
// todo: localization at some point
|
||||||
|
label: context => context.datasetIndex !== 1 ? `${context.value} players on ${playerHistory[context.index].mapAlias}` : context.value === '0' ? '' : 'Server Unreachable!',
|
||||||
|
title: context => context[0].datasetIndex !== 1 ? moment(context[0].label).local().calendar() : ''
|
||||||
|
},
|
||||||
|
mode: 'nearest',
|
||||||
|
intersect: false,
|
||||||
|
animationDuration: 0,
|
||||||
|
cornerRadius: 0,
|
||||||
|
displayColors: false
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
|
display: false,
|
||||||
|
}],
|
||||||
|
yAxes: [{
|
||||||
|
display: false,
|
||||||
|
gridLines: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
max: 1,
|
||||||
|
min: maxClients + 2
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'nearest',
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
radius: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
axisX: {
|
animation: {
|
||||||
interval: 1,
|
duration: 1000,
|
||||||
gridThickness: 0,
|
onProgress: function (context) {
|
||||||
lineThickness: 0,
|
animationProgress = context.currentStep / context.numSteps;
|
||||||
tickThickness: 0,
|
if (animationProgress >= 1) {
|
||||||
margin: 0,
|
initialAnimationComplete = true;
|
||||||
valueFormatString: " "
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
axisY: {
|
|
||||||
gridThickness: 0,
|
|
||||||
lineThickness: 0,
|
|
||||||
tickThickness: 0,
|
|
||||||
minimum: 0,
|
|
||||||
maximum: maxClients + 1,
|
|
||||||
margin: 0,
|
|
||||||
valueFormatString: " ",
|
|
||||||
labelMaxWidth: 0
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
maxWidth: 0,
|
|
||||||
maxHeight: 0,
|
|
||||||
dockInsidePlotArea: true
|
|
||||||
},
|
|
||||||
data: [{
|
|
||||||
showInLegend: false,
|
|
||||||
type: "splineArea",
|
|
||||||
color: color,
|
|
||||||
markerSize: 0,
|
|
||||||
dataPoints: playerHistory
|
|
||||||
}]
|
|
||||||
});
|
});
|
||||||
//////////////////////////////////////
|
|
||||||
}
|
}
|
||||||
var charts = {};
|
|
||||||
|
|
||||||
$(window).resize(function () {
|
|
||||||
$('.server-history-row').each(function (index) {
|
|
||||||
let serverId = $(this).data('serverid');
|
|
||||||
charts[serverId].options.width = $('.server-header').first().width();
|
|
||||||
charts[serverId].render();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function refreshClientActivity() {
|
function refreshClientActivity() {
|
||||||
$('.server-history-row').each(function (index) {
|
$('.server-history-row').each(function (index) {
|
||||||
@ -88,12 +187,8 @@ $(document).ready(function () {
|
|||||||
let clientHistory = $(this).data('clienthistory-ex');
|
let clientHistory = $(this).data('clienthistory-ex');
|
||||||
let serverId = $(this).data('serverid');
|
let serverId = $(this).data('serverid');
|
||||||
let maxClients = parseInt($('#server_header_' + serverId + ' .server-maxclients').text());
|
let maxClients = parseInt($('#server_header_' + serverId + ' .server-maxclients').text());
|
||||||
let primaryColor = $('title').css('background-color');
|
|
||||||
let color = $(this).data('online') === 'True' ? primaryColor : '#ff6060';
|
|
||||||
let width = $('.server-header').first().width();
|
let width = $('.server-header').first().width();
|
||||||
let historyChart = getPlayerHistoryChart(clientHistory, serverId, width, color, maxClients);
|
getPlayerHistoryChart(clientHistory, serverId, width, maxClients);
|
||||||
historyChart.render();
|
|
||||||
charts[serverId] = historyChart;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.moment-date').each((index, element) => {
|
$('.moment-date').each((index, element) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user