inline DeepDrftCms RCL into DeepDrftManager and delete the project

This commit is contained in:
Daniel Harvey
2026-05-21 20:36:00 -04:00
parent db463a9049
commit ce1cbccad5
16 changed files with 11 additions and 83 deletions
@@ -0,0 +1,88 @@
@using System.Net.Http
@using System.Net.Http.Headers
@using Microsoft.AspNetCore.Components
@inject IHttpClientFactory HttpClientFactory
@inject AuthBlocksWeb.Services.ITokenService TokenService
<MudDialog>
<DialogContent>
<MudText Typo="Typo.body1">
Are you sure you want to delete '@TrackName'? This cannot be undone.
</MudText>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<MudAlert Severity="Severity.Error" Class="mt-3" Dense="true">@_errorMessage</MudAlert>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Disabled="_isDeleting">Cancel</MudButton>
<MudButton Color="Color.Error"
Variant="Variant.Filled"
OnClick="ConfirmAsync"
Disabled="_isDeleting">
@if (_isDeleting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
<span>Deleting...</span>
}
else
{
<span>Delete</span>
}
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public long TrackId { get; set; }
[Parameter] public string TrackName { get; set; } = "";
[Parameter] public EventCallback OnDeleted { get; set; }
private bool _isDeleting;
private string? _errorMessage;
private async Task ConfirmAsync()
{
_isDeleting = true;
_errorMessage = null;
try
{
var client = HttpClientFactory.CreateClient("DeepDrft.API");
var token = await TokenService.GetAccessTokenAsync();
if (!string.IsNullOrEmpty(token))
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.DeleteAsync($"api/cms/track/{TrackId}");
if (response.IsSuccessStatusCode)
{
if (OnDeleted.HasDelegate)
{
await OnDeleted.InvokeAsync();
}
MudDialog.Close(DialogResult.Ok(true));
return;
}
_errorMessage = response.StatusCode switch
{
System.Net.HttpStatusCode.NotFound => "Track not found. It may have already been deleted.",
System.Net.HttpStatusCode.Unauthorized => "You are not authorized to delete this track.",
System.Net.HttpStatusCode.Forbidden => "You are not authorized to delete this track.",
_ => $"Delete failed ({(int)response.StatusCode})."
};
}
catch (Exception ex)
{
_errorMessage = $"Delete failed: {ex.Message}";
}
finally
{
_isDeleting = false;
}
}
private void Cancel() => MudDialog.Cancel();
}
@@ -0,0 +1,34 @@
@rendermode InteractiveServer
@inherits LayoutComponentBase
@using DeepDrftShared.Client.Common
<MudThemeProvider IsDarkMode="false" Theme="@DeepDrftPalettes.Cms" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<MudLayout>
<MudAppBar Dense="true" Elevation="1" Color="Color.Primary">
<MudText Typo="Typo.h6" Class="ml-3" Style="font-family: 'DM Sans', sans-serif; letter-spacing: 0.05em;">
Deep Drft — Admin
</MudText>
<MudSpacer />
<MudTooltip Text="Back to site">
<MudIconButton Icon="@Icons.Material.Filled.Home"
Href="/"
Color="Color.Inherit" />
</MudTooltip>
</MudAppBar>
<MudMainContent Class="pt-14 pb-8">
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4">
@Body
</MudContainer>
</MudMainContent>
</MudLayout>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@@ -0,0 +1,11 @@
@page "/cms"
@rendermode InteractiveServer
@using AuthBlocksWeb.HierarchicalAuthorize
@attribute [HierarchicalRoleAuthorize("Admin")]
<PageTitle>DeepDrft CMS</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
<MudText Typo="Typo.h3" GutterBottom="true">DeepDrft CMS</MudText>
<MudText Typo="Typo.body1">Administration panel — under construction.</MudText>
</MudContainer>
@@ -0,0 +1 @@
@layout DeepDrftManager.Components.Layout.CmsLayout
@@ -0,0 +1,233 @@
@page "/cms/tracks/{Id:int}"
@using AuthBlocksWeb.HierarchicalAuthorize
@using AuthBlocksWeb.Services
@using DeepDrftData
@using System.Net.Http.Headers
@using System.Net.Http.Json
@attribute [HierarchicalRoleAuthorize("Admin")]
@inject ITrackService TrackService
@inject IHttpClientFactory HttpClientFactory
@inject ITokenService TokenService
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager Nav
@inject ILogger<TrackEdit> Logger
<PageTitle>Edit Track — DeepDrft CMS</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="mt-8">
<MudButton Variant="Variant.Text"
StartIcon="@Icons.Material.Filled.ArrowBack"
Href="/cms/tracks"
Class="mb-4">
Back to tracks
</MudButton>
@if (_loading)
{
<MudProgressCircular Indeterminate="true" />
}
else if (_track is null)
{
<MudAlert Severity="Severity.Warning">
Track not found.
</MudAlert>
}
else
{
<MudText Typo="Typo.h4" GutterBottom="true">Edit Track</MudText>
<MudPaper Class="pa-6" Elevation="2">
<MudStack Spacing="4">
<MudField Label="Entry Key" Variant="Variant.Outlined" InnerPadding="false">
<MudText Typo="Typo.body1" Style="font-family: monospace;">@_track.EntryKey</MudText>
<MudText Typo="Typo.caption" Color="Color.Default">
Vault reference — not editable.
</MudText>
</MudField>
<MudTextField @bind-Value="_form.TrackName"
Label="Track Name"
Required="true"
RequiredError="Track name is required"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="_form.Artist"
Label="Artist"
Required="true"
RequiredError="Artist is required"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="_form.Album"
Label="Album"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="_form.Genre"
Label="Genre"
Variant="Variant.Outlined" />
<MudDatePicker @bind-Date="_form.ReleaseDate"
Label="Release Date"
DateFormat="yyyy-MM-dd"
Variant="Variant.Outlined" />
<MudStack Row="true" Spacing="2" Justify="Justify.SpaceBetween">
<MudButton Variant="Variant.Filled"
Color="Color.Error"
StartIcon="@Icons.Material.Filled.Delete"
Disabled="_busy"
OnClick="ConfirmDelete">
Delete
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Save"
Disabled="_busy || !CanSave"
OnClick="SaveAsync">
Save Changes
</MudButton>
</MudStack>
</MudStack>
</MudPaper>
}
</MudContainer>
@code {
[Parameter] public int Id { get; set; }
private TrackEntity? _track;
private TrackEditForm _form = new();
private bool _loading = true;
private bool _busy;
private bool CanSave =>
!string.IsNullOrWhiteSpace(_form.TrackName)
&& !string.IsNullOrWhiteSpace(_form.Artist);
protected override async Task OnInitializedAsync()
{
await LoadAsync();
}
private async Task LoadAsync()
{
_loading = true;
var result = await TrackService.GetById(Id);
_track = result.Success ? result.Value : null;
if (_track is not null)
{
_form = TrackEditForm.From(_track);
}
_loading = false;
}
private async Task SaveAsync()
{
if (_track is null || !CanSave) return;
_busy = true;
try
{
var http = HttpClientFactory.CreateClient("DeepDrft.API");
await AttachBearerAsync(http);
var payload = new
{
TrackName = _form.TrackName,
Artist = _form.Artist,
Album = string.IsNullOrWhiteSpace(_form.Album) ? null : _form.Album,
Genre = string.IsNullOrWhiteSpace(_form.Genre) ? null : _form.Genre,
ReleaseDate = _form.ReleaseDate is { } d ? DateOnly.FromDateTime(d) : (DateOnly?)null
};
var response = await http.PutAsJsonAsync($"api/cms/track/{Id}", payload);
if (response.IsSuccessStatusCode)
{
Snackbar.Add("Track updated.", Severity.Success);
await LoadAsync();
}
else
{
Snackbar.Add($"Save failed: {(int)response.StatusCode} {response.ReasonPhrase}", Severity.Error);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Save failed for track {TrackId}", Id);
Snackbar.Add("Save failed — please try again.", Severity.Error);
}
finally
{
_busy = false;
}
}
// DELETE api/cms/track/{Id} is handled by CmsDeleteController (T3 branch).
private async Task ConfirmDelete()
{
if (_track is null) return;
var confirmed = await DialogService.ShowMessageBox(
"Delete track",
$"Permanently delete \"{_track.TrackName}\" by {_track.Artist}? This cannot be undone.",
yesText: "Delete",
cancelText: "Cancel");
if (confirmed != true) return;
_busy = true;
try
{
var http = HttpClientFactory.CreateClient("DeepDrft.API");
await AttachBearerAsync(http);
var response = await http.DeleteAsync($"api/cms/track/{Id}");
if (response.IsSuccessStatusCode)
{
Snackbar.Add("Track deleted.", Severity.Success);
Nav.NavigateTo("/cms/tracks");
}
else
{
Snackbar.Add($"Delete failed: {(int)response.StatusCode} {response.ReasonPhrase}", Severity.Error);
_busy = false;
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Delete failed for track {TrackId}", Id);
Snackbar.Add("Delete failed — please try again.", Severity.Error);
_busy = false;
}
}
private async Task AttachBearerAsync(HttpClient http)
{
var token = await TokenService.GetAccessTokenAsync();
if (!string.IsNullOrEmpty(token))
{
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
private sealed class TrackEditForm
{
public string TrackName { get; set; } = string.Empty;
public string Artist { get; set; } = string.Empty;
public string? Album { get; set; }
public string? Genre { get; set; }
public DateTime? ReleaseDate { get; set; }
public static TrackEditForm From(TrackEntity track) => new()
{
TrackName = track.TrackName,
Artist = track.Artist,
Album = track.Album,
Genre = track.Genre,
ReleaseDate = track.ReleaseDate is { } d
? d.ToDateTime(TimeOnly.MinValue)
: null
};
}
}
@@ -0,0 +1,140 @@
@page "/cms/tracks"
@using System.Net
@using System.Net.Http.Headers
@using AuthBlocksWeb.HierarchicalAuthorize
@using Models.Common
@attribute [HierarchicalRoleAuthorize("Admin")]
@inject ITrackService TrackService
@inject IHttpClientFactory HttpClientFactory
@inject AuthBlocksWeb.Services.ITokenService TokenService
@inject IDialogService DialogService
@inject ISnackbar Snackbar
@inject ILogger<TrackList> Logger
<PageTitle>Tracks — DeepDrft CMS</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
<MudText Typo="Typo.h3">Tracks</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
Href="/cms/tracks/new">
Add Track
</MudButton>
</MudStack>
<MudTable T="TrackEntity"
@ref="_table"
ServerData="LoadServerData"
Hover="true"
Striped="true"
Dense="true"
Bordered="false"
FixedHeader="true"
RowsPerPage="20"
AllowUnsorted="false">
<NoRecordsContent>
<MudText Typo="Typo.body1">No tracks found.</MudText>
</NoRecordsContent>
<LoadingContent>
<MudText Typo="Typo.body1">Loading tracks…</MudText>
</LoadingContent>
<HeaderContent>
<MudTh><MudTableSortLabel SortLabel="TrackName" T="TrackEntity" InitialDirection="SortDirection.Ascending">Track Name</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="Artist" T="TrackEntity">Artist</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="Album" T="TrackEntity">Album</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="Genre" T="TrackEntity">Genre</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="ReleaseDate" T="TrackEntity">Release Date</MudTableSortLabel></MudTh>
<MudTh>Entry Key</MudTh>
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Track Name">@context.TrackName</MudTd>
<MudTd DataLabel="Artist">@context.Artist</MudTd>
<MudTd DataLabel="Album">@(context.Album ?? "—")</MudTd>
<MudTd DataLabel="Genre">@(context.Genre ?? "—")</MudTd>
<MudTd DataLabel="Release Date">@(context.ReleaseDate?.ToString("yyyy-MM-dd") ?? "—")</MudTd>
<MudTd DataLabel="Entry Key"><MudText Typo="Typo.caption" Style="font-family: monospace;">@context.EntryKey</MudText></MudTd>
<MudTd DataLabel="Actions">
<MudTooltip Text="Edit">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Color="Color.Primary"
Href="@($"/cms/tracks/{context.Id}")" />
</MudTooltip>
<MudTooltip Text="Delete">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
OnClick="@(() => ConfirmAndDelete(context))" />
</MudTooltip>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new[] { 10, 20, 50 }" />
</PagerContent>
</MudTable>
</MudContainer>
@code {
private MudTable<TrackEntity>? _table;
private async Task<TableData<TrackEntity>> LoadServerData(TableState state, CancellationToken cancellationToken)
{
var pageNumber = state.Page + 1; // MudTable is 0-based, service is 1-based.
var sortColumn = string.IsNullOrEmpty(state.SortLabel) ? "TrackName" : state.SortLabel;
var sortDescending = state.SortDirection == SortDirection.Descending;
var result = await TrackService.GetPaged(pageNumber, state.PageSize, sortColumn, sortDescending, cancellationToken);
if (!result.Success || result.Value is null)
{
var errorText = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
Snackbar.Add($"Failed to load tracks: {errorText}", Severity.Error);
return new TableData<TrackEntity> { Items = Array.Empty<TrackEntity>(), TotalItems = 0 };
}
var page = result.Value;
return new TableData<TrackEntity>
{
Items = page.Items,
TotalItems = page.TotalCount
};
}
private async Task ConfirmAndDelete(TrackEntity track)
{
var confirmed = await DialogService.ShowMessageBox(
title: "Delete track",
markupMessage: new MarkupString($"Delete <strong>{WebUtility.HtmlEncode(track.TrackName)}</strong> by {WebUtility.HtmlEncode(track.Artist)}? This removes both the metadata row and the underlying audio entry."),
yesText: "Delete",
cancelText: "Cancel");
if (confirmed != true) return;
try
{
var client = HttpClientFactory.CreateClient("DeepDrft.API");
var token = await TokenService.GetAccessTokenAsync();
if (!string.IsNullOrEmpty(token))
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.DeleteAsync($"api/cms/track/{track.Id}");
if (response.IsSuccessStatusCode)
{
Snackbar.Add($"Deleted '{track.TrackName}'.", Severity.Success);
if (_table is not null) await _table.ReloadServerData();
}
else
{
Snackbar.Add($"Delete failed ({(int)response.StatusCode} {response.ReasonPhrase}).", Severity.Error);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Delete failed for track {TrackId}", track.Id);
Snackbar.Add("Delete failed — please try again.", Severity.Error);
}
}
}
@@ -0,0 +1,181 @@
@page "/cms/tracks/new"
@using System.Net.Http.Headers
@using AuthBlocksWeb.HierarchicalAuthorize
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@attribute [HierarchicalRoleAuthorize("Admin")]
@inject IHttpClientFactory HttpClientFactory
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
@inject ILogger<TrackNew> Logger
<PageTitle>Add Track — DeepDrft CMS</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="mt-8">
<MudText Typo="Typo.h4" GutterBottom="true">Add Track</MudText>
<MudPaper Class="pa-6" Elevation="2">
<MudStack Spacing="4">
<MudText Typo="Typo.subtitle1">WAV file</MudText>
<InputFile OnChange="OnFileSelected" accept=".wav,audio/wav,audio/x-wav" />
@if (_selectedFile is not null)
{
<MudText Typo="Typo.body2">
Selected: @_selectedFile.Name (@FormatBytes(_selectedFile.Size))
</MudText>
}
<MudTextField @bind-Value="_trackName" Label="Track Name" Required="true" RequiredError="Track Name is required" Variant="Variant.Outlined" />
<MudTextField @bind-Value="_artist" Label="Artist" Required="true" RequiredError="Artist is required" Variant="Variant.Outlined" />
<MudTextField @bind-Value="_album" Label="Album" Variant="Variant.Outlined" />
<MudTextField @bind-Value="_genre" Label="Genre" Variant="Variant.Outlined" />
<MudTextField @bind-Value="_releaseDate" Label="Release Date (YYYY-MM-DD)" Placeholder="2024-01-15" Variant="Variant.Outlined" />
@if (!string.IsNullOrEmpty(_errorMessage))
{
<MudAlert Severity="Severity.Error">@_errorMessage</MudAlert>
}
<MudStack Row="true" Spacing="2" Justify="Justify.FlexEnd">
<MudButton Variant="Variant.Text"
OnClick="Cancel"
Disabled="_isUploading">
Cancel
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SubmitAsync"
Disabled="_isUploading">
@if (_isUploading)
{
<MudProgressCircular Indeterminate="true" Size="Size.Small" Class="mr-2" />
<text>Uploading…</text>
}
else
{
<text>Upload</text>
}
</MudButton>
</MudStack>
</MudStack>
</MudPaper>
</MudContainer>
@code {
// 1 GB ceiling matches the proxy controller's RequestSizeLimit; the actual streaming
// path means the limit caps the request, not in-memory buffering.
private const long MaxUploadBytes = 1_073_741_824L;
private IBrowserFile? _selectedFile;
private string _trackName = string.Empty;
private string _artist = string.Empty;
private string _album = string.Empty;
private string _genre = string.Empty;
private string _releaseDate = string.Empty;
private string? _errorMessage;
private bool _isUploading;
private void OnFileSelected(InputFileChangeEventArgs e)
{
_selectedFile = e.File;
_errorMessage = null;
}
private async Task SubmitAsync()
{
_errorMessage = null;
if (_selectedFile is null)
{
_errorMessage = "Please select a WAV file.";
return;
}
if (!_selectedFile.Name.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
{
_errorMessage = "Selected file must be a .wav file.";
return;
}
if (string.IsNullOrWhiteSpace(_trackName))
{
_errorMessage = "Track Name is required.";
return;
}
if (string.IsNullOrWhiteSpace(_artist))
{
_errorMessage = "Artist is required.";
return;
}
if (!string.IsNullOrWhiteSpace(_releaseDate)
&& !DateOnly.TryParseExact(_releaseDate, "yyyy-MM-dd", out _))
{
_errorMessage = "Release Date must be in YYYY-MM-DD format.";
return;
}
_isUploading = true;
try
{
using var multipart = new MultipartFormDataContent();
// OpenReadStream streams chunks from the browser via the SignalR circuit;
// wrapping in StreamContent avoids materialising the whole file in memory
// before the proxy controller receives it.
await using var fileStream = _selectedFile.OpenReadStream(MaxUploadBytes);
var fileContent = new StreamContent(fileStream);
fileContent.Headers.ContentType = new MediaTypeHeaderValue(
string.IsNullOrWhiteSpace(_selectedFile.ContentType) ? "audio/wav" : _selectedFile.ContentType);
multipart.Add(fileContent, "wav", _selectedFile.Name);
multipart.Add(new StringContent(_trackName), "trackName");
multipart.Add(new StringContent(_artist), "artist");
if (!string.IsNullOrWhiteSpace(_album)) multipart.Add(new StringContent(_album), "album");
if (!string.IsNullOrWhiteSpace(_genre)) multipart.Add(new StringContent(_genre), "genre");
if (!string.IsNullOrWhiteSpace(_releaseDate)) multipart.Add(new StringContent(_releaseDate), "releaseDate");
var client = HttpClientFactory.CreateClient("DeepDrft.API");
using var response = await client.PostAsync("api/cms/track", multipart);
if (response.IsSuccessStatusCode)
{
Snackbar.Add($"Uploaded '{_trackName}'.", Severity.Success);
Navigation.NavigateTo("/cms/tracks");
return;
}
var body = await response.Content.ReadAsStringAsync();
_errorMessage = string.IsNullOrWhiteSpace(body)
? $"Upload failed ({(int)response.StatusCode})."
: $"Upload failed ({(int)response.StatusCode}): {body}";
Logger.LogWarning("CMS upload rejected: {Status} {Body}", (int)response.StatusCode, body);
}
catch (Exception ex)
{
Logger.LogError(ex, "Upload failed in TrackNew");
_errorMessage = "Upload failed. Please try again.";
}
finally
{
_isUploading = false;
}
}
private void Cancel()
{
Navigation.NavigateTo("/cms/tracks");
}
private static string FormatBytes(long bytes)
{
const long KB = 1024;
const long MB = KB * 1024;
const long GB = MB * 1024;
if (bytes >= GB) return $"{bytes / (double)GB:F2} GB";
if (bytes >= MB) return $"{bytes / (double)MB:F2} MB";
if (bytes >= KB) return $"{bytes / (double)KB:F2} KB";
return $"{bytes} bytes";
}
}
@@ -0,0 +1 @@
@layout DeepDrftManager.Components.Layout.CmsLayout
+1 -1
View File
@@ -1,5 +1,5 @@
<Router AppAssembly="typeof(App).Assembly"
AdditionalAssemblies="new[] { typeof(DeepDrftCms._Imports).Assembly, typeof(AuthBlocksWeb._Imports).Assembly }">
AdditionalAssemblies="new[] { typeof(AuthBlocksWeb._Imports).Assembly }">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData">
<NotAuthorized>
@@ -1,9 +1,16 @@
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.Extensions.Logging
@using Microsoft.JSInterop
@using MudBlazor
@using AuthBlocksWeb.Components
@using DeepDrftManager
@using DeepDrftManager.Components
@using DeepDrftModels.Entities
@using DeepDrftData
@using Models.Common