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: 9 additions & 0 deletions pkg/app/web/src/components/login-form.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from "react";
import { LoginForm } from "./login-form";

export default {
title: "LoginForm",
component: LoginForm,
};

export const overview: React.FC = () => <LoginForm />;
84 changes: 84 additions & 0 deletions pkg/app/web/src/components/login-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { FC, memo } from "react";
import { makeStyles, TextField, Button, Typography } from "@material-ui/core";
import { STATIC_LOGIN_ENDPOINT } from "../constants";
import { useProjectName, clearProjectName } from "../modules/login";
import { useDispatch } from "react-redux";

const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
alignItems: "center",
flexDirection: "column",
flex: 1,
},
form: {
display: "flex",
flexDirection: "column",
textAlign: "center",
marginTop: theme.spacing(4),
width: 320,
},
fields: {
display: "flex",
flexDirection: "column",
marginTop: theme.spacing(4),
},
buttons: {
display: "flex",
justifyContent: "flex-end",
marginTop: theme.spacing(3),
},
}));

export const LoginForm: FC = memo(function LoginForm() {
const classes = useStyles();
const dispatch = useDispatch();
const projectName = useProjectName();

const handleReset = (): void => {
dispatch(clearProjectName());
};

return (
<div className={classes.root}>
<Typography variant="h4">Sign in to {projectName}</Typography>
<form
method="POST"
action={STATIC_LOGIN_ENDPOINT}
className={classes.form}
>
<input
type="hidden"
id="project"
name="project"
value={projectName || undefined}
/>
<TextField
id="username"
name="username"
label="Username"
variant="outlined"
margin="dense"
required
/>
<TextField
id="password"
name="password"
label="Password"
type="password"
variant="outlined"
margin="dense"
required
/>
<div className={classes.buttons}>
<Button type="reset" color="primary" onClick={handleReset}>
back
</Button>
<Button type="submit" color="primary" variant="contained">
login
</Button>
</div>
</form>
</div>
);
});
2 changes: 2 additions & 0 deletions pkg/app/web/src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { commandsSlice } from "./commands";
import { applicationFilterOptionsSlice } from "./application-filter-options";
import { meSlice } from "./me";
import { deploymentFilterOptionsSlice } from "./deployment-filter-options";
import { loginSlice } from "./login";

export const reducers = combineReducers({
deployments: deploymentsSlice.reducer,
Expand All @@ -26,6 +27,7 @@ export const reducers = combineReducers({
commands: commandsSlice.reducer,
toasts: toastsSlice.reducer,
me: meSlice.reducer,
login: loginSlice.reducer,
});

export type AppState = ReturnType<typeof reducers>;
Expand Down
15 changes: 15 additions & 0 deletions pkg/app/web/src/modules/login.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { loginSlice } from "./login";

describe("loginSlice reducer", () => {
it("should handle initial state", () => {
expect(
loginSlice.reducer(undefined, {
type: "TEST_ACTION",
})
).toMatchInlineSnapshot(`
Object {
"projectName": null,
}
`);
});
});
32 changes: 32 additions & 0 deletions pkg/app/web/src/modules/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { AppState } from ".";
import { useSelector } from "react-redux";

export interface LoginState {
projectName: string | null;
}

const initialState: LoginState = {
projectName: null,
};

export const loginSlice = createSlice({
name: "login",
initialState,
reducers: {
setProjectName(state, action: PayloadAction<string>) {
state.projectName = action.payload;
},
clearProjectName(state) {
state.projectName = null;
},
},
});

export const { clearProjectName, setProjectName } = loginSlice.actions;

export const useProjectName = (): string | null => {
return useSelector<AppState, string | null>(
(state) => state.login.projectName
);
};
108 changes: 67 additions & 41 deletions pkg/app/web/src/pages/login.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,92 @@
import { Button, makeStyles, TextField } from "@material-ui/core";
import React, { FC, memo } from "react";
import { Redirect } from "react-router";
import { PAGE_PATH_APPLICATIONS, STATIC_LOGIN_ENDPOINT } from "../constants";
import {
Button,
Card,
makeStyles,
TextField,
Typography,
} from "@material-ui/core";
import ArrowRightAltIcon from "@material-ui/icons/ArrowRightAlt";
import React, { FC, memo, useState } from "react";
import { useDispatch } from "react-redux";
import { LoginForm } from "../components/login-form";
import { setProjectName, useProjectName } from "../modules/login";
import { useMe } from "../modules/me";
import { PAGE_PATH_APPLICATIONS } from "../constants";
import { Redirect } from "react-router-dom";

const useStyles = makeStyles((theme) => ({
root: {
padding: theme.spacing(2),
display: "flex",
alignItems: "center",
justifyContent: "center",
flex: 1,
},
form: {
content: {
display: "flex",
flexDirection: "column",
padding: theme.spacing(3),
width: 500,
textAlign: "center",
},
fields: {
display: "flex",
flexDirection: "column",
marginTop: theme.spacing(4),
},
buttons: {
display: "flex",
justifyContent: "flex-end",
marginTop: theme.spacing(4),
},
}));

export const LoginPage: FC = memo(function LoginPage() {
const classes = useStyles();
const dispatch = useDispatch();
const me = useMe();
const projectName = useProjectName();
const [name, setName] = useState<string>("");

const handleOnContinue = (): void => {
dispatch(setProjectName(name));
};

return (
<div className={classes.root}>
{me && me.isLogin && <Redirect to={PAGE_PATH_APPLICATIONS} />}
<form
method="POST"
action={STATIC_LOGIN_ENDPOINT}
className={classes.form}
>
<TextField
id="project"
name="project"
label="Project"
variant="outlined"
margin="dense"
required
/>
<TextField
id="username"
name="username"
label="Username"
variant="outlined"
margin="dense"
required
/>
<TextField
id="password"
name="password"
label="Password"
type="password"
variant="outlined"
margin="dense"
required
/>
<div className={classes.buttons}>
<Button type="submit" color="primary">
Log In
</Button>
</div>
</form>
<Card className={classes.content}>
{projectName === null ? (
<>
<Typography variant="h4">Sign in to your project</Typography>
<div className={classes.fields}>
<TextField
id="project-name"
name="project-name"
label="Project Name"
variant="outlined"
margin="dense"
required
value={name}
onChange={(e) => setName(e.currentTarget.value)}
/>
</div>
<div className={classes.buttons}>
<Button
color="primary"
variant="contained"
endIcon={<ArrowRightAltIcon />}
onClick={handleOnContinue}
disabled={name === ""}
>
Continue
</Button>
</div>
</>
) : (
<LoginForm />
)}
</Card>
</div>
);
});