Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches:
- "**"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Build and test (${{ matrix.postgres-image }})
Expand Down
17 changes: 10 additions & 7 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<MicrosoftPackageVersionRange>[10.0.0, 11.0.0)</MicrosoftPackageVersionRange>
<NpgsqlPackageVersionRange>[10.0.0, 11.0.0)</NpgsqlPackageVersionRange>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="[10.0.102, 11.0.0)" />
<PackageVersion Include="Microsoft.AspNetCore.App.Internal.Assets" Version="[10.0.0, 11.0.0)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[10.0.0, 11.0.0)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[10.0.0, 11.0.0)" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="[10.0.0, 11.0.0)" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="[10.0.0, 11.0.0)" />
<PackageVersion Include="Microsoft.AspNetCore.App.Internal.Assets" Version="$(MicrosoftPackageVersionRange)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftPackageVersionRange)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftPackageVersionRange)" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="$(MicrosoftPackageVersionRange)" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="$(MicrosoftPackageVersionRange)" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="$(MicrosoftPackageVersionRange)" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.5.0" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="[10.0.0, 11.0.0)" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftPackageVersionRange)" />
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.6.2" />
<PackageVersion Include="Microsoft.Testing.Platform" Version="2.2.1" />
<PackageVersion Include="Cronos" Version="0.12.0" />
<PackageVersion Include="Npgsql" Version="[10.0.0, 11.0.0)" />
<PackageVersion Include="Npgsql" Version="$(NpgsqlPackageVersionRange)" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.11.0" />
<PackageVersion Include="xunit.v3.mtp-v2" Version="3.2.2" />
Expand Down
3 changes: 3 additions & 0 deletions src/Sheddueller.Dashboard/Components/DashboardApp.razor
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Space+Grotesk:wght@400;500;600;700&amp;display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet" />
<link href="_content/Sheddueller.Dashboard/vendor/prism/prism.css" rel="stylesheet" />
</head>
<body>
<Routes @rendermode="DashboardRenderMode" />
<script src="_content/Sheddueller.Dashboard/vendor/prism/prism.js" data-manual></script>
<script src="_content/Sheddueller.Dashboard/vendor/prism/sheddueller-prism.js"></script>
<script src="_framework/blazor.web.js"></script>
</body>
</html>
Expand Down
190 changes: 190 additions & 0 deletions src/Sheddueller.Dashboard/Components/Pages/JobDetail.razor
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
@page "/jobs/{JobId:guid}"
@inherits DashboardPageComponent
@using Microsoft.JSInterop
@inject IJobInspectionReader Reader
@inject IJobManager JobManager
@inject DashboardLiveUpdateStream LiveUpdates
@inject IJSRuntime JsRuntime

<div class="job-detail-page">
<header class="job-detail-toolbar">
Expand Down Expand Up @@ -208,6 +210,54 @@
</section>

<div class="job-detail-operations">
@if (_detail.Invocation is { } invocation)
{
<section class="job-detail-panel job-detail-invocation-panel">
<h2>Invocation</h2>

<pre class="job-detail-invocation-call language-csharp"><code class="language-csharp"
@ref="_invocationCodeElement">@invocation.ReconstructedCall</code></pre>

<div class="job-detail-invocation-summary">
<div class="job-detail-metadata-item">
<span>Handler</span>
<strong title="@DashboardFormat.FullHandler(job)">@DashboardFormat.InvocationHandler(invocation)</strong>
</div>
<div class="job-detail-metadata-item">
<span>Target</span>
<strong>@invocation.TargetKind.ToString()</strong>
</div>
<div class="job-detail-metadata-item">
<span>Payload</span>
<strong title="@invocation.SerializedArgumentsContentType">@DashboardFormat.Count(invocation.SerializedArgumentsByteCount) bytes</strong>
</div>
</div>

@if (invocation.SerializedArgumentsStatus != JobSerializedArgumentsInspectionStatus.Displayable)
{
<p class="job-detail-empty">@DashboardFormat.InvocationStatus(invocation)</p>
}

<div class="job-detail-parameter-list">
@foreach (var parameter in invocation.Parameters)
{
<article class="job-detail-parameter" @key="parameter.ParameterIndex">
<div class="job-detail-parameter__header">
<span>@string.Create(CultureInfo.InvariantCulture, $"#{parameter.ParameterIndex + 1}")</span>
<strong title="@parameter.ParameterType">@DashboardFormat.ShortTypeName(parameter.ParameterType)</strong>
<em>@DashboardFormat.InvocationBinding(parameter)</em>
</div>

@if (parameter.SerializedValueJson is not null)
{
<pre>@parameter.SerializedValueJson</pre>
}
</article>
}
</div>
</section>
}

