diff --git a/frontend/src/metabase-types/forms/legacy.ts b/frontend/src/metabase-types/forms/legacy.ts
deleted file mode 100644
index c7e4b5fd44c3395fc4c4d08f0e2758bd36845631..0000000000000000000000000000000000000000
--- a/frontend/src/metabase-types/forms/legacy.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * @deprecated
- */
-export type FieldName = string;
-
-/**
- * @deprecated
- */
-export type DefaultFieldValue = unknown;
-
-/**
- * @deprecated
- */
-export type FieldValues = Record<FieldName, DefaultFieldValue>;
-
-type FieldValidateResultOK = undefined;
-type FieldValidateResultError = string;
-
-// Extending Record type here as field definition's props
-// will be just spread to the final field widget
-// (e.g. autoFocus, placeholder)
-/**
- * @deprecated
- */
-export type BaseFieldDefinition = Record<string, unknown> & {
-  name: string;
-  type?: string;
-  title?: string;
-  description?: string;
-  placeholder?: string;
-  hidden?: boolean;
-
-  info?: string;
-  infoLabel?: string;
-  infoLabelTooltip?: string;
-
-  align?: "left" | "right";
-  horizontal?: boolean;
-  descriptionPosition?: "top" | "bottom";
-  visibleIf?: Record<FieldName, unknown>;
-
-  initial?: (value: unknown) => DefaultFieldValue;
-  validate?: (
-    value: DefaultFieldValue,
-  ) => FieldValidateResultOK | FieldValidateResultError;
-  normalize?: (value: unknown) => DefaultFieldValue;
-};
-
-/**
- * @deprecated
- */
-export type StandardFormFieldDefinition = BaseFieldDefinition & {
-  // If not is not provided, we're going to use default text input
-  type?: string | (() => JSX.Element);
-};
-
-/**
- * @deprecated
- */
-export type CustomFormFieldDefinition = BaseFieldDefinition & {
-  widget: () => JSX.Element;
-};
-
-/**
- * @deprecated
- */
-export type FormFieldDefinition =
-  | StandardFormFieldDefinition
-  | CustomFormFieldDefinition;
-
-/**
- * @deprecated
- */
-export type FormField<Value = DefaultFieldValue> = {
-  name: FieldName;
-  value: Value;
-  error?: string;
-  initialValue: Value;
-
-  active: boolean;
-  dirty: boolean;
-  invalid: boolean;
-  pristine: boolean;
-  touched: boolean;
-  valid: boolean;
-  visited: boolean;
-
-  onBlur: () => void;
-  onFocus: () => void;
-};
-
-/**
- * @deprecated
- */
-export type FormObject = {
-  fields:
-    | FormFieldDefinition[]
-    | ((values?: FieldValues) => FormFieldDefinition[]);
-};
-
-/**
- * @deprecated
- */
-export type PopulatedFormObject = {
-  fields: (values?: FieldValues) => FormFieldDefinition[];
-  fieldNames: (values: FieldValues) => FieldName[];
-  hidden: (obj: unknown) => void;
-  initial: (obj: unknown) => void;
-  normalize: (obj: unknown) => void;
-  validate: (obj: unknown) => void;
-  disablePristineSubmit?: boolean;
-};
diff --git a/frontend/src/metabase-types/guards/forms-legacy.ts b/frontend/src/metabase-types/guards/forms-legacy.ts
deleted file mode 100644
index 29fc6a0b1a4c673e22fc65bfdcc06e397824be27..0000000000000000000000000000000000000000
--- a/frontend/src/metabase-types/guards/forms-legacy.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type {
-  StandardFormFieldDefinition,
-  CustomFormFieldDefinition,
-  FormFieldDefinition,
-} from "metabase-types/forms/legacy";
-
-export function isCustomWidget(
-  formField: FormFieldDefinition,
-): formField is CustomFormFieldDefinition {
-  return (
-    !(formField as StandardFormFieldDefinition).type &&
-    typeof (formField as CustomFormFieldDefinition).widget === "function"
-  );
-}
diff --git a/frontend/src/metabase/components/form/CustomForm/CustomForm.tsx b/frontend/src/metabase/components/form/CustomForm/CustomForm.tsx
deleted file mode 100644
index a6e61319a4e7ab3a5ef6e8998a4664c35609ae7d..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/CustomForm/CustomForm.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import React from "react";
-import _ from "underscore";
-
-import {
-  FormFieldDefinition,
-  PopulatedFormObject,
-} from "metabase-types/forms/legacy";
-
-import {
-  BaseFormProps,
-  OptionalFormViewProps,
-  CustomFormLegacyContext,
-  LegacyContextTypes,
-} from "./types";
-
-import CustomFormField, { CustomFormFieldProps } from "./CustomFormField";
-import CustomFormFooter, { CustomFormFooterProps } from "./CustomFormFooter";
-import CustomFormMessage, { CustomFormMessageProps } from "./CustomFormMessage";
-import CustomFormSubmit from "./CustomFormSubmit";
-import Form from "./Form";
-
-interface FormRenderProps extends BaseFormProps {
-  form: PopulatedFormObject;
-  formFields: FormFieldDefinition[];
-  Form: React.ComponentType<{ children: React.ReactNode }>;
-  FormField: React.ComponentType<CustomFormFieldProps>;
-  FormSubmit: React.ComponentType<{ children: React.ReactNode }>;
-  FormMessage: React.ComponentType<CustomFormMessageProps>;
-  FormFooter: React.ComponentType<CustomFormFooterProps>;
-}
-
-interface CustomFormProps extends BaseFormProps, OptionalFormViewProps {
-  children: React.ReactNode | ((props: FormRenderProps) => JSX.Element);
-}
-
-function CustomForm(props: CustomFormProps) {
-  const { formObject: form, values, children } = props;
-  if (typeof children === "function") {
-    return children({
-      ...props,
-      form,
-      formFields: form.fields(values),
-      Form: Form,
-      FormField: CustomFormField,
-      FormSubmit: CustomFormSubmit,
-      FormMessage: CustomFormMessage,
-      FormFooter: CustomFormFooter,
-    });
-  }
-  return <Form {...props} />;
-}
-
-/**
- * @deprecated
- */
-class CustomFormWithLegacyContext extends React.Component<CustomFormProps> {
-  static childContextTypes = LegacyContextTypes;
-
-  getChildContext(): CustomFormLegacyContext {
-    const {
-      fields,
-      values,
-      formObject: form,
-      submitting,
-      invalid,
-      pristine,
-      error,
-      handleSubmit,
-      submitTitle,
-      renderSubmit,
-      className,
-      style,
-      onChangeField,
-    } = this.props;
-    const { disablePristineSubmit } = form;
-    const formFields = form.fields(values);
-    const formFieldsByName = _.indexBy(formFields, "name");
-
-    return {
-      handleSubmit,
-      submitTitle,
-      renderSubmit,
-      className,
-      style,
-      fields,
-      formFields,
-      formFieldsByName,
-      values,
-      submitting,
-      invalid,
-      pristine,
-      error,
-      onChangeField,
-      disablePristineSubmit,
-    };
-  }
-
-  render() {
-    return <CustomForm {...this.props} />;
-  }
-}
-
-export default CustomFormWithLegacyContext;
diff --git a/frontend/src/metabase/components/form/CustomForm/CustomFormField.tsx b/frontend/src/metabase/components/form/CustomForm/CustomFormField.tsx
deleted file mode 100644
index 537f7acbd4931de7e76fade0bb2ccd7097045d6b..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/CustomForm/CustomFormField.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import React, { useCallback, useMemo } from "react";
-import PropTypes from "prop-types";
-import { getIn } from "icepick";
-import _ from "underscore";
-
-import FormField from "metabase/components/form/FormField";
-import FormWidget from "metabase/components/form/FormWidget";
-
-import { useOnMount } from "metabase/hooks/use-on-mount";
-import { useOnUnmount } from "metabase/hooks/use-on-unmount";
-
-import { BaseFieldDefinition } from "metabase-types/forms/legacy";
-import { isCustomWidget } from "metabase-types/guards/forms-legacy";
-
-import {
-  CustomFormLegacyContext,
-  FormContainerLegacyContext,
-  LegacyContextTypes,
-} from "./types";
-
-export interface CustomFormFieldProps extends BaseFieldDefinition {
-  onChange?: (e: unknown) => void;
-}
-interface LegacyContextProps
-  extends CustomFormLegacyContext,
-    FormContainerLegacyContext {}
-
-function getFieldDefinition(props: CustomFormFieldProps): BaseFieldDefinition {
-  return _.pick(
-    props,
-    "name",
-    "type",
-    "title",
-    "description",
-    "initial",
-    "validate",
-    "normalize",
-  );
-}
-
-function RawCustomFormField({
-  fields,
-  formFieldsByName,
-  values,
-  onChangeField,
-  registerFormField,
-  unregisterFormField,
-  ...props
-}: CustomFormFieldProps & LegacyContextProps & { forwardedRef?: any }) {
-  const { name, onChange, forwardedRef } = props;
-
-  const field = getIn(fields, name.split("."));
-  const formField = formFieldsByName[name];
-
-  useOnMount(() => {
-    registerFormField?.(getFieldDefinition(props));
-  });
-
-  useOnUnmount(() => {
-    unregisterFormField?.(getFieldDefinition(props));
-  });
-
-  const handleChange = useCallback(
-    e => {
-      field.onChange(e);
-      onChange?.(e);
-    },
-    [field, onChange],
-  );
-
-  const fieldProps = useMemo(
-    () => ({
-      ...props,
-      values,
-      onChangeField,
-      formField,
-      field:
-        typeof onChange === "function"
-          ? {
-              ...field,
-              onChange: handleChange,
-            }
-          : field,
-    }),
-    [props, values, formField, field, onChange, onChangeField, handleChange],
-  );
-
-  if (!field || !formField) {
-    return null;
-  }
-
-  const hasCustomWidget = isCustomWidget(formField);
-  const Widget = hasCustomWidget ? formField.widget : FormWidget;
-
-  return (
-    <FormField {...fieldProps}>
-      <Widget {...fieldProps} ref={forwardedRef} />
-    </FormField>
-  );
-}
-
-const CustomFormFieldLegacyContext = (
-  props: CustomFormFieldProps & { forwardedRef?: any },
-  context: LegacyContextProps,
-) => <RawCustomFormField {...props} {...context} />;
-
-CustomFormFieldLegacyContext.contextTypes = {
-  ..._.pick(
-    LegacyContextTypes,
-    "fields",
-    "formFieldsByName",
-    "values",
-    "onChangeField",
-  ),
-  registerFormField: PropTypes.func,
-  unregisterFormField: PropTypes.func,
-};
-
-/**
- * @deprecated
- */
-const CustomFormField = React.forwardRef<
-  HTMLInputElement,
-  CustomFormFieldProps
->(function CustomFormField(props, ref) {
-  return <CustomFormFieldLegacyContext {...props} forwardedRef={ref} />;
-});
-
-export default CustomFormField;
diff --git a/frontend/src/metabase/components/form/CustomForm/CustomFormFooter/CustomFormFooter.styled.tsx b/frontend/src/metabase/components/form/CustomForm/CustomFormFooter/CustomFormFooter.styled.tsx
deleted file mode 100644
index 5e29a2fdd29d5bde5e821cdb48b579202af429af..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/CustomForm/CustomFormFooter/CustomFormFooter.styled.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import styled from "@emotion/styled";
-
-interface CustomFormFooterStyledProps {
-  shouldReverse?: boolean;
-}
-export const CustomFormFooterStyled = styled.div<CustomFormFooterStyledProps>`
-  display: flex;
-  align-items: flex-start;
-  flex-direction: ${({ shouldReverse }) =>
-    shouldReverse ? "row-reverse" : "column"};
-`;
diff --git a/frontend/src/metabase/components/form/CustomForm/CustomFormFooter/CustomFormFooter.tsx b/frontend/src/metabase/components/form/CustomForm/CustomFormFooter/CustomFormFooter.tsx
deleted file mode 100644
index fb0f108256fcf723e84a23862337f9f85dce3b7a..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/CustomForm/CustomFormFooter/CustomFormFooter.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import React from "react";
-import PropTypes from "prop-types";
-import { t } from "ttag";
-
-import Button from "metabase/core/components/Button";
-
-import CustomFormMessage from "../CustomFormMessage";
-import CustomFormSubmit from "../CustomFormSubmit";
-import { CustomFormFooterStyled } from "./CustomFormFooter.styled";
-
-import { CustomFormFooterProps } from "./CustomFormFooterTypes";
-
-interface LegacyContextProps {
-  isModal?: boolean;
-}
-
-function CustomFormFooter({
-  submitTitle,
-  cancelTitle = t`Cancel`,
-  onCancel,
-  footerExtraButtons,
-  fullWidth,
-  isModal,
-  isContextModal,
-}: CustomFormFooterProps & { isContextModal?: boolean }) {
-  return (
-    <CustomFormFooterStyled shouldReverse={isModal || isContextModal}>
-      <CustomFormSubmit fullWidth={fullWidth}>{submitTitle}</CustomFormSubmit>
-      {onCancel && (
-        <Button className="mx1" type="button" onClick={onCancel}>
-          {cancelTitle}
-        </Button>
-      )}
-      <CustomFormMessage className="mt1" />
-      {footerExtraButtons}
-    </CustomFormFooterStyled>
-  );
-}
-
-/**
- * @deprecated
- */
-const CustomFormFooterLegacyContext = (
-  props: CustomFormFooterProps,
-  { isModal: isContextModal }: LegacyContextProps,
-) => <CustomFormFooter {...props} isContextModal={isContextModal} />;
-
-CustomFormFooterLegacyContext.contextTypes = {
-  isModal: PropTypes.bool,
-};
-
-export default CustomFormFooterLegacyContext;
diff --git a/frontend/src/metabase/components/form/CustomForm/CustomFormFooter/CustomFormFooterTypes.ts b/frontend/src/metabase/components/form/CustomForm/CustomFormFooter/CustomFormFooterTypes.ts
deleted file mode 100644
index 4de033991ff7e33b324fbabcf617f62907951b97..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/CustomForm/CustomFormFooter/CustomFormFooterTypes.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export interface CustomFormFooterProps {
-  submitTitle: string;
-  cancelTitle?: string;
-  fullWidth?: boolean;
-  isModal?: boolean;
-  footerExtraButtons: React.ReactElement[];
-  onCancel?: () => void;
-}
diff --git a/frontend/src/metabase/components/form/CustomForm/CustomFormFooter/index.ts b/frontend/src/metabase/components/form/CustomForm/CustomFormFooter/index.ts
deleted file mode 100644
index c404ea9994c6db74449bfa4654f1dbc714350ede..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/CustomForm/CustomFormFooter/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from "./CustomFormFooter";
-export * from "./CustomFormFooterTypes";
diff --git a/frontend/src/metabase/components/form/CustomForm/CustomFormMessage.tsx b/frontend/src/metabase/components/form/CustomForm/CustomFormMessage.tsx
deleted file mode 100644
index b293875b8376287852f49e0b558f079f80a60963..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/CustomForm/CustomFormMessage.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from "react";
-import _ from "underscore";
-
-import FormMessage from "metabase/components/form/FormMessage";
-
-import { CustomFormLegacyContext, LegacyContextTypes } from "./types";
-
-export interface CustomFormMessageProps {
-  className?: string;
-  noPadding?: boolean;
-}
-
-function CustomFormMessage({
-  error,
-  ...props
-}: CustomFormMessageProps & CustomFormLegacyContext) {
-  if (error) {
-    return <FormMessage {...props} message={error} />;
-  }
-  return null;
-}
-
-/**
- * @deprecated
- */
-const CustomFormMessageLegacyContext = (
-  props: CustomFormMessageProps,
-  context: CustomFormLegacyContext,
-) => <CustomFormMessage {...props} {...context} />;
-
-CustomFormMessageLegacyContext.contextTypes = _.pick(
-  LegacyContextTypes,
-  "error",
-);
-
-export default CustomFormMessageLegacyContext;
diff --git a/frontend/src/metabase/components/form/CustomForm/CustomFormSection.tsx b/frontend/src/metabase/components/form/CustomForm/CustomFormSection.tsx
deleted file mode 100644
index edc83572a40c31afd355b8d5523c555cec678ef0..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/CustomForm/CustomFormSection.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from "react";
-
-import DisclosureTriangle from "metabase/components/DisclosureTriangle";
-
-import { useToggle } from "metabase/hooks/use-toggle";
-
-interface SectionProps {
-  title?: string;
-  children: React.ReactNode;
-}
-
-function StandardSection({ title, children }: SectionProps) {
-  return (
-    <section className="mb4">
-      {title && <h2 className="mb2">{title}</h2>}
-      {children}
-    </section>
-  );
-}
-
-function CollapsibleSection({ title, children }: SectionProps) {
-  const [isExpanded, { toggle: handleToggle }] = useToggle(false);
-  return (
-    <section className="mb4">
-      <div
-        className="mb2 flex align-center cursor-pointer text-brand-hover"
-        onClick={handleToggle}
-      >
-        <DisclosureTriangle className="mr1" open={isExpanded} />
-        <h3>{title}</h3>
-      </div>
-      <div className={isExpanded ? undefined : "hide"}>{children}</div>
-    </section>
-  );
-}
-
-interface CustomFormSectionProps extends SectionProps {
-  collapsible?: boolean;
-}
-
-/**
- * @deprecated
- */
-function CustomFormSection({ collapsible, ...props }: CustomFormSectionProps) {
-  const Section = collapsible ? CollapsibleSection : StandardSection;
-  return <Section {...props} />;
-}
-
-export default CustomFormSection;
diff --git a/frontend/src/metabase/components/form/CustomForm/CustomFormSubmit.tsx b/frontend/src/metabase/components/form/CustomForm/CustomFormSubmit.tsx
deleted file mode 100644
index f3cbe5b5b3c579e6b8387ce5846bc300feb5312f..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/CustomForm/CustomFormSubmit.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import React from "react";
-import { t } from "ttag";
-import _ from "underscore";
-
-import ActionButton from "metabase/components/ActionButton";
-
-import { CustomFormLegacyContext, LegacyContextTypes } from "./types";
-
-export interface CustomFormSubmitProps {
-  children: React.ReactNode;
-
-  // ActionButton props
-  fullWidth?: boolean;
-}
-
-function CustomFormSubmit({
-  submitting,
-  invalid,
-  pristine,
-  handleSubmit,
-  submitTitle,
-  renderSubmit,
-  disablePristineSubmit,
-  children,
-  ...props
-}: CustomFormSubmitProps & CustomFormLegacyContext) {
-  const title = children || submitTitle || t`Submit`;
-  const canSubmit = !(
-    submitting ||
-    invalid ||
-    (pristine && disablePristineSubmit)
-  );
-
-  if (renderSubmit) {
-    return renderSubmit({ title, canSubmit, handleSubmit });
-  }
-
-  return (
-    <ActionButton
-      normalText={title}
-      activeText={title}
-      failedText={t`Failed`}
-      successText={t`Success`}
-      primary={canSubmit}
-      disabled={!canSubmit}
-      {...props}
-      type="submit"
-      actionFn={handleSubmit}
-    />
-  );
-}
-
-/**
- * @deprecated
- */
-const CustomFormSubmitLegacyContext = (
-  props: CustomFormSubmitProps,
-  context: CustomFormLegacyContext,
-) => <CustomFormSubmit {...props} {...context} />;
-
-CustomFormSubmitLegacyContext.contextTypes = _.pick(
-  LegacyContextTypes,
-  "values",
-  "submitting",
-  "invalid",
-  "pristine",
-  "handleSubmit",
-  "submitTitle",
-  "renderSubmit",
-  "disablePristineSubmit",
-);
-
-export default CustomFormSubmitLegacyContext;
diff --git a/frontend/src/metabase/components/form/CustomForm/Form.tsx b/frontend/src/metabase/components/form/CustomForm/Form.tsx
deleted file mode 100644
index 4d4233a3e411e80e1f014e668e6f084f597670a8..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/CustomForm/Form.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from "react";
-import _ from "underscore";
-import { CustomFormLegacyContext, LegacyContextTypes } from "./types";
-
-type Props = {
-  children: React.ReactNode;
-};
-
-function Form({
-  children,
-  handleSubmit,
-  className,
-  style,
-}: Props & CustomFormLegacyContext) {
-  return (
-    <form onSubmit={handleSubmit} className={className} style={style}>
-      {children}
-    </form>
-  );
-}
-
-/**
- * @deprecated
- */
-const FormUsingLegacyContext = (
-  props: Props,
-  context: CustomFormLegacyContext,
-) => <Form {...props} {...context} />;
-
-FormUsingLegacyContext.contextTypes = _.pick(
-  LegacyContextTypes,
-  "handleSubmit",
-  "className",
-  "style",
-);
-
-export default FormUsingLegacyContext;
diff --git a/frontend/src/metabase/components/form/CustomForm/index.ts b/frontend/src/metabase/components/form/CustomForm/index.ts
deleted file mode 100644
index d7501a864a644ec20731b5ad0c4fafacecce9b42..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/CustomForm/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export { default as CustomFormField } from "./CustomFormField";
-export { default as CustomFormFooter } from "./CustomFormFooter";
-export { default as CustomFormMessage } from "./CustomFormMessage";
-export { default as CustomFormSection } from "./CustomFormSection";
-export { default as CustomFormSubmit } from "./CustomFormSubmit";
-
-export { default } from "./CustomForm";
diff --git a/frontend/src/metabase/components/form/CustomForm/types.ts b/frontend/src/metabase/components/form/CustomForm/types.ts
deleted file mode 100644
index e152ecb0725aa6446d7f4a62cbdb3a50e5dd78f4..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/CustomForm/types.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import PropTypes from "prop-types";
-import {
-  BaseFieldDefinition,
-  FieldName,
-  DefaultFieldValue,
-  FieldValues,
-  FormFieldDefinition,
-  FormField,
-  PopulatedFormObject,
-} from "metabase-types/forms/legacy";
-
-export interface BaseFormProps {
-  formKey?: string;
-  formObject: PopulatedFormObject;
-
-  formFields: FormFieldDefinition[];
-  formFieldsByName: Record<FieldName, FormFieldDefinition>;
-  disablePristineSubmit?: boolean;
-
-  fields: Record<string, FormField>;
-  values: FieldValues;
-  errors: Record<FieldName, string>;
-
-  active?: boolean;
-  asyncValidating?: boolean;
-  dirty: boolean;
-  error?: string;
-  invalid: boolean;
-  overwriteOnInitialValuesChange?: boolean;
-  pristine: boolean;
-  readonly?: boolean;
-  submitFailed: boolean;
-  submitting: boolean;
-  valid: boolean;
-
-  asyncValidate: () => void;
-  destroyForm: () => void;
-  handleSubmit: () => void;
-  initializeForm: () => void;
-  onChangeField: (fieldName: FieldName, value: DefaultFieldValue) => void;
-  onSubmitSuccess: () => void;
-  resetForm: () => void;
-}
-
-type RenderSubmitProps = {
-  title: React.ReactNode;
-  canSubmit: boolean;
-  handleSubmit: () => void;
-};
-
-export interface OptionalFormViewProps {
-  submitTitle?: string;
-  renderSubmit?: (props: RenderSubmitProps) => JSX.Element;
-  className?: string;
-  style?: React.CSSProperties;
-}
-
-export interface CustomFormLegacyContext
-  extends OptionalFormViewProps,
-    Pick<
-      BaseFormProps,
-      | "formFields"
-      | "formFieldsByName"
-      | "disablePristineSubmit"
-      | "handleSubmit"
-      | "fields"
-      | "values"
-      | "submitting"
-      | "invalid"
-      | "pristine"
-      | "error"
-      | "onChangeField"
-    > {}
-
-export interface FormContainerLegacyContext {
-  registerFormField: (fieldDef: BaseFieldDefinition) => void;
-  unregisterFormField: (fieldDef: BaseFieldDefinition) => void;
-}
-
-export const LegacyContextTypes = {
-  handleSubmit: PropTypes.func,
-  submitTitle: PropTypes.string,
-  renderSubmit: PropTypes.func,
-  className: PropTypes.string,
-  style: PropTypes.object,
-  fields: PropTypes.object,
-  formFields: PropTypes.array,
-  formFieldsByName: PropTypes.object,
-  values: PropTypes.object,
-  submitting: PropTypes.bool,
-  invalid: PropTypes.bool,
-  pristine: PropTypes.bool,
-  error: PropTypes.string,
-  onChangeField: PropTypes.func,
-  disablePristineSubmit: PropTypes.bool,
-};
diff --git a/frontend/src/metabase/components/form/FormField/FormField.styled.tsx b/frontend/src/metabase/components/form/FormField/FormField.styled.tsx
deleted file mode 100644
index f5e8e420e1a941881b08132adfd410b36f27171d..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/FormField/FormField.styled.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import styled from "@emotion/styled";
-import { css } from "@emotion/react";
-import { color } from "metabase/lib/colors";
-import Icon from "metabase/components/Icon";
-
-export const FieldRow = styled.div`
-  display: flex;
-  align-items: center;
-  margin-bottom: 0.5em;
-`;
-
-export const Label = styled.label<{
-  horizontal?: boolean;
-  standAlone?: boolean;
-}>`
-  margin-bottom: 0;
-  ${props =>
-    props.horizontal &&
-    css`
-      margin-right: auto;
-    `}
-  ${props =>
-    props.standAlone &&
-    css`
-      margin-top: 0.8em;
-    `}
-`;
-
-Label.defaultProps = { className: "Form-label" };
-
-export const InfoIcon = styled(Icon)`
-  margin-left: 8px;
-  color: ${color("bg-dark")};
-
-  &:hover {
-    color: ${() => color("brand")};
-  }
-`;
-
-export const InfoLabel = styled.span`
-  color: ${color("text-medium")};
-  font-size: 0.88em;
-  margin-left: auto;
-  cursor: default;
-`;
-
-export const FieldContainer = styled.div<{
-  horizontal?: boolean;
-  align?: "left" | "right";
-}>`
-  width: 100%;
-  margin-right: ${props => (props.horizontal ? "1rem" : "")};
-  margin-left: ${props => (props.align === "left" ? "0.5rem" : "")};
-`;
-
-export const InputContainer = styled.div`
-  flex-shrink: 0;
-`;
diff --git a/frontend/src/metabase/components/form/FormField/FormField.tsx b/frontend/src/metabase/components/form/FormField/FormField.tsx
deleted file mode 100644
index 1c2aed106c8f150f0cc2c7a304bf19b9e0078b83..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/FormField/FormField.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import React from "react";
-
-import {
-  FieldName,
-  FieldValues,
-  FormField as FormFieldType,
-  BaseFieldDefinition,
-  FormFieldDefinition,
-} from "metabase-types/forms/legacy";
-
-import FormFieldView from "./FormFieldView";
-
-type ReduxFormProps = Pick<FormFieldType, "name"> &
-  Partial<Pick<FormFieldType, "error" | "visited" | "active">>;
-
-interface FormFieldProps extends BaseFieldDefinition, ReduxFormProps {
-  field: FormFieldType;
-  formField: FormFieldDefinition;
-  values: FieldValues;
-  className?: string;
-  children: React.ReactNode;
-  onChangeField: (fieldName: FieldName, value: unknown) => void;
-}
-
-const ALL_DOT_CHARS = /\./g;
-
-function getFieldId(formFieldName: FieldName) {
-  return `formField-${formFieldName.replace(ALL_DOT_CHARS, "-")}`;
-}
-
-function getDescriptionPositionPropValue(
-  descriptionPosition?: "top" | "bottom",
-  formField?: FormFieldDefinition,
-) {
-  return descriptionPosition ?? formField?.descriptionPosition ?? "top";
-}
-
-function getHiddenPropValue(hidden?: boolean, formField?: FormFieldDefinition) {
-  if (typeof hidden === "boolean") {
-    return hidden;
-  }
-  if (formField) {
-    return formField.hidden || formField.type === "hidden";
-  }
-  return false;
-}
-
-function getHorizontalPropValue(
-  horizontal?: boolean,
-  formField?: FormFieldDefinition,
-) {
-  if (typeof horizontal === "boolean") {
-    return horizontal;
-  }
-  if (formField) {
-    return formField.horizontal || formField.type === "boolean";
-  }
-  return false;
-}
-
-/**
- * @deprecated
- */
-function FormField({
-  className,
-  formField,
-  children,
-  ...props
-}: FormFieldProps) {
-  const title = props.title ?? formField?.title;
-  const type = props.type ?? formField.type;
-  const description = props.description ?? formField?.description;
-  const descriptionPosition = getDescriptionPositionPropValue(
-    props.descriptionPosition,
-    formField,
-  );
-
-  const info = props.info ?? formField?.info;
-  const infoLabel = props.infoLabel ?? formField?.infoLabel;
-  const infoLabelTooltip =
-    props.infoLabelTooltip ?? formField?.infoLabelTooltip;
-
-  const align = props.align ?? formField?.align ?? "right";
-  const hidden = getHiddenPropValue(props.hidden, formField);
-  const horizontal = getHorizontalPropValue(props.horizontal, formField);
-
-  const isToggle = type === "boolean";
-  const standAloneLabel = isToggle && align === "right" && !description;
-
-  if (hidden) {
-    return null;
-  }
-
-  const {
-    name,
-    error: errorProp,
-    visited,
-    active,
-  } = {
-    ...(props.field || {}),
-    ...props,
-  };
-
-  const shouldShowError = visited && !active;
-  const error = !shouldShowError ? undefined : errorProp;
-
-  return (
-    <FormFieldView
-      fieldId={getFieldId(name)}
-      className={className}
-      name={name}
-      error={error}
-      title={title}
-      description={description}
-      descriptionPosition={descriptionPosition}
-      info={info}
-      infoLabel={infoLabel}
-      infoLabelTooltip={infoLabelTooltip}
-      align={align}
-      standAloneLabel={standAloneLabel}
-      horizontal={horizontal}
-    >
-      {children}
-    </FormFieldView>
-  );
-}
-
-export default FormField;
diff --git a/frontend/src/metabase/components/form/FormField/FormFieldDescription.tsx b/frontend/src/metabase/components/form/FormField/FormFieldDescription.tsx
deleted file mode 100644
index 8f0f1af812889f476a53d566ab282afd7b251da8..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/FormField/FormFieldDescription.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from "react";
-
-interface FormFieldDescriptionProps {
-  className: string;
-  description: string;
-}
-
-export const FormFieldDescription = ({
-  className,
-  description,
-}: FormFieldDescriptionProps) => {
-  if (typeof description === "string") {
-    return (
-      <div
-        className={className}
-        dangerouslySetInnerHTML={{
-          __html: description,
-        }}
-      />
-    );
-  }
-  return <div className={className}>{description}</div>;
-};
diff --git a/frontend/src/metabase/components/form/FormField/FormFieldView.tsx b/frontend/src/metabase/components/form/FormField/FormFieldView.tsx
deleted file mode 100644
index d595bb9d350a07ac2958b68fbef880e378d66913..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/FormField/FormFieldView.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import React from "react";
-import cx from "classnames";
-
-import Tooltip from "metabase/components/Tooltip";
-
-import { BaseFieldDefinition } from "metabase-types/forms/legacy";
-
-import { FormFieldDescription } from "./FormFieldDescription";
-import {
-  FieldRow,
-  Label,
-  InfoIcon,
-  InputContainer,
-  FieldContainer,
-  InfoLabel,
-} from "./FormField.styled";
-
-interface FormFieldViewProps extends BaseFieldDefinition {
-  fieldId: string;
-  error?: string;
-  className?: string;
-  standAloneLabel?: boolean;
-  children: React.ReactNode;
-}
-
-function FormFieldView({
-  fieldId,
-  className,
-  name,
-  error,
-  title,
-  description,
-  descriptionPosition,
-  info,
-  infoLabel,
-  infoLabelTooltip,
-  align,
-  horizontal,
-  standAloneLabel,
-  children,
-}: FormFieldViewProps) {
-  const rootClassNames = cx("Form-field", className, {
-    "Form--fieldError": !!error,
-    flex: horizontal,
-  });
-
-  return (
-    <div id={fieldId} className={rootClassNames}>
-      {align === "left" && <InputContainer>{children}</InputContainer>}
-      {(title || description) && (
-        <FieldContainer horizontal={horizontal} align={align}>
-          <FieldRow>
-            {title && (
-              <Label
-                id={`${name}-label`}
-                htmlFor={name}
-                horizontal={horizontal}
-                standAlone={standAloneLabel}
-              >
-                {title}
-                {error && <span className="text-error">: {error}</span>}
-              </Label>
-            )}
-            {info && (
-              <Tooltip tooltip={info}>
-                <InfoIcon name="info" size={12} />
-              </Tooltip>
-            )}
-            {infoLabel && (
-              <Tooltip tooltip={infoLabelTooltip} maxWidth="100%">
-                <InfoLabel>{infoLabel}</InfoLabel>
-              </Tooltip>
-            )}
-          </FieldRow>
-          {description && descriptionPosition === "top" && (
-            <FormFieldDescription className="mb1" description={description} />
-          )}
-        </FieldContainer>
-      )}
-      {align !== "left" && <InputContainer>{children}</InputContainer>}
-      {description && descriptionPosition === "bottom" && (
-        <FormFieldDescription className="mt1" description={description} />
-      )}
-    </div>
-  );
-}
-
-export default FormFieldView;
diff --git a/frontend/src/metabase/components/form/FormField/index.ts b/frontend/src/metabase/components/form/FormField/index.ts
deleted file mode 100644
index e2d6f74d4381bd3560ca0441c57cba1acd4dc081..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/FormField/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./FormField";
diff --git a/frontend/src/metabase/components/form/FormikCustomForm/CustomFormField.tsx b/frontend/src/metabase/components/form/FormikCustomForm/CustomFormField.tsx
index 072678a5f5b985ec9fb294f07d7fb82a98ed3cec..0c13bcc1a9da042136edc652d8d9921c33edc100 100644
--- a/frontend/src/metabase/components/form/FormikCustomForm/CustomFormField.tsx
+++ b/frontend/src/metabase/components/form/FormikCustomForm/CustomFormField.tsx
@@ -1,5 +1,4 @@
 import React, { useCallback, useMemo } from "react";
