mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
Follow-up to #48344: collapses the two resolution paths for the Admonition module into one. `src/admonition.tsx` was a back-compat shim re-exporting `src/Admonition/`. Two ways to resolve one module is exactly what produced the macOS self-import bug fixed in #48344, and the local typecheck errors that #48374 worked around. This removes the shim and standardizes on the PascalCase subpath, matching every other export in the package. **Changed:** - Codemodded all 246 `ui-patterns/admonition` imports to `ui-patterns/Admonition` (240 `.tsx`, 5 `.mdx`, 1 `.ts` across studio, docs, www, design-system, and lite-studio) - Pointed the 5 internal `'../admonition'` imports back at the `'../Admonition'` directory **Removed:** - `packages/ui-patterns/src/admonition.tsx`, and its `./admonition` entry in the exports map (regenerated with `pnpm gen:exports`) ## To test - `grep -r "ui-patterns/admonition" --include='*.ts*'` → no hits - `pnpm test:case-hazards` → passes - `pnpm typecheck` → all 15 tasks green - `pnpm --filter studio run lint:ratchet` → passes - `pnpm --filter ui-patterns vitest run src/Admonition` → 11 tests pass <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Standardized Admonition component imports across the application and documentation. * Improved compatibility with case-sensitive environments by using the canonical component path. * Removed the legacy Admonition import entry point. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
289 lines
10 KiB
TypeScript
289 lines
10 KiB
TypeScript
import dayjs from 'dayjs'
|
|
import type { Control } from 'react-hook-form'
|
|
import {
|
|
Checkbox,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
WarningIcon,
|
|
} from 'ui'
|
|
import { Admonition } from 'ui-patterns/Admonition'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import { SingleValueFieldArray } from 'ui-patterns/form/SingleValueFieldArray/SingleValueFieldArray'
|
|
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
|
|
|
|
import type { JitRoleGrantDraft, JitRoleOption, JitUserRuleDraft } from './JitDbAccess.types'
|
|
import { createEmptyIpRange, getRelativeDatetimeByMode } from './JitDbAccess.utils'
|
|
import { DatePicker } from '@/components/ui/DatePicker'
|
|
import { InlineLink } from '@/components/ui/InlineLink'
|
|
import { DOCS_URL } from '@/lib/constants'
|
|
|
|
const EXPIRY_MODE_OPTIONS: Array<{ value: JitRoleGrantDraft['expiryMode']; label: string }> = [
|
|
{ value: '1h', label: '1 hour' },
|
|
{ value: '1d', label: '1 day' },
|
|
{ value: '7d', label: '7 days' },
|
|
{ value: '30d', label: '30 days' },
|
|
{ value: 'custom', label: 'Custom' },
|
|
{ value: 'never', label: 'Never' },
|
|
]
|
|
|
|
const MAX_CUSTOM_EXPIRY_YEARS = 1
|
|
const BRANCH_SCOPE_OPTIONS = [
|
|
{ value: 'all', label: 'All project databases' },
|
|
{ value: 'preview', label: 'Preview branches only' },
|
|
] as const
|
|
|
|
interface JitDbAccessRoleGrantFieldsProps {
|
|
control: Control<JitUserRuleDraft>
|
|
grantIndex: number
|
|
role: JitRoleOption
|
|
grant: JitRoleGrantDraft
|
|
onChange: (next: JitRoleGrantDraft) => void
|
|
}
|
|
|
|
export function JitDbAccessRoleGrantFields({
|
|
control,
|
|
grantIndex,
|
|
role,
|
|
grant,
|
|
onChange,
|
|
}: JitDbAccessRoleGrantFieldsProps) {
|
|
const isSuperuserRole = role.id === 'postgres'
|
|
const isReadOnlyRole = role.id === 'supabase_read_only_user'
|
|
const checkboxId = `jit-role-${role.id}`
|
|
|
|
return (
|
|
<div className={grant.enabled ? 'bg-surface-100' : 'bg-background'}>
|
|
<label
|
|
htmlFor={checkboxId}
|
|
className="grid w-full cursor-pointer select-none grid-cols-[16px_minmax(0,1fr)] items-start gap-x-3 px-4 py-3 transition-colors hover:bg-surface-200/40"
|
|
>
|
|
<Checkbox
|
|
id={checkboxId}
|
|
checked={grant.enabled}
|
|
onCheckedChange={(value) => {
|
|
const isEnabled = value === true
|
|
|
|
if (!isEnabled) {
|
|
return onChange({ ...grant, enabled: false })
|
|
}
|
|
|
|
if (
|
|
(grant.hasExpiry && grant.expiry) ||
|
|
(!grant.hasExpiry && grant.expiryMode === 'never')
|
|
) {
|
|
return onChange({ ...grant, enabled: true })
|
|
}
|
|
|
|
onChange({
|
|
...grant,
|
|
enabled: true,
|
|
hasExpiry: true,
|
|
expiryMode: '1h',
|
|
expiry: getRelativeDatetimeByMode('1h'),
|
|
})
|
|
}}
|
|
aria-label={`Enable ${role.label}`}
|
|
className="mt-0.5"
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<code className="text-code-inline dark:bg-surface-300! dark:border-control! tracking-normal!">
|
|
{role.label}
|
|
</code>
|
|
</div>
|
|
</label>
|
|
|
|
{grant.enabled && (
|
|
<div className="grid grid-cols-[16px_minmax(0,1fr)] gap-x-3 px-4 pb-3">
|
|
<div aria-hidden />
|
|
|
|
<div className="space-y-4">
|
|
{isSuperuserRole && (
|
|
<Admonition
|
|
type="warning"
|
|
layout="vertical"
|
|
className="mb-3"
|
|
title="The selected role has unrestricted access and bypasses row-level security"
|
|
description={
|
|
<>
|
|
Consider using a{' '}
|
|
<InlineLink href={`${DOCS_URL}/guides/database/postgres/roles`}>
|
|
custom Postgres role
|
|
</InlineLink>{' '}
|
|
with only the permissions required.
|
|
</>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{isReadOnlyRole && (
|
|
<Admonition
|
|
type="warning"
|
|
layout="vertical"
|
|
title="The selected role has read-only access to all schemas"
|
|
description={
|
|
<>
|
|
Consider using a{' '}
|
|
<InlineLink href={`${DOCS_URL}/guides/database/postgres/roles`}>
|
|
custom Postgres role
|
|
</InlineLink>{' '}
|
|
with only the permissions required.
|
|
</>
|
|
}
|
|
className="mb-3"
|
|
/>
|
|
)}
|
|
|
|
<FormItemLayout
|
|
isReactForm={false}
|
|
label="Applies to"
|
|
description={
|
|
<p className="text-xs text-foreground-lighter">
|
|
{grant.branchesOnly
|
|
? 'Can only be requested from preview branch databases.'
|
|
: 'Can be requested from production and preview branch databases.'}
|
|
</p>
|
|
}
|
|
>
|
|
<Select
|
|
value={grant.branchesOnly ? 'preview' : 'all'}
|
|
onValueChange={(value) => onChange({ ...grant, branchesOnly: value === 'preview' })}
|
|
>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="Select database scope" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{BRANCH_SCOPE_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormItemLayout>
|
|
|
|
<FormItemLayout
|
|
isReactForm={false}
|
|
label="Expires in"
|
|
description={
|
|
grant.hasExpiry && grant.expiry ? (
|
|
<p className="text-xs text-foreground-lighter">
|
|
Expires at{' '}
|
|
<TimestampInfo
|
|
utcTimestamp={grant.expiry}
|
|
className="text-foreground-lighter"
|
|
labelFormat="DD MMM, HH:mm"
|
|
/>
|
|
</p>
|
|
) : grant.expiryMode === 'never' ? (
|
|
<div className="mt-3 mx-0.5 flex w-full items-center gap-x-2">
|
|
<WarningIcon />
|
|
<span className="text-left text-xs text-foreground-lighter">
|
|
No expiry means ongoing database access until manually revoked.
|
|
</span>
|
|
</div>
|
|
) : undefined
|
|
}
|
|
>
|
|
<div className="flex gap-2">
|
|
<div className="flex-1">
|
|
<Select
|
|
value={grant.expiryMode}
|
|
onValueChange={(value) => {
|
|
const nextMode = value as JitRoleGrantDraft['expiryMode']
|
|
|
|
if (nextMode === 'never') {
|
|
return onChange({
|
|
...grant,
|
|
expiryMode: nextMode,
|
|
hasExpiry: false,
|
|
expiry: '',
|
|
})
|
|
}
|
|
|
|
if (nextMode === 'custom') {
|
|
return onChange({
|
|
...grant,
|
|
expiryMode: nextMode,
|
|
hasExpiry: true,
|
|
expiry: grant.expiry || getRelativeDatetimeByMode('1h'),
|
|
})
|
|
}
|
|
|
|
onChange({
|
|
...grant,
|
|
expiryMode: nextMode,
|
|
hasExpiry: true,
|
|
expiry: getRelativeDatetimeByMode(nextMode),
|
|
})
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Expires in" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{EXPIRY_MODE_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{grant.expiryMode === 'custom' && (
|
|
<DatePicker
|
|
selectsRange={false}
|
|
triggerButtonSize="small"
|
|
contentSide="top"
|
|
to={grant.expiry || undefined}
|
|
minDate={new Date()}
|
|
maxDate={dayjs().add(MAX_CUSTOM_EXPIRY_YEARS, 'year').toDate()}
|
|
onChange={(value) => {
|
|
const selectedDate = value.to || value.from || ''
|
|
onChange({
|
|
...grant,
|
|
hasExpiry: true,
|
|
expiry: selectedDate,
|
|
})
|
|
}}
|
|
triggerButtonClassName="min-w-[120px]"
|
|
>
|
|
{grant.expiry ? dayjs(grant.expiry).format('DD MMM, HH:mm') : 'Select date'}
|
|
</DatePicker>
|
|
)}
|
|
</div>
|
|
</FormItemLayout>
|
|
|
|
<FormItemLayout
|
|
isReactForm={false}
|
|
label={
|
|
<p className="text-sm text-foreground">
|
|
Restricted IP addresses{' '}
|
|
<span className="font-normal text-foreground-lighter">(optional)</span>
|
|
</p>
|
|
}
|
|
>
|
|
<SingleValueFieldArray
|
|
control={control}
|
|
name={`grants.${grantIndex}.ipRanges` as const}
|
|
valueFieldName="value"
|
|
createEmptyRow={createEmptyIpRange}
|
|
placeholder="192.168.0.0/24"
|
|
addLabel="Add IP restriction"
|
|
removeLabel="Remove IP restriction"
|
|
minimumRows={1}
|
|
inputAutoComplete="off"
|
|
rowsClassName="space-y-2"
|
|
addButtonClassName="w-min"
|
|
/>
|
|
</FormItemLayout>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|