diff --git a/pkg/app/web/src/components/static-admin-form.stories.tsx b/pkg/app/web/src/components/static-admin-form.stories.tsx index 14e2117b6e..f1735d6c1a 100644 --- a/pkg/app/web/src/components/static-admin-form.stories.tsx +++ b/pkg/app/web/src/components/static-admin-form.stories.tsx @@ -5,7 +5,14 @@ import { StaticAdminForm } from "./static-admin-form"; export default { title: "SETTINGS/StaticAdminForm", component: StaticAdminForm, - decorators: [createDecoratorRedux({})], + decorators: [ + createDecoratorRedux({ + project: { + staticAdminDisabled: false, + username: "pipe-user", + }, + }), + ], }; export const overview: React.FC = () => ; diff --git a/pkg/app/web/src/components/static-admin-form.test.tsx b/pkg/app/web/src/components/static-admin-form.test.tsx new file mode 100644 index 0000000000..02a1205622 --- /dev/null +++ b/pkg/app/web/src/components/static-admin-form.test.tsx @@ -0,0 +1,84 @@ +import userEvent from "@testing-library/user-event"; +import React from "react"; +import { + createStore, + render, + screen, + act, + waitForElementToBeRemoved, + waitFor, +} from "../../test-utils"; +import { server } from "../mocks/server"; +import { updateStaticAdmin } from "../modules/project"; +import { StaticAdminForm } from "./static-admin-form"; + +beforeAll(() => { + server.listen(); +}); + +afterEach(() => { + server.resetHandlers(); +}); + +afterAll(() => { + server.close(); +}); + +it("should shows current username", () => { + render(, { + initialState: { + project: { + username: "pipe-user", + staticAdminDisabled: false, + }, + }, + }); + + expect(screen.getByText("pipe-user")).toBeInTheDocument(); +}); + +it("should dispatch action that update static admin when input fields and click submit button", async () => { + const store = createStore({ + project: { + username: "pipe-user", + staticAdminDisabled: false, + }, + }); + + render(, { + store, + }); + + userEvent.click( + screen.getByRole("button", { name: "edit static admin user" }) + ); + + await waitFor(() => screen.getByText("Edit Static Admin")); + + userEvent.type(screen.getByRole("textbox", { name: /username/i }), "-new"); + userEvent.type(screen.getByLabelText(/password/i), "new-password"); + + act(() => { + userEvent.click(screen.getByRole("button", { name: /save/i })); + }); + + await waitForElementToBeRemoved(() => screen.getByText("Edit Static Admin")); + + expect(store.getActions()).toMatchObject([ + { + type: updateStaticAdmin.pending.type, + meta: { + arg: { + username: "pipe-user-new", + password: "new-password", + }, + }, + }, + { + type: updateStaticAdmin.fulfilled.type, + }, + {}, + {}, + {}, + ]); +}); diff --git a/pkg/app/web/src/components/static-admin-form.tsx b/pkg/app/web/src/components/static-admin-form.tsx index 9ea3eaf165..7183c5110d 100644 --- a/pkg/app/web/src/components/static-admin-form.tsx +++ b/pkg/app/web/src/components/static-admin-form.tsx @@ -28,6 +28,8 @@ import { addToast } from "../modules/toasts"; import { AppDispatch } from "../store"; import { useProjectSettingStyles } from "../styles/project-setting"; import { ProjectSettingLabeledText } from "./project-setting-labeled-text"; +import { useFormik } from "formik"; +import * as Yup from "yup"; const useStyles = makeStyles(() => ({ disabled: { @@ -38,6 +40,81 @@ const useStyles = makeStyles(() => ({ const SECTION_TITLE = "Static Admin"; const DIALOG_TITLE = `Edit ${SECTION_TITLE}`; +const validationSchema = Yup.object().shape({ + username: Yup.string().min(1).required(), + password: Yup.string().min(1).required(), +}); + +const StaticAdminDialog: FC<{ + open: boolean; + currentUsername: string; + onClose: () => void; + onSubmit: (values: { username: string; password: string }) => void; +}> = ({ open, currentUsername, onClose, onSubmit }) => { + const formik = useFormik({ + initialValues: { + username: currentUsername, + password: "", + }, + validationSchema, + onSubmit, + }); + + return ( + { + formik.resetForm(); + }} + onClose={onClose} + > +
+ {DIALOG_TITLE} + + + + + + + + +
+
+ ); +}; + export const StaticAdminForm: FC = memo(function StaticAdminForm() { const classes = useStyles(); const projectSettingClasses = useProjectSettingStyles(); @@ -50,22 +127,12 @@ export const StaticAdminForm: FC = memo(function StaticAdminForm() { state.project.username, ]); const [isEdit, setIsEdit] = useState(false); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - - const handleClose = (): void => { - setIsEdit(false); - }; - const handleToggleAvailability = (): void => { - dispatch(toggleAvailability()).then(() => { - dispatch(fetchProject()); - }); - }; - - const handleSave = (e: React.FormEvent): void => { - e.preventDefault(); - dispatch(updateStaticAdmin({ username, password })).then((result) => { + const handleSubmit = (values: { + username: string; + password: string; + }): void => { + dispatch(updateStaticAdmin(values)).then((result) => { if (updateStaticAdmin.fulfilled.match(result)) { dispatch(fetchProject()); dispatch( @@ -79,7 +146,15 @@ export const StaticAdminForm: FC = memo(function StaticAdminForm() { setIsEdit(false); }; - const isInvalidValues = username === "" || password === ""; + const handleClose = (): void => { + setIsEdit(false); + }; + + const handleToggleAvailability = (): void => { + dispatch(toggleAvailability()).then(() => { + dispatch(fetchProject()); + }); + }; return ( <> @@ -123,6 +198,7 @@ export const StaticAdminForm: FC = memo(function StaticAdminForm() {
setIsEdit(true)} disabled={isEnabled === false} > @@ -137,48 +213,12 @@ export const StaticAdminForm: FC = memo(function StaticAdminForm() {
)} - - { - setUsername(currentUsername || ""); - setPassword(""); - }} + currentUsername={currentUsername || ""} onClose={handleClose} - > -
- {DIALOG_TITLE} - - setUsername(e.currentTarget.value)} - /> - setPassword(e.currentTarget.value)} - /> - - - - - -
-
+ onSubmit={handleSubmit} + /> ); }); diff --git a/pkg/app/web/src/mocks/handlers.ts b/pkg/app/web/src/mocks/handlers.ts index ef2ddcc5ca..1b2b1697bc 100644 --- a/pkg/app/web/src/mocks/handlers.ts +++ b/pkg/app/web/src/mocks/handlers.ts @@ -2,9 +2,11 @@ import { meHandlers } from "./services/me"; import { commandHandlers } from "./services/command"; import { applicationHandlers } from "./services/application"; import { deploymentHandlers } from "./services/deployment"; +import { projectHandlers } from "./services/project"; export const handlers = [ ...meHandlers, ...commandHandlers, ...applicationHandlers, ...deploymentHandlers, + ...projectHandlers, ]; diff --git a/pkg/app/web/src/mocks/services/project.ts b/pkg/app/web/src/mocks/services/project.ts new file mode 100644 index 0000000000..8210fcff19 --- /dev/null +++ b/pkg/app/web/src/mocks/services/project.ts @@ -0,0 +1,31 @@ +import { rest } from "msw"; +import { serialize } from "../serializer"; +import { createMask } from "../utils"; +import { + UpdateProjectStaticAdminResponse, + GetProjectResponse, +} from "pipe/pkg/app/web/api_client/service_pb"; +import { Project } from "pipe/pkg/app/web/model/project_pb"; + +export const projectHandlers = [ + rest.post(createMask("/UpdateProjectStaticAdmin"), (req, res, ctx) => { + const r = new UpdateProjectStaticAdminResponse(); + const data = serialize(r.serializeBinary()); + return res( + ctx.status(200), + ctx.set("Content-Type", "application/grpc-web+proto"), + ctx.body(data) + ); + }), + rest.post(createMask("/GetProject"), (req, res, ctx) => { + const r = new GetProjectResponse(); + const p = new Project(); + r.setProject(p); + const data = serialize(r.serializeBinary()); + return res( + ctx.status(200), + ctx.set("Content-Type", "application/grpc-web+proto"), + ctx.body(data) + ); + }), +];