Remove unused code for scoped tokens

This commit is contained in:
Gildas Garcia committed 2026-07-27 16:27:51 +02:00
1 parent bed5eac392
commit da89d64e33
10 files changed
-845

No files matched your search

@@ -1,140 +0,0 @@
import dayjs from 'dayjs'
import { useEffect, useState } from 'react'
import { Control, ControllerRenderProps } from 'react-hook-form'
import {
FormControl,
FormField,
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
WarningIcon,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
CUSTOM_EXPIRY_VALUE,
EXPIRES_AT_OPTIONS,
NON_EXPIRING_TOKEN_VALUE,
} from '../../AccessToken.constants'
import { type TokenFormValues } from '../../AccessToken.schemas'
import { DatePicker } from '@/components/ui/DatePicker'
interface BasicInfoProps {
control: Control<TokenFormValues>
expirationDate: string
onCustomDateChange?: (date: { date: string } | undefined) => void
onCustomExpiryChange?: (isCustom: boolean) => void
}
export const BasicInfo = ({
control,
expirationDate,
onCustomDateChange,
onCustomExpiryChange,
}: BasicInfoProps) => {
const [customDate, setCustomDate] = useState<Date>()
const [isCustomSelected, setIsCustomSelected] = useState(false)
useEffect(() => {
const isCustom = expirationDate === CUSTOM_EXPIRY_VALUE
setIsCustomSelected(isCustom)
onCustomExpiryChange?.(isCustom)
}, [expirationDate, onCustomExpiryChange])
const handleCustomDateChange = (date: Date | undefined) => {
setCustomDate(date)
if (date) {
onCustomDateChange?.({ date: date.toISOString() })
} else {
onCustomDateChange?.(undefined)
}
}
const handleExpiryChange = (
value: string,
field: ControllerRenderProps<TokenFormValues, 'expiresAt'>
) => {
const isCustom = value === CUSTOM_EXPIRY_VALUE
setIsCustomSelected(isCustom)
onCustomExpiryChange?.(isCustom)
field.onChange(value)
}
return (
<div className="space-y-4 px-5 sm:px-6 py-6">
<FormField
key="tokenName"
name="tokenName"
control={control}
render={({ field }) => (
<FormItemLayout name="tokenName" label="Name">
<FormControl>
<Input id="tokenName" {...field} placeholder="Provide a name for your token" />
</FormControl>
</FormItemLayout>
)}
/>
<FormField
key="expiresAt"
name="expiresAt"
control={control}
render={({ field }) => (
<FormItemLayout name="expiresAt" label="Expires in">
<div className="flex gap-2">
<FormControl className="grow">
<Select
value={field.value}
onValueChange={(value) => handleExpiryChange(value, field)}
>
<SelectTrigger>
<SelectValue placeholder="Expires at" />
</SelectTrigger>
<SelectContent>
{Object.values(EXPIRES_AT_OPTIONS).map(
(option: { value: string; label: string }) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
)
)}
</SelectContent>
</Select>
</FormControl>
{isCustomSelected && (
<DatePicker
selectsRange={false}
triggerButtonSize="small"
contentSide="top"
minDate={new Date()}
maxDate={dayjs().add(1, 'year').toDate()}
onChange={(date) => {
const selectedDate = date.to || date.from
if (selectedDate) {
handleCustomDateChange(new Date(selectedDate))
} else {
handleCustomDateChange(undefined)
}
}}
>
{customDate ? `${dayjs(customDate).format('DD MMM, HH:mm')}` : 'Select date'}
</DatePicker>
)}
</div>
{field.value === NON_EXPIRING_TOKEN_VALUE && (
<div className="w-full flex gap-x-2 items-center mt-3 mx-0.5">
<WarningIcon />
<span className="text-xs text-left text-foreground-lighter">
Make sure to keep your non-expiring token safe and secure.
</span>
</div>
)}
</FormItemLayout>
)}
/>
</div>
)
}
@@ -1,90 +0,0 @@
import { Key, Plus } from 'lucide-react'
import { Path, PathValue } from 'react-hook-form'
import {
Button,
Checkbox,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Popover,
PopoverContent,
PopoverTrigger,
} from 'ui'
import {
PermissionResource,
PermissionResourceSelectorProps,
PermissionRow,
PermissionsFormValues,
} from './Permissions.types'
import { togglePermissionResource } from './Permissions.utils'
import { ACCESS_TOKEN_RESOURCES } from '@/components/interfaces/Account/AccessTokens/AccessToken.constants'
export const PermissionResourceSelector = <TFormValues extends PermissionsFormValues>({
open,
onOpenChange,
permissionRows,
setValue,
align = 'center',
}: PermissionResourceSelectorProps<TFormValues>) => {
const handleToggleResource = (resource: PermissionResource) => {
const newRows = togglePermissionResource(permissionRows, resource)
setValue(
'permissionRows' as Path<TFormValues>,
newRows as PathValue<TFormValues, Path<TFormValues>>
)
}
return (
<Popover open={open} onOpenChange={onOpenChange} modal={true}>
<PopoverTrigger asChild>
<Button variant="default" size="tiny" icon={<Plus className="h-4 w-4" />}>
Add permission
</Button>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0" align={align}>
<Command>
<CommandInput placeholder="Search resources..." />
<CommandList>
<CommandEmpty>No resources found.</CommandEmpty>
<CommandGroup className="[&>div]:text-left">
<div className="max-h-[210px] overflow-y-auto">
{ACCESS_TOKEN_RESOURCES.map((resource) => {
const isChecked = permissionRows.some(
(row: PermissionRow) => row.resource === resource.resource
)
return (
<CommandItem
key={resource.resource}
value={`${resource.resource} ${resource.title}`}
onSelect={() => handleToggleResource(resource)}
className="text-foreground"
>
<div className="flex items-center gap-3 w-full">
<Checkbox
checked={isChecked}
onCheckedChange={() => handleToggleResource(resource)}
onClick={(e) => e.stopPropagation()}
/>
<Key size={12} className="text-foreground-lighter" />
<div className="flex flex-col text-left flex-1">
<span className="font-medium text-foreground capitalize">
{resource.title}
</span>
</div>
</div>
</CommandItem>
)
})}
</div>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
@@ -1,165 +0,0 @@
import { ChevronDown, RotateCcw, X } from 'lucide-react'
import { Path, PathValue } from 'react-hook-form'
import { Button, Checkbox, Popover, PopoverContent, PopoverTrigger, WarningIcon } from 'ui'
import { PermissionResourceSelector } from './PermissionResourceSelector'
import { PermissionRow, PermissionsFormValues, PermissionsProps } from './Permissions.types'
import { sortActions } from './Permissions.utils'
import { ACCESS_TOKEN_RESOURCES } from '@/components/interfaces/Account/AccessTokens/AccessToken.constants'
import { formatAccessText } from '@/components/interfaces/Account/AccessTokens/AccessToken.utils'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
export const Permissions = <TFormValues extends PermissionsFormValues = PermissionsFormValues>({
setValue,
watch,
resourceSearchOpen,
setResourceSearchOpen,
}: PermissionsProps<TFormValues>) => {
const permissionRows = (watch('permissionRows' as Path<TFormValues>) || []) as PermissionRow[]
return (
<div className="space-y-4 px-5 sm:px-6 py-6">
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm">Configure permissions</span>
<div className="flex items-center gap-2">
{permissionRows.length > 0 && (
<ButtonTooltip
variant="default"
size="tiny"
className="p-1"
onClick={() => {
setValue(
'permissionRows' as Path<TFormValues>,
[] as PathValue<TFormValues, Path<TFormValues>>
)
}}
icon={<RotateCcw size={16} />}
tooltip={{
content: {
side: 'top',
align: 'center',
alignOffset: -10,
text: 'Reset all permissions',
},
}}
/>
)}
<PermissionResourceSelector
open={resourceSearchOpen}
onOpenChange={setResourceSearchOpen}
permissionRows={permissionRows}
setValue={setValue}
align="end"
/>
</div>
</div>
{permissionRows.length === 0 ? (
<div className="text-center py-8 border border-dashed border-border rounded-lg">
<p className="text-sm text-foreground-light">No permissions configured yet.</p>
</div>
) : (
<div className="border border-border rounded-lg">
{permissionRows.map((row, index) => {
const selectedResource = ACCESS_TOKEN_RESOURCES.find(
(r) => r.resource === row.resource
)
return (
<div key={row.resource ?? `resource-${index}`}>
<div className="flex items-center gap-3 p-3">
<div className="flex-1">
<div className="flex items-center gap-2">
<div className="flex flex-col">
<span className="text-sm font-medium truncate max-w-[36ch] capitalize">
{selectedResource?.title}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
{selectedResource && (
<Popover>
<PopoverTrigger asChild>
<Button
variant="default"
size="tiny"
className="w-[150px] flex text-sm justify-between h-7 "
iconRight={
<ChevronDown size={14} className="text-foreground-muted" />
}
>
{row.actions.length === 0 ? (
<span className="text-foreground-lighter">Select access</span>
) : row.actions.length === 1 ? (
formatAccessText(row.actions[0])
) : (
`${row.actions.length} selected`
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[180px] p-2" align="end">
<div className="space-y-2">
{sortActions(selectedResource.actions).map((action) => (
<label
key={action}
className="flex items-center gap-2 cursor-pointer"
>
<Checkbox
checked={row.actions.includes(action)}
onCheckedChange={(checked) => {
const newActions = checked
? [...row.actions, action]
: row.actions.filter((a) => a !== action)
const newRows: PermissionRow[] = permissionRows.map((r, i) =>
i === index
? { resource: r.resource, actions: newActions }
: r
)
setValue(
'permissionRows' as Path<TFormValues>,
newRows as PathValue<TFormValues, Path<TFormValues>>,
{ shouldValidate: true, shouldDirty: true }
)
}}
/>
<span className="text-sm">{formatAccessText(action)}</span>
</label>
))}
</div>
</PopoverContent>
</Popover>
)}
<Button
variant="text"
size="tiny"
className="p-1"
onClick={() => {
const newRows = permissionRows.filter((_, i) => i !== index)
setValue(
'permissionRows' as Path<TFormValues>,
newRows as PathValue<TFormValues, Path<TFormValues>>,
{ shouldValidate: true, shouldDirty: true }
)
}}
icon={<X size={16} />}
/>
</div>
</div>
{index < permissionRows.length - 1 && <div className="border-t border-border" />}
</div>
)
})}
</div>
)}
</div>
<div className="w-full flex gap-x-2 items-center">
<WarningIcon />
<span className="text-xs text-left text-foreground-lighter">
Once you've set these permissions, you cannot edit them.
</span>
</div>
</div>
)
}
@@ -1,33 +0,0 @@
import { FieldValues, UseFormSetValue, UseFormWatch } from 'react-hook-form'
export interface PermissionResource {
resource: string
title: string
actions: string[]
}
export interface PermissionRow {
resource: string
actions: string[]
}
export interface PermissionsFormValues extends FieldValues {
permissionRows?: PermissionRow[]
}
export interface PermissionsProps<
TFormValues extends PermissionsFormValues = PermissionsFormValues,
> {
setValue: UseFormSetValue<TFormValues>
watch: UseFormWatch<TFormValues>
resourceSearchOpen: boolean
setResourceSearchOpen: (open: boolean) => void
}
export interface PermissionResourceSelectorProps<TFormValues extends PermissionsFormValues> {
open: boolean
onOpenChange: (open: boolean) => void
permissionRows: PermissionRow[]
setValue: UseFormSetValue<TFormValues>
align?: 'center' | 'end' | 'start'
}
@@ -1,128 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { PermissionResource, PermissionRow } from './Permissions.types'
import { sortActions, togglePermissionResource } from './Permissions.utils'
// --- sortActions ---
describe('sortActions', () => {
it('should sort actions by priority order: read, write, create, delete', () => {
expect(sortActions(['delete', 'create', 'write', 'read'])).toEqual([
'read',
'write',
'create',
'delete',
])
})
it('should return empty array for empty input', () => {
expect(sortActions([])).toEqual([])
})
it('should keep a single action as-is', () => {
expect(sortActions(['write'])).toEqual(['write'])
})
it('should place unknown actions after the priority ones', () => {
expect(sortActions(['custom-action', 'read', 'delete'])).toEqual([
'read',
'delete',
'custom-action',
])
})
it('should preserve relative order of multiple unknown actions', () => {
expect(sortActions(['zeta', 'alpha', 'read'])).toEqual(['read', 'zeta', 'alpha'])
})
it('should handle only unknown actions', () => {
expect(sortActions(['foo', 'bar'])).toEqual(['foo', 'bar'])
})
it('should handle a subset of priority actions', () => {
expect(sortActions(['delete', 'read'])).toEqual(['read', 'delete'])
})
it('should not mutate the original array', () => {
const original = ['delete', 'read']
sortActions(original)
expect(original).toEqual(['delete', 'read'])
})
})
// --- togglePermissionResource ---
describe('togglePermissionResource', () => {
const billingResource: PermissionResource = {
resource: 'organization:billing',
title: 'Billing',
actions: ['read', 'write'],
}
const membersResource: PermissionResource = {
resource: 'organization:members',
title: 'Members',
actions: ['read', 'write', 'create', 'delete'],
}
const storageResource: PermissionResource = {
resource: 'project:storage',
title: 'Storage',
actions: ['write', 'create'],
}
it('should add a resource with "read" as the default action when available', () => {
const result = togglePermissionResource([], billingResource)
expect(result).toEqual([{ resource: 'organization:billing', actions: ['read'] }])
})
it('should add a resource with the first action as default when "read" is not available', () => {
const result = togglePermissionResource([], storageResource)
expect(result).toEqual([{ resource: 'project:storage', actions: ['write'] }])
})
it('should remove a resource if it is already in the list', () => {
const existing: PermissionRow[] = [
{ resource: 'organization:billing', actions: ['read'] },
{ resource: 'organization:members', actions: ['read', 'write'] },
]
const result = togglePermissionResource(existing, billingResource)
expect(result).toEqual([{ resource: 'organization:members', actions: ['read', 'write'] }])
})
it('should not modify other rows when removing a resource', () => {
const existing: PermissionRow[] = [
{ resource: 'organization:billing', actions: ['read'] },
{ resource: 'organization:members', actions: ['read', 'write'] },
]
const result = togglePermissionResource(existing, billingResource)
expect(result).toHaveLength(1)
expect(result[0]).toEqual({ resource: 'organization:members', actions: ['read', 'write'] })
})
it('should append to existing rows when adding', () => {
const existing: PermissionRow[] = [{ resource: 'organization:billing', actions: ['read'] }]
const result = togglePermissionResource(existing, membersResource)
expect(result).toHaveLength(2)
expect(result[1]).toEqual({ resource: 'organization:members', actions: ['read'] })
})
it('should not mutate the original array', () => {
const existing: PermissionRow[] = [{ resource: 'organization:billing', actions: ['read'] }]
const original = [...existing]
togglePermissionResource(existing, membersResource)
expect(existing).toEqual(original)
})
it('should toggle off then on again correctly', () => {
const step1 = togglePermissionResource([], billingResource)
expect(step1).toHaveLength(1)
const step2 = togglePermissionResource(step1, billingResource)
expect(step2).toHaveLength(0)
const step3 = togglePermissionResource(step2, billingResource)
expect(step3).toHaveLength(1)
expect(step3[0]).toEqual({ resource: 'organization:billing', actions: ['read'] })
})
})
@@ -1,32 +0,0 @@
import { PermissionResource, PermissionRow } from './Permissions.types'
export const sortActions = (actions: string[]): string[] => {
const sorted: string[] = []
const remaining = [...actions]
const priority = ['read', 'write', 'create', 'delete']
for (const action of priority) {
const index = remaining.indexOf(action)
if (index !== -1) {
sorted.push(action)
remaining.splice(index, 1)
}
}
sorted.push(...remaining)
return sorted
}
export const togglePermissionResource = (
permissionRows: PermissionRow[],
resource: PermissionResource
): PermissionRow[] => {
const isAlreadyAdded = permissionRows.some((row) => row.resource === resource.resource)
if (isAlreadyAdded) {
return permissionRows.filter((row) => row.resource !== resource.resource)
}
const defaultActions = resource.actions.includes('read') ? ['read'] : [resource.actions[0]]
return [...permissionRows, { resource: resource.resource, actions: defaultActions }]
}
@@ -1,116 +0,0 @@
import { FormControl, FormField } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { ResourceAccessProps } from './ResourceAccess.types'
import { ResourceMultiSelector } from './ResourceMultiSelector'
import { ResourceOption } from './ResourceOption'
import { useOrgAndProjectData } from '@/components/interfaces/Account/AccessTokens/hooks/useOrgAndProjectData'
import { useProfile } from '@/lib/profile'
const RESOURCE_OPTIONS = [
{
value: 'all-orgs' as const,
label: 'Everything',
description: 'Access to all projects across all organizations you have access to.',
},
{
value: 'selected-orgs' as const,
label: 'Selected orgs',
description: 'Access only to the organizations you have specified.',
},
{
value: 'selected-projects' as const,
label: 'Selected projects',
description: 'Access only to the projects you have specified.',
},
]
export const ResourceAccess = ({ control, resourceAccess, setValue }: ResourceAccessProps) => {
const { profile } = useProfile()
const { organizations, projects, isLoadingOrgs, isLoadingProjects } = useOrgAndProjectData({
enabled: !!profile,
})
const handleResourceAccessChange = (newValue: string, currentValue: string) => {
if (newValue !== currentValue) {
setValue('selectedOrganizations', [])
setValue('selectedProjects', [])
}
}
return (
<div className="space-y-4 px-5 sm:px-6 py-6">
<FormField
key="resourceAccess"
name="resourceAccess"
control={control}
render={({ field }) => (
<FormItemLayout name="resourceAccess" label="Resource access">
<FormControl>
<div className="space-y-3">
<fieldset className="flex gap-3">
<legend className="sr-only">Resource access level</legend>
{RESOURCE_OPTIONS.map((option) => (
<ResourceOption
key={option.value}
value={option.value}
label={option.label}
isSelected={field.value === option.value}
onChange={() => {
handleResourceAccessChange(option.value, field.value)
field.onChange(option.value)
}}
/>
))}
</fieldset>
<p className="text-foreground-light text-sm">
{RESOURCE_OPTIONS.find((opt) => opt.value === field.value)?.description}
</p>
</div>
</FormControl>
</FormItemLayout>
)}
/>
{resourceAccess === 'selected-orgs' && (
<FormField
key="selectedOrganizations"
name="selectedOrganizations"
control={control}
render={({ field }) => (
<ResourceMultiSelector
field={field}
items={organizations.map((org) => ({ id: org.slug, name: org.name }))}
isLoading={isLoadingOrgs}
fieldName="selectedOrganizations"
label="Select organizations"
loadingMessage="Loading organizations..."
emptyMessage="No organizations available"
/>
)}
/>
)}
{resourceAccess === 'selected-projects' && (
<FormField
key="selectedProjects"
name="selectedProjects"
control={control}
render={({ field }) => (
<ResourceMultiSelector
field={field}
items={projects.map((project) => ({ id: project.ref, name: project.name }))}
isLoading={isLoadingProjects}
fieldName="selectedProjects"
label="Select projects"
loadingMessage="Loading projects..."
emptyMessage="No projects available"
/>
)}
/>
)}
</div>
)
}
@@ -1,31 +0,0 @@
import { Control, ControllerRenderProps, UseFormSetValue } from 'react-hook-form'
import { TokenFormValues } from '@/components/interfaces/Account/AccessTokens/AccessToken.schemas'
export interface ResourceAccessProps {
control: Control<TokenFormValues>
resourceAccess: string
setValue: UseFormSetValue<TokenFormValues>
}
export interface ResourceItem {
id: string
name: string
}
export interface ResourceMultiSelectorProps {
field: ControllerRenderProps<any, any>
items: ResourceItem[]
isLoading: boolean
fieldName: string
label: string
loadingMessage: string
emptyMessage: string
}
export interface ResourceOptionProps {
value: string
label: string
isSelected: boolean
onChange: () => void
}
@@ -1,69 +0,0 @@
import { useMemo } from 'react'
import { FormControl } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
import { ResourceMultiSelectorProps } from './ResourceAccess.types'
export const ResourceMultiSelector = ({
field,
items,
isLoading,
fieldName,
label,
loadingMessage,
emptyMessage,
}: ResourceMultiSelectorProps) => {
const idToName = useMemo(() => new Map(items.map((item) => [item.id, item.name])), [items])
const displayValues = (field.value || []).map((id: string) => idToName.get(id) || id)
const handleValuesChange = (names: string[]) => {
const ids = names
.map((name) => {
for (const [id, itemName] of idToName.entries()) {
if (itemName === name) return id
}
return name
})
.filter(Boolean)
field.onChange(ids)
}
return (
<FormItemLayout name={fieldName} label={label}>
<FormControl className="overflow-visible">
<MultiSelector values={displayValues} onValuesChange={handleValuesChange}>
<MultiSelectorTrigger
deletableBadge
showIcon={false}
mode="inline-combobox"
label={label}
badgeLimit="wrap"
/>
<MultiSelectorContent className="z-50">
{isLoading ? (
<div className="px-3 py-2 text-sm text-foreground-light">{loadingMessage}</div>
) : items.length === 0 ? (
<div className="px-3 py-2 text-sm text-foreground-light">{emptyMessage}</div>
) : (
<MultiSelectorList>
{items.map((item) => (
<MultiSelectorItem key={item.id} value={item.name}>
{item.name}
</MultiSelectorItem>
))}
</MultiSelectorList>
)}
</MultiSelectorContent>
</MultiSelector>
</FormControl>
</FormItemLayout>
)
}
@@ -1,41 +0,0 @@
import { Box, Check } from 'lucide-react'
import { cn } from 'ui'
export const ResourceOption = ({
value,
label,
isSelected,
onChange,
}: {
value: string
label: string
isSelected: boolean
onChange: () => void
}) => (
<label
className={cn(
'border border-default rounded-md bg-surface-200 hover:bg-overlay-hover hover:border-control px-4 py-3 cursor-pointer transition-colors flex-1 flex flex-col',
isSelected && 'border-foreground-muted hover:border-foreground-muted bg-surface-300'
)}
>
<div className="flex justify-between items-start mb-3">
<Box size={16} className="text-foreground-light" />
{isSelected && (
<div className="flex items-center justify-center p-0.5 bg-foreground text-background rounded-full">
<Check size={12} strokeWidth="4" className="text-background" />
</div>
)}
</div>
<span className={cn('text-sm', isSelected ? 'text-foreground' : 'text-foreground-light')}>
{label}
</span>
<input
type="radio"
name="resourceAccess"
value={value}
checked={isSelected}
onChange={onChange}
className="sr-only"
/>
</label>
)