mirror of
https://github.com/spacebarchat/server.git
synced 2026-07-28 22:49:24 +00:00
Local changes
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>
|
||||
@@ -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,39 @@
|
||||
<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="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>
|
||||
</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,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,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,72 @@
|
||||
@page "/Users"
|
||||
@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>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>
|
||||
|
||||
<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) {
|
||||
<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() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
@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>
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public required string Id { get; set; }
|
||||
|
||||
private Dictionary<string, DeleteProgress> ChannelDeleteProgress { get; set; } = new();
|
||||
|
||||
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/{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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.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>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20241210-161342" />
|
||||
<PackageReference Include="ArcaneLibs.Blazor.Components" Version="1.0.0-preview.20241210-161342" />
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.0" PrivateAssets="all"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="8.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,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>
|
||||
+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