Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion pkg/app/web/src/components/static-admin-form.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => <StaticAdminForm />;
84 changes: 84 additions & 0 deletions pkg/app/web/src/components/static-admin-form.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<StaticAdminForm />, {
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(<StaticAdminForm />, {
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,
},
{},
{},
{},
]);
});
152 changes: 96 additions & 56 deletions pkg/app/web/src/components/static-admin-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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 (
<Dialog
open={open}
onExited={() => {
formik.resetForm();
}}
onClose={onClose}
>
<form onSubmit={formik.handleSubmit}>
<DialogTitle>{DIALOG_TITLE}</DialogTitle>
<DialogContent>
<TextField
id="username"
name="username"
value={formik.values.username}
variant="outlined"
margin="dense"
label="Username"
fullWidth
required
autoFocus
onChange={formik.handleChange}
/>
<TextField
id="password"
name="password"
value={formik.values.password}
autoComplete="new-password"
variant="outlined"
margin="dense"
label="Password"
type="password"
fullWidth
required
onChange={formik.handleChange}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{UI_TEXT_CANCEL}</Button>
<Button
type="submit"
color="primary"
disabled={
formik.isValid === false ||
formik.values.username === currentUsername
}
>
{UI_TEXT_SAVE}
</Button>
</DialogActions>
</form>
</Dialog>
);
};

export const StaticAdminForm: FC = memo(function StaticAdminForm() {
const classes = useStyles();
const projectSettingClasses = useProjectSettingStyles();
Expand All @@ -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<HTMLFormElement>): 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(
Expand All @@ -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 (
<>
Expand Down Expand Up @@ -123,6 +198,7 @@ export const StaticAdminForm: FC = memo(function StaticAdminForm() {
</div>
<div>
<IconButton
aria-label="edit static admin user"
onClick={() => setIsEdit(true)}
disabled={isEnabled === false}
>
Expand All @@ -137,48 +213,12 @@ export const StaticAdminForm: FC = memo(function StaticAdminForm() {
</div>
)}
</div>

<Dialog
<StaticAdminDialog
open={isEdit}
onEnter={() => {
setUsername(currentUsername || "");
setPassword("");
}}
currentUsername={currentUsername || ""}
onClose={handleClose}
>
<form onSubmit={handleSave}>
<DialogTitle>{DIALOG_TITLE}</DialogTitle>
<DialogContent>
<TextField
value={username}
variant="outlined"
margin="dense"
label="Username"
fullWidth
required
autoFocus
onChange={(e) => setUsername(e.currentTarget.value)}
/>
<TextField
value={password}
autoComplete="new-password"
variant="outlined"
margin="dense"
label="Password"
type="password"
fullWidth
required
onChange={(e) => setPassword(e.currentTarget.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>{UI_TEXT_CANCEL}</Button>
<Button type="submit" color="primary" disabled={isInvalidValues}>
{UI_TEXT_SAVE}
</Button>
</DialogActions>
</form>
</Dialog>
onSubmit={handleSubmit}
/>
</>
);
});
2 changes: 2 additions & 0 deletions pkg/app/web/src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
];
31 changes: 31 additions & 0 deletions pkg/app/web/src/mocks/services/project.ts
Original file line number Diff line number Diff line change
@@ -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)
);
}),
];