From 635ad3a4e03bdfd5a465e68f703c4464687115eb Mon Sep 17 00:00:00 2001 From: Nemanja Glumac <31325167+nemanjaglumac@users.noreply.github.com> Date: Wed, 25 Sep 2024 19:03:09 +0200 Subject: [PATCH] Pre fill non-sensitive user information during setup for hosted instances (#48019) * Pre-fill cloud user info from search params * Autofocus password field for hosted instances * Add tests * Parse setup user info and store it immediatelly * Autofocus password input field for hosted instances * Add e2e reproduction * Fix the user info * Fix unit test --- .../onboarding/setup/setup.cy.spec.ts | 26 +++++++++++++++ .../setup/components/UserForm/UserForm.tsx | 8 ++++- .../setup/components/UserStep/UserStep.tsx | 2 ++ .../UserStep/UserStep.unit.spec.tsx | 33 ++++++++++++++++++- frontend/src/metabase/setup/reducers.ts | 20 ++++++++++- 5 files changed, 86 insertions(+), 3 deletions(-) diff --git a/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts b/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts index b62e8df52a1..f0e34845999 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 246141a6011..85f7db74786 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 8cf836f2554..b2d533e70ff 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 d41ee9a6300..6cff805c133 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 dbfdec7c91c..469d50ec0e7 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, -- GitLab