Updated to latest mooncore version. Cleaned up some crud controllers and replaced DataTable with the new DataGrid component

This commit is contained in:
2025-09-16 12:09:20 +00:00
parent 8e242dc8da
commit 86bec7f2ee
21 changed files with 492 additions and 848 deletions

View File

@@ -1,12 +1,10 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Models;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Mappers;
using Moonlight.ApiServer.Services;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
@@ -28,69 +26,82 @@ public class ApiKeysController : Controller
[HttpGet]
[Authorize(Policy = "permissions:admin.apikeys.get")]
public async Task<IPagedData<ApiKeyResponse>> Get([FromQuery] PagedOptions options)
public async Task<ActionResult<ICountedData<ApiKeyResponse>>> Get(
[FromQuery] int startIndex,
[FromQuery] int count,
[FromQuery] string? orderBy,
[FromQuery] string? filter,
[FromQuery] string orderByDir = "asc"
)
{
var count = await ApiKeyRepository.Get().CountAsync();
if (count > 100)
return Problem("You cannot fetch more items than 100 at a time", statusCode: 400);
var apiKeys = await ApiKeyRepository
.Get()
.OrderBy(x => x.Id)
.Skip(options.Page * options.PageSize)
.Take(options.PageSize)
IQueryable<ApiKey> query = ApiKeyRepository.Get();
query = orderBy switch
{
nameof(ApiKey.Id) => orderByDir == "desc"
? query.OrderByDescending(x => x.Id)
: query.OrderBy(x => x.Id),
nameof(ApiKey.ExpiresAt) => orderByDir == "desc"
? query.OrderByDescending(x => x.ExpiresAt)
: query.OrderBy(x => x.ExpiresAt),
nameof(ApiKey.CreatedAt) => orderByDir == "desc"
? query.OrderByDescending(x => x.CreatedAt)
: query.OrderBy(x => x.CreatedAt),
_ => query.OrderBy(x => x.Id)
};
if (!string.IsNullOrEmpty(filter))
{
query = query.Where(x =>
EF.Functions.ILike(x.Description, $"%{filter}%")
);
}
var totalCount = await query.CountAsync();
var items = await query
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToResponse()
.ToArrayAsync();
var mappedApiKey = apiKeys
.Select(x => new ApiKeyResponse()
return new CountedData<ApiKeyResponse>()
{
Id = x.Id,
Permissions = x.Permissions,
Description = x.Description,
ExpiresAt = x.ExpiresAt
})
.ToArray();
return new PagedData<ApiKeyResponse>()
{
CurrentPage = options.Page,
Items = mappedApiKey,
PageSize = options.PageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / options.PageSize
Items = items,
TotalCount = totalCount
};
}
[HttpGet("{id}")]
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.apikeys.get")]
public async Task<ApiKeyResponse> GetSingle(int id)
public async Task<ActionResult<ApiKeyResponse>> GetSingle(int id)
{
var apiKey = await ApiKeyRepository
.Get()
.AsNoTracking()
.ProjectToResponse()
.FirstOrDefaultAsync(x => x.Id == id);
if (apiKey == null)
throw new HttpApiException("No api key with that id found", 404);
return Problem("No api key with that id found", statusCode: 404);
return new ApiKeyResponse()
{
Id = apiKey.Id,
Permissions = apiKey.Permissions,
Description = apiKey.Description,
ExpiresAt = apiKey.ExpiresAt
};
return apiKey;
}
[HttpPost]
[Authorize(Policy = "permissions:admin.apikeys.create")]
public async Task<CreateApiKeyResponse> Create([FromBody] CreateApiKeyRequest request)
{
var apiKey = new ApiKey()
{
Description = request.Description,
Permissions = request.Permissions,
ExpiresAt = request.ExpiresAt
};
var apiKey = ApiKeyMapper.ToApiKey(request);
var finalApiKey = await ApiKeyRepository.Add(apiKey);
var finalApiKey = await ApiKeyRepository.AddAsync(apiKey);
var response = new CreateApiKeyResponse
{
@@ -104,41 +115,36 @@ public class ApiKeysController : Controller
return response;
}
[HttpPatch("{id}")]
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.apikeys.update")]
public async Task<ApiKeyResponse> Update([FromRoute] int id, [FromBody] UpdateApiKeyRequest request)
public async Task<ActionResult<ApiKeyResponse>> Update([FromRoute] int id, [FromBody] UpdateApiKeyRequest request)
{
var apiKey = await ApiKeyRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (apiKey == null)
throw new HttpApiException("No api key with that id found", 404);
return Problem("No api key with that id found", statusCode: 404);
apiKey.Description = request.Description;
ApiKeyMapper.Merge(apiKey, request);
await ApiKeyRepository.Update(apiKey);
await ApiKeyRepository.UpdateAsync(apiKey);
return new ApiKeyResponse()
{
Id = apiKey.Id,
Description = apiKey.Description,
Permissions = apiKey.Permissions,
ExpiresAt = apiKey.ExpiresAt
};
return ApiKeyMapper.ToResponse(apiKey);
}
[HttpDelete("{id}")]
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.apikeys.delete")]
public async Task Delete([FromRoute] int id)
public async Task<ActionResult> Delete([FromRoute] int id)
{
var apiKey = await ApiKeyRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (apiKey == null)
throw new HttpApiException("No api key with that id found", 404);
return Problem("No api key with that id found", statusCode: 404);
await ApiKeyRepository.Remove(apiKey);
await ApiKeyRepository.RemoveAsync(apiKey);
return NoContent();
}
}

View File

