From 3335bcc73f8047864b2a3d0b99e1a4a5777428f1 Mon Sep 17 00:00:00 2001 From: Alexander Polyankin <alexander.polyankin@metabase.com> Date: Wed, 31 May 2023 14:35:59 +0300 Subject: [PATCH] Migrate setup to RTK (#31002) --- .../src/metabase-types/api/mocks/index.ts | 1 + frontend/src/metabase/reducers-main.js | 4 +- frontend/src/metabase/routes.jsx | 4 +- frontend/src/metabase/setup/actions.ts | 249 +++++++++++------- .../components/ActiveStep/ActiveStep.tsx | 7 +- .../setup/components/ActiveStep/index.ts | 3 +- .../CloudMigrationHelp/CloudMigrationHelp.tsx | 25 ++ .../components/CloudMigrationHelp/index.ts | 1 + .../CompletedStep/CompletedStep.tsx | 19 +- .../CompletedStep/CompletedStep.unit.spec.tsx | 41 +-- .../setup/components/CompletedStep/index.ts | 3 +- .../components/DatabaseHelp/DatabaseHelp.tsx | 20 +- .../setup/components/DatabaseHelp/index.ts | 3 +- .../components/DatabaseStep/DatabaseStep.tsx | 123 +++++---- .../DatabaseStep/DatabaseStep.unit.spec.tsx | 68 ++--- .../setup/components/DatabaseStep/index.ts | 3 +- .../components/InvactiveStep/InactiveStep.tsx | 7 +- .../setup/components/InvactiveStep/index.ts | 3 +- .../InviteUserForm/InviteUserForm.tsx | 5 +- .../setup/components/InviteUserForm/index.ts | 3 +- .../components/LanguageStep/LanguageStep.tsx | 72 ++--- .../LanguageStep/LanguageStep.unit.spec.tsx | 65 +++-- .../setup/components/LanguageStep/index.ts | 3 +- .../NewsletterForm/NewsletterForm.tsx | 30 +-- .../NewsletterForm.unit.spec.tsx | 38 ++- .../setup/components/NewsletterForm/index.ts | 3 +- .../PreferencesStep/PreferencesStep.tsx | 63 +++-- .../PreferencesStep.unit.spec.tsx | 62 ++--- .../setup/components/PreferencesStep/index.ts | 3 +- .../components/SettingsPage/SettingsPage.tsx | 54 ++-- .../setup/components/SettingsPage/index.ts | 3 +- .../metabase/setup/components/Setup/Setup.tsx | 38 ++- .../metabase/setup/components/Setup/index.ts | 3 +- .../setup/components/SetupHelp/SetupHelp.tsx | 5 +- .../setup/components/SetupHelp/index.ts | 3 +- .../components/SetupSection/SetupSection.tsx | 7 +- .../setup/components/SetupSection/index.ts | 3 +- .../setup/components/UserForm/UserForm.tsx | 9 +- .../setup/components/UserForm/index.ts | 3 +- .../setup/components/UserStep/UserStep.tsx | 63 +++-- .../UserStep/UserStep.unit.spec.tsx | 63 ++--- .../setup/components/UserStep/index.ts | 3 +- .../setup/components/UserStep/types.ts | 15 -- .../components/WelcomePage/WelcomePage.tsx | 56 ++-- .../WelcomePage/WelcomePage.unit.spec.tsx | 59 ++--- .../setup/components/WelcomePage/index.ts | 3 +- frontend/src/metabase/setup/constants.ts | 4 + .../CloudMigrationHelp/CloudMigrationHelp.tsx | 43 --- .../containers/CloudMigrationHelp/index.ts | 2 - .../CompletedStep/CompletedStep.tsx | 13 - .../setup/containers/CompletedStep/index.ts | 2 - .../containers/DatabaseHelp/DatabaseHelp.tsx | 18 -- .../setup/containers/DatabaseHelp/index.ts | 2 - .../containers/DatabaseStep/DatabaseStep.tsx | 74 ------ .../setup/containers/DatabaseStep/index.ts | 2 - .../containers/LanguageStep/LanguageStep.tsx | 37 --- .../setup/containers/LanguageStep/index.ts | 2 - .../NewsletterForm/NewsletterForm.tsx | 16 -- .../setup/containers/NewsletterForm/index.ts | 2 - .../PreferencesStep/PreferencesStep.tsx | 47 ---- .../setup/containers/PreferencesStep/index.ts | 2 - .../containers/SettingsPage/SettingsPage.tsx | 19 -- .../setup/containers/SettingsPage/index.ts | 2 - .../setup/containers/SetupApp/SetupApp.tsx | 12 - .../setup/containers/SetupApp/index.ts | 2 - .../setup/containers/UserStep/UserStep.tsx | 38 --- .../setup/containers/UserStep/index.ts | 2 - .../containers/WelcomePage/WelcomePage.tsx | 26 -- .../setup/containers/WelcomePage/index.ts | 2 - frontend/src/metabase/setup/reducers.ts | 149 +++++------ frontend/src/metabase/setup/selectors.ts | 31 ++- frontend/src/metabase/setup/utils.ts | 19 +- .../test/__support__/server-mocks/index.ts | 1 + .../test/__support__/server-mocks/setup.ts | 4 + 74 files changed, 806 insertions(+), 1088 deletions(-) create mode 100644 frontend/src/metabase/setup/components/CloudMigrationHelp/CloudMigrationHelp.tsx create mode 100644 frontend/src/metabase/setup/components/CloudMigrationHelp/index.ts delete mode 100644 frontend/src/metabase/setup/components/UserStep/types.ts delete mode 100644 frontend/src/metabase/setup/containers/CloudMigrationHelp/CloudMigrationHelp.tsx delete mode 100644 frontend/src/metabase/setup/containers/CloudMigrationHelp/index.ts delete mode 100644 frontend/src/metabase/setup/containers/CompletedStep/CompletedStep.tsx delete mode 100644 frontend/src/metabase/setup/containers/CompletedStep/index.ts delete mode 100644 frontend/src/metabase/setup/containers/DatabaseHelp/DatabaseHelp.tsx delete mode 100644 frontend/src/metabase/setup/containers/DatabaseHelp/index.ts delete mode 100644 frontend/src/metabase/setup/containers/DatabaseStep/DatabaseStep.tsx delete mode 100644 frontend/src/metabase/setup/containers/DatabaseStep/index.ts delete mode 100644 frontend/src/metabase/setup/containers/LanguageStep/LanguageStep.tsx delete mode 100644 frontend/src/metabase/setup/containers/LanguageStep/index.ts delete mode 100644 frontend/src/metabase/setup/containers/NewsletterForm/NewsletterForm.tsx delete mode 100644 frontend/src/metabase/setup/containers/NewsletterForm/index.ts delete mode 100644 frontend/src/metabase/setup/containers/PreferencesStep/PreferencesStep.tsx delete mode 100644 frontend/src/metabase/setup/containers/PreferencesStep/index.ts delete mode 100644 frontend/src/metabase/setup/containers/SettingsPage/SettingsPage.tsx delete mode 100644 frontend/src/metabase/setup/containers/SettingsPage/index.ts delete mode 100644 frontend/src/metabase/setup/containers/SetupApp/SetupApp.tsx delete mode 100644 frontend/src/metabase/setup/containers/SetupApp/index.ts delete mode 100644 frontend/src/metabase/setup/containers/UserStep/UserStep.tsx delete mode 100644 frontend/src/metabase/setup/containers/UserStep/index.ts delete mode 100644 frontend/src/metabase/setup/containers/WelcomePage/WelcomePage.tsx delete mode 100644 frontend/src/metabase/setup/containers/WelcomePage/index.ts diff --git a/frontend/src/metabase-types/api/mocks/index.ts b/frontend/src/metabase-types/api/mocks/index.ts index 8ce1fbbc0fb..c3bd4fa37ef 100644 --- a/frontend/src/metabase-types/api/mocks/index.ts +++ b/frontend/src/metabase-types/api/mocks/index.ts @@ -8,6 +8,7 @@ export * from "./dashboard"; export * from "./database"; export * from "./dataset"; export * from "./field"; +export * from "./group"; export * from "./metric"; export * from "./models"; export * from "./modelIndexes"; diff --git a/frontend/src/metabase/reducers-main.js b/frontend/src/metabase/reducers-main.js index 2d6b0095d39..94e6730da80 100644 --- a/frontend/src/metabase/reducers-main.js +++ b/frontend/src/metabase/reducers-main.js @@ -8,7 +8,7 @@ import { PLUGIN_REDUCERS } from "metabase/plugins"; import admin from "metabase/admin/admin"; /* setup */ -import * as setup from "metabase/setup/reducers"; +import { reducer as setup } from "metabase/setup/reducers"; /* dashboards */ import dashboard from "metabase/dashboard/reducers"; @@ -48,7 +48,7 @@ export default { qb: combineReducers(qb), reference, revisions, - setup: combineReducers(setup), + setup, admin, plugins: combineReducers(PLUGIN_REDUCERS), }; diff --git a/frontend/src/metabase/routes.jsx b/frontend/src/metabase/routes.jsx index 60466ae598e..41709d61bdd 100644 --- a/frontend/src/metabase/routes.jsx +++ b/frontend/src/metabase/routes.jsx @@ -37,7 +37,7 @@ import CollectionPermissionsModal from "metabase/admin/permissions/components/Co import UserCollectionList from "metabase/containers/UserCollectionList"; import PulseEditApp from "metabase/pulse/containers/PulseEditApp"; -import SetupApp from "metabase/setup/containers/SetupApp"; +import { Setup } from "metabase/setup/components/Setup"; import NewModelOptions from "metabase/models/containers/NewModelOptions"; @@ -98,7 +98,7 @@ export const getRoutes = store => ( {/* SETUP */} <Route path="/setup" - component={SetupApp} + component={Setup} onEnter={(nextState, replace) => { if (MetabaseSettings.hasUserSetup()) { replace("/"); diff --git a/frontend/src/metabase/setup/actions.ts b/frontend/src/metabase/setup/actions.ts index e3ac67b4152..212964f8837 100644 --- a/frontend/src/metabase/setup/actions.ts +++ b/frontend/src/metabase/setup/actions.ts @@ -1,125 +1,198 @@ -import { createAction } from "redux-actions"; -import { getIn } from "icepick"; -import { SetupApi, UtilApi } from "metabase/services"; -import { createThunkAction } from "metabase/lib/redux"; -import { loadLocalization } from "metabase/lib/i18n"; +import { createAction, createAsyncThunk } from "@reduxjs/toolkit"; +import { SetupApi } from "metabase/services"; import MetabaseSettings from "metabase/lib/settings"; +import { loadLocalization } from "metabase/lib/i18n"; import { DatabaseData } from "metabase-types/api"; -import { Locale } from "metabase-types/store"; -import { getUserToken, getDefaultLocale, getLocales } from "./utils"; - -export const SET_STEP = "metabase/setup/SET_STEP"; -export const setStep = createAction(SET_STEP); - -export const SET_LOCALE = "metabase/setup/SET_LOCALE"; -export const SET_LOCALE_LOADED = "metabase/setup/SET_LOCALE_LOADED"; -export const setLocale = createThunkAction( - SET_LOCALE_LOADED, - (locale: Locale) => async (dispatch: any) => { - dispatch({ type: SET_LOCALE, payload: locale }); - await loadLocalization(locale.code); - }, -); - -export const SET_USER = "metabase/setup/SET_USER"; -export const setUser = createAction(SET_USER); - -export const SET_DATABASE_ENGINE = "metabase/setup/SET_DATABASE_ENGINE"; -export const setDatabaseEngine = createAction(SET_DATABASE_ENGINE); - -export const SET_DATABASE = "metabase/setup/SET_DATABASE"; -export const setDatabase = createAction(SET_DATABASE); - -export const SET_INVITE = "metabase/setup/SET_INVITE"; -export const setInvite = createAction(SET_INVITE); - -export const SET_TRACKING = "metabase/setup/SET_TRACKING"; -export const setTracking = createAction(SET_TRACKING); +import { InviteInfo, Locale, State, UserInfo } from "metabase-types/store"; +import { + trackAddDataLaterClicked, + trackDatabaseSelected, + trackDatabaseStepCompleted, + trackTrackingChanged, + trackUserStepCompleted, + trackWelcomeStepCompleted, +} from "./analytics"; +import { + getAvailableLocales, + getDatabase, + getInvite, + getIsTrackingAllowed, + getLocale, + getSetupToken, + getUser, +} from "./selectors"; +import { getDefaultLocale, getLocales, getUserToken } from "./utils"; + +interface ThunkConfig { + state: State; +} export const LOAD_USER_DEFAULTS = "metabase/setup/LOAD_USER_DEFAULTS"; -export const loadUserDefaults = createThunkAction( +export const loadUserDefaults = createAsyncThunk( LOAD_USER_DEFAULTS, - () => async (dispatch: any) => { + async (): Promise<UserInfo | undefined> => { const token = getUserToken(); if (token) { const defaults = await SetupApi.user_defaults({ token }); - dispatch(setUser(defaults.user)); + return defaults.user; } }, ); export const LOAD_LOCALE_DEFAULTS = "metabase/setup/LOAD_LOCALE_DEFAULTS"; -export const loadLocaleDefaults = createThunkAction( - LOAD_LOCALE_DEFAULTS, - () => async (dispatch: any) => { - const data = MetabaseSettings.get("available-locales") || []; - const locale = getDefaultLocale(getLocales(data)); - await dispatch(setLocale(locale)); +export const loadLocaleDefaults = createAsyncThunk< + Locale | undefined, + void, + ThunkConfig +>(LOAD_LOCALE_DEFAULTS, async (_, { getState }) => { + const data = getAvailableLocales(getState()); + const locale = getDefaultLocale(getLocales(data)); + if (locale) { + await loadLocalization(locale.code); + } + return locale; +}); + +export const LOAD_DEFAULTS = "metabase/setup/LOAD_DEFAULTS"; +export const loadDefaults = createAsyncThunk<void, void, ThunkConfig>( + LOAD_DEFAULTS, + (_, { dispatch }) => { + dispatch(loadUserDefaults()); + dispatch(loadLocaleDefaults()); }, ); -export const validatePassword = async (password: string) => { - const error = MetabaseSettings.passwordComplexityDescription(password); - if (error) { - return error; - } +export const SELECT_STEP = "metabase/setup/SUBMIT_WELCOME_STEP"; +export const selectStep = createAction<number>(SELECT_STEP); - try { - await UtilApi.password_check({ password }); - } catch (error) { - return getIn(error, ["data", "errors", "password"]); - } -}; +export const SUBMIT_WELCOME = "metabase/setup/SUBMIT_WELCOME_STEP"; +export const submitWelcome = createAsyncThunk(SUBMIT_WELCOME, () => { + trackWelcomeStepCompleted(); +}); + +export const UPDATE_LOCALE = "metabase/setup/UPDATE_LOCALE"; +export const updateLocale = createAsyncThunk( + UPDATE_LOCALE, + async (locale: Locale) => { + await loadLocalization(locale.code); + }, +); + +export const SUBMIT_LANGUAGE = "metabase/setup/SUBMIT_LANGUAGE"; +export const submitLanguage = createAction(SUBMIT_LANGUAGE); + +export const submitUser = createAsyncThunk( + "metabase/setup/SUBMIT_USER_INFO", + (_: UserInfo) => { + trackUserStepCompleted(); + }, +); -export const VALIDATE_DATABASE = "metabase/setup/VALIDATE_DATABASE"; -export const validateDatabase = createThunkAction( - VALIDATE_DATABASE, - (database: DatabaseData) => async () => { - await SetupApi.validate_db({ - token: MetabaseSettings.get("setup-token"), - details: database, - }); +export const UPDATE_DATABASE_ENGINE = "metabase/setup/UPDATE_DATABASE_ENGINE"; +export const updateDatabaseEngine = createAsyncThunk( + UPDATE_DATABASE_ENGINE, + (engine?: string) => { + if (engine) { + trackDatabaseSelected(engine); + } }, ); +const validateDatabase = async (token: string, database: DatabaseData) => { + await SetupApi.validate_db({ + token, + details: database, + }); +}; + export const SUBMIT_DATABASE = "metabase/setup/SUBMIT_DATABASE"; -export const submitDatabase = createThunkAction( +export const submitDatabase = createAsyncThunk< + DatabaseData, + DatabaseData, + ThunkConfig +>( SUBMIT_DATABASE, - (database: DatabaseData) => async (dispatch: any) => { + async (database: DatabaseData, { getState, rejectWithValue }) => { + const token = getSetupToken(getState()); const sslDetails = { ...database.details, ssl: true }; const sslDatabase = { ...database, details: sslDetails }; const nonSslDetails = { ...database.details, ssl: false }; const nonSslDatabase = { ...database, database: nonSslDetails }; + if (!token) { + return database; + } + try { - await dispatch(validateDatabase(sslDatabase)); - await dispatch(setDatabase(sslDatabase)); - } catch (error) { - await dispatch(validateDatabase(nonSslDatabase)); - await dispatch(setDatabase(nonSslDatabase)); + await validateDatabase(token, sslDatabase); + trackDatabaseStepCompleted(database.engine); + return sslDatabase; + } catch (error1) { + try { + await validateDatabase(token, nonSslDatabase); + trackDatabaseStepCompleted(database.engine); + return nonSslDatabase; + } catch (error2) { + return rejectWithValue(error2); + } } }, ); +export const SUBMIT_USER_INVITE = "metabase/setup/SUBMIT_USER_INVITE"; +export const submitUserInvite = createAsyncThunk( + SUBMIT_USER_INVITE, + (_: InviteInfo) => { + trackDatabaseStepCompleted(); + }, +); + +export const SKIP_DATABASE = "metabase/setup/SKIP_DATABASE"; +export const skipDatabase = createAsyncThunk( + SKIP_DATABASE, + (engine?: string) => { + trackDatabaseStepCompleted(); + trackAddDataLaterClicked(engine); + }, +); + +export const UPDATE_TRACKING = "metabase/setup/UPDATE_TRACKING"; +export const updateTracking = createAsyncThunk( + UPDATE_TRACKING, + (isTrackingAllowed: boolean) => { + trackTrackingChanged(isTrackingAllowed); + MetabaseSettings.set("anon-tracking-enabled", isTrackingAllowed); + trackTrackingChanged(isTrackingAllowed); + }, +); + export const SUBMIT_SETUP = "metabase/setup/SUBMIT_SETUP"; -export const submitSetup = createThunkAction( +export const submitSetup = createAsyncThunk<void, void, ThunkConfig>( SUBMIT_SETUP, - () => async (dispatch: any, getState: any) => { - const { setup } = getState(); - const { locale, user, database, invite, isTrackingAllowed } = setup; - - await SetupApi.create({ - token: MetabaseSettings.get("setup-token"), - user, - database, - invite, - prefs: { - site_name: user.site_name, - site_locale: locale.code, - allow_tracking: isTrackingAllowed.toString(), - }, - }); - - MetabaseSettings.set("setup-token", null); + async (_, { getState, rejectWithValue }) => { + const token = getSetupToken(getState()); + const locale = getLocale(getState()); + const user = getUser(getState()); + const database = getDatabase(getState()); + const invite = getInvite(getState()); + const isTrackingAllowed = getIsTrackingAllowed(getState()); + + try { + await SetupApi.create({ + token, + user, + database, + invite, + prefs: { + site_name: user?.site_name, + site_locale: locale?.code, + allow_tracking: isTrackingAllowed.toString(), + }, + }); + + MetabaseSettings.set("setup-token", null); + } catch (error) { + return rejectWithValue(error); + } }, ); diff --git a/frontend/src/metabase/setup/components/ActiveStep/ActiveStep.tsx b/frontend/src/metabase/setup/components/ActiveStep/ActiveStep.tsx index 5c2aff92a12..80a59680a52 100644 --- a/frontend/src/metabase/setup/components/ActiveStep/ActiveStep.tsx +++ b/frontend/src/metabase/setup/components/ActiveStep/ActiveStep.tsx @@ -6,13 +6,13 @@ import { StepLabelText, } from "./ActiveStep.styled"; -export interface ActiveStepProps { +interface ActiveStepProps { title: string; label: number; children?: ReactNode; } -const ActiveStep = ({ +export const ActiveStep = ({ title, label, children, @@ -27,6 +27,3 @@ const ActiveStep = ({ </StepRoot> ); }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default ActiveStep; diff --git a/frontend/src/metabase/setup/components/ActiveStep/index.ts b/frontend/src/metabase/setup/components/ActiveStep/index.ts index 81de9bb107c..eab397459db 100644 --- a/frontend/src/metabase/setup/components/ActiveStep/index.ts +++ b/frontend/src/metabase/setup/components/ActiveStep/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./ActiveStep"; +export * from "./ActiveStep"; diff --git a/frontend/src/metabase/setup/components/CloudMigrationHelp/CloudMigrationHelp.tsx b/frontend/src/metabase/setup/components/CloudMigrationHelp/CloudMigrationHelp.tsx new file mode 100644 index 00000000000..43f7fdd5714 --- /dev/null +++ b/frontend/src/metabase/setup/components/CloudMigrationHelp/CloudMigrationHelp.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { t } from "ttag"; +import { useSelector } from "metabase/lib/redux"; +import MetabaseSettings from "metabase/lib/settings"; +import HelpCard from "metabase/components/HelpCard"; +import { COMPLETED_STEP } from "../../constants"; +import { getIsHosted, getIsStepActive } from "../../selectors"; +import { SetupCardContainer } from "../SetupCardContainer"; + +export const CloudMigrationHelp = () => { + const isHosted = useSelector(getIsHosted); + const isStepActive = useSelector(state => + getIsStepActive(state, COMPLETED_STEP), + ); + const isVisible = isHosted && isStepActive; + + return ( + <SetupCardContainer isVisible={isVisible}> + <HelpCard + title={t`Migrating from self-hosted?`} + helpUrl={MetabaseSettings.migrateToCloudGuideUrl()} + >{t`Check out our docs for how to migrate your self-hosted instance to Cloud.`}</HelpCard> + </SetupCardContainer> + ); +}; diff --git a/frontend/src/metabase/setup/components/CloudMigrationHelp/index.ts b/frontend/src/metabase/setup/components/CloudMigrationHelp/index.ts new file mode 100644 index 00000000000..fe4ff854562 --- /dev/null +++ b/frontend/src/metabase/setup/components/CloudMigrationHelp/index.ts @@ -0,0 +1 @@ +export * from "./CloudMigrationHelp"; diff --git a/frontend/src/metabase/setup/components/CompletedStep/CompletedStep.tsx b/frontend/src/metabase/setup/components/CompletedStep/CompletedStep.tsx index 92e3e595242..16ef4cb505a 100644 --- a/frontend/src/metabase/setup/components/CompletedStep/CompletedStep.tsx +++ b/frontend/src/metabase/setup/components/CompletedStep/CompletedStep.tsx @@ -1,6 +1,9 @@ import React from "react"; import { t } from "ttag"; -import NewsletterForm from "../../containers/NewsletterForm"; +import { useSelector } from "metabase/lib/redux"; +import { COMPLETED_STEP } from "../../constants"; +import { getIsStepActive } from "../../selectors"; +import { NewsletterForm } from "../NewsletterForm"; import { StepBody, StepFooter, @@ -8,13 +11,10 @@ import { StepTitle, } from "./CompletedStep.styled"; -export interface CompletedStepProps { - isStepActive: boolean; -} - -const CompletedStep = ({ - isStepActive, -}: CompletedStepProps): JSX.Element | null => { +export const CompletedStep = (): JSX.Element | null => { + const isStepActive = useSelector(state => + getIsStepActive(state, COMPLETED_STEP), + ); if (!isStepActive) { return null; } @@ -35,6 +35,3 @@ const CompletedStep = ({ </StepRoot> ); }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default CompletedStep; diff --git a/frontend/src/metabase/setup/components/CompletedStep/CompletedStep.unit.spec.tsx b/frontend/src/metabase/setup/components/CompletedStep/CompletedStep.unit.spec.tsx index b28048c27a5..5f0d706536f 100644 --- a/frontend/src/metabase/setup/components/CompletedStep/CompletedStep.unit.spec.tsx +++ b/frontend/src/metabase/setup/components/CompletedStep/CompletedStep.unit.spec.tsx @@ -1,34 +1,37 @@ import React from "react"; -import { render, screen } from "@testing-library/react"; -import CompletedStep, { CompletedStepProps } from "./CompletedStep"; +import { + createMockSetupState, + createMockState, +} from "metabase-types/store/mocks"; +import { renderWithProviders, screen } from "__support__/ui"; +import { COMPLETED_STEP, USER_STEP } from "../../constants"; +import { CompletedStep } from "./CompletedStep"; + +interface SetupOpts { + step?: number; +} + +const setup = ({ step = COMPLETED_STEP }: SetupOpts = {}) => { + const state = createMockState({ + setup: createMockSetupState({ + step, + }), + }); -const NewsletterFormMock = () => <div>Metabase Newsletter</div>; -jest.mock("../../containers/NewsletterForm", () => NewsletterFormMock); + renderWithProviders(<CompletedStep />, { storeInitialState: state }); +}; describe("CompletedStep", () => { it("should render in inactive state", () => { - const props = getProps({ - isStepActive: false, - }); - - render(<CompletedStep {...props} />); + setup({ step: USER_STEP }); expect(screen.queryByText("You're all set up!")).not.toBeInTheDocument(); }); it("should show a newsletter form and a link to the app", () => { - const props = getProps({ - isStepActive: true, - }); - - render(<CompletedStep {...props} />); + setup({ step: COMPLETED_STEP }); expect(screen.getByText("Metabase Newsletter")).toBeInTheDocument(); expect(screen.getByText("Take me to Metabase")).toBeInTheDocument(); }); }); - -const getProps = (opts?: Partial<CompletedStepProps>): CompletedStepProps => ({ - isStepActive: false, - ...opts, -}); diff --git a/frontend/src/metabase/setup/components/CompletedStep/index.ts b/frontend/src/metabase/setup/components/CompletedStep/index.ts index 43cf9c3b966..fb74d8c165f 100644 --- a/frontend/src/metabase/setup/components/CompletedStep/index.ts +++ b/frontend/src/metabase/setup/components/CompletedStep/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./CompletedStep"; +export * from "./CompletedStep"; diff --git a/frontend/src/metabase/setup/components/DatabaseHelp/DatabaseHelp.tsx b/frontend/src/metabase/setup/components/DatabaseHelp/DatabaseHelp.tsx index ffedf6700a5..ca510bc5191 100644 --- a/frontend/src/metabase/setup/components/DatabaseHelp/DatabaseHelp.tsx +++ b/frontend/src/metabase/setup/components/DatabaseHelp/DatabaseHelp.tsx @@ -1,16 +1,15 @@ import React from "react"; +import { useSelector } from "metabase/lib/redux"; import DatabaseHelpCard from "metabase/databases/containers/DatabaseHelpCard"; +import { DATABASE_STEP } from "../../constants"; +import { getDatabaseEngine, getIsStepActive } from "../../selectors"; import { SetupCardContainer } from "../SetupCardContainer"; -export interface DatabaseHelpProps { - engine?: string; - isStepActive: boolean; -} - -const DatabaseHelp = ({ - engine, - isStepActive, -}: DatabaseHelpProps): JSX.Element => { +export const DatabaseHelp = (): JSX.Element => { + const engine = useSelector(getDatabaseEngine); + const isStepActive = useSelector(state => + getIsStepActive(state, DATABASE_STEP), + ); const isVisible = isStepActive && engine != null; return ( @@ -19,6 +18,3 @@ const DatabaseHelp = ({ </SetupCardContainer> ); }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default DatabaseHelp; diff --git a/frontend/src/metabase/setup/components/DatabaseHelp/index.ts b/frontend/src/metabase/setup/components/DatabaseHelp/index.ts index c8244930946..ea7e6f61fe5 100644 --- a/frontend/src/metabase/setup/components/DatabaseHelp/index.ts +++ b/frontend/src/metabase/setup/components/DatabaseHelp/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./DatabaseHelp"; +export * from "./DatabaseHelp"; diff --git a/frontend/src/metabase/setup/components/DatabaseStep/DatabaseStep.tsx b/frontend/src/metabase/setup/components/DatabaseStep/DatabaseStep.tsx index b7790767e09..2469370e46e 100644 --- a/frontend/src/metabase/setup/components/DatabaseStep/DatabaseStep.tsx +++ b/frontend/src/metabase/setup/components/DatabaseStep/DatabaseStep.tsx @@ -1,60 +1,72 @@ -import React, { useCallback } from "react"; +import React from "react"; import { t } from "ttag"; import { updateIn } from "icepick"; +import { useDispatch, useSelector } from "metabase/lib/redux"; import DatabaseForm from "metabase/databases/containers/DatabaseForm"; import { DatabaseData } from "metabase-types/api"; -import { InviteInfo, UserInfo } from "metabase-types/store"; -import ActiveStep from "../ActiveStep"; -import InactiveStep from "../InvactiveStep"; -import InviteUserForm from "../InviteUserForm"; -import SetupSection from "../SetupSection"; +import { InviteInfo } from "metabase-types/store"; +import { + selectStep, + skipDatabase, + submitDatabase, + submitUserInvite, + updateDatabaseEngine, +} from "../../actions"; +import { DATABASE_STEP } from "../../constants"; +import { + getDatabase, + getDatabaseEngine, + getInvite, + getIsEmailConfigured, + getIsSetupCompleted, + getIsStepActive, + getIsStepCompleted, + getUser, +} from "../../selectors"; +import { ActiveStep } from "../ActiveStep"; +import { InactiveStep } from "../InvactiveStep"; +import { InviteUserForm } from "../InviteUserForm"; +import { SetupSection } from "../SetupSection"; import { StepDescription } from "./DatabaseStep.styled"; -export interface DatabaseStepProps { - user?: UserInfo; - database?: DatabaseData; - engine?: string; - invite?: InviteInfo; - isEmailConfigured: boolean; - isStepActive: boolean; - isStepCompleted: boolean; - isSetupCompleted: boolean; - onEngineChange: (engine?: string) => void; - onStepSelect: () => void; - onDatabaseSubmit: (database: DatabaseData) => void; - onInviteSubmit: (invite: InviteInfo) => void; - onStepCancel: (engine?: string) => void; -} - -const DatabaseStep = ({ - user, - database, - engine, - invite, - isEmailConfigured, - isStepActive, - isStepCompleted, - isSetupCompleted, - onEngineChange, - onStepSelect, - onDatabaseSubmit, - onInviteSubmit, - onStepCancel, -}: DatabaseStepProps): JSX.Element => { - const handleSubmit = useCallback( - async (database: DatabaseData) => { - try { - await onDatabaseSubmit(database); - } catch (error) { - throw getSubmitError(error); - } - }, - [onDatabaseSubmit], +export const DatabaseStep = (): JSX.Element => { + const user = useSelector(getUser); + const database = useSelector(getDatabase); + const engine = useSelector(getDatabaseEngine); + const invite = useSelector(getInvite); + const isEmailConfigured = useSelector(getIsEmailConfigured); + const isStepActive = useSelector(state => + getIsStepActive(state, DATABASE_STEP), + ); + const isStepCompleted = useSelector(state => + getIsStepCompleted(state, DATABASE_STEP), ); + const isSetupCompleted = useSelector(getIsSetupCompleted); + const dispatch = useDispatch(); + + const handleEngineChange = (engine?: string) => { + dispatch(updateDatabaseEngine(engine)); + }; + + const handleDatabaseSubmit = async (database: DatabaseData) => { + try { + await dispatch(submitDatabase(database)).unwrap(); + } catch (error) { + throw getSubmitError(error); + } + }; - const handleCancel = useCallback(() => { - onStepCancel(engine); - }, [engine, onStepCancel]); + const handleInviteSubmit = (invite: InviteInfo) => { + dispatch(submitUserInvite(invite)); + }; + + const handleStepSelect = () => { + dispatch(selectStep(DATABASE_STEP)); + }; + + const handleStepCancel = () => { + dispatch(skipDatabase(engine)); + }; if (!isStepActive) { return ( @@ -63,7 +75,7 @@ const DatabaseStep = ({ label={3} isStepCompleted={isStepCompleted} isSetupCompleted={isSetupCompleted} - onStepSelect={onStepSelect} + onStepSelect={handleStepSelect} /> ); } @@ -79,9 +91,9 @@ const DatabaseStep = ({ </StepDescription> <DatabaseForm initialValues={database} - onSubmit={handleSubmit} - onEngineChange={onEngineChange} - onCancel={handleCancel} + onSubmit={handleDatabaseSubmit} + onEngineChange={handleEngineChange} + onCancel={handleStepCancel} /> {isEmailConfigured && ( <SetupSection @@ -91,7 +103,7 @@ const DatabaseStep = ({ <InviteUserForm user={user} invite={invite} - onSubmit={onInviteSubmit} + onSubmit={handleInviteSubmit} /> </SetupSection> )} @@ -120,6 +132,3 @@ const getSubmitError = (error: unknown): unknown => { details: errors, })); }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default DatabaseStep; diff --git a/frontend/src/metabase/setup/components/DatabaseStep/DatabaseStep.unit.spec.tsx b/frontend/src/metabase/setup/components/DatabaseStep/DatabaseStep.unit.spec.tsx index ed1a00d396f..c0648595bf4 100644 --- a/frontend/src/metabase/setup/components/DatabaseStep/DatabaseStep.unit.spec.tsx +++ b/frontend/src/metabase/setup/components/DatabaseStep/DatabaseStep.unit.spec.tsx @@ -1,58 +1,62 @@ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { DatabaseData } from "metabase-types/api"; import { createMockDatabaseData } from "metabase-types/api/mocks"; -import DatabaseStep, { DatabaseStepProps } from "./DatabaseStep"; +import { + createMockSettingsState, + createMockSetupState, + createMockState, +} from "metabase-types/store/mocks"; +import { renderWithProviders, screen } from "__support__/ui"; +import { DATABASE_STEP, PREFERENCES_STEP } from "../../constants"; +import { DatabaseStep } from "./DatabaseStep"; + +interface SetupOpts { + step?: number; + database?: DatabaseData; + isEmailConfigured?: boolean; +} + +const setup = ({ + step = DATABASE_STEP, + database, + isEmailConfigured = false, +}: SetupOpts = {}) => { + const state = createMockState({ + setup: createMockSetupState({ + step, + database, + }), + settings: createMockSettingsState({ + "email-configured?": isEmailConfigured, + }), + }); -const ComponentMock = () => <div />; -jest.mock("metabase/databases/containers/DatabaseForm", () => ComponentMock); + renderWithProviders(<DatabaseStep />, { storeInitialState: state }); +}; describe("DatabaseStep", () => { it("should render in active state", () => { - const props = getProps({ - isStepActive: true, - isStepCompleted: false, - }); - - render(<DatabaseStep {...props} />); + setup(); expect(screen.getByText("Add your data")).toBeInTheDocument(); }); it("should render in completed state", () => { - const props = getProps({ + setup({ + step: PREFERENCES_STEP, database: createMockDatabaseData({ name: "Test" }), - isStepActive: false, - isStepCompleted: true, }); - render(<DatabaseStep {...props} />); - expect(screen.getByText("Connecting to Test")).toBeInTheDocument(); }); it("should render a user invite form", () => { - const props = getProps({ - isStepActive: true, + setup({ isEmailConfigured: true, }); - render(<DatabaseStep {...props} />); - expect( screen.getByText("Need help connecting to your data?"), ).toBeInTheDocument(); }); }); - -const getProps = (opts?: Partial<DatabaseStepProps>): DatabaseStepProps => ({ - isEmailConfigured: false, - isStepActive: false, - isStepCompleted: false, - isSetupCompleted: false, - onEngineChange: jest.fn(), - onStepSelect: jest.fn(), - onDatabaseSubmit: jest.fn(), - onInviteSubmit: jest.fn(), - onStepCancel: jest.fn(), - ...opts, -}); diff --git a/frontend/src/metabase/setup/components/DatabaseStep/index.ts b/frontend/src/metabase/setup/components/DatabaseStep/index.ts index 60865dfe04a..081ac13a7b7 100644 --- a/frontend/src/metabase/setup/components/DatabaseStep/index.ts +++ b/frontend/src/metabase/setup/components/DatabaseStep/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./DatabaseStep"; +export * from "./DatabaseStep"; diff --git a/frontend/src/metabase/setup/components/InvactiveStep/InactiveStep.tsx b/frontend/src/metabase/setup/components/InvactiveStep/InactiveStep.tsx index 8f99c7720d7..ecbdb12d5de 100644 --- a/frontend/src/metabase/setup/components/InvactiveStep/InactiveStep.tsx +++ b/frontend/src/metabase/setup/components/InvactiveStep/InactiveStep.tsx @@ -7,7 +7,7 @@ import { StepLabelText, } from "./InactiveStep.styled"; -export interface InactiveStepProps { +interface InactiveStepProps { title: string; label: number; isStepCompleted: boolean; @@ -15,7 +15,7 @@ export interface InactiveStepProps { onStepSelect: () => void; } -const InactiveStep = ({ +export const InactiveStep = ({ title, label, isStepCompleted, @@ -38,6 +38,3 @@ const InactiveStep = ({ </StepRoot> ); }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default InactiveStep; diff --git a/frontend/src/metabase/setup/components/InvactiveStep/index.ts b/frontend/src/metabase/setup/components/InvactiveStep/index.ts index 291605f2210..322c2235fe8 100644 --- a/frontend/src/metabase/setup/components/InvactiveStep/index.ts +++ b/frontend/src/metabase/setup/components/InvactiveStep/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./InactiveStep"; +export * from "./InactiveStep"; diff --git a/frontend/src/metabase/setup/components/InviteUserForm/InviteUserForm.tsx b/frontend/src/metabase/setup/components/InviteUserForm/InviteUserForm.tsx index c294b2002a8..18172b8e9b7 100644 --- a/frontend/src/metabase/setup/components/InviteUserForm/InviteUserForm.tsx +++ b/frontend/src/metabase/setup/components/InviteUserForm/InviteUserForm.tsx @@ -28,7 +28,7 @@ interface InviteUserFormProps { onSubmit: (invite: InviteInfo) => void; } -const InviteUserForm = ({ +export const InviteUserForm = ({ user, invite, onSubmit, @@ -70,6 +70,3 @@ const InviteUserForm = ({ </FormProvider> ); }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default InviteUserForm; diff --git a/frontend/src/metabase/setup/components/InviteUserForm/index.ts b/frontend/src/metabase/setup/components/InviteUserForm/index.ts index 25c73a87eb1..e39e131da1c 100644 --- a/frontend/src/metabase/setup/components/InviteUserForm/index.ts +++ b/frontend/src/metabase/setup/components/InviteUserForm/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./InviteUserForm"; +export * from "./InviteUserForm"; diff --git a/frontend/src/metabase/setup/components/LanguageStep/LanguageStep.tsx b/frontend/src/metabase/setup/components/LanguageStep/LanguageStep.tsx index 934644edafb..c9de70d1623 100644 --- a/frontend/src/metabase/setup/components/LanguageStep/LanguageStep.tsx +++ b/frontend/src/metabase/setup/components/LanguageStep/LanguageStep.tsx @@ -1,12 +1,21 @@ import React, { useCallback, useMemo } from "react"; import { t } from "ttag"; import _ from "underscore"; +import { useDispatch, useSelector } from "metabase/lib/redux"; import Button from "metabase/core/components/Button"; -import { LocaleData } from "metabase-types/api"; import { Locale } from "metabase-types/store"; -import ActiveStep from "../ActiveStep"; -import InactiveStep from "../InvactiveStep"; +import { selectStep, updateLocale } from "../../actions"; +import { LANGUAGE_STEP, USER_STEP } from "../../constants"; +import { + getAvailableLocales, + getIsSetupCompleted, + getIsStepActive, + getIsStepCompleted, + getLocale, +} from "../../selectors"; import { getLocales } from "../../utils"; +import { ActiveStep } from "../ActiveStep"; +import { InactiveStep } from "../InvactiveStep"; import { LocaleGroup, LocaleInput, @@ -15,29 +24,31 @@ import { StepDescription, } from "./LanguageStep.styled"; -export interface LanguageStepProps { - locale?: Locale; - localeData?: LocaleData[]; - isStepActive: boolean; - isStepCompleted: boolean; - isSetupCompleted: boolean; - onLocaleChange: (locale: Locale) => void; - onStepSelect: () => void; - onStepSubmit: () => void; -} - -const LanguageStep = ({ - locale, - localeData, - isStepActive, - isStepCompleted, - isSetupCompleted, - onLocaleChange, - onStepSelect, - onStepSubmit, -}: LanguageStepProps): JSX.Element => { +export const LanguageStep = (): JSX.Element => { + const locale = useSelector(getLocale); + const localeData = useSelector(getAvailableLocales); + const isStepActive = useSelector(state => + getIsStepActive(state, LANGUAGE_STEP), + ); + const isStepCompleted = useSelector(state => + getIsStepCompleted(state, LANGUAGE_STEP), + ); + const isSetupCompleted = useSelector(state => getIsSetupCompleted(state)); const fieldId = useMemo(() => _.uniqueId(), []); const locales = useMemo(() => getLocales(localeData), [localeData]); + const dispatch = useDispatch(); + + const handleLocaleChange = (locale: Locale) => { + dispatch(updateLocale(locale)); + }; + + const handleStepSelect = () => { + dispatch(selectStep(LANGUAGE_STEP)); + }; + + const handleStepSubmit = () => { + dispatch(selectStep(USER_STEP)); + }; if (!isStepActive) { return ( @@ -46,7 +57,7 @@ const LanguageStep = ({ label={1} isStepCompleted={isStepCompleted} isSetupCompleted={isSetupCompleted} - onStepSelect={onStepSelect} + onStepSelect={handleStepSelect} /> ); } @@ -63,15 +74,17 @@ const LanguageStep = ({ locale={item} checked={item.code === locale?.code} fieldId={fieldId} - onLocaleChange={onLocaleChange} + onLocaleChange={handleLocaleChange} /> ))} </LocaleGroup> <Button primary={locale != null} disabled={locale == null} - onClick={onStepSubmit} - >{t`Next`}</Button> + onClick={handleStepSubmit} + > + {t`Next`} + </Button> </ActiveStep> ); }; @@ -107,6 +120,3 @@ const LocaleItem = ({ </LocaleLabel> ); }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default LanguageStep; diff --git a/frontend/src/metabase/setup/components/LanguageStep/LanguageStep.unit.spec.tsx b/frontend/src/metabase/setup/components/LanguageStep/LanguageStep.unit.spec.tsx index c53a2019af1..4207cc93331 100644 --- a/frontend/src/metabase/setup/components/LanguageStep/LanguageStep.unit.spec.tsx +++ b/frontend/src/metabase/setup/components/LanguageStep/LanguageStep.unit.spec.tsx @@ -1,46 +1,53 @@ import React from "react"; -import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Locale } from "metabase-types/store"; -import LanguageStep, { LanguageStepProps } from "./LanguageStep"; +import { + createMockLocale, + createMockSettingsState, + createMockSetupState, + createMockState, +} from "metabase-types/store/mocks"; +import { renderWithProviders, screen } from "__support__/ui"; +import { LANGUAGE_STEP, USER_STEP } from "../../constants"; +import { LanguageStep } from "./LanguageStep"; + +interface SetupOpts { + step?: number; + locale?: Locale; +} + +const setup = ({ step = LANGUAGE_STEP, locale }: SetupOpts = {}) => { + const state = createMockState({ + setup: createMockSetupState({ + step, + locale, + }), + settings: createMockSettingsState({ + "available-locales": [["en", "English"]], + }), + }); + + renderWithProviders(<LanguageStep />, { storeInitialState: state }); +}; describe("LanguageStep", () => { it("should render in inactive state", () => { - const props = getProps({ - locale: getLocale({ name: "English" }), - isStepActive: false, + setup({ + step: USER_STEP, + locale: createMockLocale({ name: "English" }), }); - render(<LanguageStep {...props} />); - expect(screen.getByText(/set to English/)).toBeInTheDocument(); }); it("should allow language selection", () => { - const props = getProps({ - isStepActive: true, - onLocaleChange: jest.fn(), + setup({ + step: LANGUAGE_STEP, }); - render(<LanguageStep {...props} />); - userEvent.click(screen.getByText("English")); + const option = screen.getByRole("radio", { name: "English" }); + userEvent.click(option); - expect(props.onLocaleChange).toHaveBeenCalled(); + expect(option).toBeChecked(); }); }); - -const getProps = (opts?: Partial<LanguageStepProps>): LanguageStepProps => ({ - isStepActive: false, - isSetupCompleted: false, - isStepCompleted: false, - onLocaleChange: jest.fn(), - onStepSelect: jest.fn(), - onStepSubmit: jest.fn(), - ...opts, -}); - -const getLocale = (opts?: Partial<Locale>): Locale => ({ - code: "en", - name: "English", - ...opts, -}); diff --git a/frontend/src/metabase/setup/components/LanguageStep/index.ts b/frontend/src/metabase/setup/components/LanguageStep/index.ts index 7529f054067..1dc8e31da93 100644 --- a/frontend/src/metabase/setup/components/LanguageStep/index.ts +++ b/frontend/src/metabase/setup/components/LanguageStep/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./LanguageStep"; +export * from "./LanguageStep"; diff --git a/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.tsx b/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.tsx index 7d80f9bf5c7..b2dbebfd7ff 100644 --- a/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.tsx +++ b/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.tsx @@ -1,10 +1,13 @@ import React, { useCallback, useState } from "react"; import { t } from "ttag"; import * as Yup from "yup"; +import { useSelector } from "metabase/lib/redux"; import FormProvider from "metabase/core/components/FormProvider"; import FormSubmitButton from "metabase/core/components/FormSubmitButton"; import * as Errors from "metabase/core/utils/errors"; import { SubscribeInfo } from "metabase-types/store"; +import { subscribeToNewsletter } from "../../utils"; +import { getUserEmail } from "../../selectors"; import { EmailForm, EmailFormHeader, @@ -23,25 +26,15 @@ const NEWSLETTER_SCHEMA = Yup.object({ email: Yup.string().required(Errors.required).email(Errors.email), }); -export interface NewsletterFormProps { - initialEmail?: string; - onSubscribe: (email: string) => void; -} - -const NewsletterForm = ({ - initialEmail = "", - onSubscribe, -}: NewsletterFormProps): JSX.Element => { - const initialValues = { email: initialEmail }; +export const NewsletterForm = (): JSX.Element => { + const initialEmail = useSelector(getUserEmail); + const initialValues = { email: initialEmail ?? "" }; const [isSubscribed, setIsSubscribed] = useState(false); - const handleSubmit = useCallback( - async ({ email }: SubscribeInfo) => { - await onSubscribe(email); - setIsSubscribed(true); - }, - [onSubscribe], - ); + const handleSubmit = useCallback(async ({ email }: SubscribeInfo) => { + await subscribeToNewsletter(email); + setIsSubscribed(true); + }, []); return ( <EmailFormRoot> @@ -82,6 +75,3 @@ const NewsletterForm = ({ </EmailFormRoot> ); }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default NewsletterForm; diff --git a/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.unit.spec.tsx b/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.unit.spec.tsx index f1710c807d0..1fc2a955c2b 100644 --- a/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.unit.spec.tsx +++ b/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.unit.spec.tsx @@ -1,19 +1,37 @@ import React from "react"; -import { render, screen, waitFor } from "@testing-library/react"; +import fetchMock from "fetch-mock"; import userEvent from "@testing-library/user-event"; -import NewsletterForm from "./NewsletterForm"; +import { + createMockSetupState, + createMockState, + createMockUserInfo, +} from "metabase-types/store/mocks"; +import { renderWithProviders, screen } from "__support__/ui"; +import { SUBSCRIBE_URL } from "../../constants"; +import { NewsletterForm } from "./NewsletterForm"; + +const USER_EMAIL = "user@metabase.test"; + +const setup = () => { + const state = createMockState({ + setup: createMockSetupState({ + user: createMockUserInfo({ + email: USER_EMAIL, + }), + }), + }); + + fetchMock.post(SUBSCRIBE_URL, {}); + renderWithProviders(<NewsletterForm />, { storeInitialState: state }); +}; describe("NewsletterForm", () => { it("should allow to submit the form with the provided email", async () => { - const email = "user@metabase.test"; - const onSubscribe = jest.fn().mockResolvedValue({}); + setup(); + expect(screen.getByDisplayValue(USER_EMAIL)).toBeInTheDocument(); - render(<NewsletterForm initialEmail={email} onSubscribe={onSubscribe} />); userEvent.click(screen.getByText("Subscribe")); - - await waitFor(() => { - expect(onSubscribe).toHaveBeenCalledWith(email); - }); - expect(screen.getByText(/You're subscribed/)).toBeInTheDocument(); + expect(await screen.findByText(/You're subscribed/)).toBeInTheDocument(); + expect(fetchMock.done(SUBSCRIBE_URL)).toBe(true); }); }); diff --git a/frontend/src/metabase/setup/components/NewsletterForm/index.ts b/frontend/src/metabase/setup/components/NewsletterForm/index.ts index f67211d1bc2..72401a4e8fe 100644 --- a/frontend/src/metabase/setup/components/NewsletterForm/index.ts +++ b/frontend/src/metabase/setup/components/NewsletterForm/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./NewsletterForm"; +export * from "./NewsletterForm"; diff --git a/frontend/src/metabase/setup/components/PreferencesStep/PreferencesStep.tsx b/frontend/src/metabase/setup/components/PreferencesStep/PreferencesStep.tsx index 896544afee6..22964ad7559 100644 --- a/frontend/src/metabase/setup/components/PreferencesStep/PreferencesStep.tsx +++ b/frontend/src/metabase/setup/components/PreferencesStep/PreferencesStep.tsx @@ -1,11 +1,20 @@ import React, { useState } from "react"; import { t, jt } from "ttag"; import { getIn } from "icepick"; +import { useDispatch, useSelector } from "metabase/lib/redux"; import Settings from "metabase/lib/settings"; import ActionButton from "metabase/components/ActionButton"; import ExternalLink from "metabase/core/components/ExternalLink"; -import ActiveStep from "../ActiveStep"; -import InactiveStep from "../InvactiveStep"; +import { selectStep, submitSetup, updateTracking } from "../../actions"; +import { PREFERENCES_STEP } from "../../constants"; +import { + getIsSetupCompleted, + getIsStepActive, + getIsStepCompleted, + getIsTrackingAllowed, +} from "../../selectors"; +import { ActiveStep } from "../ActiveStep"; +import { InactiveStep } from "../InvactiveStep"; import { StepDescription, StepToggleContainer, @@ -15,30 +24,29 @@ import { StepToggle, } from "./PreferencesStep.styled"; -export interface PreferencesStepProps { - isTrackingAllowed: boolean; - isStepActive: boolean; - isStepCompleted: boolean; - isSetupCompleted: boolean; - onTrackingChange: (isTrackingAllowed: boolean) => void; - onStepSelect: () => void; - onStepSubmit: (isTrackingAllowed: boolean) => void; -} - -const PreferencesStep = ({ - isTrackingAllowed, - isStepActive, - isStepCompleted, - isSetupCompleted, - onTrackingChange, - onStepSelect, - onStepSubmit, -}: PreferencesStepProps): JSX.Element => { +export const PreferencesStep = (): JSX.Element => { const [errorMessage, setErrorMessage] = useState<string>(); + const isTrackingAllowed = useSelector(getIsTrackingAllowed); + const isStepActive = useSelector(state => + getIsStepActive(state, PREFERENCES_STEP), + ); + const isStepCompleted = useSelector(state => + getIsStepCompleted(state, PREFERENCES_STEP), + ); + const isSetupCompleted = useSelector(getIsSetupCompleted); + const dispatch = useDispatch(); - const handleSubmit = async () => { + const handleTrackingChange = (isTrackingAllowed: boolean) => { + dispatch(updateTracking(isTrackingAllowed)); + }; + + const handleStepSelect = () => { + dispatch(selectStep(PREFERENCES_STEP)); + }; + + const handleStepSubmit = async () => { try { - await onStepSubmit(isTrackingAllowed); + await dispatch(submitSetup()).unwrap(); } catch (error) { setErrorMessage(getSubmitError(error)); throw error; @@ -52,7 +60,7 @@ const PreferencesStep = ({ label={4} isStepCompleted={isStepCompleted} isSetupCompleted={isSetupCompleted} - onStepSelect={onStepSelect} + onStepSelect={handleStepSelect} /> ); } @@ -74,7 +82,7 @@ const PreferencesStep = ({ <StepToggle value={isTrackingAllowed} autoFocus - onChange={onTrackingChange} + onChange={handleTrackingChange} aria-labelledby="anonymous-usage-events-label" /> <StepToggleLabel id="anonymous-usage-events-label"> @@ -97,7 +105,7 @@ const PreferencesStep = ({ successText={t`Success`} primary type="button" - actionFn={handleSubmit} + actionFn={handleStepSubmit} /> {errorMessage && <StepError>{errorMessage}</StepError>} </ActiveStep> @@ -129,6 +137,3 @@ const getSubmitError = (error: unknown): string => { return t`An error occurred`; } }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default PreferencesStep; diff --git a/frontend/src/metabase/setup/components/PreferencesStep/PreferencesStep.unit.spec.tsx b/frontend/src/metabase/setup/components/PreferencesStep/PreferencesStep.unit.spec.tsx index fb2cfd3e155..4d1e809246f 100644 --- a/frontend/src/metabase/setup/components/PreferencesStep/PreferencesStep.unit.spec.tsx +++ b/frontend/src/metabase/setup/components/PreferencesStep/PreferencesStep.unit.spec.tsx @@ -1,54 +1,50 @@ import React from "react"; -import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import PreferencesStep, { PreferencesStepProps } from "./PreferencesStep"; +import { + createMockSetupState, + createMockState, +} from "metabase-types/store/mocks"; +import { setupErrorSetupEndpoints } from "__support__/server-mocks"; +import { renderWithProviders, screen } from "__support__/ui"; +import { PREFERENCES_STEP, USER_STEP } from "../../constants"; +import { PreferencesStep } from "./PreferencesStep"; + +interface SetupOpts { + step?: number; +} + +const setup = ({ step = PREFERENCES_STEP }: SetupOpts = {}) => { + const state = createMockState({ + setup: createMockSetupState({ + step, + }), + }); + + setupErrorSetupEndpoints(); + renderWithProviders(<PreferencesStep />, { storeInitialState: state }); +}; describe("PreferencesStep", () => { it("should render in inactive state", () => { - const props = getProps({ - isStepActive: false, - }); - - render(<PreferencesStep {...props} />); + setup({ step: USER_STEP }); expect(screen.getByText("Usage data preferences")).toBeInTheDocument(); }); it("should allow toggling tracking permissions", () => { - const props = getProps({ - isTrackingAllowed: false, - isStepActive: true, - }); + setup({ step: PREFERENCES_STEP }); - render(<PreferencesStep {...props} />); - userEvent.click(screen.getByLabelText(/Allow Metabase/)); + const toggle = screen.getByRole("switch", { name: /Allow Metabase/ }); + userEvent.click(toggle); - expect(props.onTrackingChange).toHaveBeenCalledWith(true); + expect(toggle).toBeChecked(); }); it("should show an error message on submit", async () => { - const props = getProps({ - isTrackingAllowed: true, - isStepActive: true, - onStepSubmit: jest.fn().mockRejectedValue({}), - }); + setup({ step: PREFERENCES_STEP }); - render(<PreferencesStep {...props} />); userEvent.click(screen.getByText("Finish")); expect(await screen.findByText("An error occurred")).toBeInTheDocument(); }); }); - -const getProps = ( - opts?: Partial<PreferencesStepProps>, -): PreferencesStepProps => ({ - isTrackingAllowed: false, - isStepActive: false, - isStepCompleted: false, - isSetupCompleted: false, - onTrackingChange: jest.fn(), - onStepSelect: jest.fn(), - onStepSubmit: jest.fn(), - ...opts, -}); diff --git a/frontend/src/metabase/setup/components/PreferencesStep/index.ts b/frontend/src/metabase/setup/components/PreferencesStep/index.ts index 245a6767d0b..424eca37900 100644 --- a/frontend/src/metabase/setup/components/PreferencesStep/index.ts +++ b/frontend/src/metabase/setup/components/PreferencesStep/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./PreferencesStep"; +export * from "./PreferencesStep"; diff --git a/frontend/src/metabase/setup/components/SettingsPage/SettingsPage.tsx b/frontend/src/metabase/setup/components/SettingsPage/SettingsPage.tsx index bbc2c6af1b1..75fb1857b5d 100644 --- a/frontend/src/metabase/setup/components/SettingsPage/SettingsPage.tsx +++ b/frontend/src/metabase/setup/components/SettingsPage/SettingsPage.tsx @@ -1,47 +1,31 @@ -import React, { useEffect } from "react"; +import React from "react"; import LogoIcon from "metabase/components/LogoIcon"; -import MigrationHelp from "metabase/setup/containers/CloudMigrationHelp"; -import LanguageStep from "../../containers/LanguageStep"; -import UserStep from "../../containers/UserStep"; -import DatabaseStep from "../../containers/DatabaseStep"; -import DatabaseHelp from "../../containers/DatabaseHelp"; -import PreferencesStep from "../../containers/PreferencesStep"; -import CompletedStep from "../../containers/CompletedStep"; -import SetupHelp from "../SetupHelp"; -import { PageHeader, PageBody } from "./SettingsPage.styled"; - -export interface SettingsPageProps { - step: number; - onStepShow: (step: number) => void; -} - -const SettingsPage = ({ - step, - onStepShow, - ...props -}: SettingsPageProps): JSX.Element => { - useEffect(() => { - onStepShow(step); - }, [step, onStepShow]); +import { CloudMigrationHelp } from "../CloudMigrationHelp"; +import { DatabaseHelp } from "../DatabaseHelp"; +import { DatabaseStep } from "../DatabaseStep"; +import { CompletedStep } from "../CompletedStep"; +import { LanguageStep } from "../LanguageStep"; +import { PreferencesStep } from "../PreferencesStep"; +import { UserStep } from "../UserStep"; +import { SetupHelp } from "../SetupHelp"; +import { PageBody, PageHeader } from "./SettingsPage.styled"; +export const SettingsPage = (): JSX.Element => { return ( <div> <PageHeader> <LogoIcon height={51} /> </PageHeader> <PageBody> - <LanguageStep {...props} /> - <UserStep {...props} /> - <DatabaseStep {...props} /> - <DatabaseHelp {...props} /> - <PreferencesStep {...props} /> - <CompletedStep {...props} /> - <MigrationHelp {...props} /> - <SetupHelp {...props} /> + <LanguageStep /> + <UserStep /> + <DatabaseStep /> + <DatabaseHelp /> + <PreferencesStep /> + <CompletedStep /> + <CloudMigrationHelp /> + <SetupHelp /> </PageBody> </div> ); }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default SettingsPage; diff --git a/frontend/src/metabase/setup/components/SettingsPage/index.ts b/frontend/src/metabase/setup/components/SettingsPage/index.ts index 4504f4dc20b..ac38eafcaf2 100644 --- a/frontend/src/metabase/setup/components/SettingsPage/index.ts +++ b/frontend/src/metabase/setup/components/SettingsPage/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./SettingsPage"; +export * from "./SettingsPage"; diff --git a/frontend/src/metabase/setup/components/Setup/Setup.tsx b/frontend/src/metabase/setup/components/Setup/Setup.tsx index 9bf95fa0bcf..7491b5a3241 100644 --- a/frontend/src/metabase/setup/components/Setup/Setup.tsx +++ b/frontend/src/metabase/setup/components/Setup/Setup.tsx @@ -1,18 +1,30 @@ -import React from "react"; -import SettingsPage from "../../containers/SettingsPage"; -import WelcomePage from "../../containers/WelcomePage"; +import React, { useEffect } from "react"; +import { useUpdate } from "react-use"; +import { useSelector } from "metabase/lib/redux"; +import { trackStepSeen } from "../../analytics"; +import { WELCOME_STEP } from "../../constants"; +import { getIsLocaleLoaded, getStep } from "../../selectors"; +import { SettingsPage } from "../SettingsPage"; +import { WelcomePage } from "../WelcomePage"; -export interface SetupProps { - isWelcome: boolean; -} +export const Setup = (): JSX.Element => { + const step = useSelector(getStep); + const isLocaleLoaded = useSelector(getIsLocaleLoaded); + const update = useUpdate(); -const Setup = ({ isWelcome, ...props }: SetupProps): JSX.Element => { - if (isWelcome) { - return <WelcomePage {...props} />; + useEffect(() => { + trackStepSeen(step); + }, [step]); + + useEffect(() => { + if (isLocaleLoaded) { + update(); + } + }, [update, isLocaleLoaded]); + + if (step === WELCOME_STEP) { + return <WelcomePage />; } else { - return <SettingsPage {...props} />; + return <SettingsPage />; } }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default Setup; diff --git a/frontend/src/metabase/setup/components/Setup/index.ts b/frontend/src/metabase/setup/components/Setup/index.ts index e5707f00203..b68d7bc2a4d 100644 --- a/frontend/src/metabase/setup/components/Setup/index.ts +++ b/frontend/src/metabase/setup/components/Setup/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./Setup"; +export * from "./Setup"; diff --git a/frontend/src/metabase/setup/components/SetupHelp/SetupHelp.tsx b/frontend/src/metabase/setup/components/SetupHelp/SetupHelp.tsx index 00ef36fdab7..1e9f244b1df 100644 --- a/frontend/src/metabase/setup/components/SetupHelp/SetupHelp.tsx +++ b/frontend/src/metabase/setup/components/SetupHelp/SetupHelp.tsx @@ -4,7 +4,7 @@ import MetabaseSettings from "metabase/lib/settings"; import ExternalLink from "metabase/core/components/ExternalLink"; import { SetupFooterRoot } from "./SetupHelp.styled"; -const SetupHelp = (): JSX.Element => { +export const SetupHelp = (): JSX.Element => { return ( <SetupFooterRoot> {t`If you feel stuck`},{" "} @@ -19,6 +19,3 @@ const SetupHelp = (): JSX.Element => { </SetupFooterRoot> ); }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default SetupHelp; diff --git a/frontend/src/metabase/setup/components/SetupHelp/index.ts b/frontend/src/metabase/setup/components/SetupHelp/index.ts index ffc0bb44646..314044fa871 100644 --- a/frontend/src/metabase/setup/components/SetupHelp/index.ts +++ b/frontend/src/metabase/setup/components/SetupHelp/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./SetupHelp"; +export * from "./SetupHelp"; diff --git a/frontend/src/metabase/setup/components/SetupSection/SetupSection.tsx b/frontend/src/metabase/setup/components/SetupSection/SetupSection.tsx index 71fc7c7ff30..074a62d5a02 100644 --- a/frontend/src/metabase/setup/components/SetupSection/SetupSection.tsx +++ b/frontend/src/metabase/setup/components/SetupSection/SetupSection.tsx @@ -9,13 +9,13 @@ import { SectionButton, } from "./SetupSection.styled"; -export interface SetupSectionProps { +interface SetupSectionProps { title: ReactNode; description?: ReactNode; children?: ReactNode; } -const SetupSection = ({ +export const SetupSection = ({ title, description, children, @@ -41,6 +41,3 @@ const SetupSection = ({ </SectionRoot> ); }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default SetupSection; diff --git a/frontend/src/metabase/setup/components/SetupSection/index.ts b/frontend/src/metabase/setup/components/SetupSection/index.ts index 4e4f7a11c9f..aa90fcb4034 100644 --- a/frontend/src/metabase/setup/components/SetupSection/index.ts +++ b/frontend/src/metabase/setup/components/SetupSection/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./SetupSection"; +export * from "./SetupSection"; diff --git a/frontend/src/metabase/setup/components/UserForm/UserForm.tsx b/frontend/src/metabase/setup/components/UserForm/UserForm.tsx index 1f363bc6c59..37e43980eed 100644 --- a/frontend/src/metabase/setup/components/UserForm/UserForm.tsx +++ b/frontend/src/metabase/setup/components/UserForm/UserForm.tsx @@ -33,7 +33,11 @@ interface UserFormProps { onSubmit: (user: UserInfo) => void; } -const UserForm = ({ user, onValidatePassword, onSubmit }: UserFormProps) => { +export const UserForm = ({ + user, + onValidatePassword, + onSubmit, +}: UserFormProps) => { const initialValues = useMemo(() => { return user ?? USER_SCHEMA.getDefault(); }, [user]); @@ -96,6 +100,3 @@ const UserForm = ({ user, onValidatePassword, onSubmit }: UserFormProps) => { </FormProvider> ); }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default UserForm; diff --git a/frontend/src/metabase/setup/components/UserForm/index.ts b/frontend/src/metabase/setup/components/UserForm/index.ts index f572033248f..543de93ad1f 100644 --- a/frontend/src/metabase/setup/components/UserForm/index.ts +++ b/frontend/src/metabase/setup/components/UserForm/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./UserForm"; +export * from "./UserForm"; diff --git a/frontend/src/metabase/setup/components/UserStep/UserStep.tsx b/frontend/src/metabase/setup/components/UserStep/UserStep.tsx index 091ab252f36..f8e311291ea 100644 --- a/frontend/src/metabase/setup/components/UserStep/UserStep.tsx +++ b/frontend/src/metabase/setup/components/UserStep/UserStep.tsx @@ -1,32 +1,40 @@ import React from "react"; import { t } from "ttag"; +import { useDispatch, useSelector } from "metabase/lib/redux"; import { UserInfo } from "metabase-types/store"; -import ActiveStep from "../ActiveStep"; -import InactiveStep from "../InvactiveStep"; -import UserForm from "../UserForm"; +import { ActiveStep } from "../ActiveStep"; +import { InactiveStep } from "../InvactiveStep"; +import { UserForm } from "../UserForm"; +import { selectStep, submitUser } from "../../actions"; +import { USER_STEP } from "../../constants"; +import { + getIsHosted, + getIsSetupCompleted, + getIsStepActive, + getIsStepCompleted, + getUser, +} from "../../selectors"; +import { validatePassword } from "../../utils"; import { StepDescription } from "./UserStep.styled"; -export interface UserStepProps { - user?: UserInfo; - isHosted: boolean; - isStepActive: boolean; - isStepCompleted: boolean; - isSetupCompleted: boolean; - onValidatePassword: (password: string) => Promise<string | undefined>; - onStepSelect: () => void; - onStepSubmit: (user: UserInfo) => void; -} +export const UserStep = (): JSX.Element => { + const user = useSelector(getUser); + const isHosted = useSelector(getIsHosted); + const isStepActive = useSelector(state => getIsStepActive(state, USER_STEP)); + const isStepCompleted = useSelector(state => + getIsStepCompleted(state, USER_STEP), + ); + const isSetupCompleted = useSelector(getIsSetupCompleted); + const dispatch = useDispatch(); + + const handleStepSelect = () => { + dispatch(selectStep(USER_STEP)); + }; + + const handleSubmit = (user: UserInfo) => { + dispatch(submitUser(user)); + }; -const UserStep = ({ - user, - isHosted, - isStepActive, - isStepCompleted, - isSetupCompleted, - onValidatePassword, - onStepSelect, - onStepSubmit, -}: UserStepProps): JSX.Element => { if (!isStepActive) { return ( <InactiveStep @@ -34,7 +42,7 @@ const UserStep = ({ label={2} isStepCompleted={isStepCompleted} isSetupCompleted={isSetupCompleted} - onStepSelect={onStepSelect} + onStepSelect={handleStepSelect} /> ); } @@ -49,8 +57,8 @@ const UserStep = ({ )} <UserForm user={user} - onValidatePassword={onValidatePassword} - onSubmit={onStepSubmit} + onValidatePassword={validatePassword} + onSubmit={handleSubmit} /> </ActiveStep> ); @@ -62,6 +70,3 @@ const getStepTitle = (user: UserInfo | undefined, isStepCompleted: boolean) => { ? t`Hi${namePart}. Nice to meet you!` : t`What should we call you?`; }; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default UserStep; diff --git a/frontend/src/metabase/setup/components/UserStep/UserStep.unit.spec.tsx b/frontend/src/metabase/setup/components/UserStep/UserStep.unit.spec.tsx index b7022fe5de0..3f06df1b2bd 100644 --- a/frontend/src/metabase/setup/components/UserStep/UserStep.unit.spec.tsx +++ b/frontend/src/metabase/setup/components/UserStep/UserStep.unit.spec.tsx @@ -1,50 +1,43 @@ import React from "react"; -import { render, screen } from "@testing-library/react"; import { UserInfo } from "metabase-types/store"; -import UserStep, { UserStepProps } from "./UserStep"; +import { renderWithProviders, screen } from "__support__/ui"; +import { + createMockSetupState, + createMockState, + createMockUserInfo, +} from "metabase-types/store/mocks"; +import { DATABASE_STEP, USER_STEP } from "../../constants"; +import { UserStep } from "./UserStep"; + +interface SetupOpts { + step?: number; + user?: UserInfo; +} + +const setup = ({ step = USER_STEP, user }: SetupOpts = {}) => { + const state = createMockState({ + setup: createMockSetupState({ + step, + user, + }), + }); + + renderWithProviders(<UserStep />, { storeInitialState: state }); +}; describe("UserStep", () => { it("should render in active state", () => { - const props = getProps({ - isStepActive: true, - isStepCompleted: false, - }); - - render(<UserStep {...props} />); + setup({ step: USER_STEP }); expect(screen.getByText("What should we call you?")).toBeInTheDocument(); }); it("should render in completed state", () => { - const props = getProps({ - user: getUserInfo({ first_name: "Testy" }), - isStepActive: false, - isStepCompleted: true, + setup({ + step: DATABASE_STEP, + user: createMockUserInfo({ first_name: "Testy" }), }); - render(<UserStep {...props} />); - expect(screen.getByText(/Hi, Testy/)).toBeInTheDocument(); }); }); - -const getProps = (opts?: Partial<UserStepProps>): UserStepProps => ({ - isHosted: false, - isStepActive: false, - isStepCompleted: false, - isSetupCompleted: false, - onValidatePassword: jest.fn(), - onStepSelect: jest.fn(), - onStepSubmit: jest.fn(), - ...opts, -}); - -const getUserInfo = (opts?: Partial<UserInfo>): UserInfo => ({ - first_name: "Testy", - last_name: "McTestface", - email: "testy@metabase.test", - site_name: "Epic Team", - password: "metasample123", - password_confirm: "metasample123", - ...opts, -}); diff --git a/frontend/src/metabase/setup/components/UserStep/index.ts b/frontend/src/metabase/setup/components/UserStep/index.ts index 1b53c6e796e..5282afb1448 100644 --- a/frontend/src/metabase/setup/components/UserStep/index.ts +++ b/frontend/src/metabase/setup/components/UserStep/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./UserStep"; +export * from "./UserStep"; diff --git a/frontend/src/metabase/setup/components/UserStep/types.ts b/frontend/src/metabase/setup/components/UserStep/types.ts deleted file mode 100644 index 77ddb0ba99d..00000000000 --- a/frontend/src/metabase/setup/components/UserStep/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ComponentType } from "react"; - -export interface FormProps { - Form: ComponentType; - FormField: ComponentType<FormFieldProps>; - FormFooter: ComponentType<FormFooterProps>; -} - -export interface FormFieldProps { - name: string; -} - -export interface FormFooterProps { - submitTitle?: string; -} diff --git a/frontend/src/metabase/setup/components/WelcomePage/WelcomePage.tsx b/frontend/src/metabase/setup/components/WelcomePage/WelcomePage.tsx index 23271c6b9e0..74883c0a495 100644 --- a/frontend/src/metabase/setup/components/WelcomePage/WelcomePage.tsx +++ b/frontend/src/metabase/setup/components/WelcomePage/WelcomePage.tsx @@ -1,8 +1,12 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; +import { useTimeout } from "react-use"; import { t } from "ttag"; +import { useDispatch, useSelector } from "metabase/lib/redux"; import LogoIcon from "metabase/components/LogoIcon"; -import { LOCALE_TIMEOUT } from "../../constants"; -import SetupHelp from "../SetupHelp"; +import { loadDefaults, selectStep } from "../../actions"; +import { LANGUAGE_STEP, LOCALE_TIMEOUT } from "../../constants"; +import { getIsLocaleLoaded } from "../../selectors"; +import { SetupHelp } from "../SetupHelp"; import { PageRoot, PageMain, @@ -11,24 +15,20 @@ import { PageButton, } from "./WelcomePage.styled"; -export interface WelcomePageProps { - isLocaleLoaded: boolean; - onStepShow: () => void; - onStepSubmit: () => void; -} +export const WelcomePage = (): JSX.Element | null => { + const [isElapsed] = useTimeout(LOCALE_TIMEOUT); + const isLocaleLoaded = useSelector(getIsLocaleLoaded); + const dispatch = useDispatch(); -const WelcomePage = ({ - isLocaleLoaded, - onStepShow, - onStepSubmit, -}: WelcomePageProps): JSX.Element | null => { - const isElapsed = useIsElapsed(LOCALE_TIMEOUT); + const handleStepSubmit = () => { + dispatch(selectStep(LANGUAGE_STEP)); + }; useEffect(() => { - onStepShow(); - }, [onStepShow]); + dispatch(loadDefaults()); + }, [dispatch]); - if (!isElapsed && !isLocaleLoaded) { + if (!isElapsed() && !isLocaleLoaded) { return null; } @@ -41,27 +41,11 @@ const WelcomePage = ({ {t`Looks like everything is working.`}{" "} {t`Now let’s get to know you, connect to your data, and start finding you some answers!`} </PageBody> - <PageButton - primary - autoFocus - onClick={onStepSubmit} - >{t`Let's get started`}</PageButton> + <PageButton primary autoFocus onClick={handleStepSubmit}> + {t`Let's get started`} + </PageButton> </PageMain> <SetupHelp /> </PageRoot> ); }; - -const useIsElapsed = (delay: number) => { - const [isElapsed, setIsElapsed] = useState(false); - - useEffect(() => { - const timeout = setTimeout(() => setIsElapsed(true), delay); - return () => clearTimeout(timeout); - }, [delay]); - - return isElapsed; -}; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default WelcomePage; diff --git a/frontend/src/metabase/setup/components/WelcomePage/WelcomePage.unit.spec.tsx b/frontend/src/metabase/setup/components/WelcomePage/WelcomePage.unit.spec.tsx index f3829c356f8..7783520d744 100644 --- a/frontend/src/metabase/setup/components/WelcomePage/WelcomePage.unit.spec.tsx +++ b/frontend/src/metabase/setup/components/WelcomePage/WelcomePage.unit.spec.tsx @@ -1,49 +1,26 @@ import React from "react"; -import { render, screen } from "@testing-library/react"; -import WelcomePage, { WelcomePageProps } from "./WelcomePage"; - -describe("WelcomePage", () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.useRealTimers(); +import { + createMockSettingsState, + createMockState, +} from "metabase-types/store/mocks"; +import { renderWithProviders, screen } from "__support__/ui"; +import { WelcomePage } from "./WelcomePage"; + +const setup = () => { + const state = createMockState({ + settings: createMockSettingsState({ + "available-locales": [["en", "English"]], + }), }); - it("should not render until the locale is loaded", () => { - const props = getProps({ isLocaleLoaded: false }); + renderWithProviders(<WelcomePage />, { storeInitialState: state }); +}; - render(<WelcomePage {...props} />); +describe("WelcomePage", () => { + it("should render before the timeout when the locale is loaded", async () => { + setup(); expect(screen.queryByText("Welcome to Metabase")).not.toBeInTheDocument(); + expect(await screen.findByText("Welcome to Metabase")).toBeInTheDocument(); }); - - it("should render after some time even if the locale is not loaded", () => { - const oldProps = getProps({ isLocaleLoaded: false }); - const newProps = getProps({ isLocaleLoaded: false }); - - const { rerender } = render(<WelcomePage {...oldProps} />); - jest.advanceTimersByTime(310); - rerender(<WelcomePage {...newProps} />); - - expect(screen.getByText("Welcome to Metabase")).toBeInTheDocument(); - }); - - it("should render before the timeout if the locale is loaded", () => { - const oldProps = getProps({ isLocaleLoaded: false }); - const newProps = getProps({ isLocaleLoaded: true }); - - const { rerender } = render(<WelcomePage {...oldProps} />); - rerender(<WelcomePage {...newProps} />); - - expect(screen.getByText("Welcome to Metabase")).toBeInTheDocument(); - }); -}); - -const getProps = (opts?: Partial<WelcomePageProps>): WelcomePageProps => ({ - isLocaleLoaded: false, - onStepShow: jest.fn(), - onStepSubmit: jest.fn(), - ...opts, }); diff --git a/frontend/src/metabase/setup/components/WelcomePage/index.ts b/frontend/src/metabase/setup/components/WelcomePage/index.ts index 5e8fd9ce42f..741f2b83079 100644 --- a/frontend/src/metabase/setup/components/WelcomePage/index.ts +++ b/frontend/src/metabase/setup/components/WelcomePage/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./WelcomePage"; +export * from "./WelcomePage"; diff --git a/frontend/src/metabase/setup/constants.ts b/frontend/src/metabase/setup/constants.ts index 2b781deee24..49eb2fa3ea2 100644 --- a/frontend/src/metabase/setup/constants.ts +++ b/frontend/src/metabase/setup/constants.ts @@ -15,3 +15,7 @@ export const STEPS: Record<number, string> = { [PREFERENCES_STEP]: "data_usage", [COMPLETED_STEP]: "completed", }; + +export const SUBSCRIBE_URL = + "https://metabase.us10.list-manage.com/subscribe/post?u=869fec0e4689e8fd1db91e795&id=b9664113a8"; +export const SUBSCRIBE_TOKEN = "b_869fec0e4689e8fd1db91e795_b9664113a8"; diff --git a/frontend/src/metabase/setup/containers/CloudMigrationHelp/CloudMigrationHelp.tsx b/frontend/src/metabase/setup/containers/CloudMigrationHelp/CloudMigrationHelp.tsx deleted file mode 100644 index c69366987f0..00000000000 --- a/frontend/src/metabase/setup/containers/CloudMigrationHelp/CloudMigrationHelp.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from "react"; -import { connect } from "react-redux"; -import { t } from "ttag"; - -import HelpCard from "metabase/components/HelpCard"; -import { SetupCardContainer } from "metabase/setup/components/SetupCardContainer"; - -import MetabaseSettings from "metabase/lib/settings"; -import { getSetting } from "metabase/selectors/settings"; - -import type { State } from "metabase-types/store"; - -import { COMPLETED_STEP } from "../../constants"; -import { isStepActive } from "../../selectors"; - -const mapStateToProps = (state: State) => ({ - isHosted: getSetting(state, "is-hosted?"), - isStepActive: isStepActive(state, COMPLETED_STEP), -}); - -interface CloudMigrationHelpProps { - isHosted: boolean; - isStepActive: boolean; -} - -const CloudMigrationHelp = ({ - isHosted, - isStepActive, -}: CloudMigrationHelpProps) => { - const isVisible = isHosted && isStepActive; - - return ( - <SetupCardContainer isVisible={isVisible}> - <HelpCard - title={t`Migrating from self-hosted?`} - helpUrl={MetabaseSettings.migrateToCloudGuideUrl()} - >{t`Check out our docs for how to migrate your self-hosted instance to Cloud.`}</HelpCard> - </SetupCardContainer> - ); -}; - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default connect(mapStateToProps)(CloudMigrationHelp); diff --git a/frontend/src/metabase/setup/containers/CloudMigrationHelp/index.ts b/frontend/src/metabase/setup/containers/CloudMigrationHelp/index.ts deleted file mode 100644 index 8c4ece7c071..00000000000 --- a/frontend/src/metabase/setup/containers/CloudMigrationHelp/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./CloudMigrationHelp"; diff --git a/frontend/src/metabase/setup/containers/CompletedStep/CompletedStep.tsx b/frontend/src/metabase/setup/containers/CompletedStep/CompletedStep.tsx deleted file mode 100644 index 52f6d59a3da..00000000000 --- a/frontend/src/metabase/setup/containers/CompletedStep/CompletedStep.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { connect } from "react-redux"; -import { State } from "metabase-types/store"; -import CompletedStep from "../../components/CompletedStep"; -import { COMPLETED_STEP } from "../../constants"; -import { isLocaleLoaded, isStepActive } from "../../selectors"; - -const mapStateToProps = (state: State) => ({ - isStepActive: isStepActive(state, COMPLETED_STEP), - isLocaleLoaded: isLocaleLoaded(state), -}); - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default connect(mapStateToProps)(CompletedStep); diff --git a/frontend/src/metabase/setup/containers/CompletedStep/index.ts b/frontend/src/metabase/setup/containers/CompletedStep/index.ts deleted file mode 100644 index 43cf9c3b966..00000000000 --- a/frontend/src/metabase/setup/containers/CompletedStep/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./CompletedStep"; diff --git a/frontend/src/metabase/setup/containers/DatabaseHelp/DatabaseHelp.tsx b/frontend/src/metabase/setup/containers/DatabaseHelp/DatabaseHelp.tsx deleted file mode 100644 index 34fa4f327ea..00000000000 --- a/frontend/src/metabase/setup/containers/DatabaseHelp/DatabaseHelp.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { connect } from "react-redux"; -import { State } from "metabase-types/store"; -import DatabaseHelp from "../../components/DatabaseHelp"; -import { DATABASE_STEP } from "../../constants"; -import { - getDatabaseEngine, - isLocaleLoaded, - isStepActive, -} from "../../selectors"; - -const mapStateToProps = (state: State) => ({ - engine: getDatabaseEngine(state), - isStepActive: isStepActive(state, DATABASE_STEP), - isLocaleLoaded: isLocaleLoaded(state), -}); - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default connect(mapStateToProps)(DatabaseHelp); diff --git a/frontend/src/metabase/setup/containers/DatabaseHelp/index.ts b/frontend/src/metabase/setup/containers/DatabaseHelp/index.ts deleted file mode 100644 index c8244930946..00000000000 --- a/frontend/src/metabase/setup/containers/DatabaseHelp/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./DatabaseHelp"; diff --git a/frontend/src/metabase/setup/containers/DatabaseStep/DatabaseStep.tsx b/frontend/src/metabase/setup/containers/DatabaseStep/DatabaseStep.tsx deleted file mode 100644 index 9a830af4e16..00000000000 --- a/frontend/src/metabase/setup/containers/DatabaseStep/DatabaseStep.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { connect } from "react-redux"; -import Settings from "metabase/lib/settings"; -import { DatabaseData } from "metabase-types/api"; -import { State, InviteInfo } from "metabase-types/store"; -import DatabaseStep from "../../components/DatabaseStep"; -import { - setDatabaseEngine, - setDatabase, - setInvite, - setStep, - submitDatabase, -} from "../../actions"; -import { - trackDatabaseSelected, - trackAddDataLaterClicked, - trackDatabaseStepCompleted, -} from "../../analytics"; -import { DATABASE_STEP, PREFERENCES_STEP } from "../../constants"; -import { - getDatabase, - isStepActive, - isStepCompleted, - isSetupCompleted, - getDatabaseEngine, - getInvite, - getUser, - isLocaleLoaded, -} from "../../selectors"; - -const mapStateToProps = (state: State) => ({ - user: getUser(state), - database: getDatabase(state), - engine: getDatabaseEngine(state), - invite: getInvite(state), - isEmailConfigured: Settings.isEmailConfigured(), - isStepActive: isStepActive(state, DATABASE_STEP), - isStepCompleted: isStepCompleted(state, DATABASE_STEP), - isSetupCompleted: isSetupCompleted(state), - isLocaleLoaded: isLocaleLoaded(state), -}); - -const mapDispatchToProps = (dispatch: any) => ({ - onEngineChange: (engine?: string) => { - if (engine) { - trackDatabaseSelected(engine); - } - dispatch(setDatabaseEngine(engine || null)); - }, - onStepSelect: () => { - dispatch(setStep(DATABASE_STEP)); - }, - onDatabaseSubmit: async (database: DatabaseData) => { - await dispatch(submitDatabase(database)); - dispatch(setInvite(null)); - dispatch(setStep(PREFERENCES_STEP)); - trackDatabaseStepCompleted(database.engine); - }, - onInviteSubmit: (invite: InviteInfo) => { - dispatch(setDatabase(null)); - dispatch(setInvite(invite)); - dispatch(setStep(PREFERENCES_STEP)); - trackDatabaseStepCompleted(); - }, - onStepCancel: (engine?: string) => { - dispatch(setDatabase(null)); - dispatch(setInvite(null)); - dispatch(setStep(PREFERENCES_STEP)); - trackDatabaseStepCompleted(); - trackAddDataLaterClicked(engine); - }, -}); - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default connect(mapStateToProps, mapDispatchToProps)(DatabaseStep); diff --git a/frontend/src/metabase/setup/containers/DatabaseStep/index.ts b/frontend/src/metabase/setup/containers/DatabaseStep/index.ts deleted file mode 100644 index 60865dfe04a..00000000000 --- a/frontend/src/metabase/setup/containers/DatabaseStep/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./DatabaseStep"; diff --git a/frontend/src/metabase/setup/containers/LanguageStep/LanguageStep.tsx b/frontend/src/metabase/setup/containers/LanguageStep/LanguageStep.tsx deleted file mode 100644 index 59c427ece1e..00000000000 --- a/frontend/src/metabase/setup/containers/LanguageStep/LanguageStep.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { connect } from "react-redux"; -import Settings from "metabase/lib/settings"; -import { State, Locale } from "metabase-types/store"; -import LanguageStep from "../../components/LanguageStep"; -import { setLocale, setStep } from "../../actions"; -import { LANGUAGE_STEP, USER_STEP } from "../../constants"; -import { - getLocale, - isStepActive, - isStepCompleted, - isSetupCompleted, - isLocaleLoaded, -} from "../../selectors"; - -const mapStateToProps = (state: State) => ({ - locale: getLocale(state), - localeData: Settings.get("available-locales") || [], - isStepActive: isStepActive(state, LANGUAGE_STEP), - isStepCompleted: isStepCompleted(state, LANGUAGE_STEP), - isSetupCompleted: isSetupCompleted(state), - isLocaleLoaded: isLocaleLoaded(state), -}); - -const mapDispatchToProps = (dispatch: any) => ({ - onLocaleChange: (locale: Locale) => { - dispatch(setLocale(locale)); - }, - onStepSelect: () => { - dispatch(setStep(LANGUAGE_STEP)); - }, - onStepSubmit: () => { - dispatch(setStep(USER_STEP)); - }, -}); - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default connect(mapStateToProps, mapDispatchToProps)(LanguageStep); diff --git a/frontend/src/metabase/setup/containers/LanguageStep/index.ts b/frontend/src/metabase/setup/containers/LanguageStep/index.ts deleted file mode 100644 index 7529f054067..00000000000 --- a/frontend/src/metabase/setup/containers/LanguageStep/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./LanguageStep"; diff --git a/frontend/src/metabase/setup/containers/NewsletterForm/NewsletterForm.tsx b/frontend/src/metabase/setup/containers/NewsletterForm/NewsletterForm.tsx deleted file mode 100644 index 50e1ee45298..00000000000 --- a/frontend/src/metabase/setup/containers/NewsletterForm/NewsletterForm.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from "react-redux"; -import { subscribeToNewsletter } from "metabase/setup/utils"; -import NewsletterForm from "../../components/NewsletterForm"; -import { getUserEmail, isLocaleLoaded } from "../../selectors"; - -const mapStateToProps = (state: any) => ({ - initialEmail: getUserEmail(state), - isLocaleLoaded: isLocaleLoaded(state), -}); - -const mapDispatchToProps = () => ({ - onSubscribe: subscribeToNewsletter, -}); - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default connect(mapStateToProps, mapDispatchToProps)(NewsletterForm); diff --git a/frontend/src/metabase/setup/containers/NewsletterForm/index.ts b/frontend/src/metabase/setup/containers/NewsletterForm/index.ts deleted file mode 100644 index f67211d1bc2..00000000000 --- a/frontend/src/metabase/setup/containers/NewsletterForm/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./NewsletterForm"; diff --git a/frontend/src/metabase/setup/containers/PreferencesStep/PreferencesStep.tsx b/frontend/src/metabase/setup/containers/PreferencesStep/PreferencesStep.tsx deleted file mode 100644 index 4c4c8586c49..00000000000 --- a/frontend/src/metabase/setup/containers/PreferencesStep/PreferencesStep.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { connect } from "react-redux"; -import Settings from "metabase/lib/settings"; -import { State } from "metabase-types/store"; -import PreferencesStep from "../../components/PreferencesStep"; -import { setTracking, submitSetup, setStep } from "../../actions"; -import { - trackTrackingChanged, - trackSetupCompleted, - trackPreferencesStepCompleted, -} from "../../analytics"; -import { PREFERENCES_STEP, COMPLETED_STEP } from "../../constants"; -import { - isTrackingAllowed, - isStepActive, - isStepCompleted, - isSetupCompleted, - isLocaleLoaded, -} from "../../selectors"; - -const mapStateToProps = (state: State) => ({ - isTrackingAllowed: isTrackingAllowed(state), - isStepActive: isStepActive(state, PREFERENCES_STEP), - isStepCompleted: isStepCompleted(state, PREFERENCES_STEP), - isSetupCompleted: isSetupCompleted(state), - isLocaleLoaded: isLocaleLoaded(state), -}); - -const mapDispatchToProps = (dispatch: any) => ({ - onTrackingChange: (isTrackingAllowed: boolean) => { - dispatch(setTracking(isTrackingAllowed)); - trackTrackingChanged(isTrackingAllowed); - Settings.set("anon-tracking-enabled", isTrackingAllowed); - trackTrackingChanged(isTrackingAllowed); - }, - onStepSelect: () => { - dispatch(setStep(PREFERENCES_STEP)); - }, - onStepSubmit: async (isTrackingAllowed: boolean) => { - await dispatch(submitSetup()); - dispatch(setStep(COMPLETED_STEP)); - trackPreferencesStepCompleted(isTrackingAllowed); - trackSetupCompleted(); - }, -}); - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default connect(mapStateToProps, mapDispatchToProps)(PreferencesStep); diff --git a/frontend/src/metabase/setup/containers/PreferencesStep/index.ts b/frontend/src/metabase/setup/containers/PreferencesStep/index.ts deleted file mode 100644 index 245a6767d0b..00000000000 --- a/frontend/src/metabase/setup/containers/PreferencesStep/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./PreferencesStep"; diff --git a/frontend/src/metabase/setup/containers/SettingsPage/SettingsPage.tsx b/frontend/src/metabase/setup/containers/SettingsPage/SettingsPage.tsx deleted file mode 100644 index c1f300342b2..00000000000 --- a/frontend/src/metabase/setup/containers/SettingsPage/SettingsPage.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { connect } from "react-redux"; -import { State } from "metabase-types/store"; -import SettingsPage from "../../components/SettingsPage"; -import { trackStepSeen } from "../../analytics"; -import { getStep, isLocaleLoaded } from "../../selectors"; - -const mapStateToProps = (state: State) => ({ - step: getStep(state), - isLocaleLoaded: isLocaleLoaded(state), -}); - -const mapDispatchToProps = () => ({ - onStepShow: (step: number) => { - trackStepSeen(step); - }, -}); - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default connect(mapStateToProps, mapDispatchToProps)(SettingsPage); diff --git a/frontend/src/metabase/setup/containers/SettingsPage/index.ts b/frontend/src/metabase/setup/containers/SettingsPage/index.ts deleted file mode 100644 index 4504f4dc20b..00000000000 --- a/frontend/src/metabase/setup/containers/SettingsPage/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./SettingsPage"; diff --git a/frontend/src/metabase/setup/containers/SetupApp/SetupApp.tsx b/frontend/src/metabase/setup/containers/SetupApp/SetupApp.tsx deleted file mode 100644 index 1252619a4bc..00000000000 --- a/frontend/src/metabase/setup/containers/SetupApp/SetupApp.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from "react-redux"; -import { State } from "metabase-types/store"; -import Setup from "../../components/Setup"; -import { WELCOME_STEP } from "../../constants"; -import { isStepActive } from "../../selectors"; - -const mapStateToProps = (state: State) => ({ - isWelcome: isStepActive(state, WELCOME_STEP), -}); - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default connect(mapStateToProps)(Setup); diff --git a/frontend/src/metabase/setup/containers/SetupApp/index.ts b/frontend/src/metabase/setup/containers/SetupApp/index.ts deleted file mode 100644 index d9b8af8d63b..00000000000 --- a/frontend/src/metabase/setup/containers/SetupApp/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./SetupApp"; diff --git a/frontend/src/metabase/setup/containers/UserStep/UserStep.tsx b/frontend/src/metabase/setup/containers/UserStep/UserStep.tsx deleted file mode 100644 index 97909062be9..00000000000 --- a/frontend/src/metabase/setup/containers/UserStep/UserStep.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { connect } from "react-redux"; -import Settings from "metabase/lib/settings"; -import { State, UserInfo } from "metabase-types/store"; -import UserStep from "../../components/UserStep"; -import { setUser, validatePassword, setStep } from "../../actions"; -import { trackUserStepCompleted } from "../../analytics"; -import { USER_STEP, DATABASE_STEP } from "../../constants"; -import { - getUser, - isStepActive, - isStepCompleted, - isSetupCompleted, - isLocaleLoaded, -} from "../../selectors"; - -const mapStateToProps = (state: State) => ({ - user: getUser(state), - isHosted: Settings.isHosted(), - isStepActive: isStepActive(state, USER_STEP), - isStepCompleted: isStepCompleted(state, USER_STEP), - isSetupCompleted: isSetupCompleted(state), - isLocaleLoaded: isLocaleLoaded(state), - onValidatePassword: validatePassword, -}); - -const mapDispatchToProps = (dispatch: any) => ({ - onStepSelect: () => { - dispatch(setStep(USER_STEP)); - }, - onStepSubmit: (user: UserInfo) => { - dispatch(setUser(user)); - dispatch(setStep(DATABASE_STEP)); - trackUserStepCompleted(); - }, -}); - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default connect(mapStateToProps, mapDispatchToProps)(UserStep); diff --git a/frontend/src/metabase/setup/containers/UserStep/index.ts b/frontend/src/metabase/setup/containers/UserStep/index.ts deleted file mode 100644 index 1b53c6e796e..00000000000 --- a/frontend/src/metabase/setup/containers/UserStep/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./UserStep"; diff --git a/frontend/src/metabase/setup/containers/WelcomePage/WelcomePage.tsx b/frontend/src/metabase/setup/containers/WelcomePage/WelcomePage.tsx deleted file mode 100644 index 0c16d248a8b..00000000000 --- a/frontend/src/metabase/setup/containers/WelcomePage/WelcomePage.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { connect } from "react-redux"; -import { State } from "metabase-types/store"; -import WelcomePage from "../../components/WelcomePage"; -import { setStep, loadUserDefaults, loadLocaleDefaults } from "../../actions"; -import { trackWelcomeStepCompleted, trackStepSeen } from "../../analytics"; -import { LANGUAGE_STEP, WELCOME_STEP } from "../../constants"; -import { isLocaleLoaded } from "../../selectors"; - -const mapStateToProps = (state: State) => ({ - isLocaleLoaded: isLocaleLoaded(state), -}); - -const mapDispatchToProps = (dispatch: any) => ({ - onStepShow: () => { - dispatch(loadUserDefaults()); - dispatch(loadLocaleDefaults()); - trackStepSeen(WELCOME_STEP); - }, - onStepSubmit: () => { - dispatch(setStep(LANGUAGE_STEP)); - trackWelcomeStepCompleted(); - }, -}); - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default connect(mapStateToProps, mapDispatchToProps)(WelcomePage); diff --git a/frontend/src/metabase/setup/containers/WelcomePage/index.ts b/frontend/src/metabase/setup/containers/WelcomePage/index.ts deleted file mode 100644 index 5e8fd9ce42f..00000000000 --- a/frontend/src/metabase/setup/containers/WelcomePage/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./WelcomePage"; diff --git a/frontend/src/metabase/setup/reducers.ts b/frontend/src/metabase/setup/reducers.ts index 01fc76d5afb..91097f28ed5 100644 --- a/frontend/src/metabase/setup/reducers.ts +++ b/frontend/src/metabase/setup/reducers.ts @@ -1,81 +1,78 @@ -import { handleActions } from "redux-actions"; +import { createReducer } from "@reduxjs/toolkit"; +import { SetupState } from "metabase-types/store"; import { - SET_LOCALE, - SET_STEP, - SET_USER, - SET_DATABASE_ENGINE, - SET_DATABASE, - SET_TRACKING, - SET_INVITE, - SET_LOCALE_LOADED, + skipDatabase, + loadLocaleDefaults, + loadUserDefaults, + selectStep, + submitDatabase, + submitUser, + submitUserInvite, + updateDatabaseEngine, + updateLocale, + updateTracking, + submitSetup, } from "./actions"; -import { WELCOME_STEP } from "./constants"; - -export const step = handleActions( - { - [SET_STEP]: { next: (state, { payload }) => payload }, - }, +import { + COMPLETED_STEP, + DATABASE_STEP, + PREFERENCES_STEP, WELCOME_STEP, -); - -export const locale = handleActions( - { - [SET_LOCALE]: { next: (state, { payload }) => payload }, - }, - null, -); - -export const user = handleActions( - { - [SET_USER]: { next: (state, { payload }) => payload }, - }, - null, -); - -export const databaseEngine = handleActions( - { - [SET_DATABASE_ENGINE]: { next: (state, { payload }) => payload }, - }, - null, -); +} from "./constants"; -export const database = handleActions( - { - [SET_DATABASE]: { next: (state, { payload }) => payload }, - }, - null, -); - -export const invite = handleActions( - { - [SET_INVITE]: { next: (state, { payload }) => payload }, - }, - null, -); - -export const isLocaleLoaded = handleActions( - { - [SET_LOCALE]: { next: () => false }, - [SET_LOCALE_LOADED]: { next: () => true }, - }, - false, -); - -export const isTrackingAllowed = handleActions( - { - [SET_TRACKING]: { next: (state, { payload }) => payload }, - }, - true, -); - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default { - step, - locale, - user, - database, - databaseEngine, - invite, - isLocaleLoaded, - isTrackingAllowed, +const initialState: SetupState = { + step: WELCOME_STEP, + isLocaleLoaded: false, + isTrackingAllowed: true, }; + +export const reducer = createReducer(initialState, builder => { + builder.addCase(loadUserDefaults.fulfilled, (state, { payload: user }) => { + state.user = user; + }); + builder.addCase( + loadLocaleDefaults.fulfilled, + (state, { payload: locale }) => { + state.locale = locale; + state.isLocaleLoaded = true; + }, + ); + builder.addCase(selectStep, (state, { payload: step }) => { + state.step = step; + }); + builder.addCase(updateLocale.pending, (state, { meta }) => { + state.locale = meta.arg; + state.isLocaleLoaded = false; + }); + builder.addCase(updateLocale.fulfilled, state => { + state.isLocaleLoaded = true; + }); + builder.addCase(submitUser.pending, (state, { meta }) => { + state.user = meta.arg; + state.step = DATABASE_STEP; + }); + builder.addCase(updateDatabaseEngine.pending, (state, { meta }) => { + state.databaseEngine = meta.arg; + }); + builder.addCase(submitDatabase.fulfilled, (state, { payload: database }) => { + state.database = database; + state.invite = undefined; + state.step = PREFERENCES_STEP; + }); + builder.addCase(submitUserInvite.pending, (state, { meta }) => { + state.database = undefined; + state.invite = meta.arg; + state.step = PREFERENCES_STEP; + }); + builder.addCase(skipDatabase.pending, state => { + state.database = undefined; + state.invite = undefined; + state.step = PREFERENCES_STEP; + }); + builder.addCase(updateTracking.pending, (state, { meta }) => { + state.isTrackingAllowed = meta.arg; + }); + builder.addCase(submitSetup.fulfilled, state => { + state.step = COMPLETED_STEP; + }); +}); diff --git a/frontend/src/metabase/setup/selectors.ts b/frontend/src/metabase/setup/selectors.ts index 16976e07756..463c6617812 100644 --- a/frontend/src/metabase/setup/selectors.ts +++ b/frontend/src/metabase/setup/selectors.ts @@ -1,7 +1,10 @@ -import { DatabaseData } from "metabase-types/api"; +import { DatabaseData, LocaleData } from "metabase-types/api"; import { InviteInfo, Locale, State, UserInfo } from "metabase-types/store"; +import { getSetting } from "metabase/selectors/settings"; import { COMPLETED_STEP } from "./constants"; +const DEFAULT_LOCALES: LocaleData[] = []; + export const getStep = (state: State): number => { return state.setup.step; }; @@ -26,26 +29,42 @@ export const getInvite = (state: State): InviteInfo | undefined => { return state.setup.invite; }; -export const isLocaleLoaded = (state: State): boolean => { +export const getIsLocaleLoaded = (state: State): boolean => { return state.setup.isLocaleLoaded; }; -export const isTrackingAllowed = (state: State): boolean => { +export const getIsTrackingAllowed = (state: State): boolean => { return state.setup.isTrackingAllowed; }; -export const isStepActive = (state: State, step: number): boolean => { +export const getIsStepActive = (state: State, step: number): boolean => { return getStep(state) === step; }; -export const isStepCompleted = (state: State, step: number): boolean => { +export const getIsStepCompleted = (state: State, step: number): boolean => { return getStep(state) > step; }; -export const isSetupCompleted = (state: State): boolean => { +export const getIsSetupCompleted = (state: State): boolean => { return getStep(state) === COMPLETED_STEP; }; export const getDatabaseEngine = (state: State): string | undefined => { return getDatabase(state)?.engine || state.setup.databaseEngine; }; + +export const getSetupToken = (state: State) => { + return getSetting(state, "setup-token"); +}; + +export const getIsHosted = (state: State): boolean => { + return getSetting(state, "is-hosted?"); +}; + +export const getAvailableLocales = (state: State): LocaleData[] => { + return getSetting(state, "available-locales") ?? DEFAULT_LOCALES; +}; + +export const getIsEmailConfigured = (state: State): boolean => { + return getSetting(state, "email-configured?"); +}; diff --git a/frontend/src/metabase/setup/utils.ts b/frontend/src/metabase/setup/utils.ts index 3bcce768324..8452c335b30 100644 --- a/frontend/src/metabase/setup/utils.ts +++ b/frontend/src/metabase/setup/utils.ts @@ -1,6 +1,10 @@ +import { getIn } from "icepick"; import _ from "underscore"; +import { UtilApi } from "metabase/services"; +import MetabaseSettings from "metabase/lib/settings"; import { LocaleData } from "metabase-types/api"; import { Locale } from "metabase-types/store"; +import { SUBSCRIBE_URL, SUBSCRIBE_TOKEN } from "./constants"; export const getLocales = ( localeData: LocaleData[] = [["en", "English"]], @@ -28,9 +32,18 @@ export const getUserToken = (hash = window.location.hash): string => { return hash.replace(/^#/, ""); }; -const SUBSCRIBE_URL = - "https://metabase.us10.list-manage.com/subscribe/post?u=869fec0e4689e8fd1db91e795&id=b9664113a8"; -const SUBSCRIBE_TOKEN = "b_869fec0e4689e8fd1db91e795_b9664113a8"; +export const validatePassword = async (password: string) => { + const error = MetabaseSettings.passwordComplexityDescription(password); + if (error) { + return error; + } + + try { + await UtilApi.password_check({ password }); + } catch (error) { + return getIn(error, ["data", "errors", "password"]); + } +}; export const subscribeToNewsletter = async (email: string): Promise<void> => { const body = new FormData(); diff --git a/frontend/test/__support__/server-mocks/index.ts b/frontend/test/__support__/server-mocks/index.ts index 210abb96578..013d585cb9d 100644 --- a/frontend/test/__support__/server-mocks/index.ts +++ b/frontend/test/__support__/server-mocks/index.ts @@ -10,6 +10,7 @@ export * from "./dashboard"; export * from "./database"; export * from "./dataset"; export * from "./field"; +export * from "./group"; export * from "./metabot"; export * from "./metric"; export * from "./model-indexes"; diff --git a/frontend/test/__support__/server-mocks/setup.ts b/frontend/test/__support__/server-mocks/setup.ts index 99721a4e0d2..0492a17ca56 100644 --- a/frontend/test/__support__/server-mocks/setup.ts +++ b/frontend/test/__support__/server-mocks/setup.ts @@ -1,6 +1,10 @@ import fetchMock from "fetch-mock"; import { SetupCheckListItem } from "metabase-types/api"; +export function setupErrorSetupEndpoints() { + fetchMock.post("path:/api/setup", 400); +} + export function setupAdminCheckListEndpoint(items: SetupCheckListItem[]) { fetchMock.get("path:/api/setup/admin_checklist", items); } -- GitLab