From 80ccb4e8a4be53184ca12e070496f2560eb94036 Mon Sep 17 00:00:00 2001 From: Sachin Chaurasiya Date: Wed, 15 May 2024 15:40:04 +0530 Subject: [PATCH] minor: add isApplicationLoading state in ApplicationStore (#16242) * minor: add isApplicationLoading state and ApplicationStore * minor: remove setIsUserAuthenticated(true) from the oidcAuthenticator Callback onSuccess * chore: add comments * chore: update isSigningIn to isSigningUp * update comment * chore: add switch in app router * chore: improve setIsAuthenticated usage * fix test * add application loading in basic authenticator --- .../ui/src/components/AppRouter/AppRouter.tsx | 22 +++++++- .../AppRouter/UnAuthenticatedAppRouter.tsx | 4 +- .../BasicAuthAuthenticator.tsx | 13 +++++ .../GenericAuthenticator.tsx | 4 +- .../AppAuthenticators/OidcAuthenticator.tsx | 31 ++++++++--- .../Auth0Callback/Auth0Callback.test.tsx | 4 -- .../Auth0Callback/Auth0Callback.tsx | 4 +- .../AuthProviders/AuthProvider.interface.ts | 4 +- .../Auth/AuthProviders/AuthProvider.tsx | 55 ++++++++++--------- .../Auth/AuthProviders/OktaAuthProvider.tsx | 4 +- .../ui/src/hooks/useApplicationStore.ts | 11 +++- .../ui/src/interface/store.interface.ts | 2 + .../ui/src/pages/SignUp/SignUpPage.test.tsx | 2 +- .../ui/src/pages/SignUp/SignUpPage.tsx | 4 +- 14 files changed, 105 insertions(+), 59 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.tsx index 92e9f2f7daf..d7b014fbb1e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.tsx @@ -13,11 +13,12 @@ import { isNil } from 'lodash'; import React, { useCallback, useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; +import { Switch, useLocation } from 'react-router-dom'; import { useAnalytics } from 'use-analytics'; import { CustomEventTypes } from '../../generated/analytics/webAnalyticEventData'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import AppContainer from '../AppContainer/AppContainer'; +import Loader from '../common/Loader/Loader'; import { UnAuthenticatedAppRouter } from './UnAuthenticatedAppRouter'; const AppRouter = () => { @@ -26,7 +27,7 @@ const AppRouter = () => { // web analytics instance const analytics = useAnalytics(); - const { isAuthenticated } = useApplicationStore(); + const { isAuthenticated, isApplicationLoading } = useApplicationStore(); useEffect(() => { const { pathname } = location; @@ -64,7 +65,22 @@ const AppRouter = () => { return () => targetNode.removeEventListener('click', handleClickEvent); }, [handleClickEvent]); - return isAuthenticated ? : ; + /** + * isApplicationLoading is true when the application is loading in AuthProvider + * and is false when the application is loaded. + * If the application is loading, show the loader. + * If the user is authenticated, show the AppContainer. + * If the user is not authenticated, show the UnAuthenticatedAppRouter. + * */ + if (isApplicationLoading) { + return ; + } + + return ( + + {isAuthenticated ? : } + + ); }; export default AppRouter; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/UnAuthenticatedAppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/UnAuthenticatedAppRouter.tsx index b5aff0d81cf..81ded35bde0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/UnAuthenticatedAppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/UnAuthenticatedAppRouter.tsx @@ -42,7 +42,7 @@ const BasicSignupPage = withSuspenseFallback( ); export const UnAuthenticatedAppRouter = () => { - const { authConfig, isSigningIn } = useApplicationStore(); + const { authConfig, isSigningUp } = useApplicationStore(); const isBasicAuthProvider = authConfig && @@ -78,7 +78,7 @@ export const UnAuthenticatedAppRouter = () => { component={SamlCallback} path={[ROUTES.SAML_CALLBACK, ROUTES.AUTH_CALLBACK]} /> - {!isSigningIn && ( + {!isSigningUp && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/BasicAuthAuthenticator.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/BasicAuthAuthenticator.tsx index 09cf9ca3740..977ab62debe 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/BasicAuthAuthenticator.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/BasicAuthAuthenticator.tsx @@ -25,6 +25,7 @@ import { } from '../../../rest/auth-API'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import Loader from '../../common/Loader/Loader'; import { useBasicAuth } from '../AuthProviders/BasicAuthProvider'; interface BasicAuthenticatorInterface { @@ -41,6 +42,7 @@ const BasicAuthenticator = forwardRef( getRefreshToken, setRefreshToken, setOidcToken, + isApplicationLoading, } = useApplicationStore(); const handleSilentSignIn = async (): Promise => { @@ -73,6 +75,17 @@ const BasicAuthenticator = forwardRef( }, })); + /** + * isApplicationLoading is true when the application is loading in AuthProvider + * and is false when the application is loaded. + * If the application is loading, show the loader. + * If the user is authenticated, show the AppContainer. + * If the user is not authenticated, show the UnAuthenticatedAppRouter. + * */ + if (isApplicationLoading) { + return ; + } + return {children}; } ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/GenericAuthenticator.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/GenericAuthenticator.tsx index 10a77b5ade8..2afe02205b8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/GenericAuthenticator.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/GenericAuthenticator.tsx @@ -25,7 +25,7 @@ export const GenericAuthenticator = forwardRef( ({ children }: { children: ReactNode }, ref) => { const { setIsAuthenticated, - setIsSigningIn, + setIsSigningUp, removeOidcToken, setOidcToken, } = useApplicationStore(); @@ -33,7 +33,7 @@ export const GenericAuthenticator = forwardRef( const handleLogin = () => { setIsAuthenticated(false); - setIsSigningIn(true); + setIsSigningUp(true); window.location.assign('api/v1/auth/login'); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/OidcAuthenticator.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/OidcAuthenticator.tsx index bb1d7436c92..080298ed20a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/OidcAuthenticator.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/OidcAuthenticator.tsx @@ -65,13 +65,13 @@ const OidcAuthenticator = forwardRef( ) => { const { isAuthenticated, - setIsAuthenticated, - isSigningIn, - setIsSigningIn, + isSigningUp, + setIsSigningUp, updateAxiosInterceptors, currentUser, newUser, setOidcToken, + isApplicationLoading, } = useApplicationStore(); const history = useHistory(); const userManager = useMemo( @@ -80,7 +80,7 @@ const OidcAuthenticator = forwardRef( ); const login = () => { - setIsSigningIn(true); + setIsSigningUp(true); }; const logout = () => { @@ -113,16 +113,23 @@ const OidcAuthenticator = forwardRef( return ( <> + {/* render sign in page if user is not authenticated and not signing up + * else redirect to my data page as user is authenticated and not signing up + */} - {!isAuthenticated && !isSigningIn ? ( + {!isAuthenticated && !isSigningUp ? ( ) : ( )} - {!isSigningIn ? ( + + {/* render the sign in route only if user is not signing up */} + {!isSigningUp ? ( ) : null} + + {/* callback route to handle the auth flow after user has successfully provided their consent */} ( @@ -135,7 +142,6 @@ const OidcAuthenticator = forwardRef( }} onSuccess={(user) => { setOidcToken(user.id_token); - setIsAuthenticated(true); onLoginSuccess(user as OidcUser); }} /> @@ -143,6 +149,7 @@ const OidcAuthenticator = forwardRef( )} /> + {/* silent callback route to handle the silent auth flow */} ( @@ -163,15 +170,21 @@ const OidcAuthenticator = forwardRef( )} /> + + {/* render the children only if user is authenticated */} {isAuthenticated ? ( {children} - ) : !isSigningIn && isEmpty(currentUser) && isEmpty(newUser) ? ( + ) : // render the sign in page if user is not authenticated and not signing up + !isSigningUp && isEmpty(currentUser) && isEmpty(newUser) ? ( ) : ( + // render the authenticator component to handle the auth flow while user is signing in )} - {isSigningIn && } + + {/* show loader when application is loading and user is signing up*/} + {isApplicationLoading && isSigningUp && } ); } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppCallbacks/Auth0Callback/Auth0Callback.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppCallbacks/Auth0Callback/Auth0Callback.test.tsx index f78735f194c..935c9f1a50d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppCallbacks/Auth0Callback/Auth0Callback.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppCallbacks/Auth0Callback/Auth0Callback.test.tsx @@ -42,7 +42,6 @@ Object.defineProperty(window, 'localStorage', { }); const mockUseAuth0 = useAuth0 as jest.Mock; -const mockSetIsAuthenticated = jest.fn(); const mockHandleSuccessfulLogin = jest.fn(); jest.mock('@auth0/auth0-react', () => ({ @@ -53,7 +52,6 @@ jest.mock('../../../../hooks/useApplicationStore', () => { return { useApplicationStore: jest.fn(() => ({ authConfig: {}, - setIsAuthenticated: mockSetIsAuthenticated, handleSuccessfulLogin: mockHandleSuccessfulLogin, setOidcToken: jest.fn(), })), @@ -108,8 +106,6 @@ describe('Test Auth0Callback component', () => { // eslint-disable-next-line no-undef await new Promise(process.nextTick); - expect(mockSetIsAuthenticated).toHaveBeenCalledTimes(1); - expect(mockSetIsAuthenticated).toHaveBeenCalledWith(true); expect(mockHandleSuccessfulLogin).toHaveBeenCalledTimes(1); expect(mockHandleSuccessfulLogin).toHaveBeenCalledWith({ id_token: 'raw_id_token', diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppCallbacks/Auth0Callback/Auth0Callback.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppCallbacks/Auth0Callback/Auth0Callback.tsx index a7560ca12bd..5bbff633223 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppCallbacks/Auth0Callback/Auth0Callback.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppCallbacks/Auth0Callback/Auth0Callback.tsx @@ -20,13 +20,11 @@ import { OidcUser } from '../../AuthProviders/AuthProvider.interface'; const Auth0Callback: VFC = () => { const { isAuthenticated, user, getIdTokenClaims, error } = useAuth0(); - const { setIsAuthenticated, handleSuccessfulLogin, setOidcToken } = - useApplicationStore(); + const { handleSuccessfulLogin, setOidcToken } = useApplicationStore(); if (isAuthenticated) { getIdTokenClaims() .then((token) => { setOidcToken(token?.__raw || ''); - setIsAuthenticated(true); const oidcUser: OidcUser = { id_token: token?.__raw || '', scope: '', diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.interface.ts index ee60fbdefc4..5e4b2c0dbec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.interface.ts @@ -52,8 +52,8 @@ export interface IAuthContext { setIsAuthenticated: (authenticated: boolean) => void; authConfig?: AuthenticationConfiguration; authorizerConfig?: AuthorizerConfiguration; - isSigningIn: boolean; - setIsSigningIn: (authenticated: boolean) => void; + isSigningUp: boolean; + setIsSigningUp: (isSigningUp: boolean) => void; onLoginHandler: () => void; onLogoutHandler: () => void; currentUser?: User; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx index b7f720a9fdd..712a049e617 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx @@ -109,16 +109,18 @@ export const AuthProvider = ({ setHelperFunctionsRef, setCurrentUser, updateNewUser: setNewUserProfile, - setIsAuthenticated: setIsUserAuthenticated, + setIsAuthenticated, authConfig, setAuthConfig, setAuthorizerConfig, - setIsSigningIn, + setIsSigningUp, setJwtPrincipalClaims, removeRefreshToken, removeOidcToken, getOidcToken, getRefreshToken, + isApplicationLoading, + setApplicationLoading, } = useApplicationStore(); const { activeDomain } = useDomainStore(); @@ -127,7 +129,6 @@ export const AuthProvider = ({ const { t } = useTranslation(); const [timeoutId, setTimeoutId] = useState(); - const [loading, setLoading] = useState(false); const [msalInstance, setMsalInstance] = useState(); const authenticatorRef = useRef(null); @@ -140,7 +141,7 @@ export const AuthProvider = ({ const clientType = authConfig?.clientType ?? ClientType.Public; const onLoginHandler = () => { - setLoading(true); + setApplicationLoading(true); authenticatorRef.current?.invokeLogin(); @@ -151,7 +152,7 @@ export const AuthProvider = ({ clearTimeout(timeoutId); authenticatorRef.current?.invokeLogout(); - setIsUserAuthenticated(false); + setIsAuthenticated(false); // reset the user details on logout setCurrentUser({} as User); @@ -162,7 +163,7 @@ export const AuthProvider = ({ // remove the refresh token on logout removeRefreshToken(); - setLoading(false); + setApplicationLoading(false); }, [timeoutId]); const onRenewIdTokenHandler = () => { @@ -191,8 +192,8 @@ export const AuthProvider = ({ const resetUserDetails = (forceLogout = false) => { setCurrentUser({} as User); removeOidcToken(); - setIsUserAuthenticated(false); - setLoading(false); + setIsAuthenticated(false); + setApplicationLoading(false); clearTimeout(timeoutId); if (forceLogout) { onLogoutHandler(); @@ -203,12 +204,12 @@ export const AuthProvider = ({ }; const getLoggedInUserDetails = async () => { - setLoading(true); + setApplicationLoading(true); try { const res = await getLoggedInUser({ fields: userAPIQueryFields }); if (res) { setCurrentUser(res); - setIsUserAuthenticated(true); + setIsAuthenticated(true); } else { resetUserDetails(); } @@ -224,7 +225,7 @@ export const AuthProvider = ({ ); } } finally { - setLoading(false); + setApplicationLoading(false); } }; @@ -357,15 +358,15 @@ export const AuthProvider = ({ }, [timeoutId]); const handleFailedLogin = () => { - setIsSigningIn(false); - setIsUserAuthenticated(false); - setLoading(false); + setIsSigningUp(false); + setIsAuthenticated(false); + setApplicationLoading(false); history.push(ROUTES.SIGNIN); }; const handleSuccessfulLogin = async (user: OidcUser) => { - setLoading(true); - setIsUserAuthenticated(true); + setApplicationLoading(true); + setIsAuthenticated(true); const fields = authConfig?.provider === AuthProviderEnum.Basic ? userAPIQueryFields + ',' + isEmailVerifyField @@ -389,7 +390,7 @@ export const AuthProvider = ({ if (err && err.response && err.response.status === 404) { setNewUserProfile(user.profile); setCurrentUser({} as User); - setIsSigningIn(true); + setIsSigningUp(true); history.push(ROUTES.SIGNUP); } else { // eslint-disable-next-line no-console @@ -397,7 +398,7 @@ export const AuthProvider = ({ history.push(ROUTES.SIGNIN); } } finally { - setLoading(false); + setApplicationLoading(false); } }; @@ -548,7 +549,7 @@ export const AuthProvider = ({ updateAuthInstance(configJson); if (!getOidcToken()) { handleStoreProtectedRedirectPath(); - setLoading(false); + setApplicationLoading(false); } else { if (location.pathname !== ROUTES.AUTH_CALLBACK) { getLoggedInUserDetails(); @@ -556,7 +557,7 @@ export const AuthProvider = ({ } } else { // provider is either null or not supported - setLoading(false); + setApplicationLoading(false); showErrorToast( t('message.configured-sso-provider-is-not-supported', { provider: authConfig?.provider, @@ -564,11 +565,11 @@ export const AuthProvider = ({ ); } } else { - setLoading(false); + setApplicationLoading(false); showErrorToast(t('message.auth-configuration-missing')); } } catch (error) { - setLoading(false); + setApplicationLoading(false); showErrorToast( error as AxiosError, t('server.entity-fetch-error', { @@ -580,7 +581,11 @@ export const AuthProvider = ({ const getProtectedApp = () => { // Show loader if application in loading state - const childElement = loading ? : children; + const childElement = isApplicationLoading ? ( + + ) : ( + children + ); if (clientType === ClientType.Confidential) { return ( @@ -691,11 +696,11 @@ export const AuthProvider = ({ return cleanup; }, []); - const isLoading = + const isConfigLoading = !authConfig || (authConfig.provider === AuthProviderEnum.Azure && !msalInstance); - return <>{isLoading ? : getProtectedApp()}; + return <>{isConfigLoading ? : getProtectedApp()}; }; export default AuthProvider; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/OktaAuthProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/OktaAuthProvider.tsx index af6f740d46b..c90e9b4a634 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/OktaAuthProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/OktaAuthProvider.tsx @@ -31,8 +31,7 @@ export const OktaAuthProvider: FunctionComponent = ({ children, onLoginSuccess, }: Props) => { - const { authConfig, setIsAuthenticated, setOidcToken } = - useApplicationStore(); + const { authConfig, setOidcToken } = useApplicationStore(); const { clientId, issuer, redirectUri, scopes, pkce } = authConfig as OktaAuthOptions; @@ -63,7 +62,6 @@ export const OktaAuthProvider: FunctionComponent = ({ _oktaAuth .getUser() .then((info) => { - setIsAuthenticated(true); const user = { id_token: idToken, scope: scopes, diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useApplicationStore.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useApplicationStore.ts index 8aaa3fd1f95..a9756729fd3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useApplicationStore.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useApplicationStore.ts @@ -31,6 +31,7 @@ export const OM_SESSION_KEY = 'om-session'; export const useApplicationStore = create()( persist( (set, get) => ({ + isApplicationLoading: false, theme: getThemeConfig(), applicationConfig: { customTheme: getThemeConfig(), @@ -40,7 +41,7 @@ export const useApplicationStore = create()( isAuthenticated: Boolean(getOidcToken()), authConfig: undefined, authorizerConfig: undefined, - isSigningIn: false, + isSigningUp: false, jwtPrincipalClaims: [], userProfilePics: {}, cachedEntityData: {}, @@ -77,8 +78,12 @@ export const useApplicationStore = create()( setIsAuthenticated: (authenticated: boolean) => { set({ isAuthenticated: authenticated }); }, - setIsSigningIn: (signingIn: boolean) => { - set({ isSigningIn: signingIn }); + setIsSigningUp: (signingUp: boolean) => { + set({ isSigningUp: signingUp }); + }, + + setApplicationLoading: (loading: boolean) => { + set({ isApplicationLoading: loading }); }, onLoginHandler: () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/interface/store.interface.ts b/openmetadata-ui/src/main/resources/ui/src/interface/store.interface.ts index 31bbff7090f..da54d84dc16 100644 --- a/openmetadata-ui/src/main/resources/ui/src/interface/store.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/interface/store.interface.ts @@ -42,6 +42,8 @@ export interface ApplicationStore extends IAuthContext, LogoConfiguration, LoginConfiguration { + isApplicationLoading: boolean; + setApplicationLoading: (loading: boolean) => void; userProfilePics: Record; cachedEntityData: Record; selectedPersona: EntityReference; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.test.tsx index 66c8775cf6a..df986ebf3b6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.test.tsx @@ -33,7 +33,7 @@ jest.mock('react-router-dom', () => ({ jest.mock('../../hooks/useApplicationStore', () => ({ useApplicationStore: jest.fn(() => ({ - setIsSigningIn: jest.fn(), + setIsSigningUp: jest.fn(), newUser: { name: '', email: '', diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.tsx index 2f7d6a25e36..db0743d0a34 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.tsx @@ -41,7 +41,7 @@ const SignUp = () => { const { t } = useTranslation(); const history = useHistory(); const { - setIsSigningIn, + setIsSigningUp, jwtPrincipalClaims = [], authorizerConfig, updateCurrentUser, @@ -67,7 +67,7 @@ const SignUp = () => { if (urlPathname) { setUrlPathnameExpiryAfterRoute(urlPathname); } - setIsSigningIn(false); + setIsSigningUp(false); history.push(ROUTES.HOME); } catch (error) { showErrorToast(