Repository: metabase/metabase
Pull request #54687: fetch settings using RTK Query
Author: login: iethree
Labels: name: Type:Tech Debt
description: or Refactoring
name: no-backport
description: Do not backport this PR to any branch
name: .Team/AdminWebapp
description: DEPRECATED  Please use .Team/UXWest instead
Created: 2025-03-04T21:01:38Z | Merged: 2025-03-07T19:13:41Z

### Description

Use RTK to fetch settings (session/properties), and make sure that RTK api cache stays in sync with the `settings` redux slice and window object where we also rely on settings being present.

### Checklist

- [x] Tests have been added/updated to cover changes in this PR



Diff:
diff --git a/e2e/test/scenarios/permissions/admin-permissions.cy.spec.js b/e2e/test/scenarios/permissions/admin-permissions.cy.spec.js
index a5e90dd6f381..27fcdf0103c6 100644
--- a/e2e/test/scenarios/permissions/admin-permissions.cy.spec.js
+++ b/e2e/test/scenarios/permissions/admin-permissions.cy.spec.js
@@ -602,8 +602,7 @@ describe("scenarios > admin > permissions", () => {
     });
 
     cy.visit("/admin/permissions/");
-    //Both the command palette and the admin app call refresh settings
-    cy.wait(["@sessionProps", "@sessionProps"]);
+    cy.wait("@sessionProps");
 
     cy.findByRole("dialog", { name: /permissions may look different/ })
       .findByRole("button", { name: "Got it" })
@@ -650,8 +649,7 @@ describe("scenarios > admin > permissions", () => {
     });
 
     cy.visit("/admin/permissions/");
-    //Both the command palette and the admin app call refresh settings
-    cy.wait(["@sessionProps", "@sessionProps"]);
+    cy.wait("@sessionProps");
 
     cy.findByRole("dialog", { name: /permissions may look different/ })
       .findByRole("button", { name: "Got it" })
diff --git a/enterprise/frontend/src/embedding-sdk/store/auth.ts b/enterprise/frontend/src/embedding-sdk/store/auth.ts
index 93f059a0b3a2..e84d13f485d6 100644
--- a/enterprise/frontend/src/embedding-sdk/store/auth.ts
+++ b/enterprise/frontend/src/embedding-sdk/store/auth.ts
@@ -14,6 +14,7 @@ import api from "metabase/lib/api";
 import { createAsyncThunk } from "metabase/lib/redux";
 import { refreshSiteSettings } from "metabase/redux/settings";
 import { refreshCurrentUser } from "metabase/redux/user";
+import type { Settings } from "metabase-types/api";
 
 import { getOrRefreshSession } from "./reducer";
 import { getFetchRefreshTokenFn } from "./selectors";
