Skip to content

Commit d66310f

Browse files
committed
add silo access page and my tokens page
1 parent d52aec4 commit d66310f

17 files changed

+570
-176
lines changed

app/layouts/ProjectLayoutBase.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
7070
{ value: 'VPCs', path: pb.vpcs(projectSelector) },
7171
{ value: 'Floating IPs', path: pb.floatingIps(projectSelector) },
7272
{ value: 'Affinity Groups', path: pb.affinity(projectSelector) },
73-
{ value: 'Access', path: pb.projectAccess(projectSelector) },
73+
{ value: 'Project Access', path: pb.projectAccess(projectSelector) },
7474
]
7575
// filter out the entry for the path we're currently on
7676
.filter((i) => i.path !== pathname)
@@ -118,7 +118,7 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
118118
<Affinity16Icon /> Affinity Groups
119119
</NavLinkItem>
120120
<NavLinkItem to={pb.projectAccess(projectSelector)}>
121-
<Access16Icon /> Access
121+
<Access16Icon /> Project Access
122122
</NavLinkItem>
123123
</Sidebar.Nav>
124124
</Sidebar>

app/layouts/SettingsLayout.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
import { useMemo } from 'react'
99
import { useLocation, useNavigate } from 'react-router'
1010

11-
import { Folder16Icon, Key16Icon, Profile16Icon } from '@oxide/design-system/icons/react'
11+
import {
12+
Access16Icon,
13+
Folder16Icon,
14+
Key16Icon,
15+
Profile16Icon,
16+
} from '@oxide/design-system/icons/react'
1217

