From 38c99d8a350c84ac6e67f5a69c8d0cd7ff2ec2c5 Mon Sep 17 00:00:00 2001 From: Jeremy Foster Date: Fri, 26 Jan 2024 10:30:41 -0800 Subject: [PATCH] HOSTSD-228 Add user admin (#61) Add organization admin --- .../Controllers/OrganizationController.cs | 18 +- .../Admin/Controllers/TenantController.cs | 16 +- .../Areas/Admin/Controllers/UserController.cs | 19 +- .../Controllers/OrganizationController.cs | 8 +- .../Dashboard/Controllers/TenantController.cs | 12 +- .../Controllers/OrganizationController.cs | 10 +- .../Services/Controllers/TenantController.cs | 8 +- .../src/app/api/admin/users/[id]/route.ts | 2 +- .../src/app/client/admin/users/page.tsx | 187 +++++++------ .../admin/organizations/IOrganizationForm.ts | 4 + .../app/hsb/admin/organizations/IUserForm.ts | 4 - .../src/app/hsb/admin/organizations/page.tsx | 235 +++++++++-------- .../hsb/admin/organizations/utils/index.ts | 1 + .../utils/searchOrganizations.ts | 15 ++ src/dashboard/src/app/hsb/admin/page.tsx | 14 - .../src/app/hsb/admin/users/UserDialog.tsx | 180 +++++++++++++ .../src/app/hsb/admin/users/Users.module.scss | 182 ++++++------- .../src/app/hsb/admin/users/page.tsx | 245 +++++++++--------- .../src/app/hsb/admin/users/utils/index.ts | 1 + .../app/hsb/admin/users/utils/searchUsers.ts | 13 + .../src/components/dialog/Dialog.module.scss | 27 ++ .../src/components/dialog/Dialog.tsx | 78 ++++++ src/dashboard/src/components/dialog/index.ts | 1 + .../src/components/filter/Filter.tsx | 4 +- .../src/components/header/Header.tsx | 20 +- .../src/components/header/Message.tsx | 104 ++++++++ src/dashboard/src/components/index.ts | 3 +- src/dashboard/src/hooks/api/admin/index.ts | 2 + .../hooks/api/admin/useApiOrganizations.ts | 31 +++ .../src/hooks/api/admin/useApiUsers.ts | 7 +- .../api/interfaces/IOrganizationFilter.ts | 1 + .../src/hooks/api/interfaces/ITenantFilter.ts | 1 + .../src/hooks/api/interfaces/IUserFilter.ts | 3 +- .../src/hooks/api/interfaces/IUserModel.ts | 3 +- .../src/hooks/api/useApiOrganizations.ts | 4 +- .../src/hooks/data/useOrganizations.ts | 16 +- src/dashboard/src/hooks/data/useUsers.ts | 12 +- .../hooks/filter/useFilteredOrganizations.ts | 10 +- .../src/hooks/filter/useFilteredTenants.ts | 10 +- src/libs/core/Models/ErrorResponseModel.cs | 2 +- .../Services/FileSystemHistoryItemService.cs | 4 + .../dal/Services/FileSystemItemService.cs | 4 + src/libs/dal/Services/IOrganizationService.cs | 2 + src/libs/dal/Services/ITenantService.cs | 17 +- src/libs/dal/Services/IUserService.cs | 1 + .../Services/OperatingSystemItemService.cs | 8 + src/libs/dal/Services/OrganizationService.cs | 34 ++- .../dal/Services/ServerHistoryItemService.cs | 4 + src/libs/dal/Services/ServerItemService.cs | 8 + src/libs/dal/Services/TenantService.cs | 92 ++++--- src/libs/dal/Services/UserService.cs | 51 +++- src/libs/models/Filters/OrganizationFilter.cs | 3 + src/libs/models/Filters/TenantFilter.cs | 7 + src/libs/models/Filters/UserFilter.cs | 7 +- src/libs/models/GroupModel.cs | 2 + src/libs/models/OrganizationModel.cs | 19 +- src/libs/models/TenantModel.cs | 10 +- src/libs/models/UserModel.cs | 8 +- 58 files changed, 1184 insertions(+), 610 deletions(-) create mode 100644 src/dashboard/src/app/hsb/admin/organizations/IOrganizationForm.ts delete mode 100644 src/dashboard/src/app/hsb/admin/organizations/IUserForm.ts create mode 100644 src/dashboard/src/app/hsb/admin/organizations/utils/index.ts create mode 100644 src/dashboard/src/app/hsb/admin/organizations/utils/searchOrganizations.ts delete mode 100644 src/dashboard/src/app/hsb/admin/page.tsx create mode 100644 src/dashboard/src/app/hsb/admin/users/UserDialog.tsx create mode 100644 src/dashboard/src/app/hsb/admin/users/utils/index.ts create mode 100644 src/dashboard/src/app/hsb/admin/users/utils/searchUsers.ts create mode 100644 src/dashboard/src/components/dialog/Dialog.module.scss create mode 100644 src/dashboard/src/components/dialog/Dialog.tsx create mode 100644 src/dashboard/src/components/dialog/index.ts create mode 100644 src/dashboard/src/components/header/Message.tsx create mode 100644 src/dashboard/src/hooks/api/admin/useApiOrganizations.ts diff --git a/src/api/Areas/Admin/Controllers/OrganizationController.cs b/src/api/Areas/Admin/Controllers/OrganizationController.cs index 5617b637..c561cf43 100644 --- a/src/api/Areas/Admin/Controllers/OrganizationController.cs +++ b/src/api/Areas/Admin/Controllers/OrganizationController.cs @@ -55,8 +55,8 @@ 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.OrganizationFilter(query); - var result = _service.Find(filter.GeneratePredicate(), filter.Sort); - return new JsonResult(result.Select(ci => new OrganizationModel(ci))); + var result = _service.Find(filter); + return new JsonResult(result.Select(ci => new OrganizationModel(ci, true))); } /// @@ -75,7 +75,7 @@ public IActionResult GetForId(int id) if (organization == null) return new NoContentResult(); - return new JsonResult(new OrganizationModel(organization)); + return new JsonResult(new OrganizationModel(organization, true)); } /// @@ -93,7 +93,10 @@ public IActionResult Add(OrganizationModel model) var entity = model.ToEntity(); _service.Add(entity); _service.CommitTransaction(); - return CreatedAtAction(nameof(GetForId), new { id = entity.Id }, new OrganizationModel(entity)); + + var result = _service.FindForId(model.Id, true); + if (result == null) return new BadRequestObjectResult(new ErrorResponseModel("Organization does not exist")); + return CreatedAtAction(nameof(GetForId), new { id = result.Id }, new OrganizationModel(result, true)); } /// @@ -111,7 +114,10 @@ public IActionResult Update(OrganizationModel model) var entity = model.ToEntity(); _service.Update(entity); _service.CommitTransaction(); - return new JsonResult(new OrganizationModel(entity)); + + var result = _service.FindForId(model.Id, true); + if (result == null) return new BadRequestObjectResult(new ErrorResponseModel("Organization does not exist")); + return new JsonResult(new OrganizationModel(result, true)); } /// @@ -129,7 +135,7 @@ public IActionResult Remove(OrganizationModel model) var entity = model.ToEntity() ?? throw new NoContentException(); _service.Remove(entity); _service.CommitTransaction(); - return new JsonResult(new OrganizationModel(entity)); + return new JsonResult(new OrganizationModel(entity, false)); } #endregion } diff --git a/src/api/Areas/Admin/Controllers/TenantController.cs b/src/api/Areas/Admin/Controllers/TenantController.cs index 3464001d..b6848c00 100644 --- a/src/api/Areas/Admin/Controllers/TenantController.cs +++ b/src/api/Areas/Admin/Controllers/TenantController.cs @@ -56,7 +56,7 @@ public IActionResult Find() var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query); var filter = new HSB.Models.Filters.TenantFilter(query); var result = _service.Find(filter.GeneratePredicate(), filter.Sort); - return new JsonResult(result.Select(ci => new TenantModel(ci))); + return new JsonResult(result.Select(ci => new TenantModel(ci, true))); } /// @@ -75,7 +75,7 @@ public IActionResult GetForId(int id) if (tenant == null) return new NoContentResult(); - return new JsonResult(new TenantModel(tenant)); + return new JsonResult(new TenantModel(tenant, true)); } /// @@ -93,7 +93,10 @@ public IActionResult Add(TenantModel model) var entity = model.ToEntity(); _service.Add(entity); _service.CommitTransaction(); - return CreatedAtAction(nameof(GetForId), new { id = entity.Id }, new TenantModel(entity)); + + var result = _service.FindForId(model.Id, true); + if (result == null) return new BadRequestObjectResult(new ErrorResponseModel("Tenant does not exist")); + return CreatedAtAction(nameof(GetForId), new { id = result.Id }, new TenantModel(result, true)); } /// @@ -111,7 +114,10 @@ public IActionResult Update(TenantModel model) var entity = model.ToEntity(); _service.Update(entity); _service.CommitTransaction(); - return new JsonResult(new TenantModel(entity)); + + var result = _service.FindForId(model.Id, true); + if (result == null) return new BadRequestObjectResult(new ErrorResponseModel("Tenant does not exist")); + return new JsonResult(new TenantModel(result, true)); } /// @@ -129,7 +135,7 @@ public IActionResult Remove(TenantModel model) var entity = model.ToEntity() ?? throw new NoContentException(); _service.Remove(entity); _service.CommitTransaction(); - return new JsonResult(new TenantModel(entity)); + return new JsonResult(new TenantModel(entity, true)); } #endregion } diff --git a/src/api/Areas/Admin/Controllers/UserController.cs b/src/api/Areas/Admin/Controllers/UserController.cs index 5c50d0e9..3638a1e5 100644 --- a/src/api/Areas/Admin/Controllers/UserController.cs +++ b/src/api/Areas/Admin/Controllers/UserController.cs @@ -75,15 +75,16 @@ public IActionResult Find() /// Get user for the specified 'id'. /// /// + /// /// [HttpGet("{id}", Name = "GetUser-SystemAdmin")] [Produces(MediaTypeNames.Application.Json)] [ProducesResponseType(typeof(UserModel), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NoContent)] [SwaggerOperation(Tags = new[] { "User" })] - public IActionResult GetForId(int id) + public IActionResult GetForId(int id, bool includePermissions) { - var result = _userService.FindForId(id); + var result = _userService.FindForId(id, includePermissions); if (result == null) return new NoContentResult(); return new JsonResult(new UserModel(result)); } @@ -102,10 +103,12 @@ public IActionResult Add(UserModel model) { var user = (Entities.User)model; if (String.IsNullOrWhiteSpace(user.Key) || user.Key == Guid.Empty.ToString()) user.Key = Guid.NewGuid().ToString(); - var result = _userService.Add(user); + var entry = _userService.Add(user); _userService.CommitTransaction(); - var entity = result.Entity; - return CreatedAtAction(nameof(GetForId), new { id = entity.Id }, new UserModel(entity)); + + var result = _userService.FindForId(model.Id, true); + if (result == null) return new BadRequestObjectResult(new ErrorResponseModel("User does not exist")); + return CreatedAtAction(nameof(GetForId), new { id = result.Id }, new UserModel(result)); } /// @@ -133,9 +136,13 @@ public async Task UpdateAsync(UserModel model) } roles = roles.Distinct().ToList(); + // TODO: Only update if roles changed await _cssHelper.UpdateUserRolesAsync(model.Key.ToString(), roles.ToArray()); _userService.CommitTransaction(); - return new JsonResult(new UserModel(entry.Entity)); + + var result = _userService.FindForId(model.Id, true); + if (result == null) return new BadRequestObjectResult(new ErrorResponseModel("User does not exist")); + return new JsonResult(new UserModel(result)); } /// diff --git a/src/api/Areas/Dashboard/Controllers/OrganizationController.cs b/src/api/Areas/Dashboard/Controllers/OrganizationController.cs index 8cdaa7dd..23ae5824 100644 --- a/src/api/Areas/Dashboard/Controllers/OrganizationController.cs +++ b/src/api/Areas/Dashboard/Controllers/OrganizationController.cs @@ -71,7 +71,7 @@ public IActionResult Find() if (isHSB) { var result = _service.Find(filter); - return new JsonResult(result.Select(o => new OrganizationModel(o))); + return new JsonResult(result.Select(o => new OrganizationModel(o, true))); } else { @@ -80,7 +80,7 @@ public IActionResult Find() if (user == null) return Forbid(); var result = _service.FindForUser(user.Id, filter); - return new JsonResult(result.Select(o => new OrganizationModel(o))); + return new JsonResult(result.Select(o => new OrganizationModel(o, true))); } } @@ -101,7 +101,7 @@ public IActionResult GetForId(int id) { var entity = _service.FindForId(id); if (entity == null) return new NoContentResult(); - return new JsonResult(new OrganizationModel(entity)); + return new JsonResult(new OrganizationModel(entity, true)); } else { @@ -111,7 +111,7 @@ public IActionResult GetForId(int id) var entity = _service.FindForUser(user.Id, new HSB.Models.Filters.OrganizationFilter() { Id = id }).FirstOrDefault(); if (entity == null) return Forbid(); - return new JsonResult(new OrganizationModel(entity)); + return new JsonResult(new OrganizationModel(entity, true)); } } #endregion diff --git a/src/api/Areas/Dashboard/Controllers/TenantController.cs b/src/api/Areas/Dashboard/Controllers/TenantController.cs index a857de41..8680d7a6 100644 --- a/src/api/Areas/Dashboard/Controllers/TenantController.cs +++ b/src/api/Areas/Dashboard/Controllers/TenantController.cs @@ -70,7 +70,7 @@ public IActionResult Find() if (isHSB) { var result = _tenantService.Find(filter.GeneratePredicate(), filter.Sort); - return new JsonResult(result.Select(ci => new TenantModel(ci))); + return new JsonResult(result.Select(ci => new TenantModel(ci, true))); } else { @@ -78,8 +78,8 @@ public IActionResult Find() var user = _authorization.GetUser(); if (user == null) return Forbid(); - var result = _tenantService.FindForUser(user.Id, filter.GeneratePredicate(), filter.Sort); - return new JsonResult(result.Select(ci => new TenantModel(ci))); + var result = _tenantService.FindForUser(user.Id, filter); + return new JsonResult(result.Select(ci => new TenantModel(ci, true))); } } @@ -100,7 +100,7 @@ public IActionResult GetForId(int id) { var entity = _tenantService.FindForId(id); if (entity == null) return new NoContentResult(); - return new JsonResult(new TenantModel(entity)); + return new JsonResult(new TenantModel(entity, true)); } else { @@ -108,9 +108,9 @@ public IActionResult GetForId(int id) var user = _authorization.GetUser(); if (user == null) return Forbid(); - var entity = _tenantService.FindForUser(user.Id, (t) => t.Id == id, t => t.Id).FirstOrDefault(); + var entity = _tenantService.FindForUser(user.Id, new HSB.Models.Filters.TenantFilter() { Id = id }).FirstOrDefault(); if (entity == null) return Forbid(); - return new JsonResult(new TenantModel(entity)); + return new JsonResult(new TenantModel(entity, true)); } } #endregion diff --git a/src/api/Areas/Services/Controllers/OrganizationController.cs b/src/api/Areas/Services/Controllers/OrganizationController.cs index b757226f..50360be9 100644 --- a/src/api/Areas/Services/Controllers/OrganizationController.cs +++ b/src/api/Areas/Services/Controllers/OrganizationController.cs @@ -51,7 +51,7 @@ public OrganizationController(IOrganizationService service, ILogger true); - return new JsonResult(organizations.Select(ci => new OrganizationModel(ci))); + return new JsonResult(organizations.Select(ci => new OrganizationModel(ci, true))); } /// @@ -70,7 +70,7 @@ public IActionResult GetForId(int id) if (entity == null) return new NoContentResult(); - return new JsonResult(new OrganizationModel(entity)); + return new JsonResult(new OrganizationModel(entity, true)); } /// @@ -92,14 +92,14 @@ public IActionResult AddOrUpdate(OrganizationModel model) { _service.Add(entity); _service.CommitTransaction(); - return CreatedAtAction(nameof(GetForId), new { id = entity.Id }, new OrganizationModel(entity)); + return CreatedAtAction(nameof(GetForId), new { id = entity.Id }, new OrganizationModel(entity, true)); } else { _service.ClearChangeTracker(); // Remove existing from context. _service.Update(entity); _service.CommitTransaction(); - return new JsonResult(new OrganizationModel(entity)); + return new JsonResult(new OrganizationModel(entity, true)); } } @@ -118,7 +118,7 @@ public IActionResult Update(OrganizationModel model) var entity = model.ToEntity(); _service.Update(entity); _service.CommitTransaction(); - return new JsonResult(new OrganizationModel(entity)); + return new JsonResult(new OrganizationModel(entity, true)); } #endregion } diff --git a/src/api/Areas/Services/Controllers/TenantController.cs b/src/api/Areas/Services/Controllers/TenantController.cs index 46d11888..62810044 100644 --- a/src/api/Areas/Services/Controllers/TenantController.cs +++ b/src/api/Areas/Services/Controllers/TenantController.cs @@ -51,7 +51,7 @@ public TenantController(ITenantService service, ILogger logger public IActionResult Find() { var tenants = _service.Find(o => true); - return new JsonResult(tenants.Select(ci => new TenantModel(ci))); + return new JsonResult(tenants.Select(ci => new TenantModel(ci, true))); } /// @@ -70,7 +70,7 @@ public IActionResult GetForId(int id) if (tenant == null) return new NoContentResult(); - return new JsonResult(new TenantModel(tenant)); + return new JsonResult(new TenantModel(tenant, true)); } /// @@ -88,7 +88,7 @@ public IActionResult Add(TenantModel model) var entity = model.ToEntity(); _service.Add(entity); _service.CommitTransaction(); - return CreatedAtAction(nameof(GetForId), new { id = entity.Id }, new TenantModel(entity)); + return CreatedAtAction(nameof(GetForId), new { id = entity.Id }, new TenantModel(entity, true)); } /// @@ -106,7 +106,7 @@ public IActionResult Update(TenantModel model) var entity = model.ToEntity(); _service.Update(entity); _service.CommitTransaction(); - return new JsonResult(new TenantModel(entity)); + return new JsonResult(new TenantModel(entity, true)); } #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 6cc40df9..bcdf4f31 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/admin/users/${context.params.id}`); + return await dispatch(`/v1/admin/users/${context.params.id}${url.search}`); } export async function PUT(req: Request, context: { params: any }) { diff --git a/src/dashboard/src/app/client/admin/users/page.tsx b/src/dashboard/src/app/client/admin/users/page.tsx index 8547867b..6048965d 100644 --- a/src/dashboard/src/app/client/admin/users/page.tsx +++ b/src/dashboard/src/app/client/admin/users/page.tsx @@ -2,28 +2,18 @@ import styles from './ClientAdmin.module.scss'; -import { - Button, - Checkbox, - Info, - Overlay, - Select, - Sheet, - Spinner, - Table, - Text, -} from '@/components'; +import { Button, Checkbox, Info, Overlay, Select, Sheet, Spinner, Table, Text } from '@/components'; import { IUserModel, useAuth } from '@/hooks'; -import { redirect } from 'next/navigation'; import { useApiUsers } from '@/hooks/api/admin'; import { useUsers } from '@/hooks/data'; +import { redirect } from 'next/navigation'; import React from 'react'; import { IUserForm } from './IUserForm'; export default function Page() { const state = useAuth(); - const { isReady: isReadyUsers, users } = useUsers({ includeGroups: true, init: true }); + const { isReady: isReadyUsers, users } = useUsers({ includePermissions: true, init: true }); const { update: updateUser } = useApiUsers(); const [loading, setLoading] = React.useState(true); @@ -82,101 +72,100 @@ export default function Page() { const results = await Promise.all(update); setRecords(results); }, [updateUser, records]); - + // Only allow Organization Admin role to view this page. if (state.status === 'loading') return
Loading...
; if (!state.isOrganizationAdmin) redirect('/'); return ( - +
- {loading && ( - - - - )} -
-
- + setFilter(e.target.value)} + onKeyDown={(e) => { + if (e.code === 'Enter') handleSearch(); + }} + /> + +
+ + Enable access to the dashboard and/or admin access to users associated with{' '} + {organization}: + +
+
+ +
Access
+
Username
+
Email
+
Name
+
Admin
+ + } + > + {({ data }) => { + return ( + <> +
+ { + setRecords((records) => + records.map((r) => + r.id === data.id + ? { ...data, isEnabled: e.target.checked, isDirty: true } + : r, + ), + ); + }} + /> +
+
{data.username}
+
{data.email}
+
{data.displayName}
+
+ { + setRecords((records) => + records.map((r) => + r.id === data.id + ? { ...data, isEnabled: e.target.checked, isDirty: true } + : r, + ), + ); + }} + /> +
+ + ); }} - /> -
+
+
+
- - Enable access to the dashboard and/or admin access to users associated with {organization}: - -
-
- -
Access
-
Username
-
Email
-
Name
-
Admin
- - } - > - {({ data }) => { - return ( - <> -
- { - setRecords((records) => - records.map((r) => - r.id === data.id - ? { ...data, isEnabled: e.target.checked, isDirty: true } - : r, - ), - ); - }} - /> -
-
{data.username}
-
{data.email}
-
{data.displayName}
-
- { - setRecords((records) => - records.map((r) => - r.id === data.id - ? { ...data, isEnabled: e.target.checked, isDirty: true } - : r, - ), - ); - }} - /> -
- - ); - }} -
-
-
- -
); diff --git a/src/dashboard/src/app/hsb/admin/organizations/IOrganizationForm.ts b/src/dashboard/src/app/hsb/admin/organizations/IOrganizationForm.ts new file mode 100644 index 00000000..e4b3b2b0 --- /dev/null +++ b/src/dashboard/src/app/hsb/admin/organizations/IOrganizationForm.ts @@ -0,0 +1,4 @@ +import { IFormRecord } from '@/components/forms'; +import { IOrganizationModel } from '@/hooks'; + +export interface IOrganizationForm extends IOrganizationModel, IFormRecord {} diff --git a/src/dashboard/src/app/hsb/admin/organizations/IUserForm.ts b/src/dashboard/src/app/hsb/admin/organizations/IUserForm.ts deleted file mode 100644 index 22a56a3c..00000000 --- a/src/dashboard/src/app/hsb/admin/organizations/IUserForm.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { IFormRecord } from '@/components/forms'; -import { IUserModel } from '@/hooks'; - -export interface IUserForm extends IUserModel, IFormRecord {} diff --git a/src/dashboard/src/app/hsb/admin/organizations/page.tsx b/src/dashboard/src/app/hsb/admin/organizations/page.tsx index e4ea915c..684b686e 100644 --- a/src/dashboard/src/app/hsb/admin/organizations/page.tsx +++ b/src/dashboard/src/app/hsb/admin/organizations/page.tsx @@ -2,66 +2,73 @@ import styles from './Organizations.module.scss'; -import { - Button, - Checkbox, - Info, - Overlay, - Sheet, - Spinner, - Table, - Text, -} from '@/components'; -import { IUserModel, useAuth } from '@/hooks'; +import { Button, Checkbox, Info, Overlay, Sheet, Spinner, Table, Text } from '@/components'; +import { IOrganizationModel, useAuth } from '@/hooks'; +import { useApiOrganizations } from '@/hooks/api/admin'; +import { useOrganizations } from '@/hooks/data'; +import { useApp } from '@/store'; import { redirect } from 'next/navigation'; -import { useApiUsers } from '@/hooks/api/admin'; -import { useGroups, useUsers } from '@/hooks/data'; import React from 'react'; -import { IUserForm } from './IUserForm'; +import { IOrganizationForm } from './IOrganizationForm'; +import { searchOrganizations } from './utils'; export default function Page() { const state = useAuth(); - const { isReady: isReadyUsers, users } = useUsers({ includeGroups: true, init: true }); - const { isReady: isReadyGroups, groups, options: groupOptions } = useGroups({ init: true }); - const { update: updateUser } = useApiUsers(); + const { isReady: isReadyOrganizations, organizations } = useOrganizations({ + init: true, + includeTenants: true, + }); + const { update: updateOrganization } = useApiOrganizations(); + const setOrganizations = useApp((state) => state.setOrganizations); const [loading, setLoading] = React.useState(true); - const [records, setRecords] = React.useState([]); - const [items, setItems] = React.useState([]); + const [formOrganizations, setFormOrganizations] = React.useState([]); + const [filteredOrganizations, setFilteredOrganizations] = React.useState([]); const [filter, setFilter] = React.useState(''); const [isSubmitting, setIsSubmitting] = React.useState(false); React.useEffect(() => { - setLoading(!isReadyUsers && !isReadyGroups); - }, [isReadyUsers, isReadyGroups]); + setLoading(!isReadyOrganizations); + }, [isReadyOrganizations]); React.useEffect(() => { - setItems(records); - }, [records]); - - React.useEffect(() => { - setRecords((state) => - users.map((user) => { - const value = state.find((s) => s.id === user.id); - if (value) ({ ...user, isDirty: value.isDirty }); - return user; + setFormOrganizations((state) => + organizations.map((organization) => { + const value = state.find((s) => s.id === organization.id); + if (value) ({ ...organization, isDirty: value.isDirty }); + return organization; }), ); - setItems(users); - }, [users]); + setFilteredOrganizations(searchOrganizations(organizations, filter).map((o) => o.id)); + }, [filter, organizations]); const handleSearch = React.useCallback(() => { - setItems( - filter - ? records.filter( - (r) => - r.username.includes(filter) || - r.displayName.includes(filter) || - r.email.includes(filter), - ) - : records, - ); - }, [filter, records]); + setFilteredOrganizations(searchOrganizations(formOrganizations, filter).map((o) => o.id)); + }, [filter, formOrganizations]); + + const handleUpdate = React.useCallback(async () => { + const update = formOrganizations.map(async (organization) => { + if (organization.isDirty) { + try { + setIsSubmitting(true); + const res = await updateOrganization(organization); + const result: IOrganizationModel = await res.json(); + return { ...result, isDirty: false }; + } catch (error) { + console.error(error); + } finally { + setIsSubmitting(false); + } + } + return organization; + }); + const results = await Promise.all(update); + const updatedOrganizations = formOrganizations.map((organization) => { + return results.find((u) => u.id === organization.id) ?? organization; + }); + setFormOrganizations(updatedOrganizations); + setOrganizations(updatedOrganizations); + }, [formOrganizations, setOrganizations, updateOrganization]); // Only allow System Admin role to view this page. if (state.status === 'loading') return
Loading...
; @@ -69,76 +76,80 @@ export default function Page() { return (
- -
- {loading && ( - - - - )} -
- - Find an organization by name and/or associated tenant. Click checkbox to enable or disable organization on dashboard. - -
- setFilter(e.target.value)} - onKeyDown={(e) => { - if (e.code === 'Enter') handleSearch(); - }} - /> - + +
+ {loading && ( + + + + )} +
+ + Find an organization by name and/or associated tenant. Click checkbox to enable or + disable organization on dashboard. + +
+ setFilter(e.target.value)} + onKeyDown={(e) => { + if (e.code === 'Enter') handleSearch(); + }} + /> + +
+
+
+ + filteredOrganizations.some((fo) => fo === o.id), + )} + header={ + <> +
Organization Name
+
Associated Tenants
+
Dashboard Enabled
+ + } + > + {({ data }) => { + return ( + <> +
{data.name}
+
{data.tenants?.map((tenant) => tenant.name).join(', ')}
+
+ { + setFormOrganizations((formOrganizations) => + formOrganizations.map((r) => + r.id === data.id + ? { ...data, isEnabled: e.target.checked, isDirty: true } + : r, + ), + ); + }} + /> +
+ + ); + }} +
+
+
+ +
-
-
- -
Organization Name
-
Associated Tenants
-
Dashboard Enabled
- - } - > - {({ data }) => { - return ( - <> -
organization name
-
tenant name
-
- { - setRecords((records) => - records.map((r) => - r.id === data.id - ? { ...data, isEnabled: e.target.checked, isDirty: true } - : r, - ), - ); - }} - /> -
- - ); - }} -
-
-
- -
-
- +
); } diff --git a/src/dashboard/src/app/hsb/admin/organizations/utils/index.ts b/src/dashboard/src/app/hsb/admin/organizations/utils/index.ts new file mode 100644 index 00000000..f7d0ad12 --- /dev/null +++ b/src/dashboard/src/app/hsb/admin/organizations/utils/index.ts @@ -0,0 +1 @@ +export * from './searchOrganizations'; diff --git a/src/dashboard/src/app/hsb/admin/organizations/utils/searchOrganizations.ts b/src/dashboard/src/app/hsb/admin/organizations/utils/searchOrganizations.ts new file mode 100644 index 00000000..ea83feed --- /dev/null +++ b/src/dashboard/src/app/hsb/admin/organizations/utils/searchOrganizations.ts @@ -0,0 +1,15 @@ +import { IOrganizationModel } from '@/hooks'; + +export const searchOrganizations = (organizations: IOrganizationModel[], search?: string) => { + const value = search?.toLowerCase(); + return value + ? organizations.filter( + (r) => + r.name.toLowerCase().includes(value) || + r.code.toLowerCase().includes(value) || + r.tenants?.some( + (t) => t.name.toLowerCase().includes(value) || t.code.toLowerCase().includes(value), + ), + ) + : organizations; +}; diff --git a/src/dashboard/src/app/hsb/admin/page.tsx b/src/dashboard/src/app/hsb/admin/page.tsx deleted file mode 100644 index 8dedbb72..00000000 --- a/src/dashboard/src/app/hsb/admin/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client'; - -import { useAuth } from '@/hooks'; -import { redirect } from 'next/navigation'; - -export default function Page() { - const state = useAuth(); - - // Only allow System Admin role to view this page. - if (state.status === 'loading') return
Loading...
; - if (!state.isSystemAdmin) redirect('/'); - - return
HSB Admin
; -} diff --git a/src/dashboard/src/app/hsb/admin/users/UserDialog.tsx b/src/dashboard/src/app/hsb/admin/users/UserDialog.tsx new file mode 100644 index 00000000..4050abf8 --- /dev/null +++ b/src/dashboard/src/app/hsb/admin/users/UserDialog.tsx @@ -0,0 +1,180 @@ +import { Button, Checkbox, Dialog, IDialogProps } from '@/components'; +import { Text } from '@/components/forms/text'; +import { useGroups, useOrganizations, useTenants } from '@/hooks/data'; +import React from 'react'; +import { IUserForm } from './IUserForm'; +import styles from './Users.module.scss'; + +export type UserDialogVariant = 'organization' | 'tenant' | 'group'; + +export interface IUserDialogProps extends Omit { + variant?: UserDialogVariant; + user?: IUserForm; + onChange: (user: IUserForm) => void; + onSave: (user?: IUserForm) => void; +} + +export const UserDialog = React.forwardRef(function UserDialog( + { variant, user: initUser, header, actions, onChange, onSave, ...rest }, + ref, +) { + const { groups } = useGroups({ init: true }); + const { organizations } = useOrganizations({ init: true }); + const { tenants } = useTenants({ init: true }); + + const [filteredGroups, setFilteredGroups] = React.useState(groups); + const [filteredOrganizations, setFilteredOrganizations] = React.useState(organizations); + const [filteredTenants, setFilteredTenants] = React.useState(tenants); + const [form, setForm] = React.useState(initUser); + + React.useEffect(() => { + setFilteredGroups(groups); + }, [groups]); + + React.useEffect(() => { + setFilteredOrganizations(organizations); + }, [organizations]); + + React.useEffect(() => { + setFilteredTenants(tenants); + }, [tenants]); + + React.useEffect(() => { + setForm(initUser); + }, [initUser]); + + var title = React.useMemo(() => { + if (variant === 'group') { + return 'Editing roles'; + } else if (variant === 'organization') { + return 'Allow access to the following organizations'; + } else if (variant === 'tenant') { + return 'Allow access to the following tenants'; + } + return ''; + }, [variant]); + + var options: React.ReactElement[] = React.useMemo(() => { + if (variant === 'group') { + return filteredGroups.map((group, index) => ( +
+ g.id === group.id) === true} + onChange={(e) => { + if (form) { + const user = { + ...form, + groups: e.target.checked + ? [...(form.groups ?? []), group] + : form.groups?.filter((g) => g.id !== group.id), + }; + setForm(user); + onChange?.(user); + } + }} + /> +

+ +

+
+ )); + } else if (variant === 'organization') { + return filteredOrganizations.map((organization, index) => ( +
+ g.id === organization.id) === true} + onChange={(e) => { + if (form) { + const user = { + ...form, + organizations: e.target.checked + ? [...(form.organizations ?? []), organization] + : form.organizations?.filter((g) => g.id !== organization.id), + }; + setForm(user); + onChange?.(user); + } + }} + /> +

+ +

+
+ )); + } else if (variant === 'tenant') { + return filteredTenants.map((tenant, index) => ( +
+ g.id === tenant.id) === true} + onChange={(e) => { + if (form) { + const user = { + ...form, + tenants: e.target.checked + ? [...(form.tenants ?? []), tenant] + : form.tenants?.filter((g) => g.id !== tenant.id), + }; + setForm(user); + onChange?.(user); + } + }} + /> +

+ +

+
+ )); + } + return []; + }, [variant, filteredGroups, form, onChange, filteredOrganizations, filteredTenants]); + + const handleSearch = React.useCallback( + (value: string) => { + if (variant === 'group') { + setFilteredGroups( + groups.filter((group) => group.name.toLowerCase().includes(value.toLowerCase())), + ); + } else if (variant === 'organization') { + setFilteredOrganizations( + organizations.filter((organization) => + organization.name.toLowerCase().includes(value.toLowerCase()), + ), + ); + } else if (variant === 'tenant') { + setFilteredTenants( + tenants.filter((tenant) => tenant.name.toLowerCase().includes(value.toLowerCase())), + ); + } + }, + [variant, groups, organizations, tenants], + ); + + return ( + + {title} for {form?.username} +

+ } + actions={} + {...rest} + ref={ref} + > +
+ handleSearch(e.target.value)} + /> +
+
{options}
+
+ ); +}); diff --git a/src/dashboard/src/app/hsb/admin/users/Users.module.scss b/src/dashboard/src/app/hsb/admin/users/Users.module.scss index 58e569cf..86b5cdba 100644 --- a/src/dashboard/src/app/hsb/admin/users/Users.module.scss +++ b/src/dashboard/src/app/hsb/admin/users/Users.module.scss @@ -1,135 +1,109 @@ @import '@/styles/utils.scss'; .search { - margin-bottom: 34px; + margin-bottom: 34px; - input { - width: 100%; - max-width: 520px; - } + input { + width: 100%; + max-width: 520px; + } } .checkbox label { - top: -8px; + top: -8px; } .selectRow { - display: flex; - flex-direction: column; - - >div { - display: flex; - align-items: center; - padding: 5px 0; - border-radius: 4px; - - &:hover { - background-color: $white; - } - - >p { - width: 100%; - margin: 0 5px; - font-size: $font-size-small; - color: $dark-gray; - - span { - font-weight: bold; - color: $bc-black; - } - } - - button { - flex-direction: flex-end; - font-size: $font-size-small; - padding: 5px 8px; - margin-right: 10px; - - &:hover { - background-color: $chart-blue; - color: $white; - } - } - } -} + display: flex; + flex-direction: column; -.container .table>div { - .tableHeader { - flex: 0.5; + >div { + display: flex; + align-items: center; + padding: 5px 0; + border-radius: 4px; - &+div { - flex: 1.5; - } + &:hover { + background-color: $white; } - .checkbox { - flex: 0.5; - } + >p { + width: 100%; + margin: 0 5px; + font-size: $font-size-small; + color: $dark-gray; - .selectRow { - flex: 1.5; + span { + font-weight: bold; + color: $bc-black; + } } -} -.popup { - width: 100%; - max-width: 450px; - min-height: 200px; - border: 0; - border-radius: 10px; - @include dropshadow; + button { + flex-direction: flex-end; + font-size: $font-size-small; + padding: 5px 8px; + margin-right: 10px; + + &:hover { + background-color: $chart-blue; + color: $white; + } + } + } } -.popupSearch { - padding: 12px; - background-color: $light-gray; - border-radius: 4px; +.container .table>div { + .tableHeader { + flex: 0.5; - input { - background-color: $white; - width: -webkit-fill-available; - margin-right: 0; + &+div { + flex: 1.5; } + } + + .checkbox { + flex: 0.5; + } + + .selectRow { + flex: 1.5; + } } -.popupTitle { - font-weight: bold; - text-align: center; - margin-bottom: 20px; +.popupSearch { + padding: 12px; + background-color: $light-gray; + border-radius: 4px; + + input { + background-color: $white; + width: -webkit-fill-available; + margin-right: 0; + } } .popupRows { - overflow-y: scroll; - min-height: 50px; - max-height: 400px; - @include scrollBar; - margin-bottom: 20px; - - .row { - display: flex; - align-items: center; - min-height: 65px; - border-bottom: 1px solid $medium-gray; - gap: 1rem; - padding-left: 16px; - - &:hover { - background-color: $light-gray; - } - - label { - top: -7px; - } - } -} + overflow-y: scroll; + min-height: 50px; + max-height: 400px; + @include scrollBar; + margin-bottom: 20px; -.popupFooter { + .row { display: flex; - justify-content: flex-end; + align-items: center; + min-height: 65px; + border-bottom: 1px solid $medium-gray; gap: 1rem; + padding-left: 16px; - button { - width: 100px; - padding: 8px; + &:hover { + background-color: $light-gray; } -} + + label { + top: -7px; + } + } +} \ No newline at end of file diff --git a/src/dashboard/src/app/hsb/admin/users/page.tsx b/src/dashboard/src/app/hsb/admin/users/page.tsx index 8f58e243..1219efe5 100644 --- a/src/dashboard/src/app/hsb/admin/users/page.tsx +++ b/src/dashboard/src/app/hsb/admin/users/page.tsx @@ -5,14 +5,13 @@ import styles from './Users.module.scss'; import { Button, Checkbox, + ConfirmPopup, Info, Overlay, - Select, Sheet, Spinner, Table, Text, - ConfirmPopup, } from '@/components'; import { IUserModel, useAuth } from '@/hooks'; import { useApiUsers } from '@/hooks/api/admin'; @@ -20,92 +19,78 @@ import { useGroups, useUsers } from '@/hooks/data'; import { redirect } from 'next/navigation'; import React, { useRef } from 'react'; import { IUserForm } from './IUserForm'; +import { UserDialog, UserDialogVariant } from './UserDialog'; +import { searchUsers } from './utils'; export default function Page() { const state = useAuth(); - const { isReady: isReadyUsers, users } = useUsers({ includeGroups: true, init: true }); - const { isReady: isReadyGroups, groups, options: groupOptions } = useGroups({ init: true }); + const { isReady: isReadyUsers, users } = useUsers({ includePermissions: true, init: true }); + const { isReady: isReadyGroups } = useGroups({ init: true }); const { update: updateUser } = useApiUsers(); const [loading, setLoading] = React.useState(true); - const [records, setRecords] = React.useState([]); - const [items, setItems] = React.useState([]); + const [formUsers, setFormUsers] = React.useState([]); + const [filteredUsers, setFilteredUsers] = React.useState([]); const [filter, setFilter] = React.useState(''); const [isSubmitting, setIsSubmitting] = React.useState(false); + const dialogRef = useRef(null); - const [currentUsername, setCurrentUsername] = React.useState(''); - const [currentEditContext, setCurrentEditContext] = React.useState(''); + const [dialog, setDialog] = React.useState<{ user: IUserForm; variant: UserDialogVariant }>(); const [showPopup, setShowPopup] = React.useState(false); - const handleEditClick = (username: string, context: string) => { - setCurrentUsername(username); - setCurrentEditContext(context); - if (dialogRef.current) { - dialogRef.current.showModal(); - } - }; - - const closeDialog = () => { - if (dialogRef.current) { - dialogRef.current.close(); - } - }; - React.useEffect(() => { setLoading(!isReadyUsers && !isReadyGroups); }, [isReadyUsers, isReadyGroups]); React.useEffect(() => { - setItems(records); - }, [records]); - - React.useEffect(() => { - setRecords((state) => + setFormUsers((state) => users.map((user) => { const value = state.find((s) => s.id === user.id); if (value) ({ ...user, isDirty: value.isDirty }); return user; }), ); - setItems(users); - }, [users]); + setFilteredUsers(searchUsers(users, filter).map((u) => u.id)); + }, [filter, users]); const handleSearch = React.useCallback(() => { - setItems( - filter - ? records.filter( - (r) => - r.username.includes(filter) || - r.displayName.includes(filter) || - r.email.includes(filter), - ) - : records, - ); - }, [filter, records]); + setFilteredUsers(searchUsers(formUsers, filter).map((u) => u.id)); + }, [filter, formUsers]); const handleUpdate = React.useCallback(async () => { - const update = records.map(async (user) => { + const update = formUsers.map(async (user) => { if (user.isDirty) { try { + setIsSubmitting(true); const res = await updateUser(user); const result: IUserModel = await res.json(); return { ...result, isDirty: false }; } catch (error) { console.error(error); + } finally { + setIsSubmitting(false); } } return user; }); const results = await Promise.all(update); - setRecords(results); - }, [updateUser, records]); + setFormUsers((users) => + users.map((user) => { + return results.find((u) => u.id === user.id) ?? user; + }), + ); + }, [updateUser, formUsers]); + + const handleEditClick = (user: IUserForm, variant: UserDialogVariant) => { + setDialog({ user, variant }); + dialogRef.current?.showModal(); + }; // Only allow System Admin role to view this page. if (state.status === 'loading') return
Loading...
; if (!state.isSystemAdmin) redirect('/'); return ( - {showPopup && ( )} - -

{currentEditContext} for {currentUsername}

-
- -
-
-
- -

placeholder

-
-
-
- - -
-
+ + setFormUsers((formUsers) => + formUsers.map((u) => (u.id === data.id ? { ...data, isDirty: true } : u)), + ) + } + onSave={async () => { + await handleUpdate(); + dialogRef.current?.close(); + }} + />
{loading && ( @@ -161,67 +140,83 @@ export default function Page() {
- -
- -
Username
-
Email
-
Name
-
Enabled
-
Roles, Tenants, Organizations
- - } - > - {({ data }) => { - return ( - <> -
{data.username}
-
{data.email}
-
{data.displayName}
-
- { - setRecords((records) => - records.map((r) => - r.id === data.id - ? { ...data, isEnabled: e.target.checked, isDirty: true } - : r, - ), - ); - }} - /> -
-
-
-

Roles: role, role, role

- -
-
-

Organizations: Organization name, Organization name, Organization name

- -
-
-

Tenant: Tenant name, Tenant name, Tenant name

- -
-
- - ); - }} -
-
-
- -
+ +
+ filteredUsers.some((fu) => fu === u.id))} + header={ + <> +
Username
+
Email
+
Name
+
Enabled
+
Roles, Tenants, Organizations
+ + } + > + {({ data }) => { + return ( + <> +
{data.username}
+
{data.email}
+
{data.displayName}
+
+ { + setFormUsers((formUsers) => + formUsers.map((r) => + r.id === data.id + ? { ...data, isEnabled: e.target.checked, isDirty: true } + : r, + ), + ); + }} + /> +
+
+
+

+ Roles: role, role, role +

+ +
+
+

+ Organizations: Organization name, Organization name, + Organization name +

+ +
+
+

+ Tenant: Tenant name, Tenant name, Tenant name +

+ +
+
+ + ); + }} +
+
+
+ +
); diff --git a/src/dashboard/src/app/hsb/admin/users/utils/index.ts b/src/dashboard/src/app/hsb/admin/users/utils/index.ts new file mode 100644 index 00000000..172443dc --- /dev/null +++ b/src/dashboard/src/app/hsb/admin/users/utils/index.ts @@ -0,0 +1 @@ +export * from './searchUsers'; diff --git a/src/dashboard/src/app/hsb/admin/users/utils/searchUsers.ts b/src/dashboard/src/app/hsb/admin/users/utils/searchUsers.ts new file mode 100644 index 00000000..96baa830 --- /dev/null +++ b/src/dashboard/src/app/hsb/admin/users/utils/searchUsers.ts @@ -0,0 +1,13 @@ +import { IUserModel } from '@/hooks'; + +export const searchUsers = (users: IUserModel[], search?: string) => { + const value = search?.toLowerCase(); + return value + ? users.filter( + (r) => + r.username.toLowerCase().includes(value) || + r.displayName.toLowerCase().includes(value) || + r.email.toLowerCase().includes(value), + ) + : users; +}; diff --git a/src/dashboard/src/components/dialog/Dialog.module.scss b/src/dashboard/src/components/dialog/Dialog.module.scss new file mode 100644 index 00000000..8bceac65 --- /dev/null +++ b/src/dashboard/src/components/dialog/Dialog.module.scss @@ -0,0 +1,27 @@ +@import '@/styles/utils.scss'; + +.popup { + width: 100%; + max-width: 450px; + min-height: 200px; + border: 0; + border-radius: 10px; + @include dropshadow; +} + +.popupTitle { + font-weight: bold; + text-align: center; + margin-bottom: 20px; +} + +.popupFooter { + display: flex; + justify-content: flex-end; + gap: 1rem; + + button { + width: 100px; + padding: 8px; + } +} \ No newline at end of file diff --git a/src/dashboard/src/components/dialog/Dialog.tsx b/src/dashboard/src/components/dialog/Dialog.tsx new file mode 100644 index 00000000..e456e1cc --- /dev/null +++ b/src/dashboard/src/components/dialog/Dialog.tsx @@ -0,0 +1,78 @@ +import { Button } from '@/components'; +import React from 'react'; +import styles from './Dialog.module.scss'; + +export interface IDialogProps extends React.AllHTMLAttributes { + show?: boolean; + header?: React.ReactElement; + actions?: React.ReactNode; + showCancel?: boolean; + cancelLabel?: React.ReactNode; + showActions?: boolean; +} + +/** + * Provides a dialog component with a default styling and cancel button. + * returns Component + */ +export const Dialog = React.forwardRef(function Dialog( + { + show: initShow, + header, + actions, + cancelLabel = 'Cancel', + showCancel = true, + showActions = true, + children, + ...rest + }, + ref, +) { + const dialogRef = React.useRef(null); + const [show, setShow] = React.useState(initShow); + + React.useImperativeHandle(ref, () => ({ + ...dialogRef.current!, + showModal() { + dialogRef.current?.showModal(); + }, + close() { + dialogRef.current?.close(); + }, + })); + + React.useEffect(() => { + setShow(initShow); + }, [initShow]); + + React.useEffect(() => { + if (show) dialogRef.current?.showModal(); + else dialogRef.current?.close(); + }, [show]); + + const closeDialog = () => { + dialogRef.current?.close(); + setShow(false); + }; + + const Header = React.cloneElement(header ?? <>, { + className: `${styles.popupTitle} ${header?.props.className}`, + }); + + return ( + + {Header} + {children} + {showActions && ( +
+ {showCancel && ( + + )} + {actions} +
+ )} +
+ ); +}); diff --git a/src/dashboard/src/components/dialog/index.ts b/src/dashboard/src/components/dialog/index.ts new file mode 100644 index 00000000..a5d31597 --- /dev/null +++ b/src/dashboard/src/components/dialog/index.ts @@ -0,0 +1 @@ +export * from './Dialog'; diff --git a/src/dashboard/src/components/filter/Filter.tsx b/src/dashboard/src/components/filter/Filter.tsx index 967020fb..30a83479 100644 --- a/src/dashboard/src/components/filter/Filter.tsx +++ b/src/dashboard/src/components/filter/Filter.tsx @@ -17,14 +17,12 @@ import { } from '@/hooks/filter'; import { useFiltered } from '@/store'; import moment from 'moment'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useSearchParams } from 'next/navigation'; import React from 'react'; import styles from './Filter.module.scss'; import { useUrlParamsUpdateKey } from './hooks'; export const Filter: React.FC = () => { - const router = useRouter(); - const path = usePathname(); const params = useSearchParams(); const { isHSB } = useAuth(); const { isReady: tenantsReady, tenants } = useTenants({ init: true }); diff --git a/src/dashboard/src/components/header/Header.tsx b/src/dashboard/src/components/header/Header.tsx index 35db8a50..b9ab70c9 100644 --- a/src/dashboard/src/components/header/Header.tsx +++ b/src/dashboard/src/components/header/Header.tsx @@ -9,7 +9,12 @@ import Link from 'next/link'; import { redirect, usePathname } from 'next/navigation'; import React from 'react'; import { AuthState } from '../auth'; +import { Message } from './Message'; +/** + * Provides the header for the application with the navigation. + * @returns Component + */ export const Header: React.FC = () => { const path = usePathname(); const { @@ -35,12 +40,13 @@ export const Header: React.FC = () => { const isDashboardView = path.includes('/dashboard'); const isServerView = path.includes('/servers'); const isAdminView = path.includes('/admin'); + const showAdminNav = isDashboardView || isAdminView || isServerView; return ( <>
@@ -61,11 +67,7 @@ export const Header: React.FC = () => { <>
{infoIcon && } -

- Welcome to the storage dashboard. This is an overview of the storage consumption - for all organizations you belong to.
- Use the filters to see further breakdowns of storage data. -

+
{isAuthorized && ( @@ -81,9 +83,9 @@ export const Header: React.FC = () => { > Storage - {(isOrganizationAdmin || isSystemAdmin) && ( + {(isOrganizationAdmin || isSystemAdmin) && showAdminNav && ( { See all servers )} - {isSystemAdmin && !isDashboardView && !isServerView && ( + {isSystemAdmin && !isDashboardView && !isServerView && isAdminView && ( <>
/// /// - public ErrorResponseModel(string message, string details) + public ErrorResponseModel(string message, string? details = null) { this.Error = message; this.Details = details; diff --git a/src/libs/dal/Services/FileSystemHistoryItemService.cs b/src/libs/dal/Services/FileSystemHistoryItemService.cs index 0113def4..7a8bb41d 100644 --- a/src/libs/dal/Services/FileSystemHistoryItemService.cs +++ b/src/libs/dal/Services/FileSystemHistoryItemService.cs @@ -42,10 +42,14 @@ public IEnumerable FindForUser( FileSystemHistoryItemFilter filter) { var userOrganizationQuery = from uo in this.Context.UserOrganizations + join o in this.Context.Organizations on uo.OrganizationId equals o.Id where uo.UserId == userId + && o.IsEnabled select uo.OrganizationId; var userTenants = from ut in this.Context.UserTenants + join t in this.Context.Tenants on ut.TenantId equals t.Id where ut.UserId == userId + && t.IsEnabled select ut.TenantId; var query = (from fsi in this.Context.FileSystemHistoryItems diff --git a/src/libs/dal/Services/FileSystemItemService.cs b/src/libs/dal/Services/FileSystemItemService.cs index 1a7bc95d..eb3b0ed6 100644 --- a/src/libs/dal/Services/FileSystemItemService.cs +++ b/src/libs/dal/Services/FileSystemItemService.cs @@ -44,10 +44,14 @@ public IEnumerable FindForUser( FileSystemItemFilter filter) { var userOrganizationQuery = from uo in this.Context.UserOrganizations + join o in this.Context.Organizations on uo.OrganizationId equals o.Id where uo.UserId == userId + && o.IsEnabled select uo.OrganizationId; var userTenants = from ut in this.Context.UserTenants + join t in this.Context.Tenants on ut.TenantId equals t.Id where ut.UserId == userId + && t.IsEnabled select ut.TenantId; var query = (from fsi in this.Context.FileSystemItems diff --git a/src/libs/dal/Services/IOrganizationService.cs b/src/libs/dal/Services/IOrganizationService.cs index 6222e667..f381089d 100644 --- a/src/libs/dal/Services/IOrganizationService.cs +++ b/src/libs/dal/Services/IOrganizationService.cs @@ -10,4 +10,6 @@ IEnumerable Find( IEnumerable FindForUser( long userId, Models.Filters.OrganizationFilter filter); + + Organization? FindForId(int id, bool includeTenants); } diff --git a/src/libs/dal/Services/ITenantService.cs b/src/libs/dal/Services/ITenantService.cs index 864952b6..e6588815 100644 --- a/src/libs/dal/Services/ITenantService.cs +++ b/src/libs/dal/Services/ITenantService.cs @@ -4,17 +4,12 @@ namespace HSB.DAL.Services; public interface ITenantService : IBaseService { - IEnumerable FindForUser( - long userId, - System.Linq.Expressions.Expression> predicate, - System.Linq.Expressions.Expression>? sort = null, - int? take = null, - int? skip = null); + IEnumerable Find( + Models.Filters.TenantFilter filter); - public IEnumerable FindForUser( + IEnumerable FindForUser( long userId, - System.Linq.Expressions.Expression> predicate, - string[] sort, - int? take = null, - int? skip = null); + Models.Filters.TenantFilter filter); + + Tenant? FindForId(int id, bool includeOrganizations); } diff --git a/src/libs/dal/Services/IUserService.cs b/src/libs/dal/Services/IUserService.cs index c3247198..0ad69710 100644 --- a/src/libs/dal/Services/IUserService.cs +++ b/src/libs/dal/Services/IUserService.cs @@ -5,6 +5,7 @@ namespace HSB.DAL.Services; public interface IUserService : IBaseService { IEnumerable Find(Models.Filters.UserFilter filter); + User? FindForId(int id, bool includePermissions); User? FindByKey(string key); User? FindByUsername(string username); IEnumerable FindByEmail(string email); diff --git a/src/libs/dal/Services/OperatingSystemItemService.cs b/src/libs/dal/Services/OperatingSystemItemService.cs index bfb833cc..17501ae7 100644 --- a/src/libs/dal/Services/OperatingSystemItemService.cs +++ b/src/libs/dal/Services/OperatingSystemItemService.cs @@ -24,10 +24,14 @@ public IEnumerable FindForUser( int? skip = null) { var userOrganizationQuery = from uo in this.Context.UserOrganizations + join o in this.Context.Organizations on uo.OrganizationId equals o.Id where uo.UserId == userId + && o.IsEnabled select uo.OrganizationId; var userTenants = from ut in this.Context.UserTenants + join t in this.Context.Tenants on ut.TenantId equals t.Id where ut.UserId == userId + && t.IsEnabled select ut.TenantId; var query = (from osi in this.Context.OperatingSystemItems @@ -58,10 +62,14 @@ public IEnumerable FindForUser( int? skip = null) { var userOrganizationQuery = from uo in this.Context.UserOrganizations + join o in this.Context.Organizations on uo.OrganizationId equals o.Id where uo.UserId == userId + && o.IsEnabled select uo.OrganizationId; var userTenants = from ut in this.Context.UserTenants + join t in this.Context.Tenants on ut.TenantId equals t.Id where ut.UserId == userId + && t.IsEnabled select ut.TenantId; var query = (from osi in this.Context.OperatingSystemItems diff --git a/src/libs/dal/Services/OrganizationService.cs b/src/libs/dal/Services/OrganizationService.cs index 42068529..15b3e33c 100644 --- a/src/libs/dal/Services/OrganizationService.cs +++ b/src/libs/dal/Services/OrganizationService.cs @@ -21,10 +21,14 @@ public OrganizationService(HSBContext dbContext, ClaimsPrincipal principal, ISer public IEnumerable Find( Models.Filters.OrganizationFilter filter) { - var query = (from org in this.Context.Organizations - select org) - .Where(filter.GeneratePredicate()) - .Distinct(); + var query = from org in this.Context.Organizations + select org; + + if (filter.IncludeTenants == true) + query = query.Include(o => o.TenantsManyToMany).ThenInclude(t => t.Tenant); + + query = query + .Where(filter.GeneratePredicate()); if (filter.Sort?.Any() == true) query = query.OrderByProperty(filter.Sort); @@ -52,9 +56,14 @@ join ut in this.Context.UserTenants on tOrg.TenantId equals ut.TenantId where ut.UserId == userId select tOrg.OrganizationId; - var query = (from org in this.Context.Organizations - where userOrganizationQuery.Contains(org.Id) || tenantOrganizationQuery.Contains(org.Id) - select org) + var query = from org in this.Context.Organizations + where userOrganizationQuery.Contains(org.Id) || tenantOrganizationQuery.Contains(org.Id) + select org; + + if (filter.IncludeTenants == true) + query = query.Include(o => o.TenantsManyToMany).ThenInclude(t => t.Tenant); + + query = query .Where(filter.GeneratePredicate()) .Distinct(); @@ -72,5 +81,16 @@ where userOrganizationQuery.Contains(org.Id) || tenantOrganizationQuery.Contains .ToArray(); } + public Organization? FindForId(int id, bool includeTenants) + { + var query = this.Context.Organizations.Where(u => u.Id == id); + + if (includeTenants) + query = query + .Include(m => m.TenantsManyToMany).ThenInclude(g => g.Tenant); + + return query.FirstOrDefault(); + } + #endregion } diff --git a/src/libs/dal/Services/ServerHistoryItemService.cs b/src/libs/dal/Services/ServerHistoryItemService.cs index d960ee2d..329cf06d 100644 --- a/src/libs/dal/Services/ServerHistoryItemService.cs +++ b/src/libs/dal/Services/ServerHistoryItemService.cs @@ -41,10 +41,14 @@ public IEnumerable Find(ServerHistoryItemFilter filter) public IEnumerable FindForUser(int userId, ServerHistoryItemFilter filter) { var userOrganizationQuery = from uo in this.Context.UserOrganizations + join o in this.Context.Organizations on uo.OrganizationId equals o.Id where uo.UserId == userId + && o.IsEnabled select uo.OrganizationId; var userTenants = from ut in this.Context.UserTenants + join t in this.Context.Tenants on ut.TenantId equals t.Id where ut.UserId == userId + && t.IsEnabled select ut.TenantId; var query = (from si in this.Context.ServerHistoryItems diff --git a/src/libs/dal/Services/ServerItemService.cs b/src/libs/dal/Services/ServerItemService.cs index 4a3cb71e..69d13b93 100644 --- a/src/libs/dal/Services/ServerItemService.cs +++ b/src/libs/dal/Services/ServerItemService.cs @@ -43,10 +43,14 @@ public IEnumerable Find(ServerItemFilter filter) public IEnumerable FindForUser(long userId, ServerItemFilter filter) { var userOrganizationQuery = from uo in this.Context.UserOrganizations + join o in this.Context.Organizations on uo.OrganizationId equals o.Id where uo.UserId == userId + && o.IsEnabled select uo.OrganizationId; var userTenants = from ut in this.Context.UserTenants + join t in this.Context.Tenants on ut.TenantId equals t.Id where ut.UserId == userId + && t.IsEnabled select ut.TenantId; var query = (from si in this.Context.ServerItems @@ -94,10 +98,14 @@ public IEnumerable FindSimple(ServerItemFilter filter) public IEnumerable FindSimpleForUser(long userId, ServerItemFilter filter) { var userOrganizationQuery = from uo in this.Context.UserOrganizations + join o in this.Context.Organizations on uo.OrganizationId equals o.Id where uo.UserId == userId + && o.IsEnabled select uo.OrganizationId; var userTenants = from ut in this.Context.UserTenants + join t in this.Context.Tenants on ut.TenantId equals t.Id where ut.UserId == userId + && t.IsEnabled select ut.TenantId; var query = (from si in this.Context.ServerItems diff --git a/src/libs/dal/Services/TenantService.cs b/src/libs/dal/Services/TenantService.cs index b5220e19..0c3ce9af 100644 --- a/src/libs/dal/Services/TenantService.cs +++ b/src/libs/dal/Services/TenantService.cs @@ -18,25 +18,25 @@ public TenantService(HSBContext dbContext, ClaimsPrincipal principal, IServicePr #endregion #region Methods - public IEnumerable FindForUser( - long userId, - System.Linq.Expressions.Expression> predicate, - System.Linq.Expressions.Expression>? sort = null, - int? take = null, - int? skip = null) + public IEnumerable Find( + Models.Filters.TenantFilter filter) { - var query = (from t in this.Context.Tenants - join ut in this.Context.UserTenants on t.Id equals ut.TenantId - where ut.UserId == userId - select t) - .Where(predicate); - - if (sort != null) - query = query.OrderBy(sort); - if (take.HasValue) - query = query.Take(take.Value); - if (skip.HasValue) - query = query.Skip(skip.Value); + var query = from tenant in this.Context.Tenants + select tenant; + + if (filter.IncludeOrganizations == true) + query = query.Include(o => o.OrganizationsManyToMany).ThenInclude(t => t.Organization); + + query = query + .Where(filter.GeneratePredicate()); + + if (filter.Sort?.Any() == true) + query = query.OrderByProperty(filter.Sort); + else query = query.OrderBy(si => si.Name); + if (filter.Quantity.HasValue) + query = query.Take(filter.Quantity.Value); + if (filter.Page.HasValue && filter.Quantity.HasValue && filter.Page > 1) + query = query.Skip(filter.Page.Value * filter.Quantity.Value); return query .AsNoTracking() @@ -46,30 +46,52 @@ join ut in this.Context.UserTenants on t.Id equals ut.TenantId public IEnumerable FindForUser( long userId, - System.Linq.Expressions.Expression> predicate, - string[] sort, - int? take = null, - int? skip = null) + Models.Filters.TenantFilter filter) { - var query = (from t in this.Context.Tenants - join ut in this.Context.UserTenants on t.Id equals ut.TenantId - where ut.UserId == userId - select t) - .Where(predicate); - - if (sort?.Any() == true) - query = query.OrderByProperty(sort); - if (take.HasValue) - query = query.Take(take.Value); - if (skip.HasValue) - query = query.Skip(skip.Value); + var userTenantQuery = from uo in this.Context.UserTenants + where uo.UserId == userId + select uo.TenantId; + var tenantOrganizationQuery = from tOrg in this.Context.TenantOrganizations + join ut in this.Context.UserTenants on tOrg.TenantId equals ut.TenantId + where ut.UserId == userId + select tOrg.TenantId; + + var query = from tenant in this.Context.Tenants + where userTenantQuery.Contains(tenant.Id) || tenantOrganizationQuery.Contains(tenant.Id) + select tenant; + + if (filter.IncludeOrganizations == true) + query = query.Include(o => o.OrganizationsManyToMany).ThenInclude(t => t.Organization); + + query = query + .Where(filter.GeneratePredicate()) + .Distinct(); + + if (filter.Sort?.Any() == true) + query = query.OrderByProperty(filter.Sort); + else query = query.OrderBy(si => si.Name); + if (filter.Quantity.HasValue) + query = query.Take(filter.Quantity.Value); + if (filter.Page.HasValue && filter.Quantity.HasValue && filter.Page > 1) + query = query.Skip(filter.Page.Value * filter.Quantity.Value); return query .AsNoTracking() - .AsSingleQuery() + .AsSplitQuery() .ToArray(); } + public Tenant? FindForId(int id, bool includeOrganizations) + { + var query = this.Context.Tenants.Where(u => u.Id == id); + + if (includeOrganizations) + query = query + .Include(m => m.OrganizationsManyToMany).ThenInclude(g => g.Organization); + + return query.FirstOrDefault(); + } + public override EntityEntry Add(Tenant entity) { if (entity.OrganizationsManyToMany.Any()) diff --git a/src/libs/dal/Services/UserService.cs b/src/libs/dal/Services/UserService.cs index 8fa95975..07df0bd1 100644 --- a/src/libs/dal/Services/UserService.cs +++ b/src/libs/dal/Services/UserService.cs @@ -24,11 +24,11 @@ public IEnumerable Find(Models.Filters.UserFilter filter) .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); + if (filter.IncludePermissions == true) + query = query + .Include(u => u.GroupsManyToMany).ThenInclude(g => g.Group) + .Include(u => u.OrganizationsManyToMany).ThenInclude(g => g.Organization) + .Include(u => u.TenantsManyToMany).ThenInclude(g => g.Tenant); query = query.Where(filter.GeneratePredicate()); @@ -52,6 +52,19 @@ public IEnumerable FindByEmail(string email) .ToArray(); } + public User? FindForId(int id, bool includePermissions) + { + var query = this.Context.Users.Where(u => u.Id == id); + + if (includePermissions) + query = query + .Include(m => m.Groups).ThenInclude(g => g.Roles) + .Include(m => m.Organizations) + .Include(m => m.Tenants); + + return query.FirstOrDefault(); + } + public User? FindByKey(string key) { return this.Context.Users @@ -86,19 +99,35 @@ public override EntityEntry Update(User entity) } }); + // Update organizations + var originalOrganizations = this.Context.UserOrganizations.Where(ut => ut.UserId == entity.Id).ToArray(); + originalOrganizations.Except(entity.OrganizationsManyToMany).ForEach((organization) => + { + this.Context.Entry(organization).State = EntityState.Deleted; + }); + entity.OrganizationsManyToMany.ForEach((organization) => + { + var originalOrganization = originalOrganizations.FirstOrDefault(s => s.OrganizationId == organization.OrganizationId); + if (originalOrganization == null) + { + organization.UserId = entity.Id; + this.Context.Entry(organization).State = EntityState.Added; + } + }); + // Update tenants var originalTenants = this.Context.UserTenants.Where(ut => ut.UserId == entity.Id).ToArray(); - originalTenants.Except(entity.TenantsManyToMany).ForEach((source) => + originalTenants.Except(entity.TenantsManyToMany).ForEach((tenant) => { - this.Context.Entry(source).State = EntityState.Deleted; + this.Context.Entry(tenant).State = EntityState.Deleted; }); - entity.TenantsManyToMany.ForEach((group) => + entity.TenantsManyToMany.ForEach((tenant) => { - var originalTenant = originalTenants.FirstOrDefault(s => s.TenantId == group.TenantId); + var originalTenant = originalTenants.FirstOrDefault(s => s.TenantId == tenant.TenantId); if (originalTenant == null) { - group.UserId = entity.Id; - this.Context.Entry(group).State = EntityState.Added; + tenant.UserId = entity.Id; + this.Context.Entry(tenant).State = EntityState.Added; } }); diff --git a/src/libs/models/Filters/OrganizationFilter.cs b/src/libs/models/Filters/OrganizationFilter.cs index 905d9884..bd536234 100644 --- a/src/libs/models/Filters/OrganizationFilter.cs +++ b/src/libs/models/Filters/OrganizationFilter.cs @@ -20,6 +20,8 @@ public class OrganizationFilter : PageFilter public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } + public bool? IncludeTenants { get; set; } + public string[] Sort { get; set; } = Array.Empty(); #endregion @@ -38,6 +40,7 @@ public OrganizationFilter(Dictionary(); #endregion @@ -24,10 +27,12 @@ public TenantFilter(Dictionary(queryParams, StringComparer.OrdinalIgnoreCase); + this.Id = filter.GetIntNullValue(nameof(this.Id)); this.Name = filter.GetStringValue(nameof(this.Name)); this.IsEnabled = filter.GetBoolNullValue(nameof(this.IsEnabled)); this.StartDate = filter.GetDateTimeNullValue(nameof(this.StartDate)); this.EndDate = filter.GetDateTimeNullValue(nameof(this.EndDate)); + this.IncludeOrganizations = filter.GetBoolNullValue(nameof(this.IncludeOrganizations)); this.Sort = filter.GetStringArrayValue(nameof(this.Sort), new[] { nameof(TenantModel.Name) }); } @@ -37,6 +42,8 @@ public TenantFilter(Dictionary GeneratePredicate() { var predicate = PredicateBuilder.New(); + if (this.Id != null) + predicate = predicate.And((u) => u.Id == this.Id); if (this.Name != null) predicate = predicate.And((u) => EF.Functions.Like(u.Name, $"%{this.Name}%")); if (this.IsEnabled != null) diff --git a/src/libs/models/Filters/UserFilter.cs b/src/libs/models/Filters/UserFilter.cs index 679d8ad2..1948e067 100644 --- a/src/libs/models/Filters/UserFilter.cs +++ b/src/libs/models/Filters/UserFilter.cs @@ -17,9 +17,7 @@ public class UserFilter : PageFilter public bool? IsEnabled { get; set; } - public bool? IncludeGroups { get; set; } - - public bool? IncludeTenants { get; set; } + public bool? IncludePermissions { get; set; } public string[] Sort { get; set; } = Array.Empty(); #endregion @@ -36,8 +34,7 @@ public UserFilter(Dictionary r.Role != null).Select(r => new RoleModel(r.Role!)) : this.Roles; + this.Roles = group.Roles.Any() ? group.Roles.Select(r => new RoleModel(r)) : this.Roles; } #endregion diff --git a/src/libs/models/OrganizationModel.cs b/src/libs/models/OrganizationModel.cs index 0c09dc3b..585ab5ab 100644 --- a/src/libs/models/OrganizationModel.cs +++ b/src/libs/models/OrganizationModel.cs @@ -14,17 +14,25 @@ public class OrganizationModel : SortableCodeAuditableModel ///
public string ServiceNowKey { get; set; } = ""; public IEnumerable Children { get; set; } = Array.Empty(); + + public IEnumerable Tenants { get; set; } = Array.Empty(); #endregion #region Constructors public OrganizationModel() { } - public OrganizationModel(Organization entity, bool includeChildren = false) : base(entity) + public OrganizationModel(Organization entity, bool includeTenants, bool includeChildren = false) : base(entity) { this.Id = entity.Id; this.ParentId = entity.ParentId; - if (!includeChildren) this.Parent = entity.Parent != null ? new OrganizationModel(entity.Parent) : null; - if (includeChildren) this.Children = entity.Children.Select(c => new OrganizationModel(c)); + if (!includeChildren) this.Parent = entity.Parent != null ? new OrganizationModel(entity.Parent, false) : null; + if (includeChildren) this.Children = entity.Children.Select(c => new OrganizationModel(c, false)); + + if (includeTenants) + { + this.Tenants = entity.TenantsManyToMany.Any() ? entity.TenantsManyToMany.Where(t => t.Tenant != null).Select(t => new TenantModel(t.Tenant!, false)).ToArray() : this.Tenants; + this.Tenants = entity.Tenants.Any() ? entity.Tenants.Select(t => new TenantModel(t, false)).ToArray() : this.Tenants; + } this.ServiceNowKey = entity.ServiceNowKey; this.RawData = entity.RawData; @@ -51,7 +59,7 @@ public static explicit operator Organization(OrganizationModel model) { if (model.RawData == null) throw new InvalidOperationException("Property 'RawData' is required."); - return new Organization(model.Name) + var entity = new Organization(model.Name) { Id = model.Id, Description = model.Description, @@ -63,6 +71,9 @@ public static explicit operator Organization(OrganizationModel model) SortOrder = model.SortOrder, Version = model.Version, }; + entity.TenantsManyToMany.AddRange(model.Tenants.Select(t => new TenantOrganization(t.Id, entity.Id))); + + return entity; } #endregion } diff --git a/src/libs/models/TenantModel.cs b/src/libs/models/TenantModel.cs index 2107ba15..d49138a7 100644 --- a/src/libs/models/TenantModel.cs +++ b/src/libs/models/TenantModel.cs @@ -21,16 +21,18 @@ public class TenantModel : SortableCodeAuditableModel #region Constructors public TenantModel() { } - public TenantModel(Tenant entity) : base(entity) + public TenantModel(Tenant entity, bool includeOrganizations) : base(entity) { this.Id = entity.Id; this.ServiceNowKey = entity.ServiceNowKey; this.RawData = entity.RawData; - this.Organizations = entity.OrganizationsManyToMany.Where(o => o.Organization != null).Select(o => new OrganizationModel(o.Organization!)); - if (entity.Organizations.Any()) - this.Organizations = entity.Organizations.Select(o => new OrganizationModel(o)); + if (includeOrganizations) + { + this.Organizations = entity.OrganizationsManyToMany.Any() ? entity.OrganizationsManyToMany.Where(o => o.Organization != null).Select(o => new OrganizationModel(o.Organization!, false)) : this.Organizations; + this.Organizations = entity.Organizations.Any() ? entity.Organizations.Select(o => new OrganizationModel(o, false)) : this.Organizations; + } } public TenantModel(ServiceNow.ResultModel model, IEnumerable organizations) diff --git a/src/libs/models/UserModel.cs b/src/libs/models/UserModel.cs index 57b15318..ff0e6a71 100644 --- a/src/libs/models/UserModel.cs +++ b/src/libs/models/UserModel.cs @@ -118,10 +118,10 @@ public UserModel(User user) : base(user) 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; - this.Organizations = user.OrganizationsManyToMany.Any() ? user.OrganizationsManyToMany.Where(o => o.Organization != null).Select(o => new OrganizationModel(o.Organization!)) : this.Organizations; - this.Organizations = user.Organizations.Any() ? user.Organizations.Select(o => new OrganizationModel(o)) : this.Organizations; + this.Tenants = user.TenantsManyToMany.Any() ? user.TenantsManyToMany.Where(t => t.Tenant != null).Select(t => new TenantModel(t.Tenant!, false)) : this.Tenants; + this.Tenants = user.Tenants.Any() ? user.Tenants.Select(t => new TenantModel(t, false)) : this.Tenants; + this.Organizations = user.OrganizationsManyToMany.Any() ? user.OrganizationsManyToMany.Where(o => o.Organization != null).Select(o => new OrganizationModel(o.Organization!, false)) : this.Organizations; + this.Organizations = user.Organizations.Any() ? user.Organizations.Select(o => new OrganizationModel(o, false)) : this.Organizations; } #endregion