diff --git a/frontend/src/metabase/actions/containers/ActionCreatorModal/ActionCreatorModal.unit.spec.tsx b/frontend/src/metabase/actions/containers/ActionCreatorModal/ActionCreatorModal.unit.spec.tsx
index 098b3cafd4e3fb665d5e3935992e35e44313f8b0..5717b84fce97c454fed39a8cf88123c62cc03c74 100644
--- a/frontend/src/metabase/actions/containers/ActionCreatorModal/ActionCreatorModal.unit.spec.tsx
+++ b/frontend/src/metabase/actions/containers/ActionCreatorModal/ActionCreatorModal.unit.spec.tsx
@@ -2,12 +2,7 @@ import userEvent from "@testing-library/user-event";
 import fetchMock from "fetch-mock";
 import { Route } from "react-router";
 
-import {
-  renderWithProviders,
-  screen,
-  waitFor,
-  waitForElementToBeRemoved,
-} from "__support__/ui";
+import { renderWithProviders, screen, waitFor } from "__support__/ui";
 import {
   setupCardsEndpoints,
   setupDatabasesEndpoints,
@@ -76,6 +71,10 @@ async function setup({
     },
   );
 
+  await waitFor(() => {
+    expect(screen.queryByTestId("loading-spinner")).not.toBeInTheDocument();
+  });
+
   return { history: checkNotNull(history) };
 }
 
