Files
tbd-station-14/Content.Client/Administration/UI/Logs/AdminLogsWindow.xaml.cs
Javier Guardia Fernández 319aec109d Admin logs (#5419)
* Add admin logging, models, migrations

* Add logging damage changes

* Add Log admin flag, LogFilter, Logs admin menu tab, message
Refactor admin logging API

* Change admin log get method names

* Fix the name again

* Minute amount of reorganization

* Reset Postgres db snapshot

* Reset Sqlite db snapshot

* Make AdminLog have a composite primary key of round, id

* Minute cleanup

* Change admin system to do a type check instead of index check

* Make admin logs use C# 10 interpolated string handlers

* Implement UI on its own window
Custom controls
Searching
Add admin log converters

* Implement limits into the query

* Change logs to be put into an OutputPanel instead for text wrapping

* Add log <-> player m2m relationship back

* UI improvements, make text wrap, add separators

* Remove entity prefix from damaged log

* Add explicit m2m model, fix any players filter

* Add debug command to test bulk adding logs

* Admin logs now just kinda go

* Add histogram for database update time

* Make admin log system update run every 5 seconds

* Add a cap to the log queue and a metric for how many times it has been reached

* Add metric for logs sent in a round

* Make cvars out of admin logs queue send delay and cap

* Merge fixes

* Reset some changes

* Add test for adding and getting a single log

* Add tests for bulk adding logs

* Add test for querying logs

* Add CallerArgumentExpression to LogStringHandler methods and test

* Improve UI, fix SQLite, add searching by round

* Add entities to admin logs

* Move distinct after orderby

* Add migrations

* ef core eat my ass

* Add cvar for client logs batch size

* Sort logs from newest to oldest by default

* Merge fixes

* Reorganize tests and add one for date ordering

* Add note to log types to not change their numeric values

* Add impacts to logs, better UI filtering

* Make log add callable from shared for convenience

* Get current round id directly from game ticker

* Revert namespace change for DamageableSystem
2021-11-22 18:49:26 +01:00

440 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Content.Client.Administration.UI.CustomControls;
using Content.Shared.Administration.Logs;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
using static Robust.Client.UserInterface.Controls.BaseButton;
using static Robust.Client.UserInterface.Controls.LineEdit;
namespace Content.Client.Administration.UI.Logs;
[GenerateTypedNameReferences]
public partial class AdminLogsWindow : SS14Window
{
private readonly Comparer<AdminLogTypeButton> _adminLogTypeButtonComparer =
Comparer<AdminLogTypeButton>.Create((a, b) =>
string.Compare(a.Type.ToString(), b.Type.ToString(), StringComparison.Ordinal));
private readonly Comparer<AdminLogPlayerButton> _adminLogPlayerButtonComparer =
Comparer<AdminLogPlayerButton>.Create((a, b) =>
string.Compare(a.Text, b.Text, StringComparison.Ordinal));
public AdminLogsWindow()
{
RobustXamlLoader.Load(this);
TypeSearch.OnTextChanged += TypeSearchChanged;
PlayerSearch.OnTextChanged += PlayerSearchChanged;
LogSearch.OnTextChanged += LogSearchChanged;
SelectAllTypesButton.OnPressed += SelectAllTypes;
SelectNoTypesButton.OnPressed += SelectNoTypes;
SelectNoPlayersButton.OnPressed += SelectNoPlayers;
RoundSpinBox.IsValid = i => i > 0 && i <= CurrentRound;
RoundSpinBox.ValueChanged += RoundSpinBoxChanged;
RoundSpinBox.InitDefaultButtons();
ResetRoundButton.OnPressed += ResetRoundPressed;
SetImpacts(Enum.GetValues<LogImpact>().OrderBy(impact => impact).ToArray());
SetTypes(Enum.GetValues<LogType>());
}
private int CurrentRound { get; set; }
private HashSet<LogType> SelectedTypes { get; } = new();
private HashSet<Guid> SelectedPlayers { get; } = new();
private HashSet<LogImpact> SelectedImpacts { get; } = new();
public void SetCurrentRound(int round)
{
CurrentRound = round;
ResetRoundButton.Text = Loc.GetString("admin-logs-reset-with-id", ("id", round));
UpdateResetButton();
}
public void SetRoundSpinBox(int round)
{
RoundSpinBox.Value = round;
UpdateResetButton();
}
private void RoundSpinBoxChanged(object? sender, ValueChangedEventArgs args)
{
UpdateResetButton();
}
private void UpdateResetButton()
{
ResetRoundButton.Disabled = RoundSpinBox.Value == CurrentRound;
}
private void ResetRoundPressed(ButtonEventArgs args)
{
RoundSpinBox.Value = CurrentRound;
}
private void TypeSearchChanged(LineEditEventArgs args)
{
UpdateTypes();
}
private void PlayerSearchChanged(LineEditEventArgs obj)
{
UpdatePlayers();
}
private void LogSearchChanged(LineEditEventArgs args)
{
UpdateLogs();
}
private void SelectAllTypes(ButtonEventArgs obj)
{
foreach (var control in TypesContainer.Children)
{
if (control is not AdminLogTypeButton type)
{
continue;
}
type.Pressed = true;
}
UpdateLogs();
}
private void SelectNoTypes(ButtonEventArgs obj)
{
foreach (var control in TypesContainer.Children)
{
if (control is not AdminLogTypeButton type)
{
continue;
}
type.Pressed = false;
type.Visible = ShouldShowType(type);
}
UpdateLogs();
}
private void SelectNoPlayers(ButtonEventArgs obj)
{
foreach (var control in PlayersContainer.Children)
{
if (control is not AdminLogPlayerButton player)
{
continue;
}
player.Pressed = false;
}
UpdateLogs();
}
public void UpdateTypes()
{
foreach (var control in TypesContainer.Children)
{
if (control is not AdminLogTypeButton type)
{
continue;
}
type.Visible = ShouldShowType(type);
}
}
private void UpdatePlayers()
{
foreach (var control in PlayersContainer.Children)
{
if (control is not AdminLogPlayerButton player)
{
continue;
}
player.Visible = ShouldShowPlayer(player);
}
}
private void UpdateLogs()
{
foreach (var child in LogsContainer.Children)
{
if (child is not AdminLogLabel log)
{
continue;
}
child.Visible = ShouldShowLog(log);
}
}
private bool ShouldShowType(AdminLogTypeButton button)
{
return button.Text != null &&
button.Text.Contains(TypeSearch.Text, StringComparison.OrdinalIgnoreCase);
}
private bool ShouldShowPlayer(AdminLogPlayerButton button)
{
return button.Text != null &&
button.Text.Contains(PlayerSearch.Text, StringComparison.OrdinalIgnoreCase);
}
private bool ShouldShowLog(AdminLogLabel label)
{
return SelectedTypes.Contains(label.Log.Type) &&
SelectedPlayers.Overlaps(label.Log.Players) &&
SelectedImpacts.Contains(label.Log.Impact) &&
label.Log.Message.Contains(LogSearch.Text, StringComparison.OrdinalIgnoreCase);
}
private void TypeButtonPressed(ButtonEventArgs args)
{
var button = (AdminLogTypeButton) args.Button;
if (button.Pressed)
{
SelectedTypes.Add(button.Type);
}
else
{
SelectedTypes.Remove(button.Type);
}
UpdateLogs();
}
private void PlayerButtonPressed(ButtonEventArgs args)
{
var button = (AdminLogPlayerButton) args.Button;
if (button.Pressed)
{
SelectedPlayers.Add(button.Id);
}
else
{
SelectedPlayers.Remove(button.Id);
}
UpdateLogs();
}
private void ImpactButtonPressed(ButtonEventArgs args)
{
var button = (AdminLogImpactButton) args.Button;
if (button.Pressed)
{
SelectedImpacts.Add(button.Impact);
}
else
{
SelectedImpacts.Remove(button.Impact);
}
UpdateLogs();
}
private void SetImpacts(LogImpact[] impacts)
{
LogImpactContainer.RemoveAllChildren();
foreach (var impact in impacts)
{
var button = new AdminLogImpactButton(impact)
{
Text = impact.ToString()
};
SelectedImpacts.Add(impact);
button.OnPressed += ImpactButtonPressed;
LogImpactContainer.AddChild(button);
}
switch (impacts.Length)
{
case 0:
return;
case 1:
LogImpactContainer.GetChild(0).StyleClasses.Add("OpenRight");
return;
}
for (var i = 0; i < impacts.Length - 1; i++)
{
LogImpactContainer.GetChild(i).StyleClasses.Add("ButtonSquare");
}
LogImpactContainer.GetChild(LogImpactContainer.ChildCount - 1).StyleClasses.Add("OpenLeft");
}
private void SetTypes(LogType[] types)
{
var newTypes = types.ToHashSet();
var buttons = new SortedSet<AdminLogTypeButton>(_adminLogTypeButtonComparer);
foreach (var control in TypesContainer.Children.ToArray())
{
if (control is not AdminLogTypeButton type ||
!newTypes.Remove(type.Type))
{
continue;
}
buttons.Add(type);
}
foreach (var type in newTypes)
{
var button = new AdminLogTypeButton(type)
{
Text = type.ToString()
};
SelectedTypes.Add(type);
button.OnPressed += TypeButtonPressed;
buttons.Add(button);
}
TypesContainer.RemoveAllChildren();
foreach (var type in buttons)
{
TypesContainer.AddChild(type);
}
UpdateLogs();
}
public void SetPlayers(Dictionary<Guid, string> players)
{
var buttons = new SortedSet<AdminLogPlayerButton>(_adminLogPlayerButtonComparer);
foreach (var control in PlayersContainer.Children.ToArray())
{
if (control is not AdminLogPlayerButton player ||
!players.Remove(player.Id))
{
continue;
}
buttons.Add(player);
}
foreach (var (id, name) in players)
{
var button = new AdminLogPlayerButton(id)
{
Text = name
};
SelectedPlayers.Add(id);
button.OnPressed += PlayerButtonPressed;
buttons.Add(button);
}
PlayersContainer.RemoveAllChildren();
foreach (var player in buttons)
{
PlayersContainer.AddChild(player);
}
UpdateLogs();
}
public void AddLogs(SharedAdminLog[] logs)
{
for (var i = 0; i < logs.Length; i++)
{
ref var log = ref logs[i];
var separator = new HSeparator();
var label = new AdminLogLabel(ref log, separator);
label.Visible = ShouldShowLog(label);
LogsContainer.AddChild(label);
LogsContainer.AddChild(separator);
}
}
public void SetLogs(SharedAdminLog[] logs)
{
LogsContainer.RemoveAllChildren();
AddLogs(logs);
}
public int GetSelectedRoundId()
{
return RoundSpinBox.Value;
}
public List<LogType> GetSelectedLogTypes()
{
var types = new List<LogType>();
foreach (var control in TypesContainer.Children)
{
if (control is not AdminLogTypeButton {Text: { }, Pressed: true} type)
{
continue;
}
types.Add(Enum.Parse<LogType>(type.Text));
}
return types;
}
public Guid[] GetSelectedPlayerIds()
{
var players = new List<Guid>();
foreach (var control in PlayersContainer.Children)
{
if (control is not AdminLogPlayerButton {Pressed: true} player)
{
continue;
}
players.Add(player.Id);
}
return players.ToArray();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
TypeSearch.OnTextChanged -= TypeSearchChanged;
PlayerSearch.OnTextChanged -= PlayerSearchChanged;
LogSearch.OnTextChanged -= LogSearchChanged;
SelectAllTypesButton.OnPressed -= SelectAllTypes;
SelectNoTypesButton.OnPressed -= SelectNoTypes;
SelectNoPlayersButton.OnPressed -= SelectNoPlayers;
RoundSpinBox.IsValid = null;
RoundSpinBox.ValueChanged -= RoundSpinBoxChanged;
ResetRoundButton.OnPressed -= ResetRoundPressed;
}
}