-import { getIn } from "icepick";
 import _ from "underscore";
 
 import { isCustomWidget } from "metabase-types/guards";
diff --git a/frontend/src/metabase/components/form/StandardForm.tsx b/frontend/src/metabase/components/form/StandardForm.tsx
deleted file mode 100644
index 2e9df996f60019bb99884bcd3346bc8bad94611c..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/form/StandardForm.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from "react";
-import { t } from "ttag";
-
-import { BaseFormProps } from "./CustomForm/types";
-import { CustomFormFooterProps } from "./CustomForm/CustomFormFooter";
-import CustomForm from "./CustomForm";
-
-interface Props extends BaseFormProps, CustomFormFooterProps {
-  submitFullWidth?: boolean;
-  onClose?: () => void;
-}
-
-/**
- * @deprecated
- */
-const StandardForm = ({
-  submitTitle,
-  submitFullWidth,
-  onClose,
-  ...props
-}: Props) => (
-  <CustomForm {...props}>
-    {({ values, formFields, Form, FormField, FormFooter }) => (
-      <Form>
-        {formFields.map(formField => (
-          <FormField key={formField.name} name={formField.name} />
-        ))}
-        <FormFooter
-          isModal={props.isModal}
-          footerExtraButtons={props.footerExtraButtons}
-          onCancel={onClose}
-          submitTitle={
-            submitTitle || (values.id != null ? t`Update` : t`Create`)
-          }
-          fullWidth={submitFullWidth}
-        />
-      </Form>
-    )}
-  </CustomForm>
-);
-
-export default StandardForm;
diff --git a/frontend/src/metabase/containers/Form.info.js b/frontend/src/metabase/containers/Form.info.js
deleted file mode 100644
index 92187203ae169a98f95934f475b5131f0f158f7e..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/containers/Form.info.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from "react";
-import Form, { FormField, FormFooter } from "metabase/containers/Form";
-import validate from "metabase/lib/validate";
-
-const FORM = {
-  fields: [
-    {
-      name: "email",
-      placeholder: "bob@metabase.com",
-      validate: validate.required().email(),
-    },
-    {
-      name: "password",
-      type: "password",
-      validate: validate.required().passwordComplexity(),
-    },
-  ],
-};
-
-export const component = Form;
-export const category = "form";
-export const description = `A standard form component.`;
-export const examples = {
-  "with form prop": (
-    <Form form={FORM} onSubmit={values => alert(JSON.stringify(values))} />
-  ),
-  "with inline fields": (
-    <Form onSubmit={values => alert(JSON.stringify(values))}>
-      <FormField
-        name="email"
-        placeholder="bob@metabase.com"
-        validate={validate.required()}
-      />
-      <FormField
-        name="password"
-        type="password"
-        validate={validate.required().passwordComplexity()}
-      />
-      <FormFooter />
-    </Form>
-  ),
-  "with form prop and custom layout fields": (
-    <Form form={FORM} onSubmit={values => alert(JSON.stringify(values))}>
-      <FormField name="password" />
-      <FormField name="email" />
-      <FormFooter />
-    </Form>
-  ),
-};
diff --git a/frontend/src/metabase/containers/Form.jsx b/frontend/src/metabase/containers/Form.jsx
deleted file mode 100644
index 53ad7c61952b2783bc5a2130c4229ebe320c0a37..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/containers/Form.jsx
+++ /dev/null
@@ -1,299 +0,0 @@
-/* eslint-disable react/prop-types */
-import React from "react";
-import PropTypes from "prop-types";
-
-import { connect } from "react-redux";
-import { createSelector } from "reselect";
-import { reduxForm, getValues, initialize, change } from "redux-form";
-import { assocIn } from "icepick";
-import _ from "underscore";
-import { t } from "ttag";
-
-import CustomForm from "metabase/components/form/CustomForm";
-import StandardForm from "metabase/components/form/StandardForm";
-
-export {
-  CustomFormField as FormField,
-  CustomFormSubmit as FormSubmit,
-  CustomFormMessage as FormMessage,
-  CustomFormFooter as FormFooter,
-  CustomFormSection as FormSection,
-} from "metabase/components/form/CustomForm";
-
-import { makeFormObject, getValue } from "./formUtils";
-
-let FORM_ID = 0;
-// use makeMapStateToProps so each component gets it's own unique formId
-const makeMapStateToProps = () => {
-  const formId = FORM_ID++;
-  return (state, ownProps) => {
-    const formName = ownProps.formName || `form_${formId}`;
-    return {
-      formName: formName,
-      values: getValues(state.form[formName]),
-    };
-  };
-};
-
-const ReduxFormComponent = reduxForm()(
-  ({ handleSubmit, submitState, ...props }) => {
-    const FormComponent =
-      props.formComponent || (props.children ? CustomForm : StandardForm);
-    return (
-      <FormComponent
-        {...props}
-        handleSubmit={async (...args) => {
-          await handleSubmit(...args);
-          // normally handleSubmit swallows the result/error, but we want to make it available to things like ActionButton
-          if (submitState.failed) {
-            throw submitState.result;
-          } else {
-            return submitState.result;
-          }
-        }}
-      />
-    );
-  },
-);
-
-/**
- * @deprecated
- */
-class Form extends React.Component {
-  _state = {
-    submitting: false,
-    failed: false,
-    result: undefined,
-  };
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      // fields defined via child FormField elements
-      inlineFields: {},
-    };
-
-    // memoized functions
-    const getFormDefinition = createSelector(
-      [
-        (state, props) => props.form,
-        (state, props) => props.validate,
-        (state, props) => props.initial,
-        (state, props) => props.normalize,
-        (state, props) => props.fields,
-        (state, props) => state.inlineFields,
-      ],
-      (form, validate, initial, normalize, fields, inlineFields) => {
-        // use props.form if provided, otherwise generate from props.{fields,initial,validate,normalize}
-        const formDef = form || {
-          validate,
-          initial,
-          normalize,
-          fields: fields || Object.values(inlineFields),
-        };
-        return {
-          ...formDef,
-          fields: (...args) =>
-            // merge inlineFields in
-            getValue(formDef.fields, ...args).map(fieldDef => ({
-              ...fieldDef,
-              ...inlineFields[fieldDef.name],
-            })),
-        };
-      },
-    );
-    const getFormObject = createSelector([getFormDefinition], formDef =>
-      makeFormObject(formDef),
-    );
-    const getInitialValues = createSelector(
-      [
-        getFormObject,
-        (state, props) => props.initialValues || {},
-        (state, props) => props.values || {},
-      ],
-      (formObject, initialValues, values) => {
-        const formInitialValues = formObject.initial(values);
-        // merge nested fields: {details: {foo: 123}} + {details: {bar: 321}} => {details: {foo: 123, bar: 321}}
-        const merged = {};
-        for (const k of Object.keys(initialValues)) {
-          if (
-            typeof initialValues[k] === "object" &&
-            typeof formInitialValues[k] === "object"
-          ) {
-            merged[k] = { ...formInitialValues[k], ...initialValues[k] };
-          }
-        }
-        return {
-          ...initialValues,
-          ...formInitialValues,
-          ...merged,
-        };
-      },
-    );
-    const getFieldNames = createSelector(
-      [getFormObject, getInitialValues, (state, props) => props.values || {}],
-      (formObject, initialValues, values) =>
-        formObject.fieldNames({
-          ...initialValues,
-          ...values,
-        }),
-    );
-    this._getFormObject = () => getFormObject(this.state, this.props);
-    this._getFormDefinition = () => getFormDefinition(this.state, this.props);
-    this._getInitialValues = () => getInitialValues(this.state, this.props);
-    this._getFieldNames = () => getFieldNames(this.state, this.props);
-  }
-
-  static propTypes = {
-    form: PropTypes.object,
-    onSubmit: PropTypes.func.isRequired,
-    initialValues: PropTypes.object,
-    formName: PropTypes.string,
-    overwriteOnInitialValuesChange: PropTypes.bool,
-  };
-
-  static defaultProps = {
-    overwriteOnInitialValuesChange: false,
-  };
-
-  static childContextTypes = {
-    registerFormField: PropTypes.func,
-    unregisterFormField: PropTypes.func,
-    fieldNames: PropTypes.array,
-  };
-
-  componentDidUpdate(prevProps, prevState) {
-    // HACK: when new fields are added they aren't initialized with their intialValues, so we have to force it here:
-    const newFields = _.difference(
-      Object.keys(this.state.inlineFields),
-      Object.keys(prevState.inlineFields),
-    );
-    if (newFields.length > 0) {
-      this.props.dispatch(
-        initialize(this.props.formName, this._getInitialValues(), newFields),
-      );
-    }
-    this.props.onChange?.(this.props.values);
-  }
-
-  _registerFormField = field => {
-    if (!_.isEqual(this.state.inlineFields[field.name], field)) {
-      this.setState(prevState =>
-        assocIn(prevState, ["inlineFields", field.name], field),
-      );
-    }
-  };
-
-  _unregisterFormField = field => {
-    if (this.state.inlineFields[field.name]) {
-      // this.setState(prevState =>
-      //   dissocIn(prevState, ["inlineFields", field.name]),
-      // );
-    }
-  };
-
-  getChildContext() {
-    return {
-      registerFormField: this._registerFormField,
-      unregisterFormField: this._unregisterFormField,
-    };
-  }
-
-  _validate = (values, props) => {
-    // HACK: clears failed state for global error
-    if (!this._state.submitting && this._state.failed) {
-      this._state.failed = false;
-      props.stopSubmit();
-    }
-    const formObject = this._getFormObject();
-    return formObject.validate(values, props);
-  };
-
-  _onSubmit = async values => {
-    const formObject = this._getFormObject();
-    // HACK: clears failed state for global error
-    this._state.submitting = true;
-    try {
-      const normalized = formObject.normalize(values);
-      return (this._state.result = await this.props.onSubmit(normalized));
-    } catch (error) {
-      console.error("Form submission error:", error);
-      this._state.failed = true;
-      this._state.result = error;
-      // redux-form expects { "FIELD NAME": "FIELD ERROR STRING" } or {"_error": "GLOBAL ERROR STRING" }
-      if (error && error.data && error.data.errors) {
-        try {
-          // HACK: blur the current element to ensure we show the error
-          document.activeElement.blur();
-        } catch (e) {}
-        // if there are errors for fields we don't know about then inject a generic top-level _error key
-        const fieldNames = new Set(this._getFieldNames());
-        const errorNames = Object.keys(error.data.errors);
-        const hasUnknownFields = errorNames.some(name => !fieldNames.has(name));
-        throw {
-          _error:
-            error.data?.message ||
-            error.message ||
-            (hasUnknownFields ? t`An error occurred` : null),
-          ...error.data.errors,
-        };
-      } else if (error) {
-        throw {
-          _error:
-            error.data?.message ||
-            error.message ||
-            error.data ||
-            t`An error occurred`,
-        };
-      }
-    } finally {
-      setTimeout(() => (this._state.submitting = false));
-    }
-  };
-
-  _handleSubmitSuccess = async action => {
-    if (this.props.onSubmitSuccess) {
-      await this.props.onSubmitSuccess(action);
-    }
-    this.props.dispatch(
-      initialize(this.props.formName, this.props.values, this._getFieldNames()),
-    );
-  };
-
-  _handleChangeField = (fieldName, value) => {
-    return this.props.dispatch(change(this.props.formName, fieldName, value));
-  };
-
-  render() {
-    // eslint-disable-next-line
-    const { formName, overwriteOnInitialValuesChange } = this.props;
-    const formObject = this._getFormObject();
-    const initialValues = this._getInitialValues();
-    const fieldNames = this._getFieldNames();
-    return (
-      <ReduxFormComponent
-        {...this.props}
-        overwriteOnInitialValuesChange={overwriteOnInitialValuesChange}
-        formObject={formObject}
-        // redux-form props:
-        form={formName}
-        fields={fieldNames}
-        initialValues={initialValues}
-        validate={this._validate}
-        onSubmit={this._onSubmit}
-        onSubmitSuccess={this._handleSubmitSuccess}
-        onChangeField={this._handleChangeField}
-        // HACK: _state is a mutable object so we can pass by reference into the ReduxFormComponent
-        submitState={this._state}
-      />
-    );
-  }
-}
-
-/**
- * @deprecated
- */
-const DeprecatedForm = connect(makeMapStateToProps)(Form);
-
-export default DeprecatedForm;
diff --git a/frontend/src/metabase/entities/containers/EntityForm.jsx b/frontend/src/metabase/entities/containers/EntityForm.jsx
index 23d39bb811891fc5c2dd43b47071ddac9893b31d..1fbda889dca5a2b7071b1e54c6d89a61ead96419 100644
--- a/frontend/src/metabase/entities/containers/EntityForm.jsx
+++ b/frontend/src/metabase/entities/containers/EntityForm.jsx
@@ -2,7 +2,6 @@
 import React from "react";
 import { t } from "ttag";
 
