diff --git a/web/src/components/users/FirstUser.jsx b/web/src/components/users/FirstUser.jsx index 8f661d279e..4d16709c20 100644 --- a/web/src/components/users/FirstUser.jsx +++ b/web/src/components/users/FirstUser.jsx @@ -19,34 +19,30 @@ * find current contact information at www.suse.com. */ -import React, { useState, useEffect, useRef } from "react"; -import { - Alert, - Checkbox, - Form, FormGroup, TextInput, - Menu, MenuContent, MenuList, MenuItem, - Skeleton, - Stack -} from "@patternfly/react-core"; +import React, { useState, useEffect } from "react"; +import { Skeleton, Split, Stack } from "@patternfly/react-core"; import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; import { useNavigate } from "react-router-dom"; -import { RowActions, PasswordAndConfirmationInput, Popup, ButtonLink } from '~/components/core'; +import { RowActions, ButtonLink } from '~/components/core'; import { _ } from "~/i18n"; import { useCancellablePromise } from "~/utils"; import { useInstallerClient } from "~/context/installer"; -import { suggestUsernames } from '~/components/users/utils'; const UserNotDefined = ({ actionCb }) => { return ( - -
{_("No user defined yet.")}
-
- - {_("Please, be aware that a user must be defined before installing the system to be able to log into it.")} - -
- {_("Define a user now")} -
+ <> + +
{_("No user defined yet.")}
+
+ + {_("Please, be aware that a user must be defined before installing the system to be able to log into it.")} + +
+ + {_("Define a user now")} + +
+ ); }; @@ -73,38 +69,6 @@ const UserData = ({ user, actions }) => { ); }; -const UsernameSuggestions = ({ isOpen = false, entries, onSelect, setInsideDropDown, focusedIndex = -1 }) => { - if (!isOpen) return; - - return ( - setInsideDropDown(true)} - onMouseLeave={() => setInsideDropDown(false)} - > - - - {entries.map((suggestion, index) => ( - onSelect(suggestion)} - > - { /* TRANSLATORS: dropdown username suggestions */} - {_("Use suggested username")} {suggestion} - - ))} - - - - ); -}; - -const CREATE_MODE = 'create'; -const EDIT_MODE = 'edit'; - const initialUser = { userName: "", fullName: "", @@ -116,23 +80,11 @@ export default function FirstUser() { const client = useInstallerClient(); const { cancellablePromise } = useCancellablePromise(); const [user, setUser] = useState({}); - const [errors, setErrors] = useState([]); - const [formValues, setFormValues] = useState(initialUser); const [isLoading, setIsLoading] = useState(true); - const [isEditing, setIsEditing] = useState(false); - const [isFormOpen, setIsFormOpen] = useState(false); - const [isValidPassword, setIsValidPassword] = useState(true); - const [isSettingPassword, setIsSettingPassword] = useState(false); - const [showSuggestions, setShowSuggestions] = useState(false); - const [insideDropDown, setInsideDropDown] = useState(false); - const [focusedIndex, setFocusedIndex] = useState(-1); - const [suggestions, setSuggestions] = useState([]); - const usernameInputRef = useRef(); useEffect(() => { cancellablePromise(client.users.getUser()).then(userValues => { setUser(userValues); - setFormValues({ ...initialUser, ...userValues }); setIsLoading(false); }); }, [client.users, cancellablePromise]); @@ -145,41 +97,6 @@ export default function FirstUser() { }); }, [client.users]); - const openForm = (e, mode = CREATE_MODE) => { - setIsEditing(mode === EDIT_MODE); - // Password will be always set when creating the user. In the edit mode it - // depends on the user choice - setIsSettingPassword(mode === CREATE_MODE); - // To avoid confusion, do not expose the current password - setFormValues({ ...initialUser, ...user, password: "" }); - setIsFormOpen(true); - }; - - const closeForm = () => { - setErrors([]); - setIsEditing(false); - setIsFormOpen(false); - }; - - const accept = async (formName, e) => { - e.preventDefault(); - setErrors([]); - setIsLoading(true); - - // Preserve current password value if the user was not editing it. - const newUser = { ...formValues }; - if (!isSettingPassword) newUser.password = user.password; - - const { result, issues = [] } = await client.users.setUser(newUser); - setErrors(issues); - setIsLoading(false); - if (result) { - setUser(newUser); - - closeForm(); - } - }; - const remove = async () => { setIsLoading(true); @@ -187,18 +104,11 @@ export default function FirstUser() { if (result) { setUser(initialUser); - setFormValues(initialUser); setIsLoading(false); } }; - const handleInputChange = ({ target }, value) => { - const { name } = target; - setFormValues({ ...formValues, [name]: value }); - }; - const isUserDefined = user?.userName && user?.userName !== ""; - const showErrors = () => ((errors || []).length > 0); const navigate = useNavigate(); const actions = [ @@ -213,145 +123,11 @@ export default function FirstUser() { } ]; - const toggleShowPasswordField = () => setIsSettingPassword(!isSettingPassword); - const usingValidPassword = formValues.password && formValues.password !== "" && isValidPassword; - const submitDisable = formValues.userName === "" || (isSettingPassword && !usingValidPassword); - - const displaySuggestions = !formValues.userName && formValues.fullName && showSuggestions; - useEffect(() => { - if (displaySuggestions) { - setFocusedIndex(-1); - setSuggestions(suggestUsernames(formValues.fullName)); - } - }, [displaySuggestions, formValues.fullName]); - - const onSuggestionSelected = (suggestion) => { - setInsideDropDown(false); - setFormValues({ ...formValues, userName: suggestion }); - usernameInputRef.current?.focus(); - }; - - const handleKeyDown = (event) => { - switch (event.key) { - case 'ArrowDown': - event.preventDefault(); // Prevent page scrolling - if (suggestions.length > 0) setShowSuggestions(true); - setFocusedIndex((prevIndex) => (prevIndex + 1) % suggestions.length); - break; - case 'ArrowUp': - event.preventDefault(); // Prevent page scrolling - if (suggestions.length > 0) setShowSuggestions(true); - setFocusedIndex((prevIndex) => (prevIndex - (prevIndex === -1 ? 0 : 1) + suggestions.length) % suggestions.length); - break; - case 'Enter': - if (focusedIndex >= 0) { - onSuggestionSelected(suggestions[focusedIndex]); - } - break; - case 'Escape': - case 'Tab': - setShowSuggestions(false); - break; - default: - break; - } - }; - - if (isLoading) return ; - - return ( - <> - {isUserDefined ? : } - { /* TODO: Extract this form to a component, if possible */} - {isFormOpen && - -
accept("createUser", e)}> - {showErrors() && - - {errors.map((e, i) =>

{e}

)} -
} - - - - - - setShowSuggestions(true)} - onBlur={() => !insideDropDown && setShowSuggestions(false)} - > - - 0} - entries={suggestions} - onSelect={onSuggestionSelected} - setInsideDropDown={setInsideDropDown} - focusedIndex={focusedIndex} - /> - - - {isEditing && - } - - {isSettingPassword && - setIsValidPassword(isValid)} - />} - - - - - - - - -
} - - ); + if (isLoading) { + return ; + } else if (isUserDefined) { + return ; + } else { + return ; + } } diff --git a/web/src/components/users/FirstUser.test.jsx b/web/src/components/users/FirstUser.test.jsx deleted file mode 100644 index 4dae092eb6..0000000000 --- a/web/src/components/users/FirstUser.test.jsx +++ /dev/null @@ -1,389 +0,0 @@ -/* - * Copyright (c) [2022] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; - -import { act, screen, waitFor, within } from "@testing-library/react"; -import { installerRender, createCallbackMock } from "~/test-utils"; -import { createClient } from "~/client"; -import { FirstUser } from "~/components/users"; - -jest.mock("~/client"); - -let user; -const emptyUser = { - fullName: "", - userName: "", - autologin: false -}; - -let setUserResult = { result: true, issues: [] }; - -let setUserFn = jest.fn().mockResolvedValue(setUserResult); -const removeUserFn = jest.fn(); -let onUsersChangeFn = jest.fn(); - -const openUserForm = async () => { - const { user } = installerRender(); - await screen.findByText("No user defined yet."); - const button = await screen.findByText("Define a user now"); - await user.click(button); - const dialog = await screen.findByLabelText("Username"); - - return { user, dialog }; -}; - -beforeEach(() => { - user = emptyUser; - createClient.mockImplementation(() => { - return { - users: { - setUser: setUserFn, - getUser: jest.fn().mockResolvedValue(user), - removeUser: removeUserFn, - onUsersChange: onUsersChangeFn - } - }; - }); -}); - -it.skip("allows defining a new user", async () => { - const { user } = installerRender(); - await screen.findByText("No user defined yet."); - const button = await screen.findByText("Define a user now"); - await user.click(button); - - const dialog = await screen.findByRole("form"); - - const fullNameInput = within(dialog).getByLabelText("Full name"); - await user.type(fullNameInput, "Jane Doe"); - - const usernameInput = within(dialog).getByLabelText(/Username/); - await user.type(usernameInput, "jane"); - - const passwordInput = within(dialog).getByLabelText("Password"); - await user.type(passwordInput, "12345"); - - const passwordConfirmationInput = within(dialog).getByLabelText("Password confirmation"); - await user.type(passwordConfirmationInput, "12345"); - - const confirmButton = screen.getByRole("button", { name: /Confirm/i }); - expect(confirmButton).toBeEnabled(); - await user.click(confirmButton); - - expect(setUserFn).toHaveBeenCalledWith({ - fullName: "Jane Doe", - userName: "jane", - password: "12345", - autologin: false - }); - - await waitFor(() => { - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); -}); - -it.skip("doest not allow to confirm the settings if the user name and the password are not provided", async () => { - const { user } = installerRender(); - const button = await screen.findByText("Define a user now"); - await user.click(button); - - const dialog = await screen.findByRole("dialog"); - - const usernameInput = within(dialog).getByLabelText(/Username/); - await user.type(usernameInput, "jane"); - const confirmButton = within(dialog).getByRole("button", { name: /Confirm/i }); - expect(confirmButton).toBeDisabled(); -}); - -it.skip("does not change anything if the user cancels", async () => { - const { user } = installerRender(); - const button = await screen.findByRole("button", { name: "Define a user now" }); - await user.click(button); - - const dialog = await screen.findByRole("dialog"); - - const cancelButton = within(dialog).getByRole("button", { name: /Cancel/i }); - await user.click(cancelButton); - - expect(setUserFn).not.toHaveBeenCalled(); - await waitFor(() => { - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); -}); - -describe.skip("when there is some issue with the user config provided", () => { - beforeEach(() => { - setUserResult = { result: false, issues: ["There is an error"] }; - setUserFn = jest.fn().mockResolvedValue(setUserResult); - }); - - it("shows the issues found", async () => { - const { user } = installerRender(); - const button = await screen.findByRole("button", { name: "Define a user now" }); - await user.click(button); - - const dialog = await screen.findByRole("dialog"); - - const usernameInput = within(dialog).getByLabelText("Username"); - await user.type(usernameInput, "root"); - - const passwordInput = within(dialog).getByLabelText("Password"); - await user.type(passwordInput, "12345"); - - const passwordConfirmationInput = within(dialog).getByLabelText("Password confirmation"); - await user.type(passwordConfirmationInput, "12345"); - - const confirmButton = within(dialog).getByRole("button", { name: /Confirm/i }); - expect(confirmButton).toBeEnabled(); - await user.click(confirmButton); - - expect(setUserFn).toHaveBeenCalledWith({ - fullName: "", - userName: "root", - password: "12345", - autologin: false - }); - - await waitFor(() => { - expect(screen.queryByText(/Something went wrong/i)).toBeInTheDocument(); - expect(screen.queryByText(/There is an error/i)).toBeInTheDocument(); - expect(screen.queryByText("No user defined yet.")).toBeInTheDocument(); - }); - }); -}); - -describe.skip("when the user is already defined", () => { - beforeEach(() => { - user = { - fullName: "John Doe", - userName: "jdoe", - password: "sup3rSecret", - autologin: false - }; - }); - - it("renders the name and username", async () => { - installerRender(); - await screen.findByText("John Doe"); - await screen.findByText("jdoe"); - }); - - it("allows editing the user without changing the password", async () => { - const { user } = installerRender(); - - await screen.findByText("John Doe"); - - const userActionsToggler = screen.getByRole("button", { name: "Actions" }); - await user.click(userActionsToggler); - const editAction = screen.getByRole("menuitem", { name: "Edit" }); - await user.click(editAction); - const dialog = await screen.findByRole("dialog"); - - const fullNameInput = within(dialog).getByLabelText("Full name"); - await user.clear(fullNameInput); - await user.type(fullNameInput, "Jane"); - - const usernameInput = within(dialog).getByLabelText(/Username/); - await user.clear(usernameInput); - await user.type(usernameInput, "jane"); - - const autologinCheckbox = within(dialog).getByLabelText(/Auto-login/); - await user.click(autologinCheckbox); - - const confirmButton = screen.getByRole("button", { name: /Confirm/i }); - expect(confirmButton).toBeEnabled(); - await user.click(confirmButton); - - expect(setUserFn).toHaveBeenCalledWith({ - fullName: "Jane", - userName: "jane", - password: "sup3rSecret", - autologin: true - }); - }); - - it("allows changing the password", async () => { - const { user } = installerRender(); - - await screen.findByText("John Doe"); - - const userActionsToggler = screen.getByRole("button", { name: "Actions" }); - await user.click(userActionsToggler); - const editAction = screen.getByRole("menuitem", { name: "Edit" }); - await user.click(editAction); - const dialog = await screen.findByRole("dialog"); - - const confirmButton = screen.getByRole("button", { name: /Confirm/i }); - const changePasswordCheckbox = within(dialog).getByLabelText("Edit password too"); - await user.click(changePasswordCheckbox); - - expect(confirmButton).toBeDisabled(); - - const passwordInput = within(dialog).getByLabelText("Password"); - await user.type(passwordInput, "n0tSecret"); - const passwordConfirmationInput = within(dialog).getByLabelText("Password confirmation"); - await user.type(passwordConfirmationInput, "n0tSecret"); - - expect(confirmButton).toBeEnabled(); - - await user.click(confirmButton); - - expect(setUserFn).toHaveBeenCalledWith({ - fullName: "John Doe", - userName: "jdoe", - password: "n0tSecret", - autologin: false - }); - }); - - it("allows removing the user", async () => { - const { user } = installerRender(); - const table = await screen.findByRole("grid"); - const row = within(table).getByText("John Doe") - .closest("tr"); - const actionsToggler = within(row).getByRole("button", { name: "Actions" }); - await user.click(actionsToggler); - const discardAction = screen.getByRole("menuitem", { name: "Discard" }); - await user.click(discardAction); - expect(removeUserFn).toHaveBeenCalled(); - }); -}); - -describe.skip("when the user has been modified", () => { - it("updates the UI for rendering its main info", async () => { - const [mockFunction, callbacks] = createCallbackMock(); - onUsersChangeFn = mockFunction; - installerRender(); - await screen.findByText("No user defined yet."); - - const [cb] = callbacks; - act(() => { - cb({ firstUser: { userName: "ytm", fullName: "YaST Team Member", autologin: false } }); - }); - - const noUserInfo = await screen.queryByText("No user defined yet."); - expect(noUserInfo).toBeNull(); - screen.getByText("YaST Team Member"); - screen.getByText("ytm"); - }); -}); - -describe.skip("username suggestions", () => { - it("shows suggestions when full name is given and username gets focus", async () => { - const { user, dialog } = await openUserForm(); - - const fullNameInput = within(dialog).getByLabelText("Full name"); - await user.type(fullNameInput, "Jane Doe"); - - await user.tab(); - - const menuItems = screen.getAllByText("Use suggested username"); - expect(menuItems.length).toBe(4); - }); - - it("hides suggestions when username loses focus", async () => { - const { user, dialog } = await openUserForm(); - - const fullNameInput = within(dialog).getByLabelText("Full name"); - await user.type(fullNameInput, "Jane Doe"); - - await user.tab(); - - let menuItems = screen.getAllByText("Use suggested username"); - expect(menuItems.length).toBe(4); - - await user.tab(); - - menuItems = screen.queryAllByText("Use suggested username"); - expect(menuItems.length).toBe(0); - }); - - it("does not show suggestions when full name is not given", async () => { - const { user, dialog } = await openUserForm(); - - const fullNameInput = within(dialog).getByLabelText("Full name"); - fullNameInput.focus(); - - await user.tab(); - - const menuItems = screen.queryAllByText("Use suggested username"); - expect(menuItems.length).toBe(0); - }); - - it("hides suggestions if user types something", async () => { - const { user, dialog } = await openUserForm(); - - const fullNameInput = within(dialog).getByLabelText("Full name"); - await user.type(fullNameInput, "Jane Doe"); - - await user.tab(); - - // confirming that we have suggestions - let menuItems = screen.queryAllByText("Use suggested username"); - expect(menuItems.length).toBe(4); - - const usernameInput = within(dialog).getByLabelText("Username"); - // the user now types something - await user.type(usernameInput, "John Smith"); - - // checking if suggestions are gone - menuItems = screen.queryAllByText("Use suggested username"); - expect(menuItems.length).toBe(0); - }); - - it("fills username input with chosen suggestion", async () => { - const { user, dialog } = await openUserForm(); - - const fullNameInput = within(dialog).getByLabelText("Full name"); - await user.type(fullNameInput, "Will Power"); - - await user.tab(); - - const menuItem = screen.getByText('willpower'); - const usernameInput = within(dialog).getByLabelText("Username"); - - await user.click(menuItem); - - expect(usernameInput).toHaveFocus(); - expect(usernameInput.value).toBe("willpower"); - }); - - it("fills username input with chosen suggestion using keyboard for selection", async () => { - const { user, dialog } = await openUserForm(); - - const fullNameInput = within(dialog).getByLabelText("Full name"); - await user.type(fullNameInput, "Jane Doe"); - - await user.tab(); - - const menuItems = screen.getAllByRole("menuitem"); - const menuItemTwo = menuItems[1].textContent.replace("Use suggested username ", ""); - - await user.keyboard("{ArrowDown}"); - await user.keyboard("{ArrowDown}"); - await user.keyboard("{Enter}"); - - const usernameInput = within(dialog).getByLabelText("Username"); - expect(usernameInput).toHaveFocus(); - expect(usernameInput.value).toBe(menuItemTwo); - }); -}); diff --git a/web/src/components/users/FirstUserForm.jsx b/web/src/components/users/FirstUserForm.jsx index c148737805..7640be4ac6 100644 --- a/web/src/components/users/FirstUserForm.jsx +++ b/web/src/components/users/FirstUserForm.jsx @@ -254,7 +254,7 @@ export default function FirstUserForm() { {state.isEditing && setChangePassword(!changePassword)} />} diff --git a/web/src/components/users/UsersPage.jsx b/web/src/components/users/UsersPage.jsx index 1fffd6f0b3..56bef68a8e 100644 --- a/web/src/components/users/UsersPage.jsx +++ b/web/src/components/users/UsersPage.jsx @@ -24,7 +24,7 @@ import React from "react"; import { _ } from "~/i18n"; import { CardField, IssuesHint, Page } from "~/components/core"; import { FirstUser, RootAuthMethods } from "~/components/users"; -import { CardBody, Grid, GridItem, Stack } from "@patternfly/react-core"; +import { CardBody, Grid, GridItem } from "@patternfly/react-core"; import { useIssues } from "~/context/issues"; export default function UsersPage() { @@ -44,18 +44,14 @@ export default function UsersPage() { - - - + - - - +