97 lines
3.7 KiB
C#
97 lines
3.7 KiB
C#
using System.Text.Json;
|
|
|
|
namespace ScadaLink.CLI.Commands;
|
|
|
|
/// <summary>
|
|
/// Human-readable table formatter for <c>audit query --format table</c> (Audit Log
|
|
/// #23 M8-T6). Renders each fetched page as a column-aligned text table with a fixed
|
|
/// column set (<see cref="Columns"/>). Long free-text fields (Target, Actor) are
|
|
/// truncated with an ellipsis so columns stay aligned regardless of payload size.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A header row is emitted once per page (matching the streamable, page-at-a-time
|
|
/// contract of <see cref="IAuditFormatter"/>). An empty page emits the header only,
|
|
/// so the column shape is visible even with zero results.
|
|
/// </remarks>
|
|
public sealed class TableAuditFormatter : IAuditFormatter
|
|
{
|
|
/// <summary>JSON property name (camelCase, as the server serializes it) → column header.</summary>
|
|
private static readonly (string Property, string Header, int MaxWidth)[] Columns =
|
|
{
|
|
("occurredAtUtc", "OccurredAtUtc", 24),
|
|
("channel", "Channel", 14),
|
|
("kind", "Kind", 18),
|
|
("status", "Status", 12),
|
|
("target", "Target", 32),
|
|
("actor", "Actor", 20),
|
|
("durationMs", "DurationMs", 10),
|
|
("httpStatus", "HttpStatus", 10),
|
|
};
|
|
|
|
public void WritePage(IReadOnlyList<JsonElement> events, TextWriter output)
|
|
{
|
|
// Build every cell first so column widths account for the actual data.
|
|
var rows = new List<string[]>(events.Count);
|
|
foreach (var evt in events)
|
|
{
|
|
var cells = new string[Columns.Length];
|
|
for (var i = 0; i < Columns.Length; i++)
|
|
cells[i] = Truncate(CellValue(evt, Columns[i].Property), Columns[i].MaxWidth);
|
|
rows.Add(cells);
|
|
}
|
|
|
|
var widths = new int[Columns.Length];
|
|
for (var i = 0; i < Columns.Length; i++)
|
|
widths[i] = Columns[i].Header.Length;
|
|
foreach (var row in rows)
|
|
for (var i = 0; i < Columns.Length; i++)
|
|
widths[i] = Math.Max(widths[i], row[i].Length);
|
|
|
|
WriteRow(output, Columns.Select(c => c.Header).ToArray(), widths);
|
|
foreach (var row in rows)
|
|
WriteRow(output, row, widths);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts a cell value for <paramref name="property"/> from an audit event.
|
|
/// A missing property or a JSON <c>null</c> renders as an empty string (never
|
|
/// the literal text "null").
|
|
/// </summary>
|
|
private static string CellValue(JsonElement evt, string property)
|
|
{
|
|
if (evt.ValueKind != JsonValueKind.Object
|
|
|| !evt.TryGetProperty(property, out var value)
|
|
|| value.ValueKind == JsonValueKind.Null)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return value.ValueKind == JsonValueKind.String
|
|
? value.GetString() ?? string.Empty
|
|
: value.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Truncates <paramref name="value"/> to <paramref name="maxWidth"/> characters,
|
|
/// replacing the tail with a single-character ellipsis so the column stays aligned.
|
|
/// </summary>
|
|
private static string Truncate(string value, int maxWidth)
|
|
{
|
|
if (maxWidth <= 0 || value.Length <= maxWidth)
|
|
return value;
|
|
if (maxWidth == 1)
|
|
return "…";
|
|
return value.Substring(0, maxWidth - 1) + "…";
|
|
}
|
|
|
|
private static void WriteRow(TextWriter output, IReadOnlyList<string> cells, int[] widths)
|
|
{
|
|
for (var i = 0; i < cells.Count; i++)
|
|
{
|
|
// Last column is not padded — avoids trailing whitespace at line end.
|
|
output.Write(i == cells.Count - 1 ? cells[i] : cells[i].PadRight(widths[i] + 2));
|
|
}
|
|
output.WriteLine();
|
|
}
|
|
}
|