diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Classes/OpenAPI/OpenAPISchema.cs b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Classes/OpenAPI/OpenAPISchema.cs
new file mode 100644
index 00000000..8e399302
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Classes/OpenAPI/OpenAPISchema.cs
@@ -0,0 +1,352 @@
+using System.Collections.Frozen;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Spacebar.AdminAPI.TestClient.Classes.OpenAPI;
+
+public class OpenApiSchema {
+ [JsonPropertyName("openapi")]
+ public string Version { get; set; } = null!;
+
+ [JsonPropertyName("info")]
+ public OpenApiInfo Info { get; set; } = null!;
+
+ [JsonPropertyName("externalDocs")]
+ public OpenApiExternalDocs? ExternalDocs { get; set; }
+
+ [JsonPropertyName("paths")]
+ public Dictionary<string, OpenApiPath> Paths { get; set; } = null!;
+
+ [JsonPropertyName("servers")]
+ public List<OpenApiServer> Servers { get; set; } = null!;
+
+ [JsonPropertyName("components")]
+ public OpenApiComponents? Components { get; set; } = null!;
+
+ public class OpenApiComponents {
+ [JsonPropertyName("schemas")]
+ public Dictionary<string, OpenApiSchemaRef>? Schemas { get; set; } = null!;
+ }
+}
+
+public class OpenApiServer {
+ [JsonPropertyName("url")]
+ public string Url { get; set; } = null!;
+
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+}
+
+public class OpenApiPath {
+ public FrozenSet<string> GetAvailableMethods() {
+ List<string> methods = new();
+ if (Get != null) methods.Add("GET");
+ if (Post != null) methods.Add("POST");
+ if (Put != null) methods.Add("PUT");
+ if (Delete != null) methods.Add("DELETE");
+ if (Patch != null) methods.Add("PATCH");
+ if (Options != null) methods.Add("OPTIONS");
+ return methods.ToFrozenSet();
+ }
+
+ public bool HasMethod(string method) {
+ return method.ToLower() switch {
+ "get" => Get != null,
+ "post" => Post != null,
+ "put" => Put != null,
+ "delete" => Delete != null,
+ "patch" => Patch != null,
+ "options" => Options != null,
+ _ => false
+ };
+ }
+
+ public OpenApiOperation? GetOperation(string method) {
+ if (!HasMethod(method)) return null;
+ return method.ToLower() switch {
+ "get" => Get,
+ "post" => Post,
+ "put" => Put,
+ "delete" => Delete,
+ "patch" => Patch,
+ "options" => Options,
+ _ => null
+ };
+ }
+
+ [JsonPropertyName("get")]
+ public OpenApiOperation? Get { get; set; }
+
+ [JsonPropertyName("post")]
+ public OpenApiOperation? Post { get; set; }
+
+ [JsonPropertyName("put")]
+ public OpenApiOperation? Put { get; set; }
+
+ [JsonPropertyName("delete")]
+ public OpenApiOperation? Delete { get; set; }
+
+ [JsonPropertyName("patch")]
+ public OpenApiOperation? Patch { get; set; }
+
+ [JsonPropertyName("options")]
+ public OpenApiOperation? Options { get; set; }
+
+ public class OpenApiOperation {
+ [JsonPropertyName("description")]
+ public string Description { get; set; } = null!;
+
+ [JsonPropertyName("parameters")]
+ public List<OpenApiParameter>? Parameters { get; set; }
+
+ [JsonPropertyName("requestBody")]
+ public OpenApiRequestBody? RequestBody { get; set; }
+
+ public class OpenApiParameter {
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = null!;
+
+ [JsonPropertyName("in")]
+ public string In { get; set; } = null!;
+
+ [JsonPropertyName("required")]
+ public bool Required { get; set; }
+
+ [JsonPropertyName("schema")]
+ public OpenApiSchemaRef Schema { get; set; } = null!;
+
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+ }
+ }
+}
+
+public class OpenApiExternalDocs {
+ [JsonPropertyName("description")]
+ public string Description { get; set; } = null!;
+
+ [JsonPropertyName("url")]
+ public string Url { get; set; } = null!;
+}
+
+public class OpenApiInfo {
+ [JsonPropertyName("title")]
+ public string Title { get; set; } = null!;
+
+ [JsonPropertyName("version")]
+ public string Version { get; set; } = null!;
+
+ [JsonPropertyName("description")]
+ public string Description { get; set; } = null!;
+
+ [JsonPropertyName("license")]
+ public OpenApiLicense License { get; set; } = null!;
+
+ public class OpenApiLicense {
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = null!;
+
+ [JsonPropertyName("url")]
+ public string Url { get; set; } = null!;
+ }
+}
+
+public class OpenApiRequestBody {
+ [JsonPropertyName("content")]
+ public OpenApiContent Content { get; set; } = null!;
+
+ [JsonPropertyName("required")]
+ public bool Required { get; set; }
+}
+
+public class OpenApiContent {
+ [JsonPropertyName("application/json")]
+ public OpenApiSchemaContainer? ApplicationJson { get; set; }
+
+ public class OpenApiSchemaContainer {
+ [JsonPropertyName("schema")]
+ public OpenApiSchemaRef Schema { get; set; } = null!;
+ }
+}
+
+[JsonConverter(typeof(OpenApiSchemaRefConverter))]
+public class OpenApiSchemaRef {
+ public string? Description { get; set; }
+ public string? Type { get; set; } = null!;
+ public List<string>? Types { get; set; } = null!;
+ public string? Ref { get; set; } = null!;
+ public Dictionary<string, OpenApiSchemaRef>? Properties { get; set; }
+ public List<string>? Required { get; set; }
+ public int? MinLength { get; set; }
+ public int? MaxLength { get; set; }
+ public int? MinItems { get; set; }
+ public int? MaxItems { get; set; }
+ public object? Constant { get; set; }
+ public object? Default { get; set; }
+ public bool Nullable { get; set; }
+ public List<OpenApiSchemaRef>? AnyOf { get; set; }
+ public List<object>? Enum { get; set; }
+ public string? Format { get; set; }
+
+ public OpenApiSchemaRef? GetReferencedSchema(OpenApiSchema schema) {
+ if (Ref == null) return null;
+ string refKey = Ref.Replace("#/components/schemas/", "");
+ if (schema.Components?.Schemas != null && schema.Components.Schemas.TryGetValue(refKey, out var referencedSchema)) {
+ return referencedSchema;
+ }
+
+ throw new KeyNotFoundException($"Referenced schema '{refKey}' not found.");
+ }
+}
+
+public class OpenApiSchemaRefConverter : JsonConverter<OpenApiSchemaRef> {
+ public override OpenApiSchemaRef? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
+ if (reader.TokenType != JsonTokenType.StartObject) {
+ throw new JsonException("Expected StartObject token");
+ }
+
+ using var jsonDoc = JsonDocument.ParseValue(ref reader);
+ var jsonObject = jsonDoc.RootElement;
+ var schemaRef = new OpenApiSchemaRef();
+
+ foreach (var property in jsonObject.EnumerateObject()) {
+ switch (property.Name) {
+ case "type":
+ if (property.Value.ValueKind == JsonValueKind.String) {
+ schemaRef.Type = property.Value.GetString();
+ }
+ else if (property.Value.ValueKind == JsonValueKind.Array) {
+ var types = new List<string>();
+ foreach (var item in property.Value.EnumerateArray()) {
+ if (item.ValueKind == JsonValueKind.String) {
+ types.Add(item.GetString()!);
+ }
+ else throw new JsonException("Expected string in type array");
+ }
+
+ schemaRef.Types = types;
+ }
+
+ break;
+ case "$ref":
+ schemaRef.Ref = property.Value.GetString();
+ break;
+ case "description":
+ schemaRef.Description = property.Value.GetString();
+ break;
+ case "properties":
+ schemaRef.Properties = property.Value.EnumerateObject().ToDictionary(x => x.Name, x => x.Value.Deserialize<OpenApiSchemaRef>(options)!);
+ break;
+ case "required":
+ schemaRef.Required = property.Value.EnumerateArray().Select(item => item.GetString()!).ToList();
+ break;
+ case "minLength":
+ schemaRef.MinLength = property.Value.GetInt32();
+ break;
+ case "maxLength":
+ schemaRef.MaxLength = property.Value.GetInt32();
+ break;
+ case "minItems":
+ schemaRef.MinItems = property.Value.GetInt32();
+ break;
+ case "maxItems":
+ schemaRef.MaxItems = property.Value.GetInt32();
+ break;
+ case "const":
+ if (property.Value.ValueKind == JsonValueKind.String)
+ schemaRef.Constant = property.Value.GetString();
+ else if (property.Value.ValueKind == JsonValueKind.Number)
+ schemaRef.Constant = property.Value.GetInt32();
+ else if (property.Value.ValueKind is JsonValueKind.True or JsonValueKind.False)
+ schemaRef.Constant = property.Value.GetBoolean();
+ else throw new JsonException($"Expected string|int|bool in const, got {property.Value.ValueKind}");
+ break;
+ case "default":
+ if (property.Value.ValueKind == JsonValueKind.String)
+ schemaRef.Default = property.Value.GetString();
+ else if (property.Value.ValueKind == JsonValueKind.Number)
+ schemaRef.Default = property.Value.GetInt32();
+ else if (property.Value.ValueKind is JsonValueKind.True or JsonValueKind.False)
+ schemaRef.Default = property.Value.GetBoolean();
+ else if (property.Value.ValueKind == JsonValueKind.Null)
+ schemaRef.Default = null;
+ else if (property.Value.ValueKind == JsonValueKind.Array)
+ if (property.Value.GetArrayLength() > 0) throw new JsonException("Expected empty array in default");
+ else schemaRef.Default = Array.Empty<object>();
+ else throw new JsonException($"Expected string|int|bool|null in default, got {property.Value.ValueKind}");
+ break;
+ case "enum":
+ var enumValues = new List<object>();
+ foreach (var item in property.Value.EnumerateArray()) {
+ if (item.ValueKind == JsonValueKind.String)
+ enumValues.Add(item.GetString()!);
+ else if (item.ValueKind == JsonValueKind.Number)
+ enumValues.Add(item.GetInt32());
+ else if (item.ValueKind is JsonValueKind.True or JsonValueKind.False)
+ enumValues.Add(item.GetBoolean());
+ else if (item.ValueKind == JsonValueKind.Null)
+ enumValues.Add(null!);
+ else throw new JsonException($"Expected string|int|bool|null in enum, got {item.ValueKind}");
+ }
+
+ schemaRef.Enum = enumValues;
+ break;
+ case "nullable":
+ schemaRef.Nullable = property.Value.GetBoolean();
+ break;
+ case "anyOf":
+ schemaRef.AnyOf = property.Value.EnumerateArray().Select(item => item.Deserialize<OpenApiSchemaRef>(options)!).ToList();
+ break;
+ case "format":
+ schemaRef.Format = property.Value.GetString();
+ break;
+ case "additionalProperties": //TODO
+ case "patternProperties": // Side effect of using JsonValue in typescript
+ break;
+ default:
+ Console.WriteLine($"Got unexpected prop {property.Name} in OpenApiSchemaRef!");
+ break;
+ }
+ }
+
+ return schemaRef;
+ }
+
+ public override void Write(Utf8JsonWriter writer, OpenApiSchemaRef value, JsonSerializerOptions options) {
+ // throw new NotImplementedException("Serialization not implemented for OpenApiSchemaRef");
+ writer.WriteStartObject();
+ if (value.Type != null) {
+ writer.WriteString("type", value.Type);
+ }
+ else if (value.Types != null) {
+ writer.WritePropertyName("type");
+ writer.WriteStartArray();
+ foreach (var type in value.Types) {
+ writer.WriteStringValue(type);
+ }
+
+ writer.WriteEndArray();
+ }
+
+ if (value.Ref != null) {
+ writer.WriteString("$ref", value.Ref);
+ }
+
+ if (value.Description != null) {
+ writer.WriteString("description", value.Description);
+ }
+
+ if (value.Properties != null) {
+ writer.WritePropertyName("properties");
+ writer.WriteStartObject();
+ foreach (var prop in value.Properties) {
+ writer.WritePropertyName(prop.Key);
+ JsonSerializer.Serialize(writer, prop.Value, options);
+ }
+
+ writer.WriteEndObject();
+ }
+
+ writer.WriteEndObject();
+ }
+}
\ No newline at end of file
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor
index 732d46e7..c96fa30c 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor
@@ -15,6 +15,11 @@
</NavLink>
</div>
<div class="nav-item px-3">
+ <NavLink class="nav-link" href="HttpTestClient">
+ <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> HTTP Client
+ </NavLink>
+ </div>
+ <div class="nav-item px-3">
<NavLink class="nav-link" href="Users">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Users
</NavLink>
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Guilds.razor b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Guilds.razor
new file mode 100644
index 00000000..454808b4
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Guilds.razor
@@ -0,0 +1,80 @@
+@page "/Guilds"
+@using System.Net.Http.Headers
+@using System.Reflection
+@using Spacebar.AdminApi.Models
+@using Spacebar.AdminAPI.TestClient.Services
+@using ArcaneLibs.Blazor.Components
+@using ArcaneLibs.Extensions
+@inject Config Config
+@inject ILocalStorageService LocalStorage
+
+<PageTitle>Guilds</PageTitle>
+
+<details>
+ <summary>Displayed columns</summary>
+ @foreach (var column in DisplayedColumns) {
+ var value = column.Value;
+ <span>
+ <InputCheckbox @bind-Value:get="@(value)" @bind-Value:set="@(b => {
+ DisplayedColumns[column.Key] = b;
+ StateHasChanged();
+ })"/>
+ @column.Key.Name
+ </span>
+ <br/>
+ }
+</details>
+
+<p>Got @GuildList.Count guilds.</p>
+<table class="table table-bordered">
+ @{
+ var columns = DisplayedColumns.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList();
+ }
+ <thead>
+ <tr>
+ @foreach (var column in columns) {
+ <th>@column.Name</th>
+ }
+ <th>Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var user in GuildList.Where(x => !x.Unavailable).OrderByDescending(x=>x.MessageCount)) {
+ <tr>
+ @foreach (var column in columns) {
+ <td>@column.GetValue(user)</td>
+ }
+ <td>
+ <LinkButton href="@($"/Users/Delete/{user.Id}")" Color="#ff0000">Delete</LinkButton>
+ </td>
+ </tr>
+ }
+ </tbody>
+</table>
+
+@code {
+
+ private Dictionary<PropertyInfo, bool> DisplayedColumns { get; set; } = typeof(GuildModel).GetProperties()
+ .ToDictionary(p => p, p => p.Name == "Name" || p.Name == "Id" || p.Name == "MessageCount");
+
+ private List<GuildModel> GuildList { get; set; } = new();
+
+ protected override async Task OnInitializedAsync() {
+ var hc = new StreamingHttpClient();
+ hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken);
+
+ // var request = new HttpRequestMessage(HttpMethod.Get, Config.AdminUrl + "/_spacebar/admin/users/");
+
+ var response = hc.GetAsyncEnumerableFromJsonAsync<GuildModel>(Config.AdminUrl + "/_spacebar/admin/guilds/");
+ // if (!response.IsSuccessStatusCode) throw new Exception(await response.Content.ReadAsStringAsync());
+ // var content = response.Content.ReadFromJsonAsAsyncEnumerable<GuildModel>();
+ await foreach (var user in response) {
+ // Console.WriteLine(user.ToJson(indent: false, ignoreNull: true));
+ GuildList.Add(user!);
+ if(GuildList.Count % 1000 == 0)
+ StateHasChanged();
+ }
+ StateHasChanged();
+ }
+
+}
\ No newline at end of file
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/HttpTestClient.razor b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/HttpTestClient.razor
new file mode 100644
index 00000000..0be77a99
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/HttpTestClient.razor
@@ -0,0 +1,198 @@
+@page "/HttpTestClient"
+@using System.Collections.Immutable
+@using System.Text.Json
+@using ArcaneLibs.Blazor.Components
+@using ArcaneLibs.Extensions
+@using Spacebar.AdminAPI.TestClient.Classes.OpenAPI
+@using Spacebar.AdminAPI.TestClient.Pages.HttpTestClientParts
+@using Spacebar.AdminAPI.TestClient.Services
+@inject Config Config
+<h3>HttpTestClient</h3>
+
+@if (OpenApiSchema is not null) {
+ <p>Got OpenAPI schema with @OpenApiSchema.Paths.Count paths.</p>
+ <span>Server: </span>
+
+ var currentIndex = OpenApiSchema.Servers.IndexOf(Server!);
+ <InputSelect TValue="int" Value="@currentIndex" ValueExpression="@(() => currentIndex)" ValueChanged="@SetCurrentServer">
+ @for (var index = 0; index < OpenApiSchema.Servers.Count; index++) {
+ var server = OpenApiSchema.Servers[index];
+ var serverOptionName = $"{server.Description} ({server.Url})";
+ <option value="@index">@serverOptionName</option>
+ }
+ </InputSelect>
+ <br/>
+ <br/>
+ <span>Path: </span>
+
+ <InputSelect @bind-Value="_methodKey">
+ <option>-- select a method --</option>
+ @foreach (var method in OpenApiSchema.Paths.SelectMany(x => x.Value.GetAvailableMethods()).Distinct()) {
+ <option value="@method">@method</option>
+ }
+ </InputSelect>
+
+ @if (!string.IsNullOrWhiteSpace(_methodKey)) {
+ <InputSelect @bind-Value="_pathKey">
+ <option>-- select a path --</option>
+ @foreach (var path in OpenApiSchema.Paths.Where(x => x.Value.HasMethod(_methodKey!)).OrderBy(x => x.Key)) {
+ <option value="@path.Key">@path.Key</option>
+ }
+ </InputSelect>
+ <br/>
+ }
+
+ if (Operation != null) {
+ if (!string.IsNullOrWhiteSpace(Operation.Description)) {
+ <p>@Operation.Description</p>
+ }
+ }
+
+ <details>
+ <summary>@AllKnownPathParameters.Count known path parameters</summary>
+ @foreach (var (param, value) in AllKnownPathParameters) {
+ var _key = param;
+ // if (Operation?.Parameters?.Any(x => x.Name == param.Name && x.In == param.In) ?? false)
+ // continue;
+ <OpenAPIParameterDescription Parameter="@param"/>
+ <br/>
+ }
+ </details>
+ <details>
+ <summary>@AllKnownQueryParameters.Count known query parameters</summary>
+ @foreach (var (param, value) in AllKnownQueryParameters) {
+ var _key = param;
+ // if (Operation?.Parameters?.Any(x => x.Name == param.Name && x.In == param.In) ?? false)
+ // continue;
+ <OpenAPIParameterDescription Parameter="@param"/>
+ <br/>
+ }
+ </details>
+
+ @if (Operation != null) {
+ if (Operation.Parameters?.Any() ?? false) {
+ var pathParams = Operation.Parameters.Where(x => x.In == "path").ToList();
+ if (pathParams.Any()) {
+ <b>Path parameters</b>
+ <br/>
+ foreach (var key in pathParams) {
+ <span>Path parameter </span>
+ <OpenAPIParameterDescription Parameter="@key"/>
+ <br/>
+ }
+ }
+
+ var queryParams = Operation.Parameters.Except(pathParams).Where(x => x.In == "query").ToList();
+ if (queryParams.Any()) {
+ <b>Query parameters</b>
+ <br/>
+ foreach (var key in queryParams) {
+ <span>Query parameter </span>
+ <OpenAPIParameterDescription Parameter="@key"/>
+ <br/>
+ }
+ }
+
+ var otherParams = Operation.Parameters.Except(pathParams).Except(queryParams).ToList();
+ if (otherParams.Any()) {
+ <b>Other parameters</b>
+ <br/>
+ foreach (var key in otherParams) {
+ <span>Other parameter </span>
+ <OpenAPIParameterDescription Parameter="@key"/>
+ <br/>
+ }
+ }
+ }
+
+ if(Operation.RequestBody is not null) {
+ <b>Request body</b>
+ <br/>
+ <span title="@Operation.RequestBody.ToJson()">@Operation.RequestBody.Content.ApplicationJson?.Schema.ToJson()</span>
+ }
+ }
+
+ <LinkButton OnClickAsync="@Execute">Execute</LinkButton>
+ <pre>@ResultContent</pre>
+}
+
+@code {
+ private string? _pathKey;
+ private string? _methodKey;
+
+ private OpenApiSchema? OpenApiSchema { get; set; }
+ private OpenApiServer? Server { get; set; }
+ private Dictionary<OpenApiPath.OpenApiOperation.OpenApiParameter, string> AllKnownPathParameters { get; set; } = [];
+ private Dictionary<OpenApiPath.OpenApiOperation.OpenApiParameter, string> AllKnownQueryParameters { get; set; } = [];
+
+ private OpenApiPath? Path => string.IsNullOrWhiteSpace(_pathKey) ? null : OpenApiSchema?.Paths.GetValueOrDefault(_pathKey);
+ private OpenApiPath.OpenApiOperation? Operation => Path is null || string.IsNullOrWhiteSpace(_methodKey) ? null : Path.GetOperation(_methodKey);
+
+ private string? ResultContent { get; set; }
+ private readonly StreamingHttpClient _httpClient = new();
+
+ protected override async Task OnInitializedAsync() {
+ _httpClient.DefaultRequestHeaders.Authorization = new("Bearer", Config.AccessToken);
+
+ OpenApiSchema = await _httpClient.GetFromJsonAsync<OpenApiSchema>($"{Config.ApiUrl}/_spacebar/api/openapi.json");
+ OpenApiSchema!.Servers.Insert(0, Server = new() {
+ Description = "Current server (config)",
+ Url = Config.ApiUrl + "/api/v9"
+ });
+ SetCurrentServer(0);
+
+ AllKnownPathParameters = OpenApiSchema.Paths.Values
+ .SelectMany(x => x.GetAvailableMethods().Select(y => x.GetOperation(y)!.Parameters ?? []))
+ .SelectMany(x => x)
+ .Where(x => x.In == "path")
+ .DistinctBy(x => x.ToJson())
+ .OrderBy(x => x.Name)
+ .ToDictionary(x => x, _ => "");
+
+ AllKnownQueryParameters = OpenApiSchema.Paths.Values
+ .SelectMany(x => x.GetAvailableMethods().Select(y => x.GetOperation(y)!.Parameters ?? []))
+ .SelectMany(x => x)
+ .Where(x => x.In == "query")
+ .DistinctBy(x => x.ToJson())
+ .OrderBy(x => x.Name)
+ .ToDictionary(x => x, _ => "");
+ }
+
+ protected override bool ShouldRender() {
+ if (string.IsNullOrWhiteSpace(_methodKey))
+ _pathKey = null;
+ return base.ShouldRender();
+ }
+
+ private void SetCurrentServer(int index) {
+ Server = OpenApiSchema!.Servers[index];
+ _httpClient.BaseAddress = new Uri(Server.Url);
+ StateHasChanged();
+ }
+
+ private async Task Execute() {
+ var url = _pathKey!.TrimStart('/');
+ if (Operation?.Parameters?.Any(x => x.In == "path") ?? false) {
+ foreach (var param in Operation.Parameters.Where(x => x.In == "path")) {
+ if (!AllKnownPathParameters.TryGetValue(param, out var value) || string.IsNullOrWhiteSpace(value))
+ throw new Exception($"Path parameter {param.Name} not set");
+ url = url.Replace($"{{{param.Name}}}", value!);
+ }
+ }
+
+ var request = new HttpRequestMessage(new HttpMethod(_methodKey!), url);
+ try {
+ var response = await _httpClient.SendAsync(request);
+ ResultContent = response.Content.GetType().Name + "\n" + response.Content switch {
+ { Headers: { ContentType: { MediaType: "application/json" } } } => (await response.Content.ReadFromJsonAsync<JsonElement>()).ToJson(true),
+ _ => await response.Content.ReadAsStringAsync()
+ };
+ }
+ catch (Exception ex) {
+ ResultContent = ex.ToString();
+ }
+
+ StateHasChanged();
+ }
+
+}
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/HttpTestClientParts/OpenAPIParameterDescription.razor b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/HttpTestClientParts/OpenAPIParameterDescription.razor
new file mode 100644
index 00000000..89222453
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/HttpTestClientParts/OpenAPIParameterDescription.razor
@@ -0,0 +1,21 @@
+@using ArcaneLibs.Extensions
+@using Spacebar.AdminAPI.TestClient.Classes.OpenAPI
+<span title="@Parameter.ToJson()">@Summary</span>
+@if (Parameter.Name != Parameter.Description && !string.IsNullOrWhiteSpace(Parameter.Description)) {
+ <i> - @Parameter.Description</i>
+}
+
+@code {
+
+ private string Summary { get; set; } = "Unbound parameter";
+
+ [Parameter]
+ public required OpenApiPath.OpenApiOperation.OpenApiParameter Parameter {
+ get;
+ set {
+ field = value;
+ Summary = $"{Parameter.Name}{(Parameter.Required ? "*" : "")} ({Parameter.Schema.Type})";
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Spacebar.AdminAPI.TestClient.csproj b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Spacebar.AdminAPI.TestClient.csproj
index d9aeb06a..34ad6fa2 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Spacebar.AdminAPI.TestClient.csproj
+++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Spacebar.AdminAPI.TestClient.csproj
@@ -17,8 +17,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20241210-161342" />
- <PackageReference Include="ArcaneLibs.Blazor.Components" Version="1.0.0-preview.20241210-161342" />
+ <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20251005-232225" />
+ <PackageReference Include="ArcaneLibs.Blazor.Components" Version="1.0.0-preview.20251005-232225" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.0" PrivateAssets="all"/>
|