diff --git a/app/api/roles.spec.ts b/app/api/roles.spec.ts index fa0efbe68..144774e75 100644 --- a/app/api/roles.spec.ts +++ b/app/api/roles.spec.ts @@ -14,6 +14,7 @@ import { effectiveScopedRole, getEffectiveRole, roleOrder, + rolesByIdFromPolicy, updateRole, userScopedRoleEntries, type Policy, @@ -125,6 +126,19 @@ test('byGroupThenName sorts as expected', () => { expect([c, e, b, d, a].sort(byGroupThenName)).toEqual([a, b, c, d, e]) }) +describe('rolesByIdFromPolicy', () => { + it('maps each identity to its role', () => { + expect(rolesByIdFromPolicy(abcAdminPolicy)).toEqual(new Map([['abc', 'admin']])) + }) + + it('keeps the strongest role when an identity has multiple assignments', () => { + const policy: Policy = { roleAssignments: [abcViewer, abcAdmin] } + expect(rolesByIdFromPolicy(policy)).toEqual(new Map([['abc', 'admin']])) + const reversed: Policy = { roleAssignments: [abcAdmin, abcViewer] } + expect(rolesByIdFromPolicy(reversed)).toEqual(new Map([['abc', 'admin']])) + }) +}) + describe('userScopedRoleEntries', () => { it('collapses multiple assignments for the same identity to the strongest role', () => { // API permits multiple assignments for one identity in a single policy diff --git a/app/api/roles.ts b/app/api/roles.ts index 83f6e240b..56a8b96ed 100644 --- a/app/api/roles.ts +++ b/app/api/roles.ts @@ -85,11 +85,20 @@ export function updateRole( return { roleAssignments } } -/** Map from identity ID to role name for quick lookup. */ +/** + * Map from identity ID to role name for quick lookup. If an actor has multiple + * assignments in the same policy (which the API allows) take the strongest one. + */ export function rolesByIdFromPolicy( policy: Policy ): Map { - return new Map(policy.roleAssignments.map((a) => [a.identityId, a.roleName])) + const map = new Map() + for (const { identityId, roleName } of policy.roleAssignments) { + const existing = map.get(identityId) + // non-null: list is non-empty + map.set(identityId, getEffectiveRole(existing ? [existing, roleName] : [roleName])!) + } + return map } /** @@ -160,12 +169,6 @@ export type ScopedRoleEntry = { source: { type: 'direct' } | { type: 'group'; group: { id: string; displayName: string } } } -/** Strongest role assigned to an identity in a policy, if any. */ -const roleForId = (policy: Policy, id: string) => - getEffectiveRole( - policy.roleAssignments.filter((ra) => ra.identityId === id).map((ra) => ra.roleName) - ) - /** * Enumerate all role assignments relevant to a user — one entry per direct * assignment and one per group assignment — across the given policies. Each @@ -181,12 +184,13 @@ export function userScopedRoleEntries( ): ScopedRoleEntry[] { const entries: ScopedRoleEntry[] = [] for (const { scope, policy } of scopedPolicies) { - const direct = roleForId(policy, userId) + const roleById = rolesByIdFromPolicy(policy) + const direct = roleById.get(userId) if (direct) { entries.push({ roleName: direct, scope, source: { type: 'direct' } }) } for (const group of userGroups) { - const via = roleForId(policy, group.id) + const via = roleById.get(group.id) if (via) { entries.push({ roleName: via, scope, source: { type: 'group', group } }) } diff --git a/app/components/access/AccessGroupsTab.tsx b/app/components/access/AccessGroupsTab.tsx index 71c4830a6..92b74968c 100644 --- a/app/components/access/AccessGroupsTab.tsx +++ b/app/components/access/AccessGroupsTab.tsx @@ -6,27 +6,25 @@ * Copyright Oxide Computer Company */ import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table' -import { useCallback, useMemo, useState, type ComponentType } from 'react' +import { useCallback, useMemo, useState } from 'react' import * as R from 'remeda' import { api, deleteRole, - effectiveScopedRole, q, + queryClient, rolesByIdFromPolicy, + useApiMutation, usePrefetchedQuery, - userScopedRoleEntries, - type AccessScope, type Group, - type Policy, type RoleKey, - type ScopedPolicy, } from '@oxide/api' import { PersonGroup24Icon } from '@oxide/design-system/icons/react' import { Badge } from '@oxide/design-system/ui' -import { type EditRoleModalProps } from '~/forms/access-util' +import { SiloAccessEditUserSideModal } from '~/forms/silo-access' +import { addToast } from '~/stores/toast' import { EmptyCell } from '~/table/cells/EmptyCell' import { ButtonCell } from '~/table/cells/LinkCell' import { MemberCountCell } from '~/table/cells/MemberCountCell' @@ -40,12 +38,13 @@ import { ALL_ISH } from '~/util/consts' import { GroupMembersSideModal } from './GroupMembersSideModal' import { buildRoleActions } from './roleActions' -import { useCanEditPolicy } from './use-can-edit-policy' +import { useCanEditSiloPolicy } from './use-can-edit-policy' // The API only sorts groups by id, so fetch the full set and sort by name // client-side. ALL_ISH is the practical ceiling; a silo with more groups than // that would have its tail dropped in (arbitrary) id order. const groupListAll = q(api.groupList, { query: { limit: ALL_ISH } }) +const policyView = q(api.policyView, {}) const colHelper = createColumnHelper() @@ -61,23 +60,7 @@ const GroupEmptyState = () => ( type EditingState = { group: Group; defaultRole: RoleKey | undefined } -type Props = { - /** Policies that contribute to a group's effective role on this page. */ - scopedPolicies: ScopedPolicy[] - /** Scope managed by this tab — its direct roles are assignable/removable. */ - managedScope: AccessScope - /** Modal for assigning/editing a role on the managed policy. */ - EditModal: ComponentType - /** Update the managed policy. Called when removing a role. */ - updateManagedPolicy: (newPolicy: Policy) => Promise -} - -export function AccessGroupsTab({ - scopedPolicies, - managedScope, - EditModal, - updateManagedPolicy, -}: Props) { +export function AccessGroupsTab() { const [selectedGroup, setSelectedGroup] = useState(null) const [editingGroup, setEditingGroup] = useState(null) @@ -87,12 +70,17 @@ export function AccessGroupsTab({ [groups] ) - // non-null: caller is responsible for including the managed scope - const managedPolicy = scopedPolicies.find((sp) => sp.scope === managedScope)!.policy + const { data: siloPolicy } = usePrefetchedQuery(policyView) + const roleById = useMemo(() => rolesByIdFromPolicy(siloPolicy), [siloPolicy]) - const managedRoleById = useMemo(() => rolesByIdFromPolicy(managedPolicy), [managedPolicy]) + const canEdit = useCanEditSiloPolicy(siloPolicy) - const canEdit = useCanEditPolicy(scopedPolicies, managedScope) + const { mutateAsync: updatePolicy } = useApiMutation(api.policyUpdate, { + onSuccess: () => { + queryClient.invalidateEndpoint('policyView') + addToast({ content: 'Role removed' }) + }, + }) const roleCol = useMemo( () => @@ -100,18 +88,13 @@ export function AccessGroupsTab({ id: 'role', header: 'Role', cell: ({ row }) => { - // groups never inherit, so passing no groups yields direct roles only - const entries = userScopedRoleEntries(row.original.id, [], scopedPolicies) - const effective = effectiveScopedRole(entries) - if (!effective) return - return ( - - {effective.scope}.{effective.role} - - ) + // groups never inherit, so their only silo role is a direct one + const role = roleById.get(row.original.id) + if (!role) return + return silo.{role} }, }), - [scopedPolicies] + [roleById] ) const staticColumns = useMemo( @@ -137,29 +120,18 @@ export function AccessGroupsTab({ const makeActions = useCallback( (group: Group): MenuAction[] => { - const directManagedRole = managedRoleById.get(group.id) - const entries = userScopedRoleEntries(group.id, [], scopedPolicies) - const effective = effectiveScopedRole(entries) + const directRole = roleById.get(group.id) return buildRoleActions({ name: group.displayName, - managedScope, - directManagedRole, - effective, - inheritedReason: 'Role is inherited from another scope; modify it there to revoke', + directRole, + effectiveRole: directRole, canEdit, isSelf: false, // a group is never the current user openEditModal: (defaultRole) => setEditingGroup({ group, defaultRole }), - doRemove: () => updateManagedPolicy(deleteRole(group.id, managedPolicy)), + doRemove: () => updatePolicy({ body: deleteRole(group.id, siloPolicy) }), }) }, - [ - managedRoleById, - managedPolicy, - updateManagedPolicy, - scopedPolicies, - managedScope, - canEdit, - ] + [roleById, siloPolicy, updatePolicy, canEdit] ) const columns = useColsWithActions(staticColumns, makeActions) @@ -175,9 +147,9 @@ export function AccessGroupsTab({ <> {sortedGroups.length === 0 ? : } {editingGroup && ( - setEditingGroup(null)} - policy={managedPolicy} + policy={siloPolicy} name={editingGroup.group.displayName} identityId={editingGroup.group.id} identityType="silo_group" @@ -188,7 +160,7 @@ export function AccessGroupsTab({ setSelectedGroup(null)} - scopedPolicies={scopedPolicies} + scopedPolicies={[{ scope: 'silo', policy: siloPolicy }]} /> )} diff --git a/app/components/access/AccessUsersTab.tsx b/app/components/access/AccessUsersTab.tsx index c6b908280..041d5de81 100644 --- a/app/components/access/AccessUsersTab.tsx +++ b/app/components/access/AccessUsersTab.tsx @@ -6,31 +6,31 @@ * Copyright Oxide Computer Company */ import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table' -import { useCallback, useMemo, useState, type ComponentType } from 'react' +import { useCallback, useMemo, useState } from 'react' import * as R from 'remeda' import { api, deleteRole, - effectiveScopedRole, + getEffectiveRole, q, + queryClient, roleOrder, rolesByIdFromPolicy, + useApiMutation, useGroupsByUserId, usePrefetchedQuery, - userScopedRoleEntries, - type AccessScope, - type Policy, + type Group, type RoleKey, - type ScopedPolicy, type User, } from '@oxide/api' import { Person24Icon } from '@oxide/design-system/icons/react' import { Badge } from '@oxide/design-system/ui' import { ListPlusCell } from '~/components/ListPlusCell' -import { type EditRoleModalProps } from '~/forms/access-util' +import { SiloAccessEditUserSideModal } from '~/forms/silo-access' import { useCurrentUser } from '~/hooks/use-current-user' +import { addToast } from '~/stores/toast' import { EmptyCell } from '~/table/cells/EmptyCell' import { ButtonCell } from '~/table/cells/LinkCell' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' @@ -43,7 +43,7 @@ import { roleColor } from '~/util/access' import { ALL_ISH } from '~/util/consts' import { buildRoleActions } from './roleActions' -import { useCanEditPolicy } from './use-can-edit-policy' +import { useCanEditSiloPolicy } from './use-can-edit-policy' import { UserDetailsSideModal } from './UserDetailsSideModal' // The API only sorts users by id, so fetch the full set and sort by name @@ -51,6 +51,7 @@ import { UserDetailsSideModal } from './UserDetailsSideModal' // that would have its tail dropped in (arbitrary) id order. const userListAll = q(api.userList, { query: { limit: ALL_ISH } }) const groupListAll = q(api.groupList, { query: { limit: ALL_ISH } }) +const policyView = q(api.policyView, {}) const colHelper = createColumnHelper() @@ -66,25 +67,36 @@ const EmptyState = () => ( ) -type EditingState = { user: User; defaultRole: RoleKey | undefined } - -type Props = { - /** Policies that contribute to a user's effective role on this page. */ - scopedPolicies: ScopedPolicy[] - /** Scope managed by this tab — its direct roles are assignable/removable. */ - managedScope: AccessScope - /** Modal for assigning/editing a role on the managed policy. */ - EditModal: ComponentType - /** Update the managed policy. Called when removing a role. */ - updateManagedPolicy: (newPolicy: Policy) => Promise +/** + * A user's effective silo role: the strongest of their direct assignment and + * any roles inherited from their groups. When no direct assignment covers the + * effective role, `viaGroups` lists the groups it comes from. + */ +function effectiveSiloRole( + userId: string, + userGroups: Group[], + roleById: Map +): { role: RoleKey; viaGroups: Group[] } | null { + const directRole = roleById.get(userId) + const groupEntries = userGroups.flatMap((group) => { + const role = roleById.get(group.id) + return role ? [{ group, role }] : [] + }) + const role = getEffectiveRole([ + ...(directRole ? [directRole] : []), + ...groupEntries.map((e) => e.role), + ]) + if (!role) return null + const directCovers = directRole && roleOrder[directRole] <= roleOrder[role] + const viaGroups = directCovers + ? [] + : groupEntries.filter((e) => roleOrder[e.role] <= roleOrder[role]).map((e) => e.group) + return { role, viaGroups } } -export function AccessUsersTab({ - scopedPolicies, - managedScope, - EditModal, - updateManagedPolicy, -}: Props) { +type EditingState = { user: User; defaultRole: RoleKey | undefined } + +export function AccessUsersTab() { const [selectedUser, setSelectedUser] = useState(null) const [editingUser, setEditingUser] = useState(null) @@ -97,14 +109,19 @@ export function AccessUsersTab({ const { data: groups } = usePrefetchedQuery(groupListAll) const groupsByUserId = useGroupsByUserId(groups.items) - // non-null: caller is responsible for including the managed scope - const managedPolicy = scopedPolicies.find((sp) => sp.scope === managedScope)!.policy + const { data: siloPolicy } = usePrefetchedQuery(policyView) + const roleById = useMemo(() => rolesByIdFromPolicy(siloPolicy), [siloPolicy]) - const managedRoleById = useMemo(() => rolesByIdFromPolicy(managedPolicy), [managedPolicy]) - - const canEdit = useCanEditPolicy(scopedPolicies, managedScope) + const canEdit = useCanEditSiloPolicy(siloPolicy) const { me } = useCurrentUser() + const { mutateAsync: updatePolicy } = useApiMutation(api.policyUpdate, { + onSuccess: () => { + queryClient.invalidateEndpoint('policyView') + addToast({ content: 'Role removed' }) + }, + }) + const roleCol = useMemo( () => colHelper.display({ @@ -112,41 +129,15 @@ export function AccessUsersTab({ header: 'Role', cell: ({ row }) => { const userGroups = groupsByUserId.get(row.original.id) ?? [] - const entries = userScopedRoleEntries(row.original.id, userGroups, scopedPolicies) - const effective = effectiveScopedRole(entries) + const effective = effectiveSiloRole(row.original.id, userGroups, roleById) if (!effective) return - // show "via groups" tooltip when the displayed role+scope isn't - // covered by a direct assignment in that scope. (a direct project - // role doesn't suppress the tooltip if the displayed badge is the - // silo scope coming via a group, since silo wins ties.) - const displayedScopeHasDirect = entries.some( - (e) => - e.source.type === 'direct' && - e.scope === effective.scope && - roleOrder[e.roleName] <= roleOrder[effective.role] - ) - const viaGroupsMap = new Map() - if (!displayedScopeHasDirect) { - for (const e of entries) { - if ( - e.source.type === 'group' && - e.scope === effective.scope && - roleOrder[e.roleName] <= roleOrder[effective.role] - ) { - viaGroupsMap.set(e.source.group.id, e.source.group) - } - } - } - const viaGroups = [...viaGroupsMap.values()] return (
- - {effective.scope}.{effective.role} - - {viaGroups.length > 0 && ( + silo.{effective.role} + {effective.viaGroups.length > 0 && ( via{' '} - {viaGroups.map((g, i) => ( + {effective.viaGroups.map((g, i) => ( {i > 0 && ', '} {g.displayName} @@ -158,7 +149,7 @@ export function AccessUsersTab({ ) }, }), - [groupsByUserId, scopedPolicies] + [groupsByUserId, roleById] ) const groupsCol = useMemo( @@ -199,35 +190,19 @@ export function AccessUsersTab({ const makeActions = useCallback( (user: User): MenuAction[] => { - const directManagedRole = managedRoleById.get(user.id) const userGroups = groupsByUserId.get(user.id) ?? [] - const entries = userScopedRoleEntries(user.id, userGroups, scopedPolicies) - const effective = effectiveScopedRole(entries) - const inheritedReason = `Role is inherited; modify the source ${ - entries.find((e) => e.source.type === 'group') ? 'group' : 'silo assignment' - } to revoke` + const effective = effectiveSiloRole(user.id, userGroups, roleById) return buildRoleActions({ name: user.displayName, - managedScope, - directManagedRole, - effective, - inheritedReason, + directRole: roleById.get(user.id), + effectiveRole: effective?.role, canEdit, isSelf: user.id === me.id, openEditModal: (defaultRole) => setEditingUser({ user, defaultRole }), - doRemove: () => updateManagedPolicy(deleteRole(user.id, managedPolicy)), + doRemove: () => updatePolicy({ body: deleteRole(user.id, siloPolicy) }), }) }, - [ - managedRoleById, - managedPolicy, - updateManagedPolicy, - groupsByUserId, - scopedPolicies, - managedScope, - canEdit, - me, - ] + [roleById, siloPolicy, updatePolicy, groupsByUserId, canEdit, me] ) const columns = useColsWithActions(staticColumns, makeActions) @@ -243,9 +218,9 @@ export function AccessUsersTab({ <> {sortedUsers.length === 0 ? :
} {editingUser && ( - setEditingUser(null)} - policy={managedPolicy} + policy={siloPolicy} name={editingUser.user.displayName} identityId={editingUser.user.id} identityType="silo_user" @@ -256,7 +231,7 @@ export function AccessUsersTab({ setSelectedUser(null)} - scopedPolicies={scopedPolicies} + scopedPolicies={[{ scope: 'silo', policy: siloPolicy }]} userGroups={groupsByUserId.get(selectedUser.id) ?? []} /> )} diff --git a/app/components/access/roleActions.tsx b/app/components/access/roleActions.tsx index d2372677e..544130116 100644 --- a/app/components/access/roleActions.tsx +++ b/app/components/access/roleActions.tsx @@ -68,79 +68,65 @@ export function buildRemoveRoleAction({ type BuildRoleActionsArgs = { /** Display name of the user or group, used in the confirm-delete copy. */ name: string - /** Scope managed by this tab; determines labels and project-specific framing. */ - managedScope: AccessScope - /** Direct role on the managed policy, if any. Required to remove a role. */ - directManagedRole: RoleKey | undefined - /** Effective role across all scopes, or null if the identity has no role. */ - effective: { role: RoleKey } | null - /** Disabled reason shown when there's no direct managed role to remove. */ - inheritedReason: string - /** Whether the current user can add/change/remove roles in the managed scope. */ + /** Direct role on the silo policy, if any. Required to remove a role. */ + directRole: RoleKey | undefined + /** Effective role including group-inherited, undefined if none. */ + effectiveRole: RoleKey | undefined + /** Whether the current user can add/change/remove silo roles. */ canEdit: boolean /** Whether this row is the current user (shows a self-removal warning). */ isSelf: boolean /** Open the edit modal, pre-filled with the given role (undefined = assign). */ openEditModal: (defaultRole: RoleKey | undefined) => void - /** Remove the direct managed role. */ + /** Remove the direct silo role. */ doRemove: () => Promise } /** - * Row-action menu for a user or group in the access tabs. Identical logic for - * both; callers supply how the effective role was computed and the - * inherited-role message (which differs because a user can inherit via a group - * or the silo, while a group only inherits from another scope). + * Row-action menu for a user or group in the silo users/groups tabs. For + * groups, direct and effective are the same role (groups don't inherit). For + * users, an inherited (via group) role can be promoted to a direct silo + * assignment via the change action, pre-filled with the effective role. */ export function buildRoleActions({ name, - managedScope, - directManagedRole, - effective, - inheritedReason, + directRole, + effectiveRole, canEdit, isSelf, openEditModal, doRemove, }: BuildRoleActionsArgs): MenuAction[] { - const addAction: MenuAction = { - label: roleActionLabel(managedScope, 'add'), - onActivate: () => openEditModal(undefined), - disabled: !canEdit && noRolePermissionReason(managedScope, 'add'), - } - const removeAction = buildRemoveRoleAction({ - name, - role: directManagedRole, - scope: managedScope, - isSelf, - disabledReason: !canEdit - ? noRolePermissionReason(managedScope, 'remove') - : // a direct role on the managed policy is required to remove anything - !directManagedRole - ? inheritedReason - : undefined, - doRemove, - }) // No role at all — direct or inherited. - if (!effective) { - return [addAction] - } - // For the project tab, an inherited silo role doesn't give us anything to - // "change" on the project policy — frame it as adding a project role. For - // the silo tab, an inherited (via group) role can be promoted to a direct - // silo assignment via the change action, pre-filled with the effective role. - if (managedScope === 'project' && !directManagedRole) { - return [addAction, removeAction] + if (!effectiveRole) { + return [ + { + label: roleActionLabel('silo', 'add'), + onActivate: () => openEditModal(undefined), + disabled: !canEdit && noRolePermissionReason('silo', 'add'), + }, + ] } - // Pre-fill with the direct managed role if any; otherwise the effective role - // so the modal opens in 'edit' mode showing the role currently in effect. - const defaultRole = directManagedRole ?? effective.role return [ { - label: roleActionLabel(managedScope, 'change'), - onActivate: () => openEditModal(defaultRole), - disabled: !canEdit && noRolePermissionReason(managedScope, 'change'), + label: roleActionLabel('silo', 'change'), + // pre-fill with the direct role if any; otherwise the effective role so + // the modal opens in 'edit' mode showing the role currently in effect + onActivate: () => openEditModal(directRole ?? effectiveRole), + disabled: !canEdit && noRolePermissionReason('silo', 'change'), }, - removeAction, + buildRemoveRoleAction({ + name, + role: directRole, + scope: 'silo', + isSelf, + disabledReason: !canEdit + ? noRolePermissionReason('silo', 'remove') + : // a direct role is required to remove anything + !directRole + ? "Role is inherited from a group; change the group's role to revoke" + : undefined, + doRemove, + }), ] } diff --git a/app/components/access/use-can-edit-policy.ts b/app/components/access/use-can-edit-policy.ts index 4c4337f37..621ffeb07 100644 --- a/app/components/access/use-can-edit-policy.ts +++ b/app/components/access/use-can-edit-policy.ts @@ -10,6 +10,7 @@ import { userScopedRoleEntries, type AccessScope, type FleetRolePolicy, + type Policy, type ScopedPolicy, } from '@oxide/api' @@ -43,6 +44,11 @@ export function useCanEditPolicy( siloRole === 'collaborator' } +/** `useCanEditPolicy` for the silo-only tabs, which have no scope machinery. */ +export function useCanEditSiloPolicy(siloPolicy: Policy): boolean { + return useCanEditPolicy([{ scope: 'silo', policy: siloPolicy }], 'silo') +} + /** * Whether the current user can add, change, or remove fleet role assignments. * Modifying the fleet policy requires the fleet admin role. Fleet is the diff --git a/app/pages/SiloGroupsTab.tsx b/app/pages/SiloGroupsTab.tsx index 215ed7f38..939ed6dc6 100644 --- a/app/pages/SiloGroupsTab.tsx +++ b/app/pages/SiloGroupsTab.tsx @@ -5,48 +5,9 @@ * * Copyright Oxide Computer Company */ -import { useMemo } from 'react' - -import { - api, - q, - queryClient, - useApiMutation, - usePrefetchedQuery, - type Policy, - type ScopedPolicy, -} from '@oxide/api' - import { AccessGroupsTab } from '~/components/access/AccessGroupsTab' -import { SiloAccessEditUserSideModal } from '~/forms/silo-access' import { titleCrumb } from '~/hooks/use-crumbs' -import { addToast } from '~/stores/toast' - -const policyView = q(api.policyView, {}) export const handle = titleCrumb('Groups') -export default function SiloGroupsTab() { - const { data: siloPolicy } = usePrefetchedQuery(policyView) - - const scopedPolicies = useMemo( - () => [{ scope: 'silo', policy: siloPolicy }] satisfies ScopedPolicy[], - [siloPolicy] - ) - - const { mutateAsync: updatePolicy } = useApiMutation(api.policyUpdate, { - onSuccess: () => { - queryClient.invalidateEndpoint('policyView') - addToast({ content: 'Role removed' }) - }, - }) - - return ( - updatePolicy({ body })} - /> - ) -} +export default AccessGroupsTab diff --git a/app/pages/SiloUsersTab.tsx b/app/pages/SiloUsersTab.tsx index 00c0de4c9..6464b0d78 100644 --- a/app/pages/SiloUsersTab.tsx +++ b/app/pages/SiloUsersTab.tsx @@ -5,48 +5,9 @@ * * Copyright Oxide Computer Company */ -import { useMemo } from 'react' - -import { - api, - q, - queryClient, - useApiMutation, - usePrefetchedQuery, - type Policy, - type ScopedPolicy, -} from '@oxide/api' - import { AccessUsersTab } from '~/components/access/AccessUsersTab' -import { SiloAccessEditUserSideModal } from '~/forms/silo-access' import { titleCrumb } from '~/hooks/use-crumbs' -import { addToast } from '~/stores/toast' - -const policyView = q(api.policyView, {}) export const handle = titleCrumb('Users') -export default function SiloUsersTab() { - const { data: siloPolicy } = usePrefetchedQuery(policyView) - - const scopedPolicies = useMemo( - () => [{ scope: 'silo', policy: siloPolicy }] satisfies ScopedPolicy[], - [siloPolicy] - ) - - const { mutateAsync: updatePolicy } = useApiMutation(api.policyUpdate, { - onSuccess: () => { - queryClient.invalidateEndpoint('policyView') - addToast({ content: 'Role removed' }) - }, - }) - - return ( - updatePolicy({ body })} - /> - ) -} +export default AccessUsersTab