From bc35726cbfeacdff8c3c816db153c01cef8622e2 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 12 Mar 2024 20:41:17 -0700 Subject: [PATCH 01/24] Add Type column to access table --- app/components/AccessNameCell.tsx | 23 ++----------------- app/components/AccessTypeCell.tsx | 20 ++++++++++++++++ app/pages/SiloAccessPage.tsx | 2 ++ .../project/access/ProjectAccessPage.tsx | 2 ++ test/e2e/project-access.e2e.ts | 8 +++++-- test/e2e/silo-access.e2e.ts | 4 +++- 6 files changed, 35 insertions(+), 24 deletions(-) create mode 100644 app/components/AccessTypeCell.tsx diff --git a/app/components/AccessNameCell.tsx b/app/components/AccessNameCell.tsx index f3d0b920be..52ea03db32 100644 --- a/app/components/AccessNameCell.tsx +++ b/app/components/AccessNameCell.tsx @@ -7,28 +7,9 @@ */ import type { CellContext } from '@tanstack/react-table' -import type { IdentityType } from '@oxide/api' - -import { Badge } from '~/ui/lib/Badge' - /** * Display the user or group name. If the row is for a group, add a GROUP badge. */ -export const AccessNameCell = < - RowData extends { name: string; identityType: IdentityType }, ->( +export const AccessNameCell = ( info: CellContext -) => { - const name = info.getValue() - const identityType = info.row.original.identityType - return ( - <> - {name} - {identityType === 'silo_group' ? ( - - Group - - ) : null} - - ) -} +) => {info.getValue()} diff --git a/app/components/AccessTypeCell.tsx b/app/components/AccessTypeCell.tsx new file mode 100644 index 0000000000..223b7f97b7 --- /dev/null +++ b/app/components/AccessTypeCell.tsx @@ -0,0 +1,20 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import type { CellContext } from '@tanstack/react-table' + +import type { IdentityType } from '~/api' + +/** + * Display the user or group name. If the row is for a group, add a GROUP badge. + */ +export const AccessTypeCell = ( + info: CellContext +) => { + const identityType = info.row.original.identityType + return {identityType === 'silo_group' ? 'Group' : 'User'} +} diff --git a/app/pages/SiloAccessPage.tsx b/app/pages/SiloAccessPage.tsx index 0ba0d4f014..54db9a9b08 100644 --- a/app/pages/SiloAccessPage.tsx +++ b/app/pages/SiloAccessPage.tsx @@ -23,6 +23,7 @@ import { import { Access24Icon } from '@oxide/design-system/icons/react' import { AccessNameCell } from '~/components/AccessNameCell' +import { AccessTypeCell } from '~/components/AccessTypeCell' import { HL } from '~/components/HL' import { RoleBadgeCell } from '~/components/RoleBadgeCell' import { @@ -112,6 +113,7 @@ export function SiloAccessPage() { const columns = useMemo( () => [ colHelper.accessor('name', { header: 'Name', cell: AccessNameCell }), + colHelper.accessor('identityType', { header: 'Type', cell: AccessTypeCell }), colHelper.accessor('siloRole', { header: 'Silo role', cell: RoleBadgeCell, diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 08d365b291..3a263cc92b 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -25,6 +25,7 @@ import { import { Access24Icon } from '@oxide/design-system/icons/react' import { AccessNameCell } from '~/components/AccessNameCell' +import { AccessTypeCell } from '~/components/AccessTypeCell' import { HL } from '~/components/HL' import { RoleBadgeCell } from '~/components/RoleBadgeCell' import { @@ -128,6 +129,7 @@ export function ProjectAccessPage() { const columns = useMemo( () => [ colHelper.accessor('name', { header: 'Name', cell: AccessNameCell }), + colHelper.accessor('identityType', { header: 'Type', cell: AccessTypeCell }), colHelper.accessor('siloRole', { header: 'Silo role', cell: RoleBadgeCell, diff --git a/test/e2e/project-access.e2e.ts b/test/e2e/project-access.e2e.ts index a6481688bc..cc177ced09 100644 --- a/test/e2e/project-access.e2e.ts +++ b/test/e2e/project-access.e2e.ts @@ -18,22 +18,26 @@ test('Click through project access page', async ({ page }) => { const table = page.locator('table') await expectRowVisible(table, { Name: 'Hannah Arendt', + Type: 'User', 'Silo role': 'admin', 'Project role': '', }) await expectRowVisible(table, { Name: 'Jacob Klein', + Type: 'User', 'Silo role': '', 'Project role': 'collaborator', }) await expectRowVisible(table, { // no space because expectRowVisible uses textContent, not accessible name - Name: 'real-estate-devsGroup', + Name: 'real-estate-devs', + Type: 'Group', 'Silo role': 'collaborator', }) await expectRowVisible(table, { // no space because expectRowVisible uses textContent, not accessible name - Name: 'kernel-devsGroup', + Name: 'kernel-devs', + Type: 'Group', 'Silo role': '', 'Project role': 'viewer', }) diff --git a/test/e2e/silo-access.e2e.ts b/test/e2e/silo-access.e2e.ts index aaf6142c59..ec45ede7b7 100644 --- a/test/e2e/silo-access.e2e.ts +++ b/test/e2e/silo-access.e2e.ts @@ -20,11 +20,13 @@ test('Click through silo access page', async ({ page }) => { await expectVisible(page, ['role=heading[name*="Access & IAM"]']) await expectRowVisible(table, { // no space because expectRowVisible uses textContent, not accessible name - Name: 'real-estate-devsGroup', + Name: 'real-estate-devs', + Type: 'Group', 'Silo role': 'collaborator', }) await expectRowVisible(table, { Name: 'Hannah Arendt', + Type: 'User', 'Silo role': 'admin', }) await expectNotVisible(page, [`role=cell[name="${user4.display_name}"]`]) From 2b83127adbe8f58c62e11337170c467a302118f0 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 12 Mar 2024 20:46:50 -0700 Subject: [PATCH 02/24] import from @oxide/api --- app/components/AccessTypeCell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/AccessTypeCell.tsx b/app/components/AccessTypeCell.tsx index 223b7f97b7..8282dd876a 100644 --- a/app/components/AccessTypeCell.tsx +++ b/app/components/AccessTypeCell.tsx @@ -7,7 +7,7 @@ */ import type { CellContext } from '@tanstack/react-table' -import type { IdentityType } from '~/api' +import type { IdentityType } from '@oxide/api' /** * Display the user or group name. If the row is for a group, add a GROUP badge. From cccdcc5a1f800a2f990137ac8ae0b80286a95c12 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 12 Mar 2024 21:06:30 -0700 Subject: [PATCH 03/24] Proper description --- app/components/AccessTypeCell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/AccessTypeCell.tsx b/app/components/AccessTypeCell.tsx index 8282dd876a..8b00f60fc7 100644 --- a/app/components/AccessTypeCell.tsx +++ b/app/components/AccessTypeCell.tsx @@ -10,7 +10,7 @@ import type { CellContext } from '@tanstack/react-table' import type { IdentityType } from '@oxide/api' /** - * Display the user or group name. If the row is for a group, add a GROUP badge. + * Display whether this row displays a User or Group. */ export const AccessTypeCell = ( info: CellContext From 5ec39cc2dd727bd53e6ed9d3cfeecca5457ea3b6 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 12 Mar 2024 22:07:15 -0700 Subject: [PATCH 04/24] add AccessRolesCell --- app/components/AccessRolesCell.tsx | 56 +++++++++++++++++++ .../project/access/ProjectAccessPage.tsx | 5 ++ app/util/access.ts | 18 ++++++ 3 files changed, 79 insertions(+) create mode 100644 app/components/AccessRolesCell.tsx create mode 100644 app/util/access.ts diff --git a/app/components/AccessRolesCell.tsx b/app/components/AccessRolesCell.tsx new file mode 100644 index 0000000000..afcf538bf5 --- /dev/null +++ b/app/components/AccessRolesCell.tsx @@ -0,0 +1,56 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import type { CellContext } from '@tanstack/react-table' + +import type { RoleKey } from '@oxide/api' + +import { Badge } from '~/ui/lib/Badge' +import { Tooltip } from '~/ui/lib/Tooltip' +import { getBadgeColor } from '~/util/access' + +/** + * Highlight the "effective" role in green, others gray. + * + * Example: User has collab on org and viewer on project. Collab supersedes + * because it is the "stronger" role, i.e., it strictly includes the perms on + * viewer. So collab is highlighted as the "effective" role. + */ +export const AccessRolesCell = < + RowData extends { projectRole: RoleKey; siloRole: RoleKey }, +>( + info: CellContext +) => { + const effectiveRoleString = info.getValue() + + const siloRole = info.row.original.siloRole + const formattedSiloRole = siloRole ? ( + {`org.${siloRole}`} + ) : undefined + + const projectRole = info.row.original.projectRole + const formattedProjectRole = projectRole ? ( + {`project.${projectRole}`} + ) : undefined + + const multipleRoles = siloRole && projectRole ? 1 : 0 + const effectiveRole = + effectiveRoleString === siloRole ? formattedSiloRole : formattedProjectRole + const alternateRole = + effectiveRoleString === siloRole ? formattedProjectRole : formattedSiloRole + + return ( +
+ {effectiveRole} + {multipleRoles ? ( + + <>+1 + + ) : undefined} +
+ ) +} diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 3a263cc92b..4b29e2859d 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -25,6 +25,7 @@ import { import { Access24Icon } from '@oxide/design-system/icons/react' import { AccessNameCell } from '~/components/AccessNameCell' +import { AccessRolesCell } from '~/components/AccessRolesCell' import { AccessTypeCell } from '~/components/AccessTypeCell' import { HL } from '~/components/HL' import { RoleBadgeCell } from '~/components/RoleBadgeCell' @@ -130,6 +131,10 @@ export function ProjectAccessPage() { () => [ colHelper.accessor('name', { header: 'Name', cell: AccessNameCell }), colHelper.accessor('identityType', { header: 'Type', cell: AccessTypeCell }), + colHelper.accessor('effectiveRole', { + header: 'Role', + cell: AccessRolesCell, + }), colHelper.accessor('siloRole', { header: 'Silo role', cell: RoleBadgeCell, diff --git a/app/util/access.ts b/app/util/access.ts new file mode 100644 index 0000000000..41a473eb03 --- /dev/null +++ b/app/util/access.ts @@ -0,0 +1,18 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import type { BadgeColor } from '~/ui/lib/Badge' + +export const getBadgeColor = (role: 'admin' | 'collaborator' | 'viewer'): BadgeColor => { + const badgeColor = { + admin: 'default', + collaborator: 'purple', + viewer: 'blue', + } + return badgeColor[role] as BadgeColor +} From e14a009a40366de9579dc3c378f525fa84456e6f Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 12 Mar 2024 22:53:13 -0700 Subject: [PATCH 05/24] use 'silo' for label --- app/components/AccessRolesCell.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/components/AccessRolesCell.tsx b/app/components/AccessRolesCell.tsx index afcf538bf5..d161c30f5e 100644 --- a/app/components/AccessRolesCell.tsx +++ b/app/components/AccessRolesCell.tsx @@ -14,9 +14,9 @@ import { Tooltip } from '~/ui/lib/Tooltip' import { getBadgeColor } from '~/util/access' /** - * Highlight the "effective" role in green, others gray. + * Highlight the "effective" role, providing a tooltip for the alternate role. * - * Example: User has collab on org and viewer on project. Collab supersedes + * Example: User has collab on silo and viewer on project. Collab supersedes * because it is the "stronger" role, i.e., it strictly includes the perms on * viewer. So collab is highlighted as the "effective" role. */ @@ -29,7 +29,7 @@ export const AccessRolesCell = < const siloRole = info.row.original.siloRole const formattedSiloRole = siloRole ? ( - {`org.${siloRole}`} + {`silo.${siloRole}`} ) : undefined const projectRole = info.row.original.projectRole From 91d7b31e94a8d4ed997cd259999dd02b73032dbb Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 12 Mar 2024 23:19:25 -0700 Subject: [PATCH 06/24] Remove duplicate columns --- app/components/AccessRolesCell.tsx | 11 ++++++----- app/components/RoleBadgeCell.tsx | 14 +++----------- app/pages/project/access/ProjectAccessPage.tsx | 9 --------- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/app/components/AccessRolesCell.tsx b/app/components/AccessRolesCell.tsx index d161c30f5e..e5d0b01d7c 100644 --- a/app/components/AccessRolesCell.tsx +++ b/app/components/AccessRolesCell.tsx @@ -9,9 +9,9 @@ import type { CellContext } from '@tanstack/react-table' import type { RoleKey } from '@oxide/api' -import { Badge } from '~/ui/lib/Badge' import { Tooltip } from '~/ui/lib/Tooltip' -import { getBadgeColor } from '~/util/access' + +import { AccessBadge } from './AccessBadge' /** * Highlight the "effective" role, providing a tooltip for the alternate role. @@ -21,20 +21,21 @@ import { getBadgeColor } from '~/util/access' * viewer. So collab is highlighted as the "effective" role. */ export const AccessRolesCell = < - RowData extends { projectRole: RoleKey; siloRole: RoleKey }, + RowData extends { projectRole?: RoleKey; siloRole?: RoleKey }, >( info: CellContext ) => { const effectiveRoleString = info.getValue() + if (!effectiveRoleString) return null const siloRole = info.row.original.siloRole const formattedSiloRole = siloRole ? ( - {`silo.${siloRole}`} + ) : undefined const projectRole = info.row.original.projectRole const formattedProjectRole = projectRole ? ( - {`project.${projectRole}`} + ) : undefined const multipleRoles = siloRole && projectRole ? 1 : 0 diff --git a/app/components/RoleBadgeCell.tsx b/app/components/RoleBadgeCell.tsx index 67cc858bc1..ecc22d74a0 100644 --- a/app/components/RoleBadgeCell.tsx +++ b/app/components/RoleBadgeCell.tsx @@ -9,22 +9,14 @@ import type { CellContext } from '@tanstack/react-table' import type { RoleKey } from '@oxide/api' -import { Badge } from '~/ui/lib/Badge' +import { AccessBadge } from './AccessBadge' /** - * Highlight the "effective" role in green, others gray. - * - * Example: User has collab on org and viewer on project. Collab supersedes - * because it is the "stronger" role, i.e., it strictly includes the perms on - * viewer. So collab is highlighted as the "effective" role. + * Show the silo role for a user or group, formatted in a badge. */ export const RoleBadgeCell = ( info: CellContext ) => { const cellRole = info.getValue() - if (!cellRole) return null - const effectiveRole = info.row.original.effectiveRole - return ( - {cellRole} - ) + return cellRole ? : null } diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 4b29e2859d..8d68986850 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -28,7 +28,6 @@ import { AccessNameCell } from '~/components/AccessNameCell' import { AccessRolesCell } from '~/components/AccessRolesCell' import { AccessTypeCell } from '~/components/AccessTypeCell' import { HL } from '~/components/HL' -import { RoleBadgeCell } from '~/components/RoleBadgeCell' import { ProjectAccessAddUserSideModal, ProjectAccessEditUserSideModal, @@ -135,14 +134,6 @@ export function ProjectAccessPage() { header: 'Role', cell: AccessRolesCell, }), - colHelper.accessor('siloRole', { - header: 'Silo role', - cell: RoleBadgeCell, - }), - colHelper.accessor('projectRole', { - header: 'Project role', - cell: RoleBadgeCell, - }), // TODO: tooltips on disabled elements explaining why getActionsCol((row: UserRow) => [ { From ca009e82dfd56bfc5a758428d6143852c9ff7166 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 13 Mar 2024 07:19:58 -0700 Subject: [PATCH 07/24] Add AccessBadge --- app/components/AccessBadge.tsx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 app/components/AccessBadge.tsx diff --git a/app/components/AccessBadge.tsx b/app/components/AccessBadge.tsx new file mode 100644 index 0000000000..b8001a2f5f --- /dev/null +++ b/app/components/AccessBadge.tsx @@ -0,0 +1,27 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import type { RoleKey } from '~/api' +import { Badge } from '~/ui/lib/Badge' +import { getBadgeColor } from '~/util/access' + +export const AccessBadge = ({ + role, + labelPrefix, +}: { + role: RoleKey + labelPrefix?: string +}) => { + const badgeColor = getBadgeColor(role) + return ( + + {labelPrefix} + {role} + + ) +} From 42e7b5ea806f4e68ad4812bc4401f8b6d00e5b64 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 13 Mar 2024 07:23:45 -0700 Subject: [PATCH 08/24] Small refactor to AccessBadge --- app/components/AccessBadge.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/components/AccessBadge.tsx b/app/components/AccessBadge.tsx index b8001a2f5f..9052f69892 100644 --- a/app/components/AccessBadge.tsx +++ b/app/components/AccessBadge.tsx @@ -16,12 +16,9 @@ export const AccessBadge = ({ }: { role: RoleKey labelPrefix?: string -}) => { - const badgeColor = getBadgeColor(role) - return ( - - {labelPrefix} - {role} - - ) -} +}) => ( + + {labelPrefix} + {role} + +) From 5845fc57d97bfde1fd60a77b4758d08d94f431fb Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 13 Mar 2024 09:42:34 -0700 Subject: [PATCH 09/24] Refactor; make a few tweaks Ben suggested --- app/components/AccessBadge.tsx | 15 ++++----------- ...ssRolesCell.tsx => ProjectAccessRolesCell.tsx} | 2 +- .../{RoleBadgeCell.tsx => SiloAccessRoleCell.tsx} | 4 ++-- app/pages/SiloAccessPage.tsx | 7 ++----- app/pages/project/access/ProjectAccessPage.tsx | 7 ++----- test/e2e/silo-access.e2e.ts | 4 ++-- 6 files changed, 13 insertions(+), 26 deletions(-) rename app/components/{AccessRolesCell.tsx => ProjectAccessRolesCell.tsx} (97%) rename app/components/{RoleBadgeCell.tsx => SiloAccessRoleCell.tsx} (77%) diff --git a/app/components/AccessBadge.tsx b/app/components/AccessBadge.tsx index 9052f69892..47b20628a3 100644 --- a/app/components/AccessBadge.tsx +++ b/app/components/AccessBadge.tsx @@ -10,15 +10,8 @@ import type { RoleKey } from '~/api' import { Badge } from '~/ui/lib/Badge' import { getBadgeColor } from '~/util/access' -export const AccessBadge = ({ - role, - labelPrefix, -}: { - role: RoleKey - labelPrefix?: string -}) => ( - - {labelPrefix} - {role} - +type AccessBadgeProps = { labelPrefix: string; role: RoleKey } + +export const AccessBadge = ({ labelPrefix, role }: AccessBadgeProps) => ( + {`${labelPrefix}${role}`} ) diff --git a/app/components/AccessRolesCell.tsx b/app/components/ProjectAccessRolesCell.tsx similarity index 97% rename from app/components/AccessRolesCell.tsx rename to app/components/ProjectAccessRolesCell.tsx index e5d0b01d7c..405829f33c 100644 --- a/app/components/AccessRolesCell.tsx +++ b/app/components/ProjectAccessRolesCell.tsx @@ -20,7 +20,7 @@ import { AccessBadge } from './AccessBadge' * because it is the "stronger" role, i.e., it strictly includes the perms on * viewer. So collab is highlighted as the "effective" role. */ -export const AccessRolesCell = < +export const ProjectAccessRolesCell = < RowData extends { projectRole?: RoleKey; siloRole?: RoleKey }, >( info: CellContext diff --git a/app/components/RoleBadgeCell.tsx b/app/components/SiloAccessRoleCell.tsx similarity index 77% rename from app/components/RoleBadgeCell.tsx rename to app/components/SiloAccessRoleCell.tsx index ecc22d74a0..526d0be58a 100644 --- a/app/components/RoleBadgeCell.tsx +++ b/app/components/SiloAccessRoleCell.tsx @@ -14,9 +14,9 @@ import { AccessBadge } from './AccessBadge' /** * Show the silo role for a user or group, formatted in a badge. */ -export const RoleBadgeCell = ( +export const SiloAccessRoleCell = ( info: CellContext ) => { const cellRole = info.getValue() - return cellRole ? : null + return cellRole ? : null } diff --git a/app/pages/SiloAccessPage.tsx b/app/pages/SiloAccessPage.tsx index 54db9a9b08..e7c1f6a0f1 100644 --- a/app/pages/SiloAccessPage.tsx +++ b/app/pages/SiloAccessPage.tsx @@ -25,7 +25,7 @@ import { Access24Icon } from '@oxide/design-system/icons/react' import { AccessNameCell } from '~/components/AccessNameCell' import { AccessTypeCell } from '~/components/AccessTypeCell' import { HL } from '~/components/HL' -import { RoleBadgeCell } from '~/components/RoleBadgeCell' +import { SiloAccessRoleCell } from '~/components/SiloAccessRoleCell' import { SiloAccessAddUserSideModal, SiloAccessEditUserSideModal, @@ -114,10 +114,7 @@ export function SiloAccessPage() { () => [ colHelper.accessor('name', { header: 'Name', cell: AccessNameCell }), colHelper.accessor('identityType', { header: 'Type', cell: AccessTypeCell }), - colHelper.accessor('siloRole', { - header: 'Silo role', - cell: RoleBadgeCell, - }), + colHelper.accessor('siloRole', { header: 'Role', cell: SiloAccessRoleCell }), // TODO: tooltips on disabled elements explaining why getActionsCol((row: UserRow) => [ { diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 8d68986850..647eb7b4df 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -25,9 +25,9 @@ import { import { Access24Icon } from '@oxide/design-system/icons/react' import { AccessNameCell } from '~/components/AccessNameCell' -import { AccessRolesCell } from '~/components/AccessRolesCell' import { AccessTypeCell } from '~/components/AccessTypeCell' import { HL } from '~/components/HL' +import { ProjectAccessRolesCell } from '~/components/ProjectAccessRolesCell' import { ProjectAccessAddUserSideModal, ProjectAccessEditUserSideModal, @@ -130,10 +130,7 @@ export function ProjectAccessPage() { () => [ colHelper.accessor('name', { header: 'Name', cell: AccessNameCell }), colHelper.accessor('identityType', { header: 'Type', cell: AccessTypeCell }), - colHelper.accessor('effectiveRole', { - header: 'Role', - cell: AccessRolesCell, - }), + colHelper.accessor('effectiveRole', { header: 'Role', cell: ProjectAccessRolesCell }), // TODO: tooltips on disabled elements explaining why getActionsCol((row: UserRow) => [ { diff --git a/test/e2e/silo-access.e2e.ts b/test/e2e/silo-access.e2e.ts index ec45ede7b7..bd842bc078 100644 --- a/test/e2e/silo-access.e2e.ts +++ b/test/e2e/silo-access.e2e.ts @@ -59,7 +59,7 @@ test('Click through silo access page', async ({ page }) => { // User 3 shows up in the table await expectRowVisible(table, { Name: 'Jacob Klein', - 'Silo role': 'collaborator', + Role: 'collaborator', }) // now change user 3's role from collab to viewer @@ -76,7 +76,7 @@ test('Click through silo access page', async ({ page }) => { await page.click('role=option[name="Viewer"]') await page.click('role=button[name="Update role"]') - await expectRowVisible(table, { Name: user3.display_name, 'Silo role': 'viewer' }) + await expectRowVisible(table, { Name: user3.display_name, Role: 'viewer' }) // now delete user 3 const user3Row = page.getByRole('row', { name: user3.display_name, exact: false }) From cf71b017af4ec39fe7cad063516fd3e30e27572f Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 13 Mar 2024 11:17:11 -0700 Subject: [PATCH 10/24] refactoring --- app/components/AccessBadge.tsx | 4 ++-- app/components/ProjectAccessRolesCell.tsx | 14 ++++++-------- app/components/SiloAccessRoleCell.tsx | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/components/AccessBadge.tsx b/app/components/AccessBadge.tsx index 47b20628a3..bff739665a 100644 --- a/app/components/AccessBadge.tsx +++ b/app/components/AccessBadge.tsx @@ -10,8 +10,8 @@ import type { RoleKey } from '~/api' import { Badge } from '~/ui/lib/Badge' import { getBadgeColor } from '~/util/access' -type AccessBadgeProps = { labelPrefix: string; role: RoleKey } +type AccessBadgeProps = { labelPrefix: 'silo' | 'project'; role: RoleKey } export const AccessBadge = ({ labelPrefix, role }: AccessBadgeProps) => ( - {`${labelPrefix}${role}`} + {`${labelPrefix}.${role}`} ) diff --git a/app/components/ProjectAccessRolesCell.tsx b/app/components/ProjectAccessRolesCell.tsx index 405829f33c..3a90331517 100644 --- a/app/components/ProjectAccessRolesCell.tsx +++ b/app/components/ProjectAccessRolesCell.tsx @@ -30,24 +30,22 @@ export const ProjectAccessRolesCell = < const siloRole = info.row.original.siloRole const formattedSiloRole = siloRole ? ( - + ) : undefined const projectRole = info.row.original.projectRole const formattedProjectRole = projectRole ? ( - + ) : undefined - const multipleRoles = siloRole && projectRole ? 1 : 0 - const effectiveRole = - effectiveRoleString === siloRole ? formattedSiloRole : formattedProjectRole - const alternateRole = - effectiveRoleString === siloRole ? formattedProjectRole : formattedSiloRole + const effectiveRoleIsSiloRole = effectiveRoleString === siloRole + const effectiveRole = effectiveRoleIsSiloRole ? formattedSiloRole : formattedProjectRole + const alternateRole = effectiveRoleIsSiloRole ? formattedProjectRole : formattedSiloRole return (
{effectiveRole} - {multipleRoles ? ( + {siloRole && projectRole ? ( <>+1 diff --git a/app/components/SiloAccessRoleCell.tsx b/app/components/SiloAccessRoleCell.tsx index 526d0be58a..50242cbd53 100644 --- a/app/components/SiloAccessRoleCell.tsx +++ b/app/components/SiloAccessRoleCell.tsx @@ -18,5 +18,5 @@ export const SiloAccessRoleCell = ( info: CellContext ) => { const cellRole = info.getValue() - return cellRole ? : null + return cellRole ? : null } From 48f06dd70d7b64e6d65f43c73115363883dde463 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 13 Mar 2024 11:42:47 -0700 Subject: [PATCH 11/24] Remove unneeded Cell components --- app/components/AccessNameCell.tsx | 15 --------------- app/components/AccessTypeCell.tsx | 20 -------------------- app/components/SiloAccessRoleCell.tsx | 22 ---------------------- app/pages/SiloAccessPage.tsx | 10 ++++++++-- 4 files changed, 8 insertions(+), 59 deletions(-) delete mode 100644 app/components/AccessNameCell.tsx delete mode 100644 app/components/AccessTypeCell.tsx delete mode 100644 app/components/SiloAccessRoleCell.tsx diff --git a/app/components/AccessNameCell.tsx b/app/components/AccessNameCell.tsx deleted file mode 100644 index 52ea03db32..0000000000 --- a/app/components/AccessNameCell.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import type { CellContext } from '@tanstack/react-table' - -/** - * Display the user or group name. If the row is for a group, add a GROUP badge. - */ -export const AccessNameCell = ( - info: CellContext -) => {info.getValue()} diff --git a/app/components/AccessTypeCell.tsx b/app/components/AccessTypeCell.tsx deleted file mode 100644 index 8b00f60fc7..0000000000 --- a/app/components/AccessTypeCell.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import type { CellContext } from '@tanstack/react-table' - -import type { IdentityType } from '@oxide/api' - -/** - * Display whether this row displays a User or Group. - */ -export const AccessTypeCell = ( - info: CellContext -) => { - const identityType = info.row.original.identityType - return {identityType === 'silo_group' ? 'Group' : 'User'} -} diff --git a/app/components/SiloAccessRoleCell.tsx b/app/components/SiloAccessRoleCell.tsx deleted file mode 100644 index 50242cbd53..0000000000 --- a/app/components/SiloAccessRoleCell.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import type { CellContext } from '@tanstack/react-table' - -import type { RoleKey } from '@oxide/api' - -import { AccessBadge } from './AccessBadge' - -/** - * Show the silo role for a user or group, formatted in a badge. - */ -export const SiloAccessRoleCell = ( - info: CellContext -) => { - const cellRole = info.getValue() - return cellRole ? : null -} diff --git a/app/pages/SiloAccessPage.tsx b/app/pages/SiloAccessPage.tsx index 8e305dda61..da6c8b6be1 100644 --- a/app/pages/SiloAccessPage.tsx +++ b/app/pages/SiloAccessPage.tsx @@ -22,8 +22,8 @@ import { } from '@oxide/api' import { Access24Icon } from '@oxide/design-system/icons/react' +import { AccessBadge } from '~/components/AccessBadge' import { HL } from '~/components/HL' -import { SiloAccessRoleCell } from '~/components/SiloAccessRoleCell' import { SiloAccessAddUserSideModal, SiloAccessEditUserSideModal, @@ -116,7 +116,13 @@ export function SiloAccessPage() { header: 'Type', cell: (props) => accessTypeLabel(props.getValue()), }), - colHelper.accessor('siloRole', { header: 'Role', cell: SiloAccessRoleCell }), + colHelper.accessor('siloRole', { + header: 'Role', + cell: (props) => { + const role = props.getValue() + return role ? : null + }, + }), // TODO: tooltips on disabled elements explaining why getActionsCol((row: UserRow) => [ { From 4226d1445d588fd194594d33179e9c45401af2fd Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 13 Mar 2024 12:17:59 -0700 Subject: [PATCH 12/24] Update app/components/AccessBadge.tsx Co-authored-by: David Crespo --- app/components/AccessBadge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/AccessBadge.tsx b/app/components/AccessBadge.tsx index bff739665a..4bec434ad9 100644 --- a/app/components/AccessBadge.tsx +++ b/app/components/AccessBadge.tsx @@ -13,5 +13,5 @@ import { getBadgeColor } from '~/util/access' type AccessBadgeProps = { labelPrefix: 'silo' | 'project'; role: RoleKey } export const AccessBadge = ({ labelPrefix, role }: AccessBadgeProps) => ( - {`${labelPrefix}.${role}`} + {labelPrefix}.{role} ) From 74dd05a764a3597b9515c2dbd5c0297aadbc6413 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 19:19:15 +0000 Subject: [PATCH 13/24] Bot commit: format with prettier --- app/components/AccessBadge.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/components/AccessBadge.tsx b/app/components/AccessBadge.tsx index 4bec434ad9..90fe221423 100644 --- a/app/components/AccessBadge.tsx +++ b/app/components/AccessBadge.tsx @@ -13,5 +13,7 @@ import { getBadgeColor } from '~/util/access' type AccessBadgeProps = { labelPrefix: 'silo' | 'project'; role: RoleKey } export const AccessBadge = ({ labelPrefix, role }: AccessBadgeProps) => ( - {labelPrefix}.{role} + + {labelPrefix}.{role} + ) From 2e788beab14992e440745f0d2eab3cbe661e7ade Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 13 Mar 2024 14:18:00 -0700 Subject: [PATCH 14/24] Get tooltip working, though list of roles needs work --- app/components/ProjectAccessRolesCell.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/components/ProjectAccessRolesCell.tsx b/app/components/ProjectAccessRolesCell.tsx index 3a90331517..54f92cdd79 100644 --- a/app/components/ProjectAccessRolesCell.tsx +++ b/app/components/ProjectAccessRolesCell.tsx @@ -40,14 +40,24 @@ export const ProjectAccessRolesCell = < const effectiveRoleIsSiloRole = effectiveRoleString === siloRole const effectiveRole = effectiveRoleIsSiloRole ? formattedSiloRole : formattedProjectRole - const alternateRole = effectiveRoleIsSiloRole ? formattedProjectRole : formattedSiloRole + const alternateRole = effectiveRoleIsSiloRole ? ( +
+
Project role
+ {formattedProjectRole} +
+ ) : ( +
+
Silo role
+ {formattedSiloRole} +
+ ) return (
{effectiveRole} {siloRole && projectRole ? ( - - <>+1 + +
+1
) : undefined}
From e8ccc9bed6f4006ec18bcff9a90ac888b02b3d2f Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 14 Mar 2024 10:01:05 -0700 Subject: [PATCH 15/24] Fix tests --- test/e2e/project-access.e2e.ts | 25 +++++++++++-------------- test/e2e/silo-access.e2e.ts | 1 - 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/test/e2e/project-access.e2e.ts b/test/e2e/project-access.e2e.ts index cc177ced09..ed7333cd0c 100644 --- a/test/e2e/project-access.e2e.ts +++ b/test/e2e/project-access.e2e.ts @@ -13,33 +13,28 @@ test('Click through project access page', async ({ page }) => { await page.goto('/projects/mock-project') await page.click('role=link[name*="Access & IAM"]') - // page is there, we see user 1-3 but not 4 + // page is there, we see user 1 and 3 but not 4 await expectVisible(page, ['role=heading[name*="Access & IAM"]']) const table = page.locator('table') await expectRowVisible(table, { Name: 'Hannah Arendt', Type: 'User', - 'Silo role': 'admin', - 'Project role': '', + Role: 'silo.admin', }) await expectRowVisible(table, { Name: 'Jacob Klein', Type: 'User', - 'Silo role': '', - 'Project role': 'collaborator', + Role: 'project.collaborator', }) await expectRowVisible(table, { - // no space because expectRowVisible uses textContent, not accessible name Name: 'real-estate-devs', Type: 'Group', - 'Silo role': 'collaborator', + Role: 'silo.collaborator', }) await expectRowVisible(table, { - // no space because expectRowVisible uses textContent, not accessible name Name: 'kernel-devs', Type: 'Group', - 'Silo role': '', - 'Project role': 'viewer', + Role: 'project.viewer', }) await expectNotVisible(page, [`role=cell[name="${user4.display_name}"]`]) @@ -72,7 +67,8 @@ test('Click through project access page', async ({ page }) => { // User 4 shows up in the table await expectRowVisible(table, { Name: 'Simone de Beauvoir', - 'Project role': 'collaborator', + Type: 'User', + Role: 'project.collaborator', }) // now change user 4 role from collab to viewer @@ -89,7 +85,7 @@ test('Click through project access page', async ({ page }) => { await page.click('role=option[name="Viewer"]') await page.click('role=button[name="Update role"]') - await expectRowVisible(table, { Name: user4.display_name, 'Project role': 'viewer' }) + await expectRowVisible(table, { Name: user4.display_name, Role: 'project.viewer' }) // now delete user 3. has to be 3 or 4 because they're the only ones that come // from the project policy @@ -107,9 +103,10 @@ test('Click through project access page', async ({ page }) => { await page.click('role=button[name*="Role"]') await page.click('role=option[name="Viewer"]') await page.click('role=button[name="Assign role"]') + // because we only show the "effective" role, we should still see the silo admin role await expectRowVisible(table, { Name: 'Hannah Arendt', - 'Silo role': 'admin', - 'Project role': 'viewer', + Type: 'User', + Role: 'silo.admin', }) }) diff --git a/test/e2e/silo-access.e2e.ts b/test/e2e/silo-access.e2e.ts index bd842bc078..e42eb22349 100644 --- a/test/e2e/silo-access.e2e.ts +++ b/test/e2e/silo-access.e2e.ts @@ -19,7 +19,6 @@ test('Click through silo access page', async ({ page }) => { await expectVisible(page, ['role=heading[name*="Access & IAM"]']) await expectRowVisible(table, { - // no space because expectRowVisible uses textContent, not accessible name Name: 'real-estate-devs', Type: 'Group', 'Silo role': 'collaborator', From 973f4dded5398e31ffffc7f9db51e637de521161 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 15 Mar 2024 09:32:11 -0700 Subject: [PATCH 16/24] Working on inherited roles --- app/pages/project/access/ProjectAccessPage.tsx | 3 +++ mock-api/role-assignment.ts | 11 ++++++++++- mock-api/user-group.ts | 10 +++++++++- test/e2e/silo-access.e2e.ts | 7 ++++--- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 7e791576e2..6aa81db084 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -133,6 +133,9 @@ export function ProjectAccessPage() { cell: (props) => accessTypeLabel(props.getValue()), }), colHelper.accessor('effectiveRole', { header: 'Role', cell: ProjectAccessRolesCell }), + colHelper.accessor('siloRole', { header: 'Silo Role' }), + colHelper.accessor('projectRole', { header: 'Project Role' }), + // TODO: tooltips on disabled elements explaining why getActionsCol((row: UserRow) => [ { diff --git a/mock-api/role-assignment.ts b/mock-api/role-assignment.ts index 4589b3465e..e88ce28876 100644 --- a/mock-api/role-assignment.ts +++ b/mock-api/role-assignment.ts @@ -42,7 +42,9 @@ export const roleAssignments: DbRoleAssignment[] = [ resource_id: defaultSilo.id, identity_id: userGroup3.id, identity_type: 'silo_group', - role_name: 'collaborator', + // right now, trying to get the inherited roles working; if it works, Jacob Klein should be an admin because of the inherited role + role_name: 'admin', + // role_name: 'collaborator', }, { resource_type: 'silo', @@ -65,4 +67,11 @@ export const roleAssignments: DbRoleAssignment[] = [ identity_type: 'silo_group', role_name: 'viewer', }, + { + resource_type: 'project', + resource_id: project.id, + identity_id: user1.id, + identity_type: 'silo_user', + role_name: 'viewer', + }, ] diff --git a/mock-api/user-group.ts b/mock-api/user-group.ts index 48db6c5e9c..3bb7d0122b 100644 --- a/mock-api/user-group.ts +++ b/mock-api/user-group.ts @@ -9,7 +9,7 @@ import type { Group } from '@oxide/api' import type { Json } from './json-type' import { defaultSilo } from './silo' -import { user1, user2 } from './user' +import { user1, user2, user3 } from './user' export const userGroup1: Json = { id: '0ff6da96-5d6d-4326-b059-2b72c1b51457', @@ -41,8 +41,16 @@ export const groupMemberships: GroupMembership[] = [ userId: user1.id, groupId: userGroup1.id, }, + { + userId: user1.id, + groupId: userGroup3.id, + }, { userId: user2.id, groupId: userGroup3.id, }, + { + userId: user3.id, + groupId: userGroup3.id, + }, ] diff --git a/test/e2e/silo-access.e2e.ts b/test/e2e/silo-access.e2e.ts index e42eb22349..7aabf66913 100644 --- a/test/e2e/silo-access.e2e.ts +++ b/test/e2e/silo-access.e2e.ts @@ -21,12 +21,12 @@ test('Click through silo access page', async ({ page }) => { await expectRowVisible(table, { Name: 'real-estate-devs', Type: 'Group', - 'Silo role': 'collaborator', + Role: 'silo.admin', }) await expectRowVisible(table, { Name: 'Hannah Arendt', Type: 'User', - 'Silo role': 'admin', + Role: 'silo.admin', }) await expectNotVisible(page, [`role=cell[name="${user4.display_name}"]`]) @@ -58,7 +58,8 @@ test('Click through silo access page', async ({ page }) => { // User 3 shows up in the table await expectRowVisible(table, { Name: 'Jacob Klein', - Role: 'collaborator', + Role: 'silo.collaborator', + Type: 'User', }) // now change user 3's role from collab to viewer From 94a49dffa8737e562398da2ebde737f0ce03d7ce Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 19 Mar 2024 13:53:06 -0700 Subject: [PATCH 17/24] this isn't working; saving to log current state, but will back some changes out --- app/api/roles.ts | 17 ++++++++++++ .../project/access/ProjectAccessPage.tsx | 27 ++++++++++++++++++- mock-api/msw/util.ts | 2 ++ mock-api/user-group.ts | 4 +-- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/app/api/roles.ts b/app/api/roles.ts index bdd1cf97da..f8666d1e10 100644 --- a/app/api/roles.ts +++ b/app/api/roles.ts @@ -154,6 +154,8 @@ export function useActorsNotInPolicy(policy: Policy): Actor[] { }, [users, groups, policy]) } +// so we need to get the user's groups so we can feed them all into userRoleFromPolicies + export function userRoleFromPolicies( user: { id: string }, groups: { id: string }[], @@ -164,5 +166,20 @@ export function userRoleFromPolicies( .flatMap((p) => p.roleAssignments) // concat all the role assignments together .filter((ra) => myIds.has(ra.identityId)) .map((ra) => ra.roleName) + console.log({ myRoles }) return getEffectiveRole(myRoles) || null } + +export function userRolesFromPolicies( + user: { id: string }, + groups: { id: string }[], + policies: Policy[] +): Array | null { + const myIds = new Set([user.id, ...groups.map((g) => g.id)]) + const myRoles = policies + .flatMap((p) => p.roleAssignments) // concat all the role assignments together + .filter((ra) => myIds.has(ra.identityId)) + .map((ra) => ra.roleName) + console.log({ myRoles }) + return myRoles || null +} diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 6aa81db084..8235bf5550 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -61,6 +61,10 @@ ProjectAccessPage.loader = async ({ params }: LoaderFunctionArgs) => { // used to resolve user names apiQueryClient.prefetchQuery('userList', {}), apiQueryClient.prefetchQuery('groupList', {}), + // prefetch silo permissions list + apiQueryClient.prefetchQuery('siloPolicyView', { + path: { silo: 'default' }, + }), ]) return null } @@ -82,17 +86,38 @@ export function ProjectAccessPage() { const { project } = useProjectSelector() const { data: siloPolicy } = usePrefetchedApiQuery('policyView', {}) + console.log({ siloPolicy }) const siloRows = useUserRows(siloPolicy.roleAssignments, 'silo') + console.log({ siloRows }) const { data: projectPolicy } = usePrefetchedApiQuery('projectPolicyView', { path: { project }, }) const projectRows = useUserRows(projectPolicy.roleAssignments, 'project') + console.log({ projectPolicy }) + + const { data: groups } = usePrefetchedApiQuery('groupList', {}) + console.log({ groups }) + + const { data: users } = usePrefetchedApiQuery('userList', {}) + console.log({ users }) + + // We want to get a list of all users and groups with either silo or project permissions + // and then merge them into a single list of users with their effective role. + // We have a list of all groups in "groups" … let's see which ones have silo or project permissions + console.log('------------------------------------------') + console.log('let’s see who has silo or project permissions') + console.log('project: ', projectPolicy.roleAssignments) + console.log('silo: ', siloPolicy.roleAssignments) + console.log('we also want to see all users who are members of these groups') const rows = useMemo(() => { - return groupBy(siloRows.concat(projectRows), (u) => u.id) + const allRows = siloRows.concat(projectRows) + console.log({ allRows }) + return groupBy(allRows, (u) => u.id) .map(([userId, userAssignments]) => { const siloRole = userAssignments.find((a) => a.roleSource === 'silo')?.roleName + console.log({ siloRole }) const projectRole = userAssignments.find( (a) => a.roleSource === 'project' )?.roleName diff --git a/mock-api/msw/util.ts b/mock-api/msw/util.ts index d8438ea808..4ce740bee9 100644 --- a/mock-api/msw/util.ts +++ b/mock-api/msw/util.ts @@ -326,6 +326,8 @@ export function userHasRole( resourceId: string, role: RoleKey ): boolean { + const memberships = db.groupMemberships + console.log({ memberships }) const userGroupIds = db.groupMemberships .filter((gm) => gm.userId === user.id) .map((gm) => db.userGroups.find((g) => g.id === gm.groupId)) diff --git a/mock-api/user-group.ts b/mock-api/user-group.ts index 3bb7d0122b..c51b86764d 100644 --- a/mock-api/user-group.ts +++ b/mock-api/user-group.ts @@ -9,7 +9,7 @@ import type { Group } from '@oxide/api' import type { Json } from './json-type' import { defaultSilo } from './silo' -import { user1, user2, user3 } from './user' +import { user1, user2, user4 } from './user' export const userGroup1: Json = { id: '0ff6da96-5d6d-4326-b059-2b72c1b51457', @@ -50,7 +50,7 @@ export const groupMemberships: GroupMembership[] = [ groupId: userGroup3.id, }, { - userId: user3.id, + userId: user4.id, groupId: userGroup3.id, }, ] From b2befc2f464f4ba87ea1d79b2a007c486d8f308f Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 19 Mar 2024 15:24:01 -0700 Subject: [PATCH 18/24] cleanup --- app/api/roles.ts | 15 ---------- app/components/ProjectAccessRolesCell.tsx | 4 +-- app/forms/project-access.tsx | 2 +- app/forms/silo-access.tsx | 2 +- .../project/access/ProjectAccessPage.tsx | 29 +------------------ mock-api/msw/util.ts | 2 -- mock-api/role-assignment.ts | 4 +-- 7 files changed, 6 insertions(+), 52 deletions(-) diff --git a/app/api/roles.ts b/app/api/roles.ts index f8666d1e10..927b14f7b9 100644 --- a/app/api/roles.ts +++ b/app/api/roles.ts @@ -166,20 +166,5 @@ export function userRoleFromPolicies( .flatMap((p) => p.roleAssignments) // concat all the role assignments together .filter((ra) => myIds.has(ra.identityId)) .map((ra) => ra.roleName) - console.log({ myRoles }) return getEffectiveRole(myRoles) || null } - -export function userRolesFromPolicies( - user: { id: string }, - groups: { id: string }[], - policies: Policy[] -): Array | null { - const myIds = new Set([user.id, ...groups.map((g) => g.id)]) - const myRoles = policies - .flatMap((p) => p.roleAssignments) // concat all the role assignments together - .filter((ra) => myIds.has(ra.identityId)) - .map((ra) => ra.roleName) - console.log({ myRoles }) - return myRoles || null -} diff --git a/app/components/ProjectAccessRolesCell.tsx b/app/components/ProjectAccessRolesCell.tsx index 54f92cdd79..a5d34ffb77 100644 --- a/app/components/ProjectAccessRolesCell.tsx +++ b/app/components/ProjectAccessRolesCell.tsx @@ -53,11 +53,11 @@ export const ProjectAccessRolesCell = < ) return ( -
+
{effectiveRole} {siloRole && projectRole ? ( -
+1
+
+1
) : undefined}
diff --git a/app/forms/project-access.tsx b/app/forms/project-access.tsx index 9ac407e749..1f02b7c1f0 100644 --- a/app/forms/project-access.tsx +++ b/app/forms/project-access.tsx @@ -107,7 +107,7 @@ export function ProjectAccessEditUserSideModal({ form={form} formType="edit" resourceName="role" - title={`Change role for ${name}`} + title={`Change project role for ${name}`} onSubmit={({ roleName }) => { updatePolicy.mutate({ path: { project }, diff --git a/app/forms/silo-access.tsx b/app/forms/silo-access.tsx index d1bd8c3f0f..1001d9d5ab 100644 --- a/app/forms/silo-access.tsx +++ b/app/forms/silo-access.tsx @@ -102,7 +102,7 @@ export function SiloAccessEditUserSideModal({ form={form} formType="edit" resourceName="role" - title={`Change role for ${name}`} + title={`Change silo role for ${name}`} onSubmit={({ roleName }) => { updatePolicy.mutate({ body: updateRole({ identityId, identityType, roleName }, policy), diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 8235bf5550..4d7aa7ffc5 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -61,10 +61,6 @@ ProjectAccessPage.loader = async ({ params }: LoaderFunctionArgs) => { // used to resolve user names apiQueryClient.prefetchQuery('userList', {}), apiQueryClient.prefetchQuery('groupList', {}), - // prefetch silo permissions list - apiQueryClient.prefetchQuery('siloPolicyView', { - path: { silo: 'default' }, - }), ]) return null } @@ -86,38 +82,17 @@ export function ProjectAccessPage() { const { project } = useProjectSelector() const { data: siloPolicy } = usePrefetchedApiQuery('policyView', {}) - console.log({ siloPolicy }) const siloRows = useUserRows(siloPolicy.roleAssignments, 'silo') - console.log({ siloRows }) const { data: projectPolicy } = usePrefetchedApiQuery('projectPolicyView', { path: { project }, }) const projectRows = useUserRows(projectPolicy.roleAssignments, 'project') - console.log({ projectPolicy }) - - const { data: groups } = usePrefetchedApiQuery('groupList', {}) - console.log({ groups }) - - const { data: users } = usePrefetchedApiQuery('userList', {}) - console.log({ users }) - - // We want to get a list of all users and groups with either silo or project permissions - // and then merge them into a single list of users with their effective role. - // We have a list of all groups in "groups" … let's see which ones have silo or project permissions - console.log('------------------------------------------') - console.log('let’s see who has silo or project permissions') - console.log('project: ', projectPolicy.roleAssignments) - console.log('silo: ', siloPolicy.roleAssignments) - console.log('we also want to see all users who are members of these groups') const rows = useMemo(() => { - const allRows = siloRows.concat(projectRows) - console.log({ allRows }) - return groupBy(allRows, (u) => u.id) + return groupBy(siloRows.concat(projectRows), (u) => u.id) .map(([userId, userAssignments]) => { const siloRole = userAssignments.find((a) => a.roleSource === 'silo')?.roleName - console.log({ siloRole }) const projectRole = userAssignments.find( (a) => a.roleSource === 'project' )?.roleName @@ -158,8 +133,6 @@ export function ProjectAccessPage() { cell: (props) => accessTypeLabel(props.getValue()), }), colHelper.accessor('effectiveRole', { header: 'Role', cell: ProjectAccessRolesCell }), - colHelper.accessor('siloRole', { header: 'Silo Role' }), - colHelper.accessor('projectRole', { header: 'Project Role' }), // TODO: tooltips on disabled elements explaining why getActionsCol((row: UserRow) => [ diff --git a/mock-api/msw/util.ts b/mock-api/msw/util.ts index 4ce740bee9..d8438ea808 100644 --- a/mock-api/msw/util.ts +++ b/mock-api/msw/util.ts @@ -326,8 +326,6 @@ export function userHasRole( resourceId: string, role: RoleKey ): boolean { - const memberships = db.groupMemberships - console.log({ memberships }) const userGroupIds = db.groupMemberships .filter((gm) => gm.userId === user.id) .map((gm) => db.userGroups.find((g) => g.id === gm.groupId)) diff --git a/mock-api/role-assignment.ts b/mock-api/role-assignment.ts index e88ce28876..988cc9c3d4 100644 --- a/mock-api/role-assignment.ts +++ b/mock-api/role-assignment.ts @@ -42,9 +42,7 @@ export const roleAssignments: DbRoleAssignment[] = [ resource_id: defaultSilo.id, identity_id: userGroup3.id, identity_type: 'silo_group', - // right now, trying to get the inherited roles working; if it works, Jacob Klein should be an admin because of the inherited role - role_name: 'admin', - // role_name: 'collaborator', + role_name: 'collaborator', }, { resource_type: 'silo', From 0aff37fd79134456edc04473a71297ad10355d0a Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 19 Mar 2024 16:57:30 -0700 Subject: [PATCH 19/24] Add component for ExpandedCountWithDetails --- app/api/roles.ts | 2 -- app/components/ExpandedCountWithDetails.tsx | 36 +++++++++++++++++++++ app/components/ProjectAccessRolesCell.tsx | 28 ++++++---------- mock-api/role-assignment.ts | 14 ++++---- mock-api/user-group.ts | 6 +--- test/e2e/project-access.e2e.ts | 12 ++++--- test/e2e/silo-access.e2e.ts | 6 ++-- 7 files changed, 65 insertions(+), 39 deletions(-) create mode 100644 app/components/ExpandedCountWithDetails.tsx diff --git a/app/api/roles.ts b/app/api/roles.ts index 927b14f7b9..bdd1cf97da 100644 --- a/app/api/roles.ts +++ b/app/api/roles.ts @@ -154,8 +154,6 @@ export function useActorsNotInPolicy(policy: Policy): Actor[] { }, [users, groups, policy]) } -// so we need to get the user's groups so we can feed them all into userRoleFromPolicies - export function userRoleFromPolicies( user: { id: string }, groups: { id: string }[], diff --git a/app/components/ExpandedCountWithDetails.tsx b/app/components/ExpandedCountWithDetails.tsx new file mode 100644 index 0000000000..991ae781e4 --- /dev/null +++ b/app/components/ExpandedCountWithDetails.tsx @@ -0,0 +1,36 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { Tooltip } from '~/ui/lib/Tooltip' + +type ExpandedCountWithDetailsProps = { + count: number + title: string + details: React.ReactNode +} + +/** + * Gives a count with a tooltip that expands to show details when the user hovers over it + */ +export const ExpandedCountWithDetails = ({ + count, + title, + details, +}: ExpandedCountWithDetailsProps) => { + const content = ( +
+
{title}
+ {details} +
+ ) + return ( + +
+{count}
+
+ ) +} diff --git a/app/components/ProjectAccessRolesCell.tsx b/app/components/ProjectAccessRolesCell.tsx index a5d34ffb77..969be75ffb 100644 --- a/app/components/ProjectAccessRolesCell.tsx +++ b/app/components/ProjectAccessRolesCell.tsx @@ -9,9 +9,8 @@ import type { CellContext } from '@tanstack/react-table' import type { RoleKey } from '@oxide/api' -import { Tooltip } from '~/ui/lib/Tooltip' - import { AccessBadge } from './AccessBadge' +import { ExpandedCountWithDetails } from './ExpandedCountWithDetails' /** * Highlight the "effective" role, providing a tooltip for the alternate role. @@ -40,26 +39,19 @@ export const ProjectAccessRolesCell = < const effectiveRoleIsSiloRole = effectiveRoleString === siloRole const effectiveRole = effectiveRoleIsSiloRole ? formattedSiloRole : formattedProjectRole - const alternateRole = effectiveRoleIsSiloRole ? ( -
-
Project role
- {formattedProjectRole} -
- ) : ( -
-
Silo role
- {formattedSiloRole} -
- ) + const hasAlternateRole = Boolean(siloRole && projectRole) + const alternateRole = effectiveRoleIsSiloRole ? formattedProjectRole : formattedSiloRole return (
{effectiveRole} - {siloRole && projectRole ? ( - -
+1
-
- ) : undefined} + {hasAlternateRole && ( + + )}
) } diff --git a/mock-api/role-assignment.ts b/mock-api/role-assignment.ts index 988cc9c3d4..c315963b05 100644 --- a/mock-api/role-assignment.ts +++ b/mock-api/role-assignment.ts @@ -65,11 +65,11 @@ export const roleAssignments: DbRoleAssignment[] = [ identity_type: 'silo_group', role_name: 'viewer', }, - { - resource_type: 'project', - resource_id: project.id, - identity_id: user1.id, - identity_type: 'silo_user', - role_name: 'viewer', - }, + // { + // resource_type: 'project', + // resource_id: project.id, + // identity_id: user1.id, + // identity_type: 'silo_user', + // role_name: 'viewer', + // }, ] diff --git a/mock-api/user-group.ts b/mock-api/user-group.ts index c51b86764d..5187889bce 100644 --- a/mock-api/user-group.ts +++ b/mock-api/user-group.ts @@ -9,7 +9,7 @@ import type { Group } from '@oxide/api' import type { Json } from './json-type' import { defaultSilo } from './silo' -import { user1, user2, user4 } from './user' +import { user1, user2 } from './user' export const userGroup1: Json = { id: '0ff6da96-5d6d-4326-b059-2b72c1b51457', @@ -49,8 +49,4 @@ export const groupMemberships: GroupMembership[] = [ userId: user2.id, groupId: userGroup3.id, }, - { - userId: user4.id, - groupId: userGroup3.id, - }, ] diff --git a/test/e2e/project-access.e2e.ts b/test/e2e/project-access.e2e.ts index ed7333cd0c..a50084a799 100644 --- a/test/e2e/project-access.e2e.ts +++ b/test/e2e/project-access.e2e.ts @@ -13,7 +13,7 @@ test('Click through project access page', async ({ page }) => { await page.goto('/projects/mock-project') await page.click('role=link[name*="Access & IAM"]') - // page is there, we see user 1 and 3 but not 4 + // page is there, we see user 1 and 3 but not 2 or 4 await expectVisible(page, ['role=heading[name*="Access & IAM"]']) const table = page.locator('table') await expectRowVisible(table, { @@ -37,7 +37,8 @@ test('Click through project access page', async ({ page }) => { Role: 'project.viewer', }) - await expectNotVisible(page, [`role=cell[name="${user4.display_name}"]`]) + await expectNotVisible(page, [`role=cell[name="Hans Jonas"]`]) + await expectNotVisible(page, [`role=cell[name="Simone de Beauvoir"]`]) // Add user 4 as collab await page.click('role=button[name="Add user or group"]') @@ -46,6 +47,7 @@ test('Click through project access page', async ({ page }) => { await page.click('role=button[name*="User or group"]') // only users not already on the project should be visible await expectNotVisible(page, ['role=option[name="Jacob Klein"]']) + await expectVisible(page, [ 'role=option[name="Hannah Arendt"]', 'role=option[name="Hans Jonas"]', @@ -78,7 +80,9 @@ test('Click through project access page', async ({ page }) => { .click() await page.click('role=menuitem[name="Change role"]') - await expectVisible(page, ['role=heading[name*="Change role for Simone de Beauvoir"]']) + await expectVisible(page, [ + 'role=heading[name*="Change project role for Simone de Beauvoir"]', + ]) await expectVisible(page, ['button:has-text("Collaborator")']) await page.click('role=button[name*="Role"]') @@ -107,6 +111,6 @@ test('Click through project access page', async ({ page }) => { await expectRowVisible(table, { Name: 'Hannah Arendt', Type: 'User', - Role: 'silo.admin', + Role: 'silo.admin+1', }) }) diff --git a/test/e2e/silo-access.e2e.ts b/test/e2e/silo-access.e2e.ts index 7aabf66913..5db838b65b 100644 --- a/test/e2e/silo-access.e2e.ts +++ b/test/e2e/silo-access.e2e.ts @@ -21,7 +21,7 @@ test('Click through silo access page', async ({ page }) => { await expectRowVisible(table, { Name: 'real-estate-devs', Type: 'Group', - Role: 'silo.admin', + Role: 'silo.collaborator', }) await expectRowVisible(table, { Name: 'Hannah Arendt', @@ -69,14 +69,14 @@ test('Click through silo access page', async ({ page }) => { .click() await page.click('role=menuitem[name="Change role"]') - await expectVisible(page, ['role=heading[name*="Change role for Jacob Klein"]']) + await expectVisible(page, ['role=heading[name*="Change silo role for Jacob Klein"]']) await expectVisible(page, ['button:has-text("Collaborator")']) await page.click('role=button[name*="Role"]') await page.click('role=option[name="Viewer"]') await page.click('role=button[name="Update role"]') - await expectRowVisible(table, { Name: user3.display_name, Role: 'viewer' }) + await expectRowVisible(table, { Name: user3.display_name, Role: 'silo.viewer' }) // now delete user 3 const user3Row = page.getByRole('row', { name: user3.display_name, exact: false }) From 85475ffbcaff4dd1c85b5dde2b8a7878a684f82a Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 19 Mar 2024 17:17:07 -0700 Subject: [PATCH 20/24] cleanup --- mock-api/role-assignment.ts | 7 ------- mock-api/user-group.ts | 4 ---- 2 files changed, 11 deletions(-) diff --git a/mock-api/role-assignment.ts b/mock-api/role-assignment.ts index c315963b05..4589b3465e 100644 --- a/mock-api/role-assignment.ts +++ b/mock-api/role-assignment.ts @@ -65,11 +65,4 @@ export const roleAssignments: DbRoleAssignment[] = [ identity_type: 'silo_group', role_name: 'viewer', }, - // { - // resource_type: 'project', - // resource_id: project.id, - // identity_id: user1.id, - // identity_type: 'silo_user', - // role_name: 'viewer', - // }, ] diff --git a/mock-api/user-group.ts b/mock-api/user-group.ts index 5187889bce..48db6c5e9c 100644 --- a/mock-api/user-group.ts +++ b/mock-api/user-group.ts @@ -41,10 +41,6 @@ export const groupMemberships: GroupMembership[] = [ userId: user1.id, groupId: userGroup1.id, }, - { - userId: user1.id, - groupId: userGroup3.id, - }, { userId: user2.id, groupId: userGroup3.id, From 5c030b721ca2a6703869294dc831e7a491fb273c Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 19 Mar 2024 17:19:41 -0700 Subject: [PATCH 21/24] Small test refactor --- test/e2e/project-access.e2e.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/e2e/project-access.e2e.ts b/test/e2e/project-access.e2e.ts index a50084a799..c08d675b25 100644 --- a/test/e2e/project-access.e2e.ts +++ b/test/e2e/project-access.e2e.ts @@ -37,8 +37,10 @@ test('Click through project access page', async ({ page }) => { Role: 'project.viewer', }) - await expectNotVisible(page, [`role=cell[name="Hans Jonas"]`]) - await expectNotVisible(page, [`role=cell[name="Simone de Beauvoir"]`]) + await expectNotVisible(page, [ + `role=cell[name="Hans Jonas"]`, + `role=cell[name="Simone de Beauvoir"]`, + ]) // Add user 4 as collab await page.click('role=button[name="Add user or group"]') @@ -107,7 +109,7 @@ test('Click through project access page', async ({ page }) => { await page.click('role=button[name*="Role"]') await page.click('role=option[name="Viewer"]') await page.click('role=button[name="Assign role"]') - // because we only show the "effective" role, we should still see the silo admin role + // because we only show the "effective" role, we should still see the silo admin role, but should now have an additional count value await expectRowVisible(table, { Name: 'Hannah Arendt', Type: 'User', From a0bcb6b6dd4195cf45cca97c69ef5fe1c28ff290 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 21 Mar 2024 18:16:17 -0500 Subject: [PATCH 22/24] Access roles refactor suggestions (#2089) * mostly move role badges logic out of cell * ListPlusCell * siloRole is not used --- app/components/AccessBadge.tsx | 2 +- app/components/ExpandedCountWithDetails.tsx | 36 ------------ app/components/ListPlusCell.tsx | 39 +++++++++++++ app/components/ProjectAccessRolesCell.tsx | 57 ------------------- .../project/access/ProjectAccessPage.tsx | 46 ++++++++------- 5 files changed, 66 insertions(+), 114 deletions(-) delete mode 100644 app/components/ExpandedCountWithDetails.tsx create mode 100644 app/components/ListPlusCell.tsx delete mode 100644 app/components/ProjectAccessRolesCell.tsx diff --git a/app/components/AccessBadge.tsx b/app/components/AccessBadge.tsx index 90fe221423..7b24661c29 100644 --- a/app/components/AccessBadge.tsx +++ b/app/components/AccessBadge.tsx @@ -10,7 +10,7 @@ import type { RoleKey } from '~/api' import { Badge } from '~/ui/lib/Badge' import { getBadgeColor } from '~/util/access' -type AccessBadgeProps = { labelPrefix: 'silo' | 'project'; role: RoleKey } +type AccessBadgeProps = { labelPrefix: string; role: RoleKey } export const AccessBadge = ({ labelPrefix, role }: AccessBadgeProps) => ( diff --git a/app/components/ExpandedCountWithDetails.tsx b/app/components/ExpandedCountWithDetails.tsx deleted file mode 100644 index 991ae781e4..0000000000 --- a/app/components/ExpandedCountWithDetails.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ - -import { Tooltip } from '~/ui/lib/Tooltip' - -type ExpandedCountWithDetailsProps = { - count: number - title: string - details: React.ReactNode -} - -/** - * Gives a count with a tooltip that expands to show details when the user hovers over it - */ -export const ExpandedCountWithDetails = ({ - count, - title, - details, -}: ExpandedCountWithDetailsProps) => { - const content = ( -
-
{title}
- {details} -
- ) - return ( - -
+{count}
-
- ) -} diff --git a/app/components/ListPlusCell.tsx b/app/components/ListPlusCell.tsx new file mode 100644 index 0000000000..a5e1e80914 --- /dev/null +++ b/app/components/ListPlusCell.tsx @@ -0,0 +1,39 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import React from 'react' + +import { Tooltip } from '~/ui/lib/Tooltip' + +type ListPlusCellProps = { + tooltipTitle: string + children: React.ReactNode +} + +/** + * Gives a count with a tooltip that expands to show details when the user hovers over it + */ +export const ListPlusCell = ({ tooltipTitle, children }: ListPlusCellProps) => { + const [first, ...rest] = React.Children.toArray(children) + const content = ( +
+
{tooltipTitle}
+ {...rest} +
+ ) + return ( +
+ {first} + {rest.length > 0 && ( + +
+{rest.length}
+
+ )} +
+ ) +} diff --git a/app/components/ProjectAccessRolesCell.tsx b/app/components/ProjectAccessRolesCell.tsx deleted file mode 100644 index 969be75ffb..0000000000 --- a/app/components/ProjectAccessRolesCell.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import type { CellContext } from '@tanstack/react-table' - -import type { RoleKey } from '@oxide/api' - -import { AccessBadge } from './AccessBadge' -import { ExpandedCountWithDetails } from './ExpandedCountWithDetails' - -/** - * Highlight the "effective" role, providing a tooltip for the alternate role. - * - * Example: User has collab on silo and viewer on project. Collab supersedes - * because it is the "stronger" role, i.e., it strictly includes the perms on - * viewer. So collab is highlighted as the "effective" role. - */ -export const ProjectAccessRolesCell = < - RowData extends { projectRole?: RoleKey; siloRole?: RoleKey }, ->( - info: CellContext -) => { - const effectiveRoleString = info.getValue() - if (!effectiveRoleString) return null - - const siloRole = info.row.original.siloRole - const formattedSiloRole = siloRole ? ( - - ) : undefined - - const projectRole = info.row.original.projectRole - const formattedProjectRole = projectRole ? ( - - ) : undefined - - const effectiveRoleIsSiloRole = effectiveRoleString === siloRole - const effectiveRole = effectiveRoleIsSiloRole ? formattedSiloRole : formattedProjectRole - const hasAlternateRole = Boolean(siloRole && projectRole) - const alternateRole = effectiveRoleIsSiloRole ? formattedProjectRole : formattedSiloRole - - return ( -
- {effectiveRole} - {hasAlternateRole && ( - - )} -
- ) -} diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 4d7aa7ffc5..3e93d857da 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -14,7 +14,7 @@ import { apiQueryClient, byGroupThenName, deleteRole, - getEffectiveRole, + roleOrder, useApiMutation, useApiQueryClient, usePrefetchedApiQuery, @@ -24,8 +24,9 @@ import { } from '@oxide/api' import { Access24Icon } from '@oxide/design-system/icons/react' +import { AccessBadge } from '~/components/AccessBadge' import { HL } from '~/components/HL' -import { ProjectAccessRolesCell } from '~/components/ProjectAccessRolesCell' +import { ListPlusCell } from '~/components/ListPlusCell' import { ProjectAccessAddUserSideModal, ProjectAccessEditUserSideModal, @@ -39,7 +40,7 @@ import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { TableActions, TableEmptyBox } from '~/ui/lib/Table' import { accessTypeLabel } from '~/util/access' -import { groupBy, isTruthy } from '~/util/array' +import { groupBy, isTruthy, sortBy } from '~/util/array' const EmptyState = ({ onClick }: { onClick: () => void }) => ( @@ -69,9 +70,8 @@ type UserRow = { id: string identityType: IdentityType name: string - siloRole: RoleKey | undefined projectRole: RoleKey | undefined - effectiveRole: RoleKey + roleBadges: { roleSource: string; roleName: RoleKey }[] } const colHelper = createColumnHelper() @@ -92,26 +92,23 @@ export function ProjectAccessPage() { const rows = useMemo(() => { return groupBy(siloRows.concat(projectRows), (u) => u.id) .map(([userId, userAssignments]) => { - const siloRole = userAssignments.find((a) => a.roleSource === 'silo')?.roleName - const projectRole = userAssignments.find( - (a) => a.roleSource === 'project' - )?.roleName + const { name, identityType } = userAssignments[0] - const roles = [siloRole, projectRole].filter(isTruthy) + const siloAccessRow = userAssignments.find((a) => a.roleSource === 'silo') + const projectAccessRow = userAssignments.find((a) => a.roleSource === 'project') - const { name, identityType } = userAssignments[0] + const roleBadges = sortBy( + [siloAccessRow, projectAccessRow].filter(isTruthy), + (r) => roleOrder[r.roleName] // sorts strongest role first + ) - const row: UserRow = { + return { id: userId, identityType, name, - siloRole, - projectRole, - // we know there has to be at least one - effectiveRole: getEffectiveRole(roles)!, - } - - return row + projectRole: projectAccessRow?.roleName, + roleBadges, + } satisfies UserRow }) .sort(byGroupThenName) }, [siloRows, projectRows]) @@ -132,7 +129,16 @@ export function ProjectAccessPage() { header: 'Type', cell: (props) => accessTypeLabel(props.getValue()), }), - colHelper.accessor('effectiveRole', { header: 'Role', cell: ProjectAccessRolesCell }), + colHelper.accessor('roleBadges', { + header: 'Role', + cell: (props) => ( + + {props.getValue().map(({ roleName, roleSource }) => ( + + ))} + + ), + }), // TODO: tooltips on disabled elements explaining why getActionsCol((row: UserRow) => [ From e809f2082a7027f7597425eeba3f74f7f6ca9037 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 21 Mar 2024 16:51:50 -0700 Subject: [PATCH 23/24] Remove AccessBadge component and use Badge at callsite --- app/components/AccessBadge.tsx | 19 ------------------- app/pages/SiloAccessPage.tsx | 6 +++--- .../project/access/ProjectAccessPage.tsx | 8 +++++--- 3 files changed, 8 insertions(+), 25 deletions(-) delete mode 100644 app/components/AccessBadge.tsx diff --git a/app/components/AccessBadge.tsx b/app/components/AccessBadge.tsx deleted file mode 100644 index 7b24661c29..0000000000 --- a/app/components/AccessBadge.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ - -import type { RoleKey } from '~/api' -import { Badge } from '~/ui/lib/Badge' -import { getBadgeColor } from '~/util/access' - -type AccessBadgeProps = { labelPrefix: string; role: RoleKey } - -export const AccessBadge = ({ labelPrefix, role }: AccessBadgeProps) => ( - - {labelPrefix}.{role} - -) diff --git a/app/pages/SiloAccessPage.tsx b/app/pages/SiloAccessPage.tsx index da6c8b6be1..a9b14f0d09 100644 --- a/app/pages/SiloAccessPage.tsx +++ b/app/pages/SiloAccessPage.tsx @@ -22,7 +22,6 @@ import { } from '@oxide/api' import { Access24Icon } from '@oxide/design-system/icons/react' -import { AccessBadge } from '~/components/AccessBadge' import { HL } from '~/components/HL' import { SiloAccessAddUserSideModal, @@ -31,11 +30,12 @@ import { import { confirmDelete } from '~/stores/confirm-delete' import { getActionsCol } from '~/table/columns/action-col' import { Table } from '~/table/Table' +import { Badge } from '~/ui/lib/Badge' import { Button } from '~/ui/lib/Button' import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { TableActions, TableEmptyBox } from '~/ui/lib/Table' -import { accessTypeLabel } from '~/util/access' +import { accessTypeLabel, getBadgeColor } from '~/util/access' import { groupBy, isTruthy } from '~/util/array' const EmptyState = ({ onClick }: { onClick: () => void }) => ( @@ -120,7 +120,7 @@ export function SiloAccessPage() { header: 'Role', cell: (props) => { const role = props.getValue() - return role ? : null + return role ? silo.{role} : null }, }), // TODO: tooltips on disabled elements explaining why diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 3e93d857da..d00bf648c9 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -24,7 +24,6 @@ import { } from '@oxide/api' import { Access24Icon } from '@oxide/design-system/icons/react' -import { AccessBadge } from '~/components/AccessBadge' import { HL } from '~/components/HL' import { ListPlusCell } from '~/components/ListPlusCell' import { @@ -35,11 +34,12 @@ import { getProjectSelector, useProjectSelector } from '~/hooks' import { confirmDelete } from '~/stores/confirm-delete' import { getActionsCol } from '~/table/columns/action-col' import { Table } from '~/table/Table' +import { Badge } from '~/ui/lib/Badge' import { Button } from '~/ui/lib/Button' import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { TableActions, TableEmptyBox } from '~/ui/lib/Table' -import { accessTypeLabel } from '~/util/access' +import { accessTypeLabel, getBadgeColor } from '~/util/access' import { groupBy, isTruthy, sortBy } from '~/util/array' const EmptyState = ({ onClick }: { onClick: () => void }) => ( @@ -134,7 +134,9 @@ export function ProjectAccessPage() { cell: (props) => ( {props.getValue().map(({ roleName, roleSource }) => ( - + + {roleSource}.{roleName} + ))} ), From 52198863d30d53bff9c43a90f3bb25f2c33bae76 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 22 Mar 2024 10:31:21 -0500 Subject: [PATCH 24/24] extract TipIcon and add one to Role column header --- .../project/access/ProjectAccessPage.tsx | 11 ++++++- app/ui/lib/FieldLabel.tsx | 12 ++------ app/ui/lib/TipIcon.tsx | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 app/ui/lib/TipIcon.tsx diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index d00bf648c9..3cffabd7ce 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -39,6 +39,7 @@ import { Button } from '~/ui/lib/Button' import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { TableActions, TableEmptyBox } from '~/ui/lib/Table' +import { TipIcon } from '~/ui/lib/TipIcon' import { accessTypeLabel, getBadgeColor } from '~/util/access' import { groupBy, isTruthy, sortBy } from '~/util/array' @@ -130,7 +131,15 @@ export function ProjectAccessPage() { cell: (props) => accessTypeLabel(props.getValue()), }), colHelper.accessor('roleBadges', { - header: 'Role', + header: () => ( + + Role + + A user or group's effective role for this project is the strongest role + on either the silo or project. + + + ), cell: (props) => ( {props.getValue().map(({ roleName, roleSource }) => ( diff --git a/app/ui/lib/FieldLabel.tsx b/app/ui/lib/FieldLabel.tsx index 2ae97ae5e6..5fb298051c 100644 --- a/app/ui/lib/FieldLabel.tsx +++ b/app/ui/lib/FieldLabel.tsx @@ -8,9 +8,7 @@ import cn from 'classnames' import type { ElementType, PropsWithChildren } from 'react' -import { Question12Icon } from '@oxide/design-system/icons/react' - -import { Tooltip } from '~/ui/lib/Tooltip' +import { TipIcon } from './TipIcon' interface FieldLabelProps { id: string @@ -43,13 +41,7 @@ export const FieldLabel = ({ )} - {tip && ( - - - - )} + {tip && {tip}}
) } diff --git a/app/ui/lib/TipIcon.tsx b/app/ui/lib/TipIcon.tsx new file mode 100644 index 0000000000..c704d54073 --- /dev/null +++ b/app/ui/lib/TipIcon.tsx @@ -0,0 +1,29 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import cn from 'classnames' + +import { Question12Icon } from '@oxide/design-system/icons/react' + +import { Tooltip } from './Tooltip' + +type TipIconProps = { + children: React.ReactNode + className?: string +} +export function TipIcon({ children, className }: TipIconProps) { + return ( + + + + ) +}