Implemented template crud, db entities, import/export, ptero and pelican import

This commit is contained in:
2026-03-12 13:00:32 +00:00
parent 7c5dc657dc
commit e7b1e77d0a
68 changed files with 4269 additions and 24 deletions

View File

@@ -0,0 +1,75 @@
@page "/admin/servers"
@using LucideBlazor
@using MoonlightServers.Shared
@using ShadcnBlazor.Tab
@inject IAuthorizationService AuthorizationService
@inject NavigationManager Navigation
@attribute [Authorize(Policy = Permissions.Servers.View)]
<Tabs DefaultValue="@(Tab ?? "servers")" OnValueChanged="OnTabChanged">
<TabsList ClassName="inline-flex w-full lg:w-fit justify-start overflow-x-auto overflow-y-hidden">
<TabsTrigger Value="servers" Disabled="@(!ServersAccess.Succeeded)">
<ContainerIcon />
Servers
</TabsTrigger>
<TabsTrigger Value="nodes" Disabled="@(!NodesAccess.Succeeded)">
<ServerIcon />
Nodes
</TabsTrigger>
<TabsTrigger Value="templates" Disabled="@(!TemplatesAccess.Succeeded)">
<Package2Icon />
Templates
</TabsTrigger>
<TabsTrigger Value="manager" Disabled="@(!NodesAccess.Succeeded)">
<TableIcon />
Manager
</TabsTrigger>
</TabsList>
@if (ServersAccess.Succeeded)
{
<TabsContent Value="servers">
</TabsContent>
}
@if (NodesAccess.Succeeded)
{
<TabsContent Value="nodes">
<MoonlightServers.Frontend.Admin.Nodes.Overview />
</TabsContent>
}
@if (TemplatesAccess.Succeeded)
{
<TabsContent Value="templates">
<MoonlightServers.Frontend.Admin.Templates.Overview />
</TabsContent>
}
</Tabs>
@code
{
[Parameter]
[SupplyParameterFromQuery(Name = "tab")]
public string? Tab { get; set; }
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
private AuthorizationResult ServersAccess;
private AuthorizationResult NodesAccess;
private AuthorizationResult TemplatesAccess;
protected override async Task OnInitializedAsync()
{
var authState = await AuthState;
ServersAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Servers.View);
NodesAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Nodes.View);
TemplatesAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Templates.View);
}
private void OnTabChanged(string tab) => Navigation.NavigateTo($"/admin/servers?tab={tab}");
}

View File

@@ -27,7 +27,7 @@
<div class="flex flex-row gap-x-1.5">
<Button Variant="ButtonVariant.Secondary">
<Slot>
<a href="/admin/servers/nodes" @attributes="context">
<a href="/admin/servers?tab=nodes" @attributes="context">
<ChevronLeftIcon/>
Back
</a>
@@ -50,7 +50,7 @@
<FieldSet>
<FieldGroup>
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-5">
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
<Field>
<FieldLabel for="nodeName">Name</FieldLabel>
<TextInputField
@@ -98,7 +98,7 @@
$"Successfully created node {Request.Name}"
);
Navigation.NavigateTo("/admin/servers/nodes");
Navigation.NavigateTo("/admin/servers?tab=nodes");
return true;
}
}

View File

@@ -47,7 +47,7 @@
<div class="flex flex-row gap-x-1.5">
<Button Variant="ButtonVariant.Secondary">
<Slot>
<a href="/admin/servers/nodes" @attributes="context">
<a href="/admin/servers?tab=nodes" @attributes="context">
<ChevronLeftIcon/>
Back
</a>
@@ -70,7 +70,7 @@
<FieldSet>
<FieldGroup>
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-5">
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
<Field>
<FieldLabel for="nodeName">Name</FieldLabel>
<TextInputField
@@ -148,7 +148,7 @@
$"Successfully updated node {Request.Name}"
);
Navigation.NavigateTo("/admin/servers/nodes");
Navigation.NavigateTo("/admin/servers?tab=nodes");
return true;
}
}

View File

@@ -1,5 +1,3 @@
@page "/admin/servers/nodes"
@using LucideBlazor
@using Moonlight.Shared.Http.Requests
@using Moonlight.Shared.Http.Responses
@@ -15,7 +13,6 @@
@inject HttpClient HttpClient
@inject AlertDialogService AlertDialogService
@inject DialogService DialogService
@inject ToastService ToastService
@inject NavigationManager NavigationManager
@inject IAuthorizationService AuthorizationService
@@ -61,7 +58,6 @@
</CellTemplate>
</TemplateColumn>
<PropertyColumn Title="HTTP Endpoint"
Identifier="@nameof(NodeDto.HttpEndpointUrl)"
Field="u => u.HttpEndpointUrl"/>
<TemplateColumn>
<CellTemplate>

View File

@@ -0,0 +1,78 @@
@using LucideBlazor
@using MoonlightServers.Shared.Admin.Templates
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Tabels
@inherits Editor<List<MoonlightServers.Shared.Admin.Templates.UpdateConfigurationFileMappingDto>>
<div class="rounded-md bg-card shadow-sm border">
<Table>
<TableHeader>
<TableRow>
<TableHead>
Key
</TableHead>
<TableHead>
Value
</TableHead>
<TableHead ClassName="w-10"/>
</TableRow>
</TableHeader>
<TableBody>
@foreach (var mapping in Value)
{
<TableRow @key="mapping">
<TableCell>
<TextInputField @bind-Value="mapping.Key"
placeholder="server-port"/>
</TableCell>
<TableCell>
<TextInputField @bind-Value="mapping.Value"
placeholder="{{SERVER_PORT}}"/>
</TableCell>
<TableCell ClassName="text-right pr-4">
<Button @onclick="() => DeleteAsync(mapping)"
Size="ButtonSize.Icon"
Variant="ButtonVariant.Destructive">
<Trash2Icon/>
</Button>
</TableCell>
</TableRow>
}
<TableRow>
<TableCell colspan="999999">
<div class="flex justify-end">
<Button
@onclick="AddAsync"
Variant="ButtonVariant.Outline"
Size="ButtonSize.Sm">
<PlusIcon/>
Add Mapping
</Button>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
@code
{
private async Task DeleteAsync(UpdateConfigurationFileMappingDto mappingDto)
{
Value.Remove(mappingDto);
await ValueChanged.InvokeAsync(Value);
}
private async Task AddAsync()
{
Value.Add(new UpdateConfigurationFileMappingDto()
{
Key = "Change me",
Value = "{{CHANGE_ME}}"
});
await ValueChanged.InvokeAsync(Value);
}
}

