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

Replace `isLoggedIn` and `isInitialized` with `LoginStatus` (#41637)

parent dc90d8de
No related branches found
No related tags found
No related merge requests found
Showing
with 177 additions and 88 deletions
import type * as React from "react";
import type { ReactNode } from "react";
import { t } from "ttag";
import {
......@@ -13,7 +13,7 @@ import type { SDKConfigType } from "embedding-sdk/types";
import { SdkContentWrapper } from "./SdkContentWrapper";
interface AppInitializeControllerProps {
children: React.ReactNode;
children: ReactNode;
config: SDKConfigType;
}
......
import type { ComponentType } from "react";
import { t } from "ttag";
import { SdkError } from "embedding-sdk/components/private/SdkError";
import { useSdkSelector } from "embedding-sdk/store";
import { getLoginStatus } from "embedding-sdk/store/selectors";
export const PublicComponentWrapper = ({
children,
}: {
children: JSX.Element;
}) => {
const loginStatus = useSdkSelector(getLoginStatus);
if (loginStatus.status === "uninitialized") {
return <div>{t`Initializing…`}</div>;
}
if (loginStatus.status === "initialized") {
return <div>{t`API Key / JWT is valid.`}</div>;
}
if (loginStatus.status === "loading") {
return <div>{t`Loading`}</div>;
}
if (loginStatus.status === "error") {
return <SdkError />;
}
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 { getLoginStatus } from "embedding-sdk/store/selectors";
import type { LoginStatusError } from "embedding-sdk/store/types";
// TODO: Allow this component to be customizable by clients
export const SdkError = () => {
const loginStatus = useSdkSelector(getLoginStatus) as LoginStatusError;
return (
<div>
<div>{t`Error`}</div>
<div>{loginStatus.error.message}</div>
</div>
);
};
import cx from "classnames";
import { useEffect } from "react";
import { useSdkSelector } from "embedding-sdk/store";
import { getIsInitialized, getIsLoggedIn } from "embedding-sdk/store/selectors";
import { withPublicComponentWrapper } from "embedding-sdk/components/private/PublicComponentWrapper";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import CS from "metabase/css/core/index.css";
import { useDispatch, useSelector } from "metabase/lib/redux";
......@@ -27,12 +26,9 @@ interface InteractiveQuestionProps {
questionId: CardId;
}
export const InteractiveQuestion = ({
export const _InteractiveQuestion = ({
questionId,
}: InteractiveQuestionProps): JSX.Element | null => {
const isInitialized = useSdkSelector(getIsInitialized);
const isLoggedIn = useSdkSelector(getIsLoggedIn);
const dispatch = useDispatch();
const question = useSelector(getQuestion);
const mode = question && getEmbeddingMode(question);
......@@ -56,7 +52,7 @@ export const InteractiveQuestion = ({
dispatch(initializeQB(mockLocation, params));
}, [dispatch, questionId]);
if (!isInitialized || !isLoggedIn || !question) {
if (!question) {
return null;
}
......@@ -102,3 +98,6 @@ export const InteractiveQuestion = ({
</LoadingAndErrorWrapper>
);
};
export const InteractiveQuestion =
withPublicComponentWrapper(_InteractiveQuestion);
import cx from "classnames";
import { useEffect, useState } from "react";
import { useSdkSelector } from "embedding-sdk/store";
import { getIsInitialized, getIsLoggedIn } from "embedding-sdk/store/selectors";
import { withPublicComponentWrapper } from "embedding-sdk/components/private/PublicComponentWrapper";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import CS from "metabase/css/core/index.css";
import { useSelector } from "metabase/lib/redux";
......@@ -15,7 +14,7 @@ import QueryVisualization from "metabase/query_builder/components/QueryVisualiza
import ChartTypeSidebar from "metabase/query_builder/components/view/sidebars/ChartTypeSidebar";
import { getMetadata } from "metabase/selectors/metadata";
import { CardApi } from "metabase/services";
import { Box, Group, Text } from "metabase/ui";
import { Box, Group } from "metabase/ui";
import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMode";
import Question from "metabase-lib/v1/Question";
import type { Card, CardId, Dataset } from "metabase-types/api";
......@@ -33,13 +32,10 @@ type State = {
resultError?: Dataset | string | null;
};
export const StaticQuestion = ({
const _StaticQuestion = ({
questionId,
showVisualizationSelector,
}: QueryVisualizationProps): JSX.Element | null => {
const isInitialized = useSdkSelector(getIsInitialized);
const isLoggedIn = useSdkSelector(getIsLoggedIn);
const metadata = useSelector(getMetadata);
const [{ loading, card, result, cardError, resultError }, setState] =
......@@ -86,18 +82,8 @@ export const StaticQuestion = ({
};
useEffect(() => {
if (!isInitialized || !isLoggedIn) {
setState({
loading: false,
card: null,
result: null,
cardError: null,
resultError: null,
});
} else {
loadCardData({ questionId });
}
}, [isInitialized, isLoggedIn, questionId]);
loadCardData({ questionId });
}, [questionId]);
const changeVisualization = (newQuestion: Question) => {
setState({
......@@ -107,18 +93,6 @@ export const StaticQuestion = ({
});
};
if (!isInitialized) {
return null;
}
if (!isLoggedIn) {
return (
<div>
<Text>You should be logged in to see this content.</Text>
</div>
);
}
const isLoading = loading || (!result && !resultError);
return (
......@@ -167,3 +141,5 @@ export const StaticQuestion = ({
</LoadingAndErrorWrapper>
);
};
export const StaticQuestion = withPublicComponentWrapper(_StaticQuestion);
import { useEffect } from "react";
import { t } from "ttag";
import _ from "underscore";
import { useSdkDispatch, useSdkSelector } from "embedding-sdk/store";
import {
getOrRefreshSession,
setIsInitialized,
setIsLoggedIn,
setLoginStatus,
} from "embedding-sdk/store/reducer";
import { getIsInitialized, getIsLoggedIn } from "embedding-sdk/store/selectors";
import { getLoginStatus } 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";
......@@ -21,20 +21,27 @@ interface InitDataLoaderParameters {
config: SDKConfigType;
}
export const useInitData = ({
config,
}: InitDataLoaderParameters): {
isLoggedIn: boolean;
isInitialized: boolean;
} => {
const getErrorMessage = (authType: SDKConfigType["authType"]) => {
if (authType === "jwt") {
return t`Could not authenticate: invalid JWT token`;
}
if (authType === "apiKey") {
return t`Could not authenticate: invalid API key`;
}
return t`Invalid auth type`;
};
export const useInitData = ({ config }: InitDataLoaderParameters) => {
const dispatch = useSdkDispatch();
const isInitialized = useSdkSelector(getIsInitialized);
const isLoggedIn = useSdkSelector(getIsLoggedIn);
const loginStatus = useSdkSelector(getLoginStatus);
useEffect(() => {
registerVisualizationsOnce();
}, []);
dispatch(setLoginStatus({ status: "uninitialized" }));
}, [dispatch]);
useEffect(() => {
api.basename = config.metabaseInstanceUrl;
......@@ -49,24 +56,56 @@ export const useInitData = ({
tokenState.payload as EmbeddingSessionTokenState["token"]
)?.id;
};
dispatch(setLoginStatus({ status: "initialized" }));
} else if (config.authType === "apiKey" && config.apiKey) {
api.apiKey = config.apiKey;
dispatch(setLoginStatus({ status: "initialized" }));
} else {
dispatch(setIsLoggedIn(false));
return;
dispatch(
setLoginStatus({
status: "error",
error: new Error(getErrorMessage(config.authType)),
}),
);
}
Promise.all([
dispatch(refreshCurrentUser()),
dispatch(reloadSettings()),
]).then(() => {
dispatch(setIsInitialized(true));
dispatch(setIsLoggedIn(true));
});
}, [config, dispatch]);
return {
isLoggedIn,
isInitialized,
};
useEffect(() => {
const fetchData = async () => {
if (loginStatus.status === "initialized") {
dispatch(setLoginStatus({ status: "loading" }));
try {
const [userResponse, [_, siteSettingsResponse]] = await Promise.all([
dispatch(refreshCurrentUser()),
dispatch(reloadSettings()),
]);
if (
userResponse.meta.requestStatus === "rejected" ||
siteSettingsResponse.meta.requestStatus === "rejected"
) {
dispatch(
setLoginStatus({
status: "error",
error: new Error(getErrorMessage(config.authType)),
}),
);
return;
}
dispatch(setLoginStatus({ status: "success" }));
} catch (error) {
dispatch(
setLoginStatus({
status: "error",
error: new Error(getErrorMessage(config.authType)),
}),
);
}
}
};
fetchData();
}, [config.authType, dispatch, loginStatus.status]);
};
import { useMemo } from "react";
import { useSdkSelector } from "embedding-sdk/store";
import { getIsInitialized, getIsLoggedIn } from "embedding-sdk/store/selectors";
import { getIsLoggedIn } from "embedding-sdk/store/selectors";
import { useSearchListQuery } from "metabase/common/hooks";
export const useQuestionSearch = (searchQuery?: string) => {
const isInitialized = useSdkSelector(getIsInitialized);
const isLoggedIn = useSdkSelector(getIsLoggedIn);
const query = useMemo(() => {
......@@ -21,6 +20,6 @@ export const useQuestionSearch = (searchQuery?: string) => {
return useSearchListQuery({
query,
enabled: isLoggedIn && isInitialized,
enabled: isLoggedIn,
});
};
......@@ -3,6 +3,7 @@ import { createReducer } from "@reduxjs/toolkit";
import { createAction } from "redux-actions";
import type {
LoginStatus,
EmbeddingSessionTokenState,
SdkState,
SdkStoreState,
......@@ -11,11 +12,9 @@ import { createAsyncThunk } from "metabase/lib/redux";
import { getSessionTokenState } from "./selectors";
const SET_IS_LOGGED_IN = "sdk/SET_IS_LOGGED_IN";
const SET_IS_INITIALIZED = "sdk/SET_IS_INITIALIZED";
const SET_LOGIN_STATUS = "sdk/SET_LOGIN_STATUS";
export const setIsLoggedIn = createAction<boolean>(SET_IS_LOGGED_IN);
export const setIsInitialized = createAction<boolean>(SET_IS_INITIALIZED);
export const setLoginStatus = createAction<LoginStatus>(SET_LOGIN_STATUS);
const GET_OR_REFRESH_SESSION = "sdk/token/GET_OR_REFRESH_SESSION";
const REFRESH_TOKEN = "sdk/token/REFRESH_TOKEN";
......@@ -53,8 +52,7 @@ const initialState: SdkState = {
loading: false,
error: null,
},
isLoggedIn: false,
isInitialized: false,
loginStatus: { status: "uninitialized" },
};
export const sdk = createReducer(initialState, {
......@@ -90,16 +88,10 @@ export const sdk = createReducer(initialState, {
},
};
},
[SET_IS_LOGGED_IN]: (state, action: PayloadAction<boolean>) => {
[SET_LOGIN_STATUS]: (state, action: PayloadAction<LoginStatus>) => {
return {
...state,
isLoggedIn: action.payload,
};
},
[SET_IS_INITIALIZED]: (state, action: PayloadAction<boolean>) => {
return {
...state,
isInitialized: action.payload,
loginStatus: action.payload,
};
},
});
import type { SdkStoreState } from "embedding-sdk/store/types";
export const getIsLoggedIn = (state: SdkStoreState) => state.sdk.isLoggedIn;
export const getLoginStatus = (state: SdkStoreState) => state.sdk.loginStatus;
export const getIsInitialized = (state: SdkStoreState) =>
state.sdk.isInitialized;
getLoginStatus(state).status !== "uninitialized";
export const getIsLoggedIn = (state: SdkStoreState) =>
getLoginStatus(state).status === "success";
export const getSessionTokenState = (state: SdkStoreState) => state.sdk.token;
......@@ -11,10 +11,22 @@ export type EmbeddingSessionTokenState = {
error: SerializedError | null;
};
type LoginStatusUninitialized = { status: "uninitialized" };
type LoginStatusInitialized = { status: "initialized" };
type LoginStatusSuccess = { status: "success" };
type LoginStatusLoading = { status: "loading" };
export type LoginStatusError = { status: "error"; error: Error };
export type LoginStatus =
| LoginStatusUninitialized
| LoginStatusInitialized
| LoginStatusSuccess
| LoginStatusLoading
| LoginStatusError;
export type SdkState = {
token: EmbeddingSessionTokenState;
isLoggedIn: boolean;
isInitialized: boolean;
loginStatus: LoginStatus;
};
export interface SdkStoreState extends State {
......
......@@ -17,7 +17,7 @@ import {
// ACTION TYPES AND ACTION CREATORS
export const reloadSettings = () => async (dispatch, getState) => {
await Promise.all([
return await Promise.all([
dispatch(refreshSettingsList()),
dispatch(refreshSiteSettings()),
]);
......
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