Skip to content
Snippets Groups Projects
Unverified Commit 253fd4b3 authored by Oisin Coveney's avatar Oisin Coveney Committed by GitHub
Browse files

Add customization for loader and error components (#41828)

parent 858aa150
No related branches found
No related tags found
No related merge requests found
Showing
with 174 additions and 79 deletions
import type { ComponentType } from "react"; import type { JSX } from "react";
import { t } from "ttag"; import { t } from "ttag";
import { SdkError } from "embedding-sdk/components/private/SdkError"; import { SdkError } from "embedding-sdk/components/private/PublicComponentWrapper/SdkError";
import { SdkLoader } from "embedding-sdk/components/private/PublicComponentWrapper/SdkLoader";
import { useSdkSelector } from "embedding-sdk/store"; import { useSdkSelector } from "embedding-sdk/store";
import { getLoginStatus } from "embedding-sdk/store/selectors"; import { getLoginStatus } from "embedding-sdk/store/selectors";
import { Loader } from "metabase/ui";
export const PublicComponentWrapper = ({ export const PublicComponentWrapper = ({
children, children,
...@@ -22,7 +22,7 @@ export const PublicComponentWrapper = ({ ...@@ -22,7 +22,7 @@ export const PublicComponentWrapper = ({
} }
if (loginStatus.status === "loading") { if (loginStatus.status === "loading") {
return <Loader data-testid="loading-spinner" />; return <SdkLoader />;
} }
if (loginStatus.status === "error") { if (loginStatus.status === "error") {
...@@ -31,21 +31,3 @@ export const PublicComponentWrapper = ({ ...@@ -31,21 +31,3 @@ export const PublicComponentWrapper = ({
return children; return children;
}; };
export function withPublicComponentWrapper<P>(
WrappedComponent: ComponentType<P>,
): React.FC<P> {
const WithPublicComponentWrapper: React.FC<P> = props => {
return (
<PublicComponentWrapper>
<WrappedComponent {...props} />
</PublicComponentWrapper>
);
};
WithPublicComponentWrapper.displayName = `withPublicComponentWrapper(${
WrappedComponent.displayName || WrappedComponent.name || "Component"
})`;
return WithPublicComponentWrapper;
}
import { t } from "ttag";
import { useSdkSelector } from "embedding-sdk/store";
import { getErrorComponent } from "embedding-sdk/store/selectors";
export type SdkErrorProps = { message: string };
export const SdkError = ({ message }: SdkErrorProps) => {
const CustomError = useSdkSelector(getErrorComponent);
if (CustomError) {
return <CustomError message={message} />;
}
return (
<div>
<div>{t`Error`}</div>
<div>{message}</div>
</div>
);
};
import { useSdkSelector } from "embedding-sdk/store";
import { getLoaderComponent } from "embedding-sdk/store/selectors";
import { Loader } from "metabase/ui";
export const SdkLoader = () => {
const CustomLoader = useSdkSelector(getLoaderComponent);
const LoaderComponent = CustomLoader || Loader;
return <LoaderComponent data-testid="loading-spinner" />;
};
export * from "./PublicComponentWrapper"; export * from "./PublicComponentWrapper";
export * from "./withPublicComponentWrapper";
export * from "./SdkError";
export * from "./SdkLoader";
import type { ComponentType } from "react";
import { PublicComponentWrapper } from "./PublicComponentWrapper";
export function withPublicComponentWrapper<P>(
WrappedComponent: ComponentType<P>,
): React.FC<P> {
const WithPublicComponentWrapper: React.FC<P> = props => {
return (
<PublicComponentWrapper>
<WrappedComponent {...props} />
</PublicComponentWrapper>
);
};
WithPublicComponentWrapper.displayName = `withPublicComponentWrapper(${
WrappedComponent.displayName || WrappedComponent.name || "Component"
})`;
return WithPublicComponentWrapper;
}
import { t } from "ttag";
// TODO: Allow this component to be customizable by clients
export const SdkError = ({ message }: { message: string }) => {
return (
<div>
<div>{t`Error`}</div>
<div>{message}</div>
</div>
);
};
...@@ -2,9 +2,11 @@ import cx from "classnames"; ...@@ -2,9 +2,11 @@ import cx from "classnames";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { t } from "ttag"; import { t } from "ttag";
import { withPublicComponentWrapper } from "embedding-sdk/components/private/PublicComponentWrapper"; import {
withPublicComponentWrapper,
SdkError,
} from "embedding-sdk/components/private/PublicComponentWrapper";
import { ResetButton } from "embedding-sdk/components/private/ResetButton"; import { ResetButton } from "embedding-sdk/components/private/ResetButton";
import { SdkError } from "embedding-sdk/components/private/SdkError";
import type { SdkClickActionPluginsConfig } from "embedding-sdk/lib/plugins"; import type { SdkClickActionPluginsConfig } from "embedding-sdk/lib/plugins";
import { useSdkSelector } from "embedding-sdk/store"; import { useSdkSelector } from "embedding-sdk/store";
import { getPlugins } from "embedding-sdk/store/selectors"; import { getPlugins } from "embedding-sdk/store/selectors";
...@@ -20,6 +22,7 @@ import { FilterHeader } from "metabase/query_builder/components/view/ViewHeader/ ...@@ -20,6 +22,7 @@ import { FilterHeader } from "metabase/query_builder/components/view/ViewHeader/
import { import {
getCard, getCard,
getFirstQueryResult, getFirstQueryResult,
getQueryResults,
getQuestion, getQuestion,
getUiControls, getUiControls,
} from "metabase/query_builder/selectors"; } from "metabase/query_builder/selectors";
...@@ -53,6 +56,7 @@ export const _InteractiveQuestion = ({ ...@@ -53,6 +56,7 @@ export const _InteractiveQuestion = ({
const card = useSelector(getCard); const card = useSelector(getCard);
const result = useSelector(getFirstQueryResult); const result = useSelector(getFirstQueryResult);
const uiControls = useSelector(getUiControls); const uiControls = useSelector(getUiControls);
const queryResults = useSelector(getQueryResults);
const hasQuestionChanges = const hasQuestionChanges =
card && (!card.id || card.id !== card.original_card_id); card && (!card.id || card.id !== card.original_card_id);
...@@ -90,11 +94,17 @@ export const _InteractiveQuestion = ({ ...@@ -90,11 +94,17 @@ export const _InteractiveQuestion = ({
loadQuestion(dispatch, questionId); loadQuestion(dispatch, questionId);
}, [dispatch, questionId]); }, [dispatch, questionId]);
useEffect(() => {
if (queryResults) {
setLoading(false);
}
}, [queryResults]);
if (loading) { if (loading) {
return <Loader data-testid="loading-spinner" />; return <Loader data-testid="loading-spinner" />;
} }
if (!question) { if (!queryResults || !question) {
return <SdkError message={t`Question not found`} />; return <SdkError message={t`Question not found`} />;
} }
......
...@@ -3,9 +3,14 @@ import { memo } from "react"; ...@@ -3,9 +3,14 @@ import { memo } from "react";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { AppInitializeController } from "embedding-sdk/components/private/AppInitializeController"; import { AppInitializeController } from "embedding-sdk/components/private/AppInitializeController";
import {} from "embedding-sdk/components/private/PublicComponentWrapper";
import type { SdkPluginsConfig } from "embedding-sdk/lib/plugins"; import type { SdkPluginsConfig } from "embedding-sdk/lib/plugins";
import { store } from "embedding-sdk/store"; import { store } from "embedding-sdk/store";
import { setPlugins } from "embedding-sdk/store/reducer"; import {
setErrorComponent,
setLoaderComponent,
setPlugins,
} from "embedding-sdk/store/reducer";
import type { SDKConfig } from "embedding-sdk/types"; import type { SDKConfig } from "embedding-sdk/types";
import { EmotionCacheProvider } from "metabase/styled-components/components/EmotionCacheProvider"; import { EmotionCacheProvider } from "metabase/styled-components/components/EmotionCacheProvider";
import { ThemeProvider } from "metabase/ui/components/theme/ThemeProvider"; import { ThemeProvider } from "metabase/ui/components/theme/ThemeProvider";
...@@ -28,6 +33,14 @@ const MetabaseProviderInternal = ({ ...@@ -28,6 +33,14 @@ const MetabaseProviderInternal = ({
store.dispatch(setPlugins(pluginsConfig || null)); store.dispatch(setPlugins(pluginsConfig || null));
}, [pluginsConfig]); }, [pluginsConfig]);
useEffect(() => {
store.dispatch(setLoaderComponent(config.loaderComponent ?? null));
}, [config.loaderComponent]);
useEffect(() => {
store.dispatch(setErrorComponent(config.errorComponent ?? null));
}, [config.errorComponent]);
return ( return (
<Provider store={store}> <Provider store={store}>
<EmotionCacheProvider> <EmotionCacheProvider>
......
...@@ -2,9 +2,11 @@ import cx from "classnames"; ...@@ -2,9 +2,11 @@ import cx from "classnames";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { t } from "ttag"; import { t } from "ttag";
import { withPublicComponentWrapper } from "embedding-sdk/components/private/PublicComponentWrapper/PublicComponentWrapper"; import {
import { SdkError } from "embedding-sdk/components/private/SdkError"; withPublicComponentWrapper,
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; SdkError,
SdkLoader,
} from "embedding-sdk/components/private/PublicComponentWrapper";
import CS from "metabase/css/core/index.css"; import CS from "metabase/css/core/index.css";
import type { GenericErrorResponse } from "metabase/lib/errors"; import type { GenericErrorResponse } from "metabase/lib/errors";
import { getResponseErrorMessage } from "metabase/lib/errors"; import { getResponseErrorMessage } from "metabase/lib/errors";
...@@ -103,49 +105,45 @@ const _StaticQuestion = ({ ...@@ -103,49 +105,45 @@ const _StaticQuestion = ({
); );
} }
return ( if (isLoading) {
<LoadingAndErrorWrapper return <SdkLoader />;
className={cx(CS.flexFull, CS.fullWidth)} }
loading={isLoading}
noWrapper
>
{() => {
const question = new Question(card, metadata);
const legacyQuery = question.legacyQuery({
useStructuredQuery: true,
});
return ( const question = new Question(card, metadata);
<Group h="100%" pos="relative" align="flex-start"> const legacyQuery = question.legacyQuery({
{showVisualizationSelector && ( useStructuredQuery: true,
<Box w="355px"> });
<ChartTypeSidebar
question={question} return (
result={result} <Box className={cx(CS.flexFull, CS.fullWidth)}>
onOpenChartSettings={onOpenChartSettings} <Group h="100%" pos="relative" align="flex-start">
onCloseChartType={onCloseChartType} {showVisualizationSelector && (
query={legacyQuery} <Box w="355px">
setUIControls={setUIControls} <ChartTypeSidebar
updateQuestion={changeVisualization}
/>
</Box>
)}
<QueryVisualization
className={cx(CS.flexFull, CS.fullWidth)}
question={question} question={question}
rawSeries={[{ card, data: result?.data }]}
isRunning={isLoading}
isObjectDetail={false}
isResultDirty={false}
isNativeEditorOpen={false}
result={result} result={result}
noHeader onOpenChartSettings={onOpenChartSettings}
mode={PublicMode} onCloseChartType={onCloseChartType}
query={legacyQuery}
setUIControls={setUIControls}
updateQuestion={changeVisualization}
/> />
</Group> </Box>
); )}
}} <QueryVisualization
</LoadingAndErrorWrapper> className={cx(CS.flexFull, CS.fullWidth)}
question={question}
rawSeries={[{ card, data: result?.data }]}
isRunning={isLoading}
isObjectDetail={false}
isResultDirty={false}
isNativeEditorOpen={false}
result={result}
noHeader
mode={PublicMode}
/>
</Group>
</Box>
); );
}; };
......
...@@ -14,8 +14,16 @@ import { createAsyncThunk } from "metabase/lib/redux"; ...@@ -14,8 +14,16 @@ import { createAsyncThunk } from "metabase/lib/redux";
import { getSessionTokenState } from "./selectors"; import { getSessionTokenState } from "./selectors";
const SET_LOGIN_STATUS = "sdk/SET_LOGIN_STATUS"; const SET_LOGIN_STATUS = "sdk/SET_LOGIN_STATUS";
const SET_LOADER_COMPONENT = "sdk/SET_LOADER_COMPONENT";
const SET_ERROR_COMPONENT = "sdk/SET_ERROR_COMPONENT";
export const setLoginStatus = createAction<LoginStatus>(SET_LOGIN_STATUS); export const setLoginStatus = createAction<LoginStatus>(SET_LOGIN_STATUS);
export const setLoaderComponent = createAction<null | (() => JSX.Element)>(
SET_LOADER_COMPONENT,
);
export const setErrorComponent = createAction<
null | (({ message }: { message: string }) => JSX.Element)
>(SET_ERROR_COMPONENT);
const GET_OR_REFRESH_SESSION = "sdk/token/GET_OR_REFRESH_SESSION"; const GET_OR_REFRESH_SESSION = "sdk/token/GET_OR_REFRESH_SESSION";
const REFRESH_TOKEN = "sdk/token/REFRESH_TOKEN"; const REFRESH_TOKEN = "sdk/token/REFRESH_TOKEN";
...@@ -58,6 +66,8 @@ const initialState: SdkState = { ...@@ -58,6 +66,8 @@ const initialState: SdkState = {
}, },
loginStatus: { status: "uninitialized" }, loginStatus: { status: "uninitialized" },
plugins: null, plugins: null,
loaderComponent: null,
errorComponent: null,
}; };
export const sdk = createReducer(initialState, { export const sdk = createReducer(initialState, {
...@@ -105,4 +115,24 @@ export const sdk = createReducer(initialState, { ...@@ -105,4 +115,24 @@ export const sdk = createReducer(initialState, {
plugins: action.payload, plugins: action.payload,
}; };
}, },
[SET_LOADER_COMPONENT]: (
state,
action: PayloadAction<null | (() => JSX.Element)>,
) => {
return {
...state,
loaderComponent: action.payload,
};
},
[SET_ERROR_COMPONENT]: (
state,
action: PayloadAction<
null | (({ message }: { message: string }) => JSX.Element)
>,
) => {
return {
...state,
errorComponent: action.payload,
};
},
}); });
...@@ -11,3 +11,9 @@ export const getIsLoggedIn = (state: SdkStoreState) => ...@@ -11,3 +11,9 @@ export const getIsLoggedIn = (state: SdkStoreState) =>
export const getSessionTokenState = (state: SdkStoreState) => state.sdk.token; export const getSessionTokenState = (state: SdkStoreState) => state.sdk.token;
export const getPlugins = (state: SdkStoreState) => state.sdk.plugins; export const getPlugins = (state: SdkStoreState) => state.sdk.plugins;
export const getLoaderComponent = (state: SdkStoreState) =>
state.sdk.loaderComponent;
export const getErrorComponent = (state: SdkStoreState) =>
state.sdk.errorComponent;
...@@ -3,6 +3,7 @@ import type { ...@@ -3,6 +3,7 @@ import type {
AnyAction, AnyAction,
ThunkDispatch, ThunkDispatch,
} from "@reduxjs/toolkit"; } from "@reduxjs/toolkit";
import type { JSX } from "react";
import type { SdkPluginsConfig } from "embedding-sdk/lib/plugins"; import type { SdkPluginsConfig } from "embedding-sdk/lib/plugins";
import type { State } from "metabase-types/store"; import type { State } from "metabase-types/store";
...@@ -46,6 +47,8 @@ export type SdkState = { ...@@ -46,6 +47,8 @@ export type SdkState = {
token: EmbeddingSessionTokenState; token: EmbeddingSessionTokenState;
loginStatus: LoginStatus; loginStatus: LoginStatus;
plugins: null | SdkPluginsConfig; plugins: null | SdkPluginsConfig;
loaderComponent: null | (() => JSX.Element);
errorComponent: null | (({ message }: { message: string }) => JSX.Element);
}; };
export interface SdkStoreState extends State { export interface SdkStoreState extends State {
......
...@@ -30,6 +30,8 @@ export const createMockSdkState = ({ ...@@ -30,6 +30,8 @@ export const createMockSdkState = ({
loginStatus: createMockLoginStatusState(), loginStatus: createMockLoginStatusState(),
token: createMockTokenState(), token: createMockTokenState(),
plugins: {}, plugins: {},
loaderComponent: null,
errorComponent: null,
...opts, ...opts,
}; };
}; };
import type { JSX } from "react";
import type { SdkErrorProps } from "embedding-sdk/components/private/PublicComponentWrapper/SdkError";
export type SDKConfig = { export type SDKConfig = {
metabaseInstanceUrl: string; metabaseInstanceUrl: string;
font?: string; font?: string;
jwtProviderUri: string; jwtProviderUri: string;
loaderComponent?: () => JSX.Element;
errorComponent?: ({ message }: SdkErrorProps) => JSX.Element;
}; };
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