huge commit for advanced stats feature.
broke data out into its own library. may be breaking changes with existing plugins
69
WebfrontCore/Controllers/API/StatsController.cs
Normal file
@ -0,0 +1,69 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using Stats.Dtos;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace StatsWeb.API
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/stats")]
|
||||
public class StatsController : ControllerBase
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IResourceQueryHelper<StatsInfoRequest, StatsInfoResult> _statsQueryHelper;
|
||||
|
||||
public StatsController(ILogger<StatsController> logger, IResourceQueryHelper<StatsInfoRequest, StatsInfoResult> statsQueryHelper)
|
||||
{
|
||||
_statsQueryHelper = statsQueryHelper;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[HttpGet("{clientId}")]
|
||||
public async Task<IActionResult> ClientStats(int clientId)
|
||||
{
|
||||
if (clientId < 1 || !ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(new ErrorResponse
|
||||
{
|
||||
Messages = new[] { $"Client Id must be between 1 and {int.MaxValue}" }
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
var request = new StatsInfoRequest()
|
||||
{
|
||||
ClientId = clientId
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _statsQueryHelper.QueryResource(request);
|
||||
|
||||
if (result.RetrievedResultCount == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(result.Results);
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not get client stats for client id {clientId}", clientId);
|
||||
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new ErrorResponse
|
||||
{
|
||||
Messages = new[] { e.Message }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
using FluentValidation;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using Data.Models;
|
||||
using FluentValidation;
|
||||
using SharedLibraryCore.Dtos;
|
||||
|
||||
namespace WebfrontCore.Controllers.API.Validation
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models.Client;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore;
|
||||
@ -10,7 +11,6 @@ using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using WebfrontCore.ViewModels;
|
||||
using static SharedLibraryCore.Database.Models.EFClient;
|
||||
|
||||
namespace WebfrontCore.Controllers
|
||||
{
|
||||
@ -215,10 +215,10 @@ namespace WebfrontCore.Controllers
|
||||
Name = "level",
|
||||
Label = Localization["WEBFRONT_PROFILE_LEVEL"],
|
||||
Type = "select",
|
||||
Values = Enum.GetValues(typeof(Permission)).OfType<Permission>()
|
||||
Values = Enum.GetValues(typeof(EFClient.Permission)).OfType<EFClient.Permission>()
|
||||
.Where(p => p <= Client.Level)
|
||||
.Where(p => p != Permission.Banned)
|
||||
.Where(p => p != Permission.Flagged)
|
||||
.Where(p => p != EFClient.Permission.Banned)
|
||||
.Where(p => p != EFClient.Permission.Flagged)
|
||||
.ToDictionary(p => p.ToString(), p => p.ToLocalizedLevelName())
|
||||
},
|
||||
},
|
||||
|
@ -5,24 +5,26 @@ using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using SharedLibraryCore.Services;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using IW4MAdmin.Plugins.Stats.Config;
|
||||
using WebfrontCore.ViewComponents;
|
||||
using static SharedLibraryCore.Database.Models.EFClient;
|
||||
using static SharedLibraryCore.Database.Models.EFPenalty;
|
||||
|
||||
namespace WebfrontCore.Controllers
|
||||
{
|
||||
public class ClientController : BaseController
|
||||
{
|
||||
private readonly IMetaService _metaService;
|
||||
private readonly IConfigurationHandler<StatsConfiguration> _configurationHandler;
|
||||
|
||||
public ClientController(IManager manager, IMetaService metaService) : base(manager)
|
||||
public ClientController(IManager manager, IMetaService metaService,
|
||||
IConfigurationHandler<StatsConfiguration> configurationHandler) : base(manager)
|
||||
{
|
||||
_metaService = metaService;
|
||||
_configurationHandler = configurationHandler;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ProfileAsync(int id, MetaType? metaFilterType)
|
||||
@ -42,13 +44,13 @@ namespace WebfrontCore.Controllers
|
||||
client.SetAdditionalProperty(EFMeta.ClientTag, tag.LinkedMeta.Value);
|
||||
}
|
||||
|
||||
int displayLevelInt = (int)client.Level;
|
||||
string displayLevel = client.Level.ToLocalizedLevelName();
|
||||
var displayLevelInt = (int)client.Level;
|
||||
var displayLevel = client.Level.ToLocalizedLevelName();
|
||||
|
||||
if (!Authorized && client.Level.ShouldHideLevel())
|
||||
{
|
||||
displayLevelInt = (int)Permission.User;
|
||||
displayLevel = Permission.User.ToLocalizedLevelName();
|
||||
displayLevelInt = (int)Data.Models.Client.EFClient.Permission.User;
|
||||
displayLevel = Data.Models.Client.EFClient.Permission.User.ToLocalizedLevelName();
|
||||
}
|
||||
|
||||
displayLevel = string.IsNullOrEmpty(client.Tag) ? displayLevel : $"{displayLevel} ({client.Tag})";
|
||||
@ -77,7 +79,7 @@ namespace WebfrontCore.Controllers
|
||||
.Prepend(client.CurrentAlias.IPAddress.ConvertIPtoString())
|
||||
.Distinct()
|
||||
.ToList(),
|
||||
HasActivePenalty = activePenalties.Any(_penalty => _penalty.Type != PenaltyType.Flag),
|
||||
HasActivePenalty = activePenalties.Any(_penalty => _penalty.Type != EFPenalty.PenaltyType.Flag),
|
||||
Online = Manager.GetActiveClients().FirstOrDefault(c => c.ClientId == client.ClientId) != null,
|
||||
TimeOnline = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(),
|
||||
LinkedAccounts = client.LinkedAccounts,
|
||||
@ -111,6 +113,7 @@ namespace WebfrontCore.Controllers
|
||||
ViewBag.Title += " " + Localization["WEBFRONT_CLIENT_PROFILE_TITLE"];
|
||||
ViewBag.Description = $"Client information for {strippedName}";
|
||||
ViewBag.Keywords = $"IW4MAdmin, client, profile, {strippedName}";
|
||||
ViewBag.UseNewStats = _configurationHandler.Configuration().EnableAdvancedMetrics;
|
||||
|
||||
return View("Profile/Index", clientDto);
|
||||
}
|
||||
@ -155,10 +158,10 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
foreach (var client in clientsDto)
|
||||
{
|
||||
if (!Authorized && ((Permission)client.LevelInt).ShouldHideLevel())
|
||||
if (!Authorized && ((Data.Models.Client.EFClient.Permission)client.LevelInt).ShouldHideLevel())
|
||||
{
|
||||
client.LevelInt = (int)Permission.User;
|
||||
client.Level = Permission.User.ToLocalizedLevelName();
|
||||
client.LevelInt = (int)Data.Models.Client.EFClient.Permission.User;
|
||||
client.Level = Data.Models.Client.EFClient.Permission.User.ToLocalizedLevelName();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using Stats.Dtos;
|
||||
|
||||
namespace WebfrontCore.Controllers
|
||||
{
|
||||
[Route("clientstatistics")]
|
||||
public class ClientStatisticsController : BaseController
|
||||
{
|
||||
private IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> _queryHelper;
|
||||
private readonly DefaultSettings _defaultConfig;
|
||||
|
||||
public ClientStatisticsController(IManager manager,
|
||||
IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> queryHelper,
|
||||
IConfigurationHandler<DefaultSettings> configurationHandler) : base(manager)
|
||||
{
|
||||
_queryHelper = queryHelper;
|
||||
_defaultConfig = configurationHandler.Configuration();
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/advanced")]
|
||||
public async Task<IActionResult> Advanced(int id, [FromQuery] string serverId)
|
||||
{
|
||||
ViewBag.Config = _defaultConfig.GameStrings;
|
||||
var hitInfo = await _queryHelper.QueryResource(new StatsInfoRequest
|
||||
{
|
||||
ClientId = id,
|
||||
ServerEndpoint = serverId
|
||||
});
|
||||
|
||||
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo.Results.First());
|
||||
}
|
||||
}
|
||||
}
|
213
WebfrontCore/Controllers/Client/Legacy/StatsController.cs
Normal file
@ -0,0 +1,213 @@
|
||||
using IW4MAdmin.Plugins.Stats;
|
||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using Stats.Dtos;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
using Data.Abstractions;
|
||||
using IW4MAdmin.Plugins.Stats.Config;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
{
|
||||
public class StatsController : BaseController
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IManager _manager;
|
||||
private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatResourceQueryHelper;
|
||||
private readonly ITranslationLookup _translationLookup;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly IConfigurationHandler<StatsConfiguration> _configurationHandler;
|
||||
|
||||
public StatsController(ILogger<StatsController> logger, IManager manager, IResourceQueryHelper<ChatSearchQuery,
|
||||
MessageResponse> resourceQueryHelper, ITranslationLookup translationLookup,
|
||||
IDatabaseContextFactory contextFactory,
|
||||
IConfigurationHandler<StatsConfiguration> configurationHandler) : base(manager)
|
||||
{
|
||||
_logger = logger;
|
||||
_manager = manager;
|
||||
_chatResourceQueryHelper = resourceQueryHelper;
|
||||
_translationLookup = translationLookup;
|
||||
_contextFactory = contextFactory;
|
||||
_configurationHandler = configurationHandler;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult TopPlayersAsync()
|
||||
{
|
||||
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"];
|
||||
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"];
|
||||
ViewBag.Servers = _manager.GetServers()
|
||||
.Select(_server => new ServerInfo() {Name = _server.Hostname, ID = _server.EndPoint});
|
||||
ViewBag.Localization = _translationLookup;
|
||||
|
||||
return View("~/Views/Client/Statistics/Index.cshtml");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetTopPlayersAsync(int count, int offset, long? serverId = null)
|
||||
{
|
||||
// this prevents empty results when we really want aggregate
|
||||
if (serverId == 0)
|
||||
{
|
||||
serverId = null;
|
||||
}
|
||||
|
||||
var server = _manager.GetServers().FirstOrDefault(_server => _server.EndPoint == serverId);
|
||||
|
||||
if (server != null)
|
||||
{
|
||||
serverId = StatManager.GetIdForServer(server);
|
||||
}
|
||||
|
||||
var results = _configurationHandler.Configuration().EnableAdvancedMetrics
|
||||
? await Plugin.Manager.GetNewTopStats(offset, count, serverId)
|
||||
: await Plugin.Manager.GetTopStats(offset, count, serverId);
|
||||
|
||||
// this returns an empty result so we know to stale the loader
|
||||
if (results.Count == 0 && offset > 0)
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
ViewBag.UseNewStats = _configurationHandler.Configuration().EnableAdvancedMetrics;
|
||||
return View("~/Views/Client/Statistics/Components/TopPlayers/_List.cshtml", results);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetMessageAsync(string serverId, long when)
|
||||
{
|
||||
var whenTime = DateTime.FromFileTimeUtc(when);
|
||||
var whenUpper = whenTime.AddMinutes(5);
|
||||
var whenLower = whenTime.AddMinutes(-5);
|
||||
|
||||
var messages = await _chatResourceQueryHelper.QueryResource(new ChatSearchQuery()
|
||||
{
|
||||
ServerId = serverId,
|
||||
SentBefore = whenUpper,
|
||||
SentAfter = whenLower
|
||||
});
|
||||
|
||||
return View("~/Views/Client/_MessageContext.cshtml", messages.Results.ToList());
|
||||
}
|
||||
|
||||
[HttpGet("Message/Find")]
|
||||
public async Task<IActionResult> FindMessage([FromQuery] string query)
|
||||
{
|
||||
ViewBag.Localization = _translationLookup;
|
||||
ViewBag.EnableColorCodes = _manager.GetApplicationSettings().Configuration().EnableColorCodes;
|
||||
ViewBag.Query = query;
|
||||
ViewBag.QueryLimit = 100;
|
||||
ViewBag.Title = _translationLookup["WEBFRONT_STATS_MESSAGES_TITLE"];
|
||||
ViewBag.Error = null;
|
||||
ViewBag.IsFluid = true;
|
||||
ChatSearchQuery searchRequest = null;
|
||||
|
||||
try
|
||||
{
|
||||
searchRequest = query.ParseSearchInfo(int.MaxValue, 0);
|
||||
}
|
||||
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not parse chat message search query {query}", query);
|
||||
ViewBag.Error = e;
|
||||
}
|
||||
|
||||
catch (FormatException e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not parse chat message search query filter format {query}", query);
|
||||
ViewBag.Error = e;
|
||||
}
|
||||
|
||||
var result = searchRequest != null ? await _chatResourceQueryHelper.QueryResource(searchRequest) : null;
|
||||
return View("~/Views/Client/Message/Find.cshtml", result);
|
||||
}
|
||||
|
||||
[HttpGet("Message/FindNext")]
|
||||
public async Task<IActionResult> FindNextMessages([FromQuery] string query, [FromQuery] int count,
|
||||
[FromQuery] int offset)
|
||||
{
|
||||
ChatSearchQuery searchRequest;
|
||||
|
||||
try
|
||||
{
|
||||
searchRequest = query.ParseSearchInfo(count, offset);
|
||||
}
|
||||
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not parse chat message search query {query}", query);
|
||||
throw;
|
||||
}
|
||||
|
||||
catch (FormatException e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not parse chat message search query filter format {query}", query);
|
||||
throw;
|
||||
}
|
||||
|
||||
var result = await _chatResourceQueryHelper.QueryResource(searchRequest);
|
||||
return PartialView("~/Views/Client/Message/_Item.cshtml", result.Results);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GetAutomatedPenaltyInfoAsync(int penaltyId)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
|
||||
var penalty = await context.Penalties
|
||||
.Select(_penalty => new
|
||||
{_penalty.OffenderId, _penalty.PenaltyId, _penalty.When, _penalty.AutomatedOffense})
|
||||
.FirstOrDefaultAsync(_penalty => _penalty.PenaltyId == penaltyId);
|
||||
|
||||
if (penalty == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// todo: this can be optimized
|
||||
var iqSnapshotInfo = context.ACSnapshots
|
||||
.Where(s => s.ClientId == penalty.OffenderId)
|
||||
.Include(s => s.LastStrainAngle)
|
||||
.Include(s => s.HitOrigin)
|
||||
.Include(s => s.HitDestination)
|
||||
.Include(s => s.CurrentViewAngle)
|
||||
.Include(s => s.PredictedViewAngles)
|
||||
.ThenInclude(_angles => _angles.Vector)
|
||||
.OrderBy(s => s.When)
|
||||
.ThenBy(s => s.Hits);
|
||||
|
||||
var penaltyInfo = await iqSnapshotInfo.ToListAsync();
|
||||
|
||||
if (penaltyInfo.Count > 0)
|
||||
{
|
||||
return View("~/Views/Client/_PenaltyInfo.cshtml", penaltyInfo);
|
||||
}
|
||||
|
||||
// we want to show anything related to the automated offense
|
||||
else
|
||||
{
|
||||
return View("~/Views/Client/_MessageContext.cshtml", new List<MessageResponse>
|
||||
{
|
||||
new MessageResponse()
|
||||
{
|
||||
ClientId = penalty.OffenderId,
|
||||
Message = penalty.AutomatedOffense,
|
||||
When = penalty.When
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
|
||||
namespace WebfrontCore.Controllers
|
||||
{
|
||||
|
@ -1,10 +1,15 @@
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models.Client;
|
||||
using Data.Models.Client.Stats;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static SharedLibraryCore.Server;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
@ -15,11 +20,23 @@ namespace WebfrontCore.Controllers
|
||||
{
|
||||
private readonly ITranslationLookup _translationLookup;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
|
||||
private const string ServerStatKey = nameof(ServerStatKey);
|
||||
|
||||
public HomeController(ILogger<HomeController> logger, IManager manager, ITranslationLookup translationLookup) : base(manager)
|
||||
public HomeController(ILogger<HomeController> logger, IManager manager, ITranslationLookup translationLookup,
|
||||
IDataValueCache<EFClient, (int, int)> serverStatsCache) : base(manager)
|
||||
{
|
||||
_logger = logger;
|
||||
_translationLookup = translationLookup;
|
||||
_serverStatsCache = serverStatsCache;
|
||||
|
||||
_serverStatsCache.SetCacheItem(async set =>
|
||||
{
|
||||
var count = await set.CountAsync();
|
||||
var startOfPeriod = DateTime.UtcNow.AddHours(-24);
|
||||
var recentCount = await set.CountAsync(client => client.LastConnection >= startOfPeriod);
|
||||
return (count, recentCount);
|
||||
}, ServerStatKey);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index(Game? game = null)
|
||||
@ -28,14 +45,15 @@ namespace WebfrontCore.Controllers
|
||||
ViewBag.Title = Localization["WEBFRONT_HOME_TITLE"];
|
||||
ViewBag.Keywords = Localization["WEBFRONT_KEWORDS_HOME"];
|
||||
|
||||
var servers = Manager.GetServers().Where(_server => !game.HasValue ? true : _server.GameName == game);
|
||||
var servers = Manager.GetServers().Where(_server => !game.HasValue || _server.GameName == game);
|
||||
var (count, recentCount) = await _serverStatsCache.GetCacheItem(ServerStatKey);
|
||||
|
||||
var model = new IW4MAdminInfo()
|
||||
{
|
||||
TotalAvailableClientSlots = servers.Sum(_server => _server.MaxClients),
|
||||
TotalOccupiedClientSlots = servers.SelectMany(_server => _server.GetClientsAsList()).Count(),
|
||||
TotalClientCount = await Manager.GetClientService().GetTotalClientsAsync(),
|
||||
RecentClientCount = await Manager.GetClientService().GetRecentClientCount(),
|
||||
TotalClientCount = count,
|
||||
RecentClientCount = recentCount,
|
||||
Game = game,
|
||||
ActiveServerGames = Manager.GetServers().Select(_server => _server.GameName).Distinct().ToArray()
|
||||
};
|
||||
@ -46,7 +64,8 @@ namespace WebfrontCore.Controllers
|
||||
public IActionResult Error()
|
||||
{
|
||||
var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
|
||||
_logger.LogError("[Webfront] {path} {message} {@exception}", exceptionFeature.Path, exceptionFeature.Error.Message, exceptionFeature.Error);
|
||||
_logger.LogError("[Webfront] {path} {message} {@exception}", exceptionFeature.Path,
|
||||
exceptionFeature.Error.Message, exceptionFeature.Error);
|
||||
ViewBag.Description = Localization["WEBFRONT_ERROR_DESC"];
|
||||
ViewBag.Title = Localization["WEBFRONT_ERROR_TITLE"];
|
||||
return View(exceptionFeature.Error);
|
||||
@ -71,15 +90,16 @@ namespace WebfrontCore.Controllers
|
||||
.GroupBy(_cmd =>
|
||||
{
|
||||
// we need the plugin type the command is defined in
|
||||
var pluginType = _cmd.GetType().Assembly.GetTypes().FirstOrDefault(_type => _type.Assembly != excludedAssembly && typeof(IPlugin).IsAssignableFrom(_type));
|
||||
return pluginType == null ?
|
||||
_translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"] :
|
||||
var pluginType = _cmd.GetType().Assembly.GetTypes().FirstOrDefault(_type =>
|
||||
_type.Assembly != excludedAssembly && typeof(IPlugin).IsAssignableFrom(_type));
|
||||
return pluginType == null ? _translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"] :
|
||||
pluginType.Name == "ScriptPlugin" ? _translationLookup["WEBFRONT_HELP_SCRIPT_PLUGIN"] :
|
||||
Manager.Plugins.First(_plugin => _plugin.GetType() == pluginType).Name; // for now we're just returning the name of the plugin, maybe later we'll include more info
|
||||
Manager.Plugins.First(_plugin => _plugin.GetType() == pluginType)
|
||||
.Name; // for now we're just returning the name of the plugin, maybe later we'll include more info
|
||||
})
|
||||
.Select(_grp => (_grp.Key, _grp.AsEnumerable()));
|
||||
|
||||
return View(commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using static SharedLibraryCore.Database.Models.EFPenalty;
|
||||
using Data.Abstractions;
|
||||
using Data.Models;
|
||||
|
||||
namespace WebfrontCore.Controllers
|
||||
{
|
||||
@ -20,7 +20,7 @@ namespace WebfrontCore.Controllers
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
public IActionResult List(PenaltyType showOnly = PenaltyType.Any, bool hideAutomatedPenalties = true)
|
||||
public IActionResult List(EFPenalty.PenaltyType showOnly = EFPenalty.PenaltyType.Any, bool hideAutomatedPenalties = true)
|
||||
{
|
||||
ViewBag.Description = Localization["WEBFRONT_DESCRIPTION_PENALTIES"];
|
||||
ViewBag.Title = Localization["WEBFRONT_PENALTY_TITLE"];
|
||||
@ -30,7 +30,7 @@ namespace WebfrontCore.Controllers
|
||||
return View(showOnly);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ListAsync(int offset = 0, PenaltyType showOnly = PenaltyType.Any, bool hideAutomatedPenalties = true)
|
||||
public async Task<IActionResult> ListAsync(int offset = 0, EFPenalty.PenaltyType showOnly = EFPenalty.PenaltyType.Any, bool hideAutomatedPenalties = true)
|
||||
{
|
||||
return await Task.FromResult(View("_List", new ViewModels.PenaltyFilterInfo()
|
||||
{
|
||||
@ -52,7 +52,7 @@ namespace WebfrontCore.Controllers
|
||||
await using var ctx = _contextFactory.CreateContext(false);
|
||||
var iqPenalties = ctx.Penalties
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Type == PenaltyType.Ban && p.Active)
|
||||
.Where(p => p.Type == EFPenalty.PenaltyType.Ban && p.Active)
|
||||
.OrderByDescending(_penalty => _penalty.When)
|
||||
.Select(p => new PenaltyInfo()
|
||||
{
|
||||
|
@ -6,7 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using static SharedLibraryCore.Database.Models.EFClient;
|
||||
using Data.Models.Client;
|
||||
using static SharedLibraryCore.GameEvent;
|
||||
|
||||
namespace WebfrontCore.Middleware
|
||||
@ -36,10 +36,10 @@ namespace WebfrontCore.Middleware
|
||||
private void OnGameEvent(object sender, GameEvent gameEvent)
|
||||
{
|
||||
if (gameEvent.Type == EventType.ChangePermission &&
|
||||
gameEvent.Extra is Permission perm)
|
||||
gameEvent.Extra is EFClient.Permission perm)
|
||||
{
|
||||
// we want to remove the claims when the client is demoted
|
||||
if (perm < Permission.Trusted)
|
||||
if (perm < EFClient.Permission.Trusted)
|
||||
{
|
||||
lock (_privilegedClientIds)
|
||||
{
|
||||
@ -47,7 +47,7 @@ namespace WebfrontCore.Middleware
|
||||
}
|
||||
}
|
||||
// and add if promoted
|
||||
else if (perm > Permission.Trusted &&
|
||||
else if (perm > EFClient.Permission.Trusted &&
|
||||
!_privilegedClientIds.Contains(gameEvent.Target.ClientId))
|
||||
{
|
||||
lock (_privilegedClientIds)
|
||||
|
@ -4,27 +4,28 @@ using FluentValidation.AspNetCore;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Database;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Services;
|
||||
using Stats.Dtos;
|
||||
using Stats.Helpers;
|
||||
using StatsWeb;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Helpers;
|
||||
using IW4MAdmin.Plugins.Stats.Config;
|
||||
using Stats.Client.Abstractions;
|
||||
using WebfrontCore.Controllers.API.Validation;
|
||||
using WebfrontCore.Middleware;
|
||||
|
||||
@ -80,11 +81,14 @@ namespace WebfrontCore
|
||||
});
|
||||
|
||||
#if DEBUG
|
||||
mvcBuilder = mvcBuilder.AddRazorRuntimeCompilation();
|
||||
services.Configure<RazorViewEngineOptions>(_options =>
|
||||
{
|
||||
_options.ViewLocationFormats.Add(@"/Views/Plugins/{1}/{0}" + RazorViewEngine.ViewExtension);
|
||||
});
|
||||
mvcBuilder = mvcBuilder.AddRazorRuntimeCompilation();
|
||||
services.Configure<RazorViewEngineOptions>(_options =>
|
||||
{
|
||||
_options.ViewLocationFormats.Add(@"/Views/Plugins/{1}/{0}" + RazorViewEngine.ViewExtension);
|
||||
_options.ViewLocationFormats.Add("/Views/Plugins/Stats/Advanced.cshtml");
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
foreach (var asm in pluginAssemblies())
|
||||
@ -106,7 +110,8 @@ namespace WebfrontCore
|
||||
services.AddTransient<IValidator<FindClientRequest>, FindClientRequestValidator>();
|
||||
services.AddSingleton<IResourceQueryHelper<FindClientRequest, FindClientResult>, ClientService>();
|
||||
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, StatsInfoResult>, StatsResourceQueryHelper>();
|
||||
|
||||
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>, AdvancedClientStatsResourceQueryHelper>();
|
||||
services.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>));
|
||||
// todo: this needs to be handled more gracefully
|
||||
services.AddSingleton(Program.ApplicationServiceProvider.GetService<IConfigurationHandlerFactory>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider.GetService<IDatabaseContextFactory>());
|
||||
@ -116,6 +121,12 @@ namespace WebfrontCore
|
||||
services.AddSingleton(Program.ApplicationServiceProvider.GetService<IMetaService>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider.GetService<ApplicationConfiguration>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ClientService>());
|
||||
services.AddSingleton(
|
||||
Program.ApplicationServiceProvider.GetRequiredService<IServerDistributionCalculator>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider
|
||||
.GetRequiredService<IConfigurationHandler<DefaultSettings>>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider
|
||||
.GetRequiredService<IConfigurationHandler<StatsConfiguration>>());
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
|
@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
|
||||
namespace WebfrontCore.ViewComponents
|
||||
{
|
||||
|
42
WebfrontCore/ViewComponents/TopPlayersViewComponent.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using IW4MAdmin.Plugins.Stats;
|
||||
using IW4MAdmin.Plugins.Stats.Config;
|
||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace WebfrontCore.ViewComponents
|
||||
{
|
||||
public class TopPlayersViewComponent : ViewComponent
|
||||
{
|
||||
private readonly IConfigurationHandler<StatsConfiguration> _configurationHandler;
|
||||
|
||||
public TopPlayersViewComponent(IConfigurationHandler<StatsConfiguration> configurationHandler)
|
||||
{
|
||||
_configurationHandler = configurationHandler;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(int count, int offset, long? serverId = null)
|
||||
{
|
||||
if (serverId == 0)
|
||||
{
|
||||
serverId = null;
|
||||
}
|
||||
|
||||
var server = Plugin.ServerManager.GetServers().FirstOrDefault(_server => _server.EndPoint == serverId);
|
||||
|
||||
if (server != null)
|
||||
{
|
||||
serverId = StatManager.GetIdForServer(server);
|
||||
}
|
||||
|
||||
|
||||
ViewBag.UseNewStats = _configurationHandler.Configuration().EnableAdvancedMetrics;
|
||||
return View("~/Views/Client/Statistics/Components/TopPlayers/_List.cshtml",
|
||||
_configurationHandler.Configuration().EnableAdvancedMetrics
|
||||
? await Plugin.Manager.GetNewTopStats(offset, count, serverId)
|
||||
: await Plugin.Manager.GetTopStats(offset, count, serverId));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
using static SharedLibraryCore.Database.Models.EFPenalty;
|
||||
using Data.Models;
|
||||
|
||||
namespace WebfrontCore.ViewModels
|
||||
{
|
||||
@ -15,7 +15,7 @@ namespace WebfrontCore.ViewModels
|
||||
/// <summary>
|
||||
/// show only a certain type of penalty
|
||||
/// </summary>
|
||||
public PenaltyType ShowOnly { get; set; }
|
||||
public EFPenalty.PenaltyType ShowOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ignore penalties that are automated
|
||||
|
@ -1,6 +1,6 @@
|
||||
@model IList<SharedLibraryCore.Dtos.PlayerInfo>
|
||||
@{
|
||||
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
|
||||
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
}
|
||||
|
||||
<div class="row d-none d-lg-block ">
|
||||
|
39
WebfrontCore/Views/Client/Message/Find.cshtml
Normal file
@ -0,0 +1,39 @@
|
||||
@using SharedLibraryCore.Dtos.Meta.Responses
|
||||
@model SharedLibraryCore.Helpers.ResourceQueryHelperResult<MessageResponse>
|
||||
|
||||
@if (ViewBag.Error != null)
|
||||
{
|
||||
<h4 class="text-red">@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_INVALID_QUERY"], ViewBag.Error.Message)</h4>
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<h4 class="pb-3 text-center">@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_STATS_MESSAGES_FOUND"], Model.TotalResultCount.ToString("N0"))</h4>
|
||||
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="d-none d-lg-table-header-group">
|
||||
<tr class="bg-primary pt-2 pb-2">
|
||||
<th scope="col">@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
|
||||
<th scope="col">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th>
|
||||
<th scope="col">@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]</th>
|
||||
<th scope="col" class="text-right">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="message_table_body" class="border-bottom bg-dark">
|
||||
<partial name="Message/_Item" model="@Model.Results" />
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<span id="load_more_messages_button" class="loader-load-more oi oi-chevron-bottom text-center text-primary w-100 h3 pb-0 mb-0 d-none d-lg-block"></span>
|
||||
|
||||
@section scripts {
|
||||
<environment include="Development">
|
||||
<script type="text/javascript" src="~/js/loader.js"></script>
|
||||
</environment>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
initLoader('/Message/FindNext?query=@ViewBag.Query', '#message_table_body', @Model.RetrievedResultCount, @ViewBag.QueryLimit);
|
||||
});
|
||||
</script>
|
||||
}
|
||||
}
|
68
WebfrontCore/Views/Client/Message/_Item.cshtml
Normal file
@ -0,0 +1,68 @@
|
||||
@using SharedLibraryCore.Dtos.Meta.Responses
|
||||
@model IEnumerable<MessageResponse>
|
||||
|
||||
@foreach (var message in Model)
|
||||
{
|
||||
<!-- desktop -->
|
||||
<tr class="d-none d-lg-table-row">
|
||||
<td>
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@message.ClientId" class="link-inverse">
|
||||
<color-code value="@message.ClientName" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-light w-50 text-break">
|
||||
@if (message.IsHidden && !ViewBag.Authorized)
|
||||
{
|
||||
<color-code value="@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
}
|
||||
</td>
|
||||
<td class="text-light">
|
||||
<color-code value="@(message.ServerName ?? "--")" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</td>
|
||||
<td class="text-right text-light">
|
||||
@message.When
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- mobile -->
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
|
||||
<td class="text-light">
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@message.ClientId" class="link-inverse">
|
||||
<color-code value="@message.ClientName" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th>
|
||||
<td class="text-light">
|
||||
@if (message.IsHidden && !ViewBag.Authorized)
|
||||
{
|
||||
<color-code value="@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]</th>
|
||||
<td class="text-light">
|
||||
<color-code value="@(message.ServerName ?? "--")" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary" style="border-bottom: 1px solid #222">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
|
||||
<td class="text-light mb-2 border-bottom">
|
||||
@message.When
|
||||
</td>
|
||||
</tr>
|
||||
}
|
@ -1,17 +1,16 @@
|
||||
@using SharedLibraryCore.Database.Models
|
||||
@using SharedLibraryCore.Interfaces
|
||||
@using SharedLibraryCore
|
||||
@using SharedLibraryCore.Interfaces
|
||||
@using Data.Models
|
||||
@model SharedLibraryCore.Dtos.PlayerInfo
|
||||
@{
|
||||
string match = System.Text.RegularExpressions.Regex.Match(Model.Name.ToUpper(), "[A-Z]").Value;
|
||||
string shortCode = match == string.Empty ? "?" : match;
|
||||
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
|
||||
string gravatarUrl = Model.Meta.FirstOrDefault(m => m.Key == "GravatarEmail")?.Value;
|
||||
bool isFlagged = Model.LevelInt == (int)SharedLibraryCore.Database.Models.EFClient.Permission.Flagged;
|
||||
bool isPermBanned = Model.LevelInt == (int)SharedLibraryCore.Database.Models.EFClient.Permission.Banned;
|
||||
bool isFlagged = Model.LevelInt == (int) SharedLibraryCore.Database.Models.EFClient.Permission.Flagged;
|
||||
bool isPermBanned = Model.LevelInt == (int) SharedLibraryCore.Database.Models.EFClient.Permission.Banned;
|
||||
bool isTempBanned = Model.ActivePenalty?.Type == EFPenalty.PenaltyType.TempBan;
|
||||
string translationKey = $"WEBFRONT_PROFILE_{Model.ActivePenalty?.Type.ToString().ToUpper()}_INFO";
|
||||
var ignoredMetaTypes = new[] { MetaType.Information, MetaType.Other, MetaType.QuickMessage };
|
||||
var ignoredMetaTypes = new[] {MetaType.Information, MetaType.Other, MetaType.QuickMessage};
|
||||
}
|
||||
|
||||
<div id="profile_wrapper" class="pb-3 row d-flex flex-column flex-lg-row">
|
||||
@ -25,7 +24,9 @@
|
||||
<!-- Name/Level Column -->
|
||||
<div class="w-75 d-block d-lg-inline-flex flex-column flex-fill text-center text-lg-left pb-3 pb-lg-0 pt-3 pt-lg-0 pl-3 pr-3 ml-auto mr-auto" style="overflow-wrap: anywhere">
|
||||
<div class="mt-n2 flex-fill d-block d-lg-inline-flex">
|
||||
<div id="profile_name" class="client-name h1 mb-0"><color-code value="@Model.Name" allow="@ViewBag.EnableColorCodes"></color-code></div>
|
||||
<div id="profile_name" class="client-name h1 mb-0">
|
||||
<color-code value="@Model.Name" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</div>
|
||||
@if (ViewBag.Authorized)
|
||||
{
|
||||
<div id="profile_aliases_btn" class="oi oi-caret-bottom h3 ml-0 ml-lg-2 mb-0 pt-lg-2 mt-lg-1"></div>
|
||||
@ -37,16 +38,18 @@
|
||||
<div id="profile_aliases" class="text-muted pt-0 pt-lg-2 pb-2">
|
||||
@foreach (var linked in Model.LinkedAccounts)
|
||||
{
|
||||
@Html.ActionLink(linked.Value.ToString("X"), "ProfileAsync", "Client", new { id = linked.Key }, new { @class = "link-inverse" })<br />
|
||||
@Html.ActionLink(linked.Value.ToString("X"), "ProfileAsync", "Client", new {id = linked.Key}, new {@class = "link-inverse"})<br/>
|
||||
}
|
||||
@foreach (string alias in Model.Aliases)
|
||||
@foreach (var alias in Model.Aliases)
|
||||
{
|
||||
<color-code value="@alias" allow="@ViewBag.EnableColorCodes"></color-code><br />
|
||||
<color-code value="@alias" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
<br/>
|
||||
}
|
||||
|
||||
@foreach (string ip in Model.IPs)
|
||||
{
|
||||
<a class="ip-locate-link" href="#" data-ip="@ip">@ip</a><br />
|
||||
<a class="ip-locate-link" href="#" data-ip="@ip">@ip</a>
|
||||
<br/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@ -62,7 +65,7 @@
|
||||
break;
|
||||
case "time":
|
||||
<span class="text-white font-weight-lighter">
|
||||
@Utilities.HumanizeForCurrentCulture(Model.ActivePenalty.Expires.Value - DateTime.UtcNow)
|
||||
@((Model.ActivePenalty.Expires.Value - DateTime.UtcNow).HumanizeForCurrentCulture())
|
||||
</span>
|
||||
break;
|
||||
default:
|
||||
@ -89,20 +92,21 @@
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@if (ViewBag.Authorized)
|
||||
{
|
||||
<div class="pr-lg-0 text-center text-lg-right">
|
||||
|
||||
<div class="pr-lg-0 text-center text-lg-right">
|
||||
@if (ViewBag.Authorized)
|
||||
{
|
||||
@if (!isPermBanned)
|
||||
{
|
||||
<div class="profile-action oi oi-flag h3 ml-2 @(isFlagged ? "text-secondary" : "text-success")" data-action="@(isFlagged ? "unflag" : "flag")" aria-hidden="true"></div>
|
||||
}
|
||||
|
||||
@if (Model.LevelInt < (int)ViewBag.User.Level && !Model.HasActivePenalty)
|
||||
@if (Model.LevelInt < (int) ViewBag.User.Level && !Model.HasActivePenalty)
|
||||
{
|
||||
<div id="profile_action_ban_btn" class="profile-action oi oi-lock-unlocked text-success h3 ml-2" title="Ban Client" data-action="ban" aria-hidden="true"></div>
|
||||
}
|
||||
|
||||
@if (Model.LevelInt < (int)ViewBag.User.Level && Model.HasActivePenalty)
|
||||
@if (Model.LevelInt < (int) ViewBag.User.Level && Model.HasActivePenalty)
|
||||
{
|
||||
@if (isTempBanned)
|
||||
{
|
||||
@ -120,12 +124,16 @@
|
||||
{
|
||||
<div id="profile_action_edit_btn" class="profile-action oi oi-cog text-muted h3 ml-2" title="Client Options" data-action="edit" aria-hidden="true"></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (ViewBag.UseNewStats)
|
||||
{
|
||||
<a asp-controller="ClientStatistics" asp-action="Advanced" asp-route-id="@Model.ClientId" class="oi oi-graph text-primary h3 ml-2" title="Stats" aria-hidden="true"></a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="profile_info" class="row d-block d-lg-flex flex-row border-bottom border-top pt-2 pb-2">
|
||||
<partial name="Meta/_Information.cshtml" model="@Model.Meta" />
|
||||
<partial name="Meta/_Information.cshtml" model="@Model.Meta"/>
|
||||
</div>
|
||||
|
||||
<div class="row border-bottom">
|
||||
@ -136,7 +144,9 @@
|
||||
<div class="d-none d-md-flex flex-fill" id="filter_meta_container">
|
||||
<a asp-action="ProfileAsync" asp-controller="Client"
|
||||
class="nav-link p-2 pl-3 pr-3 text-center col-12 col-md-auto text-md-left @(!Model.MetaFilterType.HasValue ? "btn-primary text-white" : "text-muted")"
|
||||
asp-route-id="@Model.ClientId">@ViewBag.Localization["META_TYPE_ALL_NAME"]</a>
|
||||
asp-route-id="@Model.ClientId">
|
||||
@ViewBag.Localization["META_TYPE_ALL_NAME"]
|
||||
</a>
|
||||
|
||||
@foreach (MetaType type in Enum.GetValues(typeof(MetaType)))
|
||||
{
|
||||
@ -146,7 +156,9 @@
|
||||
class="nav-link p-2 pl-3 pr-3 text-center col-12 col-md-auto text-md-left @(Model.MetaFilterType.HasValue && Model.MetaFilterType.Value.ToString() == type.ToString() ? "btn-primary text-white" : "text-muted")"
|
||||
asp-route-id="@Model.ClientId"
|
||||
asp-route-metaFilterType="@type"
|
||||
data-meta-type="@type">@type.ToTranslatedName()</a>
|
||||
data-meta-type="@type">
|
||||
@type.ToTranslatedName()
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@ -166,7 +178,7 @@
|
||||
</div>
|
||||
|
||||
@section targetid {
|
||||
<input type="hidden" name="targetId" value="@Model.ClientId" />
|
||||
<input type="hidden" name="targetId" value="@Model.ClientId"/>
|
||||
}
|
||||
|
||||
@section scripts {
|
||||
@ -175,4 +187,4 @@
|
||||
<script type="text/javascript" src="~/js/profile.js"></script>
|
||||
</environment>
|
||||
<script>initLoader('/Client/Meta/@Model.ClientId', '#profile_events', 30, 30, [{ 'name': 'metaFilterType', 'value': '@Model.MetaFilterType' }]);</script>
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@
|
||||
else if (match.MatchValue == "reason")
|
||||
{
|
||||
<span class="text-white">
|
||||
@if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Warning)
|
||||
@if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Warning)
|
||||
{
|
||||
<span>@Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.AutomatedOffense)</span>
|
||||
<span class="oi oi-list-rich align-top text-primary automated-penalty-info-detailed" data-penalty-id="@Model.PenaltyId" style="margin-top: 0.125rem;" title="@ViewBag.Localization["WEBFRONT_CLIENT_META_AC_METRIC"]"></span>
|
||||
|
@ -1,5 +1,4 @@
|
||||
@using SharedLibraryCore.Dtos.Meta.Responses
|
||||
@using SharedLibraryCore
|
||||
@model ReceivedPenaltyResponse
|
||||
|
||||
@{
|
||||
@ -28,7 +27,7 @@
|
||||
else if (match.MatchValue == "reason")
|
||||
{
|
||||
<span class="text-white">
|
||||
@if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Warning && Model.PenaltyType != SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Kick)
|
||||
@if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Warning && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Kick)
|
||||
{
|
||||
<span>@Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.AutomatedOffense)</span>
|
||||
<span class="oi oi-list-rich align-top text-primary automated-penalty-info-detailed" data-penalty-id="@Model.PenaltyId" style="margin-top: 0.125rem;" title="@ViewBag.Localization["WEBFRONT_CLIENT_META_AC_METRIC"]"></span>
|
||||
|
457
WebfrontCore/Views/Client/Statistics/Advanced.cshtml
Normal file
@ -0,0 +1,457 @@
|
||||
@using SharedLibraryCore.Configuration
|
||||
@using Data.Models.Client.Stats
|
||||
@using Stats.Helpers
|
||||
@using Data.Models.Client
|
||||
@using Data.Models.Client.Stats.Reference
|
||||
@using Humanizer
|
||||
@using Humanizer.Localisation
|
||||
@using IW4MAdmin.Plugins.Stats
|
||||
@model Stats.Dtos.AdvancedStatsInfo
|
||||
@{
|
||||
ViewBag.Title = "Advanced Client Statistics";
|
||||
ViewBag.Description = Model.ClientName;
|
||||
|
||||
const int maxItems = 5;
|
||||
const string headshotKey = "MOD_HEAD_SHOT";
|
||||
const string meleeKey = "MOD_MELEE";
|
||||
|
||||
var suicideKeys = new[] {"MOD_SUICIDE", "MOD_FALLING"};
|
||||
var config = (GameStringConfiguration) ViewBag.Config;
|
||||
|
||||
var headerClass = Model.Level == EFClient.Permission.Banned ? "bg-danger" : "bg-primary";
|
||||
var textClass = Model.Level == EFClient.Permission.Banned ? "text-danger" : "text-primary";
|
||||
var borderBottomClass = Model.Level == EFClient.Permission.Banned ? "border-bottom-danger border-top-danger" : "border-bottom border-top";
|
||||
var borderClass = Model.Level == EFClient.Permission.Banned ? "border-danger" : "border-primary";
|
||||
var buttonClass = Model.Level == EFClient.Permission.Banned ? "btn-danger" : "btn-primary";
|
||||
|
||||
string GetWeaponNameForHit(EFClientHitStatistic stat)
|
||||
{
|
||||
if (stat == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var rebuiltName = stat.RebuildWeaponName();
|
||||
var name = config.GetStringForGame(rebuiltName);
|
||||
return !rebuiltName.Equals(name, StringComparison.InvariantCultureIgnoreCase)
|
||||
? name
|
||||
: config.GetStringForGame(stat.Weapon.Name);
|
||||
}
|
||||
|
||||
string GetWeaponAttachmentName(EFWeaponAttachmentCombo attachment)
|
||||
{
|
||||
if (attachment == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var attachmentText = string.Join('+', new[]
|
||||
{
|
||||
config.GetStringForGame(attachment.Attachment1.Name),
|
||||
config.GetStringForGame(attachment.Attachment2?.Name),
|
||||
config.GetStringForGame(attachment.Attachment3?.Name)
|
||||
}.Where(attach => !string.IsNullOrWhiteSpace(attach)));
|
||||
|
||||
return attachmentText;
|
||||
}
|
||||
|
||||
var weapons = Model.ByWeapon
|
||||
.Where(hit => hit.DamageInflicted > 0)
|
||||
.GroupBy(hit => new {hit.WeaponId})
|
||||
.Select(group =>
|
||||
{
|
||||
var withoutAttachments = group.FirstOrDefault(hit => hit.WeaponAttachmentComboId == null);
|
||||
var mostUsedAttachment = group.Except(new[] {withoutAttachments})
|
||||
.OrderByDescending(g => g.DamageInflicted)
|
||||
.GroupBy(g => g.WeaponAttachmentComboId)
|
||||
.FirstOrDefault()
|
||||
?.FirstOrDefault();
|
||||
|
||||
if (withoutAttachments == null || mostUsedAttachment == null)
|
||||
{
|
||||
return withoutAttachments;
|
||||
}
|
||||
|
||||
withoutAttachments.WeaponAttachmentComboId = mostUsedAttachment.WeaponAttachmentComboId;
|
||||
withoutAttachments.WeaponAttachmentCombo = mostUsedAttachment.WeaponAttachmentCombo;
|
||||
|
||||
return withoutAttachments;
|
||||
})
|
||||
.Where(hit => hit != null)
|
||||
.OrderByDescending(hit => hit.KillCount)
|
||||
.ToList();
|
||||
|
||||
var allPerServer = Model.All.Where(hit => hit.ServerId == Model.ServerId).ToList();
|
||||
|
||||
// if the serverId is supplied we want all the entries with serverID but nothing else
|
||||
var aggregate = Model.ServerId == null
|
||||
? Model.Aggregate
|
||||
: allPerServer.Where(hit => hit.WeaponId == null)
|
||||
.Where(hit => hit.HitLocation == null)
|
||||
.Where(hit => hit.ServerId == Model.ServerId)
|
||||
.Where(hit => hit.WeaponAttachmentComboId == null)
|
||||
.FirstOrDefault(hit => hit.MeansOfDeathId == null);
|
||||
|
||||
var filteredHitLocations = Model.ByHitLocation
|
||||
.Where(hit => hit.HitCount > 0)
|
||||
.Where(hit => hit.HitLocation.Name != "none")
|
||||
.Where(hit => hit.HitLocation.Name != "neck")
|
||||
.Where(hit => hit.ServerId == Model.ServerId)
|
||||
.OrderByDescending(hit => hit.HitCount)
|
||||
.ThenBy(hit => hit.HitLocationId)
|
||||
.ToList();
|
||||
|
||||
var uniqueWeapons = allPerServer.Any()
|
||||
? Model.ByWeapon.Where(hit => hit.ServerId == Model.ServerId)
|
||||
.Where(weapon => weapon.DamageInflicted > 0)
|
||||
.GroupBy(weapon => weapon.WeaponId)
|
||||
.Count()
|
||||
: (int?) null; // want to default to -- in ui instead of 0
|
||||
|
||||
var activeTime = weapons.Any()
|
||||
? TimeSpan.FromSeconds(weapons.Sum(weapon => weapon.UsageSeconds ?? 0))
|
||||
: (TimeSpan?) null; // want to default to -- in ui instead of 0
|
||||
|
||||
var kdr = aggregate == null
|
||||
? null
|
||||
: Math.Round(aggregate.KillCount / (float) aggregate.DeathCount, 2).ToString(Utilities.CurrentLocalization.Culture);
|
||||
|
||||
var serverLegacyStat = Model.LegacyStats
|
||||
.FirstOrDefault(stat => stat.ServerId == Model.ServerId);
|
||||
|
||||
// legacy stats section
|
||||
var performance = Model.Performance;
|
||||
var skill = Model.ServerId != null ? serverLegacyStat?.Skill.ToNumericalString() : Model.LegacyStats.WeightValueByPlaytime(nameof(EFClientStatistics.Skill), 0).ToNumericalString();
|
||||
var elo = Model.ServerId != null ? serverLegacyStat?.EloRating.ToNumericalString() : Model.LegacyStats.WeightValueByPlaytime(nameof(EFClientStatistics.EloRating), 0).ToNumericalString();
|
||||
var spm = Model.ServerId != null ? serverLegacyStat?.SPM.ToNumericalString() : Model.LegacyStats.WeightValueByPlaytime(nameof(EFClientStatistics.SPM), 0).ToNumericalString();
|
||||
|
||||
var performanceHistory = Model.Ratings
|
||||
.Select(rating => rating.PerformanceMetric);
|
||||
|
||||
if (performance != null)
|
||||
{
|
||||
performanceHistory = performanceHistory.Append(performance.Value);
|
||||
}
|
||||
|
||||
var score = allPerServer.Any()
|
||||
? allPerServer.Sum(stat => stat.Score)
|
||||
: null;
|
||||
|
||||
var headShots = allPerServer.Any()
|
||||
? allPerServer.Where(hit => hit.MeansOfDeath?.Name == headshotKey).Sum(hit => hit.HitCount)
|
||||
: (int?) null; // want to default to -- in ui instead of 0
|
||||
|
||||
var meleeKills = allPerServer.Any()
|
||||
? allPerServer.Where(hit => hit.MeansOfDeath?.Name == meleeKey).Sum(hit => hit.KillCount)
|
||||
: (int?) null;
|
||||
|
||||
var suicides = allPerServer.Any()
|
||||
? allPerServer.Where(hit => suicideKeys.Contains(hit.MeansOfDeath?.Name ?? "")).Sum(hit => hit.KillCount)
|
||||
: (int?) null;
|
||||
|
||||
var statCards = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Name = (ViewBag.Localization["PLUGINS_STATS_TEXT_KILLS"] as string).Titleize(),
|
||||
Value = aggregate?.KillCount.ToNumericalString()
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = (ViewBag.Localization["PLUGINS_STATS_TEXT_DEATHS"] as string).Titleize(),
|
||||
Value = aggregate?.DeathCount.ToNumericalString()
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = (ViewBag.Localization["PLUGINS_STATS_TEXT_KDR"] as string).Titleize(),
|
||||
Value = kdr
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = (ViewBag.Localization["WEBFRONT_ADV_STATS_SCORE"] as string).Titleize(),
|
||||
Value = score.ToNumericalString()
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = (ViewBag.Localization["WEBFRONT_ADV_STATS_ZSCORE"] as string),
|
||||
Value = Model.ZScore.ToNumericalString(2)
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = (ViewBag.Localization["PLUGINS_STATS_TEXT_SKILL"] as string).ToLower().Titleize(),
|
||||
Value = skill
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = (ViewBag.Localization["WEBFRONT_ADV_STATS_ELO"] as string).Titleize(),
|
||||
Value = elo
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = (ViewBag.Localization["PLUGINS_STATS_META_SPM"] as string).Titleize(),
|
||||
Value = spm
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_TOTAL_DAMAGE"] as string,
|
||||
Value = aggregate?.DamageInflicted.ToNumericalString()
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_SUICIDES"] as string,
|
||||
Value = suicides.ToNumericalString()
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_HEADSHOTS"] as string,
|
||||
Value = headShots.ToNumericalString()
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_MELEES"] as string,
|
||||
Value = meleeKills.ToNumericalString()
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_FAV_WEAP"] as string,
|
||||
Value = GetWeaponNameForHit(weapons.FirstOrDefault())
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_FAV_ATTACHMENTS"] as string,
|
||||
Value = GetWeaponAttachmentName(weapons.FirstOrDefault()?.WeaponAttachmentCombo)
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_TOTAL_WEAPONS_USED"] as string,
|
||||
Value = uniqueWeapons.ToNumericalString()
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_TOTAL_ACTIVE_TIME"] as string,
|
||||
Value = activeTime?.HumanizeForCurrentCulture()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
<div class="w-100 @headerClass mb-1">
|
||||
<select class="w-100 @headerClass text-white pl-4 pr-4 pt-2 pb-2 m-auto h5 @borderClass"
|
||||
id="server_selector"
|
||||
onchange="if (this.value) window.location.href=this.value">
|
||||
@if (Model.ServerId == null)
|
||||
{
|
||||
<option value="@Url.Action("Advanced", "ClientStatistics")" selected>@ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"]</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@Url.Action("Advanced", "ClientStatistics")">@ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"]</option>
|
||||
}
|
||||
@foreach (var server in Model.Servers)
|
||||
{
|
||||
if (server.Endpoint == Model.ServerEndpoint)
|
||||
{
|
||||
<option value="@Url.Action("Advanced", "ClientStatistics", new {serverId = server.Endpoint})" selected>@server.Name.StripColors()</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@Url.Action("Advanced", "ClientStatistics", new {serverId = server.Endpoint})">@server.Name.StripColors()</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="@headerClass p-4 mb-0 d-flex flex-wrap">
|
||||
|
||||
<div class="align-self-center d-flex flex-column flex-lg-row text-center text-lg-left mb-3 mb-md-0 p-2 ml-lg-0 mr-lg-0 ml-auto mr-auto">
|
||||
<div class="mr-lg-3 m-auto">
|
||||
<img class="img-fluid align-self-center" id="rank_icon" src="~/images/stats/ranks/rank_@(Model.ZScore.RankIconIndexForZScore()).png" alt="@performance"/>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-self-center" id="client_stats_summary">
|
||||
<div class="h1 mb-0 font-weight-bold">
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@Model.ClientId">@Model.ClientName.StripColors()</a>
|
||||
</div>
|
||||
@if (Model.Level == EFClient.Permission.Banned)
|
||||
{
|
||||
<div class="h5 mb-0">@ViewBag.Localization["GLOBAL_PERMISSION_BANNED"]</div>
|
||||
}
|
||||
else if (Model.ZScore != null)
|
||||
{
|
||||
if (Model.ServerId != null)
|
||||
{
|
||||
<div class="h5 mb-0">@((ViewBag.Localization["WEBFRONT_ADV_STATS_PERFORMANCE"] as string).FormatExt(performance.ToNumericalString()))</div>
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<div class="h5 mb-0">@((ViewBag.Localization["WEBFRONT_ADV_STATS_RATING"] as string).FormatExt(Model.Rating.ToNumericalString()))</div>
|
||||
}
|
||||
|
||||
if (Model.Ranking > 0)
|
||||
{
|
||||
<div class="h5 mb-0">@((ViewBag.Localization["WEBFRONT_ADV_STATS_RANKED"] as string).FormatExt(Model.Ranking.ToNumericalString()))</div>
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<div class="h5 mb-0">@ViewBag.Localization["WEBFRONT_ADV_STATS_EXPIRED"]</div>
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<div class="h5 mb-0">@ViewBag.Localization["WEBFRONT_STATS_INDEX_UNRANKED"]</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-50 m-auto ml-md-auto mr-md-0" id="client_performance_history_container">
|
||||
<canvas id="client_performance_history" data-history="@Html.Raw(Json.Serialize(performanceHistory))"></canvas>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mb-4 bg-dark @borderBottomClass d-flex flex-wrap">
|
||||
@foreach (var card in statCards)
|
||||
{
|
||||
<div class="pl-3 pr-4 pb-3 pt-3 stat-card flex-fill w-50 w-md-auto">
|
||||
@if (string.IsNullOrWhiteSpace(card.Value))
|
||||
{
|
||||
<h5 class="card-title @textClass">—</h5>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h5 class="card-title @textClass">@card.Value</h5>
|
||||
}
|
||||
<h6 class="card-subtitle mb-0 text-muted">@card.Name</h6>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- WEAPONS USED -->
|
||||
<div class="col-12 mb-4">
|
||||
<div class="@headerClass h4 mb-1 p-2">
|
||||
<div class="text-center">@ViewBag.Localization["WEBFRONT_ADV_STATS_WEAP_USAGE"]</div>
|
||||
</div>
|
||||
<table class="table mb-0">
|
||||
<tr class="@headerClass">
|
||||
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_WEAPON"]</th>
|
||||
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_FAV_ATTACHMENTS"]</th>
|
||||
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_KILLS"]</th>
|
||||
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_HITS"]</th>
|
||||
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_DAMAGE"]</th>
|
||||
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_USAGE"]</th>
|
||||
</tr>
|
||||
@foreach (var weaponHit in weapons.Take(maxItems))
|
||||
{
|
||||
<tr class="bg-dark">
|
||||
<td class="@textClass text-force-break">@GetWeaponNameForHit(weaponHit)</td>
|
||||
@{ var attachments = GetWeaponAttachmentName(weaponHit.WeaponAttachmentCombo); }
|
||||
@if (string.IsNullOrWhiteSpace(attachments))
|
||||
{
|
||||
<td class="text-muted text-force-break">—</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-muted text-force-break">@attachments</td>
|
||||
}
|
||||
<td class="text-success text-force-break">@weaponHit.KillCount.ToNumericalString()</td>
|
||||
<td class="text-muted text-force-break">@weaponHit.HitCount.ToNumericalString()</td>
|
||||
<td class="text-muted text-force-break">@weaponHit.DamageInflicted.ToNumericalString()</td>
|
||||
<td class="text-muted text-force-break">@TimeSpan.FromSeconds(weaponHit.UsageSeconds ?? 0).HumanizeForCurrentCulture(minUnit: TimeUnit.Second)</td>
|
||||
</tr>
|
||||
}
|
||||
<!-- OVERFLOW -->
|
||||
@foreach (var weaponHit in weapons.Skip(maxItems))
|
||||
{
|
||||
<tr class="bg-dark hidden-row" style="display:none">
|
||||
<td class="@textClass text-force-break">@GetWeaponNameForHit(weaponHit)</td>
|
||||
@{ var attachments = GetWeaponAttachmentName(weaponHit.WeaponAttachmentCombo); }
|
||||
@if (string.IsNullOrWhiteSpace(attachments))
|
||||
{
|
||||
<td class="text-muted text-force-break">—</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-muted text-force-break">@attachments</td>
|
||||
}
|
||||
<td class="text-success text-force-break">@weaponHit.KillCount.ToNumericalString()</td>
|
||||
<td class="text-muted text-force-break">@weaponHit.HitCount.ToNumericalString()</td>
|
||||
<td class="text-muted text-force-break">@weaponHit.DamageInflicted.ToNumericalString()</td>
|
||||
<td class="text-muted text-force-break">@TimeSpan.FromSeconds(weaponHit.UsageSeconds ?? 0).HumanizeForCurrentCulture()</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
</table>
|
||||
<button class="btn @buttonClass btn-block table-slide">
|
||||
<span class="oi oi-chevron-bottom"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- HIT LOCATIONS -->
|
||||
<div class="col-lg-6 col-12 pr-3 pr-lg-0" id="hit_location_table">
|
||||
<div class="@headerClass h4 mb-1 p-2">
|
||||
<div class="text-center">@ViewBag.Localization["WEBFRONT_ADV_STATS_HIT_LOCATIONS"]</div>
|
||||
</div>
|
||||
<table class="table @borderBottomClass bg-dark mb-0 pb-0">
|
||||
<tr class="@headerClass">
|
||||
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_LOCATION"]</th>
|
||||
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_HITS"]</th>
|
||||
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_PERCENTAGE"]</th>
|
||||
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_DAMAGE"]</th>
|
||||
</tr>
|
||||
@{
|
||||
var totalHits = filteredHitLocations.Sum(hit => hit.HitCount);
|
||||
}
|
||||
@foreach (var hitLocation in filteredHitLocations.Take(8))
|
||||
{
|
||||
<tr>
|
||||
<td class="@textClass text-force-break">@config.GetStringForGame(hitLocation.HitLocation.Name)</td>
|
||||
<td class="text-success text-force-break">@hitLocation.HitCount</td>
|
||||
<td class="text-muted text-force-break">@Math.Round((hitLocation.HitCount / (float) totalHits) * 100.0).ToString(Utilities.CurrentLocalization.Culture)%</td>
|
||||
<td class="text-muted text-force-break">@hitLocation.DamageInflicted.ToNumericalString()</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@foreach (var hitLocation in filteredHitLocations.Skip(8))
|
||||
{
|
||||
<tr class="bg-dark hidden-row" style="display:none;">
|
||||
<td class="@textClass text-force-break">@config.GetStringForGame(hitLocation.HitLocation.Name)</td>
|
||||
<td class="text-success text-force-break">@hitLocation.HitCount</td>
|
||||
<td class="text-muted text-force-break">@Math.Round((hitLocation.HitCount / (float) totalHits) * 100.0).ToString(Utilities.CurrentLocalization.Culture)%</td>
|
||||
<td class="text-muted text-force-break">@hitLocation.DamageInflicted.ToNumericalString()</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<button class="btn @buttonClass btn-block table-slide">
|
||||
<span class="oi oi-chevron-bottom"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-lg-6 col-12 pl-3 pl-lg-0">
|
||||
<div class="@borderBottomClass text-center h-100" id="hitlocation_container">
|
||||
<canvas id="hitlocation_model">
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@{
|
||||
var projection = filteredHitLocations.Select(loc => new
|
||||
{
|
||||
name = loc.HitLocation.Name,
|
||||
// we want to count head and neck as the same
|
||||
percentage = (loc.HitLocation.Name == "head"
|
||||
? filteredHitLocations.FirstOrDefault(c => c.HitLocation.Name == "neck")?.HitCount ?? 0 + loc.HitCount
|
||||
: loc.HitCount) / (float) totalHits
|
||||
}).ToList();
|
||||
var maxPercentage = projection.Any() ? projection.Max(p => p.percentage) : 0;
|
||||
}
|
||||
|
||||
@section scripts
|
||||
{
|
||||
<script type="text/javascript">
|
||||
const hitLocationData = @Html.Raw(Json.Serialize(projection));
|
||||
const maxPercentage = @maxPercentage;
|
||||
</script>
|
||||
|
||||
<environment include="Development">
|
||||
<script type="text/javascript" src="~/js/advanced_stats.js"></script>
|
||||
</environment>
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
@using IW4MAdmin.Plugins.Stats
|
||||
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
|
||||
@{
|
||||
Layout = null;
|
||||
var loc = Utilities.CurrentLocalization.LocalizationIndex.Set;
|
||||
double getDeviation(double deviations) => Math.Pow(Math.E, 5.259 + (deviations * 0.812));
|
||||
string rankIcon(double? elo)
|
||||
{
|
||||
if (elo >= getDeviation(-0.75) && elo < getDeviation(1.25))
|
||||
{
|
||||
return "0_no-place/menu_div_no_place.png";
|
||||
}
|
||||
if (elo >= getDeviation(0.125) && elo < getDeviation(0.625))
|
||||
{
|
||||
return "1_iron/menu_div_iron_sub03.png";
|
||||
}
|
||||
if (elo >= getDeviation(0.625) && elo < getDeviation(1.0))
|
||||
{
|
||||
return "2_bronze/menu_div_bronze_sub03.png";
|
||||
}
|
||||
if (elo >= getDeviation(1.0) && elo < getDeviation(1.25))
|
||||
{
|
||||
return "3_silver/menu_div_silver_sub03.png";
|
||||
}
|
||||
if (elo >= getDeviation(1.25) && elo < getDeviation(1.5))
|
||||
{
|
||||
return "4_gold/menu_div_gold_sub03.png";
|
||||
}
|
||||
if (elo >= getDeviation(1.5) && elo < getDeviation(1.75))
|
||||
{
|
||||
return "5_platinum/menu_div_platinum_sub03.png";
|
||||
}
|
||||
if (elo >= getDeviation(1.75) && elo < getDeviation(2.0))
|
||||
{
|
||||
return "6_semipro/menu_div_semipro_sub03.png";
|
||||
}
|
||||
if (elo >= getDeviation(2.0))
|
||||
{
|
||||
return "7_pro/menu_div_pro_sub03.png";
|
||||
}
|
||||
|
||||
return "0_no-place/menu_div_no_place.png";
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.Count == 0)
|
||||
{
|
||||
<div class="p-2 text-center">@Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_NOQUALIFY"]</div>
|
||||
}
|
||||
@foreach (var stat in Model)
|
||||
{
|
||||
<div class="row ml-0 mr-0 pt-2 pb-2">
|
||||
@if (ViewBag.UseNewStats)
|
||||
{
|
||||
<img class="align-self-center d-block d-md-none m-auto pb-3 pt-3" src="~/images/stats/ranks/rank_@(stat.ZScore.RankIconIndexForZScore()).png" alt="@stat.Performance"/>
|
||||
}
|
||||
<div class="col-md-4 text-md-left text-center">
|
||||
<div class="h2 d-flex flex-row justify-content-center justify-content-md-start align-items-center">
|
||||
<div class="text-muted">#@stat.Ranking</div>
|
||||
@if (stat.RatingChange > 0)
|
||||
{
|
||||
<div class="d-flex flex-column text-center pl-1">
|
||||
<div class="oi oi-caret-top text-success client-rating-change-up"></div>
|
||||
<div class="client-rating-change-amount text-success">@stat.RatingChange</div>
|
||||
</div>
|
||||
}
|
||||
@if (stat.RatingChange < 0)
|
||||
{
|
||||
<div class="d-flex flex-column text-center pl-1">
|
||||
<div class="client-rating-change-amount client-rating-change-amount-down text-danger">@Math.Abs(stat.RatingChange)</div>
|
||||
<div class="oi oi-caret-bottom text-danger client-rating-change-down"></div>
|
||||
</div>
|
||||
}
|
||||
<span class="text-muted pr-1 pl-1">–</span>
|
||||
@if (!ViewBag.UseNewStats)
|
||||
{
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@stat.ClientId">
|
||||
<color-code value="@stat.Name" allow="ViewBag.EnableColorCodes"></color-code>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-controller="ClientStatistics" asp-action="Advanced" asp-route-id="@stat.ClientId">
|
||||
<color-code value="@stat.Name" allow="ViewBag.EnableColorCodes"></color-code>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (ViewBag.UseNewStats)
|
||||
{
|
||||
<div class="d-flex flex-column">
|
||||
<div>
|
||||
<span class="text-primary font-weight-bold h5">
|
||||
@stat.Performance.ToNumericalString()
|
||||
</span>
|
||||
@if (stat.ServerId == null)
|
||||
{
|
||||
<span class="text-muted font-weight-bold h5">@loc["WEBFRONT_ADV_STATS_RATING"].FormatExt("").ToLower()</span>
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<span class="text-muted font-weight-bold h5">@loc["WEBFRONT_ADV_STATS_PERFORMANCE"].FormatExt("").ToLower()</span>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-primary">@stat.Kills.ToNumericalString()</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KILLS"]</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-primary">@stat.Deaths.ToNumericalString()</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_DEATHS"]</span><br />
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-primary">@stat.KDR</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KDR"]</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-primary">@stat.TimePlayedValue.HumanizeForCurrentCulture() </span><span class="text-muted">@loc["WEBFRONT_PROFILE_PLAYER"]</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-primary"> @stat.LastSeenValue.HumanizeForCurrentCulture() </span><span class="text-muted">@loc["WEBFRONT_PROFILE_LSEEN"]</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-primary">@stat.Performance</span> <span class="text-muted"> @loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"]</span>
|
||||
<br/>
|
||||
<span class="text-primary">@stat.KDR</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KDR"]</span>
|
||||
<span class="text-primary">@stat.Kills</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KILLS"]</span>
|
||||
<span class="text-primary">@stat.Deaths</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_DEATHS"]</span><br />
|
||||
<span class="text-muted">@loc["WEBFRONT_PROFILE_PLAYER"]</span> <span class="text-primary"> @stat.TimePlayed </span><span class="text-muted">@loc["GLOBAL_TIME_HOURS"]</span><br />
|
||||
<span class="text-muted">@loc["WEBFRONT_PROFILE_LSEEN"]</span><span class="text-primary"> @stat.LastSeen </span><span class="text-muted">@loc["WEBFRONT_PENALTY_TEMPLATE_AGO"]</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 client-rating-graph" id="rating_history_@(stat.ClientId + "_" + stat.Id)" data-history="@Html.Raw(Json.Serialize(stat.PerformanceHistory))">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 client-rating-icon text-md-right text-center align-items-center d-flex justify-content-center">
|
||||
@if (ViewBag.UseNewStats)
|
||||
{
|
||||
<img class="align-self-center d-none d-md-block" src="~/images/stats/ranks/rank_@(stat.ZScore.RankIconIndexForZScore()).png" alt="@stat.Performance"/>
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<img src="/images/icons/@rankIcon(stat.Performance)"/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
34
WebfrontCore/Views/Client/Statistics/Index.cshtml
Normal file
@ -0,0 +1,34 @@
|
||||
<ul class="nav nav-tabs border-top border-bottom nav-fill row" role="tablist" id="stats_top_players">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active top-players-link" href="#server_0" role="tab" data-toggle="tab" aria-selected="true" data-serverid="0">@ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"]</a>
|
||||
</li>
|
||||
|
||||
@foreach (var server in ViewBag.Servers)
|
||||
{
|
||||
<li class="nav-item ">
|
||||
<a class="nav-link top-players-link" href="#server_@server.ID" role="tab" data-toggle="tab" aria-selected="false" data-serverid="@server.ID">
|
||||
<color-code value="@server.Name" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="tab-content border-bottom row">
|
||||
<div role="tabpanel" class="tab-pane active striped flex-fill" id="server_0">
|
||||
@await Component.InvokeAsync("TopPlayers", new { count = 25, offset = 0 })
|
||||
</div>
|
||||
|
||||
@foreach (var server in ViewBag.Servers)
|
||||
{
|
||||
<div role="tabpanel" class="tab-pane striped flex-fill" id="server_@server.ID">
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@section scripts
|
||||
{
|
||||
<environment include="Development">
|
||||
<script type="text/javascript" src="~/js/loader.js"></script>
|
||||
<script type="text/javascript" src="~/js/stats.js"></script>
|
||||
</environment>
|
||||
<script>initLoader('/Stats/GetTopPlayersAsync', '#server_0', 25);</script>
|
||||
}
|
24
WebfrontCore/Views/Client/_MessageContext.cshtml
Normal file
@ -0,0 +1,24 @@
|
||||
@using SharedLibraryCore.Dtos.Meta.Responses
|
||||
@model IList<MessageResponse>
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<div class="client-message-context">
|
||||
<h5 class="bg-primary pt-2 pb-2 pl-3 mb-0 mt-2 text-white">@Model.First().When.ToString()</h5>
|
||||
<div class="bg-dark p-3 mb-2 border-bottom">
|
||||
@foreach (var message in Model)
|
||||
{
|
||||
<span class="text-white">
|
||||
<color-code value="@message.ClientName" allow="ViewBag.EnableColorCodes"></color-code>
|
||||
</span>
|
||||
<span>
|
||||
—
|
||||
<span class="@(message.IsQuickMessage ? "font-italic" : "")">
|
||||
<color-code value="@(message.IsHidden ? message.HiddenMessage : message.Message)" allow="ViewBag.EnableColorCodes"></color-code>
|
||||
</span>
|
||||
</span>
|
||||
<br />
|
||||
}
|
||||
</div>
|
||||
</div>
|
22
WebfrontCore/Views/Client/_PenaltyInfo.cshtml
Normal file
@ -0,0 +1,22 @@
|
||||
@model IEnumerable<Data.Models.Client.Stats.EFACSnapshot>
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<div class="penalty-info-context bg-dark p-2 mt-2 mb-2 border-top border-bottom">
|
||||
@foreach (var snapshot in Model)
|
||||
{
|
||||
<!-- this is not ideal, but I didn't want to manually write out all the properties-->
|
||||
var snapProperties = Model.First().GetType().GetProperties();
|
||||
foreach (var prop in snapProperties)
|
||||
{
|
||||
@if ((prop.Name.EndsWith("Id") && prop.Name != "WeaponId") || new[] { "Active", "Client", "PredictedViewAngles" }.Contains(prop.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
<span class="text-white">@prop.Name </span> <span>— @prop.GetValue(snapshot).ToString()</span><br />
|
||||
}
|
||||
<div class="w-100 mt-1 mb-1 border-bottom"></div>
|
||||
}
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
@model SharedLibraryCore.Database.Models.EFPenalty.PenaltyType
|
||||
@model Data.Models.EFPenalty.PenaltyType
|
||||
@{
|
||||
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
|
||||
}
|
||||
@ -7,11 +7,11 @@
|
||||
<div class="d-block d-md-flex w-100 pb-2">
|
||||
<select class="form-control bg-dark text-muted" id="penalty_filter_selection">
|
||||
@{
|
||||
foreach (var penaltyType in Enum.GetValues(typeof(SharedLibraryCore.Database.Models.EFPenalty.PenaltyType)))
|
||||
foreach (var penaltyType in Enum.GetValues(typeof(Data.Models.EFPenalty.PenaltyType)))
|
||||
{
|
||||
if ((SharedLibraryCore.Database.Models.EFPenalty.PenaltyType)penaltyType == SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Any)
|
||||
if ((Data.Models.EFPenalty.PenaltyType)penaltyType == Data.Models.EFPenalty.PenaltyType.Any)
|
||||
{
|
||||
if (Model == SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Any)
|
||||
if (Model == Data.Models.EFPenalty.PenaltyType.Any)
|
||||
{
|
||||
<option value="@Convert.ToInt32(penaltyType)" selected="selected" )>@loc["WEBFRONT_PENALTY_TEMPLATE_SHOW"] @penaltyType.ToString()</option>
|
||||
}
|
||||
@ -22,7 +22,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((SharedLibraryCore.Database.Models.EFPenalty.PenaltyType)penaltyType == Model)
|
||||
if ((Data.Models.EFPenalty.PenaltyType)penaltyType == Model)
|
||||
{
|
||||
<option value="@Convert.ToInt32(penaltyType)" selected="selected">@loc["WEBFRONT_PENALTY_TEMPLATE_SHOWONLY"] @penaltyType.ToString()s</option>
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html xmlns="http://www.w3.org/1999/html">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
@ -159,6 +159,7 @@
|
||||
<script type="text/javascript" src="~/lib/moment-timezone/moment-timezone.js"></script>
|
||||
<script type="text/javascript" src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
|
||||
<script type="text/javascript" src="~/lib/canvas.js/canvasjs.js"></script>
|
||||
<script type="text/javascript" src="~/lib/chart.js/dist/Chart.bundle.min.js"></script>
|
||||
<script type="text/javascript" src="~/js/action.js"></script>
|
||||
<script type="text/javascript" src="~/js/search.js"></script>
|
||||
</environment>
|
||||
@ -172,6 +173,6 @@
|
||||
_localization[key] = value;
|
||||
});
|
||||
</script>
|
||||
@RenderSection("scripts", required: false)
|
||||
@await RenderSectionAsync("scripts", required: false)
|
||||
</body>
|
||||
</html>
|
@ -5,7 +5,7 @@
|
||||
<RazorCompileOnBuild Condition="'$(CONFIG)'!='Debug'">true</RazorCompileOnBuild>
|
||||
<RazorCompiledOnPublish Condition="'$(CONFIG)'!='Debug'">true</RazorCompiledOnPublish>
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
<PreserveCompilationContext Condition="'$(CONFIG)'!='Debug'">false</PreserveCompilationContext>
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
<TypeScriptToolsVersion>2.6</TypeScriptToolsVersion>
|
||||
<PackageId>RaidMax.IW4MAdmin.WebfrontCore</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
@ -30,29 +30,11 @@
|
||||
<TieredCompilation>true</TieredCompilation>
|
||||
<LangVersion>Latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="bower.json" />
|
||||
<Content Remove="bundleconfig.json" />
|
||||
<Content Remove="compilerconfig.json" />
|
||||
<Content Remove="Views\Plugins\Stats\Components\TopPlayers\_List.cshtml" />
|
||||
<Content Remove="Views\Plugins\Stats\Index.cshtml" />
|
||||
<Content Remove="Views\Plugins\Stats\_MessageContext.cshtml" />
|
||||
<Content Remove="Views\Plugins\Stats\_PenaltyInfo.cshtml" />
|
||||
<Content Remove="Views\Plugins\_ViewImports.cshtml" />
|
||||
<Content Remove="Views\Plugins\**" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="bundleconfig.json" />
|
||||
<None Include="compilerconfig.json" />
|
||||
<None Include="Views\Plugins\Stats\Components\TopPlayers\_List.cshtml" />
|
||||
<None Include="Views\Plugins\Stats\Index.cshtml" />
|
||||
<None Include="Views\Plugins\Stats\_MessageContext.cshtml" />
|
||||
<None Include="Views\Plugins\Stats\_PenaltyInfo.cshtml" />
|
||||
<None Include="Views\Plugins\_ViewImports.cshtml" />
|
||||
<None Include="wwwroot\css\src\global.css" />
|
||||
<None Remove="Views\Plugins\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -63,6 +45,7 @@
|
||||
<None Include="wwwroot\css\global.min.css" CopyToPublishDirectory="PreserveNewest" />
|
||||
<None Include="wwwroot\js\global.min.js" CopyToPublishDirectory="PreserveNewest" />
|
||||
<None Include="wwwroot\images\**\*.*" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Remove="wwwroot\images\icons\crosshair.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -85,7 +68,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Plugins\Web\StatsWeb\StatsWeb.csproj" />
|
||||
<ProjectReference Include="..\Plugins\Stats\Stats.csproj" />
|
||||
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -5,7 +5,8 @@
|
||||
"outputFileName": "wwwroot/css/global.min.css",
|
||||
// An array of relative input file paths. Globbing patterns supported
|
||||
"inputFiles": [
|
||||
"wwwroot/css/global.css"
|
||||
"wwwroot/css/global.css",
|
||||
"wwwroot/lib/chart.js/dist/Chart.min.css"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -17,6 +18,7 @@
|
||||
"wwwroot/lib/moment-timezone/moment-timezone.min.js",
|
||||
"wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js",
|
||||
"wwwroot/lib/canvas.js/canvasjs.js",
|
||||
"wwwroot/lib/chart.js/dist/Chart.bundle.min.js",
|
||||
"wwwroot/js/action.js",
|
||||
"wwwroot/js/console.js",
|
||||
"wwwroot/js/penalty.js",
|
||||
@ -25,7 +27,8 @@
|
||||
"wwwroot/js/search.js",
|
||||
"wwwroot/js/loader.js",
|
||||
"wwwroot/js/stats.js",
|
||||
"wwwroot/js/configuration.js"
|
||||
"wwwroot/js/configuration.js",
|
||||
"wwwroot/js/advanced_stats.js"
|
||||
],
|
||||
// Optionally specify minification options
|
||||
"minify": {
|
||||
|
@ -26,6 +26,10 @@
|
||||
{
|
||||
"library": "moment@2.24.0",
|
||||
"destination": "wwwroot/lib/moment.js/"
|
||||
},
|
||||
{
|
||||
"library": "chart.js@2.9.4",
|
||||
"destination": "wwwroot/lib/chart.js"
|
||||
}
|
||||
]
|
||||
}
|
@ -103,7 +103,7 @@ a.link-inverse:hover {
|
||||
border-bottom-color: $orange;
|
||||
}
|
||||
|
||||
form *, select {
|
||||
form *, select, button.btn {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
@ -140,7 +140,6 @@ form *, select {
|
||||
z-index: 100;
|
||||
font-size: 4rem;
|
||||
-webkit-animation: rotation 1s infinite linear;
|
||||
background-color: $black;
|
||||
background-color: rgba(0, 0,0, 0.5);
|
||||
border-radius: 40px;
|
||||
padding: 5px;
|
||||
@ -209,7 +208,6 @@ form *, select {
|
||||
}
|
||||
|
||||
.nav-tabs, .nav-tabs .nav-link.active {
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
border: none;
|
||||
}
|
||||
@ -400,3 +398,42 @@ input:checked + .toggle-switch-slider:before {
|
||||
color: #ff6060 !important;
|
||||
color: rgba(255, 69, 69, 0.85) !important;
|
||||
}
|
||||
|
||||
.text-force-break
|
||||
{
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
div.card {
|
||||
min-width: 15rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
max-width: 15rem;
|
||||
}
|
||||
|
||||
#rank_icon {
|
||||
max-height: 6rem;
|
||||
}
|
||||
|
||||
.border-bottom-danger {
|
||||
border-bottom: $danger;
|
||||
}
|
||||
|
||||
.border-top-danger {
|
||||
border-top: $danger;
|
||||
}
|
||||
|
||||
.border-danger {
|
||||
border: 1px solid $danger;
|
||||
}
|
||||
|
||||
#client_stats_summary a:hover {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
#hitlocation_container {
|
||||
background-color: #141414;
|
||||
}
|
||||
|
BIN
WebfrontCore/wwwroot/images/stats/hit_location_model.png
Normal file
After Width: | Height: | Size: 178 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_0.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_1.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_10.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_11.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_12.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_13.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_14.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_15.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_16.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_17.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_18.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_19.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_2.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_20.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_21.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_22.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_23.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_24.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_3.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_4.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_5.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_6.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_7.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_8.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_9.png
Normal file
After Width: | Height: | Size: 19 KiB |
406
WebfrontCore/wwwroot/js/advanced_stats.js
Normal file
@ -0,0 +1,406 @@
|
||||
window.onresize = function () {
|
||||
drawPlayerModel();
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.table-slide').click(function () {
|
||||
$(this).siblings().children().children('.hidden-row').slideToggle(0);
|
||||
$(this).children('span').toggleClass('oi-chevron-top oi-chevron-bottom');
|
||||
});
|
||||
setupPerformanceGraph();
|
||||
drawPlayerModel();
|
||||
})
|
||||
|
||||
function setupPerformanceGraph() {
|
||||
const summary = $('#client_stats_summary');
|
||||
if (summary === undefined) {
|
||||
return;
|
||||
}
|
||||
const chart = $('#client_performance_history');
|
||||
const container = $('#client_performance_history_container');
|
||||
chart.attr('height', summary.height());
|
||||
chart.attr('width', container.width());
|
||||
renderPerformanceChart();
|
||||
}
|
||||
|
||||
function drawPlayerModel() {
|
||||
const canvas = document.getElementById('hitlocation_model');
|
||||
if (canvas === null) {
|
||||
return;
|
||||
}
|
||||
const context = canvas.getContext('2d');
|
||||
const container = $('#hitlocation_container');
|
||||
const background = new Image();
|
||||
background.onload = () => {
|
||||
const backgroundRatioX = background.width / background.height;
|
||||
|
||||
canvas.height = container.height() - 28;
|
||||
canvas.width = (canvas.height * backgroundRatioX);
|
||||
|
||||
const scalar = canvas.height / background.height;
|
||||
|
||||
drawHitLocationChart(context, background, scalar, canvas.width, canvas.height);
|
||||
}
|
||||
background.src = '/images/stats/hit_location_model.png';
|
||||
}
|
||||
|
||||
function buildHitLocationPosition() {
|
||||
let hitLocations = {}
|
||||
hitLocations['head'] = {
|
||||
x: 454.5,
|
||||
y: 108.5,
|
||||
width: 157,
|
||||
height: 217
|
||||
}
|
||||
|
||||
hitLocations['torso_upper'] = {
|
||||
x: 457,
|
||||
y: 318,
|
||||
width: 254,
|
||||
height: 202
|
||||
}
|
||||
|
||||
hitLocations['torso_lower'] = {
|
||||
x: 456.50,
|
||||
y: 581,
|
||||
width: 315,
|
||||
height: 324
|
||||
}
|
||||
|
||||
hitLocations['right_leg_upper'] = {
|
||||
x: 527.5,
|
||||
y: 856.7,
|
||||
width: 149,
|
||||
height: 228
|
||||
}
|
||||
|
||||
hitLocations['right_leg_lower'] = {
|
||||
x: 542,
|
||||
y: 1077.6,
|
||||
width: 120,
|
||||
height: 214
|
||||
}
|
||||
|
||||
hitLocations['right_foot'] = {
|
||||
x: 558.5,
|
||||
y: 1253.5,
|
||||
width: 93,
|
||||
height: 138
|
||||
}
|
||||
|
||||
hitLocations['left_leg_upper'] = {
|
||||
x: 382.5,
|
||||
y: 857,
|
||||
width: 141,
|
||||
height: 228
|
||||
}
|
||||
|
||||
hitLocations['left_leg_lower'] = {
|
||||
x: 371.5,
|
||||
y: 1078,
|
||||
width: 119,
|
||||
height: 214
|
||||
}
|
||||
|
||||
hitLocations['left_foot'] = {
|
||||
x: 353,
|
||||
y: 1254,
|
||||
width: 90,
|
||||
height: 138
|
||||
}
|
||||
|
||||
hitLocations['left_arm_upper'] = {
|
||||
p1: {
|
||||
x: 330,
|
||||
y: 218
|
||||
},
|
||||
p2: {
|
||||
x: 330,
|
||||
y: 400
|
||||
},
|
||||
p3: {
|
||||
x: 255,
|
||||
y: 475
|
||||
},
|
||||
p4: {
|
||||
x: 165,
|
||||
y: 375
|
||||
},
|
||||
type: 'polygon'
|
||||
}
|
||||
|
||||
hitLocations['right_arm_upper'] = {
|
||||
p1: {
|
||||
x: 584,
|
||||
y: 218
|
||||
},
|
||||
p2: {
|
||||
x: 584,
|
||||
y: 400
|
||||
},
|
||||
p3: {
|
||||
x: 659,
|
||||
y: 475
|
||||
},
|
||||
p4: {
|
||||
x: 749,
|
||||
y: 375
|
||||
},
|
||||
type: 'polygon'
|
||||
}
|
||||
|
||||
hitLocations['left_arm_lower'] = {
|
||||
p1: {
|
||||
x: 165,
|
||||
y: 375
|
||||
},
|
||||
p2: {
|
||||
x: 255,
|
||||
y: 475
|
||||
},
|
||||
p3: {
|
||||
x: 121,
|
||||
y: 584
|
||||
},
|
||||
p4: {
|
||||
x: 30,
|
||||
y: 512
|
||||
},
|
||||
type: 'polygon'
|
||||
}
|
||||
|
||||
hitLocations['right_arm_lower'] = {
|
||||
p1: {
|
||||
x: 749,
|
||||
y: 375
|
||||
},
|
||||
p2: {
|
||||
x: 659,
|
||||
y: 475
|
||||
},
|
||||
p3: {
|
||||
x: 789,
|
||||
y: 587
|
||||
},
|
||||
p4: {
|
||||
x: 876,
|
||||
y: 497
|
||||
},
|
||||
type: 'polygon'
|
||||
}
|
||||
|
||||
hitLocations['left_hand'] = {
|
||||
p1: {
|
||||
x: 30,
|
||||
y: 512
|
||||
},
|
||||
p2: {
|
||||
x: 121,
|
||||
y: 584
|
||||
},
|
||||
p3: {
|
||||
x: 0,
|
||||
y: 669
|
||||
},
|
||||
p4: {
|
||||
x: 0,
|
||||
y: 582
|
||||
},
|
||||
type: 'polygon'
|
||||
}
|
||||
|
||||
hitLocations['right_hand'] = {
|
||||
p1: {
|
||||
x: 789,
|
||||
y: 587
|
||||
},
|
||||
p2: {
|
||||
x: 876,
|
||||
y: 497
|
||||
},
|
||||
p3: {
|
||||
x: 905,
|
||||
y: 534
|
||||
},
|
||||
p4: {
|
||||
x: 905,
|
||||
y: 666
|
||||
},
|
||||
type: 'polygon'
|
||||
}
|
||||
return hitLocations;
|
||||
}
|
||||
|
||||
function drawHitLocationChart(context, background, scalar, width, height) {
|
||||
context.drawImage(background, 0, 0, background.width, background.height, 0, 0, width, height);
|
||||
|
||||
const hitLocations = buildHitLocationPosition();
|
||||
|
||||
$.each(hitLocationData, (index, hit) => {
|
||||
let scaledPercentage = hit.percentage / maxPercentage;
|
||||
let red;
|
||||
let green = 255;
|
||||
|
||||
if (scaledPercentage < 0.5) {
|
||||
red = Math.round(scaledPercentage * 255 * 2);
|
||||
} else {
|
||||
red = 255;
|
||||
green = Math.round((1 - scaledPercentage) * 255 * 2);
|
||||
}
|
||||
|
||||
red = red.toString(16).padStart(2, '0');
|
||||
green = green.toString(16).padStart(2, '0');
|
||||
|
||||
const color = '#' + red + green + '0077';
|
||||
const location = hitLocations[hit.name];
|
||||
|
||||
if (location.type === 'polygon') {
|
||||
drawPolygon(context, scalar, location.p1, location.p2, location.p3, location.p4, color);
|
||||
} else {
|
||||
drawRectangle(context, scalar, location.x, location.y, location.width, location.height, color);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawRectangle(context, scalar, x, y, width, height, color) {
|
||||
const scaledRectWidth = width * scalar;
|
||||
const scaledRectHeight = height * scalar;
|
||||
const rectX = x * scalar - (scaledRectWidth / 2);
|
||||
const rectY = y * scalar - (scaledRectHeight / 2);
|
||||
context.beginPath();
|
||||
context.fillStyle = color
|
||||
context.fillRect(rectX, rectY, scaledRectWidth, scaledRectHeight);
|
||||
context.closePath();
|
||||
}
|
||||
|
||||
function drawPolygon(context, scalar, p1, p2, p3, p4, color) {
|
||||
|
||||
const points = [p1, p2, p3, p4];
|
||||
|
||||
$.each(points, (index, point) => {
|
||||
point.x = point.x * scalar;
|
||||
point.y = point.y * scalar;
|
||||
});
|
||||
|
||||
context.beginPath();
|
||||
context.fillStyle = color;
|
||||
context.moveTo(p1.x, p1.y);
|
||||
context.lineTo(p2.x, p2.y);
|
||||
context.lineTo(p3.x, p3.y);
|
||||
context.lineTo(p4.x, p4.y);
|
||||
context.fill();
|
||||
context.closePath();
|
||||
}
|
||||
|
||||
function getClosestMultiple(baseValue, value) {
|
||||
return Math.round(value / baseValue) * baseValue;
|
||||
}
|
||||
|
||||
function renderPerformanceChart() {
|
||||
const id = 'client_performance_history';
|
||||
const data = $('#' + id).data('history');
|
||||
|
||||
if (data === undefined) {
|
||||
return;
|
||||
}
|
||||
if (data.length <= 1) {
|
||||
// only 0 perf
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = [];
|
||||
data.forEach(function (item, i) {
|
||||
labels.push(i);
|
||||
});
|
||||
|
||||
const padding = 4;
|
||||
let dataMin = Math.min(...data);
|
||||
const dataMax = Math.max(...data);
|
||||
|
||||
if (dataMax - dataMin === 0) {
|
||||
dataMin = 0;
|
||||
}
|
||||
|
||||
dataMin = Math.max(0, dataMin);
|
||||
|
||||
const min = getClosestMultiple(padding, dataMin - padding);
|
||||
const max = getClosestMultiple(padding, dataMax + padding);
|
||||
|
||||
const chartData = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: data,
|
||||
pointBackgroundColor: 'rgba(255, 255, 255, 0)',
|
||||
pointBorderColor: 'rgba(255, 255, 255, 0)',
|
||||
pointHoverRadius: 5,
|
||||
pointHoverBackgroundColor: 'rgba(255, 255, 255, 1)',
|
||||
}]
|
||||
};
|
||||
|
||||
const options = {
|
||||
defaultFontFamily: '-apple-system, BlinkMacSystemFont, "Open Sans", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
legend: false,
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: (tooltipItem) => Math.round(tooltipItem.yLabel) + ' ' + _localization["PLUGINS_STATS_COMMANDS_PERFORMANCE"],
|
||||
title: () => ''
|
||||
},
|
||||
mode: 'nearest',
|
||||
intersect: false,
|
||||
animationDuration: 0,
|
||||
cornerRadius: 0,
|
||||
displayColors: false
|
||||
},
|
||||
hover: {
|
||||
mode: 'nearest',
|
||||
intersect: false
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
fill: false,
|
||||
borderColor: 'rgba(255, 255, 255, 0.75)',
|
||||
borderWidth: 2
|
||||
},
|
||||
point: {
|
||||
radius: 5
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: false,
|
||||
}],
|
||||
yAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
|
||||
position: 'right',
|
||||
ticks: {
|
||||
callback: function (value, index, values) {
|
||||
if (index === values.length - 1) {
|
||||
return min;
|
||||
} else if (index === 0) {
|
||||
return max;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
fontColor: 'rgba(255, 255, 255, 0.25)'
|
||||
}
|
||||
}]
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
left: 15
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
new Chart(id, {
|
||||
type: 'line',
|
||||
data: chartData,
|
||||
options: options
|
||||
});
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
function getStatsChart(id, width, height) {
|
||||
const data = $('#' + id).data('history');
|
||||
|
||||
if (data === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fixedData = [];
|
||||
data.forEach(function (item, i) {
|
||||
fixedData[i] = { x: i, y: Math.floor(item) };
|
||||
@ -12,7 +17,7 @@
|
||||
dataMin = 0;
|
||||
}
|
||||
|
||||
const padding = (dataMax - dataMin) * 0.075;
|
||||
const padding = (dataMax - dataMin) * 0.5;
|
||||
const min = Math.max(0, dataMin - padding);
|
||||
const max = dataMax + padding;
|
||||
let interval = Math.floor((max - min) / 2);
|
||||
@ -27,12 +32,11 @@
|
||||
animationEnabled: false,
|
||||
toolTip: {
|
||||
contentFormatter: function (e) {
|
||||
return Math.round(e.entries[0].dataPoint.y, 1);
|
||||
return `${_localization['WEBFRONT_ADV_STATS_RANKING_METRIC']} ${Math.round(e.entries[0].dataPoint.y, 1)}`;
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: _localization['WEBFRONT_STATS_PERFORMANCE_HISTORY'],
|
||||
fontSize: 14
|
||||
fontSize: 0
|
||||
},
|
||||
axisX: {
|
||||
gridThickness: 0,
|
||||
|