View File

@@ -0,0 +1,76 @@
@using LucideBlazor
@using MoonlightServers.Shared.Admin.Templates
@using ShadcnBlazor.Accordions
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs
<div class="mb-5 flex justify-end">
<Button @onclick="Add">
<PlusIcon/>
Create
</Button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
@foreach (var file in FilesConfig.ConfigurationFiles)
{
<Accordion ClassName="-space-y-px" Type="AccordionType.Single">
<AccordionItem
ClassName="overflow-hidden border bg-card px-4 first:rounded-t-lg last:rounded-b-lg last:border-b"
Value="element">
<AccordionTrigger ClassName="hover:no-underline">
<div class="flex items-center gap-3">
<FileCogIcon/>
<span class="text-left">@file.Path</span>
</div>
</AccordionTrigger>
<AccordionContent ClassName="ps-7">
<FieldGroup>
<FieldSet>
<Field>
@{
var id = $"configFilePath{file.GetHashCode()}";
}
<FieldLabel for="@id">Path</FieldLabel>
<TextInputField
@bind-Value="file.Path"
id="@id"/>
</Field>
<Field>
@{
var id = $"configFileParser{file.GetHashCode()}";
}
<FieldLabel for="@id">Parser</FieldLabel>
<TextInputField
@bind-Value="file.Parser"
id="@id"/>
</Field>
</FieldSet>
<FieldSet>
<ConfigFileMappingEditor @bind-Value="file.Mappings" />
</FieldSet>
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
<Button Variant="ButtonVariant.Destructive"
@onclick="() => Delete(file)">
Delete
</Button>
</Field>
</FieldGroup>
</AccordionContent>
</AccordionItem>
</Accordion>
}
</div>
@code
{
[Parameter] public UpdateFilesConfigDto FilesConfig { get; set; }
private void Add()
=> FilesConfig.ConfigurationFiles.Add(new());
private void Delete(UpdateConfigurationFileDto dto)
=> FilesConfig.ConfigurationFiles.Remove(dto);
}

View File

@@ -0,0 +1,132 @@
@page "/admin/servers/templates/create"
@using LucideBlazor
@using Moonlight.Frontend.Helpers
@using MoonlightServers.Shared
@using MoonlightServers.Shared.Admin.Templates
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Tab
@inject HttpClient HttpClient
@inject NavigationManager Navigation
@inject ToastService ToastService
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="formContext">
<div class="flex flex-row justify-between">
<div class="flex flex-col">
<h1 class="text-xl font-semibold">Create Template</h1>
<div class="text-muted-foreground">
Create a new template
</div>
</div>
<div class="flex flex-row gap-x-1.5">
<Button Variant="ButtonVariant.Secondary">
<Slot>
<a href="/admin/servers?tab=templates" @attributes="context">
<ChevronLeftIcon/>
Back
</a>
</Slot>
</Button>
<SubmitButton>
<CheckIcon/>
Continue
</SubmitButton>
</div>
</div>
<DataAnnotationsValidator/>
<div class="mt-8">
<Card>
<CardContent>
<FieldGroup>
<FormValidationSummary/>
<FieldSet>
<FieldGroup>
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
<Field>
<FieldLabel for="templateName">Name</FieldLabel>
<TextInputField
@bind-Value="Request.Name"
id="templateName"/>
</Field>
<Field>
<FieldLabel for="templateAuthor">Author</FieldLabel>
<TextInputField
@bind-Value="Request.Author"
id="templateAuthor"/>
</Field>
<Field>
<FieldLabel for="templateVersion">Version</FieldLabel>
<TextInputField
@bind-Value="Request.Version"
id="templateVersion"/>
</Field>
<Field ClassName="col-span-1 lg:col-span-2 xl:col-span-3">
<FieldLabel for="templateDescription">Description</FieldLabel>
<TextareaInputField
@bind-Value="Request.Description"
id="templateDescription"/>
</Field>
<Field>
<FieldLabel for="templateDonateUrl">Donate URL</FieldLabel>
<TextInputField
@bind-Value="Request.DonateUrl"
id="templateDonateUrl"/>
</Field>
<Field>
<FieldLabel for="templateUpdateUrl">Update URL</FieldLabel>
<TextInputField
@bind-Value="Request.UpdateUrl"
id="templateUpdateUrl"/>
</Field>
</div>
</FieldGroup>
</FieldSet>
</FieldGroup>
</CardContent>
</Card>
</div>
</EnhancedEditForm>
@code
{
private CreateTemplateDto Request = new();
private async Task<bool> OnSubmitAsync(EditContext context, ValidationMessageStore validationMessageStore)
{
var response = await HttpClient.PostAsJsonAsync(
"/api/admin/servers/templates",
Request,
SerializationContext.Default.Options
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync(
"Template Creation",
$"Successfully created template {Request.Name}"
);
Navigation.NavigateTo("/admin/servers?tab=templates");
return true;
}
}

View File

@@ -0,0 +1,95 @@
@using Moonlight.Frontend.Helpers
@using MoonlightServers.Shared
@using MoonlightServers.Shared.Admin.Templates
@using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Switches
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader>
<DialogTitle>Create Docker Image</DialogTitle>
<DialogDescription>
Create a new docker image
</DialogDescription>
</DialogHeader>
<EnhancedEditForm @ref="Form" Model="Model" OnValidSubmit="OnValidSubmitAsync">
<FieldGroup>
<DataAnnotationsValidator/>
<FormValidationSummary/>
<FieldSet>
<Field>
<FieldLabel for="dockerImageDisplayName">Display Name</FieldLabel>
<TextInputField
@bind-Value="Model.DisplayName"
id="dockerImageDisplayName"/>
</Field>
<Field>
<FieldLabel for="dockerImageIdentifier">Image Name</FieldLabel>
<TextInputField
@bind-Value="Model.ImageName"
id="dockerImageIdentifier"/>
</Field>
<Field>
<FieldLabel for="dockerImageSkipPulling">Skip Pulling</FieldLabel>
<Switch id="dockerImageSkipPulling" @bind-Value="Model.SkipPulling" />
</Field>
</FieldSet>
</FieldGroup>
</EnhancedEditForm>
<DialogFooter>
<DialogClose>
<Slot>
<Button Type="button" Variant="ButtonVariant.Outline" @attributes="context">
Cancel
</Button>
</Slot>
</DialogClose>
<Button @onclick="() => Form.SubmitAsync()" Type="button">
Save
</Button>
</DialogFooter>
@code
{
[Parameter] public DetailedTemplateDto Template { get; set; }
[Parameter] public Func<Task> OnSubmit { get; set; }
private CreateDockerImageDto Model = new();
private EnhancedEditForm Form;
private async Task<bool> OnValidSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{
var response = await HttpClient.PostAsJsonAsync(
$"api/admin/servers/templates/{Template.Id}/dockerImages",
Model,
SerializationContext.Default.Options
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Model, validationMessageStore);
return false;
}
await OnSubmit.Invoke();
await ToastService.SuccessAsync(
"Docker Image Creation",
$"Successfully created variable {Model.DisplayName}"
);
await CloseAsync();
return true;
}
}