1318
import { TopBar } from '~/components/TopBar'
1419
import { makeCrumb } from '~/hooks/use-crumbs'
@@ -31,6 +36,7 @@ export default function SettingsLayout() {
3136
[
3237
{ value: 'Profile', path: pb.profile() },
3338
{ value: 'SSH Keys', path: pb.sshKeys() },
39+
{ value: 'Access Tokens', path: pb.accessTokens() },
3440
]
3541
// filter out the entry for the path we're currently on
3642
.filter((i) => i.path !== pathname)
@@ -61,6 +67,9 @@ export default function SettingsLayout() {
6167
<NavLinkItem to={pb.sshKeys()}>
6268
<Key16Icon /> SSH Keys
6369
</NavLinkItem>
70+
<NavLinkItem to={pb.accessTokens()}>
71+
<Access16Icon /> Access Tokens
72+
</NavLinkItem>
6473
</Sidebar.Nav>
6574
</Sidebar>
6675
<ContentPane />

app/layouts/SiloLayout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default function SiloLayout() {
3636
{ value: 'Projects', path: pb.projects() },
3737
{ value: 'Images', path: pb.siloImages() },
3838
{ value: 'Utilization', path: pb.siloUtilization() },
39-
{ value: 'Access', path: pb.siloAccess() },
39+
{ value: 'Silo Access', path: pb.siloAccessPolicy() },
4040
]
4141
// filter out the entry for the path we're currently on
4242
.filter((i) => i.path !== pathname)
@@ -67,7 +67,7 @@ export default function SiloLayout() {
6767
<NavLinkItem to={pb.siloUtilization()}>
6868
<Metrics16Icon /> Utilization
6969
</NavLinkItem>
70-
<NavLinkItem to={pb.siloAccess()}>
70+
<NavLinkItem to={pb.siloAccessPolicy()}>
7171
<Access16Icon /> Silo Access
7272
</NavLinkItem>
7373
</Sidebar.Nav>

app/pages/SiloAccessPage.tsx

Lines changed: 9 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -5,52 +5,15 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
9-
import { useMemo, useState } from 'react'
10-
11-
import {
12-
apiQueryClient,
13-
byGroupThenName,
14-
deleteRole,
15-
getEffectiveRole,
16-
useApiMutation,
17-
useApiQueryClient,
18-
usePrefetchedApiQuery,
19-
useUserRows,
20-
type IdentityType,
21-
type RoleKey,
22-
} from '@oxide/api'
8+
import { apiQueryClient } from '@oxide/api'
239
import { Access16Icon, Access24Icon } from '@oxide/design-system/icons/react'
2410

2511
import { DocsPopover } from '~/components/DocsPopover'
26-
import { HL } from '~/components/HL'
27-
import {
28-
SiloAccessAddUserSideModal,
29-
SiloAccessEditUserSideModal,
30-
} from '~/forms/silo-access'
31-
import { confirmDelete } from '~/stores/confirm-delete'
32-
import { getActionsCol } from '~/table/columns/action-col'
33-
import { Table } from '~/table/Table'
34-
import { Badge } from '~/ui/lib/Badge'
35-
import { CreateButton } from '~/ui/lib/CreateButton'
36-
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
12+
import { RouteTabs, Tab } from '~/components/RouteTabs'
13+
import { makeCrumb } from '~/hooks/use-crumbs'
3714
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
38-
import { TableActions, TableEmptyBox } from '~/ui/lib/Table'
39-
import { identityTypeLabel, roleColor } from '~/util/access'
40-
import { groupBy } from '~/util/array'
4115
import { docLinks } from '~/util/links'
42-
43-
const EmptyState = ({ onClick }: { onClick: () => void }) => (
44-
<TableEmptyBox>
45-
<EmptyMessage
46-
icon={<Access24Icon />}
47-
title="No authorized users"
48-
body="Give permission to view, edit, or administer this silo"
49-
buttonText="Add user or group"
50-
onClick={onClick}
51-
/>
52-
</TableEmptyBox>
53-
)
16+
import { pb } from '~/util/path-builder'
5417

5518
export async function clientLoader() {
5619
await Promise.all([
@@ -62,106 +25,9 @@ export async function clientLoader() {
6225
return null
6326
}
6427

65-
export const handle = { crumb: 'Silo Access' }
66-
67-
type UserRow = {
68-
id: string
69-
identityType: IdentityType
70-
name: string
71-
siloRole: RoleKey | undefined
72-
effectiveRole: RoleKey
73-
}
74-
75-
const colHelper = createColumnHelper<UserRow>()
28+
export const handle = makeCrumb('Silo Access', pb.siloAccessPolicy())
7629

7730
export default function SiloAccessPage() {
78-
const [addModalOpen, setAddModalOpen] = useState(false)
79-
const [editingUserRow, setEditingUserRow] = useState<UserRow | null>(null)
80-
81-
const { data: siloPolicy } = usePrefetchedApiQuery('policyView', {})
82-
const siloRows = useUserRows(siloPolicy.roleAssignments, 'silo')
83-
84-
const rows = useMemo(() => {
85-
return groupBy(siloRows, (u) => u.id)
86-
.map(([userId, userAssignments]) => {
87-
const siloRole = userAssignments.find((a) => a.roleSource === 'silo')?.roleName
88-
89-
const roles = siloRole ? [siloRole] : []
90-
91-
const { name, identityType } = userAssignments[0]
92-
93-
const row: UserRow = {
94-
id: userId,
95-
identityType,
96-
name,
97-
siloRole,
98-
// we know there has to be at least one
99-
effectiveRole: getEffectiveRole(roles)!,
100-
}
101-
102-
return row
103-
})
104-
.sort(byGroupThenName)
105-
}, [siloRows])
106-
107-
const queryClient = useApiQueryClient()
108-
const { mutateAsync: updatePolicy } = useApiMutation('policyUpdate', {
109-
onSuccess: () => queryClient.invalidateQueries('policyView'),
110-
// TODO: handle 403
111-
})
112-
113-
// TODO: checkboxes and bulk delete? not sure
114-
// TODO: disable delete on permissions you can't delete
115-
116-
const columns = useMemo(
117-
() => [
118-
colHelper.accessor('name', { header: 'Name' }),
119-
colHelper.accessor('identityType', {
120-
header: 'Type',
121-
cell: (info) => identityTypeLabel[info.getValue()],
122-
}),
123-
colHelper.accessor('siloRole', {
124-
header: 'Role',
125-
cell: (info) => {
126-
const role = info.getValue()
127-
return role ? <Badge color={roleColor[role]}>silo.{role}</Badge> : null
128-
},
129-
}),
130-
// TODO: tooltips on disabled elements explaining why
131-
getActionsCol((row: UserRow) => [
132-
{
133-
label: 'Change role',
134-
onActivate: () => setEditingUserRow(row),
135-
disabled: !row.siloRole && "You don't have permission to change this user's role",
136-
},
137-
// TODO: only show if you have permission to do this
138-
{
139-
label: 'Delete',
140-
onActivate: confirmDelete({
141-
doDelete: () =>
142-
updatePolicy({
143-
// we know policy is there, otherwise there's no row to display
144-
body: deleteRole(row.id, siloPolicy),
145-
}),
146-
label: (
147-
<span>
148-
the <HL>{row.siloRole}</HL> role for <HL>{row.name}</HL>
149-
</span>
150-
),
151-
}),
152-
disabled: !row.siloRole && "You don't have permission to delete this user",
153-
},
154-
]),
155-
],
156-
[siloPolicy, updatePolicy]
157-
)
158-
159-
const tableInstance = useReactTable({
160-
columns,
161-
data: rows,
162-
getCoreRowModel: getCoreRowModel(),
163-
})
164-
16531
return (
16632
<>
16733
<PageHeader>
@@ -174,30 +40,10 @@ export default function SiloAccessPage() {
17440
/>
17541
</PageHeader>
17642

177-
<TableActions>
178-
<CreateButton onClick={() => setAddModalOpen(true)}>Add user or group</CreateButton>
179-
</TableActions>
180-
{siloPolicy && addModalOpen && (
181-
<SiloAccessAddUserSideModal
182-
onDismiss={() => setAddModalOpen(false)}
183-
policy={siloPolicy}
184-
/>
185-
)}
186-
{siloPolicy && editingUserRow?.siloRole && (
187-
<SiloAccessEditUserSideModal
188-
onDismiss={() => setEditingUserRow(null)}
189-
policy={siloPolicy}
190-
name={editingUserRow.name}
191-
identityId={editingUserRow.id}
192-
identityType={editingUserRow.identityType}
193-
defaultValues={{ roleName: editingUserRow.siloRole }}
194-
/>
195-
)}
196-
{rows.length === 0 ? (
197-
<EmptyState onClick={() => setAddModalOpen(true)} />
198-
) : (
199-
<Table table={tableInstance} />
200-
)}
43+
<RouteTabs fullWidth>
44+
<Tab to={pb.siloAccessPolicy()}>Policy</Tab>
45+
<Tab to={pb.siloAccessSettings()}>Settings</Tab>
46+
</RouteTabs>
20147
</>
20248
)
20349
}

0 commit comments

Comments
 (0)