Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bc35726
Add Type column to access table
charliepark Mar 13, 2024
2b83127
import from @oxide/api
charliepark Mar 13, 2024
cccdcc5
Proper description
charliepark Mar 13, 2024
5ec39cc
add AccessRolesCell
charliepark Mar 13, 2024
e14a009
use 'silo' for label
charliepark Mar 13, 2024
91d7b31
Remove duplicate columns
charliepark Mar 13, 2024
ca009e8
Add AccessBadge
charliepark Mar 13, 2024
42e7b5e
Small refactor to AccessBadge
charliepark Mar 13, 2024
5845fc5
Refactor; make a few tweaks Ben suggested
charliepark Mar 13, 2024
3a51fb8
Merge main and resolve conflicts
charliepark Mar 13, 2024
cf71b01
refactoring
charliepark Mar 13, 2024
48f06dd
Remove unneeded Cell components
charliepark Mar 13, 2024
4226d14
Update app/components/AccessBadge.tsx
charliepark Mar 13, 2024
74dd05a
Bot commit: format with prettier
github-actions[bot] Mar 13, 2024
2e788be
Get tooltip working, though list of roles needs work
charliepark Mar 13, 2024
266afb2
Merge branch 'main' into add_access_roles_column
charliepark Mar 14, 2024
e8ccc9b
Fix tests
charliepark Mar 14, 2024
973f4dd
Working on inherited roles
charliepark Mar 15, 2024
ee10e30
Merge branch 'main' into add_access_roles_column
charliepark Mar 19, 2024
94a49df
this isn't working; saving to log current state, but will back some c…
charliepark Mar 19, 2024
b2befc2
cleanup
charliepark Mar 19, 2024
0aff37f
Add component for ExpandedCountWithDetails
charliepark Mar 19, 2024
85475ff
cleanup
charliepark Mar 20, 2024
5c030b7
Small test refactor
charliepark Mar 20, 2024
f5154e3
Merge branch 'main' into add_access_roles_column
charliepark Mar 21, 2024
a0bcb6b
Access roles refactor suggestions (#2089)
david-crespo Mar 21, 2024
e809f20
Remove AccessBadge component and use Badge at callsite
charliepark Mar 21, 2024
5219886
extract TipIcon and add one to Role column header
david-crespo Mar 22, 2024
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
39 changes: 39 additions & 0 deletions app/components/ListPlusCell.tsx
Original file line number Diff line number Diff line change
@@ -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 = (
<div>
<div className="mb-2">{tooltipTitle}</div>
{...rest}
</div>
)
return (
<div className="flex items-baseline gap-2">
{first}
{rest.length > 0 && (
<Tooltip content={content} placement="bottom">
<div className="text-mono-sm">+{rest.length}</div>
</Tooltip>
)}
</div>
)
}
30 changes: 0 additions & 30 deletions app/components/RoleBadgeCell.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion app/forms/project-access.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
2 changes: 1 addition & 1 deletion app/forms/silo-access.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
11 changes: 7 additions & 4 deletions app/pages/SiloAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ import {
import { Access24Icon } from '@oxide/design-system/icons/react'

import { HL } from '~/components/HL'
import { RoleBadgeCell } from '~/components/RoleBadgeCell'
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 { 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 }) => (
Expand Down Expand Up @@ -117,8 +117,11 @@ export function SiloAccessPage() {
cell: (props) => accessTypeLabel(props.getValue()),
}),
colHelper.accessor('siloRole', {
header: 'Silo role',
cell: RoleBadgeCell,
header: 'Role',
cell: (props) => {
const role = props.getValue()
return role ? <Badge color={getBadgeColor(role)}>silo.{role}</Badge> : null
},
}),
// TODO: tooltips on disabled elements explaining why
getActionsCol((row: UserRow) => [
Expand Down
65 changes: 38 additions & 27 deletions app/pages/project/access/ProjectAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
apiQueryClient,
byGroupThenName,
deleteRole,
getEffectiveRole,
roleOrder,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
Expand All @@ -25,7 +25,7 @@ import {
import { Access24Icon } from '@oxide/design-system/icons/react'

import { HL } from '~/components/HL'
import { RoleBadgeCell } from '~/components/RoleBadgeCell'
import { ListPlusCell } from '~/components/ListPlusCell'
import {
ProjectAccessAddUserSideModal,
ProjectAccessEditUserSideModal,
Expand All @@ -34,12 +34,14 @@ 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 { groupBy, isTruthy } from '~/util/array'
import { TipIcon } from '~/ui/lib/TipIcon'
import { accessTypeLabel, getBadgeColor } from '~/util/access'
import { groupBy, isTruthy, sortBy } from '~/util/array'

const EmptyState = ({ onClick }: { onClick: () => void }) => (
<TableEmptyBox>
Expand Down Expand Up @@ -69,9 +71,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<UserRow>()
Expand All @@ -92,26 +93,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])
Expand All @@ -132,14 +130,27 @@ export function ProjectAccessPage() {
header: 'Type',
cell: (props) => accessTypeLabel(props.getValue()),
}),
colHelper.accessor('siloRole', {
header: 'Silo role',
cell: RoleBadgeCell,
}),
colHelper.accessor('projectRole', {
header: 'Project role',
cell: RoleBadgeCell,
colHelper.accessor('roleBadges', {
header: () => (
<span className="inline-flex items-center">
Role
<TipIcon className="ml-2">
A user or group&apos;s effective role for this project is the strongest role
on either the silo or project.
</TipIcon>
</span>
),
cell: (props) => (
<ListPlusCell tooltipTitle="Other roles">
{props.getValue().map(({ roleName, roleSource }) => (
<Badge key={roleSource} color={getBadgeColor(roleName)}>
{roleSource}.{roleName}
</Badge>
))}
</ListPlusCell>
),
}),

// TODO: tooltips on disabled elements explaining why
getActionsCol((row: UserRow) => [
{
Expand Down
12 changes: 2 additions & 10 deletions app/ui/lib/FieldLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -43,13 +41,7 @@ export const FieldLabel = ({
</span>
)}
</Component>
{tip && (
<Tooltip content={tip} placement="top">
<button className="svg:pointer-events-none" type="button">
<Question12Icon className="text-quinary" />
</button>
</Tooltip>
)}
{tip && <TipIcon>{tip}</TipIcon>}
</div>
)
}
29 changes: 29 additions & 0 deletions app/ui/lib/TipIcon.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Tooltip content={children} placement="top">
<button
className={cn('inline-flex svg:pointer-events-none', className)}
type="button"
>
<Question12Icon className="text-quinary" />
</button>
</Tooltip>
)
}
8 changes: 7 additions & 1 deletion app/util/access.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@
*/
import { expect, test } from 'vitest'

import { accessTypeLabel } from './access'
import { accessTypeLabel, getBadgeColor } from './access'

test('accessTypeLabel', () => {
expect(accessTypeLabel('silo_group')).toEqual('Group')
expect(accessTypeLabel('silo_user')).toEqual('User')
})

test('getBadgeColor', () => {
expect(getBadgeColor('admin')).toEqual('default')
expect(getBadgeColor('collaborator')).toEqual('purple')
expect(getBadgeColor('viewer')).toEqual('blue')
})
11 changes: 11 additions & 0 deletions app/util/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@
*
* Copyright Oxide Computer Company
*/

import type { IdentityType } from '~/api'
import type { BadgeColor } from '~/ui/lib/Badge'

export const accessTypeLabel = (identityType: IdentityType) =>
identityType === 'silo_group' ? 'Group' : 'User'

export const getBadgeColor = (role: 'admin' | 'collaborator' | 'viewer'): BadgeColor => {
const badgeColor = {
admin: 'default',
collaborator: 'purple',
viewer: 'blue',
}
return badgeColor[role] as BadgeColor
}
Loading