View File

@@ -0,0 +1,103 @@
@using Moonlight.Frontend.Helpers
@using MoonlightServers.Shared
@using MoonlightServers.Shared.Admin.Templates
@using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader>
<DialogTitle>Create Variable</DialogTitle>
<DialogDescription>
Create a new variable
</DialogDescription>
</DialogHeader>
<EnhancedEditForm @ref="Form" Model="Model" OnValidSubmit="OnValidSubmitAsync">
<FieldGroup>
<DataAnnotationsValidator/>
<FormValidationSummary/>
<FieldSet>
<Field>
<FieldLabel for="variableDisplayName">Display Name</FieldLabel>
<TextInputField
@bind-Value="Model.DisplayName"
id="variableDisplayName"/>
</Field>
<Field>
<FieldLabel for="variableDescription">Description</FieldLabel>
<TextareaInputField
@bind-Value="Model.Description"
id="variableDescription"/>
</Field>
<Field>
<FieldLabel for="variableKey">Key</FieldLabel>
<TextInputField
@bind-Value="Model.EnvName"
id="variableKey"/>
</Field>
<Field>
<FieldLabel for="variableDefaultValue">Default Value</FieldLabel>
<TextInputField
@bind-Value="Model.DefaultValue"
id="variableDefaultValue"/>
</Field>
</FieldSet>
</FieldGroup>
</EnhancedEditForm>
<DialogFooter>
<DialogClose>
<Slot>
<Button Type="button" Variant="ButtonVariant.Outline" @attributes="context">
Cancel
</Button>
</Slot>
</DialogClose>
<Button @onclick="() => Form.SubmitAsync()" Type="button">
Save
</Button>
</DialogFooter>
@code
{
[Parameter] public DetailedTemplateDto Template { get; set; }
[Parameter] public Func<Task> OnSubmit { get; set; }
private CreateVariableDto Model = new();
private EnhancedEditForm Form;
private async Task<bool> OnValidSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{
var response = await HttpClient.PostAsJsonAsync(
$"api/admin/servers/templates/{Template.Id}/variables",
Model,
SerializationContext.Default.Options
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Model, validationMessageStore);
return false;
}
await OnSubmit.Invoke();
await ToastService.SuccessAsync(
"Variable Creation",
$"Successfully created variable {Model.DisplayName}"
);
await CloseAsync();
return true;
}
}

View File

