diff --git a/enterprise/frontend/src/embedding-sdk/components/private/AppInitializeController.tsx b/enterprise/frontend/src/embedding-sdk/components/private/AppInitializeController.tsx index 939e008f55cbf3a396f079da72dbc1fc249bb10e..c6d29ccfb5c99d7fd6e3adb6645588cd739b22d7 100644 --- a/enterprise/frontend/src/embedding-sdk/components/private/AppInitializeController.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/private/AppInitializeController.tsx @@ -5,8 +5,9 @@ import { DEFAULT_FONT, EMBEDDING_SDK_ROOT_ELEMENT_ID, } from "embedding-sdk/config"; -import { EmbeddingContext } from "embedding-sdk/context"; import { useInitData } from "embedding-sdk/hooks"; +import { useSdkSelector } from "embedding-sdk/store"; +import { getIsInitialized } from "embedding-sdk/store/selectors"; import type { SDKConfigType } from "embedding-sdk/types"; import { SdkContentWrapper } from "./SdkContentWrapper"; @@ -20,23 +21,18 @@ export const AppInitializeController = ({ config, children, }: AppInitializeControllerProps) => { - const { isLoggedIn, isInitialized } = useInitData({ + useInitData({ config, }); + const isInitialized = useSdkSelector(getIsInitialized); + return ( - <EmbeddingContext.Provider - value={{ - isInitialized, - isLoggedIn, - }} + <SdkContentWrapper + id={EMBEDDING_SDK_ROOT_ELEMENT_ID} + font={config.font ?? DEFAULT_FONT} > - <SdkContentWrapper - id={EMBEDDING_SDK_ROOT_ELEMENT_ID} - font={config.font ?? DEFAULT_FONT} - > - {!isInitialized ? <div>{t`Loading…`}</div> : children} - </SdkContentWrapper> - </EmbeddingContext.Provider> + {!isInitialized ? <div>{t`Loading…`}</div> : children} + </SdkContentWrapper> ); }; diff --git a/enterprise/frontend/src/embedding-sdk/components/public/InteractiveQuestion.tsx b/enterprise/frontend/src/embedding-sdk/components/public/InteractiveQuestion.tsx index 6452032507c49afd19f2c4bcdf44ebfec417ecbe..ed5d7d3cf2a15d33fcf9310bd412e74762fde1f4 100644 --- a/enterprise/frontend/src/embedding-sdk/components/public/InteractiveQuestion.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/public/InteractiveQuestion.tsx @@ -1,6 +1,8 @@ import cx from "classnames"; import { useEffect } from "react"; +import { useSdkSelector } from "embedding-sdk/store"; +import { getIsInitialized, getIsLoggedIn } from "embedding-sdk/store/selectors"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; import CS from "metabase/css/core/index.css"; import { useDispatch, useSelector } from "metabase/lib/redux"; @@ -21,8 +23,6 @@ import { Group, Stack } from "metabase/ui"; import { getEmbeddingMode } from "metabase/visualizations/click-actions/lib/modes"; import type { CardId } from "metabase-types/api"; -import { useEmbeddingContext } from "../../context"; - interface InteractiveQuestionProps { questionId: CardId; } @@ -30,7 +30,9 @@ interface InteractiveQuestionProps { export const InteractiveQuestion = ({ questionId, }: InteractiveQuestionProps): JSX.Element | null => { - const { isInitialized, isLoggedIn } = useEmbeddingContext(); + const isInitialized = useSdkSelector(getIsInitialized); + const isLoggedIn = useSdkSelector(getIsLoggedIn); + const dispatch = useDispatch(); const question = useSelector(getQuestion); const mode = question && getEmbeddingMode(question); diff --git a/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion.tsx b/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion.tsx index 8d0aa5b5624872f794c063e0fe2e3e7741b1249e..801bf5e0b556ce7a5743c42b2f2d868297dbff2e 100644 --- a/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion.tsx @@ -1,6 +1,8 @@ import cx from "classnames"; import { useEffect, useState } from "react"; +import { useSdkSelector } from "embedding-sdk/store"; +import { getIsInitialized, getIsLoggedIn } from "embedding-sdk/store/selectors"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; import CS from "metabase/css/core/index.css"; import { useSelector } from "metabase/lib/redux"; @@ -18,8 +20,6 @@ import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMo import Question from "metabase-lib/v1/Question"; import type { Card, CardId, Dataset } from "metabase-types/api"; -import { useEmbeddingContext } from "../../context"; - interface QueryVisualizationProps { questionId: CardId; showVisualizationSelector?: boolean; @@ -37,7 +37,9 @@ export const StaticQuestion = ({ questionId, showVisualizationSelector, }: QueryVisualizationProps): JSX.Element | null => { - const { isInitialized, isLoggedIn } = useEmbeddingContext(); + const isInitialized = useSdkSelector(getIsInitialized); + const isLoggedIn = useSdkSelector(getIsLoggedIn); + const metadata = useSelector(getMetadata); const [{ loading, card, result, cardError, resultError }, setState] = diff --git a/enterprise/frontend/src/embedding-sdk/context.ts b/enterprise/frontend/src/embedding-sdk/context.ts deleted file mode 100644 index bac2f6c52704c7b50ee16a31947ad47ee4f778e3..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/embedding-sdk/context.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createContext, useContext } from "react"; - -interface EmbeddingSdkContextData { - isInitialized: boolean; - isLoggedIn: boolean; -} - -export const EmbeddingContext = createContext<EmbeddingSdkContextData>({ - isInitialized: false, - isLoggedIn: false, -}); - -export const useEmbeddingContext = () => { - return useContext(EmbeddingContext); -}; diff --git a/enterprise/frontend/src/embedding-sdk/hooks/private/use-init-data.ts b/enterprise/frontend/src/embedding-sdk/hooks/private/use-init-data.ts index 43dab7b993a3d1cc95a634dc142922ed16aefd7d..71a7214f9a60316788194bb22da6070dba189898 100644 --- a/enterprise/frontend/src/embedding-sdk/hooks/private/use-init-data.ts +++ b/enterprise/frontend/src/embedding-sdk/hooks/private/use-init-data.ts @@ -1,16 +1,21 @@ import { useEffect, useState } from "react"; import _ from "underscore"; -import { store } from "embedding-sdk/store"; +import { store, useSdkDispatch, useSdkSelector } from "embedding-sdk/store"; import { getOrRefreshSession, - getSessionTokenState, + setIsInitialized, + setIsLoggedIn, } from "embedding-sdk/store/reducer"; +import { + getIsInitialized, + getIsLoggedIn, + getSessionTokenState, +} from "embedding-sdk/store/selectors"; import type { EmbeddingSessionTokenState } from "embedding-sdk/store/types"; import type { SDKConfigType } from "embedding-sdk/types"; import { reloadSettings } from "metabase/admin/settings/settings"; import api from "metabase/lib/api"; -import { useDispatch } from "metabase/lib/redux"; import { refreshCurrentUser } from "metabase/redux/user"; import registerVisualizations from "metabase/visualizations/register"; @@ -26,10 +31,11 @@ export const useInitData = ({ isLoggedIn: boolean; isInitialized: boolean; } => { - const dispatch = useDispatch(); + const dispatch = useSdkDispatch(); + + const isInitialized = useSdkSelector(getIsInitialized); + const isLoggedIn = useSdkSelector(getIsLoggedIn); - const [isInitialized, setIsInitialized] = useState(false); - const [isLoggedIn, setIsLoggedIn] = useState(false); const [sessionTokenState, setSessionTokenState] = useState<EmbeddingSessionTokenState | null>(null); @@ -68,7 +74,7 @@ export const useInitData = ({ } else if (config.authType === "apiKey" && config.apiKey) { api.apiKey = config.apiKey; } else { - setIsLoggedIn(false); + dispatch(setIsLoggedIn(false)); return; } @@ -76,8 +82,8 @@ export const useInitData = ({ dispatch(refreshCurrentUser()), dispatch(reloadSettings()), ]).then(() => { - setIsInitialized(true); - setIsLoggedIn(true); + dispatch(setIsInitialized(true)); + dispatch(setIsLoggedIn(true)); }); }, [config, dispatch, sessionTokenState]); diff --git a/enterprise/frontend/src/embedding-sdk/hooks/public/use-question-search.ts b/enterprise/frontend/src/embedding-sdk/hooks/public/use-question-search.ts index e8b16893e8ce861351a8c3ae8786115b0c657736..560d6702ddeccd5af10bc65cbbcb5c35639bb4b1 100644 --- a/enterprise/frontend/src/embedding-sdk/hooks/public/use-question-search.ts +++ b/enterprise/frontend/src/embedding-sdk/hooks/public/use-question-search.ts @@ -1,10 +1,12 @@ import { useMemo } from "react"; -import { useEmbeddingContext } from "embedding-sdk/context"; +import { useSdkSelector } from "embedding-sdk/store"; +import { getIsInitialized, getIsLoggedIn } from "embedding-sdk/store/selectors"; import { useSearchListQuery } from "metabase/common/hooks"; export const useQuestionSearch = (searchQuery?: string) => { - const { isInitialized, isLoggedIn } = useEmbeddingContext(); + const isInitialized = useSdkSelector(getIsInitialized); + const isLoggedIn = useSdkSelector(getIsLoggedIn); const query = useMemo(() => { return searchQuery diff --git a/enterprise/frontend/src/embedding-sdk/store/index.ts b/enterprise/frontend/src/embedding-sdk/store/index.ts index d2838a30095d2ef199da3a60d5981b35f92db1f0..02317a458c2a171cda0e48211830ac19e3856ec2 100644 --- a/enterprise/frontend/src/embedding-sdk/store/index.ts +++ b/enterprise/frontend/src/embedding-sdk/store/index.ts @@ -1,17 +1,27 @@ -import type { AnyAction, Store } from "@reduxjs/toolkit"; +import type { AnyAction, Store, ThunkDispatch } from "@reduxjs/toolkit"; +import type { TypedUseSelectorHook } from "react-redux"; +import { useSelector, useDispatch } from "react-redux"; -import { tokenReducer } from "embedding-sdk/store/reducer"; -import type { SdkState } from "embedding-sdk/store/types"; +import type { SdkStoreState } from "embedding-sdk/store/types"; import reducers from "metabase/reducers-main"; import { getStore } from "metabase/store"; +import { sdk } from "./reducer"; + const SDK_REDUCERS = { ...reducers, - embeddingSessionToken: tokenReducer, + sdk, }; export const store = getStore(SDK_REDUCERS, null, { embed: { isEmbeddingSdk: true, }, -}) as unknown as Store<SdkState, AnyAction>; +}) as unknown as Store<SdkStoreState, AnyAction>; + +export const useSdkSelector: TypedUseSelectorHook<SdkStoreState> = useSelector; +export const useSdkDispatch: () => ThunkDispatch< + SdkStoreState, + void, + AnyAction +> = useDispatch; diff --git a/enterprise/frontend/src/embedding-sdk/store/reducer.ts b/enterprise/frontend/src/embedding-sdk/store/reducer.ts index ae82c63428a9aab83cd9bbb88c2095df4a2a24d7..ad0f3cc46d3d0be4b071772353df1177de78729e 100644 --- a/enterprise/frontend/src/embedding-sdk/store/reducer.ts +++ b/enterprise/frontend/src/embedding-sdk/store/reducer.ts @@ -1,26 +1,25 @@ +import type { PayloadAction } from "@reduxjs/toolkit"; import { createReducer } from "@reduxjs/toolkit"; +import { createAction } from "redux-actions"; -import type { - EmbeddingSessionTokenState, - SdkState, -} from "embedding-sdk/store/types"; +import type { SdkState, SdkStoreState } from "embedding-sdk/store/types"; import { createAsyncThunk } from "metabase/lib/redux"; -const initialState: EmbeddingSessionTokenState = { - token: null, - loading: false, - error: null, -}; +import { getSessionTokenState } from "./selectors"; + +const SET_IS_LOGGED_IN = "sdk/SET_IS_LOGGED_IN"; +const SET_IS_INITIALIZED = "sdk/SET_IS_INITIALIZED"; -export const getSessionTokenState = (state: SdkState) => - state.embeddingSessionToken; +export const setIsLoggedIn = createAction<boolean>(SET_IS_LOGGED_IN); +export const setIsInitialized = createAction<boolean>(SET_IS_INITIALIZED); -const GET_OR_REFRESH_SESSION = "embeddingSessionToken/GET_OR_REFRESH_SESSION"; +const GET_OR_REFRESH_SESSION = "sdk/token/GET_OR_REFRESH_SESSION"; +const REFRESH_TOKEN = "sdk/token/REFRESH_TOKEN"; export const getOrRefreshSession = createAsyncThunk( GET_OR_REFRESH_SESSION, async (url: string, { dispatch, getState }) => { - const state = getSessionTokenState(getState() as SdkState); + const state = getSessionTokenState(getState() as SdkStoreState); const token = state?.token; const isTokenValid = token && token.exp * 1000 >= Date.now(); @@ -32,8 +31,6 @@ export const getOrRefreshSession = createAsyncThunk( }, ); -const REFRESH_TOKEN = "embeddingSessionToken/REFRESH_TOKEN"; - export const refreshTokenAsync = createAsyncThunk( REFRESH_TOKEN, async (url: string) => { @@ -45,24 +42,59 @@ export const refreshTokenAsync = createAsyncThunk( }, ); -const tokenReducer = createReducer(initialState, builder => - builder - .addCase(refreshTokenAsync.pending, state => { - state.loading = true; - return state; - }) - .addCase(refreshTokenAsync.fulfilled, (state, action) => { - state.token = action.payload; - state.error = null; - state.loading = false; - return state; - }) - .addCase(refreshTokenAsync.rejected, (state, action) => { - state.token = null; - state.error = action.error; - state.loading = false; - return state; - }), -); +const initialState: SdkState = { + token: { + token: null, + loading: false, + error: null, + }, + isLoggedIn: false, + isInitialized: false, +}; -export { tokenReducer }; +export const sdk = createReducer(initialState, { + [refreshTokenAsync.pending.type]: state => { + return { + ...state, + token: { + ...state.token, + loading: true, + }, + }; + }, + [refreshTokenAsync.fulfilled.type]: (state, action) => { + return { + ...state, + token: { + ...state.token, + token: action.payload, + error: null, + loading: false, + }, + }; + }, + [refreshTokenAsync.rejected.type]: (state, action) => { + return { + ...state, + isLoggedIn: false, + token: { + ...state.token, + token: null, + error: action.error, + loading: false, + }, + }; + }, + [SET_IS_LOGGED_IN]: (state, action: PayloadAction<boolean>) => { + return { + ...state, + isLoggedIn: action.payload, + }; + }, + [SET_IS_INITIALIZED]: (state, action: PayloadAction<boolean>) => { + return { + ...state, + isInitialized: action.payload, + }; + }, +}); diff --git a/enterprise/frontend/src/embedding-sdk/store/selectors.ts b/enterprise/frontend/src/embedding-sdk/store/selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..0225c4dca9c3cc396e9c9b0e192edfc54aed16f1 --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/store/selectors.ts @@ -0,0 +1,6 @@ +import type { SdkStoreState } from "embedding-sdk/store/types"; + +export const getIsLoggedIn = (state: SdkStoreState) => state.sdk.isLoggedIn; +export const getIsInitialized = (state: SdkStoreState) => + state.sdk.isInitialized; +export const getSessionTokenState = (state: SdkStoreState) => state.sdk.token; diff --git a/enterprise/frontend/src/embedding-sdk/store/types.ts b/enterprise/frontend/src/embedding-sdk/store/types.ts index 4a7a896f001e8e4054e14379fb2d5240abb2681f..7fdb042d757d6fd38ca4bfa605733a2edc8def30 100644 --- a/enterprise/frontend/src/embedding-sdk/store/types.ts +++ b/enterprise/frontend/src/embedding-sdk/store/types.ts @@ -11,6 +11,12 @@ export type EmbeddingSessionTokenState = { error: SerializedError | null; }; -export interface SdkState extends State { - embeddingSessionToken: EmbeddingSessionTokenState; +export type SdkState = { + token: EmbeddingSessionTokenState; + isLoggedIn: boolean; + isInitialized: boolean; +}; + +export interface SdkStoreState extends State { + sdk: SdkState; }