mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 12:25:05 +03:00
## Problem We still use our old `Input` in some places. This means multiple components are bundled for the same use cases and we have some design differences across the application. ## Solution - [x] Migrate to the new ShadCN inputs - [x] Fix `<InputGroupButton>` cannot be used with components that triggers modal, popovers, dropdowns - [x] Fix `<FormLayout>` does not display errors for inputs that are not inside a React Hook Form - [x] Fix `InputGroup` invalid design ## Screenshots ### Table Editor - table edition sidepanel Before: <img width="758" height="1206" alt="image" src="https://github.com/user-attachments/assets/d4da4af0-a9d3-4967-935f-554233d7896b" /> After: <img width="747" height="1209" alt="image" src="https://github.com/user-attachments/assets/6286e6a0-317f-486c-a8b4-0e233706ba0f" /> ### Table Editor - row edition sidepanel Before: <img width="675" height="710" alt="image" src="https://github.com/user-attachments/assets/9fdfe819-6d62-40c8-bdc8-fa6051dab834" /> After: (I placed the TextArea button at the bottom because ShadCN reserves a full line space for it. It was weird at the top. <img width="674" height="714" alt="image" src="https://github.com/user-attachments/assets/611d5f8d-de12-4c16-ac38-bd9192cd6d73" /> ### Database settings - password reset modal Before: <img width="773" height="548" alt="image" src="https://github.com/user-attachments/assets/17f679a7-3aed-4cf9-8245-194a8a16823f" /> After: <img width="563" height="311" alt="image" src="https://github.com/user-attachments/assets/08888471-4cc8-4a3c-bf1e-8dce364f1aa6" /> ### Database - Event triggers Before: <img width="1134" height="453" alt="image" src="https://github.com/user-attachments/assets/e9d06d58-782c-4ccb-93c0-2ce1ca8c5748" /> After: <img width="1115" height="451" alt="image" src="https://github.com/user-attachments/assets/c437acb0-c602-4dd2-b249-66c7a7e739d6" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Copy action now shows confirmation state (“Copy” → “Copied”) when copying error details. * **UI Improvements** * Unified form-field layouts and input-group composition across editors, settings, and integration forms for a more consistent experience. * Password-strength feedback moved into field layout for clearer messaging. * Improved inline input/button/dropdown behaviors and non-React-form error display. * **Removed** * Display configuration settings component. [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45791) <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Ali Waseem <waseema393@gmail.com>
195 lines
6.5 KiB
TypeScript
195 lines
6.5 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||
import { useParams } from 'common'
|
||
import { useEffect, useState } from 'react'
|
||
import { toast } from 'sonner'
|
||
import { Button, Card, CardContent, Modal } from 'ui'
|
||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||
import {
|
||
PageSection,
|
||
PageSectionContent,
|
||
PageSectionDescription,
|
||
PageSectionMeta,
|
||
PageSectionSummary,
|
||
PageSectionTitle,
|
||
} from 'ui-patterns/PageSection'
|
||
|
||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||
import { PasswordStrengthBar } from '@/components/ui/PasswordStrengthBar'
|
||
import { useDatabasePasswordResetMutation } from '@/data/database/database-password-reset-mutation'
|
||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||
import { useIsProjectActive, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||
import { DEFAULT_MINIMUM_PASSWORD_STRENGTH } from '@/lib/constants'
|
||
import { passwordStrength, PasswordStrengthScore } from '@/lib/password-strength'
|
||
import { generateStrongPassword } from '@/lib/project'
|
||
|
||
const ResetDbPassword = ({ disabled = false }) => {
|
||
const { ref } = useParams()
|
||
const isProjectActive = useIsProjectActive()
|
||
const { data: project } = useSelectedProjectQuery()
|
||
|
||
const { can: canResetDbPassword } = useAsyncCheckPermissions(
|
||
PermissionAction.UPDATE,
|
||
'projects',
|
||
{
|
||
resource: {
|
||
project_id: project?.id,
|
||
},
|
||
}
|
||
)
|
||
|
||
const [showResetDbPass, setShowResetDbPass] = useState<boolean>(false)
|
||
|
||
const [password, setPassword] = useState<string>('')
|
||
const [passwordStrengthMessage, setPasswordStrengthMessage] = useState<string>('')
|
||
const [passwordStrengthWarning, setPasswordStrengthWarning] = useState<string>('')
|
||
const [passwordStrengthScore, setPasswordStrengthScore] = useState(0)
|
||
|
||
const { mutate: resetDatabasePassword, isPending: isUpdatingPassword } =
|
||
useDatabasePasswordResetMutation({
|
||
onSuccess: async () => {
|
||
toast.success('Successfully updated database password')
|
||
setShowResetDbPass(false)
|
||
},
|
||
})
|
||
|
||
useEffect(() => {
|
||
if (showResetDbPass) {
|
||
setPassword('')
|
||
setPasswordStrengthMessage('')
|
||
setPasswordStrengthWarning('')
|
||
setPasswordStrengthScore(0)
|
||
}
|
||
}, [showResetDbPass])
|
||
|
||
async function checkPasswordStrength(value: any) {
|
||
const { message, warning, strength } = await passwordStrength(value)
|
||
setPasswordStrengthScore(strength)
|
||
setPasswordStrengthWarning(warning)
|
||
setPasswordStrengthMessage(message)
|
||
}
|
||
|
||
const onDbPassChange = (e: any) => {
|
||
const value = e.target.value
|
||
setPassword(value)
|
||
if (value == '') {
|
||
setPasswordStrengthScore(-1)
|
||
setPasswordStrengthMessage('')
|
||
} else checkPasswordStrength(value)
|
||
}
|
||
|
||
const confirmResetDbPass = async () => {
|
||
if (!ref) return console.error('Project ref is required')
|
||
|
||
if (passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
|
||
resetDatabasePassword({ ref, password })
|
||
}
|
||
}
|
||
|
||
function generatePassword() {
|
||
const password = generateStrongPassword()
|
||
setPassword(password)
|
||
checkPasswordStrength(password)
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<PageSection id="database-password">
|
||
<PageSectionMeta>
|
||
<PageSectionSummary>
|
||
<PageSectionTitle>Database password</PageSectionTitle>
|
||
|
||
<PageSectionDescription>Used for direct Postgres connections</PageSectionDescription>
|
||
</PageSectionSummary>
|
||
</PageSectionMeta>
|
||
<PageSectionContent>
|
||
<Card>
|
||
<CardContent className="flex flex-row items-center gap-x-2 justify-between">
|
||
<div className="space-y-0.5">
|
||
<h3 className="text-sm text-foreground">Reset database password</h3>
|
||
<p className="text-sm text-foreground-light text-balance">
|
||
The database password isn’t viewable after creation. Resetting it will break any
|
||
existing connections.
|
||
</p>
|
||
</div>
|
||
|
||
<ButtonTooltip
|
||
type="default"
|
||
disabled={!canResetDbPassword || !isProjectActive || disabled}
|
||
onClick={() => setShowResetDbPass(true)}
|
||
tooltip={{
|
||
content: {
|
||
side: 'bottom',
|
||
text: !canResetDbPassword
|
||
? 'You need additional permissions to reset the database password'
|
||
: !isProjectActive
|
||
? 'Unable to reset database password as project is not active'
|
||
: undefined,
|
||
},
|
||
}}
|
||
>
|
||
Reset password
|
||
</ButtonTooltip>
|
||
</CardContent>
|
||
</Card>
|
||
</PageSectionContent>
|
||
</PageSection>
|
||
<Modal
|
||
hideFooter
|
||
header="Reset database password"
|
||
confirmText="Reset password"
|
||
size="medium"
|
||
visible={showResetDbPass}
|
||
loading={isUpdatingPassword}
|
||
onCancel={() => setShowResetDbPass(false)}
|
||
>
|
||
<Modal.Content className="w-full space-y-8">
|
||
<FormItemLayout
|
||
layout="vertical"
|
||
isReactForm={false}
|
||
error={passwordStrengthWarning}
|
||
description={
|
||
<PasswordStrengthBar
|
||
passwordStrengthScore={passwordStrengthScore as PasswordStrengthScore}
|
||
passwordStrengthMessage={passwordStrengthMessage}
|
||
password={password}
|
||
generateStrongPassword={generatePassword}
|
||
/>
|
||
}
|
||
>
|
||
<Input
|
||
copy={password.length > 0}
|
||
aria-invalid={!!passwordStrengthWarning}
|
||
type="password"
|
||
placeholder="Type in a strong password"
|
||
value={password}
|
||
autoComplete="off"
|
||
onChange={onDbPassChange}
|
||
/>
|
||
</FormItemLayout>
|
||
</Modal.Content>
|
||
<Modal.Separator />
|
||
<Modal.Content className="flex items-center justify-end space-x-2">
|
||
<Button
|
||
type="default"
|
||
disabled={isUpdatingPassword}
|
||
onClick={() => setShowResetDbPass(false)}
|
||
>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
loading={isUpdatingPassword}
|
||
disabled={isUpdatingPassword}
|
||
onClick={() => confirmResetDbPass()}
|
||
>
|
||
Reset password
|
||
</Button>
|
||
</Modal.Content>
|
||
</Modal>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default ResetDbPassword
|