@@ -0,0 +1,264 @@
@using System.Text.Json
@using LucideBlazor
@using Moonlight.Shared.Http.Responses
@using MoonlightServers.Shared
@using MoonlightServers.Shared.Admin.Templates
@using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Extras.AlertDialogs
@using ShadcnBlazor.Extras.Dialogs
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Selects
@using ShadcnBlazor.Switches
@using ShadcnBlazor.Tabels
@inject HttpClient HttpClient
@inject ToastService ToastService
@inject AlertDialogService AlertDialogService
@inject DialogService DialogService
<LazyLoader @ref="LazyLoader" Load="LoadAsync">
<Card ClassName="mb-5">
<CardContent>
<FieldGroup>
<FieldSet>
<FieldGroup>
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
<Field>
<FieldLabel for="templateAllowUserDockerImageChange">Allow User Docker Image Change
</FieldLabel>
<Switch
@bind-Value="Request.AllowUserDockerImageChange"
id="templateAllowUserDockerImageChange"/>
</Field>
<Field>
<FieldLabel>Default Docker Image</FieldLabel>
<Select @bind-Value="DefaultDockerImageBinder">
<SelectTrigger>
<SelectValue Placeholder="Select a docker image" />
</SelectTrigger>
<SelectContent>
@foreach (var (id, dockerImage) in DockerImages)
{
<SelectItem Value="@id.ToString()">
@dockerImage.DisplayName
</SelectItem>
}
</SelectContent>
</Select>
</Field>
</div>
</FieldGroup>
</FieldSet>
</FieldGroup>
</CardContent>
</Card>
<div class="overflow-hidden rounded-md border bg-card shadow-sm">
<Table>
<TableHeader>
<TableRow>
<TableHead>
Display Name
</TableHead>
<TableHead>
Identifier
</TableHead>
<TableHead>
Skip Pulling
</TableHead>
<TableHead ClassName="w-10"/>
</TableRow>
</TableHeader>
<TableBody>
@foreach (var (id, dockerImage) in DockerImages)
{
<TableRow
@key="dockerImage">
<TableCell>
<TextInputField
@bind-Value="dockerImage.DisplayName"
placeholder="Default Image"/>
</TableCell>
<TableCell>
<TextInputField
@bind-Value="dockerImage.ImageName"
placeholder="debian:latest"/>
</TableCell>
<TableCell>
<Switch @bind-Value="dockerImage.SkipPulling"/>
</TableCell>
<TableCell ClassName="text-right pr-4">
<Button @onclick="() => UpdateAsync(id, dockerImage)"
Size="ButtonSize.Icon"
Variant="ButtonVariant.Outline">
<SaveIcon/>
</Button>
<Button @onclick="() => DeleteAsync(id, dockerImage)"
Size="ButtonSize.Icon"
Variant="ButtonVariant.Destructive">
<Trash2Icon/>
</Button>
</TableCell>
</TableRow>
}
<TableRow>
<TableCell colspan="999999">
<div class="flex justify-end">
<Button
@onclick="AddAsync"
Variant="ButtonVariant.Outline"
Size="ButtonSize.Sm">
<PlusIcon/>
Add
</Button>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</LazyLoader>
@code
{
[Parameter] public DetailedTemplateDto Template { get; set; }
[Parameter] public UpdateTemplateDto Request { get; set; }
private readonly List<(int, UpdateDockerImageDto)> DockerImages = new();
private LazyLoader LazyLoader;
private string? DefaultDockerImageBinder
{
get => Request.DefaultDockerImageId?.ToString();
set
{
if (int.TryParse(value, out var intValue))
Request.DefaultDockerImageId = intValue;
else
Request.DefaultDockerImageId = null;
}
}
private async Task LoadAsync(LazyLoader _)
{
DockerImages.Clear();
var totalAmount = 0;
var currentIndex = 0;
const int pageSize = 50;
do
{
var dockerImages = await HttpClient.GetFromJsonAsync<PagedData<DockerImageDto>>(
$"api/admin/servers/templates/{Template.Id}/dockerImages?startIndex={currentIndex}&length={pageSize}"
);
if (dockerImages == null)
continue;
currentIndex += dockerImages.Data.Length;
totalAmount = dockerImages.TotalLength;
DockerImages.AddRange(dockerImages.Data.Select(x => (x.Id, new UpdateDockerImageDto()
{
DisplayName = x.DisplayName,
ImageName = x.ImageName,
SkipPulling = x.SkipPulling
})));
} while (DockerImages.Count < totalAmount);
}
private async Task DeleteAsync(int id, UpdateDockerImageDto dto)
{
await AlertDialogService.ConfirmDangerAsync(
"Docker Image Deletion",
$"Do you really want to delete the docker image {dto.DisplayName}? This cannot be undone",
async () =>
{
var response = await HttpClient.DeleteAsync($"api/admin/servers/templates/{Template.Id}/dockerImages/{id}");
response.EnsureSuccessStatusCode();
await ToastService.SuccessAsync(
"Docker Image Deletion",
$"Successfully deleted docker image {dto.DisplayName}"
);
await LazyLoader.ReloadAsync();
}
);
}
private async Task UpdateAsync(int id, UpdateDockerImageDto dto)
{
var response = await HttpClient.PutAsJsonAsync(
$"api/admin/servers/templates/{Template.Id}/dockerImages/{id}",
dto,
SerializationContext.Default.Options
);
if (!response.IsSuccessStatusCode)
{
try
{
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>(
Moonlight.Shared.Http.SerializationContext.Default.Options
);
if (problemDetails == null)
{
// Fallback
response.EnsureSuccessStatusCode();
return;
}
if (problemDetails.Errors is { Count: > 0 })
{
var errorMessages = string.Join(
", ",
problemDetails.Errors.Select(x => $"{x.Key}: {x.Value.FirstOrDefault()}")
);
await ToastService.ErrorAsync(
"Docker Image Update",
$"{problemDetails.Detail ?? problemDetails.Title} {errorMessages}"
);
}
else
{
await ToastService.ErrorAsync(
"Docker Image Update",
$"{problemDetails.Detail ?? problemDetails.Title}"
);
}
return;
}
catch (JsonException)
{
// Fallback if unable to deserialize
response.EnsureSuccessStatusCode();
}
}
await ToastService.SuccessAsync(
"Docker Image Update",
$"Successfully updated docker image {dto.DisplayName}"
);
await LazyLoader.ReloadAsync();
}
private async Task AddAsync()
{
await DialogService.LaunchAsync<CreateDockerImageDialog>(parameters =>
{
parameters[nameof(CreateDockerImageDialog.Template)] = Template;
parameters[nameof(CreateDockerImageDialog.OnSubmit)] = async () => { await LazyLoader.ReloadAsync(); };
});
}
}

View File

