mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Joshenlim/fe 4475 fdw update sql to run proper alter statements instead of (#50988)
## Context Currently for FDWs under integrations, editing an FDW involves tearing it down then re-creating it - which while conveniently works has a lot of problems like: - Blast radius is way bigger than the edit - Everything is recreated, including vault secrets - Silent drops grants/comments/ownership - Cascades on dependent objects - Views or functions built on top of foreign tables would get dropped along with it Changes in this PR hence updates `getUpdateFDWSql` to diff the current wrapper state against the form state and generate targeted `ALTER` statements for only what actually changed ## To test - [ ] Verify that updating an existing wrapper still works as expected <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Wrapper changes can be saved in place, including server options, encrypted values, foreign tables, and column definitions. * Input fields can display placeholder text, and missing server-option values display their defaults. * **Improvements** * Saving is unavailable until encrypted values are ready; a waiting message appears while they load. * The edit panel closes after a successful save. Confirmation text explains that table or column changes may affect dependent functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
b91870b012
commit
9aae037dff
6 files changed
+674
-157
No files matched your search
@@ -312,7 +312,12 @@ export const CreateWrapperSheet = ({
|
||||
{wrapperMeta.server.options
|
||||
.filter((option) => !option.hidden)
|
||||
.map((option) => (
|
||||
<InputField option={option} control={form.control} key={option.name} />
|
||||
<InputField
|
||||
key={option.name}
|
||||
option={option}
|
||||
control={form.control}
|
||||
placeholder={option.defaultValue}
|
||||
/>
|
||||
))}
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from './Wrappers.utils'
|
||||
import WrapperTableEditor from './WrapperTableEditor'
|
||||
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||||
import {
|
||||
FormSection,
|
||||
FormSectionContent,
|
||||
@@ -70,10 +71,11 @@ export const EditWrapperSheet = ({
|
||||
const { mutate: updateFDW, isPending: isSaving } = useFDWUpdateMutation({
|
||||
onSuccess: () => {
|
||||
toast.success(`Successfully updated ${wrapperMeta?.label} foreign data wrapper`)
|
||||
|
||||
const { tables } = getValues()
|
||||
const hasNewSchema = (tables as Record<string, any>[]).some((table) => table.is_new_schema)
|
||||
if (hasNewSchema) invalidateSchemasQuery(queryClient, project?.ref)
|
||||
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -151,7 +153,11 @@ export const EditWrapperSheet = ({
|
||||
const wrapper_name = useWatch({ name: 'wrapper_name', control: form.control })
|
||||
|
||||
const [isLoadingSecrets, setIsLoadingSecrets] = useState(false)
|
||||
const [secretsReady, setSecretsReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
||||
const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted)
|
||||
|
||||
const encryptedIdsToFetch = compact(
|
||||
@@ -160,7 +166,14 @@ export const EditWrapperSheet = ({
|
||||
return value ?? null
|
||||
})
|
||||
).filter((x) => UUID_REGEX.test(x))
|
||||
// [Joshen] ^ Validate UUID to filter out already decrypted values
|
||||
|
||||
if (encryptedIdsToFetch.length === 0) {
|
||||
setSecretsReady(true)
|
||||
setIsLoadingSecrets(false)
|
||||
return
|
||||
}
|
||||
|
||||
setSecretsReady(false)
|
||||
|
||||
const fetchEncryptedValues = async (ids: string[]) => {
|
||||
try {
|
||||
@@ -171,21 +184,26 @@ export const EditWrapperSheet = ({
|
||||
connectionString: project?.connectionString,
|
||||
ids: ids,
|
||||
})
|
||||
if (!isCurrent) return
|
||||
|
||||
encryptedOptions.forEach((option) => {
|
||||
const encryptedId = initialValues[option.name]
|
||||
|
||||
resetField(option.name, { defaultValue: decryptedValues[encryptedId] })
|
||||
})
|
||||
setSecretsReady(true)
|
||||
} catch (error) {
|
||||
if (!isCurrent) return
|
||||
toast.error('Failed to fetch encrypted values')
|
||||
} finally {
|
||||
setIsLoadingSecrets(false)
|
||||
if (isCurrent) setIsLoadingSecrets(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (encryptedIdsToFetch.length > 0) {
|
||||
fetchEncryptedValues(encryptedIdsToFetch)
|
||||
fetchEncryptedValues(encryptedIdsToFetch)
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [initialValues, wrapperMeta, resetField, project?.ref, project?.connectionString])
|
||||
|
||||
@@ -248,6 +266,7 @@ export const EditWrapperSheet = ({
|
||||
key={option.name}
|
||||
option={option}
|
||||
control={form.control}
|
||||
placeholder={option.defaultValue}
|
||||
loading={option.secureEntry ? isLoadingSecrets : undefined}
|
||||
/>
|
||||
))}
|
||||
@@ -335,16 +354,22 @@ export const EditWrapperSheet = ({
|
||||
<Button size="tiny" type="button" onClick={confirmOnClose} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
<ButtonTooltip
|
||||
size="tiny"
|
||||
variant="primary"
|
||||
form={FORM_ID}
|
||||
type="submit"
|
||||
disabled={isSubmitting || !isDirty}
|
||||
disabled={isSubmitting || !isDirty || !secretsReady}
|
||||
loading={isSubmitting}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'top',
|
||||
text: !secretsReady ? 'Waiting for encrypted values to load' : undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Save wrapper
|
||||
</Button>
|
||||
</ButtonTooltip>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -352,11 +377,11 @@ export const EditWrapperSheet = ({
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isUpdateConfirmationOpen}
|
||||
title="Recreate wrapper?"
|
||||
size="medium"
|
||||
title="Save wrapper changes?"
|
||||
size="small"
|
||||
variant="warning"
|
||||
confirmLabel="Recreate wrapper"
|
||||
confirmLabelLoading="Recreating wrapper"
|
||||
confirmLabel="Save changes"
|
||||
confirmLabelLoading="Saving changes"
|
||||
loading={isSaving}
|
||||
onCancel={() => {
|
||||
setIsUpdateConfirmationOpen(false)
|
||||
@@ -376,9 +401,7 @@ export const EditWrapperSheet = ({
|
||||
}}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Saving changes will drop the existing wrapper and recreate it. Foreign servers and tables
|
||||
will be recreated, and dependent objects like functions or views that reference those
|
||||
tables may need to be updated manually afterwards.
|
||||
Removing a table or retyping a column may break views or functions that reference it.
|
||||
</p>
|
||||
<p className="text-sm text-foreground-light mt-2">Are you sure you want to continue?</p>
|
||||
</ConfirmationModal>
|
||||
|
||||
@@ -11,6 +11,7 @@ interface InputFieldProps<TFieldValues extends FieldValues = FieldValues> {
|
||||
option: ServerOption
|
||||
control: Control<TFieldValues>
|
||||
loading?: boolean
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
const InputField = <
|
||||
@@ -20,6 +21,7 @@ const InputField = <
|
||||
control,
|
||||
option,
|
||||
loading = false,
|
||||
placeholder,
|
||||
}: InputFieldProps<TFieldValues>) => {
|
||||
return (
|
||||
<FormField
|
||||
@@ -57,7 +59,7 @@ const InputField = <
|
||||
) : option.secureEntry ? (
|
||||
<PasswordInput copy reveal {...field} />
|
||||
) : (
|
||||
<Input {...field} />
|
||||
<Input {...field} placeholder={placeholder} />
|
||||
)}
|
||||
</FormControl>
|
||||
</FormItemLayout>
|
||||
|
||||
@@ -41,6 +41,7 @@ export const WrapperRow = ({ wrapper, isShared }: WrapperRowProps) => {
|
||||
|
||||
const _tables = formatWrapperTables(wrapper, integration?.meta)
|
||||
const canEdit = canManageWrappers && !isShared
|
||||
|
||||
let editTooltip = 'Edit wrapper'
|
||||
if (!canManageWrappers) editTooltip = 'You need additional permissions to edit wrappers'
|
||||
else if (isShared) editTooltip = 'Shared wrappers cannot be edited in the dashboard'
|
||||
@@ -87,7 +88,7 @@ export const WrapperRow = ({ wrapper, isShared }: WrapperRowProps) => {
|
||||
>
|
||||
<span className="text-foreground-lighter text-nowrap">{metadata.label}:</span>
|
||||
<span className="truncate max-w-72" title={serverOptions[metadata.name]}>
|
||||
{serverOptions[metadata.name]}
|
||||
{serverOptions[metadata.name] ?? metadata.defaultValue}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user