diff --git a/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts b/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts index b62e8df52a1d83955a7bdfaa763900011b38e6c6..f0e348459998d74e4f83c144c782f7e1b5987661 100644 --- a/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts +++ b/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts @@ -8,9 +8,12 @@ import { expectNoBadSnowplowEvents, isEE, main, + mockSessionProperty, + onlyOnEE, popover, resetSnowplow, restore, + setTokenFeatures, } from "e2e/support/helpers"; import { SUBSCRIBE_URL } from "metabase/setup/constants"; @@ -233,6 +236,29 @@ describe("scenarios > setup", () => { }); }); + it("should pre-fill user info for hosted instances (infra-frontend#1109)", () => { + onlyOnEE(); + setTokenFeatures("none"); + mockSessionProperty("is-hosted?", true); + + cy.visit( + "/setup?first_name=John&last_name=Doe&email=john@doe.test&site_name=Doe%20Unlimited", + ); + + skipWelcomePage(); + selectPreferredLanguageAndContinue(); + + cy.findByTestId("setup-forms").within(() => { + cy.findByDisplayValue("John").should("exist"); + cy.findByDisplayValue("Doe").should("exist"); + cy.findByDisplayValue("john@doe.test").should("exist"); + cy.findByDisplayValue("Doe Unlimited").should("exist"); + cy.findByLabelText("Create a password") + .should("be.focused") + .and("be.empty"); + }); + }); + it("should allow you to connect a db during setup", () => { const dbName = "SQLite db"; diff --git a/frontend/src/metabase/setup/components/UserForm/UserForm.tsx b/frontend/src/metabase/setup/components/UserForm/UserForm.tsx index 246141a6011326bf228c330cea48259c6c44dbad..85f7db74786f65bfcc37977619f1cead276bdc09 100644 --- a/frontend/src/metabase/setup/components/UserForm/UserForm.tsx +++ b/frontend/src/metabase/setup/components/UserForm/UserForm.tsx @@ -32,12 +32,14 @@ const USER_SCHEMA = Yup.object({ interface UserFormProps { user?: UserInfo; + isHosted: boolean; onValidatePassword: (password: string) => Promise<string | undefined>; onSubmit: (user: UserInfo) => Promise<void>; } export const UserForm = ({ user, + isHosted, onValidatePassword, onSubmit, }: UserFormProps) => { @@ -66,7 +68,7 @@ export const UserForm = ({ title={t`First name`} placeholder={t`Johnny`} nullable - autoFocus + autoFocus={!isHosted} /> <FormInput name="last_name" @@ -91,6 +93,10 @@ export const UserForm = ({ type="password" title={t`Create a password`} placeholder={t`Shhh...`} + // Hosted instances always pass user information in the URLSearchParams + // during the initial setup. Password is the first empty field + // so it makes sense to focus on it. + autoFocus={isHosted && initialValues.site_name !== ""} /> <FormInput name="password_confirm" diff --git a/frontend/src/metabase/setup/components/UserStep/UserStep.tsx b/frontend/src/metabase/setup/components/UserStep/UserStep.tsx index 8cf836f2554646007abd4db5cca14c4f9734ac7b..b2d533e70ff1354c67c4358717056d81d1581ef1 100644 --- a/frontend/src/metabase/setup/components/UserStep/UserStep.tsx +++ b/frontend/src/metabase/setup/components/UserStep/UserStep.tsx @@ -16,6 +16,7 @@ import { StepDescription } from "./UserStep.styled"; export const UserStep = ({ stepLabel }: NumberedStepProps): JSX.Element => { const { isStepActive, isStepCompleted } = useStep("user_info"); + const user = useSelector(getUser); const isHosted = useSelector(getIsHosted); @@ -45,6 +46,7 @@ export const UserStep = ({ stepLabel }: NumberedStepProps): JSX.Element => { )} <UserForm user={user} + isHosted={isHosted} onValidatePassword={validatePassword} onSubmit={handleSubmit} /> 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 d41ee9a6300b52e259c9c07af4fae0a18fb60774..6cff805c133dd3ebb625901115bce096d2ef6e63 100644 --- a/frontend/src/metabase/setup/components/UserStep/UserStep.unit.spec.tsx +++ b/frontend/src/metabase/setup/components/UserStep/UserStep.unit.spec.tsx @@ -1,3 +1,4 @@ +import { mockSettings } from "__support__/settings"; import { renderWithProviders, screen } from "__support__/ui"; import type { SetupStep } from "metabase/setup/types"; import type { UserInfo } from "metabase-types/store"; @@ -12,10 +13,16 @@ import { UserStep } from "./UserStep"; interface SetupOpts { step?: SetupStep; user?: UserInfo; + isHosted?: boolean; } -const setup = ({ step = "user_info", user }: SetupOpts = {}) => { +const setup = ({ + step = "user_info", + user, + isHosted = false, +}: SetupOpts = {}) => { const state = createMockState({ + settings: mockSettings({ "is-hosted?": isHosted }), setup: createMockSetupState({ step, user, @@ -32,6 +39,30 @@ describe("UserStep", () => { expect(screen.getByText("What should we call you?")).toBeInTheDocument(); }); + it("should autofocus the first name input field", () => { + setup({ step: "user_info" }); + + expect(screen.getByLabelText("First name")).toHaveFocus(); + }); + + it("should autofocus the password input field for hosted instances", () => { + const user = createMockUserInfo(); + setup({ step: "user_info", isHosted: true, user }); + + expect(screen.getByLabelText("Create a password")).toHaveFocus(); + }); + + it("should pre-fill the user information if provided", () => { + const user = createMockUserInfo(); + setup({ step: "user_info", user }); + + Object.values(user) + .filter(v => v.length > 0) + .forEach(v => { + expect(screen.getByDisplayValue(v)).toBeInTheDocument(); + }); + }); + it("should render in completed state", () => { setup({ step: "db_connection", diff --git a/frontend/src/metabase/setup/reducers.ts b/frontend/src/metabase/setup/reducers.ts index dbfdec7c91ce38234a9a71cffc1cfa97fe8d1f4a..469d50ec0e75ed710db35d360b55b0a7fc9836c8 100644 --- a/frontend/src/metabase/setup/reducers.ts +++ b/frontend/src/metabase/setup/reducers.ts @@ -17,15 +17,33 @@ import { updateTracking, } from "./actions"; +const getUserFromQueryParams = () => { + const params = new URLSearchParams(window.location.search); + const getParam = (key: string, defaultValue = "") => + params.get(key) || defaultValue; + + return { + first_name: getParam("first_name") || null, + last_name: getParam("last_name") || null, + email: getParam("email"), + site_name: getParam("site_name"), + password: "", + password_confirm: "", + }; +}; + const initialState: SetupState = { step: "welcome", isLocaleLoaded: false, isTrackingAllowed: true, + user: getUserFromQueryParams(), }; export const reducer = createReducer(initialState, builder => { builder.addCase(loadUserDefaults.fulfilled, (state, { payload: user }) => { - state.user = user; + if (user) { + state.user = user; + } }); builder.addCase( loadLocaleDefaults.fulfilled,