@@ -26,65 +26,96 @@ public class ThemesController : Controller
[HttpGet]
[Authorize(Policy = "permissions:admin.system.customisation.themes.read")]
public async Task<PagedData<ThemeResponse>> Get([FromQuery] PagedOptions options)
public async Task<ActionResult<ICountedData<ThemeResponse>>> Get(
[FromQuery] int startIndex,
[FromQuery] int count,
[FromQuery] string? orderBy,
[FromQuery] string? filter,
[FromQuery] string orderByDir = "asc"
)
{
var count = await ThemeRepository.Get().CountAsync();
if (count > 100)
return Problem("You cannot fetch more items than 100 at a time", statusCode: 400);
var items = await ThemeRepository
.Get()
.Skip(options.Page * options.PageSize)
.Take(options.PageSize)
IQueryable<Theme> query = ThemeRepository.Get();
query = orderBy switch
{
nameof(Theme.Id) => orderByDir == "desc"
? query.OrderByDescending(x => x.Id)
: query.OrderBy(x => x.Id),
nameof(Theme.Name) => orderByDir == "desc"
? query.OrderByDescending(x => x.Name)
: query.OrderBy(x => x.Name),
nameof(Theme.Version) => orderByDir == "desc"
? query.OrderByDescending(x => x.Version)
: query.OrderBy(x => x.Version),
_ => query.OrderBy(x => x.Id)
};
if (!string.IsNullOrEmpty(filter))
{
query = query.Where(x =>
EF.Functions.ILike(x.Name, $"%{filter}%")
);
}
var totalCount = await query.CountAsync();
var items = await query
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToResponse()
.ToArrayAsync();
var mappedItems = items
.Select(ThemeMapper.ToResponse)
.ToArray();
return new PagedData<ThemeResponse>()
return new CountedData<ThemeResponse>()
{
CurrentPage = options.Page,
Items = mappedItems,
PageSize = options.PageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / options.PageSize
Items = items,
TotalCount = totalCount
};
}
[HttpGet("{id:int}")]
[Authorize(Policy = "permissions:admin.system.customisation.themes.read")]
public async Task<ThemeResponse> GetSingle([FromRoute] int id)
public async Task<ActionResult<ThemeResponse>> GetSingle([FromRoute] int id)
{
var theme = await ThemeRepository
.Get()
.AsNoTracking()
.ProjectToResponse()
.FirstOrDefaultAsync(t => t.Id == id);
if (theme == null)
throw new HttpApiException("Theme with this id not found", 404);
return Problem("Theme with this id not found", statusCode: 404);
return ThemeMapper.ToResponse(theme);
return theme;
}
[HttpPost]
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
public async Task<ThemeResponse> Create([FromBody] CreateThemeRequest request)
public async Task<ActionResult<ThemeResponse>> Create([FromBody] CreateThemeRequest request)
{
var theme = ThemeMapper.ToTheme(request);
var finalTheme = await ThemeRepository.Add(theme);
var finalTheme = await ThemeRepository.AddAsync(theme);
return ThemeMapper.ToResponse(finalTheme);
}
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
public async Task<ThemeResponse> Update([FromRoute] int id, [FromBody] UpdateThemeRequest request)
public async Task<ActionResult<ThemeResponse>> Update([FromRoute] int id, [FromBody] UpdateThemeRequest request)
{
var theme = await ThemeRepository
.Get()
.FirstOrDefaultAsync(t => t.Id == id);
if (theme == null)
throw new HttpApiException("Theme with this id not found", 404);
return Problem("Theme with this id not found", statusCode: 404);
// Disable all other enabled themes if we are enabling the current theme.
// This ensures only one theme is enabled at the time
@@ -98,29 +129,28 @@ public class ThemesController : Controller
foreach (var otherTheme in otherThemes)
otherTheme.IsEnabled = false;
await ThemeRepository.RunTransaction(set =>
{
set.UpdateRange(otherThemes);
});
await ThemeRepository.RunTransactionAsync(set => { set.UpdateRange(otherThemes); });
}
ThemeMapper.Merge(theme, request);
await ThemeRepository.Update(theme);
await ThemeRepository.UpdateAsync(theme);
return ThemeMapper.ToResponse(theme);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "permissions:admin.system.customisation.themes.write")]
public async Task Delete([FromRoute] int id)
public async Task<ActionResult> Delete([FromRoute] int id)
{
var theme = await ThemeRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (theme == null)
throw new HttpApiException("Theme with this id not found", 404);
return Problem("Theme with this id not found", statusCode: 404);
await ThemeRepository.Remove(theme);
await ThemeRepository.RemoveAsync(theme);
return NoContent();
}
}

View File

