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'
239import { Access16Icon , Access24Icon } from '@oxide/design-system/icons/react'
2410
2511import { 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'
3714import { 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'
4115import { 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
5518export 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
7730export 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