-import LegacyForm from "metabase/containers/Form";
 import Form from "metabase/containers/FormikForm";
 import ModalContent from "metabase/components/ModalContent";
 
@@ -22,21 +21,8 @@ const EForm = ({
   create,
   onSubmit = object => (object.id ? update(object) : create(object)),
   onSaved,
-  useLegacyForm,
   ...props
 }) => {
-  if (useLegacyForm) {
-    return (
-      <LegacyForm
-        {...props}
-        form={form}
-        initialValues={entityObject}
-        onSubmit={onSubmit}
-        onSubmitSuccess={action => onSaved && onSaved(action.payload.object)}
-      />
-    );
-  }
-
   return (
     <Form
       {...props}
diff --git a/frontend/src/metabase/entities/databases.js b/frontend/src/metabase/entities/databases.js
index bf6cfe5eeff21735fd5e5fcfae1494298dd7cf1f..6925a42ef7161146a009812058ca0e73b0d84735 100644
--- a/frontend/src/metabase/entities/databases.js
+++ b/frontend/src/metabase/entities/databases.js
@@ -14,8 +14,6 @@ import Schemas from "metabase/entities/schemas";
 
 import { getMetadata, getFields } from "metabase/selectors/metadata";
 
-import forms from "./databases/forms";
-
 // OBJECT ACTIONS
 export const FETCH_DATABASE_METADATA =
   "metabase/entities/database/FETCH_DATABASE_METADATA";
@@ -89,9 +87,6 @@ const Databases = createEntity({
         }),
     ),
   },