@@ -1,4 +1,3 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@@ -6,10 +5,10 @@ using Microsoft.Extensions.DependencyInjection;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Extended.Models;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Services;
using Moonlight.ApiServer.Mappers;
using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses.Admin.Users;
@@ -28,60 +27,78 @@ public class UsersController : Controller
[HttpGet]
[Authorize(Policy = "permissions:admin.users.get")]
public async Task<IPagedData<UserResponse>> Get([FromQuery] PagedOptions options)
public async Task<ActionResult<ICountedData<UserResponse>>> Get(
[FromQuery] int startIndex,
[FromQuery] int count,
[FromQuery] string? orderBy,
[FromQuery] string? filter,
[FromQuery] string orderByDir = "asc"
)
{
var count = await UserRepository.Get().CountAsync();
if (count > 100)
return Problem("You cannot fetch more items than 100 at a time", statusCode: 400);
var users = await UserRepository
.Get()
.OrderBy(x => x.Id)
.Skip(options.Page * options.PageSize)
.Take(options.PageSize)
IQueryable<User> query = UserRepository.Get();
query = orderBy switch
{
nameof(Database.Entities.User.Id) => orderByDir == "desc"
? query.OrderByDescending(x => x.Id)
: query.OrderBy(x => x.Id),
nameof(Database.Entities.User.Username) => orderByDir == "desc"
? query.OrderByDescending(x => x.Username)
: query.OrderBy(x => x.Username),
nameof(Database.Entities.User.Email) => orderByDir == "desc"
? query.OrderByDescending(x => x.Email)
: query.OrderBy(x => x.Email),
_ => query.OrderBy(x => x.Id)
};
if (!string.IsNullOrEmpty(filter))
{
query = query.Where(x =>
EF.Functions.ILike(x.Username, $"%{filter}%") ||
EF.Functions.ILike(x.Email, $"%{filter}%")
);
}
var totalCount = await query.CountAsync();
var items = await query
.Skip(startIndex)
.Take(count)
.AsNoTracking()
.ProjectToResponse()
.ToArrayAsync();
var mappedUsers = users
.Select(x => new UserResponse()
return new CountedData<UserResponse>()
{
Id = x.Id,
Email = x.Email,
Username = x.Username,
Permissions = x.Permissions
})
.ToArray();
return new PagedData<UserResponse>()
{
CurrentPage = options.Page,
Items = mappedUsers,
PageSize = options.PageSize,
TotalItems = count,
TotalPages = count == 0 ? 0 : (count - 1) / options.PageSize
Items = items,
TotalCount = totalCount
};
}
[HttpGet("{id}")]
[Authorize(Policy = "permissions:admin.users.get")]
public async Task<UserResponse> GetSingle(int id)
public async Task<ActionResult<UserResponse>> GetSingle(int id)
{
var user = await UserRepository
.Get()
.ProjectToResponse()
.FirstOrDefaultAsync(x => x.Id == id);
if (user == null)
throw new HttpApiException("No user with that id found", 404);
return Problem("No user with that id found", statusCode: 404);
return new UserResponse()
{
Id = user.Id,
Email = user.Email,
Username = user.Username,
Permissions = user.Permissions
};
return user;
}
[HttpPost]
[Authorize(Policy = "permissions:admin.users.create")]
public async Task<UserResponse> Create([FromBody] CreateUserRequest request)
public async Task<ActionResult<UserResponse>> Create([FromBody] CreateUserRequest request)
{
// Reformat values
request.Username = request.Username.ToLower().Trim();
@@ -89,10 +106,10 @@ public class UsersController : Controller
// Check for users with the same values
if (UserRepository.Get().Any(x => x.Username == request.Username))
throw new HttpApiException("A user with that username already exists", 400);
return Problem("A user with that username already exists", statusCode: 400);
if (UserRepository.Get().Any(x => x.Email == request.Email))
throw new HttpApiException("A user with that email address already exists", 400);
return Problem("A user with that email address already exists", statusCode: 400);
var hashedPassword = HashHelper.Hash(request.Password);
@@ -104,27 +121,21 @@ public class UsersController : Controller
Permissions = request.Permissions
};
var finalUser = await UserRepository.Add(user);
var finalUser = await UserRepository.AddAsync(user);
return new UserResponse()
{
Id = finalUser.Id,
Email = finalUser.Email,
Username = finalUser.Username,
Permissions = finalUser.Permissions
};
return UserMapper.ToResponse(finalUser);
}
[HttpPatch("{id}")]
[Authorize(Policy = "permissions:admin.users.update")]
public async Task<UserResponse> Update([FromRoute] int id, [FromBody] UpdateUserRequest request)
public async Task<ActionResult<UserResponse>> Update([FromRoute] int id, [FromBody] UpdateUserRequest request)
{
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (user == null)
throw new HttpApiException("No user with that id found", 404);
return Problem("No user with that id found", statusCode: 404);
// Reformat values
request.Username = request.Username.ToLower().Trim();
@@ -132,10 +143,10 @@ public class UsersController : Controller
// Check for users with the same values
if (UserRepository.Get().Any(x => x.Username == request.Username && x.Id != user.Id))
throw new HttpApiException("A user with that username already exists", 400);
return Problem("Another user with that username already exists", statusCode: 400);
if (UserRepository.Get().Any(x => x.Email == request.Email && x.Id != user.Id))
throw new HttpApiException("A user with that email address already exists", 400);
return Problem("Another user with that email address already exists", statusCode: 400);
// Perform hashing the password if required
if (!string.IsNullOrEmpty(request.Password))
@@ -153,27 +164,21 @@ public class UsersController : Controller
user.Email = request.Email;
user.Username = request.Username;
await UserRepository.Update(user);
await UserRepository.UpdateAsync(user);
return new UserResponse()
{
Id = user.Id,
Email = user.Email,
Username = user.Username,
Permissions = user.Permissions
};
return UserMapper.ToResponse(user);
}
[HttpDelete("{id}")]
[Authorize(Policy = "permissions:admin.users.delete")]
public async Task Delete([FromRoute] int id, [FromQuery] bool force = false)
public async Task<ActionResult> Delete([FromRoute] int id, [FromQuery] bool force = false)
{
var user = await UserRepository
.Get()
.FirstOrDefaultAsync(x => x.Id == id);
if (user == null)
throw new HttpApiException("No user with that id found", 404);
return Problem("No user with that id found", statusCode: 404);
var deletionService = HttpContext.RequestServices.GetRequiredService<UserDeletionService>();
@@ -182,9 +187,10 @@ public class UsersController : Controller
var validationResult = await deletionService.Validate(user);
if (!validationResult.IsAllowed)
throw new HttpApiException($"Unable to delete user", 400, validationResult.Reason);
return Problem("Unable to delete user", statusCode: 400, title: validationResult.Reason);
}
await deletionService.Delete(user, force);
return NoContent();
}
}

View File

@@ -180,7 +180,7 @@ public class LocalAuthController : Controller
Permissions = permissions
};
var finalUser = await UserRepository.Add(user);
var finalUser = await UserRepository.AddAsync(user);
return finalUser;
}

View File

@@ -0,0 +1,19 @@
using Moonlight.ApiServer.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
using Riok.Mapperly.Abstractions;
namespace Moonlight.ApiServer.Mappers;
[Mapper]
public static partial class ApiKeyMapper
{
// Mappers
public static partial ApiKeyResponse ToResponse(ApiKey apiKey);
public static partial ApiKey ToApiKey(CreateApiKeyRequest request);
public static partial void Merge([MappingTarget] ApiKey apiKey, UpdateApiKeyRequest request);
// EF Relations
public static partial IQueryable<ApiKeyResponse> ProjectToResponse(this IQueryable<ApiKey> apiKeys);
}

View File

