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 (
);
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
} />