@@ -0,0 +1,78 @@
@using LucideBlazor
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Tabels
@using ShadcnBlazor.Buttons
@inherits Editor<List<string>>
<div class="rounded-md bg-card shadow-sm border">
<Table>
<TableHeader>
<TableRow>
<TableHead>
Online Text
</TableHead>
<TableHead ClassName="w-10"/>
</TableRow>
</TableHeader>
<TableBody>
@foreach (var command in Value)
{
<TableRow
@key="command">
<TableCell>
<TextInputField Value="@command"
ValueExpression="() => command" disabled/>
</TableCell>
<TableCell ClassName="text-right pr-4">
<Button @onclick="() => DeleteAsync(command)"
Size="ButtonSize.Icon"
Variant="ButtonVariant.Destructive">
<Trash2Icon/>
</Button>
</TableCell>
</TableRow>
}
<TableRow>
<TableCell colspan="999999">
<div class="flex justify-end gap-1">
<TextInputField ClassName="h-8"
@bind-Value="Input"
placeholder="Enter text..." />
<Button
@onclick="AddAsync"
Variant="ButtonVariant.Outline"
Size="ButtonSize.Sm">
<PlusIcon/>
Add Online Text
</Button>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
@code
{
private string Input;
private async Task DeleteAsync(string command)
{
Value.Remove(command);
await ValueChanged.InvokeAsync(Value);
}
private async Task AddAsync()
{
if(string.IsNullOrEmpty(Input))
return;
if(Value.Contains(Input))
return;
Value.Add(Input);
Input = "";
await ValueChanged.InvokeAsync(Value);
}
}

View File

@@ -0,0 +1,202 @@
@using LucideBlazor
@using Moonlight.Shared.Http.Requests
@using Moonlight.Shared.Http.Responses
@using MoonlightServers.Shared
@using MoonlightServers.Shared.Admin.Templates
@using ShadcnBlazor.DataGrids
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Dropdowns
@using ShadcnBlazor.Extras.AlertDialogs
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Tabels
@inject HttpClient HttpClient
@inject AlertDialogService AlertDialogService
@inject ToastService ToastService
@inject NavigationManager NavigationManager
@inject IAuthorizationService AuthorizationService
<InputFile OnChange="OnFileSelectedAsync" id="import-template" class="hidden" multiple accept=".yml,.yaml,.json"/>
<div class="flex flex-row justify-between mt-5">
<div class="flex flex-col">
<h1 class="text-xl font-semibold">Templates</h1>
<div class="text-muted-foreground">
Manage templates
</div>
</div>
<div class="flex flex-row gap-x-1.5">
<Button Variant="ButtonVariant.Outline">
<Slot>
<label for="import-template" @attributes="context">
<HardDriveUploadIcon/>
Import
</label>
</Slot>
</Button>
<Button>
<Slot Context="buttonCtx">
<a @attributes="buttonCtx" href="/admin/servers/templates/create"
data-disabled="@(!CreateAccess.Succeeded)">
<PlusIcon/>
Create
</a>
</Slot>
</Button>
</div>
</div>
<div class="mt-3">
<DataGrid @ref="Grid" TGridItem="TemplateDto" Loader="LoadAsync" PageSize="10" ClassName="bg-card">
<PropertyColumn Field="u => u.Id"/>
<TemplateColumn IsFilterable="true" Identifier="@nameof(TemplateDto.Name)" Title="@nameof(TemplateDto.Name)">
<CellTemplate>
<TableCell>
<a class="text-primary" href="#"
@onclick="() => Edit(context)" @onclick:preventDefault>
@context.Name
</a>
</TableCell>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="@nameof(TemplateDto.Description)" HeadClassName="hidden lg:table-cell">
<CellTemplate>
<TableCell ClassName="hidden lg:table-cell">
<div class="truncate max-w-md">@context.Description</div>
</TableCell>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Field="u => u.Author"
HeadClassName="hidden xl:table-cell"
CellClassName="hidden xl:table-cell"/>
<PropertyColumn Field="u => u.Version"
HeadClassName="hidden xl:table-cell"
CellClassName="hidden xl:table-cell"/>
<TemplateColumn>
<CellTemplate>
<TableCell>
<div class="flex flex-row items-center justify-end me-3">
<DropdownMenu>
<DropdownMenuTrigger>
<Slot Context="dropdownSlot">
<Button Size="ButtonSize.IconSm" Variant="ButtonVariant.Ghost"
@attributes="dropdownSlot">
<EllipsisIcon/>
</Button>
</Slot>
</DropdownMenuTrigger>
<DropdownMenuContent SideOffset="2">
<DropdownMenuItem OnClick="() => Export(context)">
Export
<DropdownMenuShortcut>
<HardDriveDownloadIcon/>
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem OnClick="() => Edit(context)"
Disabled="@(!EditAccess.Succeeded)">
Edit
<DropdownMenuShortcut>
<PenIcon/>
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem OnClick="() => DeleteAsync(context)"
Variant="DropdownMenuItemVariant.Destructive"
Disabled="@(!DeleteAccess.Succeeded)">
Delete
<DropdownMenuShortcut>
<TrashIcon/>
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TableCell>
</CellTemplate>
</TemplateColumn>
</DataGrid>
</div>
@code
{
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
private DataGrid<TemplateDto> Grid;
private AuthorizationResult EditAccess;
private AuthorizationResult DeleteAccess;
private AuthorizationResult CreateAccess;
protected override async Task OnInitializedAsync()
{
var authState = await AuthState;
EditAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Templates.Edit);
DeleteAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Templates.Delete);
CreateAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Templates.Create);
}
private async Task<DataGridResponse<TemplateDto>> LoadAsync(DataGridRequest<TemplateDto> request)
{
var query = $"?startIndex={request.StartIndex}&length={request.Length}";
var filterOptions = request.Filters.Count > 0 ? new FilterOptions(request.Filters) : null;
var response = await HttpClient.GetFromJsonAsync<PagedData<TemplateDto>>(
$"api/admin/servers/templates{query}&filterOptions={filterOptions}",
SerializationContext.Default.Options
);
return new DataGridResponse<TemplateDto>(response!.Data, response.TotalLength);
}
private void Edit(TemplateDto context) => NavigationManager.NavigateTo($"/admin/servers/templates/{context.Id}");
private void Export(TemplateDto dto) => NavigationManager.NavigateTo($"api/admin/servers/templates/{dto.Id}/export", true);
private async Task DeleteAsync(TemplateDto context)
{
await AlertDialogService.ConfirmDangerAsync(
"Template Deletion",
$"Do you really want to delete the template {context.Name}? This cannot be undone.",
async () =>
{
var response = await HttpClient.DeleteAsync($"api/admin/servers/templates/{context.Id}");
response.EnsureSuccessStatusCode();
await Grid.RefreshAsync();
await ToastService.SuccessAsync(
"Template Deletion",
$"Successfully deleted template {context.Name}"
);
}
);
}
private async Task OnFileSelectedAsync(InputFileChangeEventArgs eventArgs)
{
var files = eventArgs.GetMultipleFiles();
foreach (var browserFile in files)
{
await using var contentStream = browserFile.OpenReadStream(browserFile.Size);
var response = await HttpClient.PostAsync(
"api/admin/servers/templates/import",
new StreamContent(contentStream)
);
response.EnsureSuccessStatusCode();
var importedTemplate = await response
.Content
.ReadFromJsonAsync<TemplateDto>(SerializationContext.Default.Options);
if (importedTemplate == null)
continue;
await Grid.RefreshAsync();
await ToastService.SuccessAsync("Template Import", $"Successfully imported template {importedTemplate.Name}");
}
}
}