@@ -8,7 +8,12 @@ namespace Moonlight.ApiServer.Mappers;
[Mapper]
public static partial class ThemeMapper
{
// Mappers
public static partial ThemeResponse ToResponse(Theme theme);
public static partial Theme ToTheme(CreateThemeRequest request);
public static partial void Merge([MappingTarget] Theme theme, UpdateThemeRequest request);
// EF Relations
public static partial IQueryable<ThemeResponse> ProjectToResponse(this IQueryable<Theme> themes);
}

View File

@@ -0,0 +1,15 @@
using Moonlight.ApiServer.Database.Entities;
using Moonlight.Shared.Http.Responses.Admin.Users;
using Riok.Mapperly.Abstractions;
namespace Moonlight.ApiServer.Mappers;
[Mapper]
public static partial class UserMapper
{
// Mappers
public static partial UserResponse ToResponse(User user);
// EF Relations
public static partial IQueryable<UserResponse> ProjectToResponse(this IQueryable<User> users);
}

View File

@@ -25,9 +25,10 @@
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.20"/>
<PackageReference Include="Hangfire.Core" Version="1.8.20"/>
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.9" />
<PackageReference Include="MoonCore" Version="1.9.7" />
<PackageReference Include="MoonCore.Extended" Version="1.3.7" />
<PackageReference Include="MoonCore" Version="1.9.9" />
<PackageReference Include="MoonCore.Extended" Version="1.3.8" />
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2"/>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.12.0-beta.1"/>

View File

