Home / Function/ EditWrapperSheet() — supabase Function Reference

EditWrapperSheet() — supabase Function Reference

Architecture documentation for the EditWrapperSheet() function in EditWrapperSheet.tsx from the supabase codebase.

Entity Profile

Dependency Diagram

graph TD
  2d7aff71_ad70_13c1_d314_aaa9543be33f["EditWrapperSheet()"]
  c8623a94_43a1_47f9_96c7_09ed48ad65f2["formatWrapperTables()"]
  2d7aff71_ad70_13c1_d314_aaa9543be33f -->|calls| c8623a94_43a1_47f9_96c7_09ed48ad65f2
  3f0a904f_bdfa_22ec_c956_24f285f3ee4c["convertKVStringArrayToJson()"]
  2d7aff71_ad70_13c1_d314_aaa9543be33f -->|calls| 3f0a904f_bdfa_22ec_c956_24f285f3ee4c
  f17778f0_0474_c6f9_6004_6763d2604934["makeValidateRequired()"]
  2d7aff71_ad70_13c1_d314_aaa9543be33f -->|calls| f17778f0_0474_c6f9_6004_6763d2604934
  style 2d7aff71_ad70_13c1_d314_aaa9543be33f fill:#6366f1,stroke:#818cf8,color:#fff

Relationship Graph

Source Code

apps/studio/components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx lines 37–414

