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 { useMemo } from "react";
import { useSelector } from "react-redux";
import { defaultFontFiles } from "metabase/css/core/fonts.styled";
import { useSelector } from "metabase/lib/redux";
import { getFontFiles } from "metabase/styled-components/selectors";
/**
......
import type { Query } from "history";
import type { ComponentType, FC } from "react";
import { type ConnectedProps, connect } from "react-redux";
import type { ConnectedProps } from "react-redux";
import _ from "underscore";
import type { SdkPluginsConfig } from "embedding-sdk";
......@@ -39,6 +39,7 @@ import type {
DashboardRefreshPeriodControls,
} from "metabase/dashboard/types";
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 { useDashboardLoadHandlers } from "metabase/public/containers/PublicOrEmbeddedDashboard/use-dashboard-load-handlers";
import { closeNavbar, setErrorPage } from "metabase/redux/app";
......
import { Global } from "@emotion/react";
import type { Action, Store } from "@reduxjs/toolkit";
import { type JSX, type ReactNode, memo, useEffect, useRef } from "react";
import { Provider } from "react-redux";
import { SdkThemeProvider } from "embedding-sdk/components/private/SdkThemeProvider";
import { EMBEDDING_SDK_ROOT_ELEMENT_ID } from "embedding-sdk/config";
......@@ -22,6 +21,7 @@ import type {
} from "embedding-sdk/store/types";
import type { MetabaseAuthConfig } from "embedding-sdk/types";
import type { MetabaseTheme } from "embedding-sdk/types/theme";
import { MetabaseReduxProvider } from "metabase/lib/redux";
import { LocaleProvider } from "metabase/public/LocaleProvider";
import { setOptions } from "metabase/redux/embed";
import { EmotionCacheProvider } from "metabase/styled-components/components/EmotionCacheProvider";
......@@ -141,8 +141,8 @@ export const MetabaseProvider = memo(function MetabaseProvider(
}
return (
<Provider store={storeRef.current}>
<MetabaseReduxProvider store={storeRef.current}>
<MetabaseProviderInternal store={storeRef.current} {...props} />
</Provider>
</MetabaseReduxProvider>
);
});
import { useEffect, useMemo, useRef } from "react";
import { useDispatch } from "react-redux";
import type { MetabaseAuthConfig } from "embedding-sdk";
import { printUsageProblemToConsole } from "embedding-sdk/lib/print-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 { useSetting } from "metabase/common/hooks";
import { useSelector } from "metabase/lib/redux";
import { getTokenFeature } from "metabase/setup/selectors";
export function useSdkUsageProblem({
......@@ -18,7 +17,7 @@ export function useSdkUsageProblem({
}) {
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
// feature is _enabled_ first. Otherwise, when a user's instance is temporarily down,
......@@ -26,7 +25,7 @@ export function useSdkUsageProblem({
// TODO: replace this with "enable-embedding-sdk" once the settings PR landed.
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.
// Same reason as above.
if (!state.settings.values?.["token-features"]) {
......
import { useSelector } from "react-redux";
import { useSelector } from "metabase/lib/redux";
import { getUser } from "metabase/selectors/user";
export const useCurrentUser = () => useSelector(getUser);
import type {
AnyAction,
Reducer,
Store,
ThunkDispatch,
} from "@reduxjs/toolkit";
/* eslint-disable no-restricted-imports */
import type { AnyAction, Reducer, Store } from "@reduxjs/toolkit";
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 { getStore } from "metabase/store";
......@@ -29,11 +29,7 @@ export const getSdkStore = () =>
},
}) as unknown as Store<SdkStoreState, AnyAction>;
export const useSdkDispatch: () => ThunkDispatch<
SdkStoreState,
void,
AnyAction
> = () => {
export const useSdkDispatch = () => {
useCheckSdkReduxContext();
return useDispatch();
......@@ -46,7 +42,7 @@ export const useSdkStore = () => {
};
const useCheckSdkReduxContext = () => {
const context = useContext(ReactReduxContext);
const context = useContext(MetabaseReduxContext);
if (!context) {
console.warn(
......
import { useContext } from "react";
import {
ReactReduxContext,
type TypedUseSelectorHook,
useSelector,
} from "react-redux";
import type { TypedUseSelectorHook } from "react-redux";
import { createSelectorHook } from "react-redux";
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.
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> = (
selector,
options,
) => {
const context = useContext(ReactReduxContext);
const context = useContext(MetabaseReduxContext);
if (!context) {
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 { useSelector } from "react-redux";
import { withRouter } from "react-router";
import { push } from "react-router-redux";
import { useAsyncFn, useMount } from "react-use";
......@@ -17,11 +16,11 @@ import { useDispatch } from "metabase/lib/redux";
import { updateImpersonation } from "metabase-enterprise/advanced_permissions/reducer";
import { getImpersonation } from "metabase-enterprise/advanced_permissions/selectors";
import type {
AdvancedPermissionsStoreState,
ImpersonationModalParams,
ImpersonationParams,
} from "metabase-enterprise/advanced_permissions/types";
import { getImpersonatedDatabaseId } from "metabase-enterprise/advanced_permissions/utils";
import { useEnterpriseSelector } from "metabase-enterprise/redux";
import { ImpersonationApi } from "metabase-enterprise/services";
import { fetchUserAttributes } from "metabase-enterprise/shared/reducer";
import { getUserAttributes } from "metabase-enterprise/shared/selectors";
......@@ -76,11 +75,10 @@ const _ImpersonationModal = ({ route, params }: ImpersonationModalProps) => {
id: databaseId,
});
const attributes = useSelector(getUserAttributes);
const draftImpersonation = useSelector<
AdvancedPermissionsStoreState,
Impersonation | undefined
>(getImpersonation(databaseId, groupId));
const attributes = useEnterpriseSelector(getUserAttributes);
const draftImpersonation = useEnterpriseSelector(
getImpersonation(databaseId, groupId),
);
const selectedAttribute =
draftImpersonation?.attribute ?? impersonation?.attribute;
......
import { useCallback, useEffect } from "react";
import { connect } from "react-redux";
import type { Route } from "react-router";
import _ from "underscore";
......@@ -7,6 +6,7 @@ import { ApplicationPermissionsHelp } from "metabase/admin/permissions/component
import { PermissionsEditor } from "metabase/admin/permissions/components/PermissionsEditor";
import PermissionsPageLayout from "metabase/admin/permissions/components/PermissionsPageLayout";
import Groups from "metabase/entities/groups";
import { connect } from "metabase/lib/redux";
import {
initializeApplicationPermissions,
saveApplicationPermissions,
......
......@@ -3,13 +3,13 @@ import "../components/AuditTableVisualization";
import { chain } from "icepick";
import PropTypes from "prop-types";
import { useState } from "react";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import _ from "underscore";
import { PaginationControls } from "metabase/components/PaginationControls";
import CS from "metabase/css/core/index.css";
import { usePagination } from "metabase/hooks/use-pagination";
import { connect } from "metabase/lib/redux";
import { getMetadata } from "metabase/selectors/metadata";
import Question from "metabase-lib/v1/Question";
......
import { connect } from "react-redux";
import { t } from "ttag";
import _ from "underscore";
import Users from "metabase/entities/users";
import { connect } from "metabase/lib/redux";
import { addUndo } from "metabase/redux/undo";
import { AuditApi } from "metabase-enterprise/services";
......
import { useCallback, useMemo } from "react";
import { connect } from "react-redux";
import { t } from "ttag";
import _ from "underscore";
......@@ -19,6 +18,7 @@ import {
FormSwitch,
FormTextInput,
} from "metabase/forms";
import { connect } from "metabase/lib/redux";
import { Flex, Stack, rem } from "metabase/ui";
import type { SettingValue } from "metabase-types/api";
......
import cx from "classnames";
import PropTypes from "prop-types";
import { useCallback, useMemo } from "react";
import { connect } from "react-redux";
import { jt, t } from "ttag";
import _ from "underscore";
......@@ -23,6 +22,7 @@ import {
FormTextInput,
FormTextarea,
} from "metabase/forms";
import { connect } from "metabase/lib/redux";
import { Stack } from "metabase/ui";
import {
......
import { connect } from "react-redux";
import { t } from "ttag";
import AuthCard from "metabase/admin/settings/auth/components/AuthCard";
import { updateSettings } from "metabase/admin/settings/settings";
import { connect } from "metabase/lib/redux";
import { getSetting } from "metabase/selectors/settings";
import type { Dispatch, State } from "metabase-types/store";
......
import { connect } from "react-redux";
import { t } from "ttag";
import AuthCard from "metabase/admin/settings/auth/components/AuthCard";
import { updateSettings } from "metabase/admin/settings/settings";
import { connect } from "metabase/lib/redux";
import { getSetting } from "metabase/selectors/settings";
import type { Dispatch, State } from "metabase-types/store";
......
import moment from "moment-timezone"; // eslint-disable-line no-restricted-imports -- deprecated usage
import { connect } from "react-redux";
import { jt, t } from "ttag";
import { LicenseInput } from "metabase/admin/settings/components/LicenseInput";
......@@ -13,6 +12,7 @@ import {
import { ExplorePlansIllustration } from "metabase/admin/settings/components/SettingsLicense/ExplorePlansIllustration";
import LoadingSpinner from "metabase/components/LoadingSpinner";
import ExternalLink from "metabase/core/components/ExternalLink";
import { connect } from "metabase/lib/redux";
import { getUpgradeUrl } from "metabase/selectors/settings";
import { useGetBillingInfoQuery } from "metabase-enterprise/api";
import { showLicenseAcceptedToast } from "metabase-enterprise/license/actions";
......
import PropTypes from "prop-types";
import { Fragment } from "react";
import { connect } from "react-redux";
import { useEditItemVerificationMutation } from "metabase/api";
import { connect } from "metabase/lib/redux";
import { getIsModerator } from "metabase-enterprise/moderation/selectors";
import { getLatestModerationReview } from "metabase-enterprise/moderation/service";
......
import { connect } from "react-redux";
import _ from "underscore";
import { connect } from "metabase/lib/redux";
import { getUser } from "metabase/selectors/user";
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