<section class="job-detail-panel">
<h2>Lifecycle Timeline</h2>

Expand Down Expand Up @@ -571,6 +621,8 @@
.job-detail-mono,
.job-detail-metadata-item strong,
.job-detail-chip,
.job-detail-invocation-call,
.job-detail-parameter pre,
.job-detail-progress-percent,
.job-detail-log-table,
.job-detail-timeline__time span {
Expand Down Expand Up @@ -944,6 +996,112 @@
line-height: 16px;
}

.job-detail-invocation-panel {
display: flex;
flex-direction: column;
gap: 14px;
}

.job-detail-invocation-summary {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(120px, 1fr) minmax(160px, 1fr);
gap: 12px;
margin-top: 2px;
}

.job-detail-invocation-call {
overflow: auto;
margin: 0;
border: 1px solid var(--sd-outline-variant);
border-radius: 2px;
background: var(--sd-surface-container);
color: var(--sd-on-surface);
font-size: 13px;
line-height: 20px;
padding: 10px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}

pre.job-detail-invocation-call[class*="language-"] {
overflow: auto;
margin: 0;
border: 1px solid var(--sd-outline-variant);
border-radius: 2px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 13px;
line-height: 20px;
padding: 10px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}

pre.job-detail-invocation-call[class*="language-"] > code[class*="language-"] {
display: block;
background: transparent;
font: inherit;
white-space: inherit;
overflow-wrap: inherit;
}

.job-detail-parameter-list {
display: flex;
min-width: 0;
flex-direction: column;
border-top: 1px solid var(--sd-outline-variant);
}

.job-detail-parameter {
min-width: 0;
border-bottom: 1px solid var(--sd-outline-variant);
padding: 10px 0;
}

.job-detail-parameter:last-child {
border-bottom: 0;
padding-bottom: 0;
}

.job-detail-parameter__header {
display: flex;
min-width: 0;
align-items: center;
gap: 8px;
}

.job-detail-parameter__header span,
.job-detail-parameter__header em {
flex: 0 0 auto;
color: var(--sd-on-surface-variant);
font-size: 12px;
font-style: normal;
line-height: 16px;
}

.job-detail-parameter__header strong {
overflow: hidden;
color: var(--sd-on-surface);
font-size: 13px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}

.job-detail-parameter pre {
overflow: auto;
max-height: 240px;
margin-top: 8px;
border: 1px solid var(--sd-outline-variant);
border-radius: 2px;
background: var(--sd-surface-container);
color: var(--sd-on-surface);
font-size: 12px;
line-height: 18px;
padding: 10px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}

.job-detail-operations {
gap: var(--sd-page-margin);
}
Expand Down Expand Up @@ -1335,17 +1493,23 @@
.job-detail-timeline__time {
text-align: left;
}

.job-detail-invocation-summary {
grid-template-columns: 1fr;
}
}
</style>

