Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/layouts/SiloLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function SiloLayout() {
{ value: 'Projects', path: pb.projects() },
{ value: 'Images', path: pb.siloImages() },
{ value: 'Utilization', path: pb.siloUtilization() },
{ value: 'Silo Access', path: pb.siloAccess() },
{ value: 'Silo Access', path: pb.siloAccessPolicy() },
]
// filter out the entry for the path we're currently on
.filter((i) => i.path !== pathname)
Expand Down Expand Up @@ -67,7 +67,7 @@ export default function SiloLayout() {
<NavLinkItem to={pb.siloUtilization()}>
<Metrics16Icon /> Utilization
</NavLinkItem>
<NavLinkItem to={pb.siloAccess()}>
<NavLinkItem to={pb.siloAccessPolicy()}>
<Access16Icon /> Silo Access
</NavLinkItem>
</Sidebar.Nav>
Expand Down
172 changes: 9 additions & 163 deletions app/pages/SiloAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,15 @@
*
* Copyright Oxide Computer Company
*/
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { useMemo, useState } from 'react'

import {
apiQueryClient,
byGroupThenName,
deleteRole,
getEffectiveRole,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
useUserRows,
type IdentityType,
type RoleKey,
} from '@oxide/api'
import { apiQueryClient } from '@oxide/api'
import { Access16Icon, Access24Icon } from '@oxide/design-system/icons/react'

import { DocsPopover } from '~/components/DocsPopover'
import { HL } from '~/components/HL'
import {
SiloAccessAddUserSideModal,
SiloAccessEditUserSideModal,
} from '~/forms/silo-access'
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 { CreateButton } from '~/ui/lib/CreateButton'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { RouteTabs, Tab } from '~/components/RouteTabs'
import { makeCrumb } from '~/hooks/use-crumbs'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { TableActions, TableEmptyBox } from '~/ui/lib/Table'
import { identityTypeLabel, roleColor } from '~/util/access'
import { groupBy } from '~/util/array'
import { docLinks } from '~/util/links'

const EmptyState = ({ onClick }: { onClick: () => void }) => (
<TableEmptyBox>
<EmptyMessage
icon={<Access24Icon />}
title="No authorized users"
body="Give permission to view, edit, or administer this silo"
buttonText="Add user or group"
onClick={onClick}
/>
</TableEmptyBox>
)
import { pb } from '~/util/path-builder'

export async function clientLoader() {
await Promise.all([
Expand All @@ -62,106 +25,9 @@ export async function clientLoader() {
return null
}

export const handle = { crumb: 'Silo Access' }

type UserRow = {
id: string
identityType: IdentityType
name: string
siloRole: RoleKey | undefined
effectiveRole: RoleKey
}

const colHelper = createColumnHelper<UserRow>()
export const handle = makeCrumb('Silo Access', pb.siloAccessPolicy())

export default function SiloAccessPage() {
const [addModalOpen, setAddModalOpen] = useState(false)
const [editingUserRow, setEditingUserRow] = useState<UserRow | null>(null)

const { data: siloPolicy } = usePrefetchedApiQuery('policyView', {})
const siloRows = useUserRows(siloPolicy.roleAssignments, 'silo')

const rows = useMemo(() => {
return groupBy(siloRows, (u) => u.id)
.map(([userId, userAssignments]) => {
const siloRole = userAssignments.find((a) => a.roleSource === 'silo')?.roleName

const roles = siloRole ? [siloRole] : []

const { name, identityType } = userAssignments[0]

const row: UserRow = {
id: userId,
identityType,
name,
siloRole,
// we know there has to be at least one
effectiveRole: getEffectiveRole(roles)!,
}

return row
})
.sort(byGroupThenName)
}, [siloRows])

const queryClient = useApiQueryClient()
const { mutateAsync: updatePolicy } = useApiMutation('policyUpdate', {
onSuccess: () => queryClient.invalidateQueries('policyView'),
// TODO: handle 403
})

// TODO: checkboxes and bulk delete? not sure
// TODO: disable delete on permissions you can't delete

const columns = useMemo(
() => [
colHelper.accessor('name', { header: 'Name' }),
colHelper.accessor('identityType', {
header: 'Type',
cell: (info) => identityTypeLabel[info.getValue()],
}),
colHelper.accessor('siloRole', {
header: 'Role',
cell: (info) => {
const role = info.getValue()
return role ? <Badge color={roleColor[role]}>silo.{role}</Badge> : null
},
}),
// TODO: tooltips on disabled elements explaining why
getActionsCol((row: UserRow) => [
{
label: 'Change role',
onActivate: () => setEditingUserRow(row),
disabled: !row.siloRole && "You don't have permission to change this user's role",
},
// TODO: only show if you have permission to do this
{
label: 'Delete',
onActivate: confirmDelete({
doDelete: () =>
updatePolicy({
// we know policy is there, otherwise there's no row to display
body: deleteRole(row.id, siloPolicy),
}),
label: (
<span>
the <HL>{row.siloRole}</HL> role for <HL>{row.name}</HL>
</span>
),
}),
disabled: !row.siloRole && "You don't have permission to delete this user",
},
]),
],
[siloPolicy, updatePolicy]
)

const tableInstance = useReactTable({
columns,
data: rows,
getCoreRowModel: getCoreRowModel(),
})

return (
<>
<PageHeader>
Expand All @@ -174,30 +40,10 @@ export default function SiloAccessPage() {
/>
</PageHeader>

<TableActions>
<CreateButton onClick={() => setAddModalOpen(true)}>Add user or group</CreateButton>
</TableActions>
{siloPolicy && addModalOpen && (
<SiloAccessAddUserSideModal
onDismiss={() => setAddModalOpen(false)}
policy={siloPolicy}
/>
)}
{siloPolicy && editingUserRow?.siloRole && (
<SiloAccessEditUserSideModal
onDismiss={() => setEditingUserRow(null)}
policy={siloPolicy}
name={editingUserRow.name}
identityId={editingUserRow.id}
identityType={editingUserRow.identityType}
defaultValues={{ roleName: editingUserRow.siloRole }}
/>
)}
{rows.length === 0 ? (
<EmptyState onClick={() => setAddModalOpen(true)} />
) : (
<Table table={tableInstance} />
)}
<RouteTabs fullWidth>
<Tab to={pb.siloAccessPolicy()}>Policy</Tab>
<Tab to={pb.siloAccessSettings()}>Settings</Tab>
</RouteTabs>
</>
)
}
Loading
Loading