feat: add stacked progress bar for parent jobs with sub-job breakdown

Parent jobs with fetched sub-jobs now show a multi-color stacked
progress bar instead of the single parent-progress bar. Segments
represent sub-job status distribution: completed (success),
running (primary), waiting (info), failed (danger), with-errors
(warning), and queued (primary for visibility in dark theme).

ProgressBar gets an optional Segments parameter for stacked mode.
JobRow accepts Children and computes status-weighted segments.
JobTree passes cached children to JobRow.
This commit is contained in:
REDCODE
2026-07-08 00:20:46 +02:00
parent 6c9a608ce6
commit defd97e7f0
3 changed files with 73 additions and 6 deletions

View File

@@ -17,7 +17,11 @@
<span class="text-truncate text-muted small text-end">@(Job.Started?.ToLocalTime().ToString("g")) &mdash; @(Job.Finished?.ToLocalTime().ToString("g"))</span>
} else {
<span class="flex-shrink-0" style="width: 90px;">
<ProgressBar Value="@Job.Progress" Color="@StatusColor" />
@if (_segments is not null) {
<ProgressBar Segments="@_segments" />
} else {
<ProgressBar Value="@Job.Progress" Color="@StatusColor" />
}
</span>
}
<span class="flex-shrink-0 text-muted small" style="width: 60px; text-align: right;">@Elapsed</span>
@@ -55,6 +59,46 @@
[Parameter] public string? ChildSummaryText { get; set; }
[Parameter] public bool IsExpanded { get; set; }
[Parameter] public EventCallback<bool> ExpandedChanged { get; set; }
[Parameter] public IReadOnlyList<JobStatusDto>? Children { get; set; }
static readonly Dictionary<EJobStatus, string> _statusColors = new() {
[EJobStatus.Queued] = "primary",
[EJobStatus.Running] = "primary",
[EJobStatus.Waiting] = "info",
[EJobStatus.Completed] = "success",
[EJobStatus.CompletedWithErrors] = "warning",
[EJobStatus.Failed] = "danger",
[EJobStatus.Canceled] = "secondary",
};
static readonly EJobStatus[] _segmentOrder = [
EJobStatus.Completed,
EJobStatus.CompletedWithErrors,
EJobStatus.Failed,
EJobStatus.Canceled,
EJobStatus.Running,
EJobStatus.Waiting,
EJobStatus.Queued,
];
List<(float Width, string Color)>? _segments;
protected override void OnParametersSet() {
_segments = ComputeSegments();
}
List<(float Width, string Color)>? ComputeSegments() {
if (Children is not { Count: > 0 }) return null;
var total = (float)Children.Count;
var ordered = _segmentOrder
.Select(s => (Status: s, Children.Where(c => c.Status == s).ToList()))
.Where(g => g.Item2.Count > 0)
.Select(g => (g.Item2.Count / total, _statusColors[g.Status]))
.ToList();
return ordered;
}
string Icon => Job.JobType switch {
EJobType.FileSystemScan => "<i class=\"bi bi-folder2-open\"></i>",

View File

@@ -13,6 +13,7 @@
Job="root"
Level="@Level"
ChildSummaryText="@ChildSummary(root, hasFetched, children)"
Children="@(hasFetched ? children : null)"
IsExpanded="@(_expandedParents.Contains(root.Id) && hasFetched)"
ExpandedChanged="(bool e) => OnExpanded(root, e)" />

View File

@@ -1,15 +1,26 @@
@* Generic progress bar. Value: 0.0 to 1.0. Color: Bootstrap color name (primary, success, etc). Size: sm (default), md, lg. *@
<div class="progress" role="progressbar" style="height: @Height">
<div class="progress-bar @BgClass" style="width: @(Value * 100)%">
@if (ShowPercent) { @($"{Value * 100:F0}%") }
@* Generic progress bar. Value: 0.0 to 1.0. Color: Bootstrap color name (primary, success, etc). Size: sm (default), md, lg. Segments for stacked mode. *@
@if (Segments is not null and { Count: > 0 }) {
<div class="progress-stacked" style="height: @Height">
@foreach (var seg in Segments) {
<div class="progress" role="progressbar" style="width: @(seg.Width * 100)%">
<div class="progress-bar @(SegmentClass(seg.Color))"></div>
</div>
}
</div>
</div>
} else {
<div class="progress" role="progressbar" style="height: @Height">
<div class="progress-bar @BgClass" style="width: @(Value * 100)%">
@if (ShowPercent) { @($"{Value * 100:F0}%") }
</div>
</div>
}
@code {
[Parameter] public float Value { get; set; }
[Parameter] public string? Color { get; set; }
[Parameter] public bool ShowPercent { get; set; }
[Parameter] public string Size { get; set; } = "sm";
[Parameter] public IReadOnlyList<(float Width, string Color)>? Segments { get; set; }
string Height => Size switch {
"sm" => "6px",
@@ -28,4 +39,15 @@
"dark" => "text-bg-dark",
var c => $"bg-{c}"
};
string SegmentClass(string color) => color switch {
"primary" => "bg-primary",
"success" => "bg-success",
"danger" => "bg-danger",
"warning" => "bg-warning",
"info" => "bg-info",
"secondary" => "bg-secondary",
"dark" => "bg-dark",
var c => $"bg-{c}"
};
}