@@ -80,7 +80,7 @@ public class UserAuthService
permissions = ["*"];
}
user = await UserRepository.Add(new User()
user = await UserRepository.AddAsync(new User()
{
Email = email,
TokenValidTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1),
@@ -94,7 +94,7 @@ public class UserAuthService
if (user.Username != username)
{
user.Username = username;
await UserRepository.Update(user);
await UserRepository.UpdateAsync(user);
}
// Enrich claims with required metadata

View File

@@ -35,8 +35,8 @@ public class UserDeletionService
public async Task Delete(User user, bool force)
{
foreach (var handler in Handlers)
await Delete(user, force);
await handler.Delete(user, force);
await UserRepository.Remove(user);
await UserRepository.RemoveAsync(user);
}
}

View File

@@ -24,9 +24,9 @@
<PackageReference Include="Blazor-ApexCharts" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
<PackageReference Include="MoonCore" Version="1.9.7" />
<PackageReference Include="MoonCore" Version="1.9.9" />
<PackageReference Include="MoonCore.Blazor" Version="1.3.1" />
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.1.9" />
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.2.2" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -17,21 +17,14 @@ public class ThemeService
ApiClient = apiClient;
}
public async Task<PagedData<ThemeResponse>> Get(int page, int pageSize)
{
return await ApiClient.GetJson<PagedData<ThemeResponse>>(
$"api/admin/system/customisation/themes?page={page}&pageSize={pageSize}"
);
}
public async Task<ThemeResponse> Get(int id)
public async Task<ThemeResponse> GetAsync(int id)
{
return await ApiClient.GetJson<ThemeResponse>(
$"api/admin/system/customisation/themes/{id}"
);
}
public async Task<ThemeResponse> Create(CreateThemeRequest request)
public async Task<ThemeResponse> CreateAsync(CreateThemeRequest request)
{
return await ApiClient.PostJson<ThemeResponse>(
"api/admin/system/customisation/themes",
@@ -39,7 +32,7 @@ public class ThemeService
);
}
public async Task<ThemeResponse> Update(int id, UpdateThemeRequest request)
public async Task<ThemeResponse> UpdateAsync(int id, UpdateThemeRequest request)
{
return await ApiClient.PatchJson<ThemeResponse>(
$"api/admin/system/customisation/themes/{id}",
@@ -47,7 +40,7 @@ public class ThemeService
);
}
public async Task Delete(int id)
public async Task DeleteAsync(int id)
{
await ApiClient.Delete(
$"api/admin/system/customisation/themes/{id}"

View File

@@ -1,518 +0,0 @@
!bg-base-100
!border-base-content/40
!border-none
!flex
!font-medium
!font-semibold
!h-2.5
!justify-between
!me-1.5
!ms-auto
!px-2.5
!rounded-full
!text-sm
!w-2.5
*:[grid-area:1/1]
*:first:rounded-tl-lg
*:last:rounded-tr-lg
-left-4
-ml-4
-translate-x-full
-translate-y-1/2
absolute
accordion
accordion-bordered
accordion-toggle
active
active-tab:bg-primary
active-tab:text-base-content
advance-select-menu
advance-select-option
advance-select-tag
advance-select-toggle
alert
alert-error
alert-outline
alert-soft
align-bottom
align-middle
animate-bounce
animate-ping
aria-[current='page']:text-bg-soft-primary
avatar
avatar-away-bottom
avatar-away-top
avatar-busy-bottom
avatar-busy-top
avatar-offline-bottom
avatar-offline-top
avatar-online-bottom
avatar-online-top
avatar-placeholder
badge
badge-error
badge-info
badge-outline
badge-primary
badge-soft
badge-success
bg-background
bg-background/60
bg-base-100
bg-base-150
bg-base-200
bg-base-200/50
bg-base-300
bg-base-300/45
bg-base-300/50
bg-base-300/60
bg-error
bg-info
bg-primary
bg-primary/5
bg-success
bg-transparent
bg-warning
block
blur
border
border-0
border-2
border-b
border-base-content
border-base-content/20
border-base-content/25
border-base-content/40
border-base-content/5
border-dashed
border-t
border-transparent
bottom-0
bottom-full
break-words
btn
btn-accent
btn-active
btn-circle
btn-disabled
btn-error
btn-info
btn-outline
btn-primary
btn-secondary
btn-sm
btn-soft
btn-square
btn-success
btn-text
btn-warning
card
card-alert
card-body
card-border
card-footer
card-header
card-title
carousel
carousel-body
carousel-next
carousel-prev
carousel-slide
chat
chat-avatar
chat-bubble
chat-footer
chat-header
chat-receiver
chat-sender
checkbox
checkbox-primary
checkbox-xs
col-span-1
collapse
combo-box-selected:block
combo-box-selected:dropdown-active
complete
container
contents
cursor-default
cursor-not-allowed
cursor-pointer
diff
disabled
divide-base-150/60
divide-y
drop-shadow
dropdown
dropdown-disabled
dropdown-item
dropdown-menu
dropdown-open:opacity-100
dropdown-open:rotate-180
dropdown-toggle
duration-300
duration-500
ease-in-out
ease-linear
end-3
file-upload-complete:progress-success
fill-black
filter
filter-reset
fixed
flex
flex-1
flex-col
flex-grow
flex-nowrap
flex-row
flex-shrink-0
flex-wrap
focus-visible:outline-none
focus-within:border-primary
focus:border-primary
focus:outline-1
focus:outline-none
focus:outline-primary
focus:ring-0
font-bold
font-inter
font-medium
font-normal
font-semibold
gap-0.5
gap-1
gap-1.5
gap-2
gap-3
gap-4
gap-5
gap-6
gap-x-1
gap-x-2
gap-x-3
gap-y-1
gap-y-3
grid
grid-cols-1
grid-flow-col
grow
grow-0
h-12
h-2
h-32
h-64
h-8
h-auto
h-full
h-screen
helper-text
hidden
hover:bg-primary/5
hover:bg-transparent
hover:text-base-content
hover:text-base-content/60
hover:text-primary
image-full
inline
inline-block
inline-flex
inline-grid
input
input-floating
input-floating-label
input-lg
input-md
input-sm
input-xl
inset-0
inset-y-0
inset-y-2
invisible
is-invalid
is-valid
isolate
italic
items-center
items-end
items-start
join
join-item
justify-between
justify-center
justify-end
justify-start
justify-stretch
label-text
leading-3
leading-3.5
leading-6
left-0
lg:bg-base-100/20
lg:flex
lg:gap-y-0
lg:grid-cols-2
lg:hidden
lg:justify-end
lg:justify-start
lg:min-w-0
lg:p-10
lg:pb-5
lg:pl-64
lg:pr-3.5
lg:pt-5
lg:ring-1
lg:ring-base-content/10
lg:rounded-lg
lg:shadow-xs
list-disc
list-inside
loading
loading-lg
loading-sm
loading-spinner
loading-xl
loading-xs
lowercase
m-10
mask
max-h-52
max-lg:flex-col
max-lg:hidden
max-w-7xl
max-w-80
max-w-full
max-w-lg
max-w-sm
max-w-xl
mb-0.5
mb-1
mb-2
mb-3
mb-4
mb-5
md:table-cell
md:text-3xl
me-1
me-1.5
me-2
me-2.5
me-5
menu
menu-active
menu-disabled
menu-dropdown
menu-dropdown-show
menu-focus
menu-horizontal
menu-title
min-h-0
min-h-svh
min-w-0
min-w-28
min-w-48
min-w-60
min-w-[100px]
ml-3
ml-4
modal
modal-content
modal-dialog
modal-middle
modal-title
mr-4
ms-1
ms-2
ms-3
mt-1
mt-1.5
mt-10
mt-12
mt-2
mt-2.5
mt-3
mt-4
mt-5
mt-8
mx-1
mx-auto
my-3
my-auto
opacity-0
opacity-100
open
origin-top-left
outline
outline-0
overflow-hidden
overflow-x-auto
overflow-y-auto
overlay-open:duration-50
overlay-open:opacity-100
p-0.5
p-1
p-2
p-3
p-4
p-5
p-6
p-8
pin-input
pin-input-underline
placeholder-base-content/60
pointer-events-auto
pointer-events-none
progress
progress-bar
progress-indeterminate
progress-primary
pt-0
pt-0.5
pt-3
px-1.5
px-2
px-2.5
px-3
px-4
px-5
py-0.5
py-1.5
py-2
py-2.5
py-6
radio
range
relative
resize
ring-0
ring-1
ring-white/10
rounded-box
rounded-field
rounded-full
rounded-lg
rounded-md
rounded-t-lg
row-active
row-hover
rtl:!mr-0
select
select-disabled:opacity-40
select-disabled:pointer-events-none
select-floating
select-floating-label
selected
selected:select-active
shadow-base-300/20
shadow-lg
shadow-xs
shrink-0
size-10
size-4
size-5
size-8
skeleton
skeleton-animated
sm:auto-cols-max
sm:flex
sm:items-center
sm:items-end
sm:justify-between
sm:justify-end
sm:max-w-2xl
sm:max-w-3xl
sm:max-w-4xl
sm:max-w-5xl
sm:max-w-6xl
sm:max-w-7xl
sm:max-w-lg
sm:max-w-md
sm:max-w-xl
sm:mb-0
sm:mt-5
sm:mt-6
sm:p-6
sm:py-2
sm:text-sm/5
space-x-1
space-y-1
space-y-4
sr-only
static
status
status-error
sticky
switch
tab
tab-active
table
table-pin-cols
table-pin-rows
tabs
tabs-bordered
tabs-lg
tabs-lifted
tabs-md
tabs-sm
tabs-xl
tabs-xs
text-2xl
text-4xl
text-accent
text-base
text-base-content
text-base-content/40
text-base-content/50
text-base-content/60
text-base-content/70
text-base-content/80
text-base/6
text-center
text-error
text-error-content
text-gray-400
text-info
text-info-content
text-left
text-lg
text-primary
text-primary-content
text-sm
text-sm/5
text-success
text-success-content
text-warning
text-warning-content
text-xl
text-xs
text-xs/5
textarea
textarea-floating
textarea-floating-label
theme-controller
tooltip
tooltip-content
top-0
top-1/2
top-full
transform
transition
transition-all
transition-opacity
translate-x-0
truncate
underline
uppercase
validate
w-0
w-0.5
w-12
w-4
w-56
w-64
w-fit
w-full
whitespace-nowrap
z-10
z-40
z-50

View File

@@ -44,6 +44,7 @@ animate-bounce
animate-ping
aria-[current='page']:text-bg-soft-primary
avatar
avatar-placeholder
badge
badge-error
badge-info
@@ -73,16 +74,23 @@ block
blur
border
border-0
border-1
border-2
border-b
border-b-2
border-b-base-content/20
border-base-content
border-base-content/20
border-base-content/25
border-base-content/40
border-base-content/5
border-dashed
border-error/30
border-info/30
border-success/30
border-t
border-transparent
border-warning/30
bottom-0
bottom-full
break-words
@@ -93,7 +101,6 @@ btn-circle
btn-disabled
btn-error
btn-info
btn-outline
btn-primary
btn-secondary
btn-sm
@@ -200,7 +207,7 @@ grid-cols-4
grid-flow-col
grow
grow-0
h-12
h-10
h-2
h-3
h-32
@@ -245,11 +252,9 @@ justify-between
justify-center
justify-end
justify-start
justify-stretch
label-text
leading-3
leading-3.5
leading-6
leading-none
left-0
lg:bg-base-100/20
@@ -287,6 +292,9 @@ mask
max-h-52
max-lg:flex-col
max-lg:hidden
max-md:flex-wrap
max-md:justify-center
max-sm:hidden
max-w-7xl
max-w-80
max-w-full
@@ -323,6 +331,7 @@ min-w-28
min-w-48
min-w-60
min-w-[100px]
min-w-full
min-w-sm
ml-3
ml-4
@@ -330,10 +339,10 @@ modal
modal-content
modal-dialog
modal-middle
modal-title
mr-4
ms-0.5
ms-1
ms-1.5
ms-2
ms-3
ms-auto
@@ -368,11 +377,13 @@ p-0.5
p-1
p-1.5
p-2
p-2.5
p-3
p-4
p-5
p-6
p-8
pb-1
pin-input
placeholder-base-content/60
pointer-events-auto
@@ -383,7 +394,9 @@ progress-indeterminate
progress-primary
pt-0
pt-0.5
pt-2
pt-3
px-0.5
px-1.5
px-2
px-2.5
@@ -418,6 +431,7 @@ select-disabled:opacity-40
select-disabled:pointer-events-none
select-floating
select-floating-label
select-sm
selected
selected:select-active
shadow-base-300/20
@@ -426,6 +440,7 @@ shadow-md
shadow-xs
shrink-0
size-10
size-12
size-4
size-5
size-8
@@ -448,11 +463,11 @@ sm:max-w-md
sm:max-w-xl
sm:mb-0
sm:mt-5
sm:mt-6
sm:p-6
sm:py-2
sm:text-sm/5
space-x-1
space-x-2.5
space-y-1
space-y-4
sr-only
@@ -495,6 +510,7 @@ text-left
text-lg
text-primary
text-primary-content
text-right
text-sm
text-sm/5
text-success
@@ -525,6 +541,7 @@ validate
w-0
w-0.5
w-12
w-13
w-4
w-56
w-64

View File

@@ -3,7 +3,9 @@
@using MoonCore.Helpers
@using MoonCore.Models
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
@using MoonCore.Blazor.FlyonUi.DataTables
@using MoonCore.Blazor.FlyonUi.Grid
@using MoonCore.Blazor.FlyonUi.Grid.Columns
@using MoonCore.Blazor.FlyonUi.Grid.ToolbarItems
@inject HttpApiClient ApiClient
@inject AlertService AlertService
@@ -47,48 +49,72 @@
</div>
</div>
<div class="mb-5 flex justify-end">
<a href="/admin/api/create" class="btn btn-primary">
Create
</a>
</div>
<DataTable @ref="Table" TItem="ApiKeyResponse">
<Configuration>
<Pagination TItem="ApiKeyResponse" ItemSource="LoadData" />
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.Id)" Name="Id"/>
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.Description)" Name="Description"/>
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.ExpiresAt)" Name="Expires at">
<ColumnTemplate>
@(Formatter.FormatDate(context.ExpiresAt.UtcDateTime))
</ColumnTemplate>
</DataTableColumn>
<DataTableColumn TItem="ApiKeyResponse">
<ColumnTemplate>
<DataGrid @ref="Grid"
TGridItem="ApiKeyResponse"
ItemsProvider="ItemsProvider"
EnableFiltering="true"
EnablePagination="true">
<PropertyColumn Field="x => x.Id" Sortable="true" />
<PropertyColumn Field="x => x.Description" />
<TemplateColumn Sortable="true" Title="Expires At">
<td>
@Formatter.FormatDate(context.ExpiresAt.UtcDateTime)
</td>
</TemplateColumn>
<TemplateColumn Sortable="true" Title="Created At">
<td>
@Formatter.FormatDate(context.CreatedAt.UtcDateTime)
</td>
</TemplateColumn>
<TemplateColumn>
<td>
<div class="flex justify-end">
<a href="/admin/api/@(context.Id)" class="text-primary mr-2 sm:mr-3">
<i class="icon-pencil text-base"></i>
</a>
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault
<a href="#" @onclick="() => DeleteAsync(context)" @onclick:preventDefault
class="text-error">
<i class="icon-trash text-base"></i>
</a>
</div>
</ColumnTemplate>
</DataTableColumn>
</Configuration>
</DataTable>
</td>
</TemplateColumn>
<TemplateToolbarItem>
<a href="/admin/api/create" class="btn btn-primary ms-1.5">
Create
</a>
</TemplateToolbarItem>
</DataGrid>
@code
{
private DataTable<ApiKeyResponse> Table;
private DataGrid<ApiKeyResponse> Grid;
private async Task<IPagedData<ApiKeyResponse>> LoadData(PaginationOptions options)
=> await ApiClient.GetJson<PagedData<ApiKeyResponse>>($"api/admin/apikeys?page={options.Page}&pageSize={options.PerPage}");
private async Task<DataGridItemResult<ApiKeyResponse>> ItemsProvider(DataGridItemRequest request)
{
var query = $"?startIndex={request.StartIndex}&count={request.Count}";
private async Task Delete(ApiKeyResponse apiKeyResponse)
if (!string.IsNullOrEmpty(request.SortColumn))
{
var dir = request.SortDirection == SortState.Descending ? "desc" : "asc";
query += $"&orderBy={request.SortColumn}&orderByDir={dir}";
}
if (!string.IsNullOrEmpty(request.Filter))
query += $"&filter={request.Filter}";
var data = await ApiClient.GetJson<CountedData<ApiKeyResponse>>($"api/admin/apikeys{query}");
return new()
{
Items = data.Items,
TotalCount = data.TotalCount
};
}
private async Task DeleteAsync(ApiKeyResponse apiKeyResponse)
{
await AlertService.ConfirmDanger(
"API Key deletion",
@@ -98,7 +124,7 @@
await ApiClient.Delete($"api/admin/apikeys/{apiKeyResponse.Id}");
await ToastService.Success("Successfully deleted api key");
await Table.Refresh();
await Grid.RefreshAsync();
}
);
}