@@ -97,10 +96,6 @@ describe("actions > containers > ActionCreatorModal", () => {
     const initialRoute = `/model/${MODEL.id}/detail/actions/${ACTION_NOT_FOUND_ID}`;
     const { history } = await setup({ initialRoute, action: null });
 
-    await waitForElementToBeRemoved(() =>
-      screen.queryAllByTestId("loading-spinner"),
-    );
-
     expect(await screen.findByTestId("mock-model-detail")).toBeInTheDocument();
     expect(screen.queryByTestId("action-creator")).not.toBeInTheDocument();
     expect(history.getCurrentLocation().pathname).toBe(
@@ -113,10 +108,6 @@ describe("actions > containers > ActionCreatorModal", () => {
     const initialRoute = `/model/${MODEL.id}/detail/actions/${action.id}`;
     const { history } = await setup({ initialRoute, action });
 
-    await waitForElementToBeRemoved(() =>
-      screen.queryAllByTestId("loading-spinner"),
-    );
-
     expect(await screen.findByTestId("mock-model-detail")).toBeInTheDocument();
     expect(screen.queryByTestId("action-creator")).not.toBeInTheDocument();
     expect(history.getCurrentLocation().pathname).toBe(
diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.tsx b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.tsx
index 4b839058708ee9c47b6cd46aead39480ff65abc0..ddc17b97f9eff44e9855ec83cc549dec5d51df34 100644
--- a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.tsx
+++ b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.tsx
@@ -1,21 +1,25 @@
 import type { ComponentType } from "react";
 import { useState } from "react";
 import { connect } from "react-redux";
+import { push } from "react-router-redux";
+import type { Route } from "react-router";
 
 import { t } from "ttag";
 import _ from "underscore";
 import { updateIn } from "icepick";
 
 import { useMount } from "react-use";
-import type { Location } from "history";
+import type { Location, LocationDescriptor } from "history";
 import title from "metabase/hoc/Title";
 
 import Breadcrumbs from "metabase/components/Breadcrumbs";
 import Sidebar from "metabase/admin/databases/components/DatabaseEditApp/Sidebar/Sidebar";
 import { getUserIsAdmin } from "metabase/selectors/user";
+import { useCallbackEffect } from "metabase/hooks/use-callback-effect";
 
 import { getSetting } from "metabase/selectors/settings";
 
+import { LeaveConfirmationModal } from "metabase/components/LeaveConfirmationModal";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 import { DatabaseForm } from "metabase/databases/components/DatabaseForm";
 import ErrorBoundary from "metabase/ErrorBoundary";
@@ -26,7 +30,6 @@ import type {
   DatabaseId,
 } from "metabase-types/api";
 import type { State } from "metabase-types/store";
-import useBeforeUnload from "metabase/hooks/use-before-unload";
 import Database from "metabase-lib/metadata/Database";
 
 import { getEditingDatabase, getInitializeError } from "../selectors";
@@ -73,6 +76,8 @@ interface DatabaseEditAppProps {
   isAdmin: boolean;
   isModelPersistenceEnabled: boolean;
   initializeError?: DatabaseEditErrorType;
+  route: Route;
+  onChangeLocation: (location: LocationDescriptor) => void;
 }
 
 const mapStateToProps = (state: State) => {
@@ -97,6 +102,7 @@ const mapDispatchToProps = {
   discardSavedFieldValues,
   deleteDatabase,
   selectEngine,
+  onChangeLocation: push,
 };
 
 type DatabaseEditErrorType = {
@@ -124,6 +130,8 @@ function DatabaseEditApp(props: DatabaseEditAppProps) {
     initializeDatabase,
     params,
     saveDatabase,
+    route,
+    onChangeLocation,
   } = props;
 
   const editingExistingDatabase = database?.id != null;
@@ -131,7 +139,11 @@ function DatabaseEditApp(props: DatabaseEditAppProps) {
 
   const [isDirty, setIsDirty] = useState(false);
 
-  useBeforeUnload(isDirty);
+  /**
+   * Navigation is scheduled so that LeaveConfirmationModal's isEnabled
+   * prop has a chance to re-compute on re-render
+   */
+  const [isCallbackScheduled, scheduleCallback] = useCallbackEffect();
 
   useMount(async () => {
     await reset();
@@ -145,6 +157,12 @@ function DatabaseEditApp(props: DatabaseEditAppProps) {
   const handleSubmit = async (database: DatabaseData) => {
     try {
       await saveDatabase(database);
+
+      if (addingNewDatabase) {
+        scheduleCallback(() => {
+          onChangeLocation("/admin/databases?created=true");
+        });
+      }
     } catch (error) {
       throw getSubmitError(error as DatabaseEditErrorType);
     }
@@ -195,6 +213,11 @@ function DatabaseEditApp(props: DatabaseEditAppProps) {
           />
         )}
       </DatabaseEditMain>
+
+      <LeaveConfirmationModal
+        isEnabled={isDirty && !isCallbackScheduled}
+        route={route}
+      />
     </DatabaseEditRoot>
   );
 }
diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.unit.spec.js b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.unit.spec.js
deleted file mode 100644
index 89bd00a0806bfc9ba78edd3fa110bebeadf3ed8c..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.unit.spec.js
+++ /dev/null
@@ -1,150 +0,0 @@
-import { IndexRoute, Route } from "react-router";
-
-import userEvent from "@testing-library/user-event";
-import {
-  renderWithProviders,
-  screen,
-  waitForElementToBeRemoved,
-  waitFor,
-} from "__support__/ui";
-import { setupEnterpriseTest } from "__support__/enterprise";
-import { mockSettings } from "__support__/settings";
-
-import {
-  createMockDatabase,
-  createMockTokenFeatures,
-} from "metabase-types/api/mocks";
-import { setupDatabaseEndpoints } from "__support__/server-mocks";
-
-import { BEFORE_UNLOAD_UNSAVED_MESSAGE } from "metabase/hooks/use-before-unload";
-import { callMockEvent } from "__support__/events";
-import DatabaseEditApp from "./DatabaseEditApp";
-
-const ENGINES_MOCK = {
-  H2: {
-    "details-fields": [
-      { "display-name": "Connection String", name: "db", required: true },
-      { name: "advanced-options", type: "section", default: true },
-    ],
-    "driver-name": "H2",
-    "superseded-by": null,
-  },
-  sqlite: {
-    "details-fields": [
-      { "display-name": "Filename", name: "db", required: true },
-      { name: "advanced-options", type: "section", default: true },
-    ],
-    "driver-name": "SQLite",
-    "superseded-by": null,
-  },
-};
-
-async function setup({ cachingEnabled = false, databaseIdParam = "" } = {}) {
-  const mockEventListener = jest.spyOn(window, "addEventListener");
-
-  const settings = mockSettings({
-    engines: ENGINES_MOCK,
-    "token-features": createMockTokenFeatures({
-      cache_granular_controls: true,
-    }),
-    "enable-query-caching": cachingEnabled,
-  });
-
-  renderWithProviders(
-    <Route path="/">
-      <IndexRoute component={DatabaseEditApp} />
-      <Route path=":databaseId" component={DatabaseEditApp} />
-    </Route>,
-    {
-      withRouter: true,
-      initialRoute: `/${databaseIdParam}`,
-      storeInitialState: {
-        settings,
-      },
-    },
-  );
-
-  await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
-
-  return { mockEventListener };
-}
-
-describe("DatabaseEditApp", () => {
-  describe("Database connections", () => {
-    afterEach(() => {
-      jest.restoreAllMocks();
-    });
-
-    it("should have save changes button disabled", async () => {
-      setupDatabaseEndpoints(createMockDatabase());
-      await setup({ databaseIdParam: "1" });
-
-      const saveButton = await screen.findByRole("button", {
-        name: /save changes/i,
-      });
-      expect(saveButton).toBeDisabled();
-
-      const connectionField = await screen.findByLabelText("Connection String");
-      userEvent.type(connectionField, "Test Connection");
-
-      await waitFor(() => {
-        expect(saveButton).toBeEnabled();
-      });
-    });
-
-    it("should trigger beforeunload event when database connection is edited", async () => {
-      const { mockEventListener } = await setup();
-
-      const databaseForm = await screen.findByLabelText("Display name");
-
-      userEvent.type(databaseForm, "Test database");
-      const mockEvent = await waitFor(() => {
-        return callMockEvent(mockEventListener, "beforeunload");
-      });
-      expect(mockEvent.preventDefault).toHaveBeenCalled();
-      expect(mockEvent.returnValue).toBe(BEFORE_UNLOAD_UNSAVED_MESSAGE);
-    });
-
-    it("should not trigger beforeunload event when database connection is unchanged", async () => {
-      const { mockEventListener } = await setup();
-      const mockEvent = callMockEvent(mockEventListener, "beforeunload");
-
-      expect(mockEvent.preventDefault).not.toHaveBeenCalled();
-      expect(mockEvent.returnValue).toBe(undefined);
-    });
-  });
-
-  describe("Cache TTL field", () => {
-    describe("OSS", () => {
-      it("is invisible", async () => {
-        await setup({ cachingEnabled: true });
-
-        expect(
-          screen.queryByText("Default result cache duration"),
-        ).not.toBeInTheDocument();
-      });
-    });
-
-    describe("EE", () => {
-      beforeEach(() => {
-        setupEnterpriseTest();
-      });
-
-      it("is visible", async () => {
-        await setup({ cachingEnabled: true });
-
-        expect(
-          screen.getByText("Default result cache duration"),
-        ).toBeInTheDocument();
-      });
-
-      it("is invisible when caching disabled", async () => {
-        await setup({ cachingEnabled: false });
-
-        expect(
-          screen.queryByText("Default result cache duration"),
-        ).not.toBeInTheDocument();
-      });
-    });
-  });
-});
diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.unit.spec.tsx b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3d34462abeabee138d130a88462529d3669bffaa
--- /dev/null
+++ b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.unit.spec.tsx
@@ -0,0 +1,264 @@
+import { IndexRoute, Route } from "react-router";
+import userEvent from "@testing-library/user-event";
+
+import {
+  renderWithProviders,
+  screen,
+  waitFor,
+  waitForElementToBeRemoved,
+} from "__support__/ui";
+import { setupEnterpriseTest } from "__support__/enterprise";
+import { mockSettings } from "__support__/settings";
+
+import type { Engine } from "metabase-types/api";
+import {
+  createMockDatabase,
+  createMockEngineSource,
+  createMockTokenFeatures,
+} from "metabase-types/api/mocks";
+import {
+  setupDatabaseEndpoints,
+  setupDatabasesEndpoints,
+} from "__support__/server-mocks";
+
+import { checkNotNull } from "metabase/core/utils/types";
+import { BEFORE_UNLOAD_UNSAVED_MESSAGE } from "metabase/hooks/use-before-unload";
+import { callMockEvent } from "__support__/events";
+import DatabaseEditApp from "./DatabaseEditApp";
+
+const ENGINES_MOCK: Record<string, Engine> = {
+  H2: {
+    "details-fields": [
+      { "display-name": "Connection String", name: "db", required: true },
+      { name: "advanced-options", type: "section", default: true },
+    ],
+    "driver-name": "H2",
+    "superseded-by": null,
+    source: createMockEngineSource(),
+  },
+  sqlite: {
+    "details-fields": [
+      { "display-name": "Filename", name: "db", required: true },
+      { name: "advanced-options", type: "section", default: true },
+    ],
+    "driver-name": "SQLite",
+    "superseded-by": null,
+    source: createMockEngineSource(),
+  },
+};
+
+const MockComponent = () => <div />;
+
+interface SetupOpts {
+  cachingEnabled?: boolean;
+  databaseIdParam?: string;
+  initialRoute?: string;
+}
+
+async function setup({
+  cachingEnabled = false,
+  databaseIdParam = "",
+  initialRoute = `/${databaseIdParam}`,
+}: SetupOpts = {}) {
+  const mockEventListener = jest.spyOn(window, "addEventListener");
+
+  setupDatabasesEndpoints([]);
+
+  const settings = mockSettings({
+    engines: ENGINES_MOCK,
+    "token-features": createMockTokenFeatures({
+      cache_granular_controls: true,
+    }),
+    "enable-query-caching": cachingEnabled,
+  });
+
+  const { history } = renderWithProviders(
+    <Route path="/">
+      <Route path="/home" component={MockComponent} />
+      <Route path="/admin/databases" component={MockComponent} />
+      <IndexRoute component={DatabaseEditApp} />
+      <Route path=":databaseId" component={DatabaseEditApp} />
+    </Route>,
+    {
+      withRouter: true,
+      initialRoute,
+      storeInitialState: {
+        settings,
+      },
+    },
+  );
+
+  await waitFor(() => {
+    expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
+  });
+
+  return {
+    history: checkNotNull(history),
+    mockEventListener,
+  };
+}
+
+describe("DatabaseEditApp", () => {
+  describe("Database connections", () => {
+    afterEach(() => {
+      jest.restoreAllMocks();
+    });
+
+    it("should have save changes button disabled", async () => {
+      setupDatabaseEndpoints(createMockDatabase());
+      await setup({ databaseIdParam: "1" });
+
+      const saveButton = await screen.findByRole("button", {
+        name: /save changes/i,
+      });
+      expect(saveButton).toBeDisabled();
+
+      const connectionField = await screen.findByLabelText("Connection String");
+      userEvent.type(connectionField, "Test Connection");
+
+      await waitFor(() => {
+        expect(saveButton).toBeEnabled();
+      });
+    });
+
+    it("should trigger beforeunload event when database connection is edited", async () => {
+      const { mockEventListener } = await setup();
+
+      const displayNameInput = await screen.findByLabelText("Display name");
+
+      userEvent.type(displayNameInput, "Test database");
+      const mockEvent = await waitFor(() => {
+        return callMockEvent(mockEventListener, "beforeunload");
+      });
+      expect(mockEvent.preventDefault).toHaveBeenCalled();
+      expect(mockEvent.returnValue).toBe(BEFORE_UNLOAD_UNSAVED_MESSAGE);
+    });
+
+    it("should not trigger beforeunload event when database connection is unchanged", async () => {
+      const { mockEventListener } = await setup();
+      const mockEvent = callMockEvent(mockEventListener, "beforeunload");
+
+      expect(mockEvent.preventDefault).not.toHaveBeenCalled();
+      expect(mockEvent.returnValue).toBe(undefined);
+    });
+
+    it("does not show custom warning modal when leaving with no changes via SPA navigation", async () => {
+      const { history } = await setup({ initialRoute: "/home" });
+
+      history.push("/");
+
+      await waitForElementToBeRemoved(() =>
+        screen.queryAllByTestId("loading-spinner"),
+      );
+
+      const displayNameInput = await screen.findByLabelText("Display name");
+      userEvent.type(displayNameInput, "ab");
+      userEvent.type(displayNameInput, "{backspace}{backspace}");
+
+      history.goBack();
+
+      expect(
+        screen.queryByText("Changes were not saved"),
+      ).not.toBeInTheDocument();
+      expect(
+        screen.queryByText(
+          "Navigating away from here will cause you to lose any changes you have made.",
+        ),
+      ).not.toBeInTheDocument();
+    });
+
+    it("shows custom warning modal when leaving with unsaved changes via SPA navigation", async () => {
+      const { history } = await setup({ initialRoute: "/home" });
+
+      history.push("/");
+
+      await waitForElementToBeRemoved(() =>
+        screen.queryAllByTestId("loading-spinner"),
+      );
+      const displayNameInput = await screen.findByLabelText("Display name");
+      userEvent.type(displayNameInput, "Test database");
+
+      history.goBack();
+
+      expect(screen.getByText("Changes were not saved")).toBeInTheDocument();
+      expect(
+        screen.getByText(
+          "Navigating away from here will cause you to lose any changes you have made.",
+        ),
+      ).toBeInTheDocument();
+    });
+
+    it("does not show custom warning modal after creating new database connection", async () => {
+      const { history } = await setup({ initialRoute: "/home" });
+
+      history.push("/");
+
+      await waitForElementToBeRemoved(() =>
+        screen.queryAllByTestId("loading-spinner"),
+      );
+
+      const displayNameInput = await screen.findByLabelText("Display name");
+      userEvent.type(displayNameInput, "Test database");
+      const connectionStringInput = await screen.findByLabelText(
+        "Connection String",
+      );
+      userEvent.type(
+        connectionStringInput,
+        "file:/sample-database.db;USER=GUEST;PASSWORD=guest",
+      );
+
+      userEvent.click(await screen.findByText("Save"));
+
+      await waitFor(() => {
+        expect(history.getCurrentLocation().pathname).toEqual(
+          "/admin/databases",
+        );
+      });
+
+      expect(history.getCurrentLocation().search).toEqual("?created=true");
+
+      expect(
+        screen.queryByText("Changes were not saved"),
+      ).not.toBeInTheDocument();
+      expect(
+        screen.queryByText(
+          "Navigating away from here will cause you to lose any changes you have made.",
+        ),
+      ).not.toBeInTheDocument();
+    });
+  });
+
+  describe("Cache TTL field", () => {
+    describe("OSS", () => {
+      it("is invisible", async () => {
+        await setup({ cachingEnabled: true });
+
+        expect(
+          screen.queryByText("Default result cache duration"),
+        ).not.toBeInTheDocument();
+      });
+    });
+
+    describe("EE", () => {
+      beforeEach(() => {
+        setupEnterpriseTest();
+      });
+
+      it("is visible", async () => {
+        await setup({ cachingEnabled: true });
+
+        expect(
+          screen.getByText("Default result cache duration"),
+        ).toBeInTheDocument();
+      });
+
+      it("is invisible when caching disabled", async () => {
+        await setup({ cachingEnabled: false });
+
+        expect(
+          screen.queryByText("Default result cache duration"),
+        ).not.toBeInTheDocument();
+      });
+    });
+  });
+});
diff --git a/frontend/src/metabase/admin/databases/database.js b/frontend/src/metabase/admin/databases/database.js
index 9b050bc9c92b76938cf25ffce2c64d8acdeefb47..452312b297c6c7e6c995d12eabcd1e58054d25ce 100644
--- a/frontend/src/metabase/admin/databases/database.js
+++ b/frontend/src/metabase/admin/databases/database.js
@@ -166,7 +166,6 @@ export const createDatabase = function (database) {
       );
 
       dispatch({ type: CREATE_DATABASE });
-      dispatch(push("/admin/databases?created=true"));
     } catch (error) {
       console.error("error creating a database", error);
       MetabaseAnalytics.trackStructEvent(
diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.unit.spec.tsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.unit.spec.tsx
index 5e91a045a7273594028d144a027e5bb0ecbdda57..351e4bcb809d5144cf7b6ca4a44d6050b10f6de6 100644
--- a/frontend/src/metabase/query_builder/containers/QueryBuilder.unit.spec.tsx
+++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.unit.spec.tsx
@@ -243,11 +243,9 @@ const setup = async ({
     },
   );
 
-  if (initialRoute !== "/home") {
-    await waitForElementToBeRemoved(() =>
-      screen.queryByTestId("loading-spinner"),
-    );
-  }
+  await waitFor(() => {
+    expect(screen.queryByTestId("loading-spinner")).not.toBeInTheDocument();
+  });
 
   return {
     history: checkNotNull(history),
diff --git a/frontend/test/__support__/server-mocks/database.ts b/frontend/test/__support__/server-mocks/database.ts
index a3a6bffdb24a1f233b7a7d2376d5f2a924e4fb23..5cd419798b237c3686c8be5a1a1b722fad6702bf 100644
--- a/frontend/test/__support__/server-mocks/database.ts
+++ b/frontend/test/__support__/server-mocks/database.ts
@@ -39,6 +39,10 @@ export function setupDatabasesEndpoints(
     hasSavedQuestions ? [...dbs, SAVED_QUESTIONS_DATABASE] : dbs,
   );
   fetchMock.get({ url: "path:/api/database", overwriteRoutes: false }, dbs);
+  fetchMock.post("path:/api/database", async url => {
+    const lastCall = fetchMock.lastCall(url);
+    return await lastCall?.request?.json();
+  });
 
   dbs.forEach(db => setupDatabaseEndpoints(db));
 }