View File

@@ -0,0 +1,16 @@
@using ShadcnBlazor.Extras.Editors
@inherits Editor<string>
<Editor @ref="Editor" Language="EditorLanguage.None" InitialValue="@Value" />
@code
{
private Editor Editor;
public async Task SubmitAsync()
{
Value = await Editor.GetValueAsync();
await ValueChanged.InvokeAsync(Value);
}
}

View File

@@ -0,0 +1,78 @@
@using LucideBlazor
@using MoonlightServers.Shared.Admin.Templates
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Tabels
@inherits Editor<List<MoonlightServers.Shared.Admin.Templates.UpdateStartupCommandDto>>
<div class="rounded-md bg-card shadow-sm border">
<Table>
<TableHeader>
<TableRow>
<TableHead>
Display Name
</TableHead>
<TableHead>
Command
</TableHead>
<TableHead ClassName="w-10"/>
</TableRow>
</TableHeader>
<TableBody>
@foreach (var command in Value)
{
<TableRow
@key="command">
<TableCell>
<TextInputField @bind-Value="command.DisplayName"
placeholder="Default Command"/>
</TableCell>
<TableCell>
<TextInputField @bind-Value="command.Command"
placeholder="java -Xmx{{SERVER_MEMORY}} server.jar"/>
</TableCell>
<TableCell ClassName="text-right pr-4">
<Button @onclick="() => DeleteAsync(command)"
Size="ButtonSize.Icon"
Variant="ButtonVariant.Destructive">
<Trash2Icon/>
</Button>
</TableCell>
</TableRow>
}
<TableRow>
<TableCell colspan="999999">
<div class="flex justify-end">
<Button
@onclick="AddAsync"
Variant="ButtonVariant.Outline"
Size="ButtonSize.Sm">
<PlusIcon/>
Add Startup Command
</Button>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
@code
{
private async Task DeleteAsync(UpdateStartupCommandDto commandDto)
{
Value.Remove(commandDto);
await ValueChanged.InvokeAsync(Value);
}
private async Task AddAsync()
{
Value.Add(new UpdateStartupCommandDto()
{
Command = "Change me",
DisplayName = "Change me"
});
await ValueChanged.InvokeAsync(Value);
}
}

View File

@@ -0,0 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using MoonlightServers.Shared.Admin.Templates;
using Riok.Mapperly.Abstractions;
namespace MoonlightServers.Frontend.Admin.Templates;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
public static partial class TemplateMapper
{
public static partial UpdateTemplateDto ToRequest(DetailedTemplateDto dto);
}

View File