export const EditWrapperSheet = ({
  wrapper,
  wrapperMeta,
  isClosing,
  setIsClosing,
  onClose,
}: EditWrapperSheetProps) => {
  const queryClient = useQueryClient()
  const { data: project } = useSelectedProjectQuery()

  const { mutate: updateFDW, isPending: isSaving } = useFDWUpdateMutation({
    onSuccess: () => {
      toast.success(`Successfully updated ${wrapperMeta?.label} foreign data wrapper`)
      setWrapperTables([])

      const hasNewSchema = wrapperTables.some((table) => table.is_new_schema)
      if (hasNewSchema) invalidateSchemasQuery(queryClient, project?.ref)
    },
  })

  const [wrapperTables, setWrapperTables] = useState(() =>
    formatWrapperTables(wrapper, wrapperMeta)
  )
  const [isEditingTable, setIsEditingTable] = useState(false)
  const [selectedTableToEdit, setSelectedTableToEdit] = useState<FormattedWrapperTable | undefined>(
    undefined
  )
  const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({})
  const [isUpdateConfirmationOpen, setIsUpdateConfirmationOpen] = useState(false)
  const [pendingFormState, setPendingFormState] = useState<Record<string, string> | null>(null)
  const hasChangesRef = useRef(false)

  const initialValues = {
    wrapper_name: wrapper?.name,
    server_name: wrapper?.server_name,
    ...convertKVStringArrayToJson(wrapper?.server_options ?? []),
  }

  const onUpdateTable = (values: FormattedWrapperTable) => {
    setWrapperTables((prev) => {
      // if the new values have tableIndex, we are editing an existing table
      if (values.tableIndex !== undefined) {
        const tableIndex = values.tableIndex
        const wrapperTables = [...prev]
        delete values.tableIndex
        wrapperTables[tableIndex] = values
        return wrapperTables
      }
      return [...prev, values]
    })
    setIsEditingTable(false)
    setSelectedTableToEdit(undefined)
  }

  const onSubmit = async (values: Record<string, string>) => {
    const validate = makeValidateRequired(wrapperMeta.server.options)
    const errors = validate(values)

    const { wrapper_name } = values
    if (wrapper_name.length === 0) errors.name = 'Please provide a name for your wrapper'
    if (!wrapperMeta.canTargetSchema && wrapperTables.length === 0)
      errors.tables = 'Please add at least one table'
    if (!isEmpty(errors)) {
      setFormErrors(errors)
      return
    }

    setFormErrors({})
    setPendingFormState({ ...values, server_name: `${wrapper_name}_server` })
    setIsUpdateConfirmationOpen(true)
  }

  const checkIsDirty = useCallback(() => hasChangesRef.current, [])

  const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
    checkIsDirty,
    onClose,
  })

  useEffect(() => {
    if (!isClosing) return
    if (checkIsDirty()) {
      confirmOnClose()
    } else {
      onClose()
    }
    setIsClosing(false)
  }, [checkIsDirty, confirmOnClose, isClosing, onClose, setIsClosing])

  return (
    <>
      <div className="flex flex-col h-full" tabIndex={-1}>
        <Form
          id={FORM_ID}
          initialValues={initialValues}
          onSubmit={onSubmit}
          className="h-full flex flex-col"
        >
          {({
            values,
            initialValues,
            resetForm,
          }: {
            values: Record<string, string>
            initialValues: Record<string, string>
            resetForm: (value: Record<string, Record<string, string>>) => void
          }) => {
            // [Alaister] although this "technically" is breaking the rules of React hooks
            // it won't error because the hooks are always rendered in the same order
            // eslint-disable-next-line react-hooks/rules-of-hooks
            const [loadingSecrets, setLoadingSecrets] = useState(false)

            const initialTables = formatWrapperTables({
              handler: wrapper.handler,
              tables: wrapper?.tables ?? [],
            })
            const hasFormChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
            const hasTableChanges = JSON.stringify(initialTables) !== JSON.stringify(wrapperTables)
            const hasChanges = hasFormChanges || hasTableChanges
            hasChangesRef.current = hasChanges

            // [Alaister] although this "technically" is breaking the rules of React hooks
            // it won't error because the hooks are always rendered in the same order
            // eslint-disable-next-line react-hooks/rules-of-hooks
            useEffect(() => {
              const fetchEncryptedValues = async (ids: string[]) => {
                try {
                  setLoadingSecrets(true)
                  // If the secrets haven't loaded, escape and run the effect again when they're loaded
                  const decryptedValues = await getDecryptedValues({
                    projectRef: project?.ref,
                    connectionString: project?.connectionString,
                    ids: ids,
                  })

                  // replace all values which are in the decryptedValues object with the decrypted value
                  const transformValues = (values: Record<string, string>) => {
                    return mapValues(values, (value) => {
                      return decryptedValues[value] ?? value
                    })
                  }

                  resetForm({
                    values: transformValues(values),
                    initialValues: transformValues(initialValues),
                  })
                } catch (error) {
                  toast.error('Failed to fetch encrypted values')
                } finally {
                  setLoadingSecrets(false)
                }
              }

              const encryptedOptions = wrapperMeta.server.options.filter(
                (option) => option.encrypted
              )

              const encryptedIdsToFetch = compact(
                encryptedOptions.map((option) => {
                  const value = initialValues[option.name]
                  return value ?? null
                })
              ).filter((x) => UUID_REGEX.test(x))
              // [Joshen] ^ Validate UUID to filter out already decrypted values

              if (encryptedIdsToFetch.length > 0) {
                fetchEncryptedValues(encryptedIdsToFetch)
              }
              /**
               * [Joshen] We're deliberately not adding values and initialValues to the dependency array here
               * as we only want to fetch the encrypted values once on load + values and initialValues will be updated
               * as a result of that
               */
              // eslint-disable-next-line react-hooks/exhaustive-deps
            }, [project?.ref, project?.connectionString])

            return (
              <>
                <SheetHeader>
                  <SheetTitle>
                    Edit {wrapperMeta.label} wrapper: {wrapper.name}
                  </SheetTitle>
                </SheetHeader>
                <div className="flex-grow overflow-y-auto">
                  <FormSection header={<FormSectionLabel>Wrapper Configuration</FormSectionLabel>}>
                    <FormSectionContent loading={false}>
                      <Input
                        id="wrapper_name"
                        label="Wrapper Name"
                        error={formErrors.wrapper_name}
                        descriptionText={
                          values.wrapper_name !== initialValues.wrapper_name ? (
                            <>
                              Your wrapper's server name will be updated to{' '}
                              <code className="text-code-inline">{values.wrapper_name}_server</code>
                            </>
                          ) : (
                            <>
                              Your wrapper's server name is{' '}
                              <code className="text-code-inline">{values.wrapper_name}_server</code>
                            </>
                          )
                        }
                      />
                    </FormSectionContent>
                  </FormSection>
                  <FormSection
                    header={<FormSectionLabel>{wrapperMeta.label} Configuration</FormSectionLabel>}
                  >
                    <FormSectionContent loading={false}>
                      {wrapperMeta.server.options
                        .filter((option) => !option.hidden)
                        .map((option) => (
                          <InputField
                            key={option.name}
                            option={option}
                            loading={option.encrypted ? loadingSecrets : false}
                            error={formErrors[option.name]}
                          />
                        ))}
                    </FormSectionContent>
                  </FormSection>
                  <FormSection
                    header={
                      <FormSectionLabel>
                        <p>Foreign Tables</p>
                        <p className="text-foreground-light mt-2 w-[90%]">
                          You can query your data from these foreign tables after the wrapper is
                          created
                        </p>
                      </FormSectionLabel>
                    }
                  >
                    <FormSectionContent loading={false}>
                      {wrapperTables.length === 0 ? (
                        <div className="flex justify-end translate-y-4">
                          <Button type="default" onClick={() => setIsEditingTable(true)}>
                            Add foreign table
                          </Button>
                        </div>
                      ) : (
                        <div className="space-y-2">
                          {wrapperTables.map((table, i) => {
                            const target = table?.table ?? table.object

                            return (
                              <div
                                key={`${table.schema_name}.${table.table_name}`}
                                className="flex items-center justify-between px-4 py-2 border rounded-md border-control"
                              >
                                <div>
                                  <p className="text-sm">
                                    {table.schema_name}.{table.table_name}{' '}
                                  </p>
                                  <p className="text-sm text-foreground-light mt-1">
                                    Target: {target}
                                  </p>
                                  <p className="text-sm text-foreground-light">
                                    Columns: {table.columns.map((column) => column.name).join(', ')}
                                  </p>
                                </div>
                                <div className="flex items-center space-x-2">
                                  {/* Wrappers which import foreign schema don't have tables and their tables can't be edited */}
                                  {wrapperMeta.tables.length !== 0 && (
                                    <Button
                                      type="default"
                                      className="px-1"
                                      icon={<Edit />}
                                      onClick={() => {
                                        setIsEditingTable(true)
                                        setSelectedTableToEdit({ ...table, tableIndex: i })
                                      }}
                                    />
                                  )}
                                  <Button
                                    type="default"
                                    className="px-1"
                                    icon={<Trash />}
                                    onClick={() => {
                                      setWrapperTables((prev) => prev.filter((_, j) => j !== i))
                                    }}
                                  />
                                </div>
                              </div>
                            )
                          })}
                        </div>
                      )}
                      {wrapperTables.length > 0 && (
                        <div className="flex justify-end">
                          <Button type="default" onClick={() => setIsEditingTable(true)}>
                            Add foreign table
                          </Button>
                        </div>
                      )}
                      {wrapperTables.length === 0 && formErrors.tables && (
                        <p className="text-sm text-right text-red-900">{formErrors.tables}</p>
                      )}
                    </FormSectionContent>
                  </FormSection>
                </div>
                <SheetFooter>
                  <Button
                    size="tiny"
                    type="default"
                    htmlType="button"
                    onClick={confirmOnClose}
                    disabled={isSaving}
                  >
                    Cancel
                  </Button>
                  <Button
                    size="tiny"
                    type="primary"
                    form={FORM_ID}
                    htmlType="submit"
                    disabled={isSaving}
                    loading={isSaving}
                  >
                    Save wrapper
                  </Button>
                </SheetFooter>
              </>
            )
          }}
        </Form>
      </div>

      <ConfirmationModal
        visible={isUpdateConfirmationOpen}
        title="Recreate wrapper?"
        size="medium"
        variant="warning"
        confirmLabel="Recreate wrapper"
        confirmLabelLoading="Recreating wrapper"
        loading={isSaving}
        onCancel={() => {
          setIsUpdateConfirmationOpen(false)
          setPendingFormState(null)
          onClose()
        }}
        onConfirm={() => {
          if (pendingFormState === null) return
          updateFDW({
            projectRef: project?.ref,
            connectionString: project?.connectionString,
            wrapper,
            wrapperMeta,
            formState: pendingFormState,
            tables: wrapperTables,
          })
          setIsUpdateConfirmationOpen(false)
          setPendingFormState(null)
        }}
      >
        <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.
        </p>
        <p className="text-sm text-foreground-light mt-2">Are you sure you want to continue?</p>
      </ConfirmationModal>

      <CloseConfirmationModal {...closeConfirmationModalProps} />

      <WrapperTableEditor
        visible={isEditingTable}
        tables={wrapperMeta.tables}
        onCancel={() => {
          setSelectedTableToEdit(undefined)
          setIsEditingTable(false)
        }}
        onSave={onUpdateTable}
        initialData={selectedTableToEdit}
      />
    </>
  )
}

Subdomains

Frequently Asked Questions

What does EditWrapperSheet() do?
EditWrapperSheet() is a function in the supabase codebase.
What does EditWrapperSheet() call?
EditWrapperSheet() calls 3 function(s): convertKVStringArrayToJson, formatWrapperTables, makeValidateRequired.

Analyze Your Own Codebase

Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.

Try Supermodel Free