Skip to content

Commit 3403aa1

Browse files
committed
Add SDK version suggestion list
1 parent b65c74d commit 3403aa1

File tree

10 files changed

+154
-15
lines changed

10 files changed

+154
-15
lines changed

src/App/Lab/Settings.razor

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,23 @@ OnLoaded="(e) => SetLuminanceAsync(e.IsDark)" OnLuminanceChanged="(e) => SetLumi
8181
<h5 style="margin: 0.75rem 0 0 0">Compilers</h5>
8282

8383
@* SDK version select *@
84-
<FluentTextField @bind-Value="sdkVersion" @bind-Value:after="() => LoadSdkInfoAsync()"
85-
Placeholder="version" Style="width: 100%">
84+
<FluentComboboxEx @bind-Value="sdkVersion" @bind-Value:after="() => LoadSdkInfoAsync()"
85+
Autocomplete="ComboboxAutocomplete.Both" Placeholder="version" Style="width: 100%">
8686
<FluentLabel slot="start">SDK</FluentLabel>
87+
@foreach (var info in sdkVersions ?? [])
88+
{
89+
<FluentOption TOption="string" Value="@info.Version">
90+
@info.Version
91+
@*
92+
For some reason, we cannot customize the text displayed in the closed combo box after selection
93+
and we don't want it to contain the additional info, so we use this as a workaround.
94+
`option-badge` is a CSS class which puts the `data-text` into `content` of `::after` pseudo-element.
95+
*@
96+
<span class="option-badge" data-text="@info.ReleaseDate"></span>
97+
</FluentOption>
98+
}
8799
<FluentProgressRing slot="end" title="Loading info..." Visible="loadingSdkInfo" Width="1em" />
88-
</FluentTextField>
100+
</FluentComboboxEx>
89101
<FluentLabel Style="opacity: 80%">
90102
Enter a .NET SDK version above to automatically get
91103
the corresponding Roslyn and Razor version numbers below.
@@ -283,6 +295,7 @@ OnLoaded="(e) => SetLuminanceAsync(e.IsDark)" OnLuminanceChanged="(e) => SetLumi
283295
private string? sdkVersion, roslynVersion, razorVersion;
284296
private BuildConfiguration roslynConfiguration, razorConfiguration;
285297
private bool loadingSdkInfo, loadingRoslynInfo, loadingRazorInfo;
298+
private List<SdkVersionInfo>? sdkVersions;
286299
private SdkInfo? sdkInfo;
287300
private CompilerDependencyInfo? roslynInfo, razorInfo;
288301
private string? sdkError, roslynError, razorError;
@@ -441,10 +454,16 @@ OnLoaded="(e) => SetLuminanceAsync(e.IsDark)" OnLuminanceChanged="(e) => SetLumi
441454
modalHidden = false;
442455
StateHasChanged();
443456

457+
Task sdkVersionsTask = Task.CompletedTask;
444458
Task sdkTask = Task.CompletedTask;
445459
Task roslynTask = Task.CompletedTask;
446460
Task razorTask = Task.CompletedTask;
447461

462+
if (sdkVersions is null)
463+
{
464+
sdkVersionsTask = LoadSdkVersionsAsync();
465+
}
466+
448467
if (sdkInfo is null)
449468
{
450469
sdkTask = LoadSdkInfoAsync(saveState: false);
@@ -460,7 +479,7 @@ OnLoaded="(e) => SetLuminanceAsync(e.IsDark)" OnLuminanceChanged="(e) => SetLumi
460479
razorTask = LoadRazorInfoAsync(saveState: false);
461480
}
462481

463-
return Task.WhenAll(sdkTask, roslynTask, razorTask);
482+
return Task.WhenAll(sdkVersionsTask, sdkTask, roslynTask, razorTask);
464483
}
465484

466485
public void CloseModal()
@@ -503,6 +522,12 @@ OnLoaded="(e) => SetLuminanceAsync(e.IsDark)" OnLuminanceChanged="(e) => SetLumi
503522
return Task.WhenAll(sdkTask, roslynTask, razorTask);
504523
}
505524