@code {
private const int EventPageSize = 500;

private readonly List<JobEvent> _events = [];
private ElementReference _invocationCodeElement;
private JobInspectionDetail? _detail;
private Guid? _loadedJobId;
private string? _error;
private string? _actionMessage;
private string? _highlightedInvocationCall;
private JobDetailActionAlertKind _actionAlertKind;
private bool _isLoading;
private bool _isCancelRunning;
Expand Down Expand Up @@ -1418,6 +1582,7 @@
this._loadedJobId = this.JobId;
this._detail = null;
this._events.Clear();
this._highlightedInvocationCall = null;
this._error = null;
this.ClearActionAlert();
this._isCancelRunning = false;
Expand All @@ -1428,6 +1593,31 @@
await this.LoadAsync();
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
_ = firstRender;

if (this._detail?.Invocation is not { } invocation ||
string.Equals(this._highlightedInvocationCall, invocation.ReconstructedCall, StringComparison.Ordinal))
{
return;
}

try
{
await JsRuntime.InvokeVoidAsync(
"ShedduellerDashboard.highlightCode",
this._invocationCodeElement);
this._highlightedInvocationCall = invocation.ReconstructedCall;
}
catch (JSDisconnectedException)
{
}
catch (JSException)
{
}
}

private protected override ValueTask DisposePageAsync()
{
LiveUpdates.JobEventPublished -= this.OnJobEventPublishedAsync;
Expand Down
22 changes: 21 additions & 1 deletion src/Sheddueller.Dashboard/Internal/DashboardFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ public static string FullHandler(JobInspectionSummary job)
public static string ShortHandler(JobInspectionSummary job)
=> string.Concat(ShortTypeName(job.ServiceType), ".", job.MethodName);

public static string InvocationHandler(JobInvocationInspection invocation)
=> string.Concat(ShortTypeName(invocation.ServiceType), ".", invocation.MethodName);

public static string InvocationBinding(JobInvocationParameterInspection parameter)
=> parameter.Binding.Kind switch
{
JobMethodParameterBindingKind.Serialized => "serialized",
JobMethodParameterBindingKind.CancellationToken => "scheduler-owned",
JobMethodParameterBindingKind.JobContext => "Job.Context",
JobMethodParameterBindingKind.Service => string.Create(
CultureInfo.InvariantCulture,
$"Job.Resolve<{ShortTypeName(parameter.Binding.ServiceType ?? parameter.ParameterType)}>()"),
_ => parameter.Binding.Kind.ToString(),
};

public static string InvocationStatus(JobInvocationInspection invocation)
=> invocation.SerializedArgumentsStatus == JobSerializedArgumentsInspectionStatus.Displayable
? "Serialized arguments are displayable."
: FirstNonEmpty(invocation.SerializedArgumentsStatusMessage, invocation.SerializedArgumentsStatus.ToString());

public static string Attempts(JobInspectionSummary job, bool compact = false)
=> compact
? string.Create(CultureInfo.InvariantCulture, $"{job.AttemptCount}/{job.MaxAttempts}")
Expand Down Expand Up @@ -292,7 +312,7 @@ public static string LogLevelClass(JobEvent jobEvent)
public static string FirstNonEmpty(params string?[] values)
=> values.FirstOrDefault(static value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;

private static string ShortTypeName(string typeName)
public static string ShortTypeName(string typeName)
{
var typeDelimiterIndex = typeName.IndexOf(',', StringComparison.Ordinal);
if (typeDelimiterIndex >= 0)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
namespace Sheddueller.Dashboard.Internal;

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using Sheddueller.Runtime;

internal sealed class DashboardJobEventListenerService(
IEnumerable<IShedduellerJobEventListener> listeners) : BackgroundService
IEnumerable<IShedduellerJobEventListener> listeners,
ILogger<DashboardJobEventListenerService> logger) : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
var snapshot = listeners.ToArray();
logger.DashboardJobEventListenerServiceStarted(snapshot.Length);
return snapshot.Length == 0
? Task.CompletedTask
: Task.WhenAll(snapshot.Select(listener => listener.ListenAsync(stoppingToken)));
Expand Down
Loading