Merge pull request 'feature/app-setup' (#1) from feature/app-setup into main
Reviewed-on: #1
This commit is contained in:
commit
e6dedf5cd8
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
src/generated
|
16
frontend/codegen.yml
Normal file
16
frontend/codegen.yml
Normal file
@ -0,0 +1,16 @@
|
||||
overwrite: true
|
||||
schema: "http://127.0.0.1:8000/graphql"
|
||||
documents:
|
||||
- 'src/queries/*.graphql'
|
||||
generates:
|
||||
./src/generated/graphql.ts:
|
||||
plugins:
|
||||
- typescript
|
||||
- typescript-operations
|
||||
- typescript-react-query
|
||||
config:
|
||||
fetcher:
|
||||
fetchParams:
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
endpoint: "http://localhost:8000/graphql"
|
@ -5,24 +5,37 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"codegen": "graphql-codegen --config codegen.yml"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": ">=7.0.0 <8.0.0",
|
||||
"@graphql-codegen/typed-document-node": "^2.2.11",
|
||||
"@graphql-codegen/typescript-operations": "^2.4.0",
|
||||
"@graphql-codegen/typescript-react-query": "^3.5.12",
|
||||
"@headlessui/react": "^1.6.3",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"clsx": "^1.1.1",
|
||||
"graphql": "^16.5.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-hook-form": "^7.31.3",
|
||||
"react-query": "^3.39.0",
|
||||
"react-router-dom": "6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.1.0",
|
||||
"@graphql-codegen/cli": "^2.6.2",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@vitejs/plugin-react": "^1.3.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"postcss": "^8.4.14",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"typescript": "^4.6.3",
|
||||
"vite": "^2.9.9"
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxy": "http://localhost:4000"
|
||||
}
|
||||
|
3168
frontend/pnpm-lock.yaml
generated
3168
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -3,18 +3,22 @@ import MiddlewarePage from "./components/pages/middleware";
|
||||
import RoutersPage from "./components/pages/routers";
|
||||
import SecretsPage from "./components/pages/secrets";
|
||||
import ServicesPage from "./components/pages/services";
|
||||
import SetupPage from "./components/pages/setup";
|
||||
import LoginPage from "./components/pages/login";
|
||||
import RequireAuth from "./components/layouts/RequireAuth";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/services" element={<ServicesPage />} />
|
||||
<Route path="/routers" element={<RoutersPage />} />
|
||||
<Route path="/middleware" element={<MiddlewarePage />} />
|
||||
<Route path="/setup" element={<SetupPage />} />
|
||||
<Route path="/secrets" element={<SecretsPage />} />
|
||||
<Route path="*" element={<Navigate to="/services" />} />
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route path="/services" element={<ServicesPage />} />
|
||||
<Route path="/routers" element={<RoutersPage />} />
|
||||
<Route path="/middleware" element={<MiddlewarePage />} />
|
||||
<Route path="/secrets" element={<SecretsPage />} />
|
||||
<Route path="/" element={<Navigate to="/services" />} />
|
||||
</Route>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/*" element={<LoginPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 27 KiB |
@ -1,10 +1,35 @@
|
||||
import { FC, HTMLProps } from "react";
|
||||
import clsx from "clsx";
|
||||
import { FC, MouseEventHandler, ReactNode } from "react";
|
||||
import { InputSize } from "../../types";
|
||||
|
||||
const Button: FC<HTMLProps<HTMLButtonElement>> = (props) => {
|
||||
interface InputProps {
|
||||
onClick?: MouseEventHandler<HTMLButtonElement> | undefined;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
size?: InputSize;
|
||||
type?: "button" | "submit" | "reset";
|
||||
}
|
||||
|
||||
const getButtonSize = (size?: InputSize) => {
|
||||
switch (size) {
|
||||
case "md":
|
||||
return "px-[1rem] py-[.5rem] text-sm";
|
||||
default:
|
||||
return "px-10 py-4 text-base";
|
||||
}
|
||||
};
|
||||
|
||||
const Button: FC<InputProps> = (props) => {
|
||||
const btnClassName = getButtonSize(props.size);
|
||||
return (
|
||||
<button
|
||||
onClick={props.onClick}
|
||||
className="flex items-center justify-center w-full px-10 py-4 text-base font-medium text-center text-white transition duration-500 ease-in-out transform bg-blue-600 rounded-xl hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
className={clsx(
|
||||
"flex items-center justify-center w-full font-medium text-center text-white transition duration-500 ease-in-out transform bg-blue-600 rounded-xl hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
|
||||
btnClassName,
|
||||
props.className
|
||||
)}
|
||||
type={props.type}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
|
11
frontend/src/components/atoms/InlineLink.tsx
Normal file
11
frontend/src/components/atoms/InlineLink.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { FC, HTMLProps } from "react";
|
||||
|
||||
const InlineLink: FC<HTMLProps<HTMLAnchorElement>> = (props) => {
|
||||
return (
|
||||
<a className="text-sky-400 inline-block" href={props.href}>
|
||||
{props.children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default InlineLink;
|
@ -1,14 +1,46 @@
|
||||
import { FC, HTMLProps } from "react";
|
||||
import clsx from "clsx";
|
||||
import { FC } from "react";
|
||||
import { InputSize } from "../../types";
|
||||
|
||||
const Input: FC<HTMLProps<HTMLLabelElement>> = (props) => {
|
||||
interface InputProps {
|
||||
id?: string;
|
||||
type?: string;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
name?: string;
|
||||
size?: InputSize;
|
||||
value?: string;
|
||||
onChange?: React.FormEventHandler<HTMLInputElement>;
|
||||
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
const getInputSize = (size?: InputSize) => {
|
||||
switch (size) {
|
||||
case "md":
|
||||
return "px-[1rem] py-[.5rem] text-sm";
|
||||
default:
|
||||
return "px-5 py-3 text-base";
|
||||
}
|
||||
};
|
||||
|
||||
const Input: FC<InputProps> = (props) => {
|
||||
const sizeClassNames = getInputSize(props.size);
|
||||
return (
|
||||
<input
|
||||
id={props.id}
|
||||
name={props.name}
|
||||
type={props.type}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
onBlur={props.onBlur}
|
||||
placeholder={props.placeholder}
|
||||
required={props.required}
|
||||
className="block w-full px-5 py-3 text-base text-neutral-600 placeholder-gray-300 transition duration-500 ease-in-out transform border border-transparent rounded-lg bg-gray-50 focus:outline-none focus:border-transparent focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-300 shadow-sm"
|
||||
className={clsx(
|
||||
"block w-full text-neutral-600 placeholder-gray-300 transition duration-500 ease-in-out transform border border-transparent rounded-lg bg-gray-50 focus:outline-none focus:border-transparent focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-300 shadow-sm",
|
||||
sizeClassNames,
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,12 +1,17 @@
|
||||
import { FC, HTMLProps } from "react";
|
||||
|
||||
const InputLabel: FC<HTMLProps<HTMLLabelElement>> = (props) => {
|
||||
interface InputLabelProps extends HTMLProps<HTMLLabelElement> {
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const InputLabel: FC<InputLabelProps> = (props) => {
|
||||
return (
|
||||
<label
|
||||
htmlFor={props.id}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{props.children}
|
||||
{props.required && <span className="text-red-600">*</span>}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
7
frontend/src/components/layouts/Container.tsx
Normal file
7
frontend/src/components/layouts/Container.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { FC, HTMLProps } from "react";
|
||||
|
||||
const Container: FC<HTMLProps<HTMLDivElement>> = (props) => {
|
||||
return <div className="w-full lg:w-1/2 mx-auto">{props.children}</div>;
|
||||
};
|
||||
|
||||
export default Container;
|
16
frontend/src/components/layouts/RequireAuth.tsx
Normal file
16
frontend/src/components/layouts/RequireAuth.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { useEffect } from "react";
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
|
||||
const RequireAuth = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem("token")) {
|
||||
navigate("/login");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default RequireAuth;
|
@ -7,6 +7,7 @@ import {
|
||||
} from "@heroicons/react/outline";
|
||||
import { FC, ReactNode } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useServices from "../../hooks/useServices";
|
||||
import Badge from "../atoms/Badge";
|
||||
import InputLabel from "../atoms/InputLabel";
|
||||
|
||||
@ -47,20 +48,6 @@ const Sidebar = () => {
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<button className="hidden rounded-lg focus:outline-none focus:shadow-outline">
|
||||
<svg fill="currentColor" viewBox="0 0 20 20" className="w-6 h-6">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow px-4 mt-8 space-y-2">
|
||||
<div className="ml-3">
|
||||
@ -70,7 +57,7 @@ const Sidebar = () => {
|
||||
<ul className="space-y-1">
|
||||
<li>
|
||||
<SidebarTab
|
||||
badge={<Badge>0</Badge>}
|
||||
badge={<Badge>12</Badge>}
|
||||
onClick={navFactory("/services")}
|
||||
>
|
||||
<PuzzleIcon className="w-6 h-6 text-gray-700" />
|
||||
@ -117,7 +104,13 @@ const Sidebar = () => {
|
||||
<p>nikuu</p>
|
||||
</div>
|
||||
<div className="w-6 h-6">
|
||||
<LogoutIcon />
|
||||
<LogoutIcon
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
localStorage.removeItem("token");
|
||||
navigate("/login");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,17 +1,38 @@
|
||||
import { FC, HTMLProps } from "react";
|
||||
import { FC } from "react";
|
||||
import { InputSize } from "../../types";
|
||||
import Input from "../atoms/Input";
|
||||
import InputLabel from "../atoms/InputLabel";
|
||||
|
||||
const TextField: FC<HTMLProps<HTMLInputElement>> = (props) => {
|
||||
interface TextFieldProps {
|
||||
id?: string;
|
||||
type?: string;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
name?: string;
|
||||
size?: InputSize;
|
||||
label?: string;
|
||||
inputClassName?: string;
|
||||
onChange?: React.FormEventHandler<HTMLInputElement>;
|
||||
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
const TextField: FC<TextFieldProps> = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<InputLabel id={props.id}>{props.label}</InputLabel>
|
||||
<div className="w-full">
|
||||
<InputLabel id={props.id} required={props.required}>
|
||||
{props.label}
|
||||
</InputLabel>
|
||||
<div className="mt-1">
|
||||
<Input
|
||||
id={props.id}
|
||||
type={props.type}
|
||||
className={props.inputClassName}
|
||||
placeholder={props.placeholder}
|
||||
required={props.required}
|
||||
size={props.size}
|
||||
onChange={props.onChange}
|
||||
onBlur={props.onBlur}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
90
frontend/src/components/pages/login/index.tsx
Normal file
90
frontend/src/components/pages/login/index.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLoginMutation } from "../../../generated/graphql";
|
||||
|
||||
import Button from "../../atoms/Button";
|
||||
import TextField from "../../molecules/TextField";
|
||||
|
||||
const LoginPage = () => {
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm();
|
||||
const navigator = useNavigate();
|
||||
const loginMutation = useLoginMutation();
|
||||
const [submitError, setSubmitError] = useState("");
|
||||
|
||||
const onSubmit = ({ name, password }: any) => {
|
||||
loginMutation.mutate({ credentials: { name, password } });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!loginMutation.data) return;
|
||||
if (Object.hasOwn(loginMutation.data.login, "message")) {
|
||||
setSubmitError(loginMutation.data.login.message);
|
||||
return;
|
||||
}
|
||||
if (Object.hasOwn(loginMutation.data.login, "token")) {
|
||||
localStorage.setItem("token", loginMutation.data.login.token);
|
||||
navigator("/services");
|
||||
}
|
||||
}, [loginMutation.data]);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="flex flex-col justify-center min-h-screen py-12 sm:px-6 lg:px-8">
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="flex flex-row justify-start items-center w-full space-x-2">
|
||||
<div className="w-16 h-16 bg-sky-300 rounded-md" />
|
||||
<div>
|
||||
<h1 className="text-xl">Traefik</h1>
|
||||
<h2 className="block text-2xl font-extrabold tracking-tighter text-gray-900 transition duration-500 ease-in-out transform hover:text-gray-900">
|
||||
Confman
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-10">
|
||||
<form
|
||||
className="space-y-6"
|
||||
action="#"
|
||||
method="POST"
|
||||
data-bitwarden-watching="1"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<TextField label="Username" placeholder="name" {...field} />
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
type="password"
|
||||
label="Password"
|
||||
placeholder="*****"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button type="submit">Login</Button>
|
||||
{submitError && <span>{submitError}</span>}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
@ -1,9 +1,104 @@
|
||||
import { PlusIcon } from "@heroicons/react/outline";
|
||||
import clsx from "clsx";
|
||||
import { useState } from "react";
|
||||
import useServices from "../../../hooks/useServices";
|
||||
import Button from "../../atoms/Button";
|
||||
import InlineLink from "../../atoms/InlineLink";
|
||||
import Container from "../../layouts/Container";
|
||||
import DashboardLayout from "../../layouts/Dashboard";
|
||||
import TextField from "../../molecules/TextField";
|
||||
|
||||
const ServicesPage = () => {
|
||||
const [toggleCreate, setToggleCreate] = useState(false);
|
||||
const services = useServices();
|
||||
|
||||
const handleToggleCreate = () => {
|
||||
setToggleCreate(!toggleCreate);
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div>Services</div>
|
||||
<Container>
|
||||
<h1 className="text-3xl font-bold mt-12 mb-8">Services</h1>
|
||||
<div className="mb-4 flex items-end justify-between">
|
||||
<TextField
|
||||
label="Search"
|
||||
size="md"
|
||||
placeholder="Search services"
|
||||
inputClassName="bg-white border border-gray-300 w-60 shadow-none"
|
||||
/>
|
||||
<Button
|
||||
size="md"
|
||||
className={clsx(
|
||||
"w-20 border border-sky-600",
|
||||
toggleCreate && "bg-white text-sky-600 hover:bg-white"
|
||||
)}
|
||||
onClick={handleToggleCreate}
|
||||
>
|
||||
<div className="flex flex-row space-x-1">
|
||||
<PlusIcon width={20} />
|
||||
<span>Add</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{toggleCreate && (
|
||||
<div className="w-full bg-gray-50 border border-gray-200 rounded-lg p-5 mb-4">
|
||||
<h1 className="mb-4">Create a new service</h1>
|
||||
<div className="space-y-3">
|
||||
<TextField
|
||||
label="Name"
|
||||
required={true}
|
||||
placeholder="service name"
|
||||
size="md"
|
||||
inputClassName="shadow-none border border-gray-300 bg-white"
|
||||
/>
|
||||
<TextField
|
||||
label="Destination"
|
||||
required={true}
|
||||
placeholder="url"
|
||||
size="md"
|
||||
inputClassName="shadow-none border border-gray-300 bg-white"
|
||||
/>
|
||||
<div className="flex justify-end items-center">
|
||||
<Button size="md" className="w-20 border border-sky-600">
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-lg border">
|
||||
<table className="w-full border-collapse">
|
||||
<thead className="h-12">
|
||||
<tr className="text-left font-semibold text-sm border-b bg-gray-50">
|
||||
<th className="px-3 py-2 lg:w-2/12">Name</th>
|
||||
<th className="px-3 py-2 lg:w-4/12">Destination</th>
|
||||
<th className="px-3 py-2 lg:w-5/12">Referenced by</th>
|
||||
<th className="px-3 py-2 lg:w-1/12">{/* Delete */}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{services.data &&
|
||||
services.data.map((service) => (
|
||||
<tr key={service.name} className="h-12 border-b ">
|
||||
<td className="px-3 py-2 truncate">{service.name}</td>
|
||||
<td className="px-3 py-2 truncate">
|
||||
<InlineLink href="https://example.com/">
|
||||
{service.destination[0]}
|
||||
</InlineLink>
|
||||
</td>
|
||||
<td className="px-3 py-2 truncate">
|
||||
<InlineLink href="https://example.com/">
|
||||
{service.references}
|
||||
</InlineLink>
|
||||
</td>
|
||||
<td className="py-2 cursor-pointer text-sky-900">Delete</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Container>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
|
@ -1,62 +0,0 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import Button from "../../atoms/Button";
|
||||
import TextField from "../../molecules/TextField";
|
||||
import TraefikImgUrl from "../../../assets/traefik.png";
|
||||
|
||||
const SetupPage = () => {
|
||||
const navigator = useNavigate();
|
||||
|
||||
const handleContinue = () => navigator("/");
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="flex flex-col justify-center min-h-screen py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<img className="w-40 h-40 mx-auto" src={TraefikImgUrl} />
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-center text-neutral-600">
|
||||
Setup Traefik Confman
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="px-4 py-8 sm:px-10">
|
||||
<form
|
||||
className="space-y-6"
|
||||
action="#"
|
||||
method="POST"
|
||||
data-bitwarden-watching="1"
|
||||
>
|
||||
<TextField
|
||||
id="username"
|
||||
label="Username"
|
||||
placeholder="name"
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
type="password"
|
||||
label="Password"
|
||||
placeholder="*****"
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
id="repeatPassword"
|
||||
type="password"
|
||||
label="Repeat Password"
|
||||
placeholder="*****"
|
||||
required
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button onClick={handleContinue}>Continue</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupPage;
|
19
frontend/src/hooks/useServices.tsx
Normal file
19
frontend/src/hooks/useServices.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { useQuery } from "react-query";
|
||||
import { Service } from "../types";
|
||||
|
||||
let services = Array(11)
|
||||
.fill(0)
|
||||
.map(() => ({
|
||||
name: faker.name.lastName(),
|
||||
references: [faker.name.firstName()],
|
||||
destination: [faker.internet.ipv4()],
|
||||
}));
|
||||
|
||||
const fetchServices = async (): Promise<Service[]> => {
|
||||
return services;
|
||||
};
|
||||
|
||||
export default function useServices() {
|
||||
return useQuery(["services"], fetchServices);
|
||||
}
|
@ -1,10 +1,19 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import { ReactQueryDevtools } from "react-query/devtools";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<ReactQueryDevtools initialIsOpen />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
export { queryClient };
|
||||
|
13
frontend/src/queries/services.graphql
Normal file
13
frontend/src/queries/services.graphql
Normal file
@ -0,0 +1,13 @@
|
||||
mutation Login($credentials: LoginInput!) {
|
||||
login(body: $credentials) {
|
||||
... on AuthSuccess {
|
||||
user {
|
||||
name
|
||||
}
|
||||
token
|
||||
}
|
||||
... on CommonError {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
7
frontend/src/types.ts
Normal file
7
frontend/src/types.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export type InputSize = "sm" | "md" | "lg";
|
||||
|
||||
export interface Service {
|
||||
name: string;
|
||||
references: string[];
|
||||
destination: string[];
|
||||
}
|
13
main.py
13
main.py
@ -1,4 +1,5 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from strawberry.fastapi import GraphQLRouter
|
||||
|
||||
from api.schema import schema
|
||||
@ -13,5 +14,17 @@ graphql_app = GraphQLRouter(
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
origins = [
|
||||
"http://localhost:3000",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(graphql_app, prefix="/graphql")
|
||||
app.mount("/", SPAStaticFiles(directory="frontend/dist", html=True), name="frontend")
|
||||
|
Loading…
x
Reference in New Issue
Block a user