View File

@@ -2,7 +2,9 @@
@using System.Text.Json
@using Microsoft.AspNetCore.Authorization
@using MoonCore.Blazor.FlyonUi.DataTables
@using MoonCore.Blazor.FlyonUi.Grid
@using MoonCore.Blazor.FlyonUi.Grid.Columns
@using MoonCore.Blazor.FlyonUi.Grid.ToolbarItems
@using MoonCore.Blazor.FlyonUi.Helpers
@using MoonCore.Helpers
@using MoonCore.Models
@@ -16,6 +18,7 @@
@inject ThemeService ThemeService
@inject AlertService AlertService
@inject ToastService ToastService
@inject HttpApiClient ApiClient
@inject DownloadService DownloadService
@inject ILogger<Index> Logger
@@ -25,23 +28,15 @@
Themes
</PageSeparator>
<div class="mt-5 flex justify-end">
<div>
<label for="import-theme" class="btn btn-info me-1">
<i class="icon-file-up"></i>
Import
</label>
<InputFile OnChange="Import" id="import-theme" class="hidden" multiple />
<a href="/admin/system/customisation/themes/create" class="btn btn-primary">Create</a>
</div>
</div>
<div class="my-8">
<DataGrid TGridItem="ThemeResponse"
ItemsProvider="ItemsProvider"
EnableFiltering="true"
EnablePagination="true">
<div class="my-2.5">
<DataTable @ref="Table" TItem="ThemeResponse">
<Configuration>
<DataTableColumn TItem="ThemeResponse" Field="@(x => x.Id)" Name="Id"/>
<DataTableColumn TItem="ThemeResponse" Field="@(x => x.Name)" Name="Name">
<ColumnTemplate>
<PropertyColumn Field="x => x.Id" Sortable="true" />
<TemplateColumn Title="Name" Sortable="true">
<td>
<div class="flex items-center">
@context.Name
@@ -50,12 +45,13 @@
<i class="icon-check text-success ms-2"></i>
}
</div>
</ColumnTemplate>
</DataTableColumn>
<DataTableColumn TItem="ThemeResponse" Field="@(x => x.Author)" Name="Author"/>
<DataTableColumn TItem="ThemeResponse" Field="@(x => x.Version)" Name="Version"/>
<DataTableColumn TItem="ThemeResponse">
<ColumnTemplate>
</td>
</TemplateColumn>
<PropertyColumn Field="x => x.Version" Sortable="true" />
<PropertyColumn Field="x => x.Author" />
<TemplateColumn>
<td>
<div class="flex justify-end">
@if (!string.IsNullOrEmpty(context.DonateUrl))
{
@@ -73,7 +69,7 @@
</a>
}
<a @onclick="() => Export(context)" @onclick:preventDefault href="#" class="flex items-center mr-2 sm:mr-3">
<a @onclick="() => ExportAsync(context)" @onclick:preventDefault href="#" class="flex items-center mr-2 sm:mr-3">
<i class="text-success icon-download me-1"></i>
<span class="text-success">Export</span>
</a>
@@ -82,15 +78,22 @@
<i class="icon-pencil text-primary"></i>
</a>
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault>
<a href="#" @onclick="() => DeleteAsync(context)" @onclick:preventDefault>
<i class="icon-trash text-error"></i>
</a>
</div>
</ColumnTemplate>
</DataTableColumn>
<Pagination TItem="ThemeResponse" PageSize="10" ItemSource="LoadItems"/>
</Configuration>
</DataTable>
</td>
</TemplateColumn>
<TemplateToolbarItem>
<label for="import-theme" class="btn btn-info me-1 ms-1.5">
<i class="icon-file-up"></i>
Import
</label>
<InputFile OnChange="ImportAsync" id="import-theme" class="hidden" multiple />
<a href="/admin/system/customisation/themes/create" class="btn btn-primary">Create</a>
</TemplateToolbarItem>
</DataGrid>
</div>
<PageSeparator Icon="icon-images">
@@ -100,12 +103,31 @@
@code
{
private DataTable<ThemeResponse> Table;
private DataGrid<ThemeResponse> Grid;
private async Task<IPagedData<ThemeResponse>> LoadItems(PaginationOptions options)
=> await ThemeService.Get(options.Page, options.PerPage);
private async Task<DataGridItemResult<ThemeResponse>> ItemsProvider(DataGridItemRequest request)
{
var query = $"?startIndex={request.StartIndex}&count={request.Count}";
private async Task Import(InputFileChangeEventArgs eventArgs)
if (!string.IsNullOrEmpty(request.SortColumn))
{
var dir = request.SortDirection == SortState.Descending ? "desc" : "asc";
query += $"&orderBy={request.SortColumn}&orderByDir={dir}";
}
if (!string.IsNullOrEmpty(request.Filter))
query += $"&filter={request.Filter}";
var data = await ApiClient.GetJson<CountedData<ThemeResponse>>($"api/admin/system/customisation/themes{query}");
return new()
{
Items = data.Items,
TotalCount = data.TotalCount
};
}
private async Task ImportAsync(InputFileChangeEventArgs eventArgs)
{
if(eventArgs.FileCount < 1)
return;
@@ -141,7 +163,7 @@
continue;
}
var theme = await ThemeService.Create(new CreateThemeRequest()
var theme = await ThemeService.CreateAsync(new CreateThemeRequest()
{
Name = themeTransfer.Name,
Author = themeTransfer.Author,
@@ -153,7 +175,7 @@
await ToastService.Success("Successfully imported theme", theme.Name);
await Table.Refresh();
await Grid.RefreshAsync();
}
catch (Exception e)
{
@@ -162,7 +184,7 @@
}
}
private async Task Export(ThemeResponse theme)
private async Task ExportAsync(ThemeResponse theme)
{
var transfer = new ThemeTransferModel()
{
@@ -184,17 +206,17 @@
await DownloadService.Download(fileName, json);
}
private async Task Delete(ThemeResponse response)
private async Task DeleteAsync(ThemeResponse response)
{
await AlertService.ConfirmDanger(
"Theme deletion",
$"Do you really want to delete the theme: {response.Name}",
async () =>
{
await ThemeService.Delete(response.Id);
await ThemeService.DeleteAsync(response.Id);
await ToastService.Success("Successfully deleted theme");
await Table.Refresh();
await Grid.RefreshAsync();
}
);
}

View File

@@ -58,7 +58,7 @@
private async Task OnValidSubmit()
{
await ThemeService.Create(Request);
await ThemeService.CreateAsync(Request);
await ToastService.Success("Successfully created theme");
NavigationManager.NavigateTo("/admin/system/customisation");

View File

@@ -82,7 +82,7 @@
private async Task Load(LazyLoader _)
{
Response = await ThemeService.Get(Id);
Response = await ThemeService.GetAsync(Id);
Request = new()
{
@@ -98,7 +98,7 @@
private async Task OnValidSubmit()
{
await ThemeService.Update(Id, Request);
await ThemeService.UpdateAsync(Id, Request);
await ToastService.Success("Successfully updated theme");
Navigation.NavigateTo("/admin/system/customisation");

View File

@@ -96,7 +96,7 @@
.ToDictionary(x => x, _ => true);
}
private async Task GenerateDiagnose(WButton _)
private async Task GenerateDiagnose(WButton button)
{
var request = new GenerateDiagnoseRequest();

View File

@@ -3,13 +3,14 @@
@using MoonCore.Helpers
@using MoonCore.Models
@using Moonlight.Shared.Http.Responses.Admin.Users
@using MoonCore.Blazor.FlyonUi.DataTables
@using MoonCore.Blazor.FlyonUi.Grid
@using MoonCore.Blazor.FlyonUi.Grid.Columns
@inject HttpApiClient ApiClient
@inject AlertService AlertService
@inject ToastService ToastService
<div class="mb-5">
<div class="mb-8">
<PageHeader Title="Users">
<a href="/admin/users/create" class="btn btn-primary">
Create
@@ -17,37 +18,57 @@
</PageHeader>
</div>
<DataTable @ref="Table" TItem="UserResponse">
<Configuration>
<Pagination TItem="UserResponse" ItemSource="LoadData" />
<DataGrid @ref="Grid"
TGridItem="UserResponse"
ItemsProvider="ItemsProvider"
EnableFiltering="true"
EnablePagination="true">
<PropertyColumn Field="x => x.Id" Sortable="true" />
<PropertyColumn Field="x => x.Username" Sortable="true" />
<PropertyColumn Field="x => x.Email" Sortable="true" />
<DataTableColumn TItem="UserResponse" Field="@(x => x.Id)" Name="Id"/>
<DataTableColumn TItem="UserResponse" Field="@(x => x.Username)" Name="Username"/>
<DataTableColumn TItem="UserResponse" Field="@(x => x.Email)" Name="Email"/>
<DataTableColumn TItem="UserResponse">
<ColumnTemplate>
<TemplateColumn>
<td>
<div class="flex justify-end">
<a href="/admin/users/@(context.Id)" class="mr-2 sm:mr-3">
<i class="icon-pencil text-primary"></i>
</a>
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault>
<a href="#" @onclick="() => DeleteAsync(context)" @onclick:preventDefault>
<i class="icon-trash text-error"></i>
</a>
</div>
</ColumnTemplate>
</DataTableColumn>
</Configuration>
</DataTable>
</td>
</TemplateColumn>
</DataGrid>
@code
{
private DataTable<UserResponse> Table;
private DataGrid<UserResponse> Grid;
private async Task<IPagedData<UserResponse>> LoadData(PaginationOptions options)
=> await ApiClient.GetJson<PagedData<UserResponse>>($"api/admin/users?page={options.Page}&pageSize={options.PerPage}");
private async Task<DataGridItemResult<UserResponse>> ItemsProvider(DataGridItemRequest request)
{
var query = $"?startIndex={request.StartIndex}&count={request.Count}";
private async Task Delete(UserResponse response)
if (!string.IsNullOrEmpty(request.SortColumn))
{
var dir = request.SortDirection == SortState.Descending ? "desc" : "asc";
query += $"&orderBy={request.SortColumn}&orderByDir={dir}";
}
if (!string.IsNullOrEmpty(request.Filter))
query += $"&filter={request.Filter}";
var data = await ApiClient.GetJson<CountedData<UserResponse>>($"api/admin/users{query}");
return new()
{
Items = data.Items,
TotalCount = data.TotalCount
};
}
private async Task DeleteAsync(UserResponse response)
{
await AlertService.ConfirmDanger(
"User deletion",
@@ -57,7 +78,7 @@
await ApiClient.Delete($"api/admin/users/{response.Id}");
await ToastService.Success("Successfully deleted user");
await Table.Refresh();
await Grid.RefreshAsync();
}
);
}

View File

@@ -6,4 +6,5 @@ public class ApiKeyResponse
public string Description { get; set; }
public string[] Permissions { get; set; } = [];
public DateTimeOffset ExpiresAt { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}