525+
private async Task LoadSdkVersionsAsync()
526+
{
527+
sdkVersions = await Worker.GetSdkVersionsAsync();
528+
await RefreshAsync();
529+
}
530+
506531
private async Task LoadSdkInfoAsync(bool saveState = true)
507532
{
508533
var versionToLoad = sdkVersion;
@@ -540,7 +565,7 @@ OnLoaded="(e) => SetLuminanceAsync(e.IsDark)" OnLuminanceChanged="(e) => SetLumi
540565
catch (Exception ex)
541566
{
542567
Logger.LogError(ex, "Failed to load SDK info.");
543-
error = ex.Message;
568+
error = ex is WorkerException w ? w.Failure.Message : ex.Message;
544569
info = null;
545570
}
546571

src/App/Lab/WorkerController.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,13 @@ public Task<CompilerDependencyInfo> GetCompilerDependencyInfoAsync(CompilerKind
430430
deserializeAs: default(CompilerDependencyInfo));
431431
}
432432

433+
public Task<List<SdkVersionInfo>> GetSdkVersionsAsync()
434+
{
435+
return PostAndReceiveMessageAsync(
436+
new WorkerInputMessage.GetSdkVersions() { Id = messageId++ },
437+
deserializeAs: default(List<SdkVersionInfo>));
438+
}
439+
433440
public Task<SdkInfo> GetSdkInfoAsync(string versionToLoad)
434441
{
435442
return PostAndReceiveMessageAsync(
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Microsoft.FluentUI.AspNetCore.Components;
2+
3+
namespace DotNetLab;
4+
5+
public sealed class FluentComboboxEx : FluentCombobox<string>
6+
{
7+
protected override Task ChangeHandlerAsync(ChangeEventArgs e)
8+
{
9+
// Ensures that when user writes a custom text into the combobox, it propagates to the bound Value.
10+
// For some reason that doesn't work automatically when using FluentOptions as ChildContent ourselves.
11+
string? value = e.Value?.ToString();
12+
Value = value;
13+
return ValueChanged.InvokeAsync(value);
14+
}
15+
}

src/App/wwwroot/css/app.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ code {
101101
color: #c02d76;
102102
}
103103

104+
.option-badge::after {
105+
content: attr(data-text);
106+
font-size: small;
107+
color: var(--input-placeholder-rest);
108+
margin-left: 0.5em;
109+
}
110+
104111
/*
105112
Causes Monaco Editor to resize properly.
106113
See https://github.com/microsoft/monaco-editor/issues/3512.

src/Worker/Executor.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,28 +87,34 @@ public async Task<string> HandleAsync(WorkerInputMessage.GetOutput message)
8787
}
8888
}
8989

90-
public async Task<bool> HandleAsync(WorkerInputMessage.UseCompilerVersion message)
90+
public Task<bool> HandleAsync(WorkerInputMessage.UseCompilerVersion message)
9191
{
9292
var compilerDependencyProvider = services.GetRequiredService<CompilerDependencyProvider>();
93-
return await compilerDependencyProvider.UseAsync(message.CompilerKind, message.Version, message.Configuration);
93+
return compilerDependencyProvider.UseAsync(message.CompilerKind, message.Version, message.Configuration);
9494
}
9595

96-
public async Task<CompilerDependencyInfo> HandleAsync(WorkerInputMessage.GetCompilerDependencyInfo message)
96+
public Task<CompilerDependencyInfo> HandleAsync(WorkerInputMessage.GetCompilerDependencyInfo message)
9797
{
9898
var compilerDependencyProvider = services.GetRequiredService<CompilerDependencyProvider>();
99-
return await compilerDependencyProvider.GetLoadedInfoAsync(message.CompilerKind);
99+
return compilerDependencyProvider.GetLoadedInfoAsync(message.CompilerKind);
100100
}
101101

102-
public async Task<SdkInfo> HandleAsync(WorkerInputMessage.GetSdkInfo message)
102+
public Task<List<SdkVersionInfo>> HandleAsync(WorkerInputMessage.GetSdkVersions message)
103103
{
104104
var sdkDownloader = services.GetRequiredService<SdkDownloader>();
105-
return await sdkDownloader.GetInfoAsync(message.VersionToLoad);
105+
return sdkDownloader.GetListAsync();
106106
}
107107

108-
public async Task<string?> HandleAsync(WorkerInputMessage.TryGetSubRepoCommitHash message)
108+
public Task<SdkInfo> HandleAsync(WorkerInputMessage.GetSdkInfo message)
109109
{
110110
var sdkDownloader = services.GetRequiredService<SdkDownloader>();
111-
return await sdkDownloader.TryGetSubRepoCommitHashAsync(message.MonoRepoCommitHash, message.SubRepoUrl);
111+
return sdkDownloader.GetInfoAsync(message.VersionToLoad);
112+
}
113+
114+
public Task<string?> HandleAsync(WorkerInputMessage.TryGetSubRepoCommitHash message)
115+
{
116+
var sdkDownloader = services.GetRequiredService<SdkDownloader>();
117+
return sdkDownloader.TryGetSubRepoCommitHashAsync(message.MonoRepoCommitHash, message.SubRepoUrl);
112118
}
113119

114120
public async Task<string> HandleAsync(WorkerInputMessage.ProvideCompletionItems message)

src/Worker/Lab/LabWorkerJsonContext.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ namespace DotNetLab.Lab;
99
[JsonSerializable(typeof(SourceManifest))]
1010
[JsonSerializable(typeof(Dictionary<string, string>))]
1111
internal sealed partial class LabWorkerJsonContext : JsonSerializerContext;
12+
13+
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, PropertyNamingPolicy = JsonKnownNamingPolicy.KebabCaseLower)]
14+
[JsonSerializable(typeof(DotNetReleaseIndex))]
15+
[JsonSerializable(typeof(DotNetReleaseIndex.ReleaseList))]
16+
internal sealed partial class LabWorkerKebabCaseJsonContext : JsonSerializerContext;

src/Worker/Lab/SdkDownloader.cs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using System.Net.Http.Json;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
14
using System.Xml.Serialization;
25

36
namespace DotNetLab.Lab;
@@ -16,9 +19,22 @@ internal sealed class SdkDownloader(
1619
private const string razorRepoUrl = "https://github.com/dotnet/razor";
1720
private const string versionDetailsRelativePath = "eng/Version.Details.xml";
1821
private const string sourceManifestRelativePath = "src/source-manifest.json";
22+
private const string releasesIndexUrl = "https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json";
1923

2024
private static readonly XmlSerializer versionDetailsSerializer = new(typeof(Dependencies));
2125

26+
public async Task<List<SdkVersionInfo>> GetListAsync()
27+
{
28+
try
29+
{
30+
var index = await client.GetFromJsonAsync(releasesIndexUrl.WithCorsProxy(), LabWorkerKebabCaseJsonContext.Default.DotNetReleaseIndex);
31+
var lists = await Task.WhenAll(index.ReleasesIndex.Select(entry => client.GetFromJsonAsync(entry.ReleasesJson.WithCorsProxy(), LabWorkerKebabCaseJsonContext.Default.ReleaseList)));
32+
return lists.SelectMany(static list => list.Releases.Select(static release => release.ToVersionInfo())).ToList();
33+
}
34+
catch (HttpRequestException) { return []; }
35+
catch (JsonException) { return []; }
36+
}
37+
2238
public async Task<SdkInfo> GetInfoAsync(string version)
2339
{
2440
CommitLink commit = await getCommitAsync(version);
@@ -91,12 +107,11 @@ async Task<SdkInfo> getInfoAsync(string version, CommitLink commit)
91107
{
92108
using var response = await SendGitHubRequestAsync($"https://api.github.com/repos/{monoRepoOwner}/{monoRepoName}/contents/{sourceManifestRelativePath}?ref={monoRepoCommitHash}");
93109

94-
if (response.StatusCode == HttpStatusCode.NotFound)
110+
if (!response.IsSuccessStatusCode)
95111
{
96112
return null;
97113
}
98114

99-
response.EnsureSuccessStatusCode();
100115
using var stream = await response.Content.ReadAsStreamAsync();
101116
return await response.Content.TryReadFromJsonAsync(LabWorkerJsonContext.Default.SourceManifest);
102117
}
@@ -165,3 +180,45 @@ public CommitLink GetCommitLink()
165180
}
166181
}
167182
}
183+
184+
/// <summary>
185+
/// For JSON from <see cref="SdkDownloader.releasesIndexUrl"/>.
186+
/// </summary>
187+
internal readonly struct DotNetReleaseIndex
188+
{
189+
public required ImmutableArray<ChannelInfo> ReleasesIndex { get; init; }
190+
191+
public readonly struct ChannelInfo
192+
{
193+
[JsonPropertyName("releases.json")]
194+
public required string ReleasesJson { get; init; }
195+
}
196+
197+
/// <summary>
198+
/// For JSON from <see cref="ChannelInfo.ReleasesJson"/>.
199+
/// </summary>
200+
public readonly struct ReleaseList
201+
{
202+
public required ImmutableArray<Release> Releases { get; init; }
203+
}
204+
205+
public readonly struct Release
206+
{
207+
public required string ReleaseDate { get; init; }
208+
public required SdkInfo Sdk { get; init; }
209+
210+
public SdkVersionInfo ToVersionInfo()
211+
{
212+
return new SdkVersionInfo
213+
{
214+
Version = Sdk.Version,
215+
ReleaseDate = ReleaseDate,
216+
};
217+
}
218+
}
219+
220+
public readonly struct SdkInfo
221+
{
222+
public required string Version { get; init; }
223+
}
224+
}

src/WorkerApi/InputMessage.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace DotNetLab;
1212
[JsonDerivedType(typeof(GetOutput), nameof(GetOutput))]
1313
[JsonDerivedType(typeof(UseCompilerVersion), nameof(UseCompilerVersion))]
1414
[JsonDerivedType(typeof(GetCompilerDependencyInfo), nameof(GetCompilerDependencyInfo))]
15+
[JsonDerivedType(typeof(GetSdkVersions), nameof(GetSdkVersions))]
1516
[JsonDerivedType(typeof(GetSdkInfo), nameof(GetSdkInfo))]
1617
[JsonDerivedType(typeof(TryGetSubRepoCommitHash), nameof(TryGetSubRepoCommitHash))]
1718
[JsonDerivedType(typeof(ProvideCompletionItems), nameof(ProvideCompletionItems))]
@@ -97,6 +98,14 @@ public override Task<CompilerDependencyInfo> HandleAsync(IExecutor executor)
9798
}
9899
}
99100

101+
public sealed record GetSdkVersions : WorkerInputMessage<List<SdkVersionInfo>>
102+
{
103+
public override Task<List<SdkVersionInfo>> HandleAsync(IExecutor executor)
104+
{
105+
return executor.HandleAsync(this);
106+
}
107+
}
108+
100109
public sealed record GetSdkInfo(string VersionToLoad) : WorkerInputMessage<SdkInfo>
101110
{
102111
public override Task<SdkInfo> HandleAsync(IExecutor executor)
@@ -193,6 +202,7 @@ public interface IExecutor
193202
Task<string> HandleAsync(GetOutput message);
194203
Task<bool> HandleAsync(UseCompilerVersion message);
195204
Task<CompilerDependencyInfo> HandleAsync(GetCompilerDependencyInfo message);
205+
Task<List<SdkVersionInfo>> HandleAsync(GetSdkVersions message);
196206
Task<SdkInfo> HandleAsync(GetSdkInfo message);
197207
Task<string?> HandleAsync(TryGetSubRepoCommitHash message);
198208
Task<string> HandleAsync(ProvideCompletionItems message);

src/WorkerApi/Lab/CompilerDependency.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,12 @@ public override void Write(Utf8JsonWriter writer, NuGetVersion value, JsonSerial
219219
}
220220
}
221221

222+
public readonly struct SdkVersionInfo
223+
{
224+
public required string Version { get; init; }
225+
public required string ReleaseDate { get; init; }
226+
}
227+
222228
public sealed record SdkInfo
223229
{
224230
public required string SdkVersion { get; init; }

src/WorkerApi/WorkerJsonContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace DotNetLab;
88
[JsonSerializable(typeof(WorkerOutputMessage))]
99
[JsonSerializable(typeof(CompiledAssembly))]
1010
[JsonSerializable(typeof(CompilerDependencyInfo))]
11+
[JsonSerializable(typeof(List<SdkVersionInfo>))]
1112
[JsonSerializable(typeof(SdkInfo))]
1213
[JsonSerializable(typeof(ImmutableArray<MarkerData>))]
1314
public sealed partial class WorkerJsonContext : JsonSerializerContext;

0 commit comments

Comments
 (0)