refine webfront pages
finish refactor of penalty information/profile optimize pull penalty query start impl of quick message mapping
This commit is contained in:
parent
9393b35c39
commit
6f80f1edbb
@ -250,10 +250,10 @@ namespace IW4MAdmin.Application
|
||||
ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
|
||||
var newConfig = ConfigHandler.Configuration();
|
||||
|
||||
newConfig.AutoMessagePeriod = defaultConfig.AutoMessagePeriod;
|
||||
newConfig.AutoMessages = defaultConfig.AutoMessages;
|
||||
newConfig.GlobalRules = defaultConfig.GlobalRules;
|
||||
newConfig.Maps = defaultConfig.Maps;
|
||||
newConfig.QuickMessages = defaultConfig.QuickMessages;
|
||||
|
||||
if (newConfig.Servers == null)
|
||||
{
|
||||
@ -493,27 +493,15 @@ namespace IW4MAdmin.Application
|
||||
return new List<ProfileMeta>();
|
||||
}
|
||||
|
||||
var penalties = await GetPenaltyService().GetAllClientPenaltiesAsync(clientId, count, offset, startAt);
|
||||
var penalties = await GetPenaltyService().GetClientPenaltyForMetaAsync(clientId, count, offset, startAt);
|
||||
|
||||
return penalties.Select(_penalty => new ProfileMeta()
|
||||
{
|
||||
Id = _penalty.PenaltyId,
|
||||
Id = _penalty.Id,
|
||||
Type = _penalty.PunisherId == clientId ? ProfileMeta.MetaType.Penalized : ProfileMeta.MetaType.ReceivedPenalty,
|
||||
Value = new PenaltyInfo
|
||||
{
|
||||
Id = _penalty.PenaltyId,
|
||||
OffenderName = _penalty.Offender.Name,
|
||||
OffenderId = _penalty.OffenderId,
|
||||
PunisherName = _penalty.Punisher.Name,
|
||||
PunisherId = _penalty.PunisherId,
|
||||
Offense = _penalty.Offense,
|
||||
PenaltyType = _penalty.Type.ToString(),
|
||||
TimeRemaining = _penalty.Expires.HasValue ? (DateTime.Now > _penalty.Expires ? "" : _penalty.Expires.ToString()) : DateTime.MaxValue.ToString(),
|
||||
AutomatedOffense = _penalty.AutomatedOffense,
|
||||
Expired = _penalty.Expires.HasValue && _penalty.Expires <= DateTime.UtcNow
|
||||
},
|
||||
When = _penalty.When,
|
||||
Sensitive = _penalty.Type == Penalty.PenaltyType.Flag
|
||||
Value = _penalty,
|
||||
When = _penalty.TimePunished,
|
||||
Sensitive = _penalty.Sensitive
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
@ -16,6 +16,51 @@
|
||||
"Keep grenade launcher use to a minimum",
|
||||
"Balance teams at ALL times"
|
||||
],
|
||||
"QuickMessages": [
|
||||
{
|
||||
"Game": "IW4",
|
||||
"Messages": {
|
||||
"QUICKMESSAGE_AREA_SECURE": "Area secure!",
|
||||
"QUICKMESSAGE_ARE_YOU_CRAZY": "Are you crazy?",
|
||||
"QUICKMESSAGE_ATTACK_LEFT_FLANK": "Attack left flank!",
|
||||
"QUICKMESSAGE_ATTACK_RIGHT_FLANK": "Attack right flank!",
|
||||
"QUICKMESSAGE_COME_ON": "Come on.",
|
||||
"QUICKMESSAGE_ENEMIES_SPOTTED": "Multiple contacts!",
|
||||
"QUICKMESSAGE_ENEMY_DOWN": "Enemy down!",
|
||||
"QUICKMESSAGE_ENEMY_GRENADE": "Enemy grenade!",
|
||||
"QUICKMESSAGE_ENEMY_SPOTTED": "Contact!",
|
||||
"QUICKMESSAGE_FALL_BACK": "Fall back!",
|
||||
"QUICKMESSAGE_FOLLOW_ME": "On me!",
|
||||
"QUICKMESSAGE_GREAT_SHOT": "Nice shot!",
|
||||
"QUICKMESSAGE_GRENADE": "Grenade!",
|
||||
"QUICKMESSAGE_HOLD_THIS_POSITION": "Hold this position!",
|
||||
"QUICKMESSAGE_HOLD_YOUR_FIRE": "Hold your fire!",
|
||||
"QUICKMESSAGE_IM_IN_POSITION": "In position.",
|
||||
"QUICKMESSAGE_IM_ON_MY_WAY": "Moving.",
|
||||
"QUICKMESSAGE_MOVE_IN": "Move in!",
|
||||
"QUICKMESSAGE_NEED_REINFORCEMENTS": "Need reinforcements!",
|
||||
"QUICKMESSAGE_NO_SIR": "Negative.",
|
||||
"QUICKMESSAGE_ON_MY_WAY": "On my way.",
|
||||
"QUICKMESSAGE_REGROUP": "Regroup!",
|
||||
"QUICKMESSAGE_SNIPER": "Sniper!",
|
||||
"QUICKMESSAGE_SORRY": "Sorry.",
|
||||
"QUICKMESSAGE_SQUAD_ATTACK_LEFT_FLANK": "Squad, attack left flank!",
|
||||
"QUICKMESSAGE_SQUAD_ATTACK_RIGHT_FLANK": "Squad, attack right flank!",
|
||||
"QUICKMESSAGE_SQUAD_HOLD_THIS_POSITION": "Squad, hold this position!",
|
||||
"QUICKMESSAGE_SQUAD_REGROUP": "Squad, regroup!",
|
||||
"QUICKMESSAGE_SQUAD_STICK_TOGETHER": "Squad, stick together!",
|
||||
"QUICKMESSAGE_STICK_TOGETHER": "Stick together!",
|
||||
"QUICKMESSAGE_SUPPRESSING_FIRE": "Base of fire!",
|
||||
"QUICKMESSAGE_TOOK_LONG_ENOUGH": "Took long enough!",
|
||||
"QUICKMESSAGE_TOOK_YOU_LONG_ENOUGH": "Took you long enough!",
|
||||
"QUICKMESSAGE_WATCH_SIX": "Watch your six!",
|
||||
"QUICKMESSAGE_YES_SIR": "Roger.",
|
||||
"QUICKMESSAGE_YOURE_CRAZY": "You're crazy!",
|
||||
"QUICKMESSAGE_YOURE_NUTS": "You're nuts!",
|
||||
"QUICKMESSAGE_YOU_OUTTA_YOUR_MIND": "You outta your mind?"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Maps": [
|
||||
{
|
||||
"Game": "IW3",
|
||||
|
@ -89,16 +89,30 @@ namespace IW4MAdmin.Plugins.Stats.Web.Controllers
|
||||
{
|
||||
using (var ctx = new SharedLibraryCore.Database.DatabaseContext(true))
|
||||
{
|
||||
var penaltyInfo = await ctx.Set<Models.EFACSnapshot>()
|
||||
.Where(s => s.ClientId == clientId)
|
||||
int linkId = await ctx.Clients
|
||||
.Where(_client => _client.ClientId == clientId)
|
||||
.Select(_client => _client.AliasLinkId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
var clientIds = await ctx.Clients.Where(_client => _client.AliasLinkId == linkId)
|
||||
.Select(_client => _client.ClientId)
|
||||
.ToListAsync();
|
||||
|
||||
var iqPenaltyInfo = ctx.Set<Models.EFACSnapshot>()
|
||||
.Where(s => clientIds.Contains(s.ClientId))
|
||||
.Include(s => s.LastStrainAngle)
|
||||
.Include(s => s.HitOrigin)
|
||||
.Include(s => s.HitDestination)
|
||||
.Include(s => s.CurrentViewAngle)
|
||||
.Include(s => s.PredictedViewAngles)
|
||||
.OrderBy(s => s.When)
|
||||
.ThenBy(s => s.Hits)
|
||||
.ToListAsync();
|
||||
.ThenBy(s => s.Hits);
|
||||
|
||||
#if DEBUG == true
|
||||
var sql = iqPenaltyInfo.ToSql();
|
||||
#endif
|
||||
|
||||
var penaltyInfo = await iqPenaltyInfo.ToListAsync();
|
||||
|
||||
return View("_PenaltyInfo", penaltyInfo);
|
||||
}
|
||||
|
@ -3,10 +3,12 @@
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<div class="client-message-context bg-dark p-2 mt-2 mb-2 border-top border-bottom">
|
||||
<h5>@Model.First().Time.ToString()</h5>
|
||||
<div class="client-message-context">
|
||||
<h5 class="bg-primary pt-2 pb-2 pl-3 mb-0 mt-2 text-white">@Model.First().Time.ToString()</h5>
|
||||
<div class="bg-dark p-3 mb-2 border-bottom">
|
||||
@foreach (var message in Model)
|
||||
{
|
||||
<span class="text-white">@Html.ActionLink(@message.Name, "ProfileAsync", "Client", new { id = message.ClientId})</span><span> — @message.Message</span><br />
|
||||
<span class="text-white">@message.Name</span><span> — @message.Message</span><br />
|
||||
}
|
||||
</div>
|
||||
</div>
|
@ -57,6 +57,7 @@ namespace SharedLibraryCore.Configuration
|
||||
public List<string> AutoMessages { get; set; }
|
||||
public List<string> GlobalRules { get; set; }
|
||||
public List<MapConfiguration> Maps { get; set; }
|
||||
public List<QuickMessageConfiguration> QuickMessages { get; set; }
|
||||
public List<string> DisallowedClientNames { get; set; }
|
||||
|
||||
public IBaseConfiguration Generate()
|
||||
|
@ -7,10 +7,10 @@ namespace SharedLibraryCore.Configuration
|
||||
{
|
||||
public class DefaultConfiguration : IBaseConfiguration
|
||||
{
|
||||
public int AutoMessagePeriod { get; set; }
|
||||
public List<string> AutoMessages { get; set; }
|
||||
public List<string> GlobalRules { get; set; }
|
||||
public List<MapConfiguration> Maps { get; set; }
|
||||
public List<QuickMessageConfiguration> QuickMessages {get; set;}
|
||||
|
||||
public IBaseConfiguration Generate() => this;
|
||||
|
||||
|
14
SharedLibraryCore/Configuration/QuickMessageConfiguration.cs
Normal file
14
SharedLibraryCore/Configuration/QuickMessageConfiguration.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace SharedLibraryCore.Configuration
|
||||
{
|
||||
public class QuickMessageConfiguration
|
||||
{
|
||||
|
||||
public Game Game { get; set; }
|
||||
public Dictionary<string, string> Messages { get; set; }
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static SharedLibraryCore.Database.Models.EFClient;
|
||||
using static SharedLibraryCore.Objects.Penalty;
|
||||
|
||||
namespace SharedLibraryCore.Dtos
|
||||
{
|
||||
@ -16,13 +14,19 @@ namespace SharedLibraryCore.Dtos
|
||||
public int PunisherId { get; set; }
|
||||
public ulong PunisherNetworkId { get; set; }
|
||||
public string PunisherIPAddress { get; set; }
|
||||
public string PunisherLevel { get; set; }
|
||||
public int PunisherLevelId { get; set; }
|
||||
public Permission PunisherLevel { get; set; }
|
||||
public string PunisherLevelText => PunisherLevel.ToLocalizedLevelName();
|
||||
public string Offense { get; set; }
|
||||
public string AutomatedOffense { get; set; }
|
||||
public string PenaltyType { get; set; }
|
||||
public string TimePunished { get; set; }
|
||||
public string TimeRemaining { get; set; }
|
||||
public bool Expired { get; set; }
|
||||
public PenaltyType PenaltyType { get; set; }
|
||||
public string PenaltyTypeText => PenaltyType.ToString();
|
||||
public DateTime TimePunished { get; set; }
|
||||
public string TimePunishedString => Utilities.GetTimePassed(TimePunished, true);
|
||||
public string TimeRemaining => DateTime.UtcNow > Expires ? "" : $"{((Expires ?? DateTime.MaxValue).Year == DateTime.MaxValue.Year ? Utilities.GetTimePassed(TimePunished, true) : Utilities.TimeSpanText((Expires ?? DateTime.MaxValue) - DateTime.UtcNow))}";
|
||||
public bool Expired => Expires.HasValue && Expires <= DateTime.UtcNow;
|
||||
public DateTime? Expires { get; set; }
|
||||
public override bool Sensitive => PenaltyType == PenaltyType.Flag;
|
||||
public bool IsEvade { get; set; }
|
||||
public string AdditionalPenaltyInformation => $"{(!string.IsNullOrEmpty(AutomatedOffense) ? $" ({AutomatedOffense})" : "")}{(IsEvade ? $" ({Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PENALTY_EVADE"]})" : "")}";
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,8 @@ namespace SharedLibraryCore.Dtos
|
||||
public List<ProfileMeta> Meta { get; set; }
|
||||
public bool Online { get; set; }
|
||||
public string TimeOnline { get; set; }
|
||||
public DateTime LastConnection { get; set; }
|
||||
public string LastConnectionText => Utilities.GetTimePassed(LastConnection, true);
|
||||
public IDictionary<int, long> LinkedAccounts { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ namespace SharedLibraryCore.Dtos
|
||||
{
|
||||
public class SharedInfo
|
||||
{
|
||||
public bool Sensitive { get; set; }
|
||||
public virtual bool Sensitive { get; set; }
|
||||
public bool Show { get; set; } = true;
|
||||
public int Id {get;set;}
|
||||
}
|
||||
public int Id { get; set; }
|
||||
}
|
||||
}
|
@ -130,22 +130,6 @@ namespace SharedLibraryCore.Services
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<IList<EFPenalty>> GetRecentPenalties(int count, int offset, Penalty.PenaltyType showOnly = Penalty.PenaltyType.Any)
|
||||
{
|
||||
using (var context = new DatabaseContext(true))
|
||||
{
|
||||
return await context.Penalties
|
||||
.Include(p => p.Offender.CurrentAlias)
|
||||
.Include(p => p.Punisher.CurrentAlias)
|
||||
.Where(p => showOnly == Penalty.PenaltyType.Any ? p.Type != Penalty.PenaltyType.Any : p.Type == showOnly)
|
||||
.Where(p => p.Active)
|
||||
.OrderByDescending(p => p.When)
|
||||
.Skip(offset)
|
||||
.Take(count)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IList<EFPenalty>> GetClientPenaltiesAsync(int clientId)
|
||||
{
|
||||
using (var context = new DatabaseContext(true))
|
||||
@ -159,136 +143,79 @@ namespace SharedLibraryCore.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IList<EFPenalty>> GetAllClientPenaltiesAsync(int clientId, int count, int offset, DateTime? startAt)
|
||||
public async Task<IList<PenaltyInfo>> GetRecentPenalties(int count, int offset, Penalty.PenaltyType showOnly = Penalty.PenaltyType.Any)
|
||||
{
|
||||
using (var ctx = new DatabaseContext(true))
|
||||
using (var context = new DatabaseContext(true))
|
||||
{
|
||||
var iqPenalties = ctx.Penalties.AsNoTracking()
|
||||
.Include(_penalty => _penalty.Offender.CurrentAlias)
|
||||
.Include(_penalty => _penalty.Punisher.CurrentAlias)
|
||||
.Where(_penalty => _penalty.Active)
|
||||
.Where(_penalty => _penalty.OffenderId == clientId || _penalty.PunisherId == clientId)
|
||||
.Where(_penalty => _penalty.When < startAt)
|
||||
.OrderByDescending(_penalty => _penalty.When)
|
||||
var iqPenalties = context.Penalties
|
||||
.Where(p => showOnly == Penalty.PenaltyType.Any ? p.Type != Penalty.PenaltyType.Any : p.Type == showOnly)
|
||||
.Where(p => p.Active)
|
||||
.OrderByDescending(p => p.When)
|
||||
.Skip(offset)
|
||||
.Take(count);
|
||||
.Take(count)
|
||||
.Select(_penalty => new PenaltyInfo()
|
||||
{
|
||||
Id = _penalty.PenaltyId,
|
||||
Offense = _penalty.Offense,
|
||||
AutomatedOffense = _penalty.AutomatedOffense,
|
||||
OffenderId = _penalty.OffenderId,
|
||||
OffenderName = _penalty.Offender.CurrentAlias.Name,
|
||||
PunisherId = _penalty.PunisherId,
|
||||
PunisherName = _penalty.Punisher.CurrentAlias.Name,
|
||||
PunisherLevel = _penalty.Punisher.Level,
|
||||
PenaltyType = _penalty.Type,
|
||||
Expires = _penalty.Expires,
|
||||
TimePunished = _penalty.When,
|
||||
IsEvade = _penalty.IsEvadedOffense
|
||||
});
|
||||
|
||||
#if DEBUG == true
|
||||
var querySql = iqPenalties.ToSql();
|
||||
#endif
|
||||
return await iqPenalties.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a read-only copy of client penalties
|
||||
/// retrieves penalty information for meta service
|
||||
/// </summary>
|
||||
/// <param name="clientId"></param>
|
||||
/// <param name="victim">Retreive penalties for clients receiving penalties, other wise given</param>
|
||||
/// <param name="clientId">database id of the client</param>
|
||||
/// <param name="count">how many items to retrieve</param>
|
||||
/// <param name="offset">not used</param>
|
||||
/// <param name="startAt">retreive penalties older than this</param>
|
||||
/// <returns></returns>
|
||||
public async Task<List<ProfileMeta>> ReadGetClientPenaltiesAsync(int clientId, bool victim = true)
|
||||
public async Task<IList<PenaltyInfo>> GetClientPenaltyForMetaAsync(int clientId, int count, int offset, DateTime? startAt)
|
||||
{
|
||||
using (var context = new DatabaseContext(true))
|
||||
using (var ctx = new DatabaseContext(true))
|
||||
{
|
||||
// todo: clean this up
|
||||
if (victim)
|
||||
var iqPenalties = ctx.Penalties.AsNoTracking()
|
||||
.Where(_penalty => _penalty.Active)
|
||||
.Where(_penalty => _penalty.OffenderId == clientId || _penalty.PunisherId == clientId)
|
||||
.Where(_penalty => _penalty.When < startAt)
|
||||
.OrderByDescending(_penalty => _penalty.When)
|
||||
.Skip(offset)
|
||||
.Take(count)
|
||||
.Select(_penalty => new PenaltyInfo()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var iqPenalties = from penalty in context.Penalties.AsNoTracking()
|
||||
where penalty.OffenderId == clientId
|
||||
join victimClient in context.Clients.AsNoTracking()
|
||||
on penalty.OffenderId equals victimClient.ClientId
|
||||
join victimAlias in context.Aliases.AsNoTracking()
|
||||
on victimClient.CurrentAliasId equals victimAlias.AliasId
|
||||
join punisherClient in context.Clients.AsNoTracking()
|
||||
on penalty.PunisherId equals punisherClient.ClientId
|
||||
join punisherAlias in context.Aliases.AsNoTracking()
|
||||
on punisherClient.CurrentAliasId equals punisherAlias.AliasId
|
||||
//orderby penalty.When descending
|
||||
select new ProfileMeta()
|
||||
{
|
||||
Key = "Event.Penalty",
|
||||
Value = new PenaltyInfo
|
||||
{
|
||||
Id = penalty.PenaltyId,
|
||||
OffenderName = victimAlias.Name,
|
||||
OffenderId = victimClient.ClientId,
|
||||
PunisherName = punisherAlias.Name,
|
||||
PunisherId = penalty.PunisherId,
|
||||
Offense = penalty.Offense,
|
||||
PenaltyType = penalty.Type.ToString(),
|
||||
TimeRemaining = penalty.Expires.HasValue ? (now > penalty.Expires ? "" : penalty.Expires.ToString()) : DateTime.MaxValue.ToString(),
|
||||
AutomatedOffense = penalty.AutomatedOffense,
|
||||
Expired = penalty.Expires.HasValue && penalty.Expires <= DateTime.UtcNow
|
||||
},
|
||||
When = penalty.When,
|
||||
Sensitive = penalty.Type == Penalty.PenaltyType.Flag
|
||||
};
|
||||
// fixme: is this good and fast?
|
||||
var list = await iqPenalties.ToListAsync();
|
||||
list.ForEach(p =>
|
||||
{
|
||||
// todo: why does this have to be done?
|
||||
if (((PenaltyInfo)p.Value).PenaltyType.Length < 2)
|
||||
{
|
||||
((PenaltyInfo)p.Value).PenaltyType = ((Penalty.PenaltyType)Convert.ToInt32(((PenaltyInfo)p.Value).PenaltyType)).ToString();
|
||||
}
|
||||
|
||||
var pi = ((PenaltyInfo)p.Value);
|
||||
if (pi.TimeRemaining?.Length > 0)
|
||||
{
|
||||
pi.TimeRemaining = (DateTime.Parse(((PenaltyInfo)p.Value).TimeRemaining) - now).TimeSpanText();
|
||||
|
||||
if (!pi.Expired)
|
||||
{
|
||||
pi.TimeRemaining = $"{pi.TimeRemaining} {Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PENALTY_TEMPLATE_REMAINING"]}";
|
||||
}
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
var iqPenalties = from penalty in context.Penalties.AsNoTracking()
|
||||
where penalty.PunisherId == clientId
|
||||
join victimClient in context.Clients.AsNoTracking()
|
||||
on penalty.OffenderId equals victimClient.ClientId
|
||||
join victimAlias in context.Aliases
|
||||
on victimClient.CurrentAliasId equals victimAlias.AliasId
|
||||
join punisherClient in context.Clients
|
||||
on penalty.PunisherId equals punisherClient.ClientId
|
||||
join punisherAlias in context.Aliases
|
||||
on punisherClient.CurrentAliasId equals punisherAlias.AliasId
|
||||
//orderby penalty.When descending
|
||||
select new ProfileMeta()
|
||||
{
|
||||
Key = "Event.Penalty",
|
||||
Value = new PenaltyInfo
|
||||
{
|
||||
Id = penalty.PenaltyId,
|
||||
OffenderName = victimAlias.Name,
|
||||
OffenderId = victimClient.ClientId,
|
||||
PunisherName = punisherAlias.Name,
|
||||
PunisherId = penalty.PunisherId,
|
||||
Offense = penalty.Offense,
|
||||
PenaltyType = penalty.Type.ToString(),
|
||||
AutomatedOffense = penalty.AutomatedOffense
|
||||
},
|
||||
When = penalty.When,
|
||||
Sensitive = penalty.Type == Penalty.PenaltyType.Flag
|
||||
};
|
||||
// fixme: is this good and fast?
|
||||
var list = await iqPenalties.ToListAsync();
|
||||
|
||||
list.ForEach(p =>
|
||||
{
|
||||
// todo: why does this have to be done?
|
||||
if (((PenaltyInfo)p.Value).PenaltyType.Length < 2)
|
||||
{
|
||||
((PenaltyInfo)p.Value).PenaltyType = ((Penalty.PenaltyType)Convert.ToInt32(((PenaltyInfo)p.Value).PenaltyType)).ToString();
|
||||
}
|
||||
Id = _penalty.PenaltyId,
|
||||
Offense = _penalty.Offense,
|
||||
AutomatedOffense = _penalty.AutomatedOffense,
|
||||
OffenderId = _penalty.OffenderId,
|
||||
OffenderName = _penalty.Offender.CurrentAlias.Name,
|
||||
PunisherId = _penalty.PunisherId,
|
||||
PunisherName = _penalty.Punisher.CurrentAlias.Name,
|
||||
PunisherLevel = _penalty.Punisher.Level,
|
||||
PenaltyType = _penalty.Type,
|
||||
Expires = _penalty.Expires,
|
||||
TimePunished = _penalty.When,
|
||||
IsEvade = _penalty.IsEvadedOffense
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
#if DEBUG == true
|
||||
var querySql = iqPenalties.ToSql();
|
||||
#endif
|
||||
|
||||
return await iqPenalties.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -596,19 +596,6 @@ namespace SharedLibraryCore
|
||||
return response;
|
||||
}
|
||||
|
||||
public static int ClientIdFromString(String[] lineSplit, int cIDPos)
|
||||
{
|
||||
int pID = -2; // apparently falling = -1 cID so i can't use it now
|
||||
int.TryParse(lineSplit[cIDPos].Trim(), out pID);
|
||||
|
||||
if (pID == -1) // special case similar to mod_suicide
|
||||
{
|
||||
int.TryParse(lineSplit[2], out pID);
|
||||
}
|
||||
|
||||
return pID;
|
||||
}
|
||||
|
||||
public static Dictionary<string, string> DictionaryFromKeyValue(this string eventLine)
|
||||
{
|
||||
string[] values = eventLine.Substring(1).Split('\\');
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore;
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
@ -24,10 +25,14 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
#if DEBUG == true
|
||||
var client = Utilities.IW4MAdminClient();
|
||||
bool loginSuccess = true;
|
||||
#else
|
||||
var client = Manager.GetPrivilegedClients()[clientId];
|
||||
|
||||
bool loginSuccess = Manager.TokenAuthenticator.AuthorizeToken(client.NetworkId, password) ||
|
||||
(await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(password, client.PasswordSalt)))[0] == client.Password;
|
||||
#endif
|
||||
|
||||
if (loginSuccess)
|
||||
{
|
||||
|
@ -119,7 +119,7 @@ namespace WebfrontCore.Controllers
|
||||
Name = c.Name,
|
||||
Level = c.Level.ToLocalizedLevelName(),
|
||||
LevelInt = (int)c.Level,
|
||||
// todo: add back last seen for search
|
||||
LastConnection = c.LastConnection,
|
||||
ClientId = c.ClientId
|
||||
})
|
||||
.ToList();
|
||||
|
@ -10,12 +10,13 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using WebfrontCore.ViewComponents;
|
||||
using static SharedLibraryCore.Objects.Penalty;
|
||||
|
||||
namespace WebfrontCore.Controllers
|
||||
{
|
||||
public class PenaltyController : BaseController
|
||||
{
|
||||
public IActionResult List(int showOnly = (int)SharedLibraryCore.Objects.Penalty.PenaltyType.Any)
|
||||
public IActionResult List(PenaltyType showOnly = PenaltyType.Any)
|
||||
{
|
||||
ViewBag.Description = "List of all the recent penalties (bans, kicks, warnings) on IW4MAdmin";
|
||||
ViewBag.Title = Localization["WEBFRONT_PENALTY_TITLE"];
|
||||
@ -24,12 +25,12 @@ namespace WebfrontCore.Controllers
|
||||
return View((SharedLibraryCore.Objects.Penalty.PenaltyType)showOnly);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ListAsync(int offset = 0, int showOnly = (int)SharedLibraryCore.Objects.Penalty.PenaltyType.Any)
|
||||
public async Task<IActionResult> ListAsync(int offset = 0, PenaltyType showOnly = PenaltyType.Any)
|
||||
{
|
||||
return await Task.FromResult(View("_List", new ViewModels.PenaltyFilterInfo()
|
||||
{
|
||||
Offset = offset,
|
||||
ShowOnly = (SharedLibraryCore.Objects.Penalty.PenaltyType)showOnly
|
||||
ShowOnly = showOnly
|
||||
}));
|
||||
}
|
||||
|
||||
@ -47,7 +48,7 @@ namespace WebfrontCore.Controllers
|
||||
// todo: this seems like it's pulling unnecessary info from LINQ to entities.
|
||||
var iqPenalties = ctx.Penalties
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Type == SharedLibraryCore.Objects.Penalty.PenaltyType.Ban && p.Active)
|
||||
.Where(p => p.Type == PenaltyType.Ban && p.Active)
|
||||
.OrderByDescending(_penalty => _penalty.When)
|
||||
.Select(p => new PenaltyInfo()
|
||||
{
|
||||
@ -61,9 +62,7 @@ namespace WebfrontCore.Controllers
|
||||
PunisherNetworkId = (ulong)p.Punisher.NetworkId,
|
||||
PunisherName = p.Punisher.CurrentAlias.Name,
|
||||
PunisherIPAddress = Authorized ? p.Punisher.CurrentAlias.IPAddress.ConvertIPtoString() : null,
|
||||
PenaltyType = p.Type.ToString(),
|
||||
TimePunished = p.When.ToString(),
|
||||
TimeRemaining = null,
|
||||
TimePunished = p.When,
|
||||
AutomatedOffense = Authorized ? p.AutomatedOffense : null,
|
||||
});
|
||||
#if DEBUG == true
|
||||
|
@ -1,9 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Objects;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -11,42 +7,14 @@ namespace WebfrontCore.ViewComponents
|
||||
{
|
||||
public class PenaltyListViewComponent : ViewComponent
|
||||
{
|
||||
private const int PENALTY_COUNT = 15;
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(int offset, Penalty.PenaltyType showOnly)
|
||||
{
|
||||
string showEvadeString(EFPenalty penalty) => penalty.IsEvadedOffense == true ?
|
||||
$"({Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PENALTY_EVADE"]}) " : "";
|
||||
var penalties = await Program.Manager.GetPenaltyService().GetRecentPenalties(PENALTY_COUNT, offset, showOnly);
|
||||
penalties = User.Identity.IsAuthenticated ? penalties : penalties.Where(p => !p.Sensitive).ToList();
|
||||
|
||||
var penalties = await Program.Manager.GetPenaltyService().GetRecentPenalties(12, offset, showOnly);
|
||||
var penaltiesDto = penalties.Select(p => new PenaltyInfo()
|
||||
{
|
||||
Id = p.PenaltyId,
|
||||
OffenderId = p.OffenderId,
|
||||
OffenderName = p.Offender.Name,
|
||||
PunisherId = p.PunisherId,
|
||||
PunisherName = p.Punisher.Name,
|
||||
PunisherLevel = p.Punisher.Level.ToLocalizedLevelName(),
|
||||
PunisherLevelId = (int)p.Punisher.Level,
|
||||
#if DEBUG
|
||||
Offense = !string.IsNullOrEmpty(p.AutomatedOffense) ? p.AutomatedOffense : p.Offense,
|
||||
#else
|
||||
Offense = (User.Identity.IsAuthenticated && !string.IsNullOrEmpty(p.AutomatedOffense)) ?
|
||||
$"{showEvadeString(p)}{p.AutomatedOffense}" :
|
||||
$"{showEvadeString(p)}{p.Offense}",
|
||||
#endif
|
||||
PenaltyType = p.Type.ToString(),
|
||||
TimePunished = Utilities.GetTimePassed(p.When, false),
|
||||
// show time passed if ban
|
||||
TimeRemaining = DateTime.UtcNow > p.Expires ? "" : $"{((p.Expires ?? DateTime.MaxValue).Year == DateTime.MaxValue.Year ? Utilities.GetTimePassed(p.When, true) : Utilities.TimeSpanText((p.Expires ?? DateTime.MaxValue) - DateTime.UtcNow))}",
|
||||
Sensitive = p.Type == Penalty.PenaltyType.Flag,
|
||||
AutomatedOffense = p.AutomatedOffense
|
||||
});
|
||||
|
||||
#if DEBUG
|
||||
penaltiesDto = penaltiesDto.ToList();
|
||||
#else
|
||||
penaltiesDto = User.Identity.IsAuthenticated ? penaltiesDto.ToList() : penaltiesDto.Where(p => !p.Sensitive).ToList();
|
||||
#endif
|
||||
return View("_List", penaltiesDto);
|
||||
return View("_List", penalties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,22 +3,38 @@
|
||||
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
|
||||
}
|
||||
|
||||
<div class="mr-auto ml-auto col-12 col-lg-7 border-bottom">
|
||||
<h4 class="pb-2 text-center ">@ViewBag.Title</h4>
|
||||
<div class="row d-none d-lg-block ">
|
||||
<h4 class="pb-2 text-center col-12">@ViewBag.Title</h4>
|
||||
<div class="mr-auto ml-auto col-12 col-lg-8 border-bottom">
|
||||
<div class="row pt-2 pb-2 bg-primary">
|
||||
<div class="col-5 ">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</div>
|
||||
<div class="col-4">@loc["WEBFRONT_PROFILE_LEVEL"]</div>
|
||||
<div class="col-3 text-right">@loc["WEBFRONT_PROFILE_LSEEN"]</div>
|
||||
<div class="col-3 text-right">@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]</div>
|
||||
</div>
|
||||
|
||||
@{
|
||||
foreach (var client in Model)
|
||||
@foreach (var client in Model)
|
||||
{
|
||||
<div class="row pt-2 pb-2 bg-dark">
|
||||
<div class="col-5">@Html.ActionLink(client.Name, "ProfileAsync", "Client", new { id = client.ClientId })</div>
|
||||
<div class="col-4 level-color-@client.LevelInt">@client.Level</div>
|
||||
@*<div class="col-3 text-right">@client.LastSeen @loc["WEBFRONT_PENALTY_TEMPLATE_AGO"]</div>*@
|
||||
<div class="col-3 text-right">@client.LastConnectionText</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row d-lg-none">
|
||||
<div class="w-100 bg-primary text-center h3 mb-0 p-3" style="border-bottom: 1px solid #222">@ViewBag.Title</div>
|
||||
@foreach (var client in Model)
|
||||
{
|
||||
<div class="col-5 bg-primary font-weight-bold" style="border-bottom: 1px solid #222">
|
||||
<div class="p-2">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</div>
|
||||
<div class="p-2">@loc["WEBFRONT_PROFILE_LEVEL"]</div>
|
||||
<div class="p-2">@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]</div>
|
||||
</div>
|
||||
<div class="col-7 bg-dark border-bottom">
|
||||
<div class="p-2">@Html.ActionLink(client.Name, "ProfileAsync", "Client", new { id = client.ClientId },new { @class = "link-inverse" } )</div>
|
||||
<div class="p-2 level-color-@client.LevelInt">@client.Level</div>
|
||||
<div class="p-2 text-white-50">@client.LastConnectionText</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
@ -1,12 +0,0 @@
|
||||
@model IEnumerable<SharedLibraryCore.Dtos.ChatInfo>
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<div class="client-message-context bg-dark p-2 mt-2 mb-2 border-top border-bottom">
|
||||
<h5>@Model.First().Time.ToString()</h5>
|
||||
@foreach (var message in Model)
|
||||
{
|
||||
<span class="text-white">@message.Name</span><span> — @message.Message</span><br />
|
||||
}
|
||||
</div>
|
@ -2,7 +2,7 @@
|
||||
@{
|
||||
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
|
||||
}
|
||||
<h4 class="pb-3 text-center ">@ViewBag.Title</h4>
|
||||
<h4 class="pb-3 text-center">@ViewBag.Title</h4>
|
||||
<div class="row">
|
||||
<select class="form-control bg-dark text-muted" id="penalty_filter_selection">
|
||||
@{
|
||||
@ -36,7 +36,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-striped">
|
||||
<thead class="d-none d-md-table-header-group">
|
||||
<thead class="d-none d-lg-table-header-group">
|
||||
<tr class="bg-primary pt-2 pb-2">
|
||||
<th scope="col">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</th>
|
||||
<th scope="col">@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]</th>
|
||||
@ -53,10 +53,10 @@
|
||||
})
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="table d-table d-md-none">
|
||||
<table class="table d-table d-lg-none">
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<span id="load_penalties_button" class="oi oi-chevron-bottom text-center text-primary w-100 h3 pb-0 mb-0 d-none d-md-block"></span>
|
||||
<span id="load_penalties_button" class="oi oi-chevron-bottom text-center text-primary w-100 h3 pb-0 mb-0 d-none d-lg-block"></span>
|
||||
</div>
|
||||
|
||||
@section scripts {
|
||||
|
@ -5,72 +5,72 @@
|
||||
|
||||
@model SharedLibraryCore.Dtos.PenaltyInfo
|
||||
|
||||
<tr class="d-table-row d-md-none bg-dark">
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</th>
|
||||
<td>
|
||||
@Html.ActionLink(Model.OffenderName, "ProfileAsync", "Client", new { id = Model.OffenderId }, new { @class = "link-inverse" })
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-table-row d-md-none bg-dark">
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]</th>
|
||||
<td class="penalties-color-@Model.PenaltyType.ToLower()">
|
||||
<td class="penalties-color-@Model.PenaltyTypeText.ToLower()">
|
||||
@Model.PenaltyType
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-table-row d-md-none bg-dark">
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@loc["WEBFRONT_PENALTY_TEMPLATE_OFFENSE"]</th>
|
||||
<td class="text-light">
|
||||
@Model.Offense
|
||||
@($"{Model.Offense}{(ViewBag.Authorized ? Model.AdditionalPenaltyInformation : "")}")
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-table-row d-md-none bg-dark">
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
|
||||
<td>
|
||||
@Html.ActionLink(Model.PunisherName, "ProfileAsync", "Client", new { id = Model.PunisherId }, new { @class = "level-color-" + Model.PunisherLevelId })
|
||||
@Html.ActionLink(Model.PunisherName, "ProfileAsync", "Client", new { id = Model.PunisherId }, new { @class = "level-color-" + (int)Model.PunisherLevel })
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-table-row d-md-none bg-dark">
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="w-25 bg-primary" style="border-bottom: 1px solid #222">@loc["WEBFRONT_PENALTY_TEMPLATE_TIME"]</th>
|
||||
<td class="text-light mb-2 border-bottom">
|
||||
@{
|
||||
if (Model.TimeRemaining == string.Empty)
|
||||
if (Model.Expired)
|
||||
{
|
||||
<span>@Model.TimePunished @loc["WEBFRONT_PENALTY_TEMPLATE_AGO"]</span>
|
||||
<span>@Model.TimePunishedString</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span> @Model.TimeRemaining</span>
|
||||
<span>@Model.TimeRemaining</span>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-none d-md-table-row">
|
||||
<tr class="d-none d-lg-table-row">
|
||||
<td>
|
||||
@Html.ActionLink(Model.OffenderName, "ProfileAsync", "Client", new { id = Model.OffenderId }, new { @class = "link-inverse" })
|
||||
</td>
|
||||
<td class="penalties-color-@Model.PenaltyType.ToLower()">
|
||||
<td class="penalties-color-@Model.PenaltyTypeText.ToLower()">
|
||||
@Model.PenaltyType
|
||||
</td>
|
||||
<td class="text-light w-50">
|
||||
@Model.Offense
|
||||
@($"{Model.Offense}{(ViewBag.Authorized ? Model.AdditionalPenaltyInformation : "")}")
|
||||
</td>
|
||||
<td>
|
||||
@Html.ActionLink(Model.PunisherName, "ProfileAsync", "Client", new { id = Model.PunisherId }, new { @class = "level-color-" + Model.PunisherLevelId })
|
||||
@Html.ActionLink(Model.PunisherName, "ProfileAsync", "Client", new { id = Model.PunisherId }, new { @class = "level-color-" + (int)Model.PunisherLevel })
|
||||
</td>
|
||||
<td class="text-right text-light">
|
||||
@{
|
||||
if (Model.TimeRemaining == string.Empty)
|
||||
if (Model.Expired)
|
||||
{
|
||||
<span>@Model.TimePunished @loc["WEBFRONT_PENALTY_TEMPLATE_AGO"]</span>
|
||||
<span>@Model.TimePunishedString</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span> @Model.TimeRemaining </span>
|
||||
<span>@Model.TimeRemaining</span>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
|
@ -9,24 +9,24 @@
|
||||
var penalty = meta.Value as SharedLibraryCore.Dtos.PenaltyInfo;
|
||||
|
||||
string localizationKey = meta.Type == SharedLibraryCore.Dtos.ProfileMeta.MetaType.Penalized ?
|
||||
$"WEBFRONT_CLIENT_META_PENALIZED_{penalty.PenaltyType.ToUpper()}" :
|
||||
$"WEBFRONT_CLIENT_META_WAS_PENALIZED_{penalty.PenaltyType.ToUpper()}";
|
||||
$"WEBFRONT_CLIENT_META_PENALIZED_{penalty.PenaltyTypeText.ToUpper()}" :
|
||||
$"WEBFRONT_CLIENT_META_WAS_PENALIZED_{penalty.PenaltyTypeText.ToUpper()}";
|
||||
|
||||
string localizationMessage = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex[localizationKey];
|
||||
var regexMatch = System.Text.RegularExpressions.Regex.Match(localizationMessage, @"^.*{{([^{}]+)}}.+$");
|
||||
string penaltyType = regexMatch.Groups[1].Value.ToString();
|
||||
|
||||
localizationMessage = localizationMessage.Replace(penaltyType, $"<span class='penalties-color-{penalty.PenaltyType.ToLower()}'>{penaltyType}</span>");
|
||||
localizationMessage = localizationMessage.Replace(penaltyType, $"<span class='penalties-color-{penalty.PenaltyTypeText.ToLower()}'>{penaltyType}</span>");
|
||||
|
||||
return meta.Type == SharedLibraryCore.Dtos.ProfileMeta.MetaType.Penalized ?
|
||||
string.Format(localizationMessage,
|
||||
$"<span class='text-highlight'><a class='link-inverse' href='{penalty.OffenderId}'>{penalty.OffenderName}</a></span>",
|
||||
$"<span class='automated-penalty-info-detailed text-white' data-clientid='{penalty.OffenderId}'>{penalty.Offense}</span>")
|
||||
$"<span class='{(ViewBag.Authorized ? "automated-penalty-info-detailed" : "")} text-white' data-clientid='{penalty.OffenderId}'>{penalty.Offense} {(ViewBag.Authorized ? penalty.AdditionalPenaltyInformation : "")}</span>")
|
||||
.Replace("{", "")
|
||||
.Replace("}", "") :
|
||||
string.Format(localizationMessage,
|
||||
$"<span class='text-highlight'><a class='link-inverse' href='{penalty.PunisherId}'>{penalty.PunisherName}</a></span>",
|
||||
$"<span class='automated-penalty-info-detailed text-white' data-clientid='{penalty.OffenderId}'>{(ViewBag.Authorized && !string.IsNullOrEmpty(penalty.AutomatedOffense) ? $"{penalty.Offense} ({penalty.AutomatedOffense})" : penalty.Offense)}</span>",
|
||||
$"<span class='{(ViewBag.Authorized ? "automated-penalty-info-detailed" : "")} text-white' data-clientid='{penalty.OffenderId}'>{penalty.Offense} {(ViewBag.Authorized ? penalty.AdditionalPenaltyInformation : "")}</span>",
|
||||
penalty.Offense)
|
||||
.Replace("{", "")
|
||||
.Replace("}", "");
|
||||
@ -55,7 +55,7 @@
|
||||
case SharedLibraryCore.Dtos.ProfileMeta.MetaType.ChatMessage:
|
||||
<div class="profile-meta-entry loader-data-time" data-time="@meta.When">
|
||||
<span style="color:white;">></span>
|
||||
<span class="client-message text-muted" data-serverid="@meta.Extra" data-when="@meta.When"> @meta.Value</span>
|
||||
<span class="client-message text-muted" data-serverid="@meta.Extra" data-when="@meta.When" title="@SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_MESSAGE_CONTEXT"]"> @meta.Value</span>
|
||||
</div>
|
||||
break;
|
||||
case SharedLibraryCore.Dtos.ProfileMeta.MetaType.ReceivedPenalty:
|
||||
|
@ -6318,6 +6318,10 @@ a.link-inverse:hover {
|
||||
.d-md-table-header-group {
|
||||
display: table-header-group !important; } }
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.d-lg-table-header-group {
|
||||
display: table-header-group !important; } }
|
||||
|
||||
#console_command_response {
|
||||
min-height: 20rem; }
|
||||
|
||||
|
@ -88,7 +88,13 @@ a.link-inverse:hover {
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.d-md-table-header-group {
|
||||
display: table-header-group !important
|
||||
display: table-header-group !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.d-lg-table-header-group {
|
||||
display: table-header-group !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user