mirror of
https://github.com/spacebarchat/server.git
synced 2026-05-25 20:44:59 +00:00
admin api packaging
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
+352
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
@inherits LayoutComponentBase
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu/>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #333333;
|
||||
border-bottom: 1px solid #444444;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">Spacebar.AdminApi.TestClient</a>
|
||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
||||
<nav class="nav flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</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>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="Guilds">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Guilds
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="Media">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Media
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool collapseNavMenu = true;
|
||||
|
||||
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||
|
||||
private void ToggleNavMenu() {
|
||||
collapseNavMenu = !collapseNavMenu;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
@page "/"
|
||||
@using System.Net.Http.Headers
|
||||
@using Spacebar.AdminApi.TestClient.Services
|
||||
@inject Config Config
|
||||
@inject ILocalStorageService LocalStorage
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<span style="@($"color: {(IsApiUrlValid ? "green" : "red")};")">Spacebar API URL: </span>
|
||||
<InputText @bind-Value:get="Config.ApiUrl" @bind-Value:set="@(async (v) => {
|
||||
Config.ApiUrl = v!;
|
||||
await ValidateAndSaveConfig();
|
||||
})"/>
|
||||
<br/>
|
||||
|
||||
<!-- <span style="@($"color: {(IsGatewayUrlValid ? "green" : "red")};")">Spacebar Gateway URL: </span> -->
|
||||
<!-- <InputText @bind-Value="GatewayUrl" /> -->
|
||||
<!-- <br /> -->
|
||||
|
||||
<span style="@($"color: {(IsCdnUrlValid ? "green" : "red")};")">Spacebar CDN URL: </span>
|
||||
<InputText @bind-Value:get="Config.CdnUrl" @bind-Value:set="@(async (v) => {
|
||||
Config.CdnUrl = v!;
|
||||
await ValidateAndSaveConfig();
|
||||
})"/>
|
||||
<br/>
|
||||
|
||||
<span style="@($"color: {(IsAdminApiUrlValid ? "green" : "red")};")">Spacebar Admin API URL: </span>
|
||||
<InputText @bind-Value:get="Config.AdminUrl" @bind-Value:set="@(async (v) => {
|
||||
Config.AdminUrl = v!;
|
||||
await ValidateAndSaveConfig();
|
||||
})"/>
|
||||
<br/>
|
||||
|
||||
<span style="@($"color: {(IsAccessTokenValid ? "green" : "red")};")">Access Token: </span>
|
||||
<InputText @bind-Value:get="Config.AccessToken" @bind-Value:set="@(async (v) => {
|
||||
Config.AccessToken = v!;
|
||||
await ValidateAndSaveConfig();
|
||||
})"/>
|
||||
<a href="/login">New access token</a>
|
||||
<br/>
|
||||
|
||||
@code {
|
||||
|
||||
private bool IsApiUrlValid { get; set; }
|
||||
|
||||
// private bool IsGatewayUrlValid { get; set; }
|
||||
private bool IsCdnUrlValid { get; set; }
|
||||
private bool IsAdminApiUrlValid { get; set; }
|
||||
private bool IsAccessTokenValid { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
await ValidateAndSaveConfig();
|
||||
}
|
||||
|
||||
private async Task ValidateAndSaveConfig() {
|
||||
await LocalStorage.SetItemAsync("sb_admin_tc_config", Config);
|
||||
|
||||
using var hc = new HttpClient();
|
||||
HttpResponseMessage response;
|
||||
try {
|
||||
response = await hc.GetAsync(Config.ApiUrl + "/api/v9/policies/instance/domains");
|
||||
IsApiUrlValid = response.IsSuccessStatusCode;
|
||||
}
|
||||
catch {
|
||||
IsApiUrlValid = false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
// response = await hc.GetAsync(Config.GatewayUrl + "/api/v9/policies/instance");
|
||||
// IsGatewayUrlValid = response.IsSuccessStatusCode;
|
||||
// StateHasChanged();
|
||||
|
||||
try {
|
||||
response = await hc.GetAsync(Config.CdnUrl + "/ping");
|
||||
IsCdnUrlValid = response.IsSuccessStatusCode;
|
||||
}
|
||||
catch {
|
||||
IsCdnUrlValid = false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
try {
|
||||
response = await hc.GetAsync(Config.AdminUrl + "/_spacebar/admin/ping");
|
||||
IsAdminApiUrlValid = response.IsSuccessStatusCode;
|
||||
}
|
||||
catch {
|
||||
IsAdminApiUrlValid = false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
try {
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, Config.AdminUrl + "/_spacebar/admin/whoami");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken);
|
||||
response = await hc.SendAsync(request);
|
||||
IsAccessTokenValid = response.IsSuccessStatusCode;
|
||||
}
|
||||
catch {
|
||||
IsAccessTokenValid = false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
@page "/HttpTestClient"
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -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})";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
@page "/Login"
|
||||
@using System.Text.Json.Nodes
|
||||
@using Spacebar.AdminApi.TestClient.Services
|
||||
@inject ILocalStorageService LocalStorage
|
||||
@inject Config Config
|
||||
@inject NavigationManager Navigation
|
||||
<h3>Login</h3>
|
||||
|
||||
<span>Email: </span>
|
||||
<InputText @bind-Value="Email"/>
|
||||
<br/>
|
||||
|
||||
<span>Password: </span>
|
||||
<InputText type="password" @bind-Value="Password"/>
|
||||
<br/>
|
||||
|
||||
<button @onclick="DoLogin">Login</button>
|
||||
<br/>
|
||||
|
||||
<pre style="color: red; font-family: 'JetBrains Mono',monospace">@Error</pre>
|
||||
|
||||
|
||||
@code {
|
||||
private string Email { get; set; }
|
||||
private string Password { get; set; }
|
||||
private string Error { get; set; }
|
||||
|
||||
private async Task DoLogin() {
|
||||
HttpResponseMessage response;
|
||||
using var hc = new HttpClient();
|
||||
|
||||
try {
|
||||
response = await hc.PostAsJsonAsync(Config.ApiUrl + "/api/v9/auth/login", new {
|
||||
login = Email,
|
||||
password = Password,
|
||||
login_source = "Spacebar Admin API Test Client",
|
||||
undelete = false
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
Error = e.ToString();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode) {
|
||||
Error = await response.Content.ReadAsStringAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var content = await response.Content.ReadFromJsonAsync<JsonObject>();
|
||||
var accessToken = content!["token"].ToString();
|
||||
Config.AccessToken = accessToken;
|
||||
await LocalStorage.SetItemAsync("sb_admin_tc_config", Config);
|
||||
Navigation.NavigateTo("/", true, true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@page "/Media"
|
||||
<h3>Index of /Media</h3>
|
||||
<hr/>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
@page "/Media/ByUser"
|
||||
@using System.Net.Http.Headers
|
||||
@using System.Reflection
|
||||
@using Spacebar.AdminApi.Models
|
||||
@using Spacebar.AdminApi.TestClient.Services
|
||||
@using ArcaneLibs.Blazor.Components
|
||||
@inject Config Config
|
||||
@inject ILocalStorageService LocalStorage
|
||||
|
||||
<PageTitle>Uploaded media by user</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>
|
||||
|
||||
<InputSelect @bind-Value="@SelectedUserId">
|
||||
<option value="">All users</option>
|
||||
@if (UserList is { Count: > 0 }) {
|
||||
@foreach (var user in UserList.OrderByDescending(u => u.Id).Where(x => !x.Deleted)) {
|
||||
<option value="@user.Id">@user.Username</option>
|
||||
}
|
||||
}
|
||||
</InputSelect>
|
||||
|
||||
|
||||
<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 UserMedia) {
|
||||
<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(FileMetadataModel).GetProperties()
|
||||
.ToDictionary(p => p, p => p.Name == "Username" || p.Name == "Id" || p.Name == "MessageCount");
|
||||
|
||||
private List<UserModel> UserList { get; set; } = new();
|
||||
private List<FileMetadataModel> UserMedia { get; set; } = new();
|
||||
|
||||
[SupplyParameterFromQuery(Name = "UserId")]
|
||||
public string? SelectedUserId {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (string.IsNullOrWhiteSpace(field))
|
||||
UserMedia.Clear();
|
||||
else _ = GetMediaForUser(value!);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
using var hc = new HttpClient();
|
||||
hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken);
|
||||
var response = await hc.GetAsync(Config.AdminUrl + "/_spacebar/admin/users/");
|
||||
if (!response.IsSuccessStatusCode) throw new Exception(await response.Content.ReadAsStringAsync());
|
||||
var content = response.Content.ReadFromJsonAsAsyncEnumerable<UserModel>();
|
||||
await foreach (var user in content) {
|
||||
UserList.Add(user!);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GetMediaForUser(string userId) {
|
||||
using var hc = new HttpClient();
|
||||
hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken);
|
||||
var response = await hc.GetAsync(Config.AdminUrl + $"/_spacebar/admin/media/user/{userId}/attachments");
|
||||
if (!response.IsSuccessStatusCode) throw new Exception(await response.Content.ReadAsStringAsync());
|
||||
var content = response.Content.ReadFromJsonAsAsyncEnumerable<FileMetadataModel>();
|
||||
await foreach (var media in content) {
|
||||
UserMedia.Add(media!);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
@page "/Users"
|
||||
@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>Users</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 @UserList.Count users.</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 UserList.Where(x => !x.Deleted).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(UserModel).GetProperties()
|
||||
.ToDictionary(p => p, p => p.Name == "Username" || p.Name == "Id" || p.Name == "MessageCount");
|
||||
|
||||
private List<UserModel> UserList { 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<UserModel>(Config.AdminUrl + "/_spacebar/admin/users/");
|
||||
// if (!response.IsSuccessStatusCode) throw new Exception(await response.Content.ReadAsStringAsync());
|
||||
// var content = response.Content.ReadFromJsonAsAsyncEnumerable<UserModel>();
|
||||
await foreach (var user in response) {
|
||||
// Console.WriteLine(user.ToJson(indent: false, ignoreNull: true));
|
||||
UserList.Add(user!);
|
||||
if(UserList.Count % 1000 == 0)
|
||||
StateHasChanged();
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
@page "/Users/Delete/{Id}"
|
||||
@using System.Net.Http.Headers
|
||||
@using System.Text.Json
|
||||
@using System.Text.Json.Nodes
|
||||
@using ArcaneLibs.Extensions
|
||||
@using Spacebar.AdminApi.Models
|
||||
@using Spacebar.AdminApi.TestClient.Services
|
||||
@inject Config Config
|
||||
<h3>UsersDelete - @Id</h3>
|
||||
|
||||
Deleted @ChannelDeleteProgress.Sum(x=>x.Value.Deleted) messages so far!
|
||||
@foreach (var (channel, progress) in ChannelDeleteProgress.Where(x=>x.Value.Deleted != x.Value.Total).OrderByDescending(x=>x.Value.Progress)) {
|
||||
<div>@channel: @progress.Total total, @progress.Deleted deleted</div>
|
||||
<progress max="@progress.Total" value="@progress.Deleted"></progress>
|
||||
}
|
||||
|
||||
@if (Done) {
|
||||
<h1>Done!</h1>
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public required string Id { get; set; }
|
||||
|
||||
private Dictionary<string, DeleteProgress> ChannelDeleteProgress { get; set; } = new();
|
||||
|
||||
private bool Done { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
var hc = new StreamingHttpClient();
|
||||
hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken);
|
||||
var response = await hc.GetAsync(Config.AdminUrl + $"/_spacebar/admin/Users/{Id}/delete?messageDeleteChunkSize=100");
|
||||
if (!response.IsSuccessStatusCode) throw new Exception(await response.Content.ReadAsStringAsync());
|
||||
var content = response.Content.ReadFromJsonAsAsyncEnumerable<AsyncActionResult>();
|
||||
await foreach (var actionResult in content) {
|
||||
Console.WriteLine(actionResult.ToJson(indent: false));
|
||||
switch (actionResult.MessageType) {
|
||||
case "STATS": {
|
||||
var data = JsonSerializer.Deserialize<JsonObject>(actionResult.Data.ToJson());
|
||||
ChannelDeleteProgress = data!["messages_per_channel"]!
|
||||
.Deserialize<Dictionary<string, int>>()!
|
||||
.ToDictionary(x=>x.Key, x=>new DeleteProgress { Total = x.Value });
|
||||
break;
|
||||
}
|
||||
case "BULK_DELETED": {
|
||||
var data = JsonSerializer.Deserialize<JsonObject>(actionResult.Data.ToJson());
|
||||
ChannelDeleteProgress[data!["channel_id"]!.ToString()].Deleted += data!["deleted"]!.GetValue<int>();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
Console.WriteLine($"Unknown message type: {actionResult.MessageType}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
await Task.Delay(1);
|
||||
}
|
||||
|
||||
Done = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private class DeleteProgress {
|
||||
public int Total { get; set; }
|
||||
public int Deleted { get; set; } = 0;
|
||||
public float Progress => (float)Deleted / Total;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Blazored.LocalStorage;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Spacebar.AdminApi.TestClient;
|
||||
using Spacebar.AdminApi.TestClient.Services;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
|
||||
try {
|
||||
builder.Configuration.AddJsonStream(await new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }.GetStreamAsync("/appsettings.json"));
|
||||
#if DEBUG
|
||||
builder.Configuration.AddJsonStream(await new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }.GetStreamAsync("/appsettings.Development.json"));
|
||||
#endif
|
||||
}
|
||||
catch (HttpRequestException e) {
|
||||
if (e.StatusCode == HttpStatusCode.NotFound)
|
||||
Console.WriteLine("Could not load appsettings, server returned 404.");
|
||||
else
|
||||
Console.WriteLine("Could not load appsettings: " + e);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Console.WriteLine("Could not load appsettings: " + e);
|
||||
}
|
||||
|
||||
builder.Logging.AddConfiguration(
|
||||
builder.Configuration.GetSection("Logging"));
|
||||
|
||||
builder.Services.AddBlazoredLocalStorageAsSingleton(config => {
|
||||
config.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
|
||||
config.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
config.JsonSerializerOptions.IgnoreReadOnlyProperties = true;
|
||||
config.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
|
||||
config.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
config.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip;
|
||||
config.JsonSerializerOptions.WriteIndented = false;
|
||||
});
|
||||
|
||||
// temporarily build the service provider to read config
|
||||
{
|
||||
await using var sp = builder.Services.BuildServiceProvider();
|
||||
var localStorage = sp.GetRequiredService<ILocalStorageService>();
|
||||
var config = await localStorage.GetItemAsync<Config>("sb_admin_tc_config");
|
||||
if (config == null) {
|
||||
config = new Config();
|
||||
await localStorage.SetItemAsync("sb_admin_tc_config", config);
|
||||
}
|
||||
builder.Services.AddSingleton(config);
|
||||
}
|
||||
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5179",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Spacebar.AdminApi.TestClient.Services;
|
||||
|
||||
public class Config {
|
||||
[JsonPropertyName("api_url")]
|
||||
public string ApiUrl { get; set; } = "http://localhost:3001";
|
||||
|
||||
[JsonPropertyName("gateway_url")]
|
||||
public string GatewayUrl { get; set; } = "http://localhost:3002";
|
||||
|
||||
[JsonPropertyName("cdn_url")]
|
||||
public string CdnUrl { get; set; } = "http://localhost:3003";
|
||||
|
||||
[JsonPropertyName("admin_url")]
|
||||
public string AdminUrl { get; set; } = "http://localhost:5112";
|
||||
|
||||
[JsonPropertyName("access_token")]
|
||||
public string? AccessToken { get; set; } = string.Empty;
|
||||
}
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
#define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances
|
||||
// #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArcaneLibs;
|
||||
using ArcaneLibs.Extensions;
|
||||
|
||||
namespace Spacebar.AdminApi.TestClient.Services;
|
||||
|
||||
#if SINGLE_HTTPCLIENT
|
||||
// TODO: Add URI wrapper for
|
||||
public class StreamingHttpClient {
|
||||
private static readonly HttpClient Client;
|
||||
|
||||
static StreamingHttpClient() {
|
||||
try {
|
||||
var handler = new SocketsHttpHandler {
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(15),
|
||||
MaxConnectionsPerServer = 4096,
|
||||
EnableMultipleHttp2Connections = true
|
||||
};
|
||||
Client = new HttpClient(handler) {
|
||||
DefaultRequestVersion = new Version(3, 0),
|
||||
Timeout = TimeSpan.FromDays(1)
|
||||
};
|
||||
}
|
||||
catch (PlatformNotSupportedException e) {
|
||||
Console.WriteLine("Failed to create HttpClient with connection pooling, continuing without connection pool!");
|
||||
Console.WriteLine("Original exception (safe to ignore!):");
|
||||
Console.WriteLine(e);
|
||||
|
||||
Client = new HttpClient {
|
||||
DefaultRequestVersion = new Version(3, 0)
|
||||
};
|
||||
}
|
||||
catch (Exception e) {
|
||||
Console.WriteLine("Failed to create HttpClient:");
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#if SYNC_HTTPCLIENT
|
||||
internal SemaphoreSlim _rateLimitSemaphore { get; } = new(1, 1);
|
||||
#endif
|
||||
|
||||
public static bool LogRequests = true;
|
||||
public Dictionary<string, string> AdditionalQueryParameters { get; set; } = new();
|
||||
|
||||
public Uri? BaseAddress { get; set; }
|
||||
|
||||
// default headers, not bound to client
|
||||
public HttpRequestHeaders DefaultRequestHeaders { get; set; } =
|
||||
typeof(HttpRequestHeaders).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, [], null)?.Invoke([]) as HttpRequestHeaders ??
|
||||
throw new InvalidOperationException("Failed to create HttpRequestHeaders");
|
||||
|
||||
private static JsonSerializerOptions GetJsonSerializerOptions(JsonSerializerOptions? options = null) {
|
||||
options ??= new JsonSerializerOptions();
|
||||
// options.Converters.Add(new JsonFloatStringConverter());
|
||||
// options.Converters.Add(new JsonDoubleStringConverter());
|
||||
// options.Converters.Add(new JsonDecimalStringConverter());
|
||||
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
return options;
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> SendUnhandledAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
|
||||
if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null");
|
||||
// if (!request.RequestUri.IsAbsoluteUri)
|
||||
request.RequestUri = request.RequestUri.EnsureAbsolute(BaseAddress!);
|
||||
var swWait = Stopwatch.StartNew();
|
||||
#if SYNC_HTTPCLIENT
|
||||
await _rateLimitSemaphore.WaitAsync(cancellationToken);
|
||||
#endif
|
||||
|
||||
if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null");
|
||||
if (!request.RequestUri.IsAbsoluteUri)
|
||||
request.RequestUri = new Uri(BaseAddress ?? throw new InvalidOperationException("Relative URI passed, but no BaseAddress is specified!"), request.RequestUri);
|
||||
swWait.Stop();
|
||||
var swExec = Stopwatch.StartNew();
|
||||
|
||||
foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value);
|
||||
foreach (var (key, value) in DefaultRequestHeaders) {
|
||||
if (request.Headers.Contains(key)) continue;
|
||||
request.Headers.Add(key, value);
|
||||
}
|
||||
|
||||
request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true);
|
||||
|
||||
if (LogRequests)
|
||||
Console.WriteLine("Sending " + request.Summarise(includeHeaders: true, includeQuery: true, includeContentIfText: false, hideHeaders: ["Accept"]));
|
||||
|
||||
HttpResponseMessage? responseMessage;
|
||||
try {
|
||||
responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
}
|
||||
catch (Exception e) {
|
||||
if (e is TaskCanceledException or TimeoutException) {
|
||||
if (request.Method == HttpMethod.Get && !cancellationToken.IsCancellationRequested) {
|
||||
await Task.Delay(Random.Shared.Next(500, 2500), cancellationToken);
|
||||
request.ResetSendStatus();
|
||||
return await SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
else if (!e.ToString().StartsWith("TypeError: NetworkError"))
|
||||
Console.WriteLine(
|
||||
$"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}");
|
||||
|
||||
throw;
|
||||
}
|
||||
#if SYNC_HTTPCLIENT
|
||||
finally {
|
||||
_rateLimitSemaphore.Release();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Console.WriteLine($"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.GetContentLength())}, WAIT={swWait.ElapsedMilliseconds}ms, EXEC={swExec.ElapsedMilliseconds}ms)");
|
||||
if (LogRequests)
|
||||
Console.WriteLine("Received " + responseMessage.Summarise(includeHeaders: true, includeContentIfText: false, hideHeaders: [
|
||||
"Server",
|
||||
"Date",
|
||||
"Transfer-Encoding",
|
||||
"Connection",
|
||||
"Vary",
|
||||
"Content-Length",
|
||||
"Access-Control-Allow-Origin",
|
||||
"Access-Control-Allow-Methods",
|
||||
"Access-Control-Allow-Headers",
|
||||
"Access-Control-Expose-Headers",
|
||||
"Cache-Control",
|
||||
"Cross-Origin-Resource-Policy",
|
||||
"X-Content-Security-Policy",
|
||||
"Referrer-Policy",
|
||||
"X-Robots-Tag",
|
||||
"Content-Security-Policy"
|
||||
]));
|
||||
|
||||
return responseMessage;
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) {
|
||||
var responseMessage = await SendUnhandledAsync(request, cancellationToken);
|
||||
if (responseMessage.IsSuccessStatusCode) return responseMessage;
|
||||
|
||||
//retry on gateway timeout
|
||||
// if (responseMessage.StatusCode == HttpStatusCode.GatewayTimeout) {
|
||||
// request.ResetSendStatus();
|
||||
// return await SendAsync(request, cancellationToken);
|
||||
// }
|
||||
|
||||
//error handling
|
||||
var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken);
|
||||
if (content.Length == 0)
|
||||
throw new DataException("Content was empty");
|
||||
// throw new MatrixException() {
|
||||
// ErrorCode = "M_UNKNOWN",
|
||||
// Error = "Unknown error, server returned no content"
|
||||
// };
|
||||
|
||||
// if (!content.StartsWith('{')) throw new InvalidDataException("Encountered invalid data:\n" + content);
|
||||
if (!content.TrimStart().StartsWith('{')) {
|
||||
responseMessage.EnsureSuccessStatusCode();
|
||||
throw new InvalidDataException("Encountered invalid data:\n" + content);
|
||||
}
|
||||
//we have a matrix error
|
||||
|
||||
throw new Exception("Unknown http exception");
|
||||
// MatrixException? ex;
|
||||
// try {
|
||||
// ex = JsonSerializer.Deserialize<MatrixException>(content);
|
||||
// }
|
||||
// catch (JsonException e) {
|
||||
// throw new LibMatrixException() {
|
||||
// ErrorCode = "M_INVALID_JSON",
|
||||
// Error = e.Message + "\nBody:\n" + await responseMessage.Content.ReadAsStringAsync(cancellationToken)
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// Debug.Assert(ex != null, nameof(ex) + " != null");
|
||||
// ex.RawContent = content;
|
||||
// // Console.WriteLine($"Failed to send request: {ex}");
|
||||
// if (ex.RetryAfterMs is null) throw ex!;
|
||||
// //we have a ratelimit error
|
||||
// await Task.Delay(ex.RetryAfterMs.Value, cancellationToken);
|
||||
request.ResetSendStatus();
|
||||
return await SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
// GetAsync
|
||||
public Task<HttpResponseMessage> GetAsync([StringSyntax("Uri")] string? requestUri, CancellationToken? cancellationToken = null) =>
|
||||
SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken ?? CancellationToken.None);
|
||||
|
||||
// GetFromJsonAsync
|
||||
public async Task<T?> TryGetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) {
|
||||
try {
|
||||
return await GetFromJsonAsync<T>(requestUri, options, cancellationToken);
|
||||
}
|
||||
catch (JsonException e) {
|
||||
Console.WriteLine($"Failed to deserialize response from {requestUri}: {e.Message}");
|
||||
return default;
|
||||
}
|
||||
catch (HttpRequestException e) {
|
||||
Console.WriteLine($"Failed to get {requestUri}: {e.Message}");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T> GetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) {
|
||||
options = GetJsonSerializerOptions(options);
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
var response = await SendAsync(request, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
|
||||
return await JsonSerializer.DeserializeAsync<T>(responseStream, options, cancellationToken) ??
|
||||
throw new InvalidOperationException("Failed to deserialize response");
|
||||
}
|
||||
|
||||
// GetStreamAsync
|
||||
public async Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) {
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
var response = await SendAsync(request, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> PutAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null,
|
||||
CancellationToken cancellationToken = default) where T : notnull {
|
||||
options = GetJsonSerializerOptions(options);
|
||||
var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options),
|
||||
Encoding.UTF8, "application/json");
|
||||
return await SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> PostAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null,
|
||||
CancellationToken cancellationToken = default) where T : notnull {
|
||||
options ??= new JsonSerializerOptions();
|
||||
// options.Converters.Add(new JsonFloatStringConverter());
|
||||
// options.Converters.Add(new JsonDoubleStringConverter());
|
||||
// options.Converters.Add(new JsonDecimalStringConverter());
|
||||
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options),
|
||||
Encoding.UTF8, "application/json");
|
||||
return await SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<T?> GetAsyncEnumerableFromJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, JsonSerializerOptions? options = null) {
|
||||
options = GetJsonSerializerOptions(options);
|
||||
var res = await GetAsync(requestUri);
|
||||
options.PropertyNameCaseInsensitive = true;
|
||||
var result = JsonSerializer.DeserializeAsyncEnumerable<T>(await res.Content.ReadAsStreamAsync(), options);
|
||||
await foreach (var resp in result) yield return resp;
|
||||
}
|
||||
|
||||
public static async Task<bool> CheckSuccessStatus(string url) {
|
||||
//cors causes failure, try to catch
|
||||
try {
|
||||
var resp = await Client.GetAsync(url);
|
||||
return resp.IsSuccessStatusCode;
|
||||
}
|
||||
catch (Exception e) {
|
||||
Console.WriteLine($"Failed to check success status: {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> PostAsync(string uri, HttpContent? content, CancellationToken cancellationToken = default) {
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, uri) {
|
||||
Content = content
|
||||
};
|
||||
return await SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string url) {
|
||||
var request = new HttpRequestMessage(HttpMethod.Delete, url);
|
||||
await SendAsync(request);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> DeleteAsJsonAsync<T>(string url, T payload) {
|
||||
var request = new HttpRequestMessage(HttpMethod.Delete, url) {
|
||||
Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
|
||||
};
|
||||
return await SendAsync(request);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
|
||||
|
||||
<UseBlazorWebAssembly>true</UseBlazorWebAssembly>
|
||||
<BlazorEnableCompression>false</BlazorEnableCompression>
|
||||
<BlazorCacheBootResources>false</BlazorCacheBootResources>
|
||||
<!-- <RunAOTCompilation>true</RunAOTCompilation>-->
|
||||
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<!-- <RuntimeFrameworkVersion>10.0.2-servicing.25570.103</RuntimeFrameworkVersion>-->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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="10.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="appsettings.Development.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="wwwroot\appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Spacebar.AdminApi.Models\Spacebar.AdminApi.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,11 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Blazored.LocalStorage
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using Spacebar.AdminApi.TestClient
|
||||
@using Spacebar.AdminApi.TestClient.Layout
|
||||
@@ -0,0 +1,217 @@
|
||||
[
|
||||
{
|
||||
"pname": "ArcaneLibs",
|
||||
"version": "1.0.0-preview.20251005-232225",
|
||||
"hash": "sha256-EsYLSiyX5Nj+ZpFb6FOcAYqDsQFSbvgm9NKaarJjK/0="
|
||||
},
|
||||
{
|
||||
"pname": "ArcaneLibs.Blazor.Components",
|
||||
"version": "1.0.0-preview.20251005-232225",
|
||||
"hash": "sha256-9rgq/bFNwZf+mpTldzhr7VPwlseLR21nDlJWGgIaGIQ="
|
||||
},
|
||||
{
|
||||
"pname": "Blazored.LocalStorage",
|
||||
"version": "4.5.0",
|
||||
"hash": "sha256-0vklTFHEGgNG8V6ivQictuooyiXS2nMn/qLpfYhEBlE="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.App.Internal.Assets",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-IyY5Ymdkmf9S9qRwYXX9rWpzcU3fuDR+ITeaaeJQ/Dk="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.Authorization",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-g1MagKFkZF0LttdK5GLdHCXe4d1qOXv57ngz7XnhRrk="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.Components",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-nrCuCDRbvD5XQyn3ySW/CD4yKYD6coC71JH2ke6xtXI="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.Components",
|
||||
"version": "9.0.9",
|
||||
"hash": "sha256-1+lIIRfIwHC3XWt2tMuQ3NxoqIsVRgAR+/l9vttQLd8="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.Components.Analyzers",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-2mbSRBB/2eT0fYouhDKM5OFRZGQ0Jv8HgcQLvufoHq4="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.Components.Forms",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-el2T9pvjNexq5lfJhp+7xZYa/1CS6RchIWKmtiKg+vI="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.Components.Forms",
|
||||
"version": "9.0.9",
|
||||
"hash": "sha256-oUwcqvDLtychAdga+fAJp70wSEdVheYsV6l5Qnfdq6M="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.Components.Web",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-xDf1DBNBceAFKu4er2inn6gUKa1T1L8T3ewlFcuxddE="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.Components.Web",
|
||||
"version": "8.0.0",
|
||||
"hash": "sha256-dsCb4B6r5iHPbEp8+uFzAfD1txGI5dEIWgwtT9+GgU8="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.Components.Web",
|
||||
"version": "9.0.9",
|
||||
"hash": "sha256-mgohB7RwOzE5XPB0Lg3h9pUIBKPXaul2F39uxfU7GD8="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.Components.WebAssembly",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-YeHralkfEzLmlrBj0jER+ta9hsbXv5skP81178V1ppw="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.Components.WebAssembly.DevServer",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-akCQI6iHuKIyDmZh5du+hifzPzQoGeMX3Y7ST/TFxRU="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.AspNetCore.Metadata",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-TxyiXUx8sWdWFWacBTWFaPeMa3z2+Zmc4VK/Qgq0YRw="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.DotNet.HotReload.WebAssembly.Browser",
|
||||
"version": "10.0.100",
|
||||
"hash": "sha256-ppwVl5tBHFFmMZ0EjpKn4OSBYNExf/S6ojeYdefX1+k="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Configuration",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-MsLskVPpkCvov5+DWIaALCt1qfRRX4u228eHxvpE0dg="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Configuration.Abstractions",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-GcgrnTAieCV7AVT13zyOjfwwL86e99iiO/MiMOxPGG0="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Configuration.Binder",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-YSiWoA3VQR22k6+bSEAUqeG7UDzZlJfHWDTubUO5V8U="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Configuration.FileExtensions",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-rN+3rqrHiTaBfHgP+E4dA8Qm2cFJPfbEcd93yKLsqlQ="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Configuration.Json",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-VCFukgsxiQ2MFGE6RDMFTGopBHbcZL2t0ER7ENaFXRY="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.DependencyInjection",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-LYm9hVlo/R9c2aAKHsDYJ5vY9U0+3Jvclme3ou3BtvQ="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.DependencyInjection",
|
||||
"version": "9.0.9",
|
||||
"hash": "sha256-UHG/uj9hjCRWmz2LZ4wR721ooZYGtBy4TT+lNeVzyrU="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.DependencyInjection.Abstractions",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-9iodXP39YqgxomnOPOxd/mzbG0JfOSXzFoNU3omT2Ps="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Diagnostics",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-o7QkCisEcFIh227qBUfWFci2ns4cgEpLqpX7YvHGToQ="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Diagnostics.Abstractions",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-cix7QxQ/g3sj6reXu3jn0cRv2RijzceaLLkchEGTt5E="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.FileProviders.Abstractions",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-CHDs2HCN8QcfuYQpgNVszZ5dfXFe4yS9K2GoQXecc20="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.FileProviders.Physical",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-2Rw/cwBO+/A3QY2IjN/c8Y0LhtC1qTBL7VdJiD1J2UQ="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.FileSystemGlobbing",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-ETfVTdsdBtp69EggLg/AARTQW4lLQYVdVldXIQrsjZA="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Logging",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-P+zPAadLL63k/GqK34/qChqQjY9aIRxZfxlB9lqsSrs="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Logging.Abstractions",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-BnhgGZc01HwTSxogavq7Ueq4V7iMA3wPnbfRwQ4RhGk="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Logging.Configuration",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-7/TWO1aq8hdgbcTEKDBWIjgSC9KpFN3kRnMX+12bOkU="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Options",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-j5MOqZSKeUtxxzmZjzZMGy0vELHdvPraqwTQQQNVsYA="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Options.ConfigurationExtensions",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-XGAs5DxMvWnmjX8dqRwKY0vsuS40SHvsfJqB1rO4L7k="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Primitives",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-Dup08KcptLjlnpN5t5//+p4n8FUTgRAq4n/w1s6us+I="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Primitives",
|
||||
"version": "9.0.9",
|
||||
"hash": "sha256-bCd4Bj5uP4kT0hCvs0LZS8IVqEtpOIyhSiay5ijJbBA="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Validation",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-BbmNqKlqNd/37IU5X1wjc3VCxfrBTA1bkVqk1myU+H0="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.JSInterop",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-/y6la0IzcE5N/thRGLDZYsvNEeWPlW2IwEJtIu3sdaE="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.JSInterop",
|
||||
"version": "9.0.9",
|
||||
"hash": "sha256-fCUkGYS6HKZ0NW1xfuI5aCD2yGqpeIjaWFcS0dBZMWE="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.JSInterop.WebAssembly",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-dJdTHFh4lZOmXI+88mLdJOf6UsKq/80N59+COq/KPpg="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.NET.Sdk.WebAssembly.Pack",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-AWqn+WUvMjdec4KX4RpTW1ZgT4K+fYhpkrcf706Zt/w="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.NETCore.App.Runtime.Mono.browser-wasm",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-1CpAq/TBpFaZncLb+Z3xaq/J7/O8ZTEl3qBidCpQj9M="
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Trace",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #0071c1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.loading-progress {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 20vh auto 1rem auto;
|
||||
}
|
||||
|
||||
.loading-progress circle {
|
||||
fill: none;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.6rem;
|
||||
transform-origin: 50% 50%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.loading-progress circle:last-child {
|
||||
stroke: #1b6ec2;
|
||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||
transition: stroke-dasharray 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-progress-text {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||
}
|
||||
|
||||
.loading-progress-text:after {
|
||||
content: var(--blazor-load-percentage-text, "Loading");
|
||||
}
|
||||
|
||||
code {
|
||||
color: #c02d76;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||
text-align: start;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Spacebar Admin API Test client</title>
|
||||
<base href="/"/>
|
||||
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="lib/jetbrains-mono/jetbrains-mono.css"/>
|
||||
<link rel="stylesheet" href="css/app.css"/>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<link href="Spacebar.AdminApi.TestClient.styles.css" rel="stylesheet"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%"/>
|
||||
<circle r="40%" cx="50%" cy="50%"/>
|
||||
</svg>
|
||||
<div class="loading-progress-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+4085
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+4084
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+597
@@ -0,0 +1,597 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+594
@@ -0,0 +1,594 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+5402
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+5393
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+12057
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+12030
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+6314
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+4447
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+4494
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+118
@@ -0,0 +1,118 @@
|
||||
/* source: https://gist.github.com/aasmpro/95776294ecf48bd7d0562504bad848ea */
|
||||
|
||||
/* normal fonts */
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url("./ttf/JetBrainsMono-Thin.ttf") format("truetype");
|
||||
src: url("./webfonts/JetBrainsMono-Thin.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url("./webfonts/JetBrainsMono-ExtraLight.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url("./webfonts/JetBrainsMono-Light.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("./webfonts/JetBrainsMono-Regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url("./webfonts/JetBrainsMono-Medium.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url("./webfonts/JetBrainsMono-SemiBold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("./webfonts/JetBrainsMono-Bold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url("./webfonts/JetBrainsMono-ExtraBold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* italic fonts */
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
src: url("./webfonts/JetBrainsMono-ThinItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
src: url("./webfonts/JetBrainsMono-ExtraLightItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url("./webfonts/JetBrainsMono-LightItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url("./webfonts/JetBrainsMono-Italic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
src: url("./webfonts/JetBrainsMono-MediumItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url("./webfonts/JetBrainsMono-SemiBoldItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url("./webfonts/JetBrainsMono-BoldItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: url("./webfonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2");
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user