mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Improving accessibility by adding aria-label. Adding Tooltip for icon only buttons. ## What is the current behavior? Some `aria-label` and `Tooltip` are missing. ## What is the new behavior? Icon-only buttons have now Tooltip and aria-label have been added. ## Additional context No visual changes have been made. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Accessibility Improvements** * Added descriptive tooltips and screen-reader labels to search-clear and close-panel controls. * Added accessible labels and tooltips for managing secrets, showing or hiding secret values, and editing or removing foreign tables. * Improved the lint details panel close control with a descriptive label. * Prevented duplicate tooltip announcements for assistive technologies. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
186 lines
6.2 KiB
TypeScript
186 lines
6.2 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import dayjs from 'dayjs'
|
|
import { Edit3, Eye, EyeOff, Key, Loader, MoreVertical, Trash } from 'lucide-react'
|
|
import { parseAsString, useQueryState } from 'nuqs'
|
|
import { useState } from 'react'
|
|
import {
|
|
Button,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
import { Input } from 'ui-patterns/DataInputs/Input'
|
|
|
|
import { SecretTableColumn } from './Secrets.types'
|
|
import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
|
|
import { useVaultSecretDecryptedValueQuery } from '@/data/vault/vault-secret-decrypted-value-query'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import type { VaultSecret } from '@/types'
|
|
|
|
interface SecretRowProps {
|
|
row: VaultSecret
|
|
col: SecretTableColumn
|
|
}
|
|
|
|
export const SecretRow = ({ row, col }: SecretRowProps) => {
|
|
const { ref } = useParams()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const [revealSecret, setRevealSecret] = useState(false)
|
|
const name = row?.name ?? 'No name provided'
|
|
|
|
const [, setSelectedSecretToEdit] = useQueryState('edit', parseAsString)
|
|
const [, setSelectedSecretToDelete] = useQueryState('delete', parseAsString)
|
|
|
|
const { can: canManageSecrets } = useAsyncCheckPermissions(
|
|
PermissionAction.TENANT_SQL_ADMIN_WRITE,
|
|
'tables'
|
|
)
|
|
|
|
const { data: revealedValue, isFetching } = useVaultSecretDecryptedValueQuery(
|
|
{
|
|
projectRef: ref!,
|
|
connectionString: project?.connectionString,
|
|
id: row.id,
|
|
},
|
|
{
|
|
enabled: !!(ref! && row.id) && revealSecret,
|
|
}
|
|
)
|
|
|
|
if (col.id === 'actions') {
|
|
return (
|
|
<div className="flex items-center justify-end w-full" onClick={(e) => e.stopPropagation()}>
|
|
<DropdownMenu>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
title="Manage Secret"
|
|
variant="text"
|
|
className="px-1"
|
|
icon={<MoreVertical />}
|
|
aria-label="Manage secret"
|
|
// Tooltip repeats the label; screen readers would read it twice
|
|
aria-describedby={undefined}
|
|
/>
|
|
</DropdownMenuTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">Manage secret</TooltipContent>
|
|
</Tooltip>
|
|
<DropdownMenuContent side="bottom" align="end" className="w-40">
|
|
<DropdownMenuItemTooltip
|
|
className="gap-x-2"
|
|
disabled={!canManageSecrets}
|
|
onClick={() => setSelectedSecretToEdit(row.id)}
|
|
tooltip={{
|
|
content: { side: 'left', text: 'You need additional permissions to edit secrets' },
|
|
}}
|
|
>
|
|
<Edit3 size={12} />
|
|
<p>Edit</p>
|
|
</DropdownMenuItemTooltip>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuItemTooltip
|
|
className="gap-x-2"
|
|
disabled={!canManageSecrets}
|
|
onClick={() => setSelectedSecretToDelete(row.id)}
|
|
tooltip={{
|
|
content: {
|
|
side: 'left',
|
|
text: 'You need additional permissions to delete secrets',
|
|
},
|
|
}}
|
|
>
|
|
<Trash size={12} />
|
|
<p className="text-foreground-light">Delete</p>
|
|
</DropdownMenuItemTooltip>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (col.id === 'secret_value') {
|
|
return (
|
|
<div className="flex items-center gap-2 w-full" onClick={(e) => e.stopPropagation()}>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="text"
|
|
className="px-1.5"
|
|
icon={
|
|
isFetching && revealedValue === undefined ? (
|
|
<Loader className="animate-spin" size={16} strokeWidth={1.5} />
|
|
) : !revealSecret ? (
|
|
<Eye size={16} strokeWidth={1.5} />
|
|
) : (
|
|
<EyeOff size={16} strokeWidth={1.5} />
|
|
)
|
|
}
|
|
onClick={() => setRevealSecret(!revealSecret)}
|
|
aria-label={revealSecret ? 'Hide secret value' : 'Show secret value'}
|
|
// Tooltip repeats the label; screen readers would read it twice
|
|
aria-describedby={undefined}
|
|
/>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
{revealSecret ? 'Hide secret value' : 'Show secret value'}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<div className="grow min-w-0">
|
|
{revealSecret && revealedValue !== undefined ? (
|
|
<Input copy readOnly size="tiny" className="font-mono" value={revealedValue} />
|
|
) : (
|
|
<p className="text-sm font-mono text-foreground">••••••••••••••••••</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (col.id === 'updated_at') {
|
|
return (
|
|
<div className="w-full flex items-center justify-start">
|
|
<p className="text-xs text-foreground-light">
|
|
{row.updated_at === row.created_at ? 'Added' : 'Updated'} on{' '}
|
|
{dayjs(row.updated_at).format('MMM D, YYYY')}
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (col.id === 'id') {
|
|
return (
|
|
<div className="w-full flex items-center">
|
|
<Key size={12} strokeWidth={2} className="text-foreground-light mr-2" />
|
|
<p className="text-foreground-light text-xs font-mono truncate" title={row.id}>
|
|
{row.id}
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="w-full flex flex-col justify-center">
|
|
<p className="text-xs text-foreground truncate select-text" title={name}>
|
|
{name}
|
|
</p>
|
|
{row.description !== undefined && row.description !== '' && (
|
|
<div>
|
|
<p className="text-xs text-foreground-lighter w-full truncate select-text">
|
|
{row.description}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|