-
-  // FORM
-  forms,
 });
 
 export default Databases;
diff --git a/frontend/src/metabase/entities/databases/big-query-fields.js b/frontend/src/metabase/entities/databases/big-query-fields.js
deleted file mode 100644
index d616c87ac2aa61e189e4efac251775136edd6909..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/entities/databases/big-query-fields.js
+++ /dev/null
@@ -1,179 +0,0 @@
-/* eslint-disable react/prop-types */
-import React from "react";
-import { t, jt } from "ttag";
-
-import { color } from "metabase/lib/colors";
-import ExternalLink from "metabase/core/components/ExternalLink";
-import Link from "metabase/core/components/Link";
-
-import MetadataSyncScheduleWidget from "metabase/admin/databases/components/widgets/MetadataSyncScheduleWidget";
-import CacheFieldValuesScheduleWidget from "metabase/admin/databases/components/widgets/CacheFieldValuesScheduleWidget";
-
-const SERVICE_ACCOUNT_DOCS_URL =
-  "https://developers.google.com/identity/protocols/OAuth2ServiceAccount";
-function BigQueryServiceAccountToggle({
-  field: { value, onChange },
-  values: { details },
-}) {
-  const saLink = (
-    <ExternalLink
-      href={SERVICE_ACCOUNT_DOCS_URL}
-    >{t`Service Accounts`}</ExternalLink>
-  );
-
-  const hasNoOldStyleData = ["client-id", "client-secret"].every(
-    key => details[key] == null,
-  );
-
-  return (!value && hasNoOldStyleData) || value === true ? (
-    <div>
-      <p>{jt`Metabase connects to Big Query via ${saLink}.`}</p>
-      {value === true && (
-        <Link className="link" onClick={() => onChange(false)}>
-          {t`Continue using an OAuth application to connect`}
-        </Link>
-      )}
-    </div>
-  ) : (
-    <div
-      style={{
-        borderLeftWidth: 3,
-        borderLeftStyle: "solid",
-        borderLeftColor: color("brand"),
-      }}
-      className="pl1"
-    >
-      <p>
-        {jt`We recommend switching to use ${saLink} instead of an OAuth application to connect to BigQuery`}
-      </p>
-      <Link
-        className="link"
-        onClick={() => onChange(true)}
-      >{t`Connect to a Service Account instead`}</Link>
-    </div>
-  );
-}
-
-export default function getFieldsForBigQuery(details) {
-  const useServiceAccount =
-    // If this field is unset, show the service account form unless an old-style connection exists.
-    details["use-service-account"] == null
-      ? ["client-id", "client-secret"].every(key => details[key] == null)
-      : details["use-service-account"];
-  return {
-    "details-fields": [
-      {
-        name: "use-service-account",
-        type: BigQueryServiceAccountToggle,
-        hidden: true,
-      },
-      ...(useServiceAccount
-        ? []
-        : [
-            {
-              name: "project-id",
-              "display-name": t`Project ID`,
-              "helper-text": t`Project ID to be used for authentication. You can omit this field if you are only querying datasets owned by your organization.`,
-              placeholder: "1w08oDRKPrOqBt06yxY8uiCz2sSvOp3u",
-              required: true,
-            },
-          ]),
-      {
-        name: "dataset-id",
-        "display-name": t`Dataset ID`,
-        "helper-text": t`Make sure to leave out the Project ID prefix in "project_name:dataset_id" and only enter “dataset_id”`,
-        placeholder: "dataset_id",
-        required: true,
-      },
-      ...(useServiceAccount
-        ? [
-            {
-              name: "service-account-json",
-              "display-name": t`Service account JSON file`,
-              "helper-text": t`This JSON file contains the credentials Metabase needs to read and query your dataset.`,
-              type: "textFile",
-              required: true,
-            },
-          ]
-        : [
-            {
-              name: "client-id",
-              "display-name": t`Client ID`,
-              placeholder:
-                "1201327674725-y6ferb0feo1hfssr7t40o4aikqll46d4.apps.googleusercontent.com",
-              required: true,
-            },
-            {
-              name: "client-secret",
-              "display-name": t`Client Secret`,
-              placeholder: "dJNi4utWgMzyIFo2JbnsK6Np",
-              required: true,
-            },
-            {
-              name: "auth-code",
-              "display-name": t`Auth Code`,
-              placeholder: "4/HSk-KtxkSzTt61j5zcbee2Rmm5JHkRFbL5gD5lgkXek",
-              required: true,
-            },
-          ]),
-      {
-        name: "advanced-options",
-        type: "section",
-        default: false,
-      },
-      {
-        name: "use-jvm-timezone",
-        "display-name": t`Use the Java Virtual Machine (JVM) timezone`,
-        default: false,
-        type: "boolean",
-        "visible-if": { "advanced-options": true },
-      },
-      {
-        name: "include-user-id-and-hash",
-        "display-name": t`Include User ID and query hash in queries`,
-        default: true,
-        type: "boolean",
-        "visible-if": { "advanced-options": true },
-      },
-      {
-        name: "auto_run_queries",
-        type: "boolean",
-        default: true,
-        "display-name": t`Rerun queries for simple explorations`,
-        description: t`We execute the underlying query when you explore data using Summarize or Filter. This is on by default but you can turn it off if performance is slow.`,
-        "visible-if": { "advanced-options": true },
-      },
-      {
-        name: "let-user-control-scheduling",
-        type: "boolean",
-        "display-name": t`Choose when syncs and scans happen`,
-        description: t`By default, Metabase does a lightweight hourly sync and an intensive daily scan of field values. If you have a large database, turn this on to make changes.`,
-        "visible-if": { "advanced-options": true },
-      },
-      {
-        name: "schedules.metadata_sync",
-        "display-name": t`Database syncing`,
-        type: MetadataSyncScheduleWidget,
-        description: t`This is a lightweight process that checks for updates to this database’s schema. In most cases, you should be fine leaving this set to sync hourly.`,
-        "visible-if": { "let-user-control-scheduling": true },
-      },
-      {
-        name: "schedules.cache_field_values",
-        "display-name": t`Scanning for Filter Values`,
-        type: CacheFieldValuesScheduleWidget,
-        description:
-          t`Metabase can scan the values present in each field in this database to enable checkbox filters in dashboards and questions. This can be a somewhat resource-intensive process, particularly if you have a very large database.` +
-          " " +
-          t`When should Metabase automatically scan and cache field values?`,
-        "visible-if": { "let-user-control-scheduling": true },
-      },
-      {
-        name: "refingerprint",
-        type: "boolean",
-        "display-name": t`Periodically refingerprint tables`,
-        description: t`This enables Metabase to scan for additional field values during syncs allowing smarter behavior, like improved auto-binning on your bar charts.`,
-        "visible-if": { "advanced-options": true },
-      },
-    ],
-  };
-}
diff --git a/frontend/src/metabase/entities/databases/forms.js b/frontend/src/metabase/entities/databases/forms.js
deleted file mode 100644
index 0193db9da655159f1fae400a87f7ef38eab8fd3d..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/entities/databases/forms.js
+++ /dev/null
@@ -1,401 +0,0 @@
-import React from "react";
-import { jt, t } from "ttag";
-
-import MetabaseSettings from "metabase/lib/settings";
-import { getElevatedEngines } from "metabase/lib/engine";
-import ExternalLink from "metabase/core/components/ExternalLink";
-import { PLUGIN_CACHING } from "metabase/plugins";
-
-import MetadataSyncScheduleWidget from "metabase/admin/databases/components/widgets/MetadataSyncScheduleWidget";
-import CacheFieldValuesScheduleWidget from "metabase/admin/databases/components/widgets/CacheFieldValuesScheduleWidget";
-import EngineWidget from "metabase/admin/databases/components/widgets/EngineWidget";
-import getFieldsForMongo from "./mongo-fields";
-import getFieldsForBigQuery from "./big-query-fields";
-
-const DATABASE_DETAIL_OVERRIDES = {
-  "tunnel-enabled": () => ({
-    title: t`Use an SSH-tunnel`,
-    description: getSshDescription(),
-  }),
-  "use-jvm-timezone": () => ({
-    title: t`Use the Java Virtual Machine (JVM) timezone`,
-    description: t`We suggest you leave this off unless you plan on doing a lot of manual timezone casting with this data.`,
-  }),
-  "include-user-id-and-hash": () => ({
-    title: t`Include User ID and query hash in queries`,
-    description: t`This can be useful for auditing and debugging, but prevents BigQuery from caching results and may increase your costs.`,
-  }),
-  "use-srv": () => ({
-    title: t`Connect using DNS SRV`,
-    description: t`If you're connecting to an Atlas cluster, you might need to turn this on. Note that your provided host must be a fully qualified domain name.`,
-  }),
-  "client-id": (engine, details) => ({
-    description: getClientIdDescription(engine, details),
-  }),
-  "auth-code": (engine, details) => ({
-    description: (
-      <div>
-        <div>{getAuthCodeLink(engine, details)}</div>
-      </div>
-    ),
-  }),
-  "service-account-json": (engine, details, id) => ({
-    validate: value => {
-      // this field is only required if this is a new entry
-      if (id) {
-        return null;
-      }
-
-      if (!value) {
-        return t`required`;
-      }
-      try {
-        JSON.parse(value);
-      } catch (e) {
-        return t`invalid JSON`;
-      }
-      return null;
-    },
-  }),
-  "tunnel-private-key": () => ({
-    title: t`SSH private key`,
-    placeholder: t`Paste the contents of your ssh private key here`,
-    type: "text",
-  }),
-  "tunnel-private-key-passphrase": () => ({
-    title: t`Passphrase for the SSH private key`,
-  }),
-  "tunnel-auth-option": () => ({
-    title: t`SSH authentication`,
-    options: [
-      { name: t`SSH Key`, value: "ssh-key" },
-      { name: t`Password`, value: "password" },
-    ],
-  }),
-  "ssl-cert": () => ({
-    title: t`Server SSL certificate chain`,
-    placeholder: t`Paste the contents of the server's SSL certificate chain here`,
-    type: "text",
-  }),
-  "ssl-key-options": engine => ({
-    description: getSslKeyOptionsDescription(engine),
-  }),
-  "schedules.metadata_sync": () => ({
-    name: "schedules.metadata_sync",
-    type: MetadataSyncScheduleWidget,
-    normalize: value => value,
-  }),
-  "schedules.cache_field_values": () => ({
-    name: "schedules.cache_field_values",
-    type: CacheFieldValuesScheduleWidget,
-    normalize: value => value,
-  }),
-  auto_run_queries: () => ({
-    name: "auto_run_queries",
-    initial: undefined,
-  }),
-  refingerprint: () => ({
-    name: "refingerprint",
-  }),
-};
-
-function getEngineName(engine) {
-  const engineInfo = ENGINES[engine];
-  return engineInfo != null ? engineInfo["driver-name"] : t`Database`;
-}
-
-function getEngineInfo(engine, details, id) {
-  const engineInfo = (MetabaseSettings.get("engines") || {})[engine];
-  switch (engine) {
-    // BigQuery has special logic to switch out forms depending on what style of authenication we use.
-    case "bigquery":
-      return getFieldsForBigQuery(details);
-    // Mongo has special logic to switch between a connection URI and broken out fields
-    case "mongo":
-      return getFieldsForMongo(details, engineInfo, id);
-    default:
-      return engineInfo;
-  }
-}
-
-function shouldShowEngineProvidedField(field, details) {
-  const detailAndValueRequiredToShowField = field.visibleIf;
-
-  if (detailAndValueRequiredToShowField) {
-    const pred = currentValue => {
-      const [detail, expectedDetailValue] = currentValue;
-
-      if (Array.isArray(expectedDetailValue)) {
-        // if the expectedDetailValue is itself an array, then consider the condition satisfied if any of those values
-        // match the current detail value
-        return expectedDetailValue.includes(details[detail]);
-      } else {
-        return details[detail] === expectedDetailValue;
-      }
-    };
-
-    // check all entries in the visible-if map, and only show this field if all key/values are satisfied
-    // (i.e. boolean AND)
-    return Object.entries(detailAndValueRequiredToShowField).every(pred);
-  }
-
-  return true;
-}
-
-function getSshDescription() {
-  const link = (
-    <ExternalLink href={MetabaseSettings.docsUrl("databases/ssh-tunnel")}>
-      {t`Learn more`}
-    </ExternalLink>
-  );
-
-  return jt`If a direct connection to your database isn't possible, you may want to use an SSH tunnel. ${link}.`;
-}
-
-function getSslKeyOptionsDescription(engine) {
-  if (engine !== "postgres") {
-    return null;
-  }
-
-  const link = (
-    <ExternalLink
-      href={MetabaseSettings.docsUrl(
-        "databases/connections/postgresql",
-        "authenticate-client-certificate",
-      )}
-    >
-      {t`Learn more`}
-    </ExternalLink>
-  );
-
-  return jt`If you have a PEM SSL client key, you can convert that key to the PKCS-8/DER format using OpenSSL. ${link}.`;
-}
-
-const AUTH_URL_PREFIXES = {
-  bigquery:
-    "https://accounts.google.com/o/oauth2/auth?redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/bigquery&client_id=",
-  bigquery_with_drive:
-    "https://accounts.google.com/o/oauth2/auth?redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/bigquery%20https://www.googleapis.com/auth/drive&client_id=",
-};
-
-const CREDENTIALS_URL_PREFIXES = {
-  bigquery:
-    "https://console.developers.google.com/apis/credentials/oauthclient?project=",
-};
-
-function concatTrimmed(a, b) {
-  return (a || "").trim() + (b || "").trim();
-}
-
-function getClientIdDescription(engine, details) {
-  if (CREDENTIALS_URL_PREFIXES[engine]) {
-    const credentialsURL = concatTrimmed(
-      CREDENTIALS_URL_PREFIXES[engine],
-      details["project-id"] || "",
-    );
-    return (
-      <span>
-        {jt`${(
-          <ExternalLink className="link" href={credentialsURL}>
-            {t`Click here`}
-          </ExternalLink>
-        )} to generate a Client ID and Client Secret for your project.`}{" "}
-        {t`Choose "Desktop App" as the application type. Name it whatever you'd like.`}
-      </span>
-    );
-  }
-}
-
-function getAuthCodeLink(engine, details) {
-  if (AUTH_URL_PREFIXES[engine] && details["client-id"]) {
-    const authCodeURL = concatTrimmed(
-      AUTH_URL_PREFIXES[engine],
-      details["client-id"],
-    );
-    const googleDriveAuthCodeURL = concatTrimmed(
-      AUTH_URL_PREFIXES["bigquery_with_drive"],
-      details["client-id"],
-    );
-    return (
-      <span>
-        {jt`${(
-          <ExternalLink href={authCodeURL}>{t`Click here`}</ExternalLink>
-        )} to get an auth code.`}
-        {engine === "bigquery" && (
-          <span>
-            {" "}
-            ({t`or`}{" "}
-            <ExternalLink href={googleDriveAuthCodeURL}>
-              {t`with Google Drive permissions`}
-            </ExternalLink>
-            )
-          </span>
-        )}
-      </span>
-    );
-  }
-}
-
-function getDefaultValue(field) {
-  return "default" in field ? field.default : null;
-}
-
-function normalizeFieldValue(value, field) {
-  if (value === "" || value == null) {
-    return getDefaultValue(field);
-  }
-
-  if (typeof value === "string" && field.type !== "password") {
-    const trimmedValue = value.trim();
-    return trimmedValue === "" ? getDefaultValue(field) : trimmedValue;
-  }
-
-  return value;
-}
-
-function getEngineFormFields(engine, details, id) {
-  const engineInfo = getEngineInfo(engine, details, id);
-  const engineFields = engineInfo ? engineInfo["details-fields"] : [];
-  const cachingField = getDatabaseCachingField();
-
-  // convert database details-fields to Form fields
-  return engineFields
-    .map(field => {
-      const overrides = DATABASE_DETAIL_OVERRIDES[field.name];
-
-      return {
-        name: `details.${field.name}`,
-        title: field["display-name"],
-        type: field.type,
-        description: field.description,
-        placeholder: field.placeholder || field.default,
-        options: field.options,
-        validate: value => (field.required && !value ? t`required` : null),
-        normalize: value => normalizeFieldValue(value, field),
-        horizontal: field.type === "boolean",
-        initial: field.default,
-        readOnly: field.readOnly || false,
-        helperText: field["helper-text"],
-        visibleIf: field["visible-if"],
-        treatBeforePosting: field["treat-before-posting"],
-        ...(overrides && overrides(engine, details, id)),
-      };
-    })
-    .concat(cachingField ? [cachingField] : [])
-    .filter(field => shouldShowEngineProvidedField(field, details));
-}
-
-const ENGINES = MetabaseSettings.get("engines", {});
-const ELEVATED_ENGINES = getElevatedEngines();
-
-const ENGINE_OPTIONS = Object.entries(ENGINES)
-  .map(([engine, info]) => ({
-    value: engine,
-    name: info["driver-name"],
-    official: info["official"] ?? true, // TODO remove default
-    index: ELEVATED_ENGINES.indexOf(engine),
-  }))
-  .sort((a, b) => a.name.localeCompare(b.name));
-
-// use top level constant for engines so we only need to compute these maps once
-const ENGINE_SUPERSEDES_MAPS = Object.keys(ENGINES).reduce(
-  (acc, engine) => {
-    const newEngine = ENGINES[engine]["superseded-by"];
-    if (newEngine) {
-      acc.supersedes[newEngine] = engine;
-      acc.superseded_by[engine] = newEngine;
-    }
-    return acc;
-  },
-  { supersedes: {}, superseded_by: {} },
-);
-
-/**
- * Returns the options to show in the engines selection widget. An engine is available to be selected if either
- *  - it is not superseded by any other engine
- *  - it is the selected engine (i.e. someone is already using it)
- *  - it is superseded by some engine, which happens to be the currently selected one
- *
- * The idea behind this behavior is to only show someone a "legacy" driver if they have at least selected the one that
- * will replace it first, at which point they can "fall back" on the legacy one if needed.
- *
- * @param currentEngine the current (selected engine)
- * @returns the filtered engine options to be shown in the selection widget
- */
-function getEngineOptions(currentEngine) {
-  return ENGINE_OPTIONS.filter(engine => {
-    const engineName = engine.value;
-    const newDriver = ENGINE_SUPERSEDES_MAPS["superseded_by"][engineName];
-    return (
-      typeof newDriver === "undefined" ||
-      newDriver === currentEngine ||
-      engineName === currentEngine
-    );
-  });
-}
-
-function getDatabaseCachingField() {
-  const hasField =
-    PLUGIN_CACHING.databaseCacheTTLFormField &&
-    MetabaseSettings.get("enable-query-caching");
-  return hasField ? PLUGIN_CACHING.databaseCacheTTLFormField : null;
-}
-
-const forms = {
-  details: {
-    fields: ({ id, engine, details = {} } = {}) =>
-      [
-        {
-          name: "engine",
-          title: t`Database type`,
-          type: "select",
-          options: getEngineOptions(engine),
-          placeholder: t`Select a database`,
-          isHosted: MetabaseSettings.isHosted(),
-        },
-        {
-          name: "name",
-          title: t`Display name`,
-          placeholder: t`Our ${getEngineName(engine)}`,
-          validate: value => !value && t`required`,
-          hidden: !engine,
-          helperText: t`Choose what this data will be called in Metabase.`,
-        },
-        ...(getEngineFormFields(engine, details, id) || []),
-        { name: "is_full_sync", type: "hidden" },
-        { name: "is_on_demand", type: "hidden" },
-      ].filter(Boolean),
-    normalize: function (database) {
-      if (!database.details["let-user-control-scheduling"]) {
-        // TODO Atte Keinänen 8/15/17: Implement engine-specific scheduling defaults
-        return {
-          ...database,
-          is_full_sync: true,
-        };
-      } else {
-        return database;
-      }
-    },
-  },
-};
-
-forms.setup = {
-  ...forms.details,
-  fields: (...args) =>
-    forms.details.fields(...args).map(field => ({
-      ...field,
-      type: field.name === "engine" ? EngineWidget : field.type,
-      title: field.name === "engine" ? null : field.title,
-      hidden: field.hidden || ADVANCED_FIELDS.has(field.name),
-    })),
-};
-
-const ADVANCED_FIELDS = new Set([
-  "auto_run_queries",
-  "details.let-user-control-scheduling",
-  "cache_ttl",
-]);
-
-export default forms;
-export const engineSupersedesMap = ENGINE_SUPERSEDES_MAPS;
-export const allEngines = ENGINES;
diff --git a/frontend/src/metabase/entities/databases/mongo-fields.js b/frontend/src/metabase/entities/databases/mongo-fields.js
deleted file mode 100644
index 59436e5dce9aa9cb60ccaed5623e7b9b92a9598d..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/entities/databases/mongo-fields.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/* eslint-disable react/prop-types */
-import React from "react";
-import { t } from "ttag";
-
-import Link from "metabase/core/components/Link";
-
-function MongoConnectionStringToggle({ field: { value, onChange } }) {
-  return (
-    <div>
-      <Link className="link" onClick={() => onChange(!value)}>
-        {value === false
-          ? t`Paste a connection string`
-          : t`Fill out individual fields`}
-      </Link>
-    </div>
-  );
-}
-
-export default function getFieldsForMongo(details, defaults, id) {
-  const useConnectionString =
-    details["use-conn-uri"] == null || details["use-conn-uri"];
-
-  const manualFields = [
-    "host",
-    "dbname",
-    "port",
-    "user",
-    "pass",
-    "authdb",
-    "additional-options",
-    "use-srv",
-    "ssl",
-  ];
-
-  const fields = defaults["details-fields"]
-    .filter(
-      field =>
-        !(
-          field["name"] === "use-conn-uri" ||
-          (useConnectionString && manualFields.includes(field["name"])) ||
-          (!useConnectionString && field["name"] === "conn-uri")
-        ),
-    )
-    .map(function (field) {
-      if (field["name"] === "conn-uri" && id) {
-        field.type = "password";
-      }
-      return field;
-    });
-
-  return {
-    "details-fields": [
-      {
-        name: "use-conn-uri",
-        type: MongoConnectionStringToggle,
-        hidden: true,
-        default: false,
-      },
-      ...fields,
-    ],
-  };
-}
diff --git a/frontend/src/metabase/store.js b/frontend/src/metabase/store.js
index 6fc0c30276159ea35a39b11943d1c83cbbd908bc..132a727d67ac1caf6d98d66470cf825151a1d49d 100644
--- a/frontend/src/metabase/store.js
+++ b/frontend/src/metabase/store.js
@@ -1,5 +1,4 @@
 import { combineReducers, applyMiddleware, createStore, compose } from "redux";