@@ -0,0 +1,297 @@
@page "/admin/servers/templates/{Id:int}"
@using System.Net
@using System.Text.Json
@using LucideBlazor
@using Moonlight.Frontend.Helpers
@using MoonlightServers.Shared
@using MoonlightServers.Shared.Admin.Templates
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Emptys
@using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Tab
@inject HttpClient HttpClient
@inject NavigationManager Navigation
@inject ToastService ToastService
<LazyLoader Load="LoadAsync">
@if (Template == null)
{
<Empty>
<EmptyHeader>
<EmptyMedia Variant="EmptyMediaVariant.Icon">
<SearchIcon/>
</EmptyMedia>
<EmptyTitle>Template not found</EmptyTitle>
<EmptyDescription>
A template with this id cannot be found
</EmptyDescription>
</EmptyHeader>
</Empty>
}
else
{
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="formContext">
<div class="flex flex-row justify-between">
<div class="flex flex-col">
<h1 class="text-xl font-semibold">Update Template</h1>
<div class="text-muted-foreground">
Update template @Template.Name
</div>
</div>
<div class="flex flex-row gap-x-1.5">
<Button Variant="ButtonVariant.Secondary">
<Slot>
<a href="/admin/servers?tab=templates" @attributes="context">
<ChevronLeftIcon/>
Back
</a>
</Slot>
</Button>
<SubmitButton>
<CheckIcon/>
Continue
</SubmitButton>
</div>
</div>
<div class="mt-8 space-y-5">
<DataAnnotationsValidator/>
<FormValidationSummary/>
<Tabs DefaultValue="meta">
<TabsList ClassName="inline-flex w-full lg:w-fit justify-start overflow-x-auto overflow-y-hidden">
<TabsTrigger Value="meta">
<IdCardIcon/>
Meta
</TabsTrigger>
<TabsTrigger Value="lifecycle">
<PlayIcon/>
Lifecycle
</TabsTrigger>
<TabsTrigger Value="installation">
<SquareTerminalIcon/>
Installation
</TabsTrigger>
<TabsTrigger Value="variables">
<VariableIcon/>
Variables
</TabsTrigger>
<TabsTrigger Value="dockerImages">
<ContainerIcon/>
Docker Images
</TabsTrigger>
<TabsTrigger Value="files">
<FileCogIcon/>
Files
</TabsTrigger>
</TabsList>
<TabsContent Value="meta">
<Card>
<CardContent>
<FieldGroup>
<FieldSet>
<FieldGroup>
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
<Field>
<FieldLabel for="templateName">Name</FieldLabel>
<TextInputField
@bind-Value="Request.Name"
id="templateName"/>
</Field>
<Field>
<FieldLabel for="templateAuthor">Author</FieldLabel>
<TextInputField
@bind-Value="Request.Author"
id="templateAuthor"/>
</Field>
<Field>
<FieldLabel for="templateVersion">Version</FieldLabel>
<TextInputField
@bind-Value="Request.Version"
id="templateVersion"/>
</Field>
<Field ClassName="col-span-1 lg:col-span-2 xl:col-span-3">
<FieldLabel for="templateDescription">Description</FieldLabel>
<TextareaInputField
@bind-Value="Request.Description"
id="templateDescription"/>
</Field>
<Field>
<FieldLabel for="templateDonateUrl">Donate URL</FieldLabel>
<TextInputField
@bind-Value="Request.DonateUrl"
id="templateDonateUrl"/>
</Field>
<Field>
<FieldLabel for="templateUpdateUrl">Update URL</FieldLabel>
<TextInputField
@bind-Value="Request.UpdateUrl"
id="templateUpdateUrl"/>
</Field>
</div>
</FieldGroup>
</FieldSet>
</FieldGroup>
</CardContent>
</Card>
</TabsContent>
<TabsContent Value="lifecycle">
<Card>
<CardContent>
<FieldGroup>
<FieldSet>
<FieldGroup>
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
<Field ClassName="col-span-1 lg:col-span-2">
<FieldLabel>Startup Commands</FieldLabel>
<StartupCommandEditor
@bind-Value="Request.LifecycleConfig.StartupCommands"/>
</Field>
<Field>
<FieldLabel for="templateStopCommand">Stop Command</FieldLabel>
<TextInputField
@bind-Value="Request.LifecycleConfig.StopCommand"
id="templateStopCommand"/>
</Field>
<Field ClassName="col-span-1 lg:col-span-2">
<FieldLabel>Online Texts</FieldLabel>
<OnlineTextEditor
@bind-Value="Request.LifecycleConfig.OnlineLogPatterns"/>
</Field>
</div>
</FieldGroup>
</FieldSet>
</FieldGroup>
</CardContent>
</Card>
</TabsContent>
<TabsContent Value="installation">
<Card>
<CardContent>
<FieldGroup>
<FieldSet>
<FieldGroup>
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
<Field>
<FieldLabel for="templateInstallDockerImage">Docker Image
</FieldLabel>
<TextInputField
@bind-Value="Request.InstallationConfig.DockerImage"
id="templateInstallDockerImage"/>
</Field>
<Field>
<FieldLabel for="templateInstallShell">Shell</FieldLabel>
<TextInputField
@bind-Value="Request.InstallationConfig.Shell"
id="templateInstallShell"/>
</Field>
<Field ClassName="col-span-1 lg:col-span-2 xl:col-span-3">
<FieldLabel>Script</FieldLabel>
<ScriptEditor
@ref="ScriptEditor"
@bind-Value="Request.InstallationConfig.Script"/>
</Field>
</div>
</FieldGroup>
</FieldSet>
</FieldGroup>
</CardContent>
</Card>
</TabsContent>
<TabsContent Value="variables">
<VariableListEditor Template="Template"/>
</TabsContent>
<TabsContent Value="dockerImages">
<DockerImageListEditor Request="Request" Template="Template"/>
</TabsContent>
<TabsContent Value="files">
<ConfigFilesEditor FilesConfig="Request.FilesConfig"/>
</TabsContent>
</Tabs>
</div>
</EnhancedEditForm>
}
</LazyLoader>
@code
{
[Parameter] public int Id { get; set; }
private UpdateTemplateDto Request;
private DetailedTemplateDto? Template;
private ScriptEditor ScriptEditor;
private async Task LoadAsync(LazyLoader _)
{
// Meta
var metaResponse = await HttpClient.GetAsync($"api/admin/servers/templates/{Id}");
if (!metaResponse.IsSuccessStatusCode)
{
if (metaResponse.StatusCode == HttpStatusCode.NotFound)
return;
metaResponse.EnsureSuccessStatusCode();
return;
}
Template = await metaResponse.Content.ReadFromJsonAsync<DetailedTemplateDto>(
SerializationContext.Default.Options
);
if (Template == null)
return;
Request = TemplateMapper.ToRequest(Template);
}
private async Task<bool> OnSubmitAsync(EditContext context, ValidationMessageStore validationMessageStore)
{
await ScriptEditor.SubmitAsync();
var response = await HttpClient.PutAsJsonAsync(
$"/api/admin/servers/templates/{Id}",
Request,
SerializationContext.Default.Options
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync(
"Template Update",
$"Successfully updated template {Request.Name}"
);
Navigation.NavigateTo("/admin/servers?tab=templates");
return true;
}
}

View File

@@ -0,0 +1,147 @@
@using LucideBlazor
@using Moonlight.Frontend.Helpers
@using MoonlightServers.Shared
@using MoonlightServers.Shared.Admin.Templates
@using ShadcnBlazor.Accordions
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Extras.AlertDialogs
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs
@inject HttpClient HttpClient
@inject ToastService ToastService
@inject AlertDialogService AlertDialogService
<Accordion ClassName="-space-y-px" Type="AccordionType.Single">
<AccordionItem
ClassName="overflow-hidden border bg-card px-4 first:rounded-t-lg last:rounded-b-lg last:border-b"
Value="element">
<AccordionTrigger ClassName="hover:no-underline">
<div class="flex items-center gap-3">
<VariableIcon/>
<span class="text-left">@Request.DisplayName</span>
</div>
</AccordionTrigger>
<AccordionContent ClassName="ps-7">
<EnhancedEditForm Model="Request" OnValidSubmit="OnValidSubmitAsync">
<FieldGroup>
<DataAnnotationsValidator/>
<FormValidationSummary/>
<FieldSet>
<Field>
@{
var id = $"variableDisplayName{Variable.Id}";
}
<FieldLabel for="@id">Display Name</FieldLabel>
<TextInputField
@bind-Value="Request.DisplayName"
id="@id"/>
</Field>
<Field>
@{
var id = $"variableDescription{Variable.Id}";
}
<FieldLabel for="@id">Description</FieldLabel>
<TextareaInputField
@bind-Value="Request.Description"
id="@id"/>
</Field>
<Field>
@{
var id = $"variableKey{Variable.Id}";
}
<FieldLabel for="@id">Key</FieldLabel>
<TextInputField
@bind-Value="Request.EnvName"
id="@id"/>
</Field>
<Field>
@{
var id = $"variableDefaultValue{Variable.Id}";
}
<FieldLabel for="@id">Default Value</FieldLabel>
<TextInputField
@bind-Value="Request.DefaultValue"
id="@id"/>
</Field>
</FieldSet>
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
<Button Variant="ButtonVariant.Destructive" @onclick="DeleteAsync">Delete</Button>
<SubmitButton>Save changes</SubmitButton>
</Field>
</FieldGroup>
</EnhancedEditForm>
</AccordionContent>
</AccordionItem>
</Accordion>
@code
{
[Parameter] public VariableDto Variable { get; set; }
[Parameter] public DetailedTemplateDto Template { get; set; }
[Parameter] public Func<Task> OnChanged { get; set; }
private UpdateVariableDto Request;
protected override void OnInitialized()
{
Request = new UpdateVariableDto()
{
DisplayName = Variable.DisplayName,
DefaultValue = Variable.DefaultValue,
EnvName = Variable.EnvName,
Description = Variable.Description
};
}
private async Task<bool> OnValidSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{
var response = await HttpClient.PutAsJsonAsync(
$"api/admin/servers/templates/{Template.Id}/variables/{Variable.Id}",
Request,
SerializationContext.Default.Options
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync(
"Variable Update",
$"Successfully updated variable {Request.DisplayName}"
);
await OnChanged.Invoke();
return true;
}
private async Task DeleteAsync()
{
await AlertDialogService.ConfirmDangerAsync(
"Variable Deletion",
$"Do you really want to delete the variable {Variable.DisplayName}? This cannot be undone",
async () =>
{
var response = await HttpClient.DeleteAsync($"api/admin/servers/templates/{Template.Id}/variables/{Variable.Id}");
response.EnsureSuccessStatusCode();
await ToastService.SuccessAsync(
"Variable Deletion",
$"Variable {Variable.DisplayName} successfully deleted"
);
await OnChanged.Invoke();
}
);
}
}

View File

@@ -0,0 +1,77 @@
@using LucideBlazor
@using Moonlight.Shared.Http.Responses
@using MoonlightServers.Shared.Admin.Templates
@using ShadcnBlazor.Accordions
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Dialogs
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs
@inject HttpClient HttpClient
@inject DialogService DialogService
<div class="flex flex-row justify-end mb-5">
<Button @onclick="LaunchCreateAsync">
<PlusIcon />
Create
</Button>
</div>
<LazyLoader @ref="LazyLoader" Load="LoadAsync">
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
@foreach (var variable in Variables)
{
<VariableEditor OnChanged="() => LazyLoader.ReloadAsync()"
Template="Template"
Variable="variable"
@key="variable" />
}
</div>
</LazyLoader>
@code
{
[Parameter] public DetailedTemplateDto Template { get; set; }
private readonly List<VariableDto> Variables = new();
private LazyLoader LazyLoader;
private async Task LoadAsync(LazyLoader _)
{
Variables.Clear();
var totalAmount = 0;
var currentIndex = 0;
const int pageSize = 50;
do
{
var variables = await HttpClient.GetFromJsonAsync<PagedData<VariableDto>>(
$"api/admin/servers/templates/{Template.Id}/variables?startIndex={currentIndex}&length={pageSize}"
);
if (variables == null)
continue;
currentIndex += variables.Data.Length;
totalAmount = variables.TotalLength;
Variables.AddRange(variables.Data);
} while (Variables.Count < totalAmount);
}
private async Task LaunchCreateAsync()
{
await DialogService.LaunchAsync<CreateVariableDialog>(parameters =>
{
parameters[nameof(CreateVariableDialog.Template)] = Template;
parameters[nameof(CreateVariableDialog.OnSubmit)] = async () =>
{
await LazyLoader.ReloadAsync();
};
}
);
}
}