@@ -45,10 +46,10 @@ export const initAuth = createAsyncThunk(
     // Fetch user and site settings
     const [user, siteSettings] = await Promise.all([
       dispatch(refreshCurrentUser()),
-      dispatch(refreshSiteSettings({})),
+      dispatch(refreshSiteSettings()),
     ]);
 
-    const mbVersion = siteSettings.payload?.version?.tag;
+    const mbVersion = (siteSettings.payload as Settings)?.version?.tag;
     const sdkVersion = getEmbeddingSdkVersion();
 
     if (mbVersion && sdkVersion !== "unknown") {
diff --git a/frontend/src/metabase-types/api/mocks/settings.ts b/frontend/src/metabase-types/api/mocks/settings.ts
index 66bb1b9d0d86..aaf8bfc33743 100644
--- a/frontend/src/metabase-types/api/mocks/settings.ts
+++ b/frontend/src/metabase-types/api/mocks/settings.ts
@@ -260,6 +260,7 @@ export const createMockSettings = (
   "show-updated-permission-modal": false,
   "show-updated-permission-banner": false,
   "site-locale": "en",
+  "site-name": "Metabae",
   "site-url": "http://localhost:3000",
   "site-uuid": "1234",
   "slack-app-token": null,
diff --git a/frontend/src/metabase-types/api/settings.ts b/frontend/src/metabase-types/api/settings.ts
index 39a073dd0433..64e81a93742f 100644
--- a/frontend/src/metabase-types/api/settings.ts
+++ b/frontend/src/metabase-types/api/settings.ts
@@ -278,6 +278,7 @@ interface InstanceSettings {
   "show-homepage-data": boolean;
   "show-homepage-pin-message": boolean;
   "show-homepage-xrays": boolean;
+  "site-name": string;
   "site-uuid": string;
   "subscription-allowed-domains": string | null;
   "uploads-settings": UploadsSettings;
diff --git a/frontend/src/metabase/admin/performance/components/ModelPersistenceConfiguration.tsx b/frontend/src/metabase/admin/performance/components/ModelPersistenceConfiguration.tsx
index 480899f05bee..d007546b19df 100644
--- a/frontend/src/metabase/admin/performance/components/ModelPersistenceConfiguration.tsx
+++ b/frontend/src/metabase/admin/performance/components/ModelPersistenceConfiguration.tsx
@@ -130,7 +130,7 @@ export const ModelPersistenceConfiguration = () => {
         ? PersistedModelsApi.enablePersistence()
         : PersistedModelsApi.disablePersistence();
       await resolveWithToasts([promise]);
-      dispatch(refreshSiteSettings({}));
+      dispatch(refreshSiteSettings());
     },
     [resolveWithToasts, setModelPersistenceEnabled, dispatch],
   );
@@ -186,7 +186,7 @@ export const ModelPersistenceConfiguration = () => {
             onChange={async (value: unknown) => {
               await resolveWithToasts([
                 PersistedModelsApi.setRefreshSchedule({ cron: value }),
-                dispatch(refreshSiteSettings({})),
+                dispatch(refreshSiteSettings()),
               ]);
             }}
           />
diff --git a/frontend/src/metabase/admin/settings/components/CloudPanel/CloudPanel.tsx b/frontend/src/metabase/admin/settings/components/CloudPanel/CloudPanel.tsx
index aec5a0876a4b..e2ef7a2cdd2c 100644
--- a/frontend/src/metabase/admin/settings/components/CloudPanel/CloudPanel.tsx
+++ b/frontend/src/metabase/admin/settings/components/CloudPanel/CloudPanel.tsx
@@ -62,7 +62,7 @@ export const CloudPanel = ({
   useEffect(
     function syncSiteSettings() {
       if (migrationState) {
-        dispatch(refreshSiteSettings({}));
+        dispatch(refreshSiteSettings());
       }
     },
     [dispatch, migrationState],
@@ -81,7 +81,7 @@ export const CloudPanel = ({
 
   const handleCreateMigration = async () => {
     const newMigration = await createCloudMigration().unwrap();
-    await dispatch(refreshSiteSettings({}));
+    await dispatch(refreshSiteSettings());
     onMigrationStart(storeUrl, newMigration);
   };
 
diff --git a/frontend/src/metabase/api/session.ts b/frontend/src/metabase/api/session.ts
index 56e3a54b5586..24e04ecca731 100644
--- a/frontend/src/metabase/api/session.ts
+++ b/frontend/src/metabase/api/session.ts
@@ -1,4 +1,6 @@
-import type { PasswordResetTokenStatus } from "metabase-types/api";
+import MetabaseSettings from "metabase/lib/settings";
+import { loadSettings } from "metabase/redux/settings";
+import type { PasswordResetTokenStatus, Settings } from "metabase-types/api";
 
 import { Api } from "./api";
 
@@ -21,11 +23,20 @@ export const sessionApi = Api.injectEndpoints({
         body: { email },
       }),
     }),
-    getSessionProperties: builder.query<void, void>({
+    getSessionProperties: builder.query<Settings, void>({
       query: () => ({
         method: "GET",
         url: "/api/session/properties",
       }),
+      providesTags: ["session-properties"],
+      onQueryStarted: async (_, { queryFulfilled, dispatch }) => {
+        const response = await queryFulfilled;
+        if (response.data) {
+          dispatch(loadSettings(response.data));
+          // compatibility layer for legacy settings on the window object
+          MetabaseSettings.setAll(response.data);
+        }
+      },
     }),
   }),
 });
@@ -35,3 +46,6 @@ export const {
   useForgotPasswordQuery,
   useGetSessionPropertiesQuery,
 } = sessionApi;
+
+// alias for easier use
+export const useGetSettingsQuery = useGetSessionPropertiesQuery;
diff --git a/frontend/src/metabase/api/tags/constants.ts b/frontend/src/metabase/api/tags/constants.ts
index 1a1bce64d391..de94e030982c 100644
--- a/frontend/src/metabase/api/tags/constants.ts
+++ b/frontend/src/metabase/api/tags/constants.ts
@@ -24,6 +24,7 @@ export const TAG_TYPES = [
   "revision",
   "schema",
   "segment",
+  "session-properties",
   "snippet",
   "subscription",
   "subscription-channel",
diff --git a/frontend/src/metabase/auth/actions.ts b/frontend/src/metabase/auth/actions.ts
index 8276cb671858..5f23669f618e 100644
--- a/frontend/src/metabase/auth/actions.ts
+++ b/frontend/src/metabase/auth/actions.ts
@@ -42,7 +42,7 @@ export const refreshSession = createAsyncThunk(
   async (_, { dispatch }) => {
     await Promise.all([
       dispatch(refreshCurrentUser()),
-      dispatch(refreshSiteSettings({})),
+      dispatch(refreshSiteSettings()),
     ]);
     await dispatch(refreshLocale()).unwrap();
   },
diff --git a/frontend/src/metabase/redux/settings.ts b/frontend/src/metabase/redux/settings.ts
index e1a5bfb23ee9..f5f92a266158 100644
--- a/frontend/src/metabase/redux/settings.ts
+++ b/frontend/src/metabase/redux/settings.ts
@@ -1,24 +1,27 @@
-import { createReducer } from "@reduxjs/toolkit";
+import { createAction, createReducer } from "@reduxjs/toolkit";
 
+import { sessionApi } from "metabase/api";
 import { createAsyncThunk } from "metabase/lib/redux";
-import MetabaseSettings from "metabase/lib/settings";
-import { SessionApi, SettingsApi } from "metabase/services";
-import type { UserSettings } from "metabase-types/api";
-
+import { SettingsApi } from "metabase/services";
+import type { Settings, UserSettings } from "metabase-types/api";
 export const REFRESH_SITE_SETTINGS = "metabase/settings/REFRESH_SITE_SETTINGS";
 
 export const refreshSiteSettings = createAsyncThunk(
   REFRESH_SITE_SETTINGS,
-  async ({ locale }: { locale?: string } = {}) => {
-    const settings = await SessionApi.properties(null, {
-      // eslint-disable-next-line no-literal-metabase-strings -- Not a user facing string
-      headers: locale ? { "X-Metabase-Locale": locale } : {},
-    });
-    MetabaseSettings.setAll(settings);
-    return settings;
+  async (_, { dispatch }) => {
+    const response = await dispatch(
+      sessionApi.endpoints.getSessionProperties.initiate(undefined, {
+        forceRefetch: true,
+      }),
+    );
+    return response.data;
   },
 );
 
+export const loadSettings = createAction<Settings>(
+  "metabase/settings/LOAD_SETTINGS",
+);
+
 interface UpdateUserSettingProps<K extends keyof UserSettings> {
   key: K;
   value: UserSettings[K];
@@ -51,29 +54,35 @@ export const updateUserSetting = createAsyncThunk(
       throw error;
     } finally {
       if (shouldRefresh) {
-        await dispatch(refreshSiteSettings({}));
+        await dispatch(refreshSiteSettings());
       }
     }
   },
 );
 
 export const settings = createReducer(
+  // note: this sets the initial state to the current values in the window object
+  // this is necessary so that we never have empty settings
   { values: window.MetabaseBootstrap || {}, loading: false },
   builder => {
-    builder.addCase(refreshSiteSettings.pending, state => {
-      state.loading = true;
-    });
-    builder.addCase(refreshSiteSettings.fulfilled, (state, { payload }) => {
-      state.loading = false;
-      state.values = payload;
-    });
-    builder.addCase(refreshSiteSettings.rejected, state => {
-      state.loading = false;
-    });
-    builder.addCase(updateUserSetting.fulfilled, (state, { payload }) => {
-      if (payload) {
-        state.values[payload.key] = payload.value;
-      }
-    });
+    builder
+      .addCase(refreshSiteSettings.pending, state => {
+        state.loading = true;
+      })
+      .addCase(refreshSiteSettings.fulfilled, state => {
+        state.loading = false;
+      })
+      .addCase(refreshSiteSettings.rejected, state => {
+        state.loading = false;
+      })
+      .addCase(loadSettings, (state, { payload }) => {
+        state.loading = false;
+        state.values = payload;
+      })
+      .addCase(updateUserSetting.fulfilled, (state, { payload }) => {
+        if (payload) {
+          state.values[payload.key] = payload.value;
+        }
+      });
   },
 );
diff --git a/frontend/src/metabase/redux/settings.unit.spec.tsx b/frontend/src/metabase/redux/settings.unit.spec.tsx
new file mode 100644
index 000000000000..bda908c23bdd
--- /dev/null
+++ b/frontend/src/metabase/redux/settings.unit.spec.tsx
@@ -0,0 +1,144 @@
+import userEvent from "@testing-library/user-event";
+import { useState } from "react";
+
+import { setupPropertiesEndpoints } from "__support__/server-mocks";
+import { renderWithProviders, screen, waitFor } from "__support__/ui";
+import {
+  sessionApi,
+  useGetSessionPropertiesQuery,
+  useGetSettingsQuery,
+} from "metabase/api";
+import { useSetting } from "metabase/common/hooks";
+import { useDispatch, useSelector } from "metabase/lib/redux";
+import { getSetting } from "metabase/selectors/settings";
+import { createMockSettings } from "metabase-types/api/mocks";
+import type { State } from "metabase-types/store";
+import { createMockSettingsState } from "metabase-types/store/mocks";
+
+import { refreshSiteSettings } from "./settings";
+
+const TestGetComponent = () => {
+  const siteNameFromSelector = useSelector((state: State) =>
+    getSetting(state, "site-name"),
+  );
+  const siteNameFromHook = useSetting("site-name");
+  const { data: settingsFromSessionProperties } =
+    useGetSessionPropertiesQuery();
+  const siteNameFromSessionProperties =
+    settingsFromSessionProperties?.["site-name"];
+
+  // we have a few different ways to access settings state
+  // and we want to make sure they stay in sync
+  return (
+    <div>
+      <div>{siteNameFromSelector}</div>
+      <div>{siteNameFromHook}</div>
+      <div>{siteNameFromSessionProperties}</div>
+    </div>
+  );
+};
+
+const TestComponentWithForceUpdateHook = () => {
+  const { data: settings, isLoading } = useGetSettingsQuery(undefined, {
+    refetchOnMountOrArgChange: true,
+  });
+
+  const siteName = settings?.["site-name"];
+
+  if (isLoading) {
+    return <div>loading</div>;
+  }
+
+  return <div>{siteName}</div>;
+};
+
+const TestUpdateComponent = () => {
+  const [showHiddenComponent, setShowHiddenComponent] = useState(false);
+  const dispatch = useDispatch();
+  const refresh = () => dispatch(refreshSiteSettings());
+  const invalidate = () =>
+    dispatch(sessionApi.util.invalidateTags(["session-properties"]));
+
+  return (
+    <div>
+      <button onClick={() => refresh()}>refresh</button>
+      <button onClick={() => invalidate()}>invalidate</button>
+      <button onClick={() => () => setShowHiddenComponent(true)}>
+        show force update component\
+      </button>
+      <TestGetComponent />
+      {showHiddenComponent && <TestComponentWithForceUpdateHook />}
+    </div>
+  );
+};
+
+const setup = () => {
+  setupPropertiesEndpoints(
+    createMockSettings({ "site-name": "site name from API" }),
+  );
+
+  return renderWithProviders(<TestUpdateComponent />, {
+    storeInitialState: {
+      settings: createMockSettingsState({ "site-name": "initial site name" }),
+    },
+  });
+};
+
+describe("metabase/redux/settings", () => {
+  it("should load settings from initial store", () => {
+    setup();
+    // will be loading if called from the api
+    expect(screen.getAllByText("initial site name")).toHaveLength(2);
+  });
+
+  it("should load settings from API", async () => {
+    setup();
+    await waitFor(() =>
+      expect(screen.getAllByText("site name from API")).toHaveLength(3),
+    );
+  });
+
+  it("settings state should get refreshed by refreshSettings action", async () => {
+    setup();
+
+    await waitFor(() =>
+      expect(screen.getAllByText("site name from API")).toHaveLength(3),
+    );
+
+    setupPropertiesEndpoints(
+      createMockSettings({ "site-name": "updated site name" }),
+    );
+
+    // nothing should have changed
+    expect(screen.getAllByText("site name from API")).toHaveLength(3);
+
+    const refreshButton = screen.getByText("refresh");
+    await userEvent.click(refreshButton);
+
+    await waitFor(() =>
+      expect(screen.getAllByText("updated site name")).toHaveLength(3),
+    );
+  });
+
+  it("settings state should get refreshed by invalidating tags", async () => {
+    setup();
+
+    await waitFor(() =>
+      expect(screen.getAllByText("site name from API")).toHaveLength(3),
+    );
+
+    setupPropertiesEndpoints(
+      createMockSettings({ "site-name": "updated site name" }),
+    );
+
+    // nothing should have changed
+    expect(screen.getAllByText("site name from API")).toHaveLength(3);
+
+    const invalidateButton = screen.getByText("invalidate");
+    await userEvent.click(invalidateButton);
+
+    await waitFor(() =>
+      expect(screen.getAllByText("updated site name")).toHaveLength(3),
+    );
+  });
+});
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index 2ab396a85ba6..00cc3ed14d5e 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -376,7 +376,6 @@ export const SessionApi = {
   createWithGoogleAuth: POST("/api/session/google_auth"),
   delete: DELETE("/api/session"),
   slo: POST("/auth/sso/logout"),
-  properties: GET("/api/session/properties"),
   forgot_password: POST("/api/session/forgot_password"),
   reset_password: POST("/api/session/reset_password"),
 };
diff --git a/frontend/test/__support__/server-mocks/session.ts b/frontend/test/__support__/server-mocks/session.ts
index 2059a840d045..3fd3fbcb1037 100644
--- a/frontend/test/__support__/server-mocks/session.ts
+++ b/frontend/test/__support__/server-mocks/session.ts
@@ -3,7 +3,9 @@ import fetchMock from "fetch-mock";
 import type { PasswordResetTokenStatus, Settings } from "metabase-types/api";
 
 export function setupPropertiesEndpoints(settings: Settings) {
-  fetchMock.get("path:/api/session/properties", settings);
+  fetchMock.get("path:/api/session/properties", settings, {
+    overwriteRoutes: true,
+  });
 }
 
 export function setupLoginEndpoint() {


Reviews:
user: login: iethree
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T21:02:57Z
user: login: iethree
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T21:03:21Z
user: login: iethree
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T21:04:14Z
user: login: iethree
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T21:04:59Z
user: login: iethree
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T23:56:06Z
user: login: fraserdrops
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T02:26:29Z
user: login: WiNloSt
body: The embedding part looks good to me.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T05:19:12Z
user: login: npretto
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-05T09:09:49Z
user: login: iethree
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T15:32:16Z
user: login: npretto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T15:50:43Z
user: login: npretto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T15:54:05Z
user: login: fraserdrops
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T00:03:38Z
user: login: iethree
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T19:13:31Z

Review comments:
diff_hunk: @@ -45,7 +45,7 @@ export const initAuth = createAsyncThunk(
     // Fetch user and site settings
     const [user, siteSettings] = await Promise.all([
       dispatch(refreshCurrentUser()),
-      dispatch(refreshSiteSettings({})),
path: enterprise/frontend/src/embedding-sdk/store/auth.ts
user: login: iethree
body: this function took a parameter, but it was never used so I removed it
created_at: 2025-03-04T21:02:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -278,6 +278,7 @@ interface InstanceSettings {
   "show-homepage-data": boolean;
   "show-homepage-pin-message": boolean;
   "show-homepage-xrays": boolean;
+  "site-name": string;
path: frontend/src/metabase-types/api/settings.ts
user: login: iethree
body: lol, I suspect we are missing a lot of settings 🥴 
created_at: 2025-03-04T21:03:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,11 +23,20 @@ export const sessionApi = Api.injectEndpoints({
         body: { email },
       }),
     }),
-    getSessionProperties: builder.query<void, void>({
+    getSessionProperties: builder.query<Settings, void>({
       query: () => ({
         method: "GET",
         url: "/api/session/properties",
       }),
+      providesTags: ["session-properties"],
+      onQueryStarted: async (_, { queryFulfilled, dispatch }) => {
+        const response = await queryFulfilled;
+        if (response.data) {
+          dispatch(loadSettings(response.data));
+          // compatibility layer for legacy settings on the window object
+          MetabaseSettings.setAll(response.data);
+        }
+      },
path: frontend/src/metabase/api/session.ts
user: login: iethree
body: yes, we are duplicating state, but this is an elegant way of keeping it in sync. Eradicating the redux settings store would be a bigger refactor than I want to take on right now.
created_at: 2025-03-04T21:04:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,3 +46,6 @@ export const {
   useForgotPasswordQuery,
   useGetSessionPropertiesQuery,
 } = sessionApi;
+
+// alias for easier use
+export const useGetSettingsQuery = useGetSessionPropertiesQuery;
path: frontend/src/metabase/api/session.ts
user: login: iethree
body: It's less than ideal that the main endpoint from which we fetch settings is called "session/properties" 🥴 
created_at: 2025-03-04T21:04:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -602,8 +602,7 @@ describe("scenarios > admin > permissions", () => {
     });
 
     cy.visit("/admin/permissions/");
-    //Both the command palette and the admin app call refresh settings
-    cy.wait(["@sessionProps", "@sessionProps"]);
+    cy.wait("@sessionProps");
path: e2e/test/scenarios/permissions/admin-permissions.cy.spec.js
user: login: iethree
body: 🎉  now we've got one request where there used to be two! 🎉 
created_at: 2025-03-04T23:56:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,3 +46,6 @@ export const {
   useForgotPasswordQuery,
   useGetSessionPropertiesQuery,
 } = sessionApi;
+
+// alias for easier use
+export const useGetSettingsQuery = useGetSessionPropertiesQuery;
path: frontend/src/metabase/api/session.ts
user: login: fraserdrops
body: This confused me as well.  Are the `useXQuery` hooks generally named after the endpoint? Just wondering if obfuscating the endpoint name with an alias helps much, or if it makes it more confusing when trying to match up the query to the api request
created_at: 2025-03-05T02:26:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,11 +23,20 @@ export const sessionApi = Api.injectEndpoints({
         body: { email },
       }),
     }),
-    getSessionProperties: builder.query<void, void>({
+    getSessionProperties: builder.query<Settings, void>({
       query: () => ({
         method: "GET",
         url: "/api/session/properties",
       }),
+      providesTags: ["session-properties"],
+      onQueryStarted: async (_, { queryFulfilled, dispatch }) => {
+        const response = await queryFulfilled;
+        if (response.data) {
+          dispatch(loadSettings(response.data));
+          // compatibility layer for legacy settings on the window object
+          MetabaseSettings.setAll(response.data);
+        }
+      },
path: frontend/src/metabase/api/session.ts
user: login: iethree
body: actually, if anyone can figure out how to make `	getSettings()` to select from the RTK cache, that might get us most of the way there, but I banged my head on it for a bit before giving up.
created_at: 2025-03-05T15:32:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,11 +23,20 @@ export const sessionApi = Api.injectEndpoints({
         body: { email },
       }),
     }),
-    getSessionProperties: builder.query<void, void>({
+    getSessionProperties: builder.query<Settings, void>({
       query: () => ({
         method: "GET",
         url: "/api/session/properties",
       }),
+      providesTags: ["session-properties"],
+      onQueryStarted: async (_, { queryFulfilled, dispatch }) => {
+        const response = await queryFulfilled;
+        if (response.data) {
+          dispatch(loadSettings(response.data));
+          // compatibility layer for legacy settings on the window object
+          MetabaseSettings.setAll(response.data);
+        }
+      },
path: frontend/src/metabase/api/session.ts
user: login: npretto
body: @iethree I think we can do `sessionApi.endpoints.getSessionProperties.select()`, but the issue is that that's not valued on first render
created_at: 2025-03-05T15:50:43Z
author_association: MEMBER
diff_hunk: @@ -21,11 +23,20 @@ export const sessionApi = Api.injectEndpoints({
         body: { email },
       }),
     }),
-    getSessionProperties: builder.query<void, void>({
+    getSessionProperties: builder.query<Settings, void>({
       query: () => ({
         method: "GET",
         url: "/api/session/properties",
       }),
+      providesTags: ["session-properties"],
+      onQueryStarted: async (_, { queryFulfilled, dispatch }) => {
+        const response = await queryFulfilled;
+        if (response.data) {
+          dispatch(loadSettings(response.data));
+          // compatibility layer for legacy settings on the window object
+          MetabaseSettings.setAll(response.data);
+        }
+      },
path: frontend/src/metabase/api/session.ts
user: login: npretto
body: If we're ok with using MetabaseSettings as fallback, this should be almost there
```
export const getSettings: <S extends State>(state: S) => GetSettings<S> =
  createSelector(
    sessionApi.endpoints.getSessionProperties.select(),
    ({ data }) => {
      return data ? data : MetabaseSettings._settings;
    },
  );
```

created_at: 2025-03-05T15:54:05Z
author_association: MEMBER
diff_hunk: @@ -21,11 +23,20 @@ export const sessionApi = Api.injectEndpoints({
         body: { email },
       }),
     }),
-    getSessionProperties: builder.query<void, void>({
+    getSessionProperties: builder.query<Settings, void>({
       query: () => ({
         method: "GET",
         url: "/api/session/properties",
       }),
+      providesTags: ["session-properties"],
+      onQueryStarted: async (_, { queryFulfilled, dispatch }) => {
+        const response = await queryFulfilled;
+        if (response.data) {
+          dispatch(loadSettings(response.data));
+          // compatibility layer for legacy settings on the window object
+          MetabaseSettings.setAll(response.data);
+        }
+      },
path: frontend/src/metabase/api/session.ts
user: login: iethree
body: gonna try this in a follow up 👍 
created_at: 2025-03-07T19:13:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-04T21:05:48Z
body: <!---conventional commit reminder comment ID-->
@iethree You have modified embedding SDK code. Please make sure the PR title follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) style.
Here are all the supported types that will be shown in the changelog:
- `feat`
- `fix`
- `perf`

These types `docs`, `style`, `refactor`, `test`, `build`, and `ci` are also encouraged for non-changelog related tasks.

Please also make sure to include `sdk` scope, otherwise, the changelog will not include this task.

For example, these are valid PR titles:
- `feat(sdk): Add interactive dashboard component`
- `feat(sdk): Support theming pie chart`
- `fix(sdk): Fix static dashboard crash`
author_association: CONTRIBUTOR
user: login: trunk-io[bot]
created_at: 2025-03-04T21:59:41Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://img.shields.io/badge/25715_tests-0c11f5e0-purple)](https://app.trunk.io/metabase/flaky-tests/pr/54687?repo=metabase/metabase&commitHash=0c11f5e0735f090941a5192263fb11686989f0eb&utm_source=gh-comment) &ensp; [![Static Badge](https://img.shields.io/badge/1-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/54687?repo=metabase/metabase&commitHash=0c11f5e0735f090941a5192263fb11686989f0eb&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/2-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/54687?repo=metabase/metabase&commitHash=0c11f5e0735f090941a5192263fb11686989f0eb&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/54687?repo=metabase/metabase&commitHash=0c11f5e0735f090941a5192263fb11686989f0eb&utm_source=gh-comment&runConclusion=QUARANTINED)


| Failed Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>fetch-token-does-not-call-db-when-cached</code> | failure\_text: token\_check\_test.clj:163No DB calls are made for the user count when checking token status if the status is cachedexpected: (= 1 (... | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54687?repo=metabase/metabase&commitHash=0c11f5e0735f090941a5192263fb11686989f0eb&utm_source=gh-comment&runConclusion=FAILURE&runId=a62f4ec6-5e93-5547-a605-ecc71a275f76) |


| Flaky Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>scenarios > dashboard warn before leave should warn a user before leaving after adding, removed, moving, or duplicating a tab</code> | Unable to find an element with the text "You're editing this dashboard" within 4 seconds. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54687?repo=metabase/metabase&commitHash=0c11f5e0735f090941a5192263fb11686989f0eb&utm_source=gh-comment&runConclusion=FAILURE&runId=b4171f75-58f5-5f14-92e3-801b5df7d917) |
| <code>scenarios > dashboard warn before leave should warn a user before leaving after adding, removed, moving, or duplicating a tab</code> | Unable to find an element with the text "You're editing this dashboard" within 4 seconds. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54687?repo=metabase/metabase&commitHash=0c11f5e0735f090941a5192263fb11686989f0eb&utm_source=gh-comment&runConclusion=FAILURE&runId=2213aec7-1efc-5e80-afcd-ed6a93b750ec) |
<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54687?repo=metabase/metabase&commitHash=0c11f5e0735f090941a5192263fb11686989f0eb&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-04T22:31:07Z
body: 
  ## e2e tests failed on `0c11f5e0735f090941a5192263fb11686989f0eb-2`

  [e2e test run](https://github.com/metabase/metabase/actions/runs/13664057058?pr=54687)

  File | Test Name
  ---- | ---------
  `dashboard.cy.spec.js` | (_flaky_) scenarios > dashboard > warn before leave > should warn a user before leaving after adding, removed, moving, or duplicating a tab

author_association: CONTRIBUTOR