-import { reducer as form } from "redux-form";
 import { routerReducer as routing, routerMiddleware } from "react-router-redux";
 import promise from "redux-promise";
 import { PLUGIN_REDUX_MIDDLEWARES } from "metabase/plugins";
@@ -29,7 +28,6 @@ const devToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION__
 export function getStore(reducers, history, intialState, enhancer = a => a) {
   const reducer = combineReducers({
     ...reducers,
-    form,
     routing,
   });
 
diff --git a/frontend/src/metabase/writeback/components/WritebackForm.styled.tsx b/frontend/src/metabase/writeback/components/WritebackForm.styled.tsx
deleted file mode 100644
index b42b4dfeb6df2bdc0118e1b4398bda44135b5b4d..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/writeback/components/WritebackForm.styled.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import styled from "@emotion/styled";
-
-import Radio from "metabase/core/components/Radio";
-
-import Form from "metabase/containers/Form";
-
-export const StyledForm = styled(Form)`
-  ${Radio.RadioGroupVariants.join(", ")} {
-    flex-wrap: wrap;
-    gap: 0.5rem 0;
-  }
-`;
diff --git a/frontend/src/metabase/writeback/containers/WritebackForm.tsx b/frontend/src/metabase/writeback/containers/WritebackForm.tsx
index 7d4b2011c687da4297d6643bc133d533d6a93170..e9f62b66c357aac132e3af7533816cb2d18dc726 100644
--- a/frontend/src/metabase/writeback/containers/WritebackForm.tsx
+++ b/frontend/src/metabase/writeback/containers/WritebackForm.tsx
@@ -1,17 +1,5 @@
-import React, { useCallback, useMemo } from "react";
-import { t } from "ttag";
-
-import validate from "metabase/lib/validate";
-
-import { TYPE } from "metabase-lib/types/constants";
-import Field from "metabase-lib/metadata/Field";
 import Table from "metabase-lib/metadata/Table";
 
-import { StyledForm } from "../components/WritebackForm.styled";
-
-import { isEditableField } from "../utils";
-import CategoryFieldPicker from "./CategoryFieldPicker";
-
 export interface WritebackFormProps {
   table: Table;
   row?: unknown[];
@@ -23,127 +11,8 @@ export interface WritebackFormProps {
   isModal?: boolean;
 }
 
-function getFieldTypeProps(field: Field) {
-  if (field.isFK()) {
-    return {
-      type: field.isNumeric() ? "integer" : "input",
-    };
-  }
-  if (field.isNumeric()) {
-    return { type: "integer" };
-  }
-  if (field.isBoolean()) {
-    return { type: "boolean" };
-  }
-  if (field.isDate()) {
-    return { type: "date" };
-  }
-  if (field.semantic_type === TYPE.Email) {
-    return { type: "email" };
-  }
-  if (
-    field.semantic_type === TYPE.Description ||
-    field.semantic_type === TYPE.Comment
-  ) {
-    return { type: "text" };
-  }
-  if (field.semantic_type === TYPE.Title) {
-    return { type: "input" };
-  }
-  if (field.isCategory() && field.semantic_type !== TYPE.Name) {
-    return {
-      fieldInstance: field,
-      widget: CategoryFieldPicker,
-    };
-  }
-  return { type: "input" };
-}
-
-function getFieldValidationProp(field: Field) {
-  let validator = validate as any;
-
-  if (field.database_required) {
-    validator = validator.required();
-  }
-
-  return {
-    validate: validator,
-  };
-}
-
-function WritebackForm({
-  table,
-  row,
-  type = row ? "update" : "insert",
-  mode,
-  onSubmit,
-  ...props
-}: WritebackFormProps) {
-  const editableFields = useMemo(() => {
-    const fields = table.fields.filter(isEditableField);
-    if (mode === "bulk") {
-      // Ideally we need to filter out fields with 'unique' constraint
-      return fields.filter(field => !field.isPK());
-    }
-    return fields;
-  }, [table, mode]);
-
-  const form = useMemo(() => {
-    return {
-      fields: editableFields.map(field => {
-        const fieldIndex = table.fields.findIndex(f => f.id === field.id);
-        const initialValue = row ? row[fieldIndex] : undefined;
-
-        let title = field.displayName();
-        if (field.database_required) {
-          title += " (" + t`required` + ")";
-        }
-
-        return {
-          name: field.name,
-          title,
-          description: field.description,
-          initial: initialValue,
-          ...getFieldTypeProps(field),
-          ...getFieldValidationProp(field),
-        };
-      }),
-    };
-  }, [table, row, editableFields]);
-
-  const handleSubmit = useCallback(
-    values => {
-      const isUpdate = type === "update";
-      const changes = isUpdate ? {} : values;
-
-      if (isUpdate) {
-        const fields = form.fields;
-
-        // makes sure we only pass fields that were actually changed
-        Object.keys(values).forEach(fieldName => {
-          const field = fields.find(field => field.name === fieldName);
-          const hasChanged = !field || field.initial !== values[fieldName];
-          if (hasChanged) {
-            changes[fieldName] = values[fieldName];
-          }
-        });
-      }
-
-      return onSubmit?.(changes);
-    },
-    [form, type, onSubmit],
-  );
-
-  const submitTitle = type === "update" ? t`Update` : t`Create`;
-
-  return (
-    <StyledForm
-      {...props}
-      form={form}
-      onSubmit={handleSubmit}
-      submitTitle={submitTitle}
-    />
-  );
+function WritebackForm(props: WritebackFormProps) {
+  return null;
 }
 
 export default WritebackForm;
diff --git a/frontend/test/__support__/ui.js b/frontend/test/__support__/ui.js
index 80be3c3ddb68431cd5c264f46b4ee6528263d79c..28fa0e2b9fbc85844eb76820c0b3b9c940dace8f 100644
--- a/frontend/test/__support__/ui.js
+++ b/frontend/test/__support__/ui.js
@@ -5,7 +5,6 @@ import { merge } from "icepick";
 import { createMemoryHistory } from "history";
 import { Router, Route } from "react-router";
 import { Provider } from "react-redux";
-import { reducer as form } from "redux-form";
 import { ThemeProvider } from "@emotion/react";
 import { DragDropContextProvider } from "react-dnd";
 import HTML5Backend from "react-dnd-html5-backend";
@@ -63,7 +62,6 @@ export function renderWithProviders(
 
   const store = getStore(
     {
-      form,
       currentUser: () => getUser(currentUser),
       settings: withSettings ? () => createMockSettingsState() : undefined,
       embed: withEmbedSettings ? () => createMockEmbedState() : undefined,
diff --git a/package.json b/package.json
index 65b10f11bb2571afa21d5a7b66d143f449da43dc..0b8498b3caa11f80c35144aad554fef0af9d88ac 100644
--- a/package.json
+++ b/package.json
@@ -87,7 +87,6 @@
     "redux": "^3.5.2",
     "redux-actions": "^2.0.1",
     "redux-auth-wrapper": "^1.0.0",
-    "redux-form": "5",
     "redux-promise": "^0.5.0",
     "redux-router": "^2.1.2",
     "regenerator": "^0.14.1",
@@ -187,7 +186,6 @@
     "@types/react-virtualized": "^9.21.13",
     "@types/redux-actions": "^2.6.2",
     "@types/redux-auth-wrapper": "^1.0.7",
-    "@types/redux-form": "5.0.2",
     "@types/redux-logger": "^3.0.9",
     "@types/redux-promise": "^0.5.29",
     "@types/redux-router": "^1.0.45",
diff --git a/yarn.lock b/yarn.lock
index bdf6b2f8d5a513a6814f74e96d8fa7a441bd2d7a..7c95d852bd976fdc51052cd47a69ccd9bbd6b2bc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5653,14 +5653,6 @@
     "@types/react" "*"
     redux "^3.7.2"
 
-"@types/redux-form@5.0.2":
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-5.0.2.tgz#f803f42cc3bf7ec27de55b9df585c223ba3372ca"
-  integrity sha512-E2WfGuRXm8DGQZWQN/IXqdPqm30+PaHa//d3WbtYhQA8VFW162RCB0Vz5yFd+zQmCoguFxCIO1NjyFH20hIJMg==
-  dependencies:
-    "@types/react" "*"
-    redux "^3.6.0"
-
 "@types/redux-logger@^3.0.9":
   version "3.0.9"
   resolved "https://registry.yarnpkg.com/@types/redux-logger/-/redux-logger-3.0.9.tgz#9193b3d51bb6ab98d25514ba7764e4f98a64d3ec"
@@ -12317,7 +12309,7 @@ hoist-non-react-statics@1.2.0:
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
   integrity sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=
 
-hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
+hoist-non-react-statics@^2.5.0:
   version "2.5.5"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
   integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
@@ -13265,11 +13257,6 @@ is-potential-custom-element-name@^1.0.1:
   resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
   integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
 
-is-promise@^2.1.0:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
-  integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
-
 is-regex@^1.0.4, is-regex@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
@@ -18079,13 +18066,6 @@ react-is@^18.0.0:
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
   integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
 
-react-lazy-cache@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/react-lazy-cache/-/react-lazy-cache-3.0.1.tgz#0dc64d38df1767ef77678c5c94190064cb11b0cd"
-  integrity sha1-DcZNON8XZ+93Z4xclBkAZMsRsM0=
-  dependencies:
-    deep-equal "^1.0.1"
-
 react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
@@ -18447,18 +18427,6 @@ redux-auth-wrapper@^1.0.0:
     lodash.isempty "4.4.0"
     prop-types "15.5.8"
 
-redux-form@5:
-  version "5.4.0"
-  resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-5.4.0.tgz#daac6b47ab20cbfb5ffdcee79f1115623a6c86a7"
-  integrity sha512-Ssftdf3Or6nwEBl1SqgPbZdxhmZC8AejHTTLnt1R6SfCqyoHgPnPyxu8NuewS2KvDN925nwRT9dGxeIYDgtAWQ==
-  dependencies:
-    deep-equal "^1.0.1"
-    hoist-non-react-statics "^2.3.1"
-    invariant "^2.0.0"
-    is-promise "^2.1.0"
-    prop-types "^15.5.8"
-    react-lazy-cache "^3.0.1"
-
 redux-promise@^0.5.0:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/redux-promise/-/redux-promise-0.5.3.tgz#e97e6c9d3bf376eacb79babe6d906da20112d6d8"