add "advanced" search functionality
This commit is contained in:
parent
c89314667c
commit
ba40478d11
@ -21,6 +21,7 @@
|
|||||||
<Win32Resource />
|
<Win32Resource />
|
||||||
<RootNamespace>IW4MAdmin.Application</RootNamespace>
|
<RootNamespace>IW4MAdmin.Application</RootNamespace>
|
||||||
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
|
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -30,6 +30,7 @@ using Integrations.Source.Extensions;
|
|||||||
using IW4MAdmin.Application.Alerts;
|
using IW4MAdmin.Application.Alerts;
|
||||||
using IW4MAdmin.Application.Extensions;
|
using IW4MAdmin.Application.Extensions;
|
||||||
using IW4MAdmin.Application.Localization;
|
using IW4MAdmin.Application.Localization;
|
||||||
|
using IW4MAdmin.Application.QueryHelpers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
|
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
|
||||||
@ -38,6 +39,7 @@ using Stats.Client.Abstractions;
|
|||||||
using Stats.Client;
|
using Stats.Client;
|
||||||
using Stats.Config;
|
using Stats.Config;
|
||||||
using Stats.Helpers;
|
using Stats.Helpers;
|
||||||
|
using WebfrontCore.QueryHelpers.Models;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
{
|
{
|
||||||
@ -431,6 +433,7 @@ namespace IW4MAdmin.Application
|
|||||||
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
|
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
|
||||||
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>, ConnectionsResourceQueryHelper>()
|
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>, ConnectionsResourceQueryHelper>()
|
||||||
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse>, PermissionLevelChangedResourceQueryHelper>()
|
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse>, PermissionLevelChangedResourceQueryHelper>()
|
||||||
|
.AddSingleton<IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>, ClientResourceQueryHelper>()
|
||||||
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
|
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
|
||||||
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
|
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
|
||||||
.AddSingleton<IMasterCommunication, MasterCommunication>()
|
.AddSingleton<IMasterCommunication, MasterCommunication>()
|
||||||
|
@ -22,7 +22,7 @@ public class GeoLocationService : IGeoLocationService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var reader = new DatabaseReader(_sourceAddress);
|
using var reader = new DatabaseReader(_sourceAddress);
|
||||||
reader.TryCountry(address, out country);
|
country = reader.Country(address);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
290
Application/QueryHelpers/ClientResourceQueryHelper.cs
Normal file
290
Application/QueryHelpers/ClientResourceQueryHelper.cs
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Data.Abstractions;
|
||||||
|
using Data.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Dtos;
|
||||||
|
using SharedLibraryCore.Helpers;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using WebfrontCore.QueryHelpers.Models;
|
||||||
|
using EFClient = Data.Models.Client.EFClient;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.QueryHelpers;
|
||||||
|
|
||||||
|
public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>
|
||||||
|
{
|
||||||
|
private readonly IDatabaseContextFactory _contextFactory;
|
||||||
|
private readonly IGeoLocationService _geoLocationService;
|
||||||
|
|
||||||
|
private class ClientAlias
|
||||||
|
{
|
||||||
|
public EFClient Client { get; set; }
|
||||||
|
public EFAlias Alias { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientResourceQueryHelper(IDatabaseContextFactory contextFactory, IGeoLocationService geoLocationService)
|
||||||
|
{
|
||||||
|
_contextFactory = contextFactory;
|
||||||
|
_geoLocationService = geoLocationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ResourceQueryHelperResult<ClientResourceResponse>> QueryResource(ClientResourceRequest query)
|
||||||
|
{
|
||||||
|
await using var context = _contextFactory.CreateContext(false);
|
||||||
|
var iqAliases = context.Aliases.AsQueryable();
|
||||||
|
var iqClients = context.Clients.AsQueryable();
|
||||||
|
|
||||||
|
var iqClientAliases = iqClients.Join(iqAliases, client => client.AliasLinkId, alias => alias.LinkId,
|
||||||
|
(client, alias) => new ClientAlias { Client = client, Alias = alias });
|
||||||
|
|
||||||
|
return await StartFromClient(query, iqClientAliases, iqClients);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ResourceQueryHelperResult<ClientResourceResponse>> StartFromClient(ClientResourceRequest query,
|
||||||
|
IQueryable<ClientAlias> clientAliases, IQueryable<EFClient> iqClients)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.ClientGuid))
|
||||||
|
{
|
||||||
|
clientAliases = SearchByGuid(query, clientAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.ClientLevel is not null)
|
||||||
|
{
|
||||||
|
clientAliases = SearchByLevel(query, clientAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.ClientConnected is not null)
|
||||||
|
{
|
||||||
|
clientAliases = SearchByLastConnection(query, clientAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.GameName is not null)
|
||||||
|
{
|
||||||
|
clientAliases = SearchByGame(query, clientAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.ClientName))
|
||||||
|
{
|
||||||
|
clientAliases = SearchByName(query, clientAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.ClientIp))
|
||||||
|
{
|
||||||
|
clientAliases = SearchByIp(query, clientAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
var iqGroupedClientAliases = clientAliases.GroupBy(a => new { a.Client.ClientId, a.Client.LastConnection });
|
||||||
|
|
||||||
|
iqGroupedClientAliases = query.Direction == SortDirection.Descending
|
||||||
|
? iqGroupedClientAliases.OrderByDescending(clientAlias => clientAlias.Key.LastConnection)
|
||||||
|
: iqGroupedClientAliases.OrderBy(clientAlias => clientAlias.Key.LastConnection);
|
||||||
|
|
||||||
|
var clientIds = iqGroupedClientAliases.Select(g => g.Key.ClientId)
|
||||||
|
.Skip(query.Offset)
|
||||||
|
.Take(query.Count);
|
||||||
|
|
||||||
|
// this pulls in more records than we need, but it's more efficient than ordering grouped entities
|
||||||
|
var clientLookups = await clientAliases
|
||||||
|
.Where(clientAlias => clientIds.Contains(clientAlias.Client.ClientId))
|
||||||
|
.Select(clientAlias => new ClientResourceResponse
|
||||||
|
{
|
||||||
|
ClientId = clientAlias.Client.ClientId,
|
||||||
|
AliasId = clientAlias.Alias.AliasId,
|
||||||
|
LinkId = clientAlias.Client.AliasLinkId,
|
||||||
|
CurrentClientName = clientAlias.Client.CurrentAlias.Name,
|
||||||
|
MatchedClientName = clientAlias.Alias.Name,
|
||||||
|
CurrentClientIp = clientAlias.Client.CurrentAlias.IPAddress,
|
||||||
|
MatchedClientIp = clientAlias.Alias.IPAddress,
|
||||||
|
ClientLevel = clientAlias.Client.Level.ToLocalizedLevelName(),
|
||||||
|
ClientLevelValue = clientAlias.Client.Level,
|
||||||
|
LastConnection = clientAlias.Client.LastConnection,
|
||||||
|
Game = clientAlias.Client.GameName
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var groupClients = clientLookups.GroupBy(x => x.ClientId);
|
||||||
|
|
||||||
|
var orderedClients = query.Direction == SortDirection.Descending
|
||||||
|
? groupClients.OrderByDescending(SearchByAliasLocal(query.ClientName, query.ClientIp))
|
||||||
|
: groupClients.OrderBy(SearchByAliasLocal(query.ClientName, query.ClientIp));
|
||||||
|
|
||||||
|
var clients = orderedClients.Select(client => client.First()).ToList();
|
||||||
|
await ProcessAliases(query, clients);
|
||||||
|
|
||||||
|
return new ResourceQueryHelperResult<ClientResourceResponse>
|
||||||
|
{
|
||||||
|
Results = clients
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessAliases(ClientResourceRequest query, IEnumerable<ClientResourceResponse> clients)
|
||||||
|
{
|
||||||
|
await Parallel.ForEachAsync(clients, new ParallelOptions { MaxDegreeOfParallelism = 15 },
|
||||||
|
async (client, token) =>
|
||||||
|
{
|
||||||
|
if (!query.IncludeGeolocationData || client.CurrentClientIp is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var geolocationData = await _geoLocationService.Locate(client.CurrentClientIp.ConvertIPtoString());
|
||||||
|
client.ClientCountryCode = geolocationData.CountryCode;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(client.ClientCountryCode))
|
||||||
|
{
|
||||||
|
client.ClientCountryDisplayName = geolocationData.Country;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<IGrouping<int, ClientResourceResponse>, DateTime> SearchByAliasLocal(string? clientName,
|
||||||
|
string? ipAddress)
|
||||||
|
{
|
||||||
|
return group =>
|
||||||
|
{
|
||||||
|
ClientResourceResponse? match = null;
|
||||||
|
var lowercaseClientName = clientName?.ToLower();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(lowercaseClientName))
|
||||||
|
{
|
||||||
|
match = group.ToList().FirstOrDefault(SearchByNameLocal(lowercaseClientName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match is null && !string.IsNullOrWhiteSpace(ipAddress))
|
||||||
|
{
|
||||||
|
match = group.ToList().FirstOrDefault(SearchByIpLocal(ipAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (match ?? group.First()).LastConnection;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<ClientResourceResponse, bool> SearchByNameLocal(string clientName)
|
||||||
|
{
|
||||||
|
return clientResourceResponse =>
|
||||||
|
clientResourceResponse.MatchedClientName.Contains(clientName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<ClientResourceResponse, bool> SearchByIpLocal(string clientIp)
|
||||||
|
{
|
||||||
|
return clientResourceResponse => clientResourceResponse.MatchedClientIp.ConvertIPtoString().Contains(clientIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ClientAlias> SearchByName(ClientResourceRequest query,
|
||||||
|
IQueryable<ClientAlias> clientAliases)
|
||||||
|
{
|
||||||
|
var lowerCaseQueryName = query.ClientName.ToLower();
|
||||||
|
|
||||||
|
clientAliases = clientAliases.Where(query.IsExactClientName
|
||||||
|
? ExactNameMatch(lowerCaseQueryName)
|
||||||
|
: LikeNameMatch(lowerCaseQueryName));
|
||||||
|
|
||||||
|
return clientAliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression<Func<ClientAlias, bool>> LikeNameMatch(string lowerCaseQueryName)
|
||||||
|
{
|
||||||
|
return clientAlias => EF.Functions.Like(
|
||||||
|
clientAlias.Alias.SearchableName,
|
||||||
|
$"%{lowerCaseQueryName}%") || EF.Functions.Like(
|
||||||
|
clientAlias.Alias.Name.ToLower(),
|
||||||
|
$"%{lowerCaseQueryName}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression<Func<ClientAlias, bool>> ExactNameMatch(string lowerCaseQueryName)
|
||||||
|
{
|
||||||
|
return clientAlias =>
|
||||||
|
lowerCaseQueryName == clientAlias.Alias.Name || lowerCaseQueryName == clientAlias.Alias.SearchableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ClientAlias> SearchByIp(ClientResourceRequest query,
|
||||||
|
IQueryable<ClientAlias> clientAliases)
|
||||||
|
{
|
||||||
|
var ipString = query.ClientIp.Trim();
|
||||||
|
var ipAddress = ipString.ConvertToIP();
|
||||||
|
|
||||||
|
if (ipAddress != null && ipString.Split('.').Length == 4 && query.IsExactClientIp)
|
||||||
|
{
|
||||||
|
clientAliases = clientAliases.Where(clientAlias =>
|
||||||
|
clientAlias.Alias.IPAddress != null && clientAlias.Alias.IPAddress == ipAddress);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
clientAliases = clientAliases.Where(clientAlias =>
|
||||||
|
EF.Functions.Like(clientAlias.Alias.SearchableIPAddress, $"{ipString}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientAliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ClientAlias> SearchByGuid(ClientResourceRequest query,
|
||||||
|
IQueryable<ClientAlias> clients)
|
||||||
|
{
|
||||||
|
var guidString = query.ClientGuid.Trim();
|
||||||
|
var parsedGuids = new List<long>();
|
||||||
|
long guid = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
guid = guidString.ConvertGuidToLong(NumberStyles.HexNumber, false, 0);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guid != 0)
|
||||||
|
{
|
||||||
|
parsedGuids.Add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
guid = guidString.ConvertGuidToLong(NumberStyles.Integer, false, 0);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guid != 0)
|
||||||
|
{
|
||||||
|
parsedGuids.Add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsedGuids.Any())
|
||||||
|
{
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
clients = clients.Where(client => parsedGuids.Contains(client.Client.NetworkId));
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ClientAlias> SearchByLevel(ClientResourceRequest query, IQueryable<ClientAlias> clients)
|
||||||
|
{
|
||||||
|
clients = clients.Where(clientAlias => clientAlias.Client.Level == query.ClientLevel);
|
||||||
|
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ClientAlias> SearchByLastConnection(ClientResourceRequest query,
|
||||||
|
IQueryable<ClientAlias> clients)
|
||||||
|
{
|
||||||
|
clients = clients.Where(clientAlias => clientAlias.Client.LastConnection >= query.ClientConnected);
|
||||||
|
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ClientAlias> SearchByGame(ClientResourceRequest query, IQueryable<ClientAlias> clients)
|
||||||
|
{
|
||||||
|
clients = clients.Where(clientAlias => clientAlias.Client.GameName == query.GameName);
|
||||||
|
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ namespace SharedLibraryCore.Dtos
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// how many items to take
|
/// how many items to take
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Count { get; set; } = 100;
|
public int Count { get; set; } = 30;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// filter query
|
/// filter query
|
||||||
@ -28,6 +28,8 @@ namespace SharedLibraryCore.Dtos
|
|||||||
public SortDirection Direction { get; set; } = SortDirection.Descending;
|
public SortDirection Direction { get; set; } = SortDirection.Descending;
|
||||||
|
|
||||||
public DateTime? Before { get; set; }
|
public DateTime? Before { get; set; }
|
||||||
|
|
||||||
|
public DateTime? After { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SortDirection
|
public enum SortDirection
|
||||||
|
@ -26,6 +26,7 @@ using static SharedLibraryCore.Server;
|
|||||||
using static Data.Models.Client.EFClient;
|
using static Data.Models.Client.EFClient;
|
||||||
using static Data.Models.EFPenalty;
|
using static Data.Models.EFPenalty;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
using RegionInfo = System.Globalization.RegionInfo;
|
||||||
|
|
||||||
namespace SharedLibraryCore
|
namespace SharedLibraryCore
|
||||||
{
|
{
|
||||||
@ -316,14 +317,20 @@ namespace SharedLibraryCore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long ConvertGuidToLong(this string str, NumberStyles numberStyle, long? fallback = null)
|
||||||
|
{
|
||||||
|
return ConvertGuidToLong(str, numberStyle, true, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// converts a string to numerical guid
|
/// converts a string to numerical guid
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="str">source string for guid</param>
|
/// <param name="str">source string for guid</param>
|
||||||
/// <param name="numberStyle">how to parse the guid</param>
|
/// <param name="numberStyle">how to parse the guid</param>
|
||||||
/// <param name="fallback">value to use if string is empty</param>
|
/// <param name="fallback">value to use if string is empty</param>
|
||||||
|
/// <param name="convertSigned">convert signed values to unsigned</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static long ConvertGuidToLong(this string str, NumberStyles numberStyle, long? fallback = null)
|
public static long ConvertGuidToLong(this string str, NumberStyles numberStyle, bool convertSigned, long? fallback = null)
|
||||||
{
|
{
|
||||||
// added for source games that provide the steam ID
|
// added for source games that provide the steam ID
|
||||||
var match = Regex.Match(str, @"^STEAM_(\d):(\d):(\d+)$");
|
var match = Regex.Match(str, @"^STEAM_(\d):(\d):(\d+)$");
|
||||||
@ -336,7 +343,7 @@ namespace SharedLibraryCore
|
|||||||
return z * 2 + 0x0110000100000000 + y;
|
return z * 2 + 0x0110000100000000 + y;
|
||||||
}
|
}
|
||||||
|
|
||||||
str = str.Substring(0, Math.Min(str.Length, 19));
|
str = str.Substring(0, Math.Min(str.Length, str.StartsWith("-") ? 20 : 19));
|
||||||
var parsableAsNumber = Regex.Match(str, @"([A-F]|[a-f]|[0-9])+").Value;
|
var parsableAsNumber = Regex.Match(str, @"([A-F]|[a-f]|[0-9])+").Value;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(str) && fallback.HasValue)
|
if (string.IsNullOrWhiteSpace(str) && fallback.HasValue)
|
||||||
@ -351,7 +358,7 @@ namespace SharedLibraryCore
|
|||||||
{
|
{
|
||||||
long.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out id);
|
long.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out id);
|
||||||
|
|
||||||
if (id < 0)
|
if (id < 0 && convertSigned)
|
||||||
{
|
{
|
||||||
id = (uint)id;
|
id = (uint)id;
|
||||||
}
|
}
|
||||||
|
@ -862,7 +862,6 @@ namespace WebfrontCore.Controllers
|
|||||||
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
|
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
|
||||||
.Concat(_appConfig.GlobalRules)
|
.Concat(_appConfig.GlobalRules)
|
||||||
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))
|
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))
|
||||||
.Distinct()
|
|
||||||
.Select((value, _) => new
|
.Select((value, _) => new
|
||||||
{
|
{
|
||||||
Value = value
|
Value = value
|
||||||
@ -872,6 +871,7 @@ namespace WebfrontCore.Controllers
|
|||||||
{
|
{
|
||||||
Value = ""
|
Value = ""
|
||||||
})
|
})
|
||||||
|
.Distinct()
|
||||||
.ToDictionary(item => item.Value, item => item.Value);
|
.ToDictionary(item => item.Value, item => item.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ using Data.Models;
|
|||||||
using SharedLibraryCore.Services;
|
using SharedLibraryCore.Services;
|
||||||
using Stats.Config;
|
using Stats.Config;
|
||||||
using WebfrontCore.Permissions;
|
using WebfrontCore.Permissions;
|
||||||
|
using WebfrontCore.QueryHelpers.Models;
|
||||||
using WebfrontCore.ViewComponents;
|
using WebfrontCore.ViewComponents;
|
||||||
|
|
||||||
namespace WebfrontCore.Controllers
|
namespace WebfrontCore.Controllers
|
||||||
@ -26,15 +27,19 @@ namespace WebfrontCore.Controllers
|
|||||||
private readonly IGeoLocationService _geoLocationService;
|
private readonly IGeoLocationService _geoLocationService;
|
||||||
private readonly ClientService _clientService;
|
private readonly ClientService _clientService;
|
||||||
private readonly IInteractionRegistration _interactionRegistration;
|
private readonly IInteractionRegistration _interactionRegistration;
|
||||||
|
private readonly IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse> _clientResourceHelper;
|
||||||
|
|
||||||
public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config,
|
public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config,
|
||||||
IGeoLocationService geoLocationService, ClientService clientService, IInteractionRegistration interactionRegistration) : base(manager)
|
IGeoLocationService geoLocationService, ClientService clientService,
|
||||||
|
IInteractionRegistration interactionRegistration,
|
||||||
|
IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse> clientResourceHelper) : base(manager)
|
||||||
{
|
{
|
||||||
_metaService = metaService;
|
_metaService = metaService;
|
||||||
_config = config;
|
_config = config;
|
||||||
_geoLocationService = geoLocationService;
|
_geoLocationService = geoLocationService;
|
||||||
_clientService = clientService;
|
_clientService = clientService;
|
||||||
_interactionRegistration = interactionRegistration;
|
_interactionRegistration = interactionRegistration;
|
||||||
|
_clientResourceHelper = clientResourceHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete]
|
[Obsolete]
|
||||||
@ -241,6 +246,17 @@ namespace WebfrontCore.Controllers
|
|||||||
return View("Find/Index", clientsDto);
|
return View("Find/Index", clientsDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> AdvancedFind(ClientResourceRequest request)
|
||||||
|
{
|
||||||
|
ViewBag.Title = Localization["WEBFRONT_SEARCH_RESULTS_TITLE"];
|
||||||
|
ViewBag.ClientResourceRequest = request;
|
||||||
|
|
||||||
|
var response = await _clientResourceHelper.QueryResource(request);
|
||||||
|
return request.Offset > 0
|
||||||
|
? PartialView("Find/_AdvancedFindList", response.Results)
|
||||||
|
: View("Find/AdvancedFind", response.Results);
|
||||||
|
}
|
||||||
|
|
||||||
public IActionResult Meta(int id, int count, int offset, long? startAt, MetaType? metaFilterType,
|
public IActionResult Meta(int id, int count, int offset, long? startAt, MetaType? metaFilterType,
|
||||||
CancellationToken token)
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
|
@ -16,7 +16,8 @@ public enum WebfrontEntity
|
|||||||
ProfilePage,
|
ProfilePage,
|
||||||
AdminMenu,
|
AdminMenu,
|
||||||
ClientNote,
|
ClientNote,
|
||||||
Interaction
|
Interaction,
|
||||||
|
AdvancedSearch
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum WebfrontPermission
|
public enum WebfrontPermission
|
||||||
|
19
WebfrontCore/QueryHelpers/Models/ClientResourceRequest.cs
Normal file
19
WebfrontCore/QueryHelpers/Models/ClientResourceRequest.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using Data.Models;
|
||||||
|
using Data.Models.Client;
|
||||||
|
using SharedLibraryCore.QueryHelper;
|
||||||
|
|
||||||
|
namespace WebfrontCore.QueryHelpers.Models;
|
||||||
|
|
||||||
|
public class ClientResourceRequest : ClientPaginationRequest
|
||||||
|
{
|
||||||
|
public string ClientName { get; set; }
|
||||||
|
public bool IsExactClientName { get; set; }
|
||||||
|
public string ClientIp { get; set; }
|
||||||
|
public bool IsExactClientIp { get; set; }
|
||||||
|
public string ClientGuid { get; set; }
|
||||||
|
public DateTime? ClientConnected { get; set; }
|
||||||
|
public EFClient.Permission? ClientLevel { get; set; }
|
||||||
|
public Reference.Game? GameName { get; set; }
|
||||||
|
public bool IncludeGeolocationData { get; set; } = true;
|
||||||
|
}
|
22
WebfrontCore/QueryHelpers/Models/ClientResourceResponse.cs
Normal file
22
WebfrontCore/QueryHelpers/Models/ClientResourceResponse.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using Data.Models;
|
||||||
|
using Data.Models.Client;
|
||||||
|
|
||||||
|
namespace WebfrontCore.QueryHelpers.Models;
|
||||||
|
|
||||||
|
public class ClientResourceResponse
|
||||||
|
{
|
||||||
|
public int ClientId { get; set; }
|
||||||
|
public int AliasId { get; set; }
|
||||||
|
public int LinkId { get; set; }
|
||||||
|
public string CurrentClientName { get; set; }
|
||||||
|
public string MatchedClientName { get; set; }
|
||||||
|
public int? CurrentClientIp { get; set; }
|
||||||
|
public int? MatchedClientIp { get; set; }
|
||||||
|
public string ClientCountryCode { get; set; }
|
||||||
|
public string ClientCountryDisplayName { get; set; }
|
||||||
|
public string ClientLevel { get; set; }
|
||||||
|
public EFClient.Permission ClientLevelValue { get; set; }
|
||||||
|
public DateTime LastConnection { get; set; }
|
||||||
|
public Reference.Game Game { get; set; }
|
||||||
|
}
|
@ -115,6 +115,9 @@ namespace WebfrontCore
|
|||||||
services.AddSingleton<IResourceQueryHelper<FindClientRequest, FindClientResult>, ClientService>();
|
services.AddSingleton<IResourceQueryHelper<FindClientRequest, FindClientResult>, ClientService>();
|
||||||
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, StatsInfoResult>, StatsResourceQueryHelper>();
|
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, StatsInfoResult>, StatsResourceQueryHelper>();
|
||||||
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>, AdvancedClientStatsResourceQueryHelper>();
|
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>, AdvancedClientStatsResourceQueryHelper>();
|
||||||
|
services.AddScoped(sp =>
|
||||||
|
Program.ApplicationServiceProvider
|
||||||
|
.GetRequiredService<IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>>());
|
||||||
services.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>));
|
services.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>));
|
||||||
// todo: this needs to be handled more gracefully
|
// todo: this needs to be handled more gracefully
|
||||||
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<DefaultSettings>());
|
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<DefaultSettings>());
|
||||||
|
39
WebfrontCore/Views/Client/Find/AdvancedFind.cshtml
Normal file
39
WebfrontCore/Views/Client/Find/AdvancedFind.cshtml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
@model IEnumerable<WebfrontCore.QueryHelpers.Models.ClientResourceResponse>
|
||||||
|
@{
|
||||||
|
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="content mt-0">
|
||||||
|
<h2 class="content-title mt-20">@loc["WEBFRONT_SEARCH_RESULTS_TITLE"]</h2>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-primary text-light d-none d-md-table-row">
|
||||||
|
<td>@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_NAME_FULL"]</td>
|
||||||
|
<td>@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_IP"]</td>
|
||||||
|
<td>@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_COUNTRY"]</td>
|
||||||
|
<td>@loc["WEBFRONT_PROFILE_LEVEL"]</td>
|
||||||
|
<td>@loc["WEBFRONT_ADVANCED_SEARCH_LABEL_GAME"]</td>
|
||||||
|
<td class="text-right">@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="searchResultContainer">
|
||||||
|
<partial name="Find/_AdvancedFindList" model="@Model"/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<i id="loaderLoad" class="oi oi-chevron-bottom loader-load-more text-primary" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section scripts {
|
||||||
|
<script>
|
||||||
|
initLoader('/Client/AdvancedFind', '#searchResultContainer', 30, 30, () => {
|
||||||
|
return $('#advancedSearchDropdownContentFullDesktop').serializeArray()
|
||||||
|
|| $('#advancedSearchDropdownContentCompactDesktop').serializeArray()
|
||||||
|
|| $('#advancedSearchDropdownContentFullMobile').serializeArray()
|
||||||
|
|| $('#advancedSearchDropdownContentCompactMobile').serializeArray()
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
107
WebfrontCore/Views/Client/Find/_AdvancedFindContent.cshtml
Normal file
107
WebfrontCore/Views/Client/Find/_AdvancedFindContent.cshtml
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
@using WebfrontCore.QueryHelpers.Models
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@using WebfrontCore.Permissions
|
||||||
|
@using Data.Models.Client
|
||||||
|
@model WebfrontCore.QueryHelpers.Models.ClientResourceResponse
|
||||||
|
|
||||||
|
@{
|
||||||
|
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||||
|
var client = Model;
|
||||||
|
|
||||||
|
string FormatNameChange(ClientResourceResponse clientResponse)
|
||||||
|
{
|
||||||
|
return clientResponse.CurrentClientName.StripColors() != clientResponse.MatchedClientName?.StripColors()
|
||||||
|
? $"{clientResponse.CurrentClientName} [{clientResponse.MatchedClientName}{(clientResponse.MatchedClientIp is null && clientResponse.MatchedClientIp != clientResponse.CurrentClientIp ? "" : $"/{clientResponse.MatchedClientIp.ConvertIPtoString()}")}]"
|
||||||
|
: clientResponse.CurrentClientName;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canSeeLevel = (ViewBag.PermissionsSet as IEnumerable<string>).HasPermission(WebfrontEntity.ClientLevel, WebfrontPermission.Read);
|
||||||
|
var canSeeIp = (ViewBag.PermissionsSet as IEnumerable<string>).HasPermission(WebfrontEntity.ClientIPAddress, WebfrontPermission.Read);
|
||||||
|
string ClassForLevel(EFClient.Permission permission) => !canSeeLevel ? "level-color-user" : $"level-color-{permission.ToString().ToLower()}";
|
||||||
|
string FormatIpForPermission(int? ip) => canSeeIp && ip is not null ? ip.ConvertIPtoString() : "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
<tr class="bg-dark-dm bg-light-lm d-none d-md-table-row">
|
||||||
|
<td class="col-3">
|
||||||
|
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId">
|
||||||
|
<color-code value="@FormatNameChange(client)"></color-code>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="col-2">
|
||||||
|
@FormatIpForPermission(client.CurrentClientIp)
|
||||||
|
</td>
|
||||||
|
<td class="col-2">
|
||||||
|
<div class="d-flex">
|
||||||
|
@if (string.IsNullOrEmpty(client.ClientCountryCode))
|
||||||
|
{
|
||||||
|
<div class="d-flex">
|
||||||
|
<i class="oi oi-question-mark ml-5 mr-20"></i>
|
||||||
|
<div class="font-size-12 font-weight-light">Unknown</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<img src="https://flagcdn.com/32x24/@(client.ClientCountryCode.ToLower()).png" class="mr-10 rounded align-self-center" alt="@client.ClientCountryDisplayName"/>
|
||||||
|
}
|
||||||
|
<div class="font-size-12 font-weight-light">@client.ClientCountryDisplayName</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="col-2 @ClassForLevel(client.ClientLevelValue)">@(canSeeLevel ? client.ClientLevel : "-")</td>
|
||||||
|
<td>
|
||||||
|
<div data-toggle="tooltip" data-title="@ViewBag.Localization["GAME_" + client.Game]">
|
||||||
|
<span class="badge">@Utilities.MakeAbbreviation(ViewBag.Localization["GAME_" + client.Game])</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="col-3">
|
||||||
|
<div class="float-right">
|
||||||
|
<div data-toggle="tooltip" data-title="@client.LastConnection.ToShortDateString()" data-placement="left">
|
||||||
|
<span class="text-muted">@client.LastConnection.HumanizeForCurrentCulture()</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="d-flex d-block d-md-none">
|
||||||
|
<td class="bg-primary text-light w-half">
|
||||||
|
<div class="pb-5">@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_NAME"]</div>
|
||||||
|
<div class="pb-5">@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_ALIAS"]</div>
|
||||||
|
<div class="pb-5">@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_IP"]</div>
|
||||||
|
<div class="pb-5">@loc["WEBFRONT_ADVANCED_SEARCH_CONTENT_TABLE_COUNTRY"]</div>
|
||||||
|
<div class="pb-5">@loc["WEBFRONT_PROFILE_LEVEL"]</div>
|
||||||
|
<div class="pb-5">@loc["WEBFRONT_ADVANCED_SEARCH_LABEL_GAME"]</div>
|
||||||
|
<div class="pb-5">@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]</div>
|
||||||
|
</td>
|
||||||
|
<td class="w-half bg-dark">
|
||||||
|
<div class="pb-5">
|
||||||
|
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId" class="no-decoration @ClassForLevel(client.ClientLevelValue)">
|
||||||
|
<color-code value="@client.CurrentClientName"></color-code>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="pb-5">
|
||||||
|
<color-code value="@client.MatchedClientName"></color-code>
|
||||||
|
@if (client.MatchedClientIp != client.CurrentClientIp)
|
||||||
|
{
|
||||||
|
<span>/ @client.MatchedClientIp.ConvertIPtoString()</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted pb-5">
|
||||||
|
@FormatIpForPermission(client.CurrentClientIp)
|
||||||
|
</div>
|
||||||
|
<div class="d-flex pb-5">
|
||||||
|
@if (string.IsNullOrEmpty(client.ClientCountryCode))
|
||||||
|
{
|
||||||
|
<div class="mr-5">Unknown</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="mr-5">@client.ClientCountryDisplayName</div>
|
||||||
|
<img src="https://flagcdn.com/24x18/@(client.ClientCountryCode.ToLower()).png" class="rounded align-self-center" alt="@client.ClientCountryDisplayName"/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="pb-5 @ClassForLevel(client.ClientLevelValue)">@(canSeeLevel ? client.ClientLevel : "-")</div>
|
||||||
|
<div data-toggle="tooltip" data-title="@ViewBag.Localization["GAME_" + client.Game]">
|
||||||
|
<span class="badge font-size-12 mt-5 mb-5">@Utilities.MakeAbbreviation(ViewBag.Localization["GAME_" + client.Game])</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted pb-5">@client.LastConnection.HumanizeForCurrentCulture()</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
6
WebfrontCore/Views/Client/Find/_AdvancedFindList.cshtml
Normal file
6
WebfrontCore/Views/Client/Find/_AdvancedFindList.cshtml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@model IEnumerable<WebfrontCore.QueryHelpers.Models.ClientResourceResponse>
|
||||||
|
|
||||||
|
@foreach (var client in Model)
|
||||||
|
{
|
||||||
|
<partial name="Find/_AdvancedFindContent" model="@client"/>
|
||||||
|
}
|
125
WebfrontCore/Views/Shared/Partials/_SearchResourceFilter.cshtml
Normal file
125
WebfrontCore/Views/Shared/Partials/_SearchResourceFilter.cshtml
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
@using Data.Models.Client
|
||||||
|
@using SharedLibraryCore.Dtos
|
||||||
|
@using WebfrontCore.QueryHelpers.Models
|
||||||
|
@using Data.Models
|
||||||
|
@model string
|
||||||
|
@{
|
||||||
|
var existingClientFilter = ViewBag.ClientResourceRequest as ClientResourceRequest;
|
||||||
|
}
|
||||||
|
<h6 class="dropdown-header">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_TITLE"]</h6>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<form asp-controller="Client" asp-action="AdvancedFind" method="get" id="advancedSearchDropdownContent@(Model)" onsubmit="showLoader()">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="searchType@(Model)">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_LABEL_FOR"]</label>
|
||||||
|
<br/>
|
||||||
|
<select class="form-control" id="searchType@(Model)" name="searchType" disabled="disabled">
|
||||||
|
<option value="client">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_SELECT_TYPE_PLAYERS"]</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="clientName@(Model)">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_LABEL_NAME"]</label>
|
||||||
|
<div class="d-flex">
|
||||||
|
<input type="text" class="form-control" name="clientName" id="clientName@(Model)"
|
||||||
|
placeholder="Unknown Soldier" value="@existingClientFilter?.ClientName"/>
|
||||||
|
<div class="custom-control ml-10 align-self-center">
|
||||||
|
<div class="custom-switch">
|
||||||
|
@if (existingClientFilter?.IsExactClientName ?? false)
|
||||||
|
{
|
||||||
|
<input type="checkbox" id="isExactClientName@(Model)" name="isExactClientName" value="true"
|
||||||
|
checked="checked">
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<input type="checkbox" id="isExactClientName@(Model)" name="isExactClientName" value="true">
|
||||||
|
}
|
||||||
|
<label for="isExactClientName@(Model)">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_LABEL_EXACT"]</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="clientIP@(Model)">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_LABEL_IP"]</label>
|
||||||
|
<div class="d-flex">
|
||||||
|
<input type="text" class="form-control" name="clientIP" id="clientIP@(Model)" placeholder="1.1.1.1"
|
||||||
|
value="@existingClientFilter?.ClientIp">
|
||||||
|
<div class="custom-control ml-10 align-self-center">
|
||||||
|
<div class="custom-switch">
|
||||||
|
@if (existingClientFilter?.IsExactClientIp ?? false)
|
||||||
|
{
|
||||||
|
<input type="checkbox" id="isExactClientIP@(Model)" name="isExactClientIP" value="true"
|
||||||
|
checked="checked">
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<input type="checkbox" id="isExactClientIP@(Model)" name="isExactClientIP" value="true">
|
||||||
|
}
|
||||||
|
<label for="isExactClientIP@(Model)">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_LABEL_EXACT"]</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="clientGuid@(Model)">GUID <span class="text-primary">•</span> XUID <span class="text-primary">•</span> NetworkID</label>
|
||||||
|
<input type="text" class="form-control" name="clientGuid" id="clientGuid@(Model)"
|
||||||
|
placeholder="110000100000001" value="@existingClientFilter?.ClientGuid"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="clientLevel@(Model)" class="w-half">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_LABEL_PERMISSION"]</label>
|
||||||
|
<label for="clientGameName@(Model)">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_LABEL_GAME"]</label>
|
||||||
|
|
||||||
|
<div class="d-flex">
|
||||||
|
<select class="form-control w-half" id="clientLevel@(Model)" name="clientLevel">
|
||||||
|
<option value="">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_SELECT_PERMISSIONS_ANY"]</option>
|
||||||
|
@foreach (EFClient.Permission permission in Enum.GetValues(typeof(EFClient.Permission)))
|
||||||
|
{
|
||||||
|
<option value="@((int)permission)" selected="@(permission == existingClientFilter?.ClientLevel)">
|
||||||
|
@permission.ToLocalizedLevelName()
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<select class="form-control w-half ml-10" id="clientGameName@(Model)" name="gameName">
|
||||||
|
<option value="">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_SELECT_PERMISSIONS_ANY"]</option>
|
||||||
|
@foreach (Reference.Game game in Enum.GetValues(typeof(Reference.Game)))
|
||||||
|
{
|
||||||
|
<option value="@((int)game)" selected="@(game == existingClientFilter?.GameName)">
|
||||||
|
@ViewBag.Localization["GAME_" + game]
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="clientConnected@(Model)">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_LABEL_CONNECTED_SINCE"]</label>
|
||||||
|
<div class="d-flex">
|
||||||
|
@{ var oneYearAgo = DateTime.UtcNow.AddYears(-1).ToShortDateString(); }
|
||||||
|
<input type="text" class="form-control date-picker-input w-half" name="clientConnected"
|
||||||
|
id="clientConnected@(Model)" data-date="@oneYearAgo"
|
||||||
|
value="@(existingClientFilter?.ClientConnected?.ToShortDateString() ?? oneYearAgo)"/>
|
||||||
|
|
||||||
|
<div class="custom-control ml-10 align-self-center">
|
||||||
|
<div class="custom-switch">
|
||||||
|
@if (existingClientFilter?.Direction is SortDirection.Descending)
|
||||||
|
{
|
||||||
|
<input type="checkbox" id="resultOrder@(Model)" name="direction"
|
||||||
|
value="@((int)SortDirection.Ascending)">
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<input type="checkbox" id="resultOrder@(Model)" name="direction"
|
||||||
|
value="@((int)SortDirection.Ascending)" checked="checked">
|
||||||
|
}
|
||||||
|
<label for="resultOrder@(Model)">@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_LABEL_OLDEST_FIRST"]</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" class="btn btn-primary" value="@ViewBag.Localization["WEBFRONT_ADVANCED_SEARCH_BUTTON_SUBMIT"]"/>
|
||||||
|
</form>
|
||||||
|
</div>
|
@ -126,7 +126,7 @@
|
|||||||
<i class="oi oi-moon"></i>
|
<i class="oi oi-moon"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-none d-md-block ">
|
<div class="d-none d-md-block ">
|
||||||
<partial name="_SearchResourceForm"/>
|
<partial name="_SearchResourceForm" model="@("Full")"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex d-lg-none">
|
<div class="d-flex d-lg-none">
|
||||||
<a href="#" onclick="halfmoon.toggleModal('contextMenuModal')">
|
<a href="#" onclick="halfmoon.toggleModal('contextMenuModal')">
|
||||||
@ -158,6 +158,7 @@
|
|||||||
<script type="text/javascript" src="~/lib/moment-timezone/moment-timezone.js"></script>
|
<script type="text/javascript" src="~/lib/moment-timezone/moment-timezone.js"></script>
|
||||||
<script type="text/javascript" src="~/lib/chart.js/dist/Chart.bundle.min.js"></script>
|
<script type="text/javascript" src="~/lib/chart.js/dist/Chart.bundle.min.js"></script>
|
||||||
<script type="text/javascript" src="~/lib/halfmoon/js/halfmoon.js"></script>
|
<script type="text/javascript" src="~/lib/halfmoon/js/halfmoon.js"></script>
|
||||||
|
<script type="text/javascript" src="~/lib/vanillajs-datepicker/dist/js/datepicker.js"></script>
|
||||||
<script type="text/javascript" src="~/js/action.js"></script>
|
<script type="text/javascript" src="~/js/action.js"></script>
|
||||||
<script type="text/javascript" src="~/js/loader.js"></script>
|
<script type="text/javascript" src="~/js/loader.js"></script>
|
||||||
<script type="text/javascript" src="~/js/search.js"></script>
|
<script type="text/javascript" src="~/js/search.js"></script>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<div class="sidebar-menu list">
|
<div class="sidebar-menu list">
|
||||||
<div class="sidebar-content m-0">
|
<div class="sidebar-content m-0">
|
||||||
<div class="pr-20 pl-20 mb-20 d-block d-lg-none">
|
<div class="pr-20 pl-20 mb-20 d-block d-lg-none">
|
||||||
<partial name="_SearchResourceForm"/>
|
<partial name="_SearchResourceForm" model="@("Compact")"/>
|
||||||
</div>
|
</div>
|
||||||
<span class="sidebar-title">@ViewBag.Localization["WEBFRONT_NAV_TITLE_MAIN"]</span>
|
<span class="sidebar-title">@ViewBag.Localization["WEBFRONT_NAV_TITLE_MAIN"]</span>
|
||||||
<div class="sidebar-divider"></div>
|
<div class="sidebar-divider"></div>
|
||||||
|
@ -1,12 +1,40 @@
|
|||||||
<form class="form-inline ml-auto" method="get" asp-controller="Client" asp-action="Find">
|
@model string
|
||||||
|
<div class="ml-auto d-inline-flex w-full">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="client_search_mobile" name="clientName" class="form-control" type="text" placeholder="@ViewBag.Localization["WEBFRONT_NAV_SEARCH"]" required="required"/>
|
<has-permission entity="AdvancedSearch" required-permission="Read">
|
||||||
|
<div class="input-group-prepend dropdown with-arrow d-block d-md-none">
|
||||||
|
<button class="btn" type="button" data-toggle="dropdown">
|
||||||
|
<i class="oi oi-chevron-bottom"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu w-300">
|
||||||
|
<partial name="Partials/_SearchResourceFilter" model="@(Model + "Mobile")"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</has-permission>
|
||||||
|
|
||||||
|
<form asp-controller="Client" asp-action="AdvancedFind" method="get" asp-route-isLegacyQuery="true"
|
||||||
|
id="basicSearchForm@(Model)" class="flex-fill">
|
||||||
|
<input name="clientName" class="form-control" type="text"
|
||||||
|
placeholder="@ViewBag.Localization["WEBFRONT_NAV_SEARCH"]" required="required"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<has-permission entity="AdvancedSearch" required-permission="Read">
|
||||||
|
<div class="input-group-append dropdown dropleft with-arrow d-md-block d-none">
|
||||||
|
<button class="btn" type="button" data-toggle="dropdown">
|
||||||
|
<i class="oi oi-chevron-bottom"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu w-400">
|
||||||
|
<partial name="Partials/_SearchResourceFilter" model="@(Model + "Desktop")"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</has-permission>
|
||||||
|
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button class="btn" type="submit">
|
<button class="btn" form="basicSearchForm@(Model)" type="submit">
|
||||||
<i class="oi oi-magnifying-glass"></i>
|
<i class="oi oi-magnifying-glass"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
</form>
|
</div>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"wwwroot/lib/chart.js/dist/Chart.bundle.min.js",
|
"wwwroot/lib/chart.js/dist/Chart.bundle.min.js",
|
||||||
"wwwroot/lib/halfmoon/js/halfmoon.min.js",
|
"wwwroot/lib/halfmoon/js/halfmoon.min.js",
|
||||||
"wwwroot/lib/canvas.js/canvasjs.js",
|
"wwwroot/lib/canvas.js/canvasjs.js",
|
||||||
|
"wwwroot/lib/vanillajs-datepicker/dist/js/datepicker.min.js",
|
||||||
"wwwroot/js/action.js",
|
"wwwroot/js/action.js",
|
||||||
"wwwroot/js/console.js",
|
"wwwroot/js/console.js",
|
||||||
"wwwroot/js/penalty.js",
|
"wwwroot/js/penalty.js",
|
||||||
|
@ -29,6 +29,10 @@
|
|||||||
{
|
{
|
||||||
"library": "halfmoon@1.1.1",
|
"library": "halfmoon@1.1.1",
|
||||||
"destination": "wwwroot/lib/halfmoon"
|
"destination": "wwwroot/lib/halfmoon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"library": "vanillajs-datepicker@1.2.0",
|
||||||
|
"destination": "wwwroot/lib/vanillajs-datepicker"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
@import 'profile.scss';
|
@import 'profile.scss';
|
||||||
|
|
||||||
|
$dp-background-color: #191c20;
|
||||||
|
$dp-cell-focus-background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
@import '../../lib/vanillajs-datepicker/sass/datepicker';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--blue-color: #117ac0;
|
--blue-color: #117ac0;
|
||||||
|
|
||||||
@ -471,3 +475,25 @@ table.no-cell-divider td.first-row, table.no-cell-divider th.first-row {
|
|||||||
table.no-cell-divider td.last-row, table.no-cell-divider th.last-row {
|
table.no-cell-divider td.last-row, table.no-cell-divider th.last-row {
|
||||||
padding-bottom: 1.5rem;
|
padding-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.datepicker-picker {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-switch {
|
||||||
|
margin: 0 1rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker-header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker-view .days {
|
||||||
|
margin: auto !important
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:root {
|
||||||
|
--sidebar-width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@ let startAt = null;
|
|||||||
let isLoaderLoading = false;
|
let isLoaderLoading = false;
|
||||||
let loadUri = '';
|
let loadUri = '';
|
||||||
let loaderResponseId = '';
|
let loaderResponseId = '';
|
||||||
let additionalParams = [];
|
let additionalParams = undefined;
|
||||||
|
|
||||||
function initLoader(location, loaderId, count = 10, start = count, additional = []) {
|
function initLoader(location, loaderId, count = 10, start = count, additional = []) {
|
||||||
loadUri = location;
|
loadUri = location;
|
||||||
@ -52,14 +52,23 @@ function loadMoreItems() {
|
|||||||
showLoader();
|
showLoader();
|
||||||
isLoaderLoading = true;
|
isLoaderLoading = true;
|
||||||
let params = {offset: loaderOffset, count: loadCount, startAt: startAt};
|
let params = {offset: loaderOffset, count: loadCount, startAt: startAt};
|
||||||
for (let i = 0; i < additionalParams.length; i++) {
|
|
||||||
let param = additionalParams[i];
|
if (additionalParams instanceof Function) {
|
||||||
params[param.name] = param.value instanceof Function ? param.value() : param.value;
|
params = {
|
||||||
|
...params,
|
||||||
|
...flatParams(additionalParams())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < additionalParams.length; i++) {
|
||||||
|
let param = additionalParams[i];
|
||||||
|
params[param.name] = param.value instanceof Function ? param.value() : param.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$.get(loadUri, params)
|
$.get(loadUri, params)
|
||||||
.done(function (response) {
|
.done(function (response) {
|
||||||
$(loaderResponseId).append(response);
|
$(loaderResponseId).append(response);
|
||||||
|
|
||||||
if (response.trim().length === 0) {
|
if (response.trim().length === 0) {
|
||||||
staleLoader();
|
staleLoader();
|
||||||
loaderReachedEnd = true;
|
loaderReachedEnd = true;
|
||||||
@ -82,3 +91,12 @@ function loadMoreItems() {
|
|||||||
});
|
});
|
||||||
loaderOffset += loadCount;
|
loaderOffset += loadCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flatParams(params) {
|
||||||
|
return params.map(function (b) {
|
||||||
|
return {[b.name]: b.value}
|
||||||
|
}).reduce(function (prev, curr) {
|
||||||
|
for (const key in curr) prev[key] = curr[key];
|
||||||
|
return prev;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -1,27 +1,35 @@
|
|||||||
$(document).ready(function() {
|
$(document).ready(function () {
|
||||||
$('.form-inline').submit(function(e) {
|
$('.form-inline').submit(function (e) {
|
||||||
const id = $(e.currentTarget).find('input');
|
const id = $(e.currentTarget).find('input');
|
||||||
if ($(id).val().length < 3) {
|
if ($(id).val().length < 3) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$(id)
|
$(id)
|
||||||
.addClass('input-text-danger')
|
.addClass('input-text-danger')
|
||||||
.delay(25)
|
.delay(25)
|
||||||
.queue(function () {
|
.queue(function () {
|
||||||
$(this).addClass('input-border-transition').dequeue();
|
$(this).addClass('input-border-transition').dequeue();
|
||||||
})
|
})
|
||||||
.delay(1000)
|
.delay(1000)
|
||||||
.queue(function () {
|
.queue(function () {
|
||||||
$(this).removeClass('input-text-danger').dequeue();
|
$(this).removeClass('input-text-danger').dequeue();
|
||||||
})
|
})
|
||||||
.delay(500)
|
.delay(500)
|
||||||
.queue(function () {
|
.queue(function () {
|
||||||
$(this).removeClass('input-border-transition').dequeue();
|
$(this).removeClass('input-border-transition').dequeue();
|
||||||
});
|
});
|
||||||
}
|
} else if ($(id).val().startsWith("chat|")) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.location = "/Message/Find?query=" + $(id).val();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
else if ($(id).val().startsWith("chat|")) {
|
$('.date-picker-input').each((index, selector) => {
|
||||||
e.preventDefault();
|
new Datepicker(selector, {
|
||||||
window.location = "/Message/Find?query=" + $(id).val();
|
buttonClass: 'btn',
|
||||||
}
|
format: 'yyyy-mm-dd',
|
||||||
});
|
nextArrow: '>',
|
||||||
|
prevArrow: '<',
|
||||||
|
orientation: 'auto top'
|
||||||
|
});
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user