From f5f7a998ca19a8e8d6ab6e5ae3e1eee7628d174a Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 7 Mar 2022 18:51:57 +0100 Subject: [PATCH] feat: introduce login page --- packages/web/src/components/AppBar/index.tsx | 24 +++-- packages/web/src/components/Form/index.tsx | 7 +- .../web/src/components/InputCreator/index.tsx | 5 - packages/web/src/components/Layout/index.tsx | 6 +- .../web/src/components/LoginForm/index.tsx | 93 +++++++++++++++++++ .../web/src/components/PowerInput/index.tsx | 7 +- .../web/src/components/PublicLayout/index.tsx | 41 ++++++++ .../web/src/components/TextField/index.tsx | 23 +++-- packages/web/src/config/urls.ts | 2 + packages/web/src/graphql/link.ts | 15 ++- packages/web/src/graphql/mutations/login.ts | 13 +++ packages/web/src/helpers/storage.ts | 10 ++ packages/web/src/hooks/useAuthentication.ts | 7 ++ ...eFormatMessage.tsx => useFormatMessage.ts} | 0 packages/web/src/pages/Login/index.tsx | 14 +++ packages/web/src/routes.tsx | 4 + 16 files changed, 235 insertions(+), 36 deletions(-) create mode 100644 packages/web/src/components/LoginForm/index.tsx create mode 100644 packages/web/src/components/PublicLayout/index.tsx create mode 100644 packages/web/src/graphql/mutations/login.ts create mode 100644 packages/web/src/helpers/storage.ts create mode 100644 packages/web/src/hooks/useAuthentication.ts rename packages/web/src/hooks/{useFormatMessage.tsx => useFormatMessage.ts} (100%) create mode 100644 packages/web/src/pages/Login/index.tsx diff --git a/packages/web/src/components/AppBar/index.tsx b/packages/web/src/components/AppBar/index.tsx index 620200b3..329344a8 100644 --- a/packages/web/src/components/AppBar/index.tsx +++ b/packages/web/src/components/AppBar/index.tsx @@ -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 ( - - - + + + - - - + + + ); } diff --git a/packages/web/src/components/Form/index.tsx b/packages/web/src/components/Form/index.tsx index 6c4c79ef..32691591 100644 --- a/packages/web/src/components/Form/index.tsx +++ b/packages/web/src/components/Form/index.tsx @@ -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; + 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 (
- {children} + {render ? render(methods) : children}
); diff --git a/packages/web/src/components/InputCreator/index.tsx b/packages/web/src/components/InputCreator/index.tsx index f14370d3..64a55591 100644 --- a/packages/web/src/components/InputCreator/index.tsx +++ b/packages/web/src/components/InputCreator/index.tsx @@ -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 ); diff --git a/packages/web/src/components/Layout/index.tsx b/packages/web/src/components/Layout/index.tsx index 067cd1ea..4962f1ae 100644 --- a/packages/web/src/components/Layout/index.tsx +++ b/packages/web/src/components/Layout/index.tsx @@ -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} /> - + {children} diff --git a/packages/web/src/components/LoginForm/index.tsx b/packages/web/src/components/LoginForm/index.tsx new file mode 100644 index 00000000..c7ea0cb6 --- /dev/null +++ b/packages/web/src/components/LoginForm/index.tsx @@ -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 ( + <> + + + + + + Login + + + ); + } +} + +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 ( + + theme.palette.text.disabled, pb: 2, mb: 2 }} + gutterBottom> + Login + + +
+ + ); +}; + +export default LoginForm; diff --git a/packages/web/src/components/PowerInput/index.tsx b/packages/web/src/components/PowerInput/index.tsx index 28b25bc0..6bfd7bd7 100644 --- a/packages/web/src/components/PowerInput/index.tsx +++ b/packages/web/src/components/PowerInput/index.tsx @@ -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; 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, diff --git a/packages/web/src/components/PublicLayout/index.tsx b/packages/web/src/components/PublicLayout/index.tsx new file mode 100644 index 00000000..f2a2ff4a --- /dev/null +++ b/packages/web/src/components/PublicLayout/index.tsx @@ -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 ( + <> + + + + + + + + + + + + + + {children} + + + ); +} diff --git a/packages/web/src/components/TextField/index.tsx b/packages/web/src/components/TextField/index.tsx index a29c322a..90f7b5c1 100644 --- a/packages/web/src/components/TextField/index.tsx +++ b/packages/web/src/components/TextField/index.tsx @@ -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; shouldUnregister?: boolean; name: string; clickToCopy?: boolean; @@ -17,21 +16,21 @@ type TextFieldProps = { const createCopyAdornment = (ref: React.RefObject): React.ReactElement => { return ( - - copyInputValue(ref.current as HTMLInputElement)} - edge="end" - > - - - -); + + copyInputValue(ref.current as HTMLInputElement)} + edge="end" + > + + + + ); } export default function TextField(props: TextFieldProps): React.ReactElement { + const { control } = useFormContext(); const inputRef = React.useRef(null); const { - control, required, name, defaultValue, diff --git a/packages/web/src/config/urls.ts b/packages/web/src/config/urls.ts index d6ee38f7..e476f2fa 100644 --- a/packages/web/src/config/urls.ts +++ b/packages/web/src/config/urls.ts @@ -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}`; diff --git a/packages/web/src/graphql/link.ts b/packages/web/src/graphql/link.ts index 37d16350..d49b883e 100644 --- a/packages/web/src/graphql/link.ts +++ b/packages/web/src/graphql/link.ts @@ -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) { diff --git a/packages/web/src/graphql/mutations/login.ts b/packages/web/src/graphql/mutations/login.ts new file mode 100644 index 00000000..426b5d82 --- /dev/null +++ b/packages/web/src/graphql/mutations/login.ts @@ -0,0 +1,13 @@ +import { gql } from '@apollo/client'; + +export const LOGIN = gql` + mutation Login($input: LoginInput) { + login(input: $input) { + token + user { + id + email + } + } + } +`; diff --git a/packages/web/src/helpers/storage.ts b/packages/web/src/helpers/storage.ts new file mode 100644 index 00000000..5d158686 --- /dev/null +++ b/packages/web/src/helpers/storage.ts @@ -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)); +} diff --git a/packages/web/src/hooks/useAuthentication.ts b/packages/web/src/hooks/useAuthentication.ts new file mode 100644 index 00000000..502a8376 --- /dev/null +++ b/packages/web/src/hooks/useAuthentication.ts @@ -0,0 +1,7 @@ +import { getItem } from 'helpers/storage'; + +export default function useAuthentication(): boolean { + const token = getItem('token'); + + return Boolean(token); +} diff --git a/packages/web/src/hooks/useFormatMessage.tsx b/packages/web/src/hooks/useFormatMessage.ts similarity index 100% rename from packages/web/src/hooks/useFormatMessage.tsx rename to packages/web/src/hooks/useFormatMessage.ts diff --git a/packages/web/src/pages/Login/index.tsx b/packages/web/src/pages/Login/index.tsx new file mode 100644 index 00000000..298ac480 --- /dev/null +++ b/packages/web/src/pages/Login/index.tsx @@ -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 ( + + + + + + ); +}; diff --git a/packages/web/src/routes.tsx b/packages/web/src/routes.tsx index de60b950..48fdd132 100644 --- a/packages/web/src/routes.tsx +++ b/packages/web/src/routes.tsx @@ -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 ( } /> + } /> + } />
404
} />