Skip to content
Snippets Groups Projects
Unverified Commit f229c492 authored by github-automation-metabase's avatar github-automation-metabase Committed by GitHub
Browse files

:robot: backported "fix(sdk): migrate to custom redux context to allow using the sdk...

:robot:

 backported "fix(sdk): migrate to custom redux context to allow using the sdk on host apps that use redux" (#51414)

* step1: add MetabaseReduxProvider and make our hooks + connect fn use it

step1: add MetabaseReduxProvider and make our hooks + connect fn use it

* step 2: codemod

* step 3: manual fixes

* step 4: e2e test for sdk

---------

Co-authored-by: default avatarNicolò Pretto <info@npretto.com>
parent aa733078
No related branches found
No related tags found
No related merge requests found
Showing
with 152 additions and 48 deletions
import {
MetabaseProvider,
StaticDashboard,
} from "@metabase/embedding-sdk-react";
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
import { describeEE } from "e2e/support/helpers";
import {
AUTH_PROVIDER_URL,
METABASE_INSTANCE_URL,
mockAuthProviderAndJwtSignIn,
signInAsAdminAndEnableEmbeddingSdk,
} from "e2e/support/helpers/component-testing-sdk";
import { getSdkRoot } from "e2e/support/helpers/e2e-embedding-sdk-helpers";
describeEE(
"scenarios > embedding-sdk > the redux provider context should not clash with the host app",
() => {
beforeEach(() => {
signInAsAdminAndEnableEmbeddingSdk();
cy.signOut();
mockAuthProviderAndJwtSignIn();
});
it("the host app redux logic should work normally even inside MetabaseProvider", () => {
cy.mount(
<Provider store={customerStore}>
<div data-testid="outside-metabase-provider">
<CounterButton />
</div>
<MetabaseProvider
authConfig={{
authProviderUri: AUTH_PROVIDER_URL,
metabaseInstanceUrl: METABASE_INSTANCE_URL,
}}
>
<div data-testid="inside-metabase-provider">
<CounterButton />
</div>
<StaticDashboard dashboardId={ORDERS_DASHBOARD_ID} withDownloads />
</MetabaseProvider>
</Provider>,
);
// the button should render and access the correct redux state both inside and outside the MetabaseProvider
cy.findByTestId("outside-metabase-provider")
.findByText("0")
.should("exist");
cy.findByTestId("inside-metabase-provider")
.findByText("0")
.should("exist");
// sanity check that it's actually working and actions work
cy.findByTestId("inside-metabase-provider").findByText("0").click();
cy.findByTestId("outside-metabase-provider")
.findByText("1")
.should("exist");
cy.findByTestId("inside-metabase-provider")
.findByText("1")
.should("exist");
// also make sure the sdk is working, most data goes through redux so if it renders
// it means it's working
getSdkRoot().findByText("Orders in a dashboard").should("exist");
getSdkRoot().findByText("Product ID").should("exist");
});
},
);
// sample code for customer app
const slice = createSlice({
name: "counter",
initialState: {
value: 0,
},
reducers: {
increment: state => {
state.value += 1;
},
},
});
const { increment } = slice.actions;
const customerStore = configureStore({
reducer: {
counter: slice.reducer,
},
});
const CounterButton = () => {
const count = useSelector(
(state: { counter: { value: number } }) => state.counter.value,
);
const dispatch = useDispatch();
return <button onClick={() => dispatch(increment())}>{count}</button>;
};
import { Global, css } from "@emotion/react"; import { Global, css } from "@emotion/react";
import { useMemo } from "react"; import { useMemo } from "react";
import { useSelector } from "react-redux";
import { defaultFontFiles } from "metabase/css/core/fonts.styled"; import { defaultFontFiles } from "metabase/css/core/fonts.styled";
import { useSelector } from "metabase/lib/redux";
import { getFontFiles } from "metabase/styled-components/selectors"; import { getFontFiles } from "metabase/styled-components/selectors";
/** /**
......
import type { Query } from "history"; import type { Query } from "history";
import type { ComponentType, FC } from "react"; import type { ComponentType, FC } from "react";
import { type ConnectedProps, connect } from "react-redux"; import type { ConnectedProps } from "react-redux";
import _ from "underscore"; import _ from "underscore";
import type { SdkPluginsConfig } from "embedding-sdk"; import type { SdkPluginsConfig } from "embedding-sdk";
...@@ -39,6 +39,7 @@ import type { ...@@ -39,6 +39,7 @@ import type {
DashboardRefreshPeriodControls, DashboardRefreshPeriodControls,
} from "metabase/dashboard/types"; } from "metabase/dashboard/types";
import { useValidatedEntityId } from "metabase/lib/entity-id/hooks/use-validated-entity-id"; import { useValidatedEntityId } from "metabase/lib/entity-id/hooks/use-validated-entity-id";
import { connect } from "metabase/lib/redux";
import type { PublicOrEmbeddedDashboardEventHandlersProps } from "metabase/public/containers/PublicOrEmbeddedDashboard/types"; import type { PublicOrEmbeddedDashboardEventHandlersProps } from "metabase/public/containers/PublicOrEmbeddedDashboard/types";
import { useDashboardLoadHandlers } from "metabase/public/containers/PublicOrEmbeddedDashboard/use-dashboard-load-handlers"; import { useDashboardLoadHandlers } from "metabase/public/containers/PublicOrEmbeddedDashboard/use-dashboard-load-handlers";
import { closeNavbar, setErrorPage } from "metabase/redux/app"; import { closeNavbar, setErrorPage } from "metabase/redux/app";
......
import { Global } from "@emotion/react"; import { Global } from "@emotion/react";
import type { Action, Store } from "@reduxjs/toolkit"; import type { Action, Store } from "@reduxjs/toolkit";
import { type JSX, type ReactNode, memo, useEffect, useRef } from "react"; import { type JSX, type ReactNode, memo, useEffect, useRef } from "react";
import { Provider } from "react-redux";
import { SdkThemeProvider } from "embedding-sdk/components/private/SdkThemeProvider"; import { SdkThemeProvider } from "embedding-sdk/components/private/SdkThemeProvider";
import { EMBEDDING_SDK_ROOT_ELEMENT_ID } from "embedding-sdk/config"; import { EMBEDDING_SDK_ROOT_ELEMENT_ID } from "embedding-sdk/config";
...@@ -22,6 +21,7 @@ import type { ...@@ -22,6 +21,7 @@ import type {
} from "embedding-sdk/store/types"; } from "embedding-sdk/store/types";
import type { MetabaseAuthConfig } from "embedding-sdk/types"; import type { MetabaseAuthConfig } from "embedding-sdk/types";
import type { MetabaseTheme } from "embedding-sdk/types/theme"; import type { MetabaseTheme } from "embedding-sdk/types/theme";
import { MetabaseReduxProvider } from "metabase/lib/redux";
import { LocaleProvider } from "metabase/public/LocaleProvider"; import { LocaleProvider } from "metabase/public/LocaleProvider";
import { setOptions } from "metabase/redux/embed"; import { setOptions } from "metabase/redux/embed";
import { EmotionCacheProvider } from "metabase/styled-components/components/EmotionCacheProvider"; import { EmotionCacheProvider } from "metabase/styled-components/components/EmotionCacheProvider";
...@@ -141,8 +141,8 @@ export const MetabaseProvider = memo(function MetabaseProvider( ...@@ -141,8 +141,8 @@ export const MetabaseProvider = memo(function MetabaseProvider(
} }
return ( return (
<Provider store={storeRef.current}> <MetabaseReduxProvider store={storeRef.current}>
<MetabaseProviderInternal store={storeRef.current} {...props} /> <MetabaseProviderInternal store={storeRef.current} {...props} />
</Provider> </MetabaseReduxProvider>
); );
}); });
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import { useDispatch } from "react-redux";
import type { MetabaseAuthConfig } from "embedding-sdk"; import type { MetabaseAuthConfig } from "embedding-sdk";
import { printUsageProblemToConsole } from "embedding-sdk/lib/print-usage-problem"; import { printUsageProblemToConsole } from "embedding-sdk/lib/print-usage-problem";
import { getSdkUsageProblem } from "embedding-sdk/lib/usage-problem"; import { getSdkUsageProblem } from "embedding-sdk/lib/usage-problem";
import { useSdkDispatch, useSdkSelector } from "embedding-sdk/store";
import { setUsageProblem } from "embedding-sdk/store/reducer"; import { setUsageProblem } from "embedding-sdk/store/reducer";
import { useSetting } from "metabase/common/hooks"; import { useSetting } from "metabase/common/hooks";
import { useSelector } from "metabase/lib/redux";
import { getTokenFeature } from "metabase/setup/selectors"; import { getTokenFeature } from "metabase/setup/selectors";
export function useSdkUsageProblem({ export function useSdkUsageProblem({
...@@ -18,7 +17,7 @@ export function useSdkUsageProblem({ ...@@ -18,7 +17,7 @@ export function useSdkUsageProblem({
}) { }) {
const hasLoggedRef = useRef(false); const hasLoggedRef = useRef(false);
const dispatch = useDispatch(); const dispatch = useSdkDispatch();
// When the setting haven't been loaded or failed to query, we assume that the // When the setting haven't been loaded or failed to query, we assume that the
// feature is _enabled_ first. Otherwise, when a user's instance is temporarily down, // feature is _enabled_ first. Otherwise, when a user's instance is temporarily down,
...@@ -26,7 +25,7 @@ export function useSdkUsageProblem({ ...@@ -26,7 +25,7 @@ export function useSdkUsageProblem({
// TODO: replace this with "enable-embedding-sdk" once the settings PR landed. // TODO: replace this with "enable-embedding-sdk" once the settings PR landed.
const isEnabled = useSetting("enable-embedding") ?? true; const isEnabled = useSetting("enable-embedding") ?? true;
const hasTokenFeature = useSelector(state => { const hasTokenFeature = useSdkSelector(state => {
// We also assume that the feature is enabled if the token-features are missing. // We also assume that the feature is enabled if the token-features are missing.
// Same reason as above. // Same reason as above.
if (!state.settings.values?.["token-features"]) { if (!state.settings.values?.["token-features"]) {
......
import { useSelector } from "react-redux"; import { useSelector } from "metabase/lib/redux";
import { getUser } from "metabase/selectors/user"; import { getUser } from "metabase/selectors/user";
export const useCurrentUser = () => useSelector(getUser); export const useCurrentUser = () => useSelector(getUser);
import type { /* eslint-disable no-restricted-imports */
AnyAction, import type { AnyAction, Reducer, Store } from "@reduxjs/toolkit";
Reducer,
Store,
ThunkDispatch,
} from "@reduxjs/toolkit";
import { useContext } from "react"; import { useContext } from "react";
import { ReactReduxContext, useDispatch, useStore } from "react-redux";
import {
MetabaseReduxContext,
useDispatch,
useStore,
} from "metabase/lib/redux";
import { mainReducers } from "metabase/reducers-main"; import { mainReducers } from "metabase/reducers-main";
import { getStore } from "metabase/store"; import { getStore } from "metabase/store";
...@@ -29,11 +29,7 @@ export const getSdkStore = () => ...@@ -29,11 +29,7 @@ export const getSdkStore = () =>
}, },
}) as unknown as Store<SdkStoreState, AnyAction>; }) as unknown as Store<SdkStoreState, AnyAction>;
export const useSdkDispatch: () => ThunkDispatch< export const useSdkDispatch = () => {
SdkStoreState,
void,
AnyAction
> = () => {
useCheckSdkReduxContext(); useCheckSdkReduxContext();
return useDispatch(); return useDispatch();
...@@ -46,7 +42,7 @@ export const useSdkStore = () => { ...@@ -46,7 +42,7 @@ export const useSdkStore = () => {
}; };
const useCheckSdkReduxContext = () => { const useCheckSdkReduxContext = () => {
const context = useContext(ReactReduxContext); const context = useContext(MetabaseReduxContext);
if (!context) { if (!context) {
console.warn( console.warn(
......
import { useContext } from "react"; import { useContext } from "react";
import { import type { TypedUseSelectorHook } from "react-redux";
ReactReduxContext, import { createSelectorHook } from "react-redux";
type TypedUseSelectorHook,
useSelector,
} from "react-redux";
import type { SdkStoreState } from "embedding-sdk/store/types"; import type { SdkStoreState } from "embedding-sdk/store/types";
import { MetabaseReduxContext } from "metabase/lib/redux";
// eslint-disable-next-line no-literal-metabase-strings -- this string only shows in the console. // eslint-disable-next-line no-literal-metabase-strings -- this string only shows in the console.
export const USE_OUTSIDE_OF_CONTEXT_MESSAGE = `Hooks from the Metabase Embedding SDK must be used within a component wrapped by the MetabaseProvider`; export const USE_OUTSIDE_OF_CONTEXT_MESSAGE = `Hooks from the Metabase Embedding SDK must be used within a component wrapped by the MetabaseProvider`;
const _useSdkSelector: TypedUseSelectorHook<SdkStoreState> =
createSelectorHook(MetabaseReduxContext);
export const useSdkSelector: TypedUseSelectorHook<SdkStoreState> = ( export const useSdkSelector: TypedUseSelectorHook<SdkStoreState> = (
selector, selector,
options, options,
) => { ) => {
const context = useContext(ReactReduxContext); const context = useContext(MetabaseReduxContext);
if (!context) { if (!context) {
throw new Error(USE_OUTSIDE_OF_CONTEXT_MESSAGE); throw new Error(USE_OUTSIDE_OF_CONTEXT_MESSAGE);
} }
return useSelector(selector, options); // @ts-expect-error -- weird error on the options type
return _useSdkSelector(selector, options);
}; };
import { useCallback } from "react"; import { useCallback } from "react";
import { useSelector } from "react-redux";
import { withRouter } from "react-router"; import { withRouter } from "react-router";
import { push } from "react-router-redux"; import { push } from "react-router-redux";
import { useAsyncFn, useMount } from "react-use"; import { useAsyncFn, useMount } from "react-use";
...@@ -17,11 +16,11 @@ import { useDispatch } from "metabase/lib/redux"; ...@@ -17,11 +16,11 @@ import { useDispatch } from "metabase/lib/redux";
import { updateImpersonation } from "metabase-enterprise/advanced_permissions/reducer"; import { updateImpersonation } from "metabase-enterprise/advanced_permissions/reducer";
import { getImpersonation } from "metabase-enterprise/advanced_permissions/selectors"; import { getImpersonation } from "metabase-enterprise/advanced_permissions/selectors";
import type { import type {
AdvancedPermissionsStoreState,
ImpersonationModalParams, ImpersonationModalParams,
ImpersonationParams, ImpersonationParams,
} from "metabase-enterprise/advanced_permissions/types"; } from "metabase-enterprise/advanced_permissions/types";
import { getImpersonatedDatabaseId } from "metabase-enterprise/advanced_permissions/utils"; import { getImpersonatedDatabaseId } from "metabase-enterprise/advanced_permissions/utils";
import { useEnterpriseSelector } from "metabase-enterprise/redux";
import { ImpersonationApi } from "metabase-enterprise/services"; import { ImpersonationApi } from "metabase-enterprise/services";
import { fetchUserAttributes } from "metabase-enterprise/shared/reducer"; import { fetchUserAttributes } from "metabase-enterprise/shared/reducer";
import { getUserAttributes } from "metabase-enterprise/shared/selectors"; import { getUserAttributes } from "metabase-enterprise/shared/selectors";
...@@ -76,11 +75,10 @@ const _ImpersonationModal = ({ route, params }: ImpersonationModalProps) => { ...@@ -76,11 +75,10 @@ const _ImpersonationModal = ({ route, params }: ImpersonationModalProps) => {
id: databaseId, id: databaseId,
}); });
const attributes = useSelector(getUserAttributes); const attributes = useEnterpriseSelector(getUserAttributes);
const draftImpersonation = useSelector< const draftImpersonation = useEnterpriseSelector(
AdvancedPermissionsStoreState, getImpersonation(databaseId, groupId),
Impersonation | undefined );
>(getImpersonation(databaseId, groupId));
const selectedAttribute = const selectedAttribute =
draftImpersonation?.attribute ?? impersonation?.attribute; draftImpersonation?.attribute ?? impersonation?.attribute;
......
import { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
import { connect } from "react-redux";
import type { Route } from "react-router"; import type { Route } from "react-router";
import _ from "underscore"; import _ from "underscore";
...@@ -7,6 +6,7 @@ import { ApplicationPermissionsHelp } from "metabase/admin/permissions/component ...@@ -7,6 +6,7 @@ import { ApplicationPermissionsHelp } from "metabase/admin/permissions/component
import { PermissionsEditor } from "metabase/admin/permissions/components/PermissionsEditor"; import { PermissionsEditor } from "metabase/admin/permissions/components/PermissionsEditor";
import PermissionsPageLayout from "metabase/admin/permissions/components/PermissionsPageLayout"; import PermissionsPageLayout from "metabase/admin/permissions/components/PermissionsPageLayout";
import Groups from "metabase/entities/groups"; import Groups from "metabase/entities/groups";
import { connect } from "metabase/lib/redux";
import { import {
initializeApplicationPermissions, initializeApplicationPermissions,
saveApplicationPermissions, saveApplicationPermissions,
......
...@@ -3,13 +3,13 @@ import "../components/AuditTableVisualization"; ...@@ -3,13 +3,13 @@ import "../components/AuditTableVisualization";
import { chain } from "icepick"; import { chain } from "icepick";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useState } from "react"; import { useState } from "react";
import { connect } from "react-redux";
import { push } from "react-router-redux"; import { push } from "react-router-redux";
import _ from "underscore"; import _ from "underscore";
import { PaginationControls } from "metabase/components/PaginationControls"; import { PaginationControls } from "metabase/components/PaginationControls";
import CS from "metabase/css/core/index.css"; import CS from "metabase/css/core/index.css";
import { usePagination } from "metabase/hooks/use-pagination"; import { usePagination } from "metabase/hooks/use-pagination";
import { connect } from "metabase/lib/redux";
import { getMetadata } from "metabase/selectors/metadata"; import { getMetadata } from "metabase/selectors/metadata";
import Question from "metabase-lib/v1/Question"; import Question from "metabase-lib/v1/Question";
......
import { connect } from "react-redux";
import { t } from "ttag"; import { t } from "ttag";
import _ from "underscore"; import _ from "underscore";
import Users from "metabase/entities/users"; import Users from "metabase/entities/users";
import { connect } from "metabase/lib/redux";
import { addUndo } from "metabase/redux/undo"; import { addUndo } from "metabase/redux/undo";
import { AuditApi } from "metabase-enterprise/services"; import { AuditApi } from "metabase-enterprise/services";
......
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { connect } from "react-redux";
import { t } from "ttag"; import { t } from "ttag";
import _ from "underscore"; import _ from "underscore";
...@@ -19,6 +18,7 @@ import { ...@@ -19,6 +18,7 @@ import {
FormSwitch, FormSwitch,
FormTextInput, FormTextInput,
} from "metabase/forms"; } from "metabase/forms";
import { connect } from "metabase/lib/redux";
import { Flex, Stack, rem } from "metabase/ui"; import { Flex, Stack, rem } from "metabase/ui";
import type { SettingValue } from "metabase-types/api"; import type { SettingValue } from "metabase-types/api";
......
import cx from "classnames"; import cx from "classnames";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { connect } from "react-redux";
import { jt, t } from "ttag"; import { jt, t } from "ttag";
import _ from "underscore"; import _ from "underscore";
...@@ -23,6 +22,7 @@ import { ...@@ -23,6 +22,7 @@ import {
FormTextInput, FormTextInput,
FormTextarea, FormTextarea,
} from "metabase/forms"; } from "metabase/forms";
import { connect } from "metabase/lib/redux";
import { Stack } from "metabase/ui"; import { Stack } from "metabase/ui";
import { import {
......
import { connect } from "react-redux";
import { t } from "ttag"; import { t } from "ttag";
import AuthCard from "metabase/admin/settings/auth/components/AuthCard"; import AuthCard from "metabase/admin/settings/auth/components/AuthCard";
import { updateSettings } from "metabase/admin/settings/settings"; import { updateSettings } from "metabase/admin/settings/settings";
import { connect } from "metabase/lib/redux";
import { getSetting } from "metabase/selectors/settings"; import { getSetting } from "metabase/selectors/settings";
import type { Dispatch, State } from "metabase-types/store"; import type { Dispatch, State } from "metabase-types/store";
......
import { connect } from "react-redux";
import { t } from "ttag"; import { t } from "ttag";
import AuthCard from "metabase/admin/settings/auth/components/AuthCard"; import AuthCard from "metabase/admin/settings/auth/components/AuthCard";
import { updateSettings } from "metabase/admin/settings/settings"; import { updateSettings } from "metabase/admin/settings/settings";
import { connect } from "metabase/lib/redux";
import { getSetting } from "metabase/selectors/settings"; import { getSetting } from "metabase/selectors/settings";
import type { Dispatch, State } from "metabase-types/store"; import type { Dispatch, State } from "metabase-types/store";
......
import moment from "moment-timezone"; // eslint-disable-line no-restricted-imports -- deprecated usage import moment from "moment-timezone"; // eslint-disable-line no-restricted-imports -- deprecated usage
import { connect } from "react-redux";
import { jt, t } from "ttag"; import { jt, t } from "ttag";
import { LicenseInput } from "metabase/admin/settings/components/LicenseInput"; import { LicenseInput } from "metabase/admin/settings/components/LicenseInput";
...@@ -13,6 +12,7 @@ import { ...@@ -13,6 +12,7 @@ import {
import { ExplorePlansIllustration } from "metabase/admin/settings/components/SettingsLicense/ExplorePlansIllustration"; import { ExplorePlansIllustration } from "metabase/admin/settings/components/SettingsLicense/ExplorePlansIllustration";
import LoadingSpinner from "metabase/components/LoadingSpinner"; import LoadingSpinner from "metabase/components/LoadingSpinner";
import ExternalLink from "metabase/core/components/ExternalLink"; import ExternalLink from "metabase/core/components/ExternalLink";
import { connect } from "metabase/lib/redux";
import { getUpgradeUrl } from "metabase/selectors/settings"; import { getUpgradeUrl } from "metabase/selectors/settings";
import { useGetBillingInfoQuery } from "metabase-enterprise/api"; import { useGetBillingInfoQuery } from "metabase-enterprise/api";
import { showLicenseAcceptedToast } from "metabase-enterprise/license/actions"; import { showLicenseAcceptedToast } from "metabase-enterprise/license/actions";
......
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { Fragment } from "react"; import { Fragment } from "react";
import { connect } from "react-redux";
import { useEditItemVerificationMutation } from "metabase/api"; import { useEditItemVerificationMutation } from "metabase/api";
import { connect } from "metabase/lib/redux";
import { getIsModerator } from "metabase-enterprise/moderation/selectors"; import { getIsModerator } from "metabase-enterprise/moderation/selectors";
import { getLatestModerationReview } from "metabase-enterprise/moderation/service"; import { getLatestModerationReview } from "metabase-enterprise/moderation/service";
......
import { connect } from "react-redux";
import _ from "underscore"; import _ from "underscore";
import { connect } from "metabase/lib/redux";
import { getUser } from "metabase/selectors/user"; import { getUser } from "metabase/selectors/user";
import type { State } from "metabase-types/store"; import type { State } from "metabase-types/store";
......
import type { TypedUseSelectorHook } from "react-redux";
import { createSelectorHook } from "react-redux";
import { MetabaseReduxContext } from "metabase/lib/redux";
// TODO: use the real type after we figure out what it is
type EnterpriseState = any;
export const useEnterpriseSelector: TypedUseSelectorHook<EnterpriseState> =
createSelectorHook(MetabaseReduxContext);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment