feat: introduce login page

This commit is contained in:
Ali BARIN
2022-03-07 18:51:57 +01:00
parent bb36748764
commit f5f7a998ca
16 changed files with 235 additions and 36 deletions

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import type { ContainerProps } from '@mui/material/Container';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import MuiAppBar from '@mui/material/AppBar';
@@ -10,6 +11,7 @@ import MenuIcon from '@mui/icons-material/Menu';
import MenuOpenIcon from '@mui/icons-material/MenuOpen';
import SettingsIcon from '@mui/icons-material/Settings';
import Container from 'components/Container';
import HideOnScroll from 'components/HideOnScroll';
import { FormattedMessage } from 'react-intl';
@@ -17,16 +19,24 @@ type AppBarProps = {
drawerOpen: boolean;
onDrawerOpen: () => void;
onDrawerClose: () => void;
maxWidth?: ContainerProps["maxWidth"];
};
export default function AppBar({ drawerOpen, onDrawerOpen, onDrawerClose }: AppBarProps): React.ReactElement {
export default function AppBar(props: AppBarProps): React.ReactElement {
const {
drawerOpen,
onDrawerOpen,
onDrawerClose,
maxWidth = false,
} = props;
const theme = useTheme();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), { noSsr: true });
return (
<Box sx={{ flexGrow: 1 }}>
<HideOnScroll>
<MuiAppBar>
<HideOnScroll>
<MuiAppBar>
<Container maxWidth={maxWidth} disableGutters>
<Toolbar>
<IconButton
size="large"
@@ -57,8 +67,8 @@ export default function AppBar({ drawerOpen, onDrawerOpen, onDrawerClose }: AppB
<SettingsIcon />
</IconButton>
</Toolbar>
</MuiAppBar>
</HideOnScroll>
</Box>
</Container>
</MuiAppBar>
</HideOnScroll>
);
}

View File

@@ -3,15 +3,16 @@ import { FormProvider, useForm, FieldValues, SubmitHandler, UseFormReturn } from
import type { UseFormProps } from 'react-hook-form';
type FormProps = {
children: React.ReactNode;
children?: React.ReactNode;
defaultValues?: UseFormProps['defaultValues'];
onSubmit?: SubmitHandler<FieldValues>;
render?: (props: UseFormReturn) => React.ReactNode;
}
const noop = () => null;
export default function Form(props: FormProps): React.ReactElement {
const { children, onSubmit = noop, defaultValues, ...formProps } = props;
const { children, onSubmit = noop, defaultValues, render, ...formProps } = props;
const methods: UseFormReturn = useForm({
defaultValues,
});
@@ -23,7 +24,7 @@ export default function Form(props: FormProps): React.ReactElement {
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>
{children}
{render ? render(methods) : children}
</form>
</FormProvider>
);

View File

@@ -1,5 +1,4 @@
import * as React from 'react';
import { useFormContext } from 'react-hook-form';
import type { IField } from '@automatisch/types';
import PowerInput from 'components/PowerInput';
@@ -20,8 +19,6 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
namePrefix,
} = props;
const { control } = useFormContext();
const {
key: name,
label,
@@ -40,7 +37,6 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
<PowerInput
label={label}
description={description}
control={control}
name={computedName}
required={required}
// onBlur={onBlur}
@@ -62,7 +58,6 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
label={label}
fullWidth
helperText={description}
control={control}
clickToCopy={clickToCopy}
/>
);

View File

@@ -7,11 +7,11 @@ import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer';
import Toolbar from '@mui/material/Toolbar';
type LayoutProps = {
type PublicLayoutProps = {
children: React.ReactNode;
}
export default function Layout({ children }: LayoutProps): React.ReactElement {
export default function PublicLayout({ children }: PublicLayoutProps): React.ReactElement {
const theme = useTheme();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'), { noSsr: true });
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
@@ -30,7 +30,7 @@ export default function Layout({ children }: LayoutProps): React.ReactElement {
onClose={closeDrawer}
/>
<Box sx={{ flex: 1 }}>
<Box sx={{ flex: 1, }}>
<Toolbar />
{children}

View File

@@ -0,0 +1,93 @@
import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import { useMutation } from '@apollo/client';
import { UseFormReturn } from 'react-hook-form';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import LoadingButton from '@mui/lab/LoadingButton';
import * as URLS from 'config/urls';
import { setItem } from 'helpers/storage';
import { LOGIN } from 'graphql/mutations/login';
import Form from 'components/Form';
import TextField from 'components/TextField';
type FormValues = {
email: string;
password: string;
}
function renderFields(props: { loading: boolean }) {
const { loading = false } = props;
return (methods: UseFormReturn) => {
return (
<>
<TextField
label="Email"
name="email"
required
fullWidth
margin="dense"
/>
<TextField
label="Password"
name="password"
type="password"
required
fullWidth
margin="dense"
/>
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2, mt: 3 }}
loading={loading}
fullWidth
>
Login
</LoadingButton>
</>
);
}
}
function LoginForm() {
const navigate = useNavigate();
const [login, { loading }] = useMutation(LOGIN);
const handleSubmit = async (values: any) => {
const { data } = await login({
variables: {
input: values
},
});
const { token } = data.login;
setItem('token', token);
navigate(URLS.FLOWS);
};
const render = React.useMemo(() => renderFields({ loading }), [loading]);
return (
<Paper sx={{ px: 2, py: 4 }}>
<Typography
variant="h3"
align="center"
sx={{ borderBottom: '1px solid', borderColor: (theme) => theme.palette.text.disabled, pb: 2, mb: 2 }}
gutterBottom>
Login
</Typography>
<Form onSubmit={handleSubmit} render={render} />
</Paper>
);
};
export default LoginForm;

View File

@@ -2,10 +2,9 @@ import * as React from 'react';
import ClickAwayListener from '@mui/base/ClickAwayListener';
import Chip from '@mui/material/Chip';
import Popper from '@mui/material/Popper';
import TextField from '@mui/material/TextField';
import InputLabel from '@mui/material/InputLabel';
import FormHelperText from '@mui/material/FormHelperText';
import { Controller, Control, FieldValues } from 'react-hook-form';
import { Controller, Control, FieldValues, useFormContext } from 'react-hook-form';
import { Editor, Transforms, Range, createEditor } from 'slate';
import {
Slate,
@@ -13,7 +12,6 @@ import {
useSelected,
useFocused,
} from 'slate-react';
import type { IExecutionStep, IStep } from '@automatisch/types';
import {
serialize,
@@ -29,7 +27,6 @@ import { VariableElement } from './types';
import { processStepWithExecutions } from './data';
type PowerInputProps = {
control?: Control<FieldValues>;
onChange?: (value: string) => void;
onBlur?: (value: string) => void;
defaultValue?: string;
@@ -44,8 +41,8 @@ type PowerInputProps = {
}
const PowerInput = (props: PowerInputProps) => {
const { control } = useFormContext();
const {
control,
defaultValue = '',
onBlur,
name,

View File

@@ -0,0 +1,41 @@
import * as React from 'react';
import Toolbar from '@mui/material/Toolbar';
import MuiAppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Container from 'components/Container';
import { FormattedMessage } from 'react-intl';
type LayoutProps = {
children: React.ReactNode;
}
export default function Layout({ children }: LayoutProps): React.ReactElement {
return (
<>
<MuiAppBar>
<Container maxWidth="lg" disableGutters>
<Toolbar>
<Typography
variant="h6"
noWrap
component="div"
sx={{ flexGrow: 1 }}
>
<FormattedMessage id="brandText" />
</Typography>
</Toolbar>
</Container>
</MuiAppBar>
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Toolbar />
{children}
</Box>
</>
);
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { Controller, Control, FieldValues } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import MuiTextField, { TextFieldProps as MuiTextFieldProps } from '@mui/material/TextField';
import IconButton from '@mui/material/IconButton';
import InputAdornment from '@mui/material/InputAdornment';
@@ -8,7 +8,6 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import copyInputValue from 'helpers/copyInputValue';
type TextFieldProps = {
control?: Control<FieldValues>;
shouldUnregister?: boolean;
name: string;
clickToCopy?: boolean;
@@ -17,21 +16,21 @@ type TextFieldProps = {
const createCopyAdornment = (ref: React.RefObject<HTMLInputElement | null>): React.ReactElement => {
return (
<InputAdornment position="end">
<IconButton
onClick={() => copyInputValue(ref.current as HTMLInputElement)}
edge="end"
>
<ContentCopyIcon color="primary" />
</IconButton>
</InputAdornment>
);
<InputAdornment position="end">
<IconButton
onClick={() => copyInputValue(ref.current as HTMLInputElement)}
edge="end"
>
<ContentCopyIcon color="primary" />
</IconButton>
</InputAdornment>
);
}
export default function TextField(props: TextFieldProps): React.ReactElement {
const { control } = useFormContext();
const inputRef = React.useRef<HTMLInputElement | null>(null);
const {
control,
required,
name,
defaultValue,

View File

@@ -2,6 +2,8 @@ export const DASHBOARD = '/dashboard';
export const CONNECTIONS = '/connections';
export const EXPLORE = '/explore';
export const LOGIN = '/login';
export const APPS = '/apps';
export const NEW_APP_CONNECTION = '/apps/new';
export const APP = (appKey: string): string => `/app/${appKey}`;

View File

@@ -2,13 +2,22 @@ import { HttpLink, from } from '@apollo/client';
import type { ApolloLink } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import * as URLS from 'config/urls';
import { getItem } from 'helpers/storage';
type CreateLinkOptions = {
uri: string;
onError?: (message: string) => void;
};
const createHttpLink = (uri: CreateLinkOptions['uri']): ApolloLink => new HttpLink({ uri });
const createHttpLink = (uri: CreateLinkOptions['uri']): ApolloLink => {
const headers = {
authorization: getItem('token') || '',
};
return new HttpLink({ uri, headers });
}
const NOT_AUTHORISED = 'Not Authorised!';
const createErrorLink = (callback: CreateLinkOptions['onError']): ApolloLink => onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) => {
@@ -17,6 +26,10 @@ const createErrorLink = (callback: CreateLinkOptions['onError']): ApolloLink =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
);
if (message === NOT_AUTHORISED) {
window.location.href = URLS.LOGIN;
}
});
if (networkError) {

View File

@@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const LOGIN = gql`
mutation Login($input: LoginInput) {
login(input: $input) {
token
user {
id
email
}
}
}
`;

View File

@@ -0,0 +1,10 @@
const NAMESPACE = 'automatisch';
const makeKey = (key: string) => `${NAMESPACE}.${key}`;
export const setItem = (key: string, value: string) => {
return localStorage.setItem(makeKey(key), value);
};
export const getItem = (key: string) => {
return localStorage.getItem(makeKey(key));
}

View File

@@ -0,0 +1,7 @@
import { getItem } from 'helpers/storage';
export default function useAuthentication(): boolean {
const token = getItem('token');
return Boolean(token);
}

View File

@@ -0,0 +1,14 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Container from 'components/Container';
import LoginForm from 'components/LoginForm';
export default function Login(): React.ReactElement {
return (
<Box sx={{ display: 'flex', flex: 1, alignItems: 'center' }}>
<Container maxWidth="sm">
<LoginForm />
</Container>
</Box>
);
};

View File

@@ -1,10 +1,12 @@
import { Route, Routes, Navigate } from 'react-router-dom';
import Layout from 'components/Layout';
import PublicLayout from 'components/PublicLayout';
import Applications from 'pages/Applications';
import Application from 'pages/Application';
import Flows from 'pages/Flows';
import Flow from 'pages/Flow';
import Explore from 'pages/Explore';
import Login from 'pages/Login';
import EditorRoutes from 'pages/Editor/routes';
import * as URLS from 'config/urls';
@@ -22,6 +24,8 @@ export default (
<Route path={`${URLS.EDITOR}/*`} element={<EditorRoutes />} />
<Route path={URLS.LOGIN} element={<PublicLayout><Login /></PublicLayout>} />
<Route path="/" element={<Navigate to={URLS.FLOWS} />} />
<Route element={<Layout><div>404</div></Layout>} />