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 { 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 { getLoginStatus } from "embedding-sdk/store/selectors";
import { Loader } from "metabase/ui";
export const PublicComponentWrapper = ({
children,
......@@ -22,7 +22,7 @@ export const PublicComponentWrapper = ({
}
if (loginStatus.status === "loading") {
return <Loader data-testid="loading-spinner" />;
return <SdkLoader />;
}
if (loginStatus.status === "error") {
......@@ -31,21 +31,3 @@ export const PublicComponentWrapper = ({
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 "./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";
import { useCallback, useEffect, useState } from "react";
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 { SdkError } from "embedding-sdk/components/private/SdkError";
import type { SdkClickActionPluginsConfig } from "embedding-sdk/lib/plugins";
import { useSdkSelector } from "embedding-sdk/store";
import { getPlugins } from "embedding-sdk/store/selectors";
......@@ -20,6 +22,7 @@ import { FilterHeader } from "metabase/query_builder/components/view/ViewHeader/
import {
getCard,
getFirstQueryResult,
getQueryResults,
getQuestion,
getUiControls,
} from "metabase/query_builder/selectors";
......@@ -53,6 +56,7 @@ export const _InteractiveQuestion = ({
const card = useSelector(getCard);
const result = useSelector(getFirstQueryResult);
const uiControls = useSelector(getUiControls);
const queryResults = useSelector(getQueryResults);
const hasQuestionChanges =
card && (!card.id || card.id !== card.original_card_id);
......@@ -90,11 +94,17 @@ export const _InteractiveQuestion = ({
loadQuestion(dispatch, questionId);
}, [dispatch, questionId]);
useEffect(() => {
if (queryResults) {
setLoading(false);
}
}, [queryResults]);
if (loading) {
return <Loader data-testid="loading-spinner" />;
}
if (!question) {
if (!queryResults || !question) {
return <SdkError message={t`Question not found`} />;
}
......
......@@ -3,9 +3,14 @@ import { memo } from "react";
import { Provider } from "react-redux";
import { AppInitializeController } from "embedding-sdk/components/private/AppInitializeController";
import {} from "embedding-sdk/components/private/PublicComponentWrapper";
import type { SdkPluginsConfig } from "embedding-sdk/lib/plugins";
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 { EmotionCacheProvider } from "metabase/styled-components/components/EmotionCacheProvider";
import { ThemeProvider } from "metabase/ui/components/theme/ThemeProvider";
......@@ -28,6 +33,14 @@ const MetabaseProviderInternal = ({
store.dispatch(setPlugins(pluginsConfig || null));
}, [pluginsConfig]);
useEffect(() => {
store.dispatch(setLoaderComponent(config.loaderComponent ?? null));
}, [config.loaderComponent]);
useEffect(() => {
store.dispatch(setErrorComponent(config.errorComponent ?? null));
}, [config.errorComponent]);
return (
<Provider store={store}>
<EmotionCacheProvider>
......
......@@ -2,9 +2,11 @@ import cx from "classnames";
import { useEffect, useState } from "react";
import { t } from "ttag";
import { withPublicComponentWrapper } from "embedding-sdk/components/private/PublicComponentWrapper/PublicComponentWrapper";
import { SdkError } from "embedding-sdk/components/private/SdkError";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import {
withPublicComponentWrapper,
SdkError,
SdkLoader,
} from "embedding-sdk/components/private/PublicComponentWrapper";
import CS from "metabase/css/core/index.css";
import type { GenericErrorResponse } from "metabase/lib/errors";
import { getResponseErrorMessage } from "metabase/lib/errors";
......@@ -103,49 +105,45 @@ const _StaticQuestion = ({
);
}
return (
<LoadingAndErrorWrapper
className={cx(CS.flexFull, CS.fullWidth)}
loading={isLoading}
noWrapper
>
{() => {
const question = new Question(card, metadata);
const legacyQuery = question.legacyQuery({
useStructuredQuery: true,
});
if (isLoading) {
return <SdkLoader />;
}
return (
<Group h="100%" pos="relative" align="flex-start">
{showVisualizationSelector && (
<Box w="355px">
<ChartTypeSidebar
question={question}
result={result}
onOpenChartSettings={onOpenChartSettings}
onCloseChartType={onCloseChartType}
query={legacyQuery}
setUIControls={setUIControls}
updateQuestion={changeVisualization}
/>
</Box>
)}
<QueryVisualization
className={cx(CS.flexFull, CS.fullWidth)}
const question = new Question(card, metadata);
const legacyQuery = question.legacyQuery({
useStructuredQuery: true,
});
return (
<Box className={cx(CS.flexFull, CS.fullWidth)}>
<Group h="100%" pos="relative" align="flex-start">
{showVisualizationSelector && (
<Box w="355px">
<ChartTypeSidebar
question={question}
rawSeries={[{ card, data: result?.data }]}
isRunning={isLoading}
isObjectDetail={false}
isResultDirty={false}
isNativeEditorOpen={false}
result={result}
noHeader
mode={PublicMode}
onOpenChartSettings={onOpenChartSettings}
onCloseChartType={onCloseChartType}
query={legacyQuery}
setUIControls={setUIControls}
updateQuestion={changeVisualization}
/>
</Group>
);
}}
</LoadingAndErrorWrapper>
</Box>
)}
<QueryVisualization
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";
import { getSessionTokenState } from "./selectors";
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 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 REFRESH_TOKEN = "sdk/token/REFRESH_TOKEN";
......@@ -58,6 +66,8 @@ const initialState: SdkState = {
},
loginStatus: { status: "uninitialized" },
plugins: null,
loaderComponent: null,
errorComponent: null,
};
export const sdk = createReducer(initialState, {
......@@ -105,4 +115,24 @@ export const sdk = createReducer(initialState, {
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) =>
export const getSessionTokenState = (state: SdkStoreState) => state.sdk.token;
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 {
AnyAction,
ThunkDispatch,
} from "@reduxjs/toolkit";
import type { JSX } from "react";
import type { SdkPluginsConfig } from "embedding-sdk/lib/plugins";
import type { State } from "metabase-types/store";
......@@ -46,6 +47,8 @@ export type SdkState = {
token: EmbeddingSessionTokenState;
loginStatus: LoginStatus;
plugins: null | SdkPluginsConfig;
loaderComponent: null | (() => JSX.Element);
errorComponent: null | (({ message }: { message: string }) => JSX.Element);
};
export interface SdkStoreState extends State {
......
......@@ -30,6 +30,8 @@ export const createMockSdkState = ({
loginStatus: createMockLoginStatusState(),
token: createMockTokenState(),
plugins: {},
loaderComponent: null,
errorComponent: null,
...opts,
};
};
import type { JSX } from "react";
import type { SdkErrorProps } from "embedding-sdk/components/private/PublicComponentWrapper/SdkError";
export type SDKConfig = {
metabaseInstanceUrl: string;
font?: 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