diff --git a/HSB.sln b/HSB.sln index cf96d98a..4e072e68 100644 --- a/HSB.sln +++ b/HSB.sln @@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HSB.CSS", "src\libs\css\HSB EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HSB.DataService", "src\data-service\HSB.DataService.csproj", "{87E5B721-F9FD-485F-A393-6C28EAB50BE7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HSB.CSS.API", "src\api-css\HSB.CSS.API.csproj", "{54C182FA-0B79-487E-92F9-7EB0D7164DCC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -64,6 +66,10 @@ Global {87E5B721-F9FD-485F-A393-6C28EAB50BE7}.Debug|Any CPU.Build.0 = Debug|Any CPU {87E5B721-F9FD-485F-A393-6C28EAB50BE7}.Release|Any CPU.ActiveCfg = Release|Any CPU {87E5B721-F9FD-485F-A393-6C28EAB50BE7}.Release|Any CPU.Build.0 = Release|Any CPU + {54C182FA-0B79-487E-92F9-7EB0D7164DCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54C182FA-0B79-487E-92F9-7EB0D7164DCC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54C182FA-0B79-487E-92F9-7EB0D7164DCC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54C182FA-0B79-487E-92F9-7EB0D7164DCC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {57BA1694-AD4C-4DEE-8D6B-144DE51DE27B} = {EF08BB60-A463-4B2B-8413-A70292255338} @@ -75,5 +81,6 @@ Global {68EB92A0-8809-41CA-A27C-71FD8A05F82A} = {57BA1694-AD4C-4DEE-8D6B-144DE51DE27B} {B54E3181-664D-4974-9E95-77CED12D2239} = {57BA1694-AD4C-4DEE-8D6B-144DE51DE27B} {87E5B721-F9FD-485F-A393-6C28EAB50BE7} = {EF08BB60-A463-4B2B-8413-A70292255338} + {54C182FA-0B79-487E-92F9-7EB0D7164DCC} = {EF08BB60-A463-4B2B-8413-A70292255338} EndGlobalSection EndGlobal diff --git a/src/api-css/Controllers/RoleMappingsController.cs b/src/api-css/Controllers/RoleMappingsController.cs index e22d6e8d..df37a688 100644 --- a/src/api-css/Controllers/RoleMappingsController.cs +++ b/src/api-css/Controllers/RoleMappingsController.cs @@ -18,51 +18,51 @@ namespace HSB.CSS.API.Controllers; [Route("v{version:apiVersion}/integrations/{integrationId}/{environment}/user-role-mappings")] public class RoleMappingsController : ControllerBase { - #region Variables - private readonly IKeycloakService _service; - private readonly CssOptions _options; - #endregion + #region Variables + private readonly IKeycloakService _service; + private readonly CssOptions _options; + #endregion - #region Constructors - /// - /// - /// - /// - /// - public RoleMappingsController(IKeycloakService service, IOptions options) - { - _service = service; - _options = options.Value; - } - #endregion - - #region Endpoints - /// - /// - /// - /// - [HttpGet] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.UnprocessableEntity)] - [SwaggerOperation(Tags = new[] { "RoleMappings" })] - public async Task GetUserRoleMappingsAsync(string? roleName, string? username) - { - var clientId = _options.ClientId ?? throw new ConfigurationException("Keycloak 'ClientId' is not configured."); + #region Constructors + /// + /// + /// + /// + /// + public RoleMappingsController(IKeycloakService service, IOptions options) + { + _service = service; + _options = options.Value; + } + #endregion - if (!String.IsNullOrWhiteSpace(username)) + #region Endpoints + /// + /// + /// + /// + [HttpGet] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] + [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.NotFound)] + [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.UnprocessableEntity)] + [SwaggerOperation(Tags = new[] { "RoleMappings" })] + public async Task GetUserRoleMappingsAsync(string? roleName, string? username) { - var users = await _service.GetUsersAsync(0, 10, new HSB.Keycloak.UserFilter() { Username = username, Exact = true }); - if (users.Length == 0) return NotFound(new ErrorResponseModel()); + var clientId = _options.ClientId ?? throw new ConfigurationException("Keycloak 'ClientId' is not configured."); + + if (!String.IsNullOrWhiteSpace(username)) + { + var users = await _service.GetUsersAsync(0, 10, new HSB.Keycloak.UserFilter() { Username = username, Exact = true }); + if (users.Length == 0) return NotFound(new ErrorResponseModel()); - var user = users.First(); - var roles = await _service.GetUserClientRolesAsync(user.Id, clientId); + var user = users.First(); + var roles = await _service.GetUserClientRolesAsync(user.Id, clientId); - return new JsonResult(new UserRoleResponseModel() - { - Users = new[] { + return new JsonResult(new UserRoleResponseModel() + { + Users = new[] { new UserModel() { Username = user.Username ?? "", Email = user.Email ?? "", @@ -71,66 +71,66 @@ public async Task GetUserRoleMappingsAsync(string? roleName, stri Attributes = user.Attributes ?? new Dictionary() } }, - Roles = roles.Select(r => new RoleModel(r.Name ?? "", r.Composite)).ToArray() - }); - } - else if (!String.IsNullOrWhiteSpace(roleName)) - { - var role = await _service.GetRoleAsync(clientId, roleName); - if (role == null) return NotFound(new ErrorResponseModel()); + Roles = roles.Select(r => new RoleModel(r.Name ?? "", r.Composite)).ToArray() + }); + } + else if (!String.IsNullOrWhiteSpace(roleName)) + { + var role = await _service.GetRoleAsync(clientId, roleName); + if (role == null) return NotFound(new ErrorResponseModel()); - var users = await _service.GetRoleMembersAsync(clientId, roleName, 0, 100); + var users = await _service.GetRoleMembersAsync(clientId, roleName, 0, 100); - return new JsonResult(new UserRoleResponseModel() - { - Users = users.Select(u => new UserModel() - { - Username = u.Username ?? "", - Email = u.Email ?? "", - FirstName = u.FirstName ?? "", - LastName = u.LastName ?? "", - Attributes = u.Attributes ?? new Dictionary() - }).ToArray(), - Roles = new[] { + return new JsonResult(new UserRoleResponseModel() + { + Users = users.Select(u => new UserModel() + { + Username = u.Username ?? "", + Email = u.Email ?? "", + FirstName = u.FirstName ?? "", + LastName = u.LastName ?? "", + Attributes = u.Attributes ?? new Dictionary() + }).ToArray(), + Roles = new[] { new RoleModel(role.Name ?? "", role.Composite) } - }); + }); + } + return BadRequest(new ErrorResponseModel()); } - return BadRequest(new ErrorResponseModel()); - } - /// - /// - /// - /// - [HttpPost] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(typeof(UserRoleResponseModel), (int)HttpStatusCode.Created)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.UnprocessableEntity)] - [SwaggerOperation(Tags = new[] { "RoleMappings" })] - public async Task UpdateUserRoleMappingsAsync(UserRoleModel mapping, ApiVersion version) - { - var clientId = _options.ClientId ?? throw new ConfigurationException("Keycloak 'ClientId' is not configured."); + /// + /// + /// + /// + [HttpPost] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(typeof(UserRoleResponseModel), (int)HttpStatusCode.Created)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] + [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.NotFound)] + [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.UnprocessableEntity)] + [SwaggerOperation(Tags = new[] { "RoleMappings" })] + public async Task UpdateUserRoleMappingsAsync(UserRoleModel mapping, ApiVersion version) + { + var clientId = _options.ClientId ?? throw new ConfigurationException("Keycloak 'ClientId' is not configured."); - var users = await _service.GetUsersAsync(0, 10, new Keycloak.UserFilter() { Username = mapping.Username }); - if (users.Length == 0) return NotFound(new ErrorResponseModel()); - else if (users.Length > 1) return BadRequest(new ErrorResponseModel()); + var users = await _service.GetUsersAsync(0, 10, new Keycloak.UserFilter() { Username = mapping.Username, Exact = true }); + if (users.Length == 0) return NotFound(new ErrorResponseModel("User does not exist")); + else if (users.Length > 1) return BadRequest(new ErrorResponseModel("User information matches more than one account")); - var user = users.First(); - var role = await _service.GetRoleAsync(clientId, mapping.RoleName); - if (role == null) return NotFound(new ErrorResponseModel()); + var user = users.First(); + var role = await _service.GetRoleAsync(clientId, mapping.RoleName); + if (role == null) return NotFound(new ErrorResponseModel($"Role does not exist: {mapping.RoleName}")); - if (mapping.Operation.Value == UserRoleOperation.Add.Value) - { - await _service.AddUserClientRolesAsync(user.Id, clientId, new[] { role }); + if (mapping.Operation.Value == UserRoleOperation.Add.Value) + { + await _service.AddUserClientRolesAsync(user.Id, clientId, new[] { role }); - var roles = await _service.GetUserClientRolesAsync(user.Id, clientId); - var model = new UserRoleResponseModel() - { - Users = new[] { + var roles = await _service.GetUserClientRolesAsync(user.Id, clientId); + var model = new UserRoleResponseModel() + { + Users = new[] { new UserModel() { Username = user.Username ?? "", Email = user.Email ?? "", @@ -139,23 +139,23 @@ public async Task UpdateUserRoleMappingsAsync(UserRoleModel mappi Attributes = user.Attributes ?? new Dictionary() } }, - Roles = roles.Select(r => new RoleModel(r.Name ?? "", r.Composite)).ToArray() - }; - return CreatedAtAction("GetUserRoleMappings", new - { - username = mapping.Username, - version = version.ToString(), - integrationId = this.RouteData.Values["integrationId"], - environment = this.RouteData.Values["environment"] - }, model); - } - else if (mapping.Operation.Value == UserRoleOperation.Delete.Value) - { - await _service.RemoveUserClientRolesAsync(user.Id, clientId, new[] { role }); - return NoContent(); - } + Roles = roles.Select(r => new RoleModel(r.Name ?? "", r.Composite)).ToArray() + }; + return CreatedAtAction("GetUserRoleMappings", new + { + username = mapping.Username, + version = version.ToString(), + integrationId = this.RouteData.Values["integrationId"], + environment = this.RouteData.Values["environment"] + }, model); + } + else if (mapping.Operation.Value == UserRoleOperation.Delete.Value) + { + await _service.RemoveUserClientRolesAsync(user.Id, clientId, new[] { role }); + return NoContent(); + } - return BadRequest(new ErrorResponseModel()); - } - #endregion + return BadRequest(new ErrorResponseModel($"Operation specified is invalid: {mapping.Operation.Value}")); + } + #endregion } diff --git a/src/api/Areas/Admin/Controllers/UserController.cs b/src/api/Areas/Admin/Controllers/UserController.cs index 5c2e7bbd..5c50d0e9 100644 --- a/src/api/Areas/Admin/Controllers/UserController.cs +++ b/src/api/Areas/Admin/Controllers/UserController.cs @@ -27,6 +27,7 @@ public class UserController : ControllerBase { #region Variables private readonly IUserService _userService; + private readonly IGroupService _groupService; private readonly ICssHelper _cssHelper; private readonly JsonSerializerOptions _serializerOptions; #endregion @@ -36,14 +37,17 @@ public class UserController : ControllerBase /// Creates a new instance of a UserController object, initializes with specified parameters. /// /// + /// /// /// public UserController( IUserService userService, + IGroupService groupService, ICssHelper cssHelper, IOptions serializerOptions) { _userService = userService; + _groupService = groupService; _cssHelper = cssHelper; _serializerOptions = serializerOptions.Value; } @@ -63,7 +67,7 @@ public IActionResult Find() var uri = new Uri(this.Request.GetDisplayUrl()); var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query); var filter = new HSB.Models.Filters.UserFilter(query); - var result = _userService.Find(filter.GeneratePredicate(), filter.Sort); + var result = _userService.Find(filter); return new JsonResult(result.Select(u => new UserModel(u)).ToArray()); } @@ -80,9 +84,7 @@ public IActionResult Find() public IActionResult GetForId(int id) { var result = _userService.FindForId(id); - if (result == null) return new NoContentResult(); - return new JsonResult(new UserModel(result)); } @@ -120,9 +122,18 @@ public IActionResult Add(UserModel model) public async Task UpdateAsync(UserModel model) { var entry = _userService.Update((Entities.User)model); - var roles = entry.Entity.Groups.SelectMany(g => g.Roles.Select(r => r.Name)).Distinct().ToArray(); - await _cssHelper.UpdateUserRolesAsync(model.Key.ToString(), roles); + // Fetch groups from database to get roles associated with them. + var roles = new List(); + foreach (var group in entry.Entity.GroupsManyToMany) + { + var entity = _groupService.FindForId(group.GroupId); + if (entity != null) + roles.AddRange(entity.RolesManyToMany.Select(r => r.Role!.Name)); + } + roles = roles.Distinct().ToList(); + + await _cssHelper.UpdateUserRolesAsync(model.Key.ToString(), roles.ToArray()); _userService.CommitTransaction(); return new JsonResult(new UserModel(entry.Entity)); } diff --git a/src/api/Controllers/AuthController.cs b/src/api/Controllers/AuthController.cs index 742c8d71..4da1d980 100644 --- a/src/api/Controllers/AuthController.cs +++ b/src/api/Controllers/AuthController.cs @@ -24,7 +24,6 @@ public class AuthController : ControllerBase { #region Variables private readonly ICssHelper _cssHelper; - private readonly JsonSerializerOptions _serializerOptions; #endregion #region Constructors @@ -32,11 +31,9 @@ public class AuthController : ControllerBase /// Creates a new instance of a AuthController object, initializes with specified parameters. /// /// - /// - public AuthController(ICssHelper cssHelper, IOptions serializerOptions) + public AuthController(ICssHelper cssHelper) { _cssHelper = cssHelper; - _serializerOptions = serializerOptions.Value; } #endregion @@ -54,7 +51,7 @@ public AuthController(ICssHelper cssHelper, IOptions seri public async Task UserInfoAsync() { var user = await _cssHelper.ActivateAsync(this.User); - return new JsonResult(new PrincipalModel(this.User, user, _serializerOptions)); + return new JsonResult(new PrincipalModel(this.User, user)); } #endregion } diff --git a/src/api/Models/Auth/PrincipalModel.cs b/src/api/Models/Auth/PrincipalModel.cs index d346840d..3e33e30a 100644 --- a/src/api/Models/Auth/PrincipalModel.cs +++ b/src/api/Models/Auth/PrincipalModel.cs @@ -14,7 +14,7 @@ public class PrincipalModel /// /// get/set - Primary key to identify user. /// - public long Id { get; set; } + public int Id { get; set; } /// /// get/set - Unique key to link to Keycloak. @@ -76,6 +76,10 @@ public class PrincipalModel /// public JsonDocument Preferences { get; set; } = JsonDocument.Parse("{}"); + /// + /// get/set - The current row version. + /// + public long? Version { get; set; } #endregion #region Constructors @@ -89,9 +93,8 @@ public PrincipalModel() { } /// /// /// - /// - public PrincipalModel(ClaimsPrincipal principal, User? user, JsonSerializerOptions options) + public PrincipalModel(ClaimsPrincipal principal, User? user) { this.Id = user?.Id ?? 0; this.Key = principal.GetKey(); @@ -105,6 +108,7 @@ public PrincipalModel(ClaimsPrincipal principal, User? user, JsonSerializerOptio this.Preferences = user?.Preferences ?? JsonDocument.Parse("{}"); this.Groups = user?.Groups.Select(g => g.Name).Distinct() ?? Array.Empty(); this.Roles = user?.Groups.SelectMany(g => g.Roles).Select(r => r.Name).Distinct() ?? principal.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value); + this.Version = user?.Version; } #endregion } diff --git a/src/dashboard/src/app/api/admin/users/[id]/route.ts b/src/dashboard/src/app/api/admin/users/[id]/route.ts index a75616cb..6cc40df9 100644 --- a/src/dashboard/src/app/api/admin/users/[id]/route.ts +++ b/src/dashboard/src/app/api/admin/users/[id]/route.ts @@ -2,7 +2,7 @@ import { dispatch } from '@/app/api/utils'; export async function GET(req: Request, context: { params: any }) { const url = new URL(req.url); - return await dispatch(`/v1/system/admin/users/${context.params.id}`); + return await dispatch(`/v1/admin/users/${context.params.id}`); } export async function PUT(req: Request, context: { params: any }) { diff --git a/src/dashboard/src/app/hsb/admin/users/page.tsx b/src/dashboard/src/app/hsb/admin/users/page.tsx index 6b08f12b..fb270662 100644 --- a/src/dashboard/src/app/hsb/admin/users/page.tsx +++ b/src/dashboard/src/app/hsb/admin/users/page.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { IUserForm } from './IUserForm'; export default function Page() { - const { isReady: isReadyUsers, users } = useUsers(); + const { isReady: isReadyUsers, users } = useUsers({ includeGroups: true }); const { isReady: isReadyGroups, groups, options: groupOptions } = useGroups(); const { update: updateUser } = useApiUsers(); diff --git a/src/dashboard/src/app/hsb/dashboard/Page.module.scss b/src/dashboard/src/app/hsb/dashboard/Page.module.scss new file mode 100644 index 00000000..dd5bf4ef --- /dev/null +++ b/src/dashboard/src/app/hsb/dashboard/Page.module.scss @@ -0,0 +1,11 @@ +@import '@/styles/utils.scss'; + +.page { + >div:first-child { + gap: 2rem; + + >div { + flex: 1; + } + } +} \ No newline at end of file diff --git a/src/dashboard/src/app/hsb/dashboard/page.tsx b/src/dashboard/src/app/hsb/dashboard/page.tsx index 1f90efbc..0b8ccfbc 100644 --- a/src/dashboard/src/app/hsb/dashboard/page.tsx +++ b/src/dashboard/src/app/hsb/dashboard/page.tsx @@ -1,10 +1,19 @@ -import { AllOrgDonutChart } from '@/components/allOrgDonutChart'; +import { Col, Row } from '@/components'; +import { + AllOrgDonutChart, + AllocationByStorageVolumeChart, + UnusedSpaceChart, +} from '@/components/charts'; +import styles from './Page.module.scss'; export default function Page() { return ( - <> - HSB Dashboard - - + + + + + + + ); } diff --git a/src/dashboard/src/components/allOrgDonutChart/Chart.tsx b/src/dashboard/src/components/allOrgDonutChart/Chart.tsx deleted file mode 100644 index 9e8b816e..00000000 --- a/src/dashboard/src/components/allOrgDonutChart/Chart.tsx +++ /dev/null @@ -1,91 +0,0 @@ -'use client'; - -import React from 'react'; -import styles from './Chart.module.scss'; -import { Button } from '@/components/buttons'; -import { Doughnut } from 'react-chartjs-2'; -import { Chart as ChartJS, ArcElement, Tooltip } from 'chart.js'; - -ChartJS.register(ArcElement, Tooltip); - -// Data for the Donut Chart with specified colors -const data = { - labels: ['Unused', 'Used', 'Allocated'], // Labels to match ring order - datasets: [ - { - label: 'Unused', // Outer ring - data: [25, 0, 0], // Represents 25% of the ring - backgroundColor: ['#C4C7CA'], - borderColor: ['#C4C7CA'], - circumference: 90, // Quarter of the circle - }, - { - label: 'Used', // Middle ring - data: [75, 0, 0], // Represents 75% of the ring - backgroundColor: ['#003366'], - borderColor: ['#003366'], - circumference: 270, // Three quarters of the circle - }, - { - label: 'Allocated', // Inner ring - data: [100, 0, 0], // Represents full 100% of the ring - backgroundColor: ['#DF9901'], - borderColor: ['#DF9901'], - }, - ], -}; - -export const AllOrgDonutChart: React.FC = () => { - return ( -
-

All Organizations

-
-
-

Totals for 6 organizations

-
-

- Allocated 50 TB -

-
-
-

- Used 50 TB -

-
-
-

- Unused 50 TB -

-
-
-
- -

50 TB Total

-
-
- -
- ); -}; diff --git a/src/dashboard/src/components/allOrgDonutChart/index.ts b/src/dashboard/src/components/allOrgDonutChart/index.ts deleted file mode 100644 index dfe4f025..00000000 --- a/src/dashboard/src/components/allOrgDonutChart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Chart'; \ No newline at end of file diff --git a/src/dashboard/src/components/allOrgDonutChart/Chart.module.scss b/src/dashboard/src/components/charts/allOrgDonut/AllOrgDonutChart.module.scss similarity index 100% rename from src/dashboard/src/components/allOrgDonutChart/Chart.module.scss rename to src/dashboard/src/components/charts/allOrgDonut/AllOrgDonutChart.module.scss diff --git a/src/dashboard/src/components/charts/allOrgDonut/AllOrgDonutChart.tsx b/src/dashboard/src/components/charts/allOrgDonut/AllOrgDonutChart.tsx new file mode 100644 index 00000000..b7601f37 --- /dev/null +++ b/src/dashboard/src/components/charts/allOrgDonut/AllOrgDonutChart.tsx @@ -0,0 +1,147 @@ +'use client'; + +import { Button } from '@/components/buttons'; +import { useFiltered } from '@/store'; +import { convertToStorageSize } from '@/utils'; +import { ArcElement, Chart as ChartJS, Tooltip } from 'chart.js'; +import React from 'react'; +import { Doughnut } from 'react-chartjs-2'; +import styles from './AllOrgDonutChart.module.scss'; +import { IStats } from './IStats'; + +ChartJS.register(ArcElement, Tooltip); + +// Data for the Donut Chart with specified colors + +const defaultData = { + space: '0 TB', + used: '0 TB', + available: '0 TB', + chart: { + labels: ['Unused', 'Used', 'Allocated'], // Labels to match ring order + datasets: [ + { + label: 'Unused', // Outer ring + data: [0, 0, 0], // Represents 25% of the ring + backgroundColor: ['#C4C7CA'], + borderColor: ['#C4C7CA'], + circumference: 360, // Quarter of the circle + }, + { + label: 'Used', // Middle ring + data: [0, 0, 0], // Represents 75% of the ring + backgroundColor: ['#003366'], + borderColor: ['#003366'], + circumference: 360, // Three quarters of the circle + }, + { + label: 'Allocated', // Inner ring + data: [0, 0, 0], // Represents full 100% of the ring + backgroundColor: ['#DF9901'], + borderColor: ['#DF9901'], + }, + ], + }, +}; + +export const AllOrgDonutChart: React.FC = () => { + const organization = useFiltered((state) => state.organization); + const organizations = useFiltered((state) => state.organizations); + const servers = useFiltered((state) => state.serverItems); + + const [data, setData] = React.useState(defaultData); + + React.useEffect(() => { + if (servers.length) { + const space = servers.map((si) => si.capacity!).reduce((a, b) => (a ?? 0) + (b ?? 0)); + const available = servers + .map((si) => si.availableSpace!) + .reduce((a, b) => (a ?? 0) + (b ?? 0)); + const used = space - available; + const usedPer = (used / space) * 100; + const availablePer = (available / space) * 100; + setData((data) => ({ + space: convertToStorageSize(space, 'MB', 'TB', navigator.language, { formula: Math.trunc }), + used: convertToStorageSize(used, 'MB', 'TB', navigator.language, { + formula: Math.trunc, + }), + available: convertToStorageSize(available, 'MB', 'TB', navigator.language, { + formula: Math.trunc, + }), + chart: { + ...data.chart, + datasets: [ + { + ...data.chart.datasets[0], + data: [usedPer, 0, 0], + circumference: (360 * usedPer) / 100, + }, + { + ...data.chart.datasets[1], + data: [availablePer, 0, 0], + circumference: (360 * availablePer) / 100, + }, + { ...data.chart.datasets[2], data: [100, 0, 0] }, + ], + }, + })); + } else { + setData(defaultData); + } + }, [servers]); + + return ( +
+

All Organizations

+
+
+

Totals for {organization ? 1 : organizations.length} organizations

+
+

+ Allocated {data.space} +

+
+
+

+ Used {data.used} +

+
+
+

+ Unused {data.available} +

+
+
+
+ +

+ {data.space} Total +

+
+
+ +
+ ); +}; diff --git a/src/dashboard/src/components/charts/allOrgDonut/IStats.ts b/src/dashboard/src/components/charts/allOrgDonut/IStats.ts new file mode 100644 index 00000000..5384f879 --- /dev/null +++ b/src/dashboard/src/components/charts/allOrgDonut/IStats.ts @@ -0,0 +1,8 @@ +import { ChartData } from 'chart.js'; + +export interface IStats { + space: string; + used: string; + available: string; + chart: ChartData<'doughnut', number[], string>; +} diff --git a/src/dashboard/src/components/charts/allOrgDonut/index.ts b/src/dashboard/src/components/charts/allOrgDonut/index.ts new file mode 100644 index 00000000..05c5578a --- /dev/null +++ b/src/dashboard/src/components/charts/allOrgDonut/index.ts @@ -0,0 +1 @@ +export * from './AllOrgDonutChart'; diff --git a/src/dashboard/src/components/charts/allocationByStorageVolume/AllocationByStorageVolumeChart.module.scss b/src/dashboard/src/components/charts/allocationByStorageVolume/AllocationByStorageVolumeChart.module.scss new file mode 100644 index 00000000..0873c922 --- /dev/null +++ b/src/dashboard/src/components/charts/allocationByStorageVolume/AllocationByStorageVolumeChart.module.scss @@ -0,0 +1,9 @@ +@import '@/styles/utils.scss'; + +.panel { + @include panel-style(370px, -height, -width, -width); + + button { + @include export-btn; + } +} \ No newline at end of file diff --git a/src/dashboard/src/components/charts/allocationByStorageVolume/AllocationByStorageVolumeChart.tsx b/src/dashboard/src/components/charts/allocationByStorageVolume/AllocationByStorageVolumeChart.tsx new file mode 100644 index 00000000..d918b8c8 --- /dev/null +++ b/src/dashboard/src/components/charts/allocationByStorageVolume/AllocationByStorageVolumeChart.tsx @@ -0,0 +1,5 @@ +import styles from './AllocationByStorageVolumeChart.module.scss'; + +export const AllocationByStorageVolumeChart = () => { + return
; +}; diff --git a/src/dashboard/src/components/charts/allocationByStorageVolume/index.ts b/src/dashboard/src/components/charts/allocationByStorageVolume/index.ts new file mode 100644 index 00000000..e34220cd --- /dev/null +++ b/src/dashboard/src/components/charts/allocationByStorageVolume/index.ts @@ -0,0 +1 @@ +export * from './AllocationByStorageVolumeChart'; diff --git a/src/dashboard/src/components/charts/index.ts b/src/dashboard/src/components/charts/index.ts index 5700b37d..2daa6f2d 100644 --- a/src/dashboard/src/components/charts/index.ts +++ b/src/dashboard/src/components/charts/index.ts @@ -1,3 +1,6 @@ +export * from './allOrgDonut'; +export * from './allocationByStorageVolume'; export * from './bar'; export * from './line'; export * from './pie'; +export * from './unusedSpace'; diff --git a/src/dashboard/src/components/charts/unusedSpace/UnusedSpaceChart.module.scss b/src/dashboard/src/components/charts/unusedSpace/UnusedSpaceChart.module.scss new file mode 100644 index 00000000..06d3465f --- /dev/null +++ b/src/dashboard/src/components/charts/unusedSpace/UnusedSpaceChart.module.scss @@ -0,0 +1,9 @@ +@import '@/styles/utils.scss'; + +.panel { + @include panel-style(370px, -height, -width, 638px); + + button { + @include export-btn; + } +} \ No newline at end of file diff --git a/src/dashboard/src/components/charts/unusedSpace/UnusedSpaceChart.tsx b/src/dashboard/src/components/charts/unusedSpace/UnusedSpaceChart.tsx new file mode 100644 index 00000000..ff77349f --- /dev/null +++ b/src/dashboard/src/components/charts/unusedSpace/UnusedSpaceChart.tsx @@ -0,0 +1,5 @@ +import styles from './UnusedSpaceChart.module.scss'; + +export const UnusedSpaceChart = () => { + return
; +}; diff --git a/src/dashboard/src/components/charts/unusedSpace/index.ts b/src/dashboard/src/components/charts/unusedSpace/index.ts new file mode 100644 index 00000000..28fc3858 --- /dev/null +++ b/src/dashboard/src/components/charts/unusedSpace/index.ts @@ -0,0 +1 @@ +export * from './UnusedSpaceChart'; diff --git a/src/dashboard/src/components/filter/Filter.tsx b/src/dashboard/src/components/filter/Filter.tsx index 13aa8f4b..0818794b 100644 --- a/src/dashboard/src/components/filter/Filter.tsx +++ b/src/dashboard/src/components/filter/Filter.tsx @@ -2,7 +2,12 @@ import { Button, DateRangePicker, Select } from '@/components'; import { IOperatingSystemItemModel, IOrganizationModel, ITenantModel } from '@/hooks'; -import { useOperatingSystemItems, useOrganizations, useTenants } from '@/hooks/data'; +import { + useOperatingSystemItems, + useOrganizations, + useServerItems, + useTenants, +} from '@/hooks/data'; import { useFilteredFileSystemItems, useFilteredOperatingSystemItems, @@ -19,6 +24,7 @@ export const Filter: React.FC = () => { const { tenants } = useTenants(); const { organizations } = useOrganizations(); const { operatingSystemItems } = useOperatingSystemItems(); + const { serverItems } = useServerItems(); const dateRange = useFiltered((state) => state.dateRange); const setDateRange = useFiltered((state) => state.setDateRange); @@ -41,7 +47,6 @@ export const Filter: React.FC = () => { const serverItem = useFiltered((state) => state.serverItem); const setServerItem = useFiltered((state) => state.setServerItem); - const serverItems = useFiltered((state) => state.serverItems); const setServerItems = useFiltered((state) => state.setServerItems); const { options: filteredServerItemOptions, findServerItems } = useFilteredServerItems(); @@ -59,6 +64,10 @@ export const Filter: React.FC = () => { setOperatingSystemItems(operatingSystemItems); }, [setOperatingSystemItems, operatingSystemItems]); + React.useEffect(() => { + setServerItems(serverItems); + }, [setServerItems, serverItems]); + React.useEffect(() => { if (!dateRange[0]) { setDateRange([ @@ -191,6 +200,8 @@ export const Filter: React.FC = () => { onChange={async (value) => { const server = serverItems.find((o) => o.serviceNowKey == value); setServerItem(server); + if (server) setServerItems([server]); + else setServerItems(serverItems); }} /> { find: async (filter: IUserFilter | undefined = {}): Promise => { return await dispatch(`/api/admin/users?${toQueryString(filter)}`); }, + get: async (id: number): Promise => { + return await dispatch(`/api/admin/users/${id}`); + }, update: async (model: IUserModel): Promise => { const url = `/api/admin/users/${model.id}`; const res = await dispatch(url, { diff --git a/src/dashboard/src/hooks/api/interfaces/IFileSystemItemModel.ts b/src/dashboard/src/hooks/api/interfaces/IFileSystemItemModel.ts index 99674139..4ef56e80 100644 --- a/src/dashboard/src/hooks/api/interfaces/IFileSystemItemModel.ts +++ b/src/dashboard/src/hooks/api/interfaces/IFileSystemItemModel.ts @@ -18,14 +18,14 @@ export interface IFileSystemItemModel extends IAuditableModel { storageType: string; mediaType: string; volumeId: string; - capacity: string; - diskSpace: string; + capacity: number; + diskSpace: number; size: string; - sizeBytes: string; - usedSizeBytes: string; - availableSpace: string; + sizeBytes: number; + usedSizeBytes?: number; + availableSpace: number; freeSpace: string; - freeSpaceBytes: string; + freeSpaceBytes: number; // Collections history?: IFileSystemHistoryItemModel[]; diff --git a/src/dashboard/src/hooks/api/interfaces/IServerItemModel.ts b/src/dashboard/src/hooks/api/interfaces/IServerItemModel.ts index ecf0da9e..50a042c5 100644 --- a/src/dashboard/src/hooks/api/interfaces/IServerItemModel.ts +++ b/src/dashboard/src/hooks/api/interfaces/IServerItemModel.ts @@ -23,12 +23,15 @@ export interface IServerItemModel extends IAuditableModel { name: string; category: string; subCategory: string; - diskSpace: string; dnsDomain: string; className: string; platform: string; ipAddress: string; fqdn: string; + diskSpace?: string; + + capacity?: number; + availableSpace?: number; // Collections fileSystemItems: IFileSystemItemModel[]; diff --git a/src/dashboard/src/hooks/api/interfaces/IUserFilter.ts b/src/dashboard/src/hooks/api/interfaces/IUserFilter.ts index c7b3bf6a..6f995fa4 100644 --- a/src/dashboard/src/hooks/api/interfaces/IUserFilter.ts +++ b/src/dashboard/src/hooks/api/interfaces/IUserFilter.ts @@ -4,4 +4,6 @@ export interface IUserFilter { enabled?: boolean; firstName?: string; lastName?: string; + includeGroups?: boolean; + includeTenants?: boolean; } diff --git a/src/dashboard/src/hooks/api/interfaces/IUserInfoModel.ts b/src/dashboard/src/hooks/api/interfaces/IUserInfoModel.ts new file mode 100644 index 00000000..ac64e536 --- /dev/null +++ b/src/dashboard/src/hooks/api/interfaces/IUserInfoModel.ts @@ -0,0 +1,16 @@ +export interface IUserInfoModel { + id: number; + key?: string; + username?: string; + email?: string; + displayName?: string; + firstName?: string; + lastName?: string; + lastLoginOn?: string; + isEnabled: boolean; + note: string; + groups: string[]; + roles: string[]; + preferences: { [key: string]: any }; + version?: number; +} diff --git a/src/dashboard/src/hooks/api/interfaces/IUserModel.ts b/src/dashboard/src/hooks/api/interfaces/IUserModel.ts index 8284f0ac..961785d0 100644 --- a/src/dashboard/src/hooks/api/interfaces/IUserModel.ts +++ b/src/dashboard/src/hooks/api/interfaces/IUserModel.ts @@ -1,7 +1,8 @@ +import { IAuditableModel } from '.'; import { IGroupModel } from './IGroupModel'; import { ITenantModel } from './ITenantModel'; -export interface IUserModel { +export interface IUserModel extends IAuditableModel { id: number; username: string; email: string; diff --git a/src/dashboard/src/hooks/api/interfaces/index.ts b/src/dashboard/src/hooks/api/interfaces/index.ts index 092d1662..09e03d2e 100644 --- a/src/dashboard/src/hooks/api/interfaces/index.ts +++ b/src/dashboard/src/hooks/api/interfaces/index.ts @@ -19,4 +19,5 @@ export * from './ISortableModel'; export * from './ITenantFilter'; export * from './ITenantModel'; export * from './IUserFilter'; +export * from './IUserInfoModel'; export * from './IUserModel'; diff --git a/src/dashboard/src/hooks/data/index.ts b/src/dashboard/src/hooks/data/index.ts index 96644a84..49652848 100644 --- a/src/dashboard/src/hooks/data/index.ts +++ b/src/dashboard/src/hooks/data/index.ts @@ -2,5 +2,6 @@ export * from './useGroups'; export * from './useOperatingSystemItems'; export * from './useOrganizations'; export * from './useRoles'; +export * from './useServerItems'; export * from './useTenants'; export * from './useUsers'; diff --git a/src/dashboard/src/hooks/data/useServerItems.ts b/src/dashboard/src/hooks/data/useServerItems.ts new file mode 100644 index 00000000..11febfaa --- /dev/null +++ b/src/dashboard/src/hooks/data/useServerItems.ts @@ -0,0 +1,30 @@ +import { useApp } from '@/store'; +import React from 'react'; +import { IServerItemModel, useApiServerItems, useAuth } from '..'; + +export const useServerItems = () => { + const { status } = useAuth(); + const { find } = useApiServerItems(); + const serverItems = useApp((state) => state.serverItems); + const setServerItems = useApp((state) => state.setServerItems); + + const [isReady, setIsReady] = React.useState(false); + + React.useEffect(() => { + // Get an array of serverItems. + if (status === 'authenticated' && !serverItems.length) { + setIsReady(false); + find() + .then(async (res) => { + const serverItems: IServerItemModel[] = await res.json(); + setServerItems(serverItems); + }) + .catch((error) => { + console.error(error); + }) + .finally(() => setIsReady(true)); + } else if (serverItems.length) setIsReady(true); + }, [find, setServerItems, status, serverItems.length]); + + return { isReady, serverItems }; +}; diff --git a/src/dashboard/src/hooks/data/useUsers.ts b/src/dashboard/src/hooks/data/useUsers.ts index 03826d6e..98ffc2c7 100644 --- a/src/dashboard/src/hooks/data/useUsers.ts +++ b/src/dashboard/src/hooks/data/useUsers.ts @@ -4,19 +4,24 @@ import React from 'react'; import { IUserModel, useAuth } from '..'; import { useApiUsers } from '../api/admin'; -export const useUsers = () => { +interface IUserProps { + includeGroups?: boolean; +} + +export const useUsers = ({ includeGroups }: IUserProps) => { const { status } = useAuth(); - const { find } = useApiUsers(); + const { find, get } = useApiUsers(); + const userInfo = useApp((state) => state.userinfo); const users = useApp((state) => state.users); const setUsers = useApp((state) => state.setUsers); const [isReady, setIsReady] = React.useState(false); React.useEffect(() => { - // Get an array of users. + // Get an array of all users. if (status === 'authenticated' && !users.length) { setIsReady(false); - find() + find({ includeGroups }) .then(async (res) => { const users: IUserModel[] = await res.json(); setUsers(users); @@ -26,7 +31,25 @@ export const useUsers = () => { }) .finally(() => setIsReady(true)); } else if (users.length) setIsReady(true); - }, [find, setUsers, status, users.length]); + }, [find, includeGroups, setUsers, status, users.length]); + + React.useEffect(() => { + // When a page is first loaded a request is made to activate the user. + // This results in the user's information being updated after a request for the list of users. + // Need to make a request for the latest user information. + if (userInfo) { + if (users.some((u) => u.id === userInfo.id && u.version !== userInfo.version)) { + get(userInfo.id) + .then(async (response) => { + const user = await response.json(); + setUsers(users.map((u) => (u.id === userInfo.id ? user : u))); + }) + .catch((error) => { + console.error(error); + }); + } + } + }, [userInfo, users, get, setUsers]); const options = React.useMemo( () => diff --git a/src/dashboard/src/store/useApp.ts b/src/dashboard/src/store/useApp.ts index 16ceffab..a77d9137 100644 --- a/src/dashboard/src/store/useApp.ts +++ b/src/dashboard/src/store/useApp.ts @@ -6,14 +6,15 @@ import { IRoleModel, IServerItemModel, ITenantModel, + IUserInfoModel, IUserModel, } from '@/hooks/api'; import { create } from 'zustand'; export interface IAppState { // User - userinfo?: any; // TODO: Replace with interface. - setUserinfo: (value: any) => void; + userinfo?: IUserInfoModel; // TODO: Replace with interface. + setUserinfo: (value: IUserInfoModel) => void; // Roles roles: IRoleModel[]; diff --git a/src/dashboard/src/utils/convertToStorageSize.ts b/src/dashboard/src/utils/convertToStorageSize.ts new file mode 100644 index 00000000..f56032df --- /dev/null +++ b/src/dashboard/src/utils/convertToStorageSize.ts @@ -0,0 +1,47 @@ +/** + * Converts a storage value to a specified size type. + * @param value The initial value to convert. + * @param input The input size type. + * @param output The output size type. + * @returns A string representing the storage size. + */ +export const convertToStorageSize = ( + value: number, + input: 'TB' | 'GB' | 'MB' | 'KB' | '' = '', + output: 'TB' | 'GB' | 'MB' | 'KB' | '' = '', + locales: Intl.LocalesArgument = navigator.language, + options?: ({ formula: (value: number) => number } & Intl.NumberFormatOptions) | undefined, +) => { + var result = value; + if (input === output) result = value; + else if (input === 'TB') { + if (output === 'GB') result = value * 1024; + else if (output === 'MB') result = value * Math.pow(1024, 2); + else if (output === 'KB') result = value * Math.pow(1024, 3); + else if (output === '') result = value * Math.pow(1024, 4); + } else if (input === 'GB') { + if (output === 'TB') result = value / 1024; + else if (output === 'MB') result = value * Math.pow(1024, 2); + else if (output === 'KB') result = value * Math.pow(1024, 3); + else if (output === '') result = value * Math.pow(1024, 4); + } else if (input === 'MB') { + if (output === 'TB') result = value / Math.pow(1024, 2); + else if (output === 'GB') result = value / 1024; + else if (output === 'KB') result = value * Math.pow(1024, 3); + else if (output === '') result = value * Math.pow(1024, 4); + } else if (input === 'KB') { + if (output === 'TB') result = value / Math.pow(1024, 3); + else if (output === 'GB') result = value / Math.pow(1024, 2); + else if (output === 'MB') result = value / 1024; + else if (output === '') result = value * Math.pow(1024, 4); + } else if (input === '') { + if (output === 'TB') result = value / Math.pow(1024, 4); + else if (output === 'GB') result = value / Math.pow(1024, 3); + else if (output === 'MB') result = value / Math.pow(1024, 2); + else if (output === 'KB') result = value / 1024; + } + return `${(options?.formula(result) ?? result).toLocaleString( + locales, + options, + )} ${output}`.trimEnd(); +}; diff --git a/src/dashboard/src/utils/index.ts b/src/dashboard/src/utils/index.ts index 035ad5e4..632d074e 100644 --- a/src/dashboard/src/utils/index.ts +++ b/src/dashboard/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from './convertToStorageSize'; export * from './dispatch'; export * from './encryption'; export * from './keycloakSessionLogOut'; diff --git a/src/data-service/DataService.cs b/src/data-service/DataService.cs index c16100b9..2864cf0d 100644 --- a/src/data-service/DataService.cs +++ b/src/data-service/DataService.cs @@ -387,14 +387,14 @@ private async Task ProcessConfigurationItemsAsync(Models.DataSyncModel option) fileSystemItem.StorageType = fileSystemItemSN.Data.StorageType ?? ""; fileSystemItem.MediaType = fileSystemItemSN.Data.MediaType ?? ""; fileSystemItem.VolumeId = fileSystemItemSN.Data.VolumeId ?? ""; - fileSystemItem.Capacity = fileSystemItemSN.Data.Capacity ?? ""; - fileSystemItem.DiskSpace = fileSystemItemSN.Data.DiskSpace ?? ""; + fileSystemItem.Capacity = !String.IsNullOrWhiteSpace(fileSystemItemSN.Data.Capacity) ? Int32.Parse(fileSystemItemSN.Data.Capacity) : 0; + fileSystemItem.DiskSpace = !String.IsNullOrWhiteSpace(fileSystemItemSN.Data.DiskSpace) ? float.Parse(fileSystemItemSN.Data.DiskSpace) : 0; fileSystemItem.Size = fileSystemItemSN.Data.Size ?? ""; - fileSystemItem.SizeBytes = fileSystemItemSN.Data.SizeBytes ?? ""; - fileSystemItem.UsedSizeBytes = fileSystemItemSN.Data.UsedSizeBytes ?? ""; - fileSystemItem.AvailableSpace = fileSystemItemSN.Data.AvailableSpace ?? ""; + fileSystemItem.SizeBytes = !String.IsNullOrWhiteSpace(fileSystemItemSN.Data.SizeBytes) ? long.Parse(fileSystemItemSN.Data.SizeBytes) : 0; + fileSystemItem.UsedSizeBytes = !String.IsNullOrWhiteSpace(fileSystemItemSN.Data.UsedSizeBytes) ? long.Parse(fileSystemItemSN.Data.UsedSizeBytes) : 0; + fileSystemItem.AvailableSpace = !String.IsNullOrWhiteSpace(fileSystemItemSN.Data.AvailableSpace) ? Int32.Parse(fileSystemItemSN.Data.AvailableSpace) : 0; fileSystemItem.FreeSpace = fileSystemItemSN.Data.FreeSpace ?? ""; - fileSystemItem.FreeSpaceBytes = fileSystemItemSN.Data.FreeSpaceBytes ?? ""; + fileSystemItem.FreeSpaceBytes = !String.IsNullOrWhiteSpace(fileSystemItemSN.Data.FreeSpaceBytes) ? long.Parse(fileSystemItemSN.Data.FreeSpaceBytes) : 0; fileSystemItem = await this.HsbApi.UpdateFileSystemItemAsync(fileSystemItem); } diff --git a/src/libs/core/Extensions/JsonElementExtensions.cs b/src/libs/core/Extensions/JsonElementExtensions.cs index 3c369394..72105f14 100644 --- a/src/libs/core/Extensions/JsonElementExtensions.cs +++ b/src/libs/core/Extensions/JsonElementExtensions.cs @@ -85,7 +85,12 @@ public static string ToJson(this JsonElement element, JsonWriterOptions? options return node.ValueKind switch { JsonValueKind.String => !typeof(T).IsEnum ? - (T)Convert.ChangeType($"{node.GetString()}".Trim(), typeof(T)) : + (typeof(T) == typeof(string) ? + (T)Convert.ChangeType($"{node.GetString()}".Trim(), typeof(T)) : + (!String.IsNullOrWhiteSpace(node.GetString()) ? + Converter.ChangeType($"{node.GetString()}") : + default + )) : (T)Enum.Parse(typeof(T), node.GetString() ?? ""), JsonValueKind.Null or JsonValueKind.Undefined => defaultValue, JsonValueKind.Number => node.ConvertNumberTo(), diff --git a/src/libs/core/Helpers/Converter.cs b/src/libs/core/Helpers/Converter.cs new file mode 100644 index 00000000..6a0d7f9b --- /dev/null +++ b/src/libs/core/Helpers/Converter.cs @@ -0,0 +1,50 @@ +namespace HSB.Core; + +public static class Converter +{ + /// + /// Convert the specified 'value' to the specified 'type'. + /// + /// + /// + /// + public static T? ChangeType(object value) + { + var t = typeof(T); + + if (t.IsGenericType && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) + { + if (value == null) + return default; + + t = Nullable.GetUnderlyingType(t); + if (t == null) + return default; + } + + return (T)Convert.ChangeType(value, t); + } + + /// + /// Convert the specified 'value' to the specified 'type'. + /// + /// + /// + /// + public static object? ChangeType(object value, Type conversion) + { + var t = conversion; + + if (t.IsGenericType && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) + { + if (value == null) + return null; + + t = Nullable.GetUnderlyingType(t); + if (t == null) + return default; + } + + return Convert.ChangeType(value, t); + } +} diff --git a/src/libs/css/Models/ErrorResponseModel.cs b/src/libs/css/Models/ErrorResponseModel.cs index cac960c1..f21897ce 100644 --- a/src/libs/css/Models/ErrorResponseModel.cs +++ b/src/libs/css/Models/ErrorResponseModel.cs @@ -2,7 +2,16 @@ namespace HSB.CSS.Models; public class ErrorResponseModel { - #region Properties - public string Message { get; set; } = ""; - #endregion + #region Properties + public string Message { get; set; } = ""; + #endregion + + #region Constructors + public ErrorResponseModel() { } + + public ErrorResponseModel(string message) + { + this.Message = message; + } + #endregion } diff --git a/src/libs/dal/Configuration/FileSystemItemConfiguration.cs b/src/libs/dal/Configuration/FileSystemItemConfiguration.cs index 014e8e52..92578056 100644 --- a/src/libs/dal/Configuration/FileSystemItemConfiguration.cs +++ b/src/libs/dal/Configuration/FileSystemItemConfiguration.cs @@ -24,14 +24,14 @@ public override void Configure(EntityTypeBuilder builder) builder.Property(m => m.StorageType).IsRequired().HasMaxLength(100).HasDefaultValueSql("''"); builder.Property(m => m.MediaType).IsRequired().HasMaxLength(100).HasDefaultValueSql("''"); builder.Property(m => m.VolumeId).IsRequired().HasMaxLength(100).HasDefaultValueSql("''"); - builder.Property(m => m.Capacity).IsRequired().HasMaxLength(50).HasDefaultValueSql("''"); - builder.Property(m => m.DiskSpace).IsRequired().HasMaxLength(50).HasDefaultValueSql("''"); + builder.Property(m => m.Capacity).IsRequired(); + builder.Property(m => m.DiskSpace).IsRequired(); builder.Property(m => m.Size).IsRequired().HasMaxLength(50).HasDefaultValueSql("''"); - builder.Property(m => m.SizeBytes).IsRequired().HasMaxLength(50).HasDefaultValueSql("''"); - builder.Property(m => m.UsedSizeBytes).IsRequired().HasMaxLength(50).HasDefaultValueSql("''"); - builder.Property(m => m.AvailableSpace).IsRequired().HasMaxLength(50).HasDefaultValueSql("''"); + builder.Property(m => m.SizeBytes).IsRequired(); + builder.Property(m => m.UsedSizeBytes); + builder.Property(m => m.AvailableSpace).IsRequired(); builder.Property(m => m.FreeSpace).IsRequired().HasMaxLength(50).HasDefaultValueSql("''"); - builder.Property(m => m.FreeSpaceBytes).IsRequired().HasMaxLength(50).HasDefaultValueSql("''"); + builder.Property(m => m.FreeSpaceBytes).IsRequired(); builder.HasOne(m => m.ServerItem).WithMany(m => m.FileSystemItems).HasForeignKey(m => m.ServerItemServiceNowKey).OnDelete(DeleteBehavior.Cascade); diff --git a/src/libs/dal/Configuration/ServerItemConfiguration.cs b/src/libs/dal/Configuration/ServerItemConfiguration.cs index e6b99407..5e06c530 100644 --- a/src/libs/dal/Configuration/ServerItemConfiguration.cs +++ b/src/libs/dal/Configuration/ServerItemConfiguration.cs @@ -27,6 +27,10 @@ public override void Configure(EntityTypeBuilder builder) builder.Property(m => m.Platform).IsRequired().HasMaxLength(100).HasDefaultValueSql("''"); builder.Property(m => m.IPAddress).IsRequired().HasMaxLength(50).HasDefaultValueSql("''"); builder.Property(m => m.FQDN).IsRequired().HasMaxLength(100).HasDefaultValueSql("''"); + builder.Property(m => m.DiskSpace); + + builder.Property(m => m.Capacity); + builder.Property(m => m.AvailableSpace); builder.HasOne(m => m.Tenant).WithMany(m => m.ServerItems).HasForeignKey(m => m.TenantId).OnDelete(DeleteBehavior.Cascade); builder.HasOne(m => m.Organization).WithMany(m => m.ServerItems).HasForeignKey(m => m.OrganizationId).OnDelete(DeleteBehavior.Cascade); diff --git a/src/libs/dal/Configuration/UserConfiguration.cs b/src/libs/dal/Configuration/UserConfiguration.cs index 2d8bb034..d19dbc9d 100644 --- a/src/libs/dal/Configuration/UserConfiguration.cs +++ b/src/libs/dal/Configuration/UserConfiguration.cs @@ -24,7 +24,6 @@ public override void Configure(EntityTypeBuilder builder) builder.Property(m => m.IsEnabled).IsRequired(); builder.Property(m => m.Note).IsRequired().HasColumnType("text").HasDefaultValueSql("''"); builder.Property(m => m.Preferences).IsRequired().HasColumnType("jsonb").HasDefaultValueSql("'{}'::jsonb"); - builder.Property(m => m.FailedLogins).IsRequired().HasDefaultValueSql("0"); builder.Property(m => m.LastLoginOn); builder.HasMany(m => m.Groups).WithMany(m => m.Users).UsingEntity(); diff --git a/src/libs/dal/HSBContext.cs b/src/libs/dal/HSBContext.cs index ba045f51..12dc6bb2 100644 --- a/src/libs/dal/HSBContext.cs +++ b/src/libs/dal/HSBContext.cs @@ -28,7 +28,7 @@ public class HSBContext : DbContext public DbSet Users => Set(); public DbSet UserGroups => Set(); public DbSet UserTenants => Set(); - public DbSet Groups => Set(); + public DbSet Groups => Set(); public DbSet GroupRoles => Set(); public DbSet Roles => Set(); #endregion diff --git a/src/libs/dal/Migrations/0.0.0/Up/PostUp/01-Roles.sql b/src/libs/dal/Migrations/0.0.0/Up/PostUp/01-Roles.sql new file mode 100644 index 00000000..7f277428 --- /dev/null +++ b/src/libs/dal/Migrations/0.0.0/Up/PostUp/01-Roles.sql @@ -0,0 +1,49 @@ +INSERT INTO public."Role" ( + "Key" + , "Name" + , "Description" + , "IsEnabled" + , "SortOrder" + , "CreatedBy" + , "UpdatedBy" +) VALUES ( + gen_random_uuid() + , 'hsb' + , 'HSB users' + , true + , 0 + , '' + , '' +), ( + gen_random_uuid() + , 'system-admin' + , 'System administrators' + , true + , 0 + , '' + , '' +), ( + gen_random_uuid() + , 'client' + , 'Client users' + , true + , 0 + , '' + , '' +), ( + gen_random_uuid() + , 'organization-admin' + , 'Client organization administrators' + , true + , 0 + , '' + , '' +), ( + gen_random_uuid() + , 'service-now' + , 'Service Account to allow the Data Sync Service to connect to the API.' + , true + , 0 + , '' + , '' +); diff --git a/src/libs/dal/Migrations/0.0.0/Up/PostUp/02-Groups.sql b/src/libs/dal/Migrations/0.0.0/Up/PostUp/02-Groups.sql new file mode 100644 index 00000000..7a076dec --- /dev/null +++ b/src/libs/dal/Migrations/0.0.0/Up/PostUp/02-Groups.sql @@ -0,0 +1,49 @@ +INSERT INTO public."Group" ( + "Key" + , "Name" + , "Description" + , "IsEnabled" + , "SortOrder" + , "CreatedBy" + , "UpdatedBy" +) VALUES ( + gen_random_uuid() + , 'hsb' + , 'HSB users' + , true + , 0 + , '' + , '' +), ( + gen_random_uuid() + , 'system-admin' + , 'System administrators' + , true + , 0 + , '' + , '' +), ( + gen_random_uuid() + , 'client' + , 'Client users' + , true + , 0 + , '' + , '' +), ( + gen_random_uuid() + , 'organization-admin' + , 'Client organization administrators' + , true + , 0 + , '' + , '' +), ( + gen_random_uuid() + , 'service-now' + , 'Service Account to allow the Data Sync Service to connect to the API.' + , true + , 0 + , '' + , '' +); diff --git a/src/libs/dal/Migrations/0.0.0/Up/PostUp/03-GroupRoles.sql b/src/libs/dal/Migrations/0.0.0/Up/PostUp/03-GroupRoles.sql new file mode 100644 index 00000000..25dcbb13 --- /dev/null +++ b/src/libs/dal/Migrations/0.0.0/Up/PostUp/03-GroupRoles.sql @@ -0,0 +1,31 @@ +INSERT INTO public."GroupRole" ( + "GroupId" + , "RoleId" + , "CreatedBy" + , "UpdatedBy" +) VALUES ( + (SELECT "Id" FROM public."Group" WHERE "Name" = 'hsb' LIMIT 1) + , (SELECT "Id" FROM public."Role" WHERE "Name" = 'hsb' LIMIT 1) + , '' + , '' +), ( + (SELECT "Id" FROM public."Group" WHERE "Name" = 'system-admin' LIMIT 1) + , (SELECT "Id" FROM public."Role" WHERE "Name" = 'system-admin' LIMIT 1) + , '' + , '' +), ( + (SELECT "Id" FROM public."Group" WHERE "Name" = 'client' LIMIT 1) + , (SELECT "Id" FROM public."Role" WHERE "Name" = 'client' LIMIT 1) + , '' + , '' +), ( + (SELECT "Id" FROM public."Group" WHERE "Name" = 'organization-admin' LIMIT 1) + , (SELECT "Id" FROM public."Role" WHERE "Name" = 'organization-admin' LIMIT 1) + , '' + , '' +), ( + (SELECT "Id" FROM public."Group" WHERE "Name" = 'service-now' LIMIT 1) + , (SELECT "Id" FROM public."Role" WHERE "Name" = 'service-now' LIMIT 1) + , '' + , '' +); diff --git a/src/libs/dal/Migrations/20231227205245_0.0.0.Designer.cs b/src/libs/dal/Migrations/20231229201941_0.0.0.Designer.cs similarity index 96% rename from src/libs/dal/Migrations/20231227205245_0.0.0.Designer.cs rename to src/libs/dal/Migrations/20231229201941_0.0.0.Designer.cs index 00c64543..181de180 100644 --- a/src/libs/dal/Migrations/20231227205245_0.0.0.Designer.cs +++ b/src/libs/dal/Migrations/20231229201941_0.0.0.Designer.cs @@ -13,7 +13,7 @@ namespace HSB.DAL.Migrations { [DbContext(typeof(HSBContext))] - [Migration("20231227205245_0.0.0")] + [Migration("20231229201941_0.0.0")] partial class _000 { /// @@ -272,19 +272,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(100) .HasColumnType("character varying(100)"); - b.Property("AvailableSpace") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasDefaultValueSql("''"); + b.Property("AvailableSpace") + .HasColumnType("integer"); - b.Property("Capacity") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasDefaultValueSql("''"); + b.Property("Capacity") + .HasColumnType("integer"); b.Property("Category") .IsRequired() @@ -310,12 +302,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasDefaultValueSql("CURRENT_TIMESTAMP"); - b.Property("DiskSpace") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasDefaultValueSql("''"); + b.Property("DiskSpace") + .HasColumnType("real"); b.Property("FreeSpace") .IsRequired() @@ -324,12 +312,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("character varying(50)") .HasDefaultValueSql("''"); - b.Property("FreeSpaceBytes") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasDefaultValueSql("''"); + b.Property("FreeSpaceBytes") + .HasColumnType("bigint"); b.Property("Label") .IsRequired() @@ -371,12 +355,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("character varying(50)") .HasDefaultValueSql("''"); - b.Property("SizeBytes") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasDefaultValueSql("''"); + b.Property("SizeBytes") + .HasColumnType("bigint"); b.Property("StorageType") .IsRequired() @@ -402,12 +382,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasDefaultValueSql("CURRENT_TIMESTAMP"); - b.Property("UsedSizeBytes") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasDefaultValueSql("''"); + b.Property("UsedSizeBytes") + .HasColumnType("bigint"); b.Property("Version") .IsConcurrencyToken() @@ -890,6 +866,12 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(100) .HasColumnType("character varying(100)"); + b.Property("AvailableSpace") + .HasColumnType("real"); + + b.Property("Capacity") + .HasColumnType("real"); + b.Property("Category") .IsRequired() .ValueGeneratedOnAdd() @@ -914,6 +896,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasDefaultValueSql("CURRENT_TIMESTAMP"); + b.Property("DiskSpace") + .HasColumnType("real"); + b.Property("DnsDomain") .IsRequired() .ValueGeneratedOnAdd() @@ -1161,11 +1146,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("EmailVerifiedOn") .HasColumnType("timestamp with time zone"); - b.Property("FailedLogins") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValueSql("0"); - b.Property("FirstName") .IsRequired() .ValueGeneratedOnAdd() diff --git a/src/libs/dal/Migrations/20231227205245_0.0.0.cs b/src/libs/dal/Migrations/20231229201941_0.0.0.cs similarity index 97% rename from src/libs/dal/Migrations/20231227205245_0.0.0.cs rename to src/libs/dal/Migrations/20231229201941_0.0.0.cs index ee8401cc..35131b19 100644 --- a/src/libs/dal/Migrations/20231227205245_0.0.0.cs +++ b/src/libs/dal/Migrations/20231229201941_0.0.0.cs @@ -175,7 +175,6 @@ protected override void Up(MigrationBuilder migrationBuilder) LastName = table.Column(type: "character varying(50)", maxLength: 50, nullable: false, defaultValueSql: "''"), Phone = table.Column(type: "character varying(15)", maxLength: 15, nullable: false, defaultValueSql: "''"), IsEnabled = table.Column(type: "boolean", nullable: false), - FailedLogins = table.Column(type: "integer", nullable: false, defaultValueSql: "0"), LastLoginOn = table.Column(type: "timestamp with time zone", nullable: true), Note = table.Column(type: "text", nullable: false, defaultValueSql: "''"), Preferences = table.Column(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"), @@ -237,6 +236,9 @@ protected override void Up(MigrationBuilder migrationBuilder) Platform = table.Column(type: "character varying(100)", maxLength: 100, nullable: false, defaultValueSql: "''"), IPAddress = table.Column(type: "character varying(50)", maxLength: 50, nullable: false, defaultValueSql: "''"), FQDN = table.Column(type: "character varying(100)", maxLength: 100, nullable: false, defaultValueSql: "''"), + DiskSpace = table.Column(type: "real", nullable: true), + Capacity = table.Column(type: "real", nullable: true), + AvailableSpace = table.Column(type: "real", nullable: true), CreatedOn = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), CreatedBy = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), UpdatedOn = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), @@ -369,14 +371,14 @@ protected override void Up(MigrationBuilder migrationBuilder) MediaType = table.Column(type: "character varying(100)", maxLength: 100, nullable: false, defaultValueSql: "''"), VolumeId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false, defaultValueSql: "''"), ClassName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false, defaultValueSql: "''"), - Capacity = table.Column(type: "character varying(50)", maxLength: 50, nullable: false, defaultValueSql: "''"), - DiskSpace = table.Column(type: "character varying(50)", maxLength: 50, nullable: false, defaultValueSql: "''"), + Capacity = table.Column(type: "integer", nullable: false), + DiskSpace = table.Column(type: "real", nullable: false), Size = table.Column(type: "character varying(50)", maxLength: 50, nullable: false, defaultValueSql: "''"), - SizeBytes = table.Column(type: "character varying(50)", maxLength: 50, nullable: false, defaultValueSql: "''"), - UsedSizeBytes = table.Column(type: "character varying(50)", maxLength: 50, nullable: false, defaultValueSql: "''"), - AvailableSpace = table.Column(type: "character varying(50)", maxLength: 50, nullable: false, defaultValueSql: "''"), + SizeBytes = table.Column(type: "bigint", nullable: false), + UsedSizeBytes = table.Column(type: "bigint", nullable: true), + AvailableSpace = table.Column(type: "integer", nullable: false), FreeSpace = table.Column(type: "character varying(50)", maxLength: 50, nullable: false, defaultValueSql: "''"), - FreeSpaceBytes = table.Column(type: "character varying(50)", maxLength: 50, nullable: false, defaultValueSql: "''"), + FreeSpaceBytes = table.Column(type: "bigint", nullable: false), CreatedOn = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), CreatedBy = table.Column(type: "character varying(250)", maxLength: 250, nullable: false), UpdatedOn = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), diff --git a/src/libs/dal/Migrations/HSBContextModelSnapshot.cs b/src/libs/dal/Migrations/HSBContextModelSnapshot.cs index 664bb226..6bef7f55 100644 --- a/src/libs/dal/Migrations/HSBContextModelSnapshot.cs +++ b/src/libs/dal/Migrations/HSBContextModelSnapshot.cs @@ -269,19 +269,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(100) .HasColumnType("character varying(100)"); - b.Property("AvailableSpace") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasDefaultValueSql("''"); + b.Property("AvailableSpace") + .HasColumnType("integer"); - b.Property("Capacity") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasDefaultValueSql("''"); + b.Property("Capacity") + .HasColumnType("integer"); b.Property("Category") .IsRequired() @@ -307,12 +299,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasDefaultValueSql("CURRENT_TIMESTAMP"); - b.Property("DiskSpace") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasDefaultValueSql("''"); + b.Property("DiskSpace") + .HasColumnType("real"); b.Property("FreeSpace") .IsRequired() @@ -321,12 +309,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(50)") .HasDefaultValueSql("''"); - b.Property("FreeSpaceBytes") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasDefaultValueSql("''"); + b.Property("FreeSpaceBytes") + .HasColumnType("bigint"); b.Property("Label") .IsRequired() @@ -368,12 +352,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(50)") .HasDefaultValueSql("''"); - b.Property("SizeBytes") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasDefaultValueSql("''"); + b.Property("SizeBytes") + .HasColumnType("bigint"); b.Property("StorageType") .IsRequired() @@ -399,12 +379,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasDefaultValueSql("CURRENT_TIMESTAMP"); - b.Property("UsedSizeBytes") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasDefaultValueSql("''"); + b.Property("UsedSizeBytes") + .HasColumnType("bigint"); b.Property("Version") .IsConcurrencyToken() @@ -887,6 +863,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(100) .HasColumnType("character varying(100)"); + b.Property("AvailableSpace") + .HasColumnType("real"); + + b.Property("Capacity") + .HasColumnType("real"); + b.Property("Category") .IsRequired() .ValueGeneratedOnAdd() @@ -911,6 +893,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasDefaultValueSql("CURRENT_TIMESTAMP"); + b.Property("DiskSpace") + .HasColumnType("real"); + b.Property("DnsDomain") .IsRequired() .ValueGeneratedOnAdd() @@ -1158,11 +1143,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EmailVerifiedOn") .HasColumnType("timestamp with time zone"); - b.Property("FailedLogins") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValueSql("0"); - b.Property("FirstName") .IsRequired() .ValueGeneratedOnAdd() diff --git a/src/libs/dal/Services/BaseService`.cs b/src/libs/dal/Services/BaseService`.cs index 240c3872..d9a9d252 100644 --- a/src/libs/dal/Services/BaseService`.cs +++ b/src/libs/dal/Services/BaseService`.cs @@ -112,7 +112,9 @@ public virtual IEnumerable Find( /// public virtual Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry Add(TEntity entity) { - return this.Context.Add(entity); + var entry = this.Context.Entry(entity); + entry.State = EntityState.Added; + return entry; } /// @@ -122,7 +124,9 @@ public virtual Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry /// public virtual Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry Update(TEntity entity) { - return this.Context.Update(entity); + var entry = this.Context.Entry(entity); + entry.State = EntityState.Modified; + return entry; } /// @@ -132,7 +136,9 @@ public virtual Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry /// public virtual Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry Remove(TEntity entity) { - return this.Context.Remove(entity); + var entry = this.Context.Entry(entity); + entry.State = EntityState.Deleted; + return entry; } #endregion } diff --git a/src/libs/dal/Services/FileSystemItemService.cs b/src/libs/dal/Services/FileSystemItemService.cs index 4452cb52..e427c9e2 100644 --- a/src/libs/dal/Services/FileSystemItemService.cs +++ b/src/libs/dal/Services/FileSystemItemService.cs @@ -2,6 +2,7 @@ using HSB.DAL.Extensions; using HSB.Entities; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.Extensions.Logging; namespace HSB.DAL.Services; @@ -69,5 +70,45 @@ join usert in this.Context.UserTenants on tenant.Id equals usert.TenantId .AsNoTracking() .ToArray(); } + + /// + /// Add a new file system item record to the database. + /// Update the owning server with the volume space information. + /// + /// + /// + public override EntityEntry Add(FileSystemItem entity) + { + // Sum up all file system items for the server and update the server. + var server = this.Context.ServerItems.FirstOrDefault(si => si.ServiceNowKey == entity.ServerItemServiceNowKey); + if (server != null) + { + var volumes = this.Context.FileSystemItems.Where(fsi => fsi.ServerItemServiceNowKey == entity.ServerItemServiceNowKey).ToArray(); + server.Capacity = volumes.Sum(v => v.Capacity); + server.AvailableSpace = volumes.Sum(v => v.AvailableSpace); + this.Context.Entry(server).State = EntityState.Modified; + } + return base.Add(entity); + } + + /// + /// Update the file system item record in the database. + /// Update the owning server with the volume space information. + /// + /// + /// + public override EntityEntry Update(FileSystemItem entity) + { + // Sum up all file system items for the server and update the server. + var server = this.Context.ServerItems.FirstOrDefault(si => si.ServiceNowKey == entity.ServerItemServiceNowKey); + if (server != null) + { + var volumes = this.Context.FileSystemItems.Where(fsi => fsi.ServerItemServiceNowKey == entity.ServerItemServiceNowKey).ToArray(); + server.Capacity = volumes.Sum(v => v.Capacity); + server.AvailableSpace = volumes.Sum(v => v.AvailableSpace); + this.Context.Entry(server).State = EntityState.Modified; + } + return base.Update(entity); + } #endregion } diff --git a/src/libs/dal/Services/GroupService.cs b/src/libs/dal/Services/GroupService.cs index 577aa8bc..d0f7c489 100644 --- a/src/libs/dal/Services/GroupService.cs +++ b/src/libs/dal/Services/GroupService.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using HSB.Entities; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace HSB.DAL.Services; @@ -14,5 +15,11 @@ public GroupService(HSBContext dbContext, ClaimsPrincipal principal, IServicePro #endregion #region Methods + public Group? FindForId(int id) + { + return this.Context.Groups + .Include(m => m.RolesManyToMany).ThenInclude(m => m.Role) + .FirstOrDefault(g => g.Id == id); + } #endregion } diff --git a/src/libs/dal/Services/IGroupService.cs b/src/libs/dal/Services/IGroupService.cs index dde3b8c9..049bdbaf 100644 --- a/src/libs/dal/Services/IGroupService.cs +++ b/src/libs/dal/Services/IGroupService.cs @@ -4,5 +4,5 @@ namespace HSB.DAL.Services; public interface IGroupService : IBaseService { - + Group? FindForId(int id); } diff --git a/src/libs/dal/Services/IUserService.cs b/src/libs/dal/Services/IUserService.cs index c94c0feb..c3247198 100644 --- a/src/libs/dal/Services/IUserService.cs +++ b/src/libs/dal/Services/IUserService.cs @@ -4,6 +4,7 @@ namespace HSB.DAL.Services; public interface IUserService : IBaseService { + IEnumerable Find(Models.Filters.UserFilter filter); User? FindByKey(string key); User? FindByUsername(string username); IEnumerable FindByEmail(string email); diff --git a/src/libs/dal/Services/UserService.cs b/src/libs/dal/Services/UserService.cs index a5eb2249..c9868402 100644 --- a/src/libs/dal/Services/UserService.cs +++ b/src/libs/dal/Services/UserService.cs @@ -1,6 +1,9 @@ using System.Security.Claims; +using HSB.DAL.Extensions; using HSB.Entities; +using LinqKit; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.Extensions.Logging; namespace HSB.DAL.Services; @@ -15,6 +18,31 @@ public UserService(HSBContext dbContext, ClaimsPrincipal principal, IServiceProv #endregion #region Methods + public IEnumerable Find(Models.Filters.UserFilter filter) + { + var query = this.Context + .Users + .AsNoTracking(); + + if (filter.IncludeGroups == true) + query = query.Include(u => u.GroupsManyToMany).ThenInclude(g => g.Group); + + if (filter.IncludeTenants == true) + query = query.Include(u => u.TenantsManyToMany).ThenInclude(g => g.Tenant); + + query = query.Where(filter.GeneratePredicate()); + + if (filter.Sort?.Any() == true) + query = query.OrderByProperty(filter.Sort); + if (filter.Quantity.HasValue) + query = query.Take(filter.Quantity.Value); + if (filter.Page.HasValue && filter.Page > 1 && filter.Quantity.HasValue) + query = query.Skip(filter.Page.Value * filter.Quantity.Value); + + return query + .ToArray(); + } + public IEnumerable FindByEmail(string email) { return this.Context.Users @@ -36,5 +64,42 @@ public IEnumerable FindByEmail(string email) .Include(u => u.Groups).ThenInclude(g => g.Roles) .FirstOrDefault(u => EF.Functions.Like(u.Username, username)); } + + public override EntityEntry Update(User entity) + { + // Update groups + var originalGroups = this.Context.UserGroups.Where(ug => ug.UserId == entity.Id).ToArray(); + originalGroups.Except(entity.GroupsManyToMany).ForEach((group) => + { + this.Context.Entry(group).State = EntityState.Deleted; + }); + entity.GroupsManyToMany.ForEach((group) => + { + var originalGroup = originalGroups.FirstOrDefault(s => s.GroupId == group.GroupId); + if (originalGroup == null) + { + group.UserId = entity.Id; + this.Context.Entry(group).State = EntityState.Added; + } + }); + + // Update tenants + var originalTenants = this.Context.UserTenants.Where(ut => ut.UserId == entity.Id).ToArray(); + originalTenants.Except(entity.TenantsManyToMany).ForEach((source) => + { + this.Context.Entry(source).State = EntityState.Deleted; + }); + entity.TenantsManyToMany.ForEach((group) => + { + var originalTenant = originalTenants.FirstOrDefault(s => s.TenantId == group.TenantId); + if (originalTenant == null) + { + group.UserId = entity.Id; + this.Context.Entry(group).State = EntityState.Added; + } + }); + + return base.Update(entity); + } #endregion } diff --git a/src/libs/entities/FileSystemItem.cs b/src/libs/entities/FileSystemItem.cs index f4f4075c..9729a09d 100644 --- a/src/libs/entities/FileSystemItem.cs +++ b/src/libs/entities/FileSystemItem.cs @@ -31,14 +31,14 @@ public class FileSystemItem : Auditable public string MediaType { get; set; } = ""; public string VolumeId { get; set; } = ""; public string ClassName { get; set; } = ""; - public string Capacity { get; set; } = ""; - public string DiskSpace { get; set; } = ""; + public int Capacity { get; set; } + public float DiskSpace { get; set; } public string Size { get; set; } = ""; - public string SizeBytes { get; set; } = ""; - public string UsedSizeBytes { get; set; } = ""; - public string AvailableSpace { get; set; } = ""; + public long SizeBytes { get; set; } + public long? UsedSizeBytes { get; set; } + public int AvailableSpace { get; set; } public string FreeSpace { get; set; } = ""; - public string FreeSpaceBytes { get; set; } = ""; + public long FreeSpaceBytes { get; set; } #endregion /// @@ -72,14 +72,14 @@ public FileSystemItem(string serverItemId, JsonDocument fileSystemItemData, Json this.StorageType = fileSystemItemData.GetElementValue(".u_platform") ?? ""; this.MediaType = fileSystemItemData.GetElementValue(".dns_domain") ?? ""; this.VolumeId = fileSystemItemData.GetElementValue(".volume_id") ?? ""; - this.Capacity = fileSystemItemData.GetElementValue(".capacity") ?? ""; - this.DiskSpace = fileSystemItemData.GetElementValue(".disk_space") ?? ""; + this.Capacity = fileSystemItemData.GetElementValue(".capacity"); + this.DiskSpace = fileSystemItemData.GetElementValue(".disk_space"); this.Size = fileSystemItemData.GetElementValue(".size") ?? ""; - this.SizeBytes = fileSystemItemData.GetElementValue(".size_bytes") ?? ""; - this.UsedSizeBytes = fileSystemItemData.GetElementValue(".used_size_bytes") ?? ""; - this.AvailableSpace = fileSystemItemData.GetElementValue(".available_space") ?? ""; + this.SizeBytes = fileSystemItemData.GetElementValue(".size_bytes"); + this.UsedSizeBytes = fileSystemItemData.GetElementValue(".used_size_bytes"); + this.AvailableSpace = fileSystemItemData.GetElementValue(".available_space"); this.FreeSpace = fileSystemItemData.GetElementValue(".free_space") ?? ""; - this.FreeSpaceBytes = fileSystemItemData.GetElementValue(".free_space_bytes") ?? ""; + this.FreeSpaceBytes = fileSystemItemData.GetElementValue(".free_space_bytes"); } #endregion } diff --git a/src/libs/entities/ServerItem.cs b/src/libs/entities/ServerItem.cs index f8ccf065..15b8965e 100644 --- a/src/libs/entities/ServerItem.cs +++ b/src/libs/entities/ServerItem.cs @@ -52,6 +52,12 @@ public class ServerItem : Auditable public string Platform { get; set; } = ""; public string IPAddress { get; set; } = ""; public string FQDN { get; set; } = ""; + public float? DiskSpace { get; set; } + #endregion + + #region ServiceNow File System Item Summary Properties + public float? Capacity { get; set; } + public float? AvailableSpace { get; set; } #endregion /// @@ -94,6 +100,7 @@ public ServerItem(int? tenantId, int organizationId, int? operatingSystemItemId, this.Platform = serverData.GetElementValue(".u_platform") ?? ""; this.IPAddress = serverData.GetElementValue(".ip_address") ?? ""; this.FQDN = serverData.GetElementValue(".fqdn") ?? ""; + this.DiskSpace = serverData.GetElementValue(".disk_space"); } #endregion } diff --git a/src/libs/entities/User.cs b/src/libs/entities/User.cs index 9fc8c8a1..a7a62600 100644 --- a/src/libs/entities/User.cs +++ b/src/libs/entities/User.cs @@ -68,11 +68,6 @@ public class User : Auditable /// public bool IsEnabled { get; set; } - /// - /// get/set - Number of failed login attempts. - /// - public int FailedLogins { get; set; } - /// /// get/set - Last time user logged in. /// diff --git a/src/libs/models/FileSystemItemModel.cs b/src/libs/models/FileSystemItemModel.cs index b818add4..0549a9c2 100644 --- a/src/libs/models/FileSystemItemModel.cs +++ b/src/libs/models/FileSystemItemModel.cs @@ -19,14 +19,14 @@ public class FileSystemItemModel : AuditableModel public string StorageType { get; set; } = ""; public string MediaType { get; set; } = ""; public string VolumeId { get; set; } = ""; - public string Capacity { get; set; } = ""; - public string DiskSpace { get; set; } = ""; + public int Capacity { get; set; } + public float DiskSpace { get; set; } public string Size { get; set; } = ""; - public string SizeBytes { get; set; } = ""; - public string UsedSizeBytes { get; set; } = ""; - public string AvailableSpace { get; set; } = ""; + public long SizeBytes { get; set; } + public long? UsedSizeBytes { get; set; } + public int AvailableSpace { get; set; } public string FreeSpace { get; set; } = ""; - public string FreeSpaceBytes { get; set; } = ""; + public long FreeSpaceBytes { get; set; } #endregion #endregion @@ -79,14 +79,14 @@ public FileSystemItemModel(string serverItemServiceNowKey this.StorageType = fileSystemItemModel.Data.StorageType ?? ""; this.MediaType = fileSystemItemModel.Data.MediaType ?? ""; this.VolumeId = fileSystemItemModel.Data.VolumeId ?? ""; - this.Capacity = fileSystemItemModel.Data.Capacity ?? ""; - this.DiskSpace = fileSystemItemModel.Data.DiskSpace ?? ""; + this.Capacity = !String.IsNullOrWhiteSpace(fileSystemItemModel.Data.Capacity) ? Int32.Parse(fileSystemItemModel.Data.Capacity) : 0; + this.DiskSpace = !String.IsNullOrWhiteSpace(fileSystemItemModel.Data.DiskSpace) ? float.Parse(fileSystemItemModel.Data.DiskSpace) : 0; this.Size = fileSystemItemModel.Data.Size ?? ""; - this.SizeBytes = fileSystemItemModel.Data.SizeBytes ?? ""; - this.UsedSizeBytes = fileSystemItemModel.Data.UsedSizeBytes ?? ""; - this.AvailableSpace = fileSystemItemModel.Data.AvailableSpace ?? ""; + this.SizeBytes = !String.IsNullOrWhiteSpace(fileSystemItemModel.Data.SizeBytes) ? long.Parse(fileSystemItemModel.Data.SizeBytes) : 0; + this.UsedSizeBytes = !String.IsNullOrWhiteSpace(fileSystemItemModel.Data.UsedSizeBytes) ? long.Parse(fileSystemItemModel.Data.UsedSizeBytes) : null; + this.AvailableSpace = !String.IsNullOrWhiteSpace(fileSystemItemModel.Data.AvailableSpace) ? Int32.Parse(fileSystemItemModel.Data.AvailableSpace) : 0; this.FreeSpace = fileSystemItemModel.Data.FreeSpace ?? ""; - this.FreeSpaceBytes = fileSystemItemModel.Data.FreeSpaceBytes ?? ""; + this.FreeSpaceBytes = !String.IsNullOrWhiteSpace(fileSystemItemModel.Data.FreeSpaceBytes) ? long.Parse(fileSystemItemModel.Data.FreeSpaceBytes) : 0; } #endregion diff --git a/src/libs/models/Filters/UserFilter.cs b/src/libs/models/Filters/UserFilter.cs index 62f29aba..679d8ad2 100644 --- a/src/libs/models/Filters/UserFilter.cs +++ b/src/libs/models/Filters/UserFilter.cs @@ -1,6 +1,5 @@ namespace HSB.Models.Filters; -using System.Linq.Expressions; using HSB.Core.Extensions; using LinqKit; using Microsoft.EntityFrameworkCore; @@ -18,6 +17,10 @@ public class UserFilter : PageFilter public bool? IsEnabled { get; set; } + public bool? IncludeGroups { get; set; } + + public bool? IncludeTenants { get; set; } + public string[] Sort { get; set; } = Array.Empty(); #endregion @@ -33,6 +36,8 @@ public UserFilter(Dictionary { #region Properties /// - /// + /// get/set - /// public Guid Key { get; set; } /// - /// + /// get/set - /// public IEnumerable Roles { get; set; } = Array.Empty(); /// - /// + /// get/set - /// public IEnumerable Users { get; set; } = Array.Empty(); #endregion diff --git a/src/libs/models/ServerItemModel.cs b/src/libs/models/ServerItemModel.cs index 556beb3a..95312e28 100644 --- a/src/libs/models/ServerItemModel.cs +++ b/src/libs/models/ServerItemModel.cs @@ -30,6 +30,12 @@ public class ServerItemModel : AuditableModel [JsonPropertyName("fqdn")] public string FQDN { get; set; } = ""; + public float? DiskSpace { get; set; } + #endregion + + #region ServiceNow File System Item Summary Properties + public float? Capacity { get; set; } + public float? AvailableSpace { get; set; } #endregion #endregion @@ -55,6 +61,10 @@ public ServerItemModel(ServerItem entity) : base(entity) this.Platform = entity.Platform; this.IPAddress = entity.IPAddress; this.FQDN = entity.FQDN; + this.DiskSpace = entity.DiskSpace; + + this.Capacity = entity.Capacity; + this.AvailableSpace = entity.AvailableSpace; } public ServerItemModel( @@ -84,6 +94,7 @@ public ServerItemModel( this.Platform = serverModel.Data.Platform ?? ""; this.IPAddress = serverModel.Data.IPAddress ?? ""; this.FQDN = serverModel.Data.FQDN ?? ""; + this.DiskSpace = !String.IsNullOrWhiteSpace(serverModel.Data.DiskSpace) ? float.Parse(serverModel.Data.DiskSpace) : null; } #endregion @@ -108,6 +119,9 @@ public static explicit operator ServerItem(ServerItemModel model) Platform = model.Platform, IPAddress = model.IPAddress, FQDN = model.FQDN, + DiskSpace = model.DiskSpace, + Capacity = model.Capacity, + AvailableSpace = model.AvailableSpace, CreatedOn = model.CreatedOn, CreatedBy = model.CreatedBy, UpdatedOn = model.UpdatedOn, diff --git a/src/libs/models/UserModel.cs b/src/libs/models/UserModel.cs index fddaaeae..891ce4b0 100644 --- a/src/libs/models/UserModel.cs +++ b/src/libs/models/UserModel.cs @@ -65,11 +65,6 @@ public class UserModel : AuditableModel /// public bool IsEnabled { get; set; } - /// - /// get/set - Number of failed login attempts. - /// - public int FailedLogins { get; set; } - /// /// get/set - Last time user logged in. /// @@ -108,7 +103,6 @@ public UserModel(User user) : base(user) this.Email = user.Email; this.EmailVerified = user.EmailVerified; this.EmailVerifiedOn = user.EmailVerifiedOn; - this.FailedLogins = user.FailedLogins; this.FirstName = user.FirstName; this.MiddleName = user.MiddleName; this.LastName = user.LastName; @@ -117,6 +111,10 @@ public UserModel(User user) : base(user) this.Note = user.Note; this.Phone = user.Phone; this.Preferences = user.Preferences; + this.Groups = user.GroupsManyToMany.Any() ? user.GroupsManyToMany.Where(g => g.Group != null).Select(g => new GroupModel(g.Group!)) : this.Groups; + this.Groups = user.Groups.Any() ? user.Groups.Select(g => new GroupModel(g)) : this.Groups; + this.Tenants = user.TenantsManyToMany.Any() ? user.TenantsManyToMany.Where(t => t.Tenant != null).Select(t => new TenantModel(t.Tenant!)) : this.Tenants; + this.Tenants = user.Tenants.Any() ? user.Tenants.Select(t => new TenantModel(t)) : this.Tenants; } #endregion @@ -134,7 +132,6 @@ public static explicit operator User(UserModel model) DisplayName = model.DisplayName, EmailVerified = model.EmailVerified, EmailVerifiedOn = model.EmailVerifiedOn, - FailedLogins = model.FailedLogins, FirstName = model.FirstName, MiddleName = model.MiddleName, LastName = model.LastName,