Merge pull request #30 from Moonlight-Panel/Subscriptions
Subscriptions
This commit is contained in:
commit
74d00174d1
14
Moonlight/App/Models/Forms/ServerOrderDataModel.cs
Normal file
14
Moonlight/App/Models/Forms/ServerOrderDataModel.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Moonlight.App.Database.Entities;
|
||||
|
||||
namespace Moonlight.App.Models.Forms;
|
||||
|
||||
public class ServerOrderDataModel
|
||||
{
|
||||
[Required(ErrorMessage = "You need to enter a name")]
|
||||
[MaxLength(32, ErrorMessage = "The name cannot be longer that 32 characters")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "You need to specify a server image")]
|
||||
public Image Image { get; set; }
|
||||
}
|
|
@ -6,6 +6,12 @@ public class SubscriptionLimit
|
|||
public int Amount { get; set; }
|
||||
public List<LimitOption> Options { get; set; } = new();
|
||||
|
||||
public string? ReadValue(string key)
|
||||
{
|
||||
var d = Options.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.InvariantCultureIgnoreCase));
|
||||
return d?.Value;
|
||||
}
|
||||
|
||||
public class LimitOption
|
||||
{
|
||||
public string Key { get; set; } = "";
|
||||
|
|
|
@ -5,10 +5,12 @@ namespace Moonlight.App.Services.Interop;
|
|||
public class AlertService
|
||||
{
|
||||
private readonly SweetAlertService SweetAlertService;
|
||||
private readonly SmartTranslateService SmartTranslateService;
|
||||
|
||||
public AlertService(SweetAlertService service)
|
||||
public AlertService(SweetAlertService service, SmartTranslateService smartTranslateService)
|
||||
{
|
||||
SweetAlertService = service;
|
||||
SmartTranslateService = smartTranslateService;
|
||||
}
|
||||
|
||||
public async Task Info(string title, string desciption)
|
||||
|
@ -21,6 +23,11 @@ public class AlertService
|
|||
});
|
||||
}
|
||||
|
||||
public async Task Info(string desciption)
|
||||
{
|
||||
await Info("", desciption);
|
||||
}
|
||||
|
||||
public async Task Success(string title, string desciption)
|
||||
{
|
||||
await SweetAlertService.FireAsync(new SweetAlertOptions()
|
||||
|
@ -31,6 +38,11 @@ public class AlertService
|
|||
});
|
||||
}
|
||||
|
||||
public async Task Success(string desciption)
|
||||
{
|
||||
await Success("", desciption);
|
||||
}
|
||||
|
||||
public async Task Warning(string title, string desciption)
|
||||
{
|
||||
await SweetAlertService.FireAsync(new SweetAlertOptions()
|
||||
|
@ -41,6 +53,11 @@ public class AlertService
|
|||
});
|
||||
}
|
||||
|
||||
public async Task Warning(string desciption)
|
||||
{
|
||||
await Warning("", desciption);
|
||||
}
|
||||
|
||||
public async Task Error(string title, string desciption)
|
||||
{
|
||||
await SweetAlertService.FireAsync(new SweetAlertOptions()
|
||||
|
@ -51,6 +68,11 @@ public class AlertService
|
|||
});
|
||||
}
|
||||
|
||||
public async Task Error(string desciption)
|
||||
{
|
||||
await Error("", desciption);
|
||||
}
|
||||
|
||||
public async Task<bool> YesNo(string title, string desciption, string yesText, string noText)
|
||||
{
|
||||
var result = await SweetAlertService.FireAsync(new SweetAlertOptions()
|
||||
|
@ -79,4 +101,27 @@ public class AlertService
|
|||
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
public async Task<bool> ConfirmMath()
|
||||
{
|
||||
var r = new Random();
|
||||
var i1 = r.Next(5, 15);
|
||||
var i2 = r.Next(5, 15);
|
||||
|
||||
var input = await Text(
|
||||
SmartTranslateService.Translate("Confirm"),
|
||||
$"{i1} + {i2} =",
|
||||
""
|
||||
);
|
||||
|
||||
if (int.TryParse(input, out int i))
|
||||
{
|
||||
if (i == i1 + i2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -15,4 +15,9 @@ public class ClipboardService
|
|||
{
|
||||
await JsRuntime.InvokeVoidAsync("copyTextToClipboard", data);
|
||||
}
|
||||
public async Task Copy(string data)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("copyTextToClipboard", data);
|
||||
}
|
||||
|
||||
}
|
64
Moonlight/App/Services/SmartDeployService.cs
Normal file
64
Moonlight/App/Services/SmartDeployService.cs
Normal file
|
@ -0,0 +1,64 @@
|
|||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Repositories;
|
||||
|
||||
namespace Moonlight.App.Services;
|
||||
|
||||
public class SmartDeployService
|
||||
{
|
||||
private readonly NodeRepository NodeRepository;
|
||||
private readonly NodeService NodeService;
|
||||
|
||||
public SmartDeployService(NodeRepository nodeRepository, NodeService nodeService)
|
||||
{
|
||||
NodeRepository = nodeRepository;
|
||||
NodeService = nodeService;
|
||||
}
|
||||
|
||||
public async Task<Node?> GetNode()
|
||||
{
|
||||
var data = new Dictionary<Node, double>();
|
||||
|
||||
foreach (var node in NodeRepository.Get().ToArray())
|
||||
{
|
||||
var u = await GetUsageScore(node);
|
||||
|
||||
if(u != 0)
|
||||
data.Add(node, u);
|
||||
}
|
||||
|
||||
if (!data.Any())
|
||||
return null;
|
||||
|
||||
return data.MaxBy(x => x.Value).Key;
|
||||
}
|
||||
|
||||
private async Task<double> GetUsageScore(Node node)
|
||||
{
|
||||
var score = 0;
|
||||
|
||||
try
|
||||
{
|
||||
var cpuStats = await NodeService.GetCpuStats(node);
|
||||
var memoryStats = await NodeService.GetMemoryStats(node);
|
||||
var diskStats = await NodeService.GetDiskStats(node);
|
||||
|
||||
var cpuWeight = 0.5; // Weight of CPU usage in the final score
|
||||
var memoryWeight = 0.3; // Weight of memory usage in the final score
|
||||
var diskSpaceWeight = 0.2; // Weight of free disk space in the final score
|
||||
|
||||
var cpuScore = (1 - cpuStats.Usage) * cpuWeight; // CPU score is based on the inverse of CPU usage
|
||||
var memoryScore = (1 - (memoryStats.Used / 1024)) * memoryWeight; // Memory score is based on the percentage of free memory
|
||||
var diskSpaceScore = (double) diskStats.FreeBytes / 1000000000 * diskSpaceWeight; // Disk space score is based on the amount of free disk space in GB
|
||||
|
||||
var finalScore = cpuScore + memoryScore + diskSpaceScore;
|
||||
|
||||
return finalScore;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
}
|
|
@ -78,49 +78,45 @@ public class SubscriptionService
|
|||
await OneTimeJwtService.Revoke(code);
|
||||
}
|
||||
|
||||
public async Task Cancel()
|
||||
{
|
||||
if (await GetCurrent() != null)
|
||||
{
|
||||
var user = await GetCurrentUser();
|
||||
|
||||
user.CurrentSubscription = null;
|
||||
|
||||
UserRepository.Update(user);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SubscriptionLimit> GetLimit(string identifier)
|
||||
{
|
||||
var configSection = ConfigService.GetSection("Moonlight").GetSection("Subscriptions");
|
||||
|
||||
var defaultLimits = configSection.GetValue<SubscriptionLimit[]>("defaultLimits");
|
||||
|
||||
var subscription = await GetCurrent();
|
||||
|
||||
if (subscription == null)
|
||||
{
|
||||
var foundDefault = defaultLimits.FirstOrDefault(x => x.Identifier == identifier);
|
||||
|
||||
if (foundDefault != null)
|
||||
return foundDefault;
|
||||
|
||||
return new()
|
||||
{
|
||||
Identifier = identifier,
|
||||
Amount = 0
|
||||
};
|
||||
}
|
||||
else
|
||||
|
||||
var subscriptionLimits =
|
||||
JsonConvert.DeserializeObject<SubscriptionLimit[]>(subscription.LimitsJson)
|
||||
?? Array.Empty<SubscriptionLimit>();
|
||||
|
||||
var foundLimit = subscriptionLimits.FirstOrDefault(x => x.Identifier == identifier);
|
||||
|
||||
if (foundLimit != null)
|
||||
return foundLimit;
|
||||
|
||||
return new()
|
||||
{
|
||||
var subscriptionLimits =
|
||||
JsonConvert.DeserializeObject<SubscriptionLimit[]>(subscription.LimitsJson)
|
||||
?? Array.Empty<SubscriptionLimit>();
|
||||
|
||||
var foundLimit = subscriptionLimits.FirstOrDefault(x => x.Identifier == identifier);
|
||||
|
||||
if (foundLimit != null)
|
||||
return foundLimit;
|
||||
|
||||
var foundDefault = defaultLimits.FirstOrDefault(x => x.Identifier == identifier);
|
||||
|
||||
if (foundDefault != null)
|
||||
return foundDefault;
|
||||
|
||||
return new()
|
||||
{
|
||||
Identifier = identifier,
|
||||
Amount = 0
|
||||
};
|
||||
}
|
||||
Identifier = identifier,
|
||||
Amount = 0
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<User?> GetCurrentUser()
|
||||
|
|
|
@ -91,6 +91,7 @@ namespace Moonlight
|
|||
builder.Services.AddScoped<NotificationAdminService>();
|
||||
builder.Services.AddScoped<NotificationClientService>();
|
||||
builder.Services.AddScoped<ModalService>();
|
||||
builder.Services.AddScoped<SmartDeployService>();
|
||||
|
||||
builder.Services.AddScoped<GoogleOAuth2Service>();
|
||||
builder.Services.AddScoped<DiscordOAuth2Service>();
|
||||
|
|
|
@ -161,7 +161,8 @@
|
|||
await JsRuntime.InvokeVoidAsync("document.body.removeAttribute", "data-kt-app-page-loading");
|
||||
await JsRuntime.InvokeVoidAsync("KTMenu.createInstances");
|
||||
await JsRuntime.InvokeVoidAsync("KTDrawer.createInstances");
|
||||
await JsRuntime.InvokeVoidAsync("createSnow");
|
||||
|
||||
//await JsRuntime.InvokeVoidAsync("createSnow");
|
||||
|
||||
await SessionService.Register();
|
||||
|
||||
|
|
168
Moonlight/Shared/Views/Admin/Subscriptions/Edit.razor
Normal file
168
Moonlight/Shared/Views/Admin/Subscriptions/Edit.razor
Normal file
|
@ -0,0 +1,168 @@
|
|||
@page "/admin/subscriptions/edit/{Id:int}"
|
||||
@using Moonlight.App.Models.Forms
|
||||
@using Moonlight.App.Models.Misc
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Database.Entities
|
||||
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject SubscriptionRepository SubscriptionRepository
|
||||
@inject SubscriptionAdminService SubscriptionAdminService
|
||||
|
||||
<OnlyAdmin>
|
||||
<div class="card card-body p-10">
|
||||
<LazyLoader Load="Load">
|
||||
@if (Subscription == null)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
No subscription with this id has been found
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<SmartForm Model="Model" OnValidSubmit="OnSubmit">
|
||||
<label class="form-label">
|
||||
<TL>Name</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<InputText @bind-Value="Model.Name" class="form-control"></InputText>
|
||||
</div>
|
||||
<label class="form-label">
|
||||
<TL>Description</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<InputText @bind-Value="Model.Description" class="form-control"></InputText>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@foreach (var limitPart in Limits.Chunk(3))
|
||||
{
|
||||
<div class="row row-cols-3 mb-5">
|
||||
@foreach (var limit in limitPart)
|
||||
{
|
||||
<div class="col">
|
||||
<div class="card card-body border">
|
||||
<label class="form-label">
|
||||
<TL>Identifier</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<input @bind="limit.Identifier" type="text" class="form-control">
|
||||
</div>
|
||||
<label class="form-label">
|
||||
<TL>Amount</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<input @bind="limit.Amount" type="number" class="form-control">
|
||||
</div>
|
||||
<div class="d-flex flex-column mb-15 fv-row">
|
||||
<div class="fs-5 fw-bold form-label mb-3">
|
||||
<TL>Options</TL>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<div class="dataTables_wrapper dt-bootstrap4 no-footer">
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle table-row-dashed fw-semibold fs-6 gy-5 dataTable no-footer">
|
||||
<thead>
|
||||
<tr class="text-start text-muted fw-bold fs-7 text-uppercase gs-0">
|
||||
<th class="pt-0 sorting_disabled">
|
||||
<TL>Key</TL>
|
||||
</th>
|
||||
<th class="pt-0 sorting_disabled">
|
||||
<TL>Value</TL>
|
||||
</th>
|
||||
<th class="pt-0 text-end sorting_disabled">
|
||||
<TL>Remove</TL>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var option in limit.Options)
|
||||
{
|
||||
<tr class="odd">
|
||||
<td>
|
||||
<input @bind="option.Key" type="text" class="form-control form-control-solid">
|
||||
</td>
|
||||
<td>
|
||||
<input @bind="option.Value" type="text" class="form-control form-control-solid">
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button @onclick="() => limit.Options.Remove(option)" type="button" class="btn btn-icon btn-flex btn-active-light-primary w-30px h-30px me-3" data-kt-action="field_remove">
|
||||
<i class="bx bx-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-5 d-flex align-items-center justify-content-center justify-content-md-start"></div>
|
||||
<div class="col-sm-12 col-md-7 d-flex align-items-center justify-content-center justify-content-md-end"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group mt-5">
|
||||
<button @onclick:preventDefault @onclick="() => limit.Options.Add(new())" type="button" class="btn btn-light-primary me-auto">Add option</button>
|
||||
<button @onclick:preventDefault @onclick="() => Limits.Remove(limit)" class="btn btn-danger float-end">
|
||||
<i class="bx bx-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="float-end">
|
||||
<button @onclick:preventDefault @onclick="() => Limits.Add(new())" class="btn btn-primary">
|
||||
<TL>Add new limit</TL>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<TL>Save subscription</TL>
|
||||
</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
}
|
||||
</LazyLoader>
|
||||
</div>
|
||||
</OnlyAdmin>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
private Subscription? Subscription;
|
||||
|
||||
private SubscriptionDataModel Model = new();
|
||||
private List<SubscriptionLimit> Limits = new();
|
||||
|
||||
private async Task OnSubmit()
|
||||
{
|
||||
Subscription!.Name = Model.Name;
|
||||
Subscription.Description = Model.Description;
|
||||
|
||||
SubscriptionRepository.Update(Subscription);
|
||||
|
||||
await SubscriptionAdminService.SaveLimits(Subscription, Limits.ToArray());
|
||||
|
||||
NavigationManager.NavigateTo("/admin/subscriptions");
|
||||
}
|
||||
|
||||
private async Task Load(LazyLoader arg)
|
||||
{
|
||||
Subscription = SubscriptionRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Id == Id);
|
||||
|
||||
if (Subscription != null)
|
||||
{
|
||||
Model.Name = Subscription.Name;
|
||||
Model.Description = Subscription.Description;
|
||||
|
||||
Limits = (await SubscriptionAdminService.GetLimits(Subscription)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,10 +3,15 @@
|
|||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Repositories
|
||||
@using BlazorTable
|
||||
@using Moonlight.App.Services.Interop
|
||||
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject SubscriptionRepository SubscriptionRepository
|
||||
|
||||
@inject SubscriptionAdminService SubscriptionAdminService
|
||||
@inject AlertService AlertService
|
||||
@inject ClipboardService ClipboardService
|
||||
|
||||
<OnlyAdmin>
|
||||
<div class="card">
|
||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||
|
@ -36,9 +41,16 @@
|
|||
</a>
|
||||
</Template>
|
||||
</Column>
|
||||
<Column TableItem="Subscription" Title="@(SmartTranslateService.Translate("Manage"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||
<Column TableItem="Subscription" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||
<Template>
|
||||
<DeleteButton Confirm="true" OnClick="() => Delete(context)" />
|
||||
<div class="float-end">
|
||||
<WButton Text="@(SmartTranslateService.Translate("Create code"))"
|
||||
WorkingText="@(SmartTranslateService.Translate("Working"))"
|
||||
CssClasses="btn-primary"
|
||||
OnClick="() => GenerateCode(context)">
|
||||
</WButton>
|
||||
<DeleteButton Confirm="true" OnClick="() => Delete(context)"/>
|
||||
</div>
|
||||
</Template>
|
||||
</Column>
|
||||
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
||||
|
@ -69,4 +81,21 @@
|
|||
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
private async Task GenerateCode(Subscription subscription)
|
||||
{
|
||||
var durationText = await AlertService.Text(
|
||||
SmartTranslateService.Translate("Duration"),
|
||||
SmartTranslateService.Translate("Enter duration of subscription"),
|
||||
"30"
|
||||
);
|
||||
|
||||
if (int.TryParse(durationText, out int duration))
|
||||
{
|
||||
var code = await SubscriptionAdminService.GenerateCode(subscription, duration);
|
||||
|
||||
await ClipboardService.Copy(code);
|
||||
await AlertService.Success(SmartTranslateService.Translate("Copied code to clipboard"));
|
||||
}
|
||||
}
|
||||
}
|
112
Moonlight/Shared/Views/Profile/Subscriptions.razor
Normal file
112
Moonlight/Shared/Views/Profile/Subscriptions.razor
Normal file
|
@ -0,0 +1,112 @@
|
|||
@page "/profile/subscriptions"
|
||||
|
||||
@using Moonlight.Shared.Components.Navigations
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Services.Interop
|
||||
|
||||
@inject ConfigService ConfigService
|
||||
@inject AlertService AlertService
|
||||
@inject SubscriptionService SubscriptionService
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
|
||||
<ProfileNavigation Index="2"/>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 p-10">
|
||||
<img src="/assets/media/svg/subscription.svg" class="img-fluid rounded-start" alt="Subscription">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card-body">
|
||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||
@if (Subscription == null)
|
||||
{
|
||||
var config = ConfigService
|
||||
.GetSection("Moonlight")
|
||||
.GetSection("Subscriptions")
|
||||
.GetSection("Sellpass");
|
||||
|
||||
var enableSellpass = config.GetValue<bool>("Enable");
|
||||
var url = config.GetValue<string>("Url");
|
||||
|
||||
<h3 class="mb-2">
|
||||
<div class="input-group mb-3">
|
||||
<input @bind="Code" type="text" class="form-control" placeholder="@(SmartTranslateService.Translate("Enter code"))">
|
||||
<WButton Text="@(SmartTranslateService.Translate("Submit"))"
|
||||
WorkingText="@(SmartTranslateService.Translate("Working"))"
|
||||
CssClasses="btn btn-primary"
|
||||
OnClick="OnSubmit">
|
||||
</WButton>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
if (enableSellpass)
|
||||
{
|
||||
<div class="d-flex justify-content-end pb-0 px-0">
|
||||
<a href="@(url)" class="btn btn-light">Buy subscription</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var d = User.SubscriptionSince.AddDays(User.SubscriptionDuration).ToUniversalTime();
|
||||
|
||||
<h3 class="mb-2">
|
||||
<TL>Active until</TL> @(Formatter.FormatDateOnly(d))
|
||||
</h3>
|
||||
<p class="fs-5 text-gray-600 fw-semibold">
|
||||
<TL>Current subscription</TL>: @(Subscription.Name)
|
||||
</p>
|
||||
<p class="fs-6 text-gray-600 fw-semibold">
|
||||
@(Subscription.Description)
|
||||
</p>
|
||||
<p class="fs-7 text-gray-600 fw-semibold">
|
||||
<TL>We will send you a notification upon subscription expiration</TL>
|
||||
</p>
|
||||
<div class="d-flex justify-content-end pb-0 px-0">
|
||||
<WButton Text="@(SmartTranslateService.Translate("Cancel"))"
|
||||
WorkingText="@(SmartTranslateService.Translate("Working"))"
|
||||
CssClasses="btn btn-light"
|
||||
OnClick="Cancel">
|
||||
</WButton>
|
||||
</div>
|
||||
}
|
||||
</LazyLoader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter]
|
||||
public User User { get; set; }
|
||||
|
||||
private Subscription? Subscription;
|
||||
private LazyLoader LazyLoader;
|
||||
|
||||
private string Code = "";
|
||||
|
||||
private async Task Load(LazyLoader arg)
|
||||
{
|
||||
Subscription = await SubscriptionService.GetCurrent();
|
||||
}
|
||||
|
||||
private async Task Cancel()
|
||||
{
|
||||
if (await AlertService.ConfirmMath())
|
||||
{
|
||||
await SubscriptionService.Cancel();
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSubmit()
|
||||
{
|
||||
await SubscriptionService.ApplyCode(Code);
|
||||
Code = "";
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
}
|
226
Moonlight/Shared/Views/Servers/Create.razor
Normal file
226
Moonlight/Shared/Views/Servers/Create.razor
Normal file
|
@ -0,0 +1,226 @@
|
|||
@page "/servers/create"
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Models.Forms
|
||||
@using Moonlight.App.Models.Misc
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Repositories.Servers
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Moonlight.App.Exceptions
|
||||
|
||||
@inject SubscriptionService SubscriptionService
|
||||
@inject ImageRepository ImageRepository
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject SmartDeployService SmartDeployService
|
||||
@inject ServerRepository ServerRepository
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ServerService ServerService
|
||||
|
||||
<LazyLoader Load="Load">
|
||||
@if (DeployNode == null)
|
||||
{
|
||||
<div class="d-flex justify-content-center flex-center">
|
||||
<div class="card">
|
||||
<img src="/assets/media/svg/nodata.svg" class="card-img-top w-25 mx-auto pt-5" alt="Not found image"/>
|
||||
<div class="card-body text-center">
|
||||
<h4 class="card-title">
|
||||
<TL>No node found</TL>
|
||||
</h4>
|
||||
<p class="card-text">
|
||||
<TL>No node found to deploy to found</TL>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="d-flex flex-column flex-lg-row">
|
||||
<div class="w-100 flex-lg-row-auto w-lg-300px mb-7 me-7 me-lg-10" data-select2-id="select2-data-131-dr2d">
|
||||
<div class="card card-flush py-4" data-select2-id="select2-data-130-ru5y">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<h2>
|
||||
<TL>Server details</TL>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pt-0">
|
||||
<div class="d-flex flex-column gap-10">
|
||||
<div class="fv-row">
|
||||
<label class="form-label">Node</label>
|
||||
<div class="fw-bold fs-3">@(DeployNode.Name)</div>
|
||||
</div>
|
||||
@if (Model.Image != null)
|
||||
{
|
||||
var limit = Images[Model.Image];
|
||||
|
||||
<div class="fv-row">
|
||||
<label class="form-label"><TL>Image</TL></label>
|
||||
<div class="fw-bold fs-3">@(Model.Image.Name)</div>
|
||||
</div>
|
||||
|
||||
<div class="fv-row">
|
||||
<label class="form-label"><TL>CPU</TL></label>
|
||||
<div class="fw-bold fs-3">
|
||||
@{
|
||||
var cpu = limit.ReadValue("cpu");
|
||||
|
||||
if (cpu == null)
|
||||
cpu = "N/A";
|
||||
else
|
||||
cpu = (int.Parse(cpu) / 100).ToString();
|
||||
}
|
||||
@(cpu) <TL>Cores</TL>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fv-row">
|
||||
<label class="form-label"><TL>Memory</TL></label>
|
||||
<div class="fw-bold fs-3">@(limit.ReadValue("memory")) MB</div>
|
||||
</div>
|
||||
|
||||
<div class="fv-row">
|
||||
<label class="form-label"><TL>Disk</TL></label>
|
||||
<div class="fw-bold fs-3">@(limit.ReadValue("disk")) MB</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-lg-row-fluid gap-7 gap-lg-10">
|
||||
<div class="card card-flush py-4">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<h2>
|
||||
<TL>Configure your server</TL>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pt-0">
|
||||
<SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
|
||||
<label class="form-label">
|
||||
<TL>Name</TL>
|
||||
</label>
|
||||
<div class="input-group mb-5">
|
||||
<InputText @bind-Value="Model.Name" class="form-control"></InputText>
|
||||
</div>
|
||||
@if (Images.Any())
|
||||
{
|
||||
<label class="form-label">
|
||||
<TL>Image</TL>
|
||||
</label>
|
||||
<SmartSelect TField="Image"
|
||||
@bind-Value="Model.Image"
|
||||
Items="Images.Keys.ToArray()"
|
||||
DisplayField="@(x => x.Name)">
|
||||
</SmartSelect>
|
||||
|
||||
<button type="submit" class="mt-5 float-end btn btn-primary">
|
||||
<TL>Create</TL>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-warning d-flex align-items-center p-5 mb-10">
|
||||
<span>
|
||||
<TL>You reached the maximum amount of servers for every image of your subscription</TL>: @(Subscription == null ? SmartTranslateService.Translate("Default") : Subscription.Name)
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</SmartForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter]
|
||||
public User User { get; set; }
|
||||
|
||||
private Node? DeployNode;
|
||||
private Subscription? Subscription;
|
||||
|
||||
private Dictionary<Image, SubscriptionLimit> Images = new();
|
||||
|
||||
private ServerOrderDataModel Model = new();
|
||||
|
||||
private async Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
// Reset state
|
||||
Images.Clear();
|
||||
Model = new();
|
||||
|
||||
await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription"));
|
||||
Subscription = await SubscriptionService.GetCurrent();
|
||||
|
||||
await lazyLoader.SetText(SmartTranslateService.Translate("Searching for deploy node"));
|
||||
|
||||
DeployNode = await SmartDeployService.GetNode();
|
||||
|
||||
await lazyLoader.SetText(SmartTranslateService.Translate("Searching for available images"));
|
||||
|
||||
var images = ImageRepository.Get().ToArray();
|
||||
|
||||
foreach (var image in images)
|
||||
{
|
||||
var limit = await SubscriptionService.GetLimit("image." + image.Id);
|
||||
|
||||
if (limit.Amount > 0)
|
||||
{
|
||||
var serversCount = ServerRepository
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Include(x => x.Image)
|
||||
.Where(x => x.Owner.Id == User.Id)
|
||||
.Count(x => x.Image.Id == image.Id);
|
||||
|
||||
if(serversCount < limit.Amount)
|
||||
Images.Add(image, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnValidSubmit()
|
||||
{
|
||||
var limit = await SubscriptionService.GetLimit("image." + Model.Image.Id);
|
||||
|
||||
if (limit.Amount > 0)
|
||||
{
|
||||
var serversCount = ServerRepository
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Include(x => x.Image)
|
||||
.Where(x => x.Owner.Id == User.Id)
|
||||
.Count(x => x.Image.Id == Model.Image.Id);
|
||||
|
||||
if (serversCount < limit.Amount)
|
||||
{
|
||||
if(int.TryParse(limit.ReadValue("cpu"), out int cpu) &&
|
||||
int.TryParse(limit.ReadValue("memory"), out int memory) &&
|
||||
int.TryParse(limit.ReadValue("disk"), out int disk))
|
||||
{
|
||||
var server = await ServerService.Create(
|
||||
Model.Name,
|
||||
cpu,
|
||||
memory,
|
||||
disk,
|
||||
User,
|
||||
Model.Image,
|
||||
DeployNode
|
||||
);
|
||||
|
||||
NavigationManager.NavigateTo($"/server/{server.Uuid}");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new DisplayException("Limits cannot be parsed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -462,8 +462,3 @@ Create subscription;Create subscription
|
|||
Options;Options
|
||||
Amount;Amount
|
||||
Do you really want to delete it?;Do you really want to delete it?
|
||||
Change your password;Change your password
|
||||
You need to change your password in order to use moonlight;You need to change your password in order to use moonlight
|
||||
You need to enter your full name in order to use moonlight;You need to enter your full name in order to use moonlight
|
||||
Enter your information;Enter your information
|
||||
The field FirstName must be a string or array type with a minimum length of '2'.;The field FirstName must be a string or array type with a minimum length of '2'.
|
||||
|
|
38
Moonlight/wwwroot/assets/media/svg/subscription.svg
Normal file
38
Moonlight/wwwroot/assets/media/svg/subscription.svg
Normal file
|
@ -0,0 +1,38 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="733.82" height="503.768" viewBox="0 0 733.82 503.768" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Group_16" data-name="Group 16" transform="translate(-196.555 -165.086)">
|
||||
<path id="Path_204-85" data-name="Path 204" d="M261.846,378.459a45.126,45.126,0,1,1,0-90.252H672.56a45.126,45.126,0,1,1,0,90.252Z" transform="translate(-20.165 -123.12)" fill="#e6e6e6"/>
|
||||
<path id="Path_205-86" data-name="Path 205" d="M264.96,297.207a39.24,39.24,0,0,0,0,78.48H675.674a39.24,39.24,0,1,0,0-78.48Z" transform="translate(-23.279 -126.234)" fill="#fff"/>
|
||||
<rect id="Rectangle_15" data-name="Rectangle 15" width="1.308" height="78.48" transform="translate(364.961 170.972)" fill="#e6e6e6"/>
|
||||
<rect id="Rectangle_17" data-name="Rectangle 17" width="1.308" height="78.48" transform="translate(527.808 170.972)" fill="#e6e6e6"/>
|
||||
<rect id="Rectangle_7" data-name="Rectangle 7" width="161.539" height="78.48" transform="translate(366.269 170.972)" fill="#6c63ff"/>
|
||||
<path id="Path_198-87" data-name="Path 198" d="M276.268,206.815a24,24,0,1,0,24,24,24,24,0,0,0-24-24Zm0,7.2a7.2,7.2,0,1,1-7.2,7.2A7.2,7.2,0,0,1,276.268,214.016Zm0,34.662a17.484,17.484,0,0,1-14.4-7.685c.115-4.8,9.6-7.442,14.4-7.442s14.285,2.642,14.4,7.442a17.513,17.513,0,0,1-14.4,7.685Z" transform="translate(170.844 -20.53)" fill="#fff"/>
|
||||
<path id="Path_200-88" data-name="Path 200" d="M620.7,206.815a24,24,0,1,0,24,24,24,24,0,0,0-24-24Zm0,7.2a7.2,7.2,0,1,1-7.2,7.2A7.2,7.2,0,0,1,620.7,214.015Zm0,34.662a17.484,17.484,0,0,1-14.4-7.685c.115-4.8,9.6-7.442,14.4-7.442s14.285,2.642,14.4,7.442a17.513,17.513,0,0,1-14.4,7.685Z" transform="translate(-336.439 -20.53)" fill="#e6e6e6"/>
|
||||
<path id="Path_242-89" data-name="Path 242" d="M620.7,206.815a24,24,0,1,0,24,24,24,24,0,0,0-24-24Zm0,7.2a7.2,7.2,0,1,1-7.2,7.2A7.2,7.2,0,0,1,620.7,214.015Zm0,34.662a17.484,17.484,0,0,1-14.4-7.685c.115-4.8,9.6-7.442,14.4-7.442s14.285,2.642,14.4,7.442a17.513,17.513,0,0,1-14.4,7.685Z" transform="translate(-10.892 -20.53)" fill="#e6e6e6"/>
|
||||
<rect id="Rectangle_9" data-name="Rectangle 9" width="56" height="56" rx="6" transform="translate(419.112 288.229)" fill="#6c63ff"/>
|
||||
<ellipse id="Ellipse_29" data-name="Ellipse 29" cx="134.439" cy="18" rx="134.439" ry="18" transform="translate(661.497 632.854)" fill="#e6e6e6"/>
|
||||
<rect id="Rectangle_12" data-name="Rectangle 12" width="56" height="56" rx="6" transform="translate(581.812 288.049)" fill="#e6e6e6"/>
|
||||
<rect id="Rectangle_13" data-name="Rectangle 13" width="40.798" height="40.798" transform="translate(589.812 295.83)" fill="#fff"/>
|
||||
<path id="Path_202-90" data-name="Path 202" d="M253.345,218.766l-7.075-9.1,4.114-3.2,3.35,4.307,11.318-11.946,3.785,3.585Z" transform="translate(191.14 106.158)" fill="#fff"/>
|
||||
<path id="Path_203-91" data-name="Path 203" d="M425.345,218.766l-7.075-9.1,4.114-3.2,3.35,4.307,11.317-11.946,3.785,3.585Z" transform="translate(182.106 106.158)" fill="#e6e6e6"/>
|
||||
<rect id="Rectangle_18" data-name="Rectangle 18" width="56" height="56" rx="6" transform="translate(256.265 288.049)" fill="#e6e6e6"/>
|
||||
<rect id="Rectangle_19" data-name="Rectangle 19" width="40.798" height="40.798" transform="translate(264.265 295.83)" fill="#fff"/>
|
||||
<path id="Path_243-92" data-name="Path 243" d="M425.345,218.766l-7.075-9.1,4.114-3.2,3.35,4.307,11.317-11.946,3.785,3.585Z" transform="translate(-143.441 106.158)" fill="#e6e6e6"/>
|
||||
<g id="Group_15" data-name="Group 15">
|
||||
<path id="Path_257-93" data-name="Path 257" d="M340.66,397.363H327.48l-6.268-50.837,19.452,0Z" transform="translate(545.904 239.259)" fill="#ffb8b8"/>
|
||||
<path id="Path_258-94" data-name="Path 258" d="M320.6,387.355h25.418v16H304.6a16,16,0,0,1,16-16Z" transform="translate(543.364 245.5)" fill="#2f2e41"/>
|
||||
<path id="Path_259-95" data-name="Path 259" d="M223.865,397.363h-13.18l-6.268-50.837,19.452,0Z" transform="translate(528.049 239.259)" fill="#ffb8b8"/>
|
||||
<path id="Path_260-96" data-name="Path 260" d="M203.81,387.355h25.418v16H187.806a16,16,0,0,1,16-16Z" transform="translate(525.51 245.5)" fill="#2f2e41"/>
|
||||
<path id="Path_261-97" data-name="Path 261" d="M487.471,249.585V243.82a37.18,37.18,0,0,1,37.18-37.18h0a37.18,37.18,0,0,1,37.18,37.18v5.764a26.8,26.8,0,0,1-26.8,26.8H514.275a26.8,26.8,0,0,1-26.8-26.8Z" transform="translate(308.465 9.946)" fill="#2f2e41"/>
|
||||
<ellipse id="Ellipse_36" data-name="Ellipse 36" cx="28.316" cy="28.316" rx="28.316" ry="28.316" transform="translate(804.801 231.687)" fill="#ffb8b8"/>
|
||||
<path id="Path_263-98" data-name="Path 263" d="M386.583,329.1a10.811,10.811,0,0,1,16.463,1.934l24.273-4.591,6.388,14.07-34.37,6A10.869,10.869,0,0,1,386.583,329.1Z" transform="translate(292.514 28.216)" fill="#ffb8b8"/>
|
||||
<path id="Path_264-99" data-name="Path 264" d="M515.087,284.516l.317.481-39.8,26.221-67.164,21.447a4.044,4.044,0,0,0-2.781,4.31l1.465,12.62a4.036,4.036,0,0,0,4.854,3.48l63.212-13.549a22.833,22.833,0,0,0,8.5-3.742L528.4,303.969A11.5,11.5,0,0,0,515.4,285Z" transform="translate(295.954 21.634)" fill="#ccc"/>
|
||||
<path id="Path_265-100" data-name="Path 265" d="M574.076,590.876a5.209,5.209,0,0,1-4.771-3.115l-60.421-149.3a1.729,1.729,0,0,0-3.238.182L456.351,583.993a5.189,5.189,0,0,1-6.781,3.333l-16.53-6.2a5.175,5.175,0,0,1-3.34-4.271c-7.437-64.782,57.413-228.3,58.069-229.946l.182-.455,59.116,13.077.123.134c23.585,25.73,42.971,188.012,46.618,220.283a5.163,5.163,0,0,1-3.425,5.472l-14.591,5.16a5.139,5.139,0,0,1-1.716.295Z" transform="translate(299.543 31.32)" fill="#2f2e41"/>
|
||||
<path id="Path_266-101" data-name="Path 266" d="M515.547,375.9c-14.323,0-30.291-2.856-35.206-14.642l-.113-.271.153-.251c3.88-6.366,9.007-17.224,6.251-19.263-5.429-4.014-8.064-10.618-7.83-19.628.508-19.559,13.835-36.925,33.163-43.212h0a147.146,147.146,0,0,1,16.443-4.234,27.993,27.993,0,0,1,23.21,5.732,28.276,28.276,0,0,1,10.486,21.755c.2,20.9-3.015,50.015-19.5,70a5.128,5.128,0,0,1-3.036,1.765A140.9,140.9,0,0,1,515.547,375.9Z" transform="translate(307.138 20.219)" fill="#ccc"/>
|
||||
<path id="Path_267-102" data-name="Path 267" d="M506.106,364.845a11.017,11.017,0,0,1,13.464-7.683,10.843,10.843,0,0,1,1.669.618l18.43-16.773,12.818,8.635L526.13,372.966a11,11,0,0,1-12.466,5.288,10.83,10.83,0,0,1-7.558-13.409Z" transform="translate(311.251 30.487)" fill="#ffb8b8"/>
|
||||
<path id="Path_268-103" data-name="Path 268" d="M534.283,373.874A5.174,5.174,0,0,1,531,372.7l-7.268-5.939a5.188,5.188,0,0,1,.126-8.134l30.484-23.38a1.733,1.733,0,0,0,.327-2.415l-18.815-24.875a15.316,15.316,0,0,1,1.023-19.731h0a15.273,15.273,0,0,1,20.622-1.649l.119.126,19.647,28.133a17.515,17.515,0,0,1-.415,27.883l-39.481,30.134a5.2,5.2,0,0,1-3.088,1.017Z" transform="translate(313.718 21.67)" fill="#ccc"/>
|
||||
<path id="Path_269-104" data-name="Path 269" d="M497.965,240.705V226.656L523.047,215.7l23.916,10.952v14.049a2.306,2.306,0,0,1-2.306,2.306H500.271a2.306,2.306,0,0,1-2.306-2.306Z" transform="translate(310.07 11.332)" fill="#2f2e41"/>
|
||||
<circle id="Ellipse_30" data-name="Ellipse 30" cx="15.722" cy="15.722" r="15.722" transform="translate(838.852 199.377)" fill="#2f2e41"/>
|
||||
<path id="Path_185-105" data-name="Path 185" d="M896.5,218.806a15.715,15.715,0,0,1,18.8-15.417,15.715,15.715,0,1,0-9.764,29.629,15.709,15.709,0,0,1-9.032-14.212Z" transform="translate(-56.438 -12.141)" fill="#2f2e41"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 7.2 KiB |
Loading…
Reference in a new issue