mirror of
https://github.com/pds-nest/nest.git
synced 2024-11-22 04:54:18 +00:00
Merge remote-tracking branch 'origin/main' into main
This commit is contained in:
commit
1478ec00cd
41 changed files with 577 additions and 284 deletions
|
@ -5,7 +5,7 @@
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/nest_backend" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/nest_backend" isTestSource="false" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Poetry (g2-progetto) (2)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Poetry (backend)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
|
@ -1,7 +1,4 @@
|
||||||
import classNames from "classnames"
|
|
||||||
import Style from "./App.module.css"
|
|
||||||
import Layout from "./components/Layout"
|
import Layout from "./components/Layout"
|
||||||
import ContextTheme from "./contexts/ContextTheme"
|
|
||||||
import { BrowserRouter } from "react-router-dom"
|
import { BrowserRouter } from "react-router-dom"
|
||||||
import { Route, Switch } from "react-router"
|
import { Route, Switch } from "react-router"
|
||||||
import PageDashboard from "./routes/PageDashboard"
|
import PageDashboard from "./routes/PageDashboard"
|
||||||
|
@ -9,11 +6,11 @@ import PageRepositories from "./routes/PageRepositories"
|
||||||
import PageAlerts from "./routes/PageAlerts"
|
import PageAlerts from "./routes/PageAlerts"
|
||||||
import PageSettings from "./routes/PageSettings"
|
import PageSettings from "./routes/PageSettings"
|
||||||
import PageSandbox from "./routes/PageSandbox"
|
import PageSandbox from "./routes/PageSandbox"
|
||||||
import useSavedTheme from "./hooks/useSavedTheme"
|
|
||||||
import PageLogin from "./routes/PageLogin"
|
import PageLogin from "./routes/PageLogin"
|
||||||
import useSavedLogin from "./hooks/useSavedLogin"
|
|
||||||
import ContextLogin from "./contexts/ContextLogin"
|
|
||||||
import PageRoot from "./routes/PageRoot"
|
import PageRoot from "./routes/PageRoot"
|
||||||
|
import GlobalTheme from "./components/GlobalTheme"
|
||||||
|
import GlobalServer from "./components/GlobalServer"
|
||||||
|
import GlobalUser from "./components/GlobalUser"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,44 +20,41 @@ import PageRoot from "./routes/PageRoot"
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const theme = useSavedTheme();
|
|
||||||
const login = useSavedLogin();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextTheme.Provider value={theme}>
|
<GlobalServer>
|
||||||
<ContextLogin.Provider value={login}>
|
<GlobalUser>
|
||||||
<BrowserRouter>
|
<GlobalTheme>
|
||||||
|
<BrowserRouter>
|
||||||
|
|
||||||
<div className={classNames(Style.App, theme)}>
|
<Layout>
|
||||||
<Layout>
|
<Switch>
|
||||||
<Switch>
|
<Route path={"/login"} exact={true}>
|
||||||
<Route path={"/login"} exact={true}>
|
<PageLogin/>
|
||||||
<PageLogin/>
|
</Route>
|
||||||
</Route>
|
<Route path={"/repositories"} exact={true}>
|
||||||
<Route path={"/repositories"} exact={true}>
|
<PageRepositories/>
|
||||||
<PageRepositories/>
|
</Route>
|
||||||
</Route>
|
<Route path={"/alerts"} exact={true}>
|
||||||
<Route path={"/alerts"} exact={true}>
|
<PageAlerts/>
|
||||||
<PageAlerts/>
|
</Route>
|
||||||
</Route>
|
<Route path={"/settings"} exact={true}>
|
||||||
<Route path={"/settings"} exact={true}>
|
<PageSettings/>
|
||||||
<PageSettings/>
|
</Route>
|
||||||
</Route>
|
<Route path={"/sandbox"} exact={true}>
|
||||||
<Route path={"/sandbox"} exact={true}>
|
<PageSandbox/>
|
||||||
<PageSandbox/>
|
</Route>
|
||||||
</Route>
|
<Route path={"/dashboard"} exact={true}>
|
||||||
<Route path={"/dashboard"} exact={true}>
|
<PageDashboard/>
|
||||||
<PageDashboard/>
|
</Route>
|
||||||
</Route>
|
<Route path={"/"}>
|
||||||
<Route path={"/"}>
|
<PageRoot/>
|
||||||
<PageRoot/>
|
</Route>
|
||||||
</Route>
|
</Switch>
|
||||||
</Switch>
|
</Layout>
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ContextLogin.Provider>
|
</GlobalTheme>
|
||||||
</ContextTheme.Provider>
|
</GlobalUser>
|
||||||
|
</GlobalServer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
.App {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
color: var(--fg-primary);
|
|
||||||
}
|
|
22
code/frontend/src/components/BoxAlert.js
Normal file
22
code/frontend/src/components/BoxAlert.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React from "react"
|
||||||
|
import Style from "./BoxAlert.module.css"
|
||||||
|
import classNames from "classnames"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A colored alert box to draw the user's attention.
|
||||||
|
*
|
||||||
|
* @param color - The color of the alert.
|
||||||
|
* @param children - The contents of the alert.
|
||||||
|
* @param className - Additional class(es) to add to the div.
|
||||||
|
* @param props - Additional props to pass to the div.
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default function BoxAlert({ color, children, className, ...props }) {
|
||||||
|
return (
|
||||||
|
<div className={classNames(Style.BoxAlert, Style[`BoxAlert${color}`], className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
25
code/frontend/src/components/BoxAlert.module.css
Normal file
25
code/frontend/src/components/BoxAlert.module.css
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
.BoxAlert {
|
||||||
|
padding: 5px 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.BoxAlertRed {
|
||||||
|
color: var(--fg-red);
|
||||||
|
background-color: var(--bg-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.BoxAlertGreen {
|
||||||
|
color: var(--fg-green);
|
||||||
|
background-color: var(--bg-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.BoxAlertYellow {
|
||||||
|
color: var(--fg-yellow);
|
||||||
|
background-color: var(--bg-yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.BoxAlertGrey {
|
||||||
|
color: var(--fg-grey);
|
||||||
|
background-color: var(--bg-grey);
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import BoxFull from "./BoxFull"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link BoxFull} whose body does not grow automatically but instead supports scrolling.
|
* A {@link BoxFull} whose body supports scrolling.
|
||||||
*
|
*
|
||||||
* @param children - The contents of the box body.
|
* @param children - The contents of the box body.
|
||||||
* @param childrenClassName - Additional class(es) added to the inner `<div>` acting as the body.
|
* @param childrenClassName - Additional class(es) added to the inner `<div>` acting as the body.
|
||||||
|
|
|
@ -3,20 +3,28 @@ import BoxFull from "./BoxFull"
|
||||||
import LoggedInUser from "./LoggedInUser"
|
import LoggedInUser from "./LoggedInUser"
|
||||||
import Button from "./Button"
|
import Button from "./Button"
|
||||||
import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons"
|
import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons"
|
||||||
import ContextLogin from "../contexts/ContextLogin"
|
import ContextUser from "../contexts/ContextUser"
|
||||||
import { useHistory } from "react-router"
|
import { useHistory } from "react-router"
|
||||||
import Style from "./BoxLoggedIn.module.css"
|
import Style from "./BoxLoggedIn.module.css"
|
||||||
|
import CurrentServer from "./CurrentServer"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link BoxFull} displaying the user's current login status, and allowing them to logout.
|
||||||
|
*
|
||||||
|
* @param props
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
export default function BoxLoggedIn({ ...props }) {
|
export default function BoxLoggedIn({ ...props }) {
|
||||||
const {logout} = useContext(ContextLogin)
|
const {logout} = useContext(ContextUser)
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxFull header={"Logged in"} {...props}>
|
<BoxFull header={"Logged in"} {...props}>
|
||||||
<div className={Style.BoxLoggedInContents}>
|
<div className={Style.BoxLoggedInContents}>
|
||||||
<div>
|
<div>
|
||||||
You are currently logged in as <LoggedInUser/>.
|
You are currently logged in at <CurrentServer/> as <LoggedInUser/>.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button color={"Red"} onClick={e => {
|
<Button color={"Red"} onClick={e => {
|
||||||
|
|
85
code/frontend/src/components/BoxLogin.js
Normal file
85
code/frontend/src/components/BoxLogin.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import React, { useContext, useState } from "react"
|
||||||
|
import FormLabelled from "./FormLabelled"
|
||||||
|
import FormLabel from "./FormLabel"
|
||||||
|
import InputWithIcon from "./InputWithIcon"
|
||||||
|
import { faArrowRight, faEnvelope, faKey } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
import BoxFull from "./BoxFull"
|
||||||
|
import FormButton from "./FormButton"
|
||||||
|
import ContextUser from "../contexts/ContextUser"
|
||||||
|
import { useHistory } from "react-router"
|
||||||
|
import FormAlert from "./FormAlert"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link BoxFull} allowing the user to login.
|
||||||
|
*
|
||||||
|
* @param props - Additional props to pass to the box.
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default function BoxLogin({ ...props }) {
|
||||||
|
const [email, setEmail] = useState("admin@admin.com")
|
||||||
|
const [password, setPassword] = useState("password")
|
||||||
|
const [working, setWorking] = useState(false)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
const {login} = useContext(ContextUser)
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
const doLogin = async (event) => {
|
||||||
|
if(working) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setWorking(true)
|
||||||
|
try {
|
||||||
|
await login(email, password)
|
||||||
|
history.push("/dashboard")
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
setError(e)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setWorking(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoxFull header={"Login"} {...props}>
|
||||||
|
<FormLabelled>
|
||||||
|
<FormLabel text={"Email"} htmlFor={"login-email"}>
|
||||||
|
<InputWithIcon
|
||||||
|
id={"login-email"}
|
||||||
|
name={"login-email"}
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
type={"email"}
|
||||||
|
icon={faEnvelope}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"Password"} htmlFor={"login-password"}>
|
||||||
|
<InputWithIcon
|
||||||
|
id={"login-password"}
|
||||||
|
name={"login-password"}
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
type={"password"}
|
||||||
|
icon={faKey}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
{error ?
|
||||||
|
<FormAlert color={"Red"}>
|
||||||
|
{error.toString()}
|
||||||
|
</FormAlert>
|
||||||
|
: null}
|
||||||
|
<FormButton
|
||||||
|
onClick={doLogin}
|
||||||
|
icon={faArrowRight}
|
||||||
|
color={"Green"}
|
||||||
|
disabled={working}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</FormButton>
|
||||||
|
</FormLabelled>
|
||||||
|
</BoxFull>
|
||||||
|
)
|
||||||
|
}
|
35
code/frontend/src/components/BoxSetServer.js
Normal file
35
code/frontend/src/components/BoxSetServer.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { useContext } from "react"
|
||||||
|
import BoxFull from "./BoxFull"
|
||||||
|
import FormLabelled from "./FormLabelled"
|
||||||
|
import FormLabel from "./FormLabel"
|
||||||
|
import InputWithIcon from "./InputWithIcon"
|
||||||
|
import { faGlobe } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
import ContextServer from "../contexts/ContextServer"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link BoxFull} allowing the user to select the backend server they want to login to.
|
||||||
|
*
|
||||||
|
* @param props - Additional props to pass to the box.
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default function BoxSetServer({ ...props }) {
|
||||||
|
const {server, setServer} = useContext(ContextServer);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoxFull header={"Choose server"} {...props}>
|
||||||
|
<FormLabelled>
|
||||||
|
<FormLabel text={"Base URL"} htmlFor={"set-server-base-url"}>
|
||||||
|
<InputWithIcon
|
||||||
|
id={"set-server-base-url"}
|
||||||
|
type={"url"}
|
||||||
|
icon={faGlobe}
|
||||||
|
value={server}
|
||||||
|
onChange={e => setServer(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
</FormLabelled>
|
||||||
|
</BoxFull>
|
||||||
|
)
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import { useRouteMatch } from "react-router"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A button residing in the sidebar, used to switch between pages.
|
* A button residing in the {@link Sidebar}, used to switch between pages.
|
||||||
*
|
*
|
||||||
* @param icon - The FontAwesome IconDefinition of the icon that should be rendered in the button.
|
* @param icon - The FontAwesome IconDefinition of the icon that should be rendered in the button.
|
||||||
* @param children - The contents of the button.
|
* @param children - The contents of the button.
|
||||||
|
|
22
code/frontend/src/components/CurrentServer.js
Normal file
22
code/frontend/src/components/CurrentServer.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React, { useContext } from "react"
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||||
|
import { faGlobe } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
import ContextServer from "../contexts/ContextServer"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An element displaying inline the current backend server.
|
||||||
|
*
|
||||||
|
* @param props - Additional props to pass to the element.
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default function CurrentServer({ ...props }) {
|
||||||
|
const {server} = useContext(ContextServer);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<b {...props}>
|
||||||
|
<FontAwesomeIcon icon={faGlobe}/> {server}
|
||||||
|
</b>
|
||||||
|
)
|
||||||
|
}
|
22
code/frontend/src/components/FormAlert.js
Normal file
22
code/frontend/src/components/FormAlert.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React from "react"
|
||||||
|
import Style from "./FormAlert.module.css"
|
||||||
|
import classNames from "classnames"
|
||||||
|
import BoxAlert from "./BoxAlert"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link BoxAlert} ready to be used in a {@link FormLabelled}.
|
||||||
|
*
|
||||||
|
* @param children - The contents of the alert.
|
||||||
|
* @param className - Additional class(es) to add to the box.
|
||||||
|
* @param props - Additional props to pass to the box.
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default function FormAlert({ children, className, ...props }) {
|
||||||
|
return (
|
||||||
|
<BoxAlert className={classNames(Style.FormAlert, className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</BoxAlert>
|
||||||
|
)
|
||||||
|
}
|
3
code/frontend/src/components/FormAlert.module.css
Normal file
3
code/frontend/src/components/FormAlert.module.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.FormAlert {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
19
code/frontend/src/components/FormButton.js
Normal file
19
code/frontend/src/components/FormButton.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import React from "react"
|
||||||
|
import Style from "./FormButton.module.css"
|
||||||
|
import classNames from "classnames"
|
||||||
|
import Button from "./Button"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Button} ready to be used in a {@link FormLabelled}.
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default function FormButton({ children, className, ...props }) {
|
||||||
|
return (
|
||||||
|
<Button className={classNames(Style.FormButton, className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
3
code/frontend/src/components/FormButton.module.css
Normal file
3
code/frontend/src/components/FormButton.module.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.FormButton {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
import React, { Fragment } from "react"
|
import React, { Fragment } from "react"
|
||||||
import Style from "./Label.module.css"
|
import Style from "./FormLabel.module.css"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A row of a {@link LabelledForm}.
|
* A row of a {@link FormLabelled}.
|
||||||
|
*
|
||||||
* It displays a label on the first column and a container for the labelled element on the second column.
|
* It displays a label on the first column and a container for the labelled element on the second column.
|
||||||
*
|
*
|
||||||
* @param children - The labelled element.
|
* @param children - The labelled element.
|
||||||
|
@ -12,7 +13,7 @@ import Style from "./Label.module.css"
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function Label({ children, text, htmlFor }) {
|
export default function FormLabel({ children, text, htmlFor }) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<label htmlFor={htmlFor} className={Style.LabelText} >
|
<label htmlFor={htmlFor} className={Style.LabelText} >
|
|
@ -1,20 +1,17 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import Style from "./LabelledForm.module.css"
|
import Style from "./FormLabelled.module.css"
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A form with two columns: the leftmost one contains labels, while the rightmost one contains input elements.
|
* A form with two columns: the leftmost one contains labels, while the rightmost one contains input elements.
|
||||||
*
|
*
|
||||||
* The {@link Label} element can be used to quickly generate labelled input elements.
|
* The {@link FormLabel} element can be used to quickly generate labelled input elements.
|
||||||
*
|
*
|
||||||
* @param children - The contents of the form.
|
|
||||||
* @param className - Additional class(es) to be added to the form.
|
|
||||||
* @param props - Additional props to be passed to the form.
|
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function LabelledForm({ children, className, ...props }) {
|
export default function FormLabelled({ children, className, ...props }) {
|
||||||
return (
|
return (
|
||||||
<form className={classNames(Style.LabelledForm, className)} {...props}>
|
<form className={classNames(Style.LabelledForm, className)} {...props}>
|
||||||
{children}
|
{children}
|
64
code/frontend/src/components/GlobalServer.js
Normal file
64
code/frontend/src/components/GlobalServer.js
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import React from "react"
|
||||||
|
import useLocalStorageState from "../hooks/useLocalStorageState"
|
||||||
|
import ContextServer from "../contexts/ContextServer"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides {@link ContextServer} to all contained elements.
|
||||||
|
*
|
||||||
|
* Defaults to using `http://127.0.0.1:5000` as server address.
|
||||||
|
*
|
||||||
|
* @param children
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default function GlobalServer({ children }) {
|
||||||
|
const [server, setServer] = useLocalStorageState("server", "http://127.0.0.1:5000")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch JSON data from the API.
|
||||||
|
*
|
||||||
|
* @param method - The method to use.
|
||||||
|
* @param path - The path to request data at (ex. `/api/repositories`)
|
||||||
|
* @param body - The body of the request (it will be automatically converted to JSON.
|
||||||
|
* @param init - Additional arguments to pass to the `init` parameter of {@link fetch}.
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
const fetchData = async (method, path, body, init) => {
|
||||||
|
if(!server) {
|
||||||
|
throw new Error(`Invalid server: ${server}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!init) {
|
||||||
|
init = {}
|
||||||
|
}
|
||||||
|
if(!init["headers"]) {
|
||||||
|
init["headers"] = {}
|
||||||
|
}
|
||||||
|
init["headers"]["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
const response = await fetch(`${server}${path}`, {
|
||||||
|
method: method,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
...init,
|
||||||
|
})
|
||||||
|
|
||||||
|
if(response.status >= 500) {
|
||||||
|
throw new Error(`Server error: ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
if(json["result"] !== "success") {
|
||||||
|
throw new Error(`Request failed: ${json["msg"]}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json["data"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextServer.Provider value={{server, setServer, fetchData}}>
|
||||||
|
{children}
|
||||||
|
</ContextServer.Provider>
|
||||||
|
)
|
||||||
|
}
|
21
code/frontend/src/components/GlobalTheme.js
Normal file
21
code/frontend/src/components/GlobalTheme.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React from "react"
|
||||||
|
import useLocalStorageState from "../hooks/useLocalStorageState"
|
||||||
|
import ContextTheme from "../contexts/ContextTheme"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides {@link ContextTheme} to all contained elements.
|
||||||
|
*
|
||||||
|
* @param children
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default function GlobalTheme({ children }) {
|
||||||
|
const [theme, setTheme] = useLocalStorageState("theme", "ThemeDark")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextTheme.Provider value={{theme, setTheme}}>
|
||||||
|
{children}
|
||||||
|
</ContextTheme.Provider>
|
||||||
|
)
|
||||||
|
}
|
86
code/frontend/src/components/GlobalUser.js
Normal file
86
code/frontend/src/components/GlobalUser.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import React, { useContext } from "react"
|
||||||
|
import useLocalStorageState from "../hooks/useLocalStorageState"
|
||||||
|
import ContextServer from "../contexts/ContextServer"
|
||||||
|
import ContextUser from "../contexts/ContextUser"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides {@link ContextUser} to all contained elements.
|
||||||
|
*
|
||||||
|
* @param children
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default function GlobalUser({ children }) {
|
||||||
|
const {fetchData} = useContext(ContextServer)
|
||||||
|
const [user, setUser] = useLocalStorageState("login", null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch JSON data from the API as the authenticated user.
|
||||||
|
*
|
||||||
|
* Requires an user to be logged in!
|
||||||
|
*
|
||||||
|
* @param method - The method to use.
|
||||||
|
* @param path - The path to request data at (ex. `/api/repositories`)
|
||||||
|
* @param body - The body of the request (it will be automatically converted to JSON.
|
||||||
|
* @param init - Additional arguments to pass to the `init` parameter of {@link fetch}.
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
const fetchDataAuth = async (method, path, body, init) => {
|
||||||
|
if(!user) {
|
||||||
|
throw new Error("Not logged in")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!init) {
|
||||||
|
init = {}
|
||||||
|
}
|
||||||
|
if(!init["headers"]) {
|
||||||
|
init["headers"] = {}
|
||||||
|
}
|
||||||
|
init["headers"]["Authorization"] = `Bearer ${user["token"]}`
|
||||||
|
|
||||||
|
return await fetchData(path, init)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to login to the active server with the passed credentials.
|
||||||
|
*
|
||||||
|
* @param email - The user's email.
|
||||||
|
* @param password - The user's password.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const login = async (email, password) => {
|
||||||
|
console.debug("Contacting server to login...")
|
||||||
|
const data = await fetchData("POST", `/api/login`, {
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.debug("Storing login state...")
|
||||||
|
setUser({
|
||||||
|
email: data["user"]["email"],
|
||||||
|
isAdmin: data["user"]["isAdmin"],
|
||||||
|
username: data["user"]["username"],
|
||||||
|
token: data["access_token"],
|
||||||
|
})
|
||||||
|
|
||||||
|
console.info("Login successful!")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout from the currently active server.
|
||||||
|
*/
|
||||||
|
const logout = () => {
|
||||||
|
console.debug("Clearing login state...")
|
||||||
|
setUser(null)
|
||||||
|
console.debug("Cleared login state!")
|
||||||
|
|
||||||
|
console.info("Logout successful!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextUser.Provider value={{user, login, logout, fetchDataAuth}}>
|
||||||
|
{children}
|
||||||
|
</ContextUser.Provider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
import React from "react"
|
import React, { useContext } from "react"
|
||||||
import Style from "./Layout.module.css"
|
import Style from "./Layout.module.css"
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import Sidebar from "./Sidebar"
|
import Sidebar from "./Sidebar"
|
||||||
|
import ContextTheme from "../contexts/ContextTheme"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,12 +15,14 @@ import Sidebar from "./Sidebar"
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function Layout({ children, className, ...props }) {
|
export default function Layout({ children, className, ...props }) {
|
||||||
|
const { theme } = useContext(ContextTheme)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(Style.Layout, className)} {...props}>
|
<div className={classNames(theme, Style.Layout, className)} {...props}>
|
||||||
<Sidebar className={Style.LayoutSidebar}/>
|
<Sidebar className={Style.LayoutSidebar}/>
|
||||||
<div className={Style.LayoutContent}>
|
<main className={Style.LayoutContent}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
.Layout {
|
.Layout {
|
||||||
width: 100%;
|
height: 100vh;
|
||||||
height: 100%;
|
width: 100vw;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--fg-primary);
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"a b"
|
"a b"
|
||||||
;
|
;
|
||||||
|
|
|
@ -1,25 +1,30 @@
|
||||||
import React, { useContext } from "react"
|
import React, { useContext } from "react"
|
||||||
import Style from "./LoggedInUser.module.css"
|
|
||||||
import classNames from "classnames"
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
import { faUser } from "@fortawesome/free-solid-svg-icons"
|
import { faUser } from "@fortawesome/free-solid-svg-icons"
|
||||||
import ContextLogin from "../contexts/ContextLogin"
|
import ContextUser from "../contexts/ContextUser"
|
||||||
|
|
||||||
|
|
||||||
export default function LoggedInUser({ children, className, ...props }) {
|
/**
|
||||||
const {state} = useContext(ContextLogin);
|
* An element displaying inline the currently logged in user.
|
||||||
|
*
|
||||||
|
* @param props - Additional props to pass to the element.
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default function LoggedInUser({ ...props }) {
|
||||||
|
const {user} = useContext(ContextUser);
|
||||||
|
|
||||||
if(!state) {
|
if(!user) {
|
||||||
return (
|
return (
|
||||||
<i className={classNames(Style.LoggedInUser, className)} {...props}>
|
<i {...props}>
|
||||||
Not logged in
|
Not logged in
|
||||||
</i>
|
</i>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<b className={classNames(Style.LoggedInUser, className)} {...props}>
|
<b {...props}>
|
||||||
<FontAwesomeIcon icon={faUser}/> {state.username}
|
<FontAwesomeIcon icon={faUser}/> {user.username}
|
||||||
</b>
|
</b>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
.LoggedInUser {
|
|
||||||
|
|
||||||
}
|
|
|
@ -19,7 +19,7 @@ import classNames from "classnames"
|
||||||
export default function Logo({ className, ...props }) {
|
export default function Logo({ className, ...props }) {
|
||||||
// I have no idea why IntelliJ is complaining about this line
|
// I have no idea why IntelliJ is complaining about this line
|
||||||
// It's perfectly fine!
|
// It's perfectly fine!
|
||||||
const [theme, ] = useContext(ContextTheme)
|
const {theme} = useContext(ContextTheme)
|
||||||
|
|
||||||
let logo;
|
let logo;
|
||||||
if(theme === "ThemeDark") {
|
if(theme === "ThemeDark") {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import Style from "./RepositorySummary.module.css"
|
import Style from "./RepositorySummaryBase.module.css"
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||||
import Button from "./Button"
|
import Button from "./Button"
|
||||||
|
|
|
@ -27,10 +27,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.IconContainer {
|
.IconContainer {
|
||||||
margin-top: 5px;
|
margin: 5px 15px 5px 5px;
|
||||||
margin-right: 15px;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
|
@ -12,7 +12,7 @@ import ContextTheme from "../contexts/ContextTheme"
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function SelectTheme({ ...props }) {
|
export default function SelectTheme({ ...props }) {
|
||||||
const [theme, setTheme] = useContext(ContextTheme);
|
const {theme, setTheme} = useContext(ContextTheme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select value={theme} onChange={e => setTheme(e.target.value)} {...props}>
|
<Select value={theme} onChange={e => setTheme(e.target.value)} {...props}>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import classNames from "classnames"
|
||||||
import Logo from "./Logo"
|
import Logo from "./Logo"
|
||||||
import ButtonSidebar from "./ButtonSidebar"
|
import ButtonSidebar from "./ButtonSidebar"
|
||||||
import { faCog, faExclamationTriangle, faFolder, faHome, faKey, faWrench } from "@fortawesome/free-solid-svg-icons"
|
import { faCog, faExclamationTriangle, faFolder, faHome, faKey, faWrench } from "@fortawesome/free-solid-svg-icons"
|
||||||
import ContextLogin from "../contexts/ContextLogin"
|
import ContextUser from "../contexts/ContextUser"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,13 +17,13 @@ import ContextLogin from "../contexts/ContextLogin"
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function Sidebar({ className, ...props }) {
|
export default function Sidebar({ className, ...props }) {
|
||||||
const {state} = useContext(ContextLogin)
|
const {user} = useContext(ContextUser)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(Style.Sidebar, className)} {...props}>
|
<aside className={classNames(Style.Sidebar, className)} {...props}>
|
||||||
<Logo/>
|
<Logo/>
|
||||||
{
|
{
|
||||||
state ?
|
user ?
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ButtonSidebar to={"/dashboard"} icon={faHome}>Dashboard</ButtonSidebar>
|
<ButtonSidebar to={"/dashboard"} icon={faHome}>Dashboard</ButtonSidebar>
|
||||||
<ButtonSidebar to={"/repositories"} icon={faFolder}>Repositories</ButtonSidebar>
|
<ButtonSidebar to={"/repositories"} icon={faFolder}>Repositories</ButtonSidebar>
|
||||||
|
@ -40,6 +40,6 @@ export default function Sidebar({ className, ...props }) {
|
||||||
<ButtonSidebar to={"/sandbox"} icon={faWrench}>Sandbox</ButtonSidebar>
|
<ButtonSidebar to={"/sandbox"} icon={faWrench}>Sandbox</ButtonSidebar>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
</div>
|
</aside>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import {createContext} from "react";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A context containing an object with the following values:
|
|
||||||
* - `state`: `null` if the user is not logged in, otherwise an object containing:
|
|
||||||
* - `server`: The base url of the N.E.S.T. backend
|
|
||||||
* - `email`: The email of the account the user is logged in as
|
|
||||||
* - `token`: The bearer token to use in authenticated API requests
|
|
||||||
* - `working`: `true` if the login procedure is running, `false` otherwise
|
|
||||||
* - `error`: `null` if no login error happened, an instance of {@link Error} otherwise.
|
|
||||||
* - `login`: an async function which logs in the user if given the following parameters: `server, email, password`
|
|
||||||
* - `logout`: a function which logs out the user
|
|
||||||
* - `fetch_unauth`: a variant of {@link fetch} which uses `state.server` as the base url, allowing only the API path
|
|
||||||
* to be passed
|
|
||||||
* - `fetch_auth`: a variant of {@link fetch_unauth} which automatically passes the correct `Authentication` header
|
|
||||||
*
|
|
||||||
* @type {React.Context}
|
|
||||||
*/
|
|
||||||
const ContextLogin = createContext(null)
|
|
||||||
export default ContextLogin
|
|
22
code/frontend/src/contexts/ContextServer.js
Normal file
22
code/frontend/src/contexts/ContextServer.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import {createContext} from "react";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A context containing an object with the following values:
|
||||||
|
* - `server`: the base URL of the currently active backend server
|
||||||
|
* - `setServer`: a function to change `server`
|
||||||
|
* - `fetchData`: a function to fetch JSON data from the backend server
|
||||||
|
*
|
||||||
|
* If accessed from outside a provider, the values will be:
|
||||||
|
* - `server`: `null`
|
||||||
|
* - `setServer`: a function showing an error in the console
|
||||||
|
* - `fetchData`: another function showing an error in the console
|
||||||
|
*
|
||||||
|
* @type {React.Context}
|
||||||
|
*/
|
||||||
|
const ContextServer = createContext({
|
||||||
|
server: null,
|
||||||
|
setServer: () => console.error("Trying to setServer while outside a ContextServer.Provider!"),
|
||||||
|
fetchData: () => console.error("Trying to fetchData while outside a ContextServer.Provider!"),
|
||||||
|
})
|
||||||
|
export default ContextServer
|
|
@ -2,11 +2,18 @@ import {createContext} from "react";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A context containing a list with the currently selected theme and the function to set a different one.
|
* A context containing an object with the following elements:
|
||||||
|
* - `theme` - A string containing the name of the current theme.
|
||||||
|
* - `setTheme` - A function that allows changing the `theme`.
|
||||||
*
|
*
|
||||||
* The function can be `undefined` if run outside a provider.
|
* If trying to access the context from outside a provider, the theme will be `ThemeDark`, and the function will display
|
||||||
|
* an error in the console.
|
||||||
*
|
*
|
||||||
* @type {React.Context}
|
* @type {React.Context}
|
||||||
*/
|
*/
|
||||||
const ContextTheme = createContext(["ThemeDark", undefined])
|
const ContextTheme = createContext({
|
||||||
|
isSet: false,
|
||||||
|
theme: "ThemeDark",
|
||||||
|
setTheme: () => console.error("Trying to setTheme while outside a ContextTheme.Provider!")
|
||||||
|
})
|
||||||
export default ContextTheme
|
export default ContextTheme
|
||||||
|
|
26
code/frontend/src/contexts/ContextUser.js
Normal file
26
code/frontend/src/contexts/ContextUser.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import {createContext} from "react";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A context containing an object with the following values:
|
||||||
|
* - `user`: an object containing data about the currently logged in user
|
||||||
|
* - `login`: a function accepting `email, password` as parameters which tries to login the user
|
||||||
|
* - `logout`: a function accepting no parameters which logs the user out
|
||||||
|
* - `fetchDataAuth`: a function with the same API as `fetchData` which fetches data from the server authenticating
|
||||||
|
* as the logged in user.
|
||||||
|
*
|
||||||
|
* If accessed from outside a provider, the values will be:
|
||||||
|
* - `user`: `null`
|
||||||
|
* - `login`: a function showing an error in the console
|
||||||
|
* - `logout`: another function showing an error in the console
|
||||||
|
* - `fetchDataAuth`: yet another function showing an error in the console
|
||||||
|
*
|
||||||
|
* @type {React.Context}
|
||||||
|
*/
|
||||||
|
const ContextUser = createContext({
|
||||||
|
user: null,
|
||||||
|
login: () => console.error("Trying to login while outside a ContextUser.Provider!"),
|
||||||
|
logout: () => console.error("Trying to logout while outside a ContextUser.Provider!"),
|
||||||
|
fetchDataAuth: () => console.error("Trying to fetchDataAuth while outside a ContextUser.Provider!"),
|
||||||
|
})
|
||||||
|
export default ContextUser
|
|
@ -1,82 +0,0 @@
|
||||||
import {useState} from "react"
|
|
||||||
import useLocalStorageState from "./useLocalStorageState"
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to place at the root to generate the contents of {@link ContextLogin}.
|
|
||||||
*/
|
|
||||||
export default function useSavedLogin() {
|
|
||||||
const [state, setState] = useLocalStorageState("login", null)
|
|
||||||
const [working, setWorking] = useState(false)
|
|
||||||
const [error, setError] = useState(null)
|
|
||||||
|
|
||||||
const login = async (server, email, password) => {
|
|
||||||
setWorking(true)
|
|
||||||
try {
|
|
||||||
console.debug("Contacting server to login...")
|
|
||||||
const response = await fetch(`${server}/api/login`, {
|
|
||||||
method: "POST",
|
|
||||||
cache: "no-cache",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
"email": email,
|
|
||||||
"password": password,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
console.debug("Decoding server response...")
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
console.debug("Ensuring the request was a success...")
|
|
||||||
if(data["result"] !== "success") {
|
|
||||||
// noinspection ExceptionCaughtLocallyJS
|
|
||||||
throw new Error(data["msg"])
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug("Storing login state...")
|
|
||||||
setState({
|
|
||||||
server: server,
|
|
||||||
email: data["user"]["email"],
|
|
||||||
isAdmin: data["user"]["isAdmin"],
|
|
||||||
username: data["user"]["username"],
|
|
||||||
token: data["access_token"],
|
|
||||||
})
|
|
||||||
|
|
||||||
console.debug("Clearing error...")
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
console.info("Login successful!")
|
|
||||||
} catch(e) {
|
|
||||||
console.error(`Caught error while trying to login: ${e}`)
|
|
||||||
setError(e)
|
|
||||||
} finally {
|
|
||||||
setWorking(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
console.debug("Clearing login state...")
|
|
||||||
setState(null)
|
|
||||||
console.debug("Cleared login state!")
|
|
||||||
|
|
||||||
console.info("Logout successful!")
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetch_unauth = async (path, init) => {
|
|
||||||
return await fetch(`${state["server"]}${path}`, init)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetch_auth = async (path, init) => {
|
|
||||||
if(typeof init["headers"] != "object") {
|
|
||||||
init["headers"] = {}
|
|
||||||
}
|
|
||||||
if(state) {
|
|
||||||
init["Authorization"] = `Bearer ${state["token"]}`
|
|
||||||
}
|
|
||||||
return await fetch_unauth(path, init)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {state, working, error, login, logout, fetch_unauth, fetch_auth}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import useLocalStorageState from "./useLocalStorageState"
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook with the same API as {@link React.useState} which stores the user's current theme setting, and syncs it to the
|
|
||||||
* browser's {@link localStorage}.
|
|
||||||
*
|
|
||||||
* @returns {[string, function]}
|
|
||||||
*/
|
|
||||||
export default function useSavedTheme() {
|
|
||||||
return useLocalStorageState("theme", "ThemeDark")
|
|
||||||
}
|
|
|
@ -24,6 +24,10 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: var(--font-title);
|
font-family: var(--font-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.ThemeDark {
|
.ThemeDark {
|
||||||
--bg-primary: #3B7097;
|
--bg-primary: #3B7097;
|
||||||
--fg-primary: #FFFFFF;
|
--fg-primary: #FFFFFF;
|
||||||
|
|
|
@ -8,8 +8,8 @@ import InputWithIcon from "../components/InputWithIcon"
|
||||||
import { faFolder, faPlus } from "@fortawesome/free-solid-svg-icons"
|
import { faFolder, faPlus } from "@fortawesome/free-solid-svg-icons"
|
||||||
import Radio from "../components/Radio"
|
import Radio from "../components/Radio"
|
||||||
import Button from "../components/Button"
|
import Button from "../components/Button"
|
||||||
import LabelledForm from "../components/LabelledForm"
|
import FormLabelled from "../components/FormLabelled"
|
||||||
import Label from "../components/Label"
|
import FormLabel from "../components/FormLabel"
|
||||||
|
|
||||||
|
|
||||||
export default function PageDashboard({ children, className, ...props }) {
|
export default function PageDashboard({ children, className, ...props }) {
|
||||||
|
@ -34,11 +34,11 @@ export default function PageDashboard({ children, className, ...props }) {
|
||||||
🚧 Not implemented.
|
🚧 Not implemented.
|
||||||
</BoxFull>
|
</BoxFull>
|
||||||
<BoxFull className={Style.CreateDialog} header={"Create repository"}>
|
<BoxFull className={Style.CreateDialog} header={"Create repository"}>
|
||||||
<LabelledForm>
|
<FormLabelled>
|
||||||
<Label htmlFor={"repo-name"} text={"Repository name"}>
|
<FormLabel htmlFor={"repo-name"} text={"Repository name"}>
|
||||||
<InputWithIcon id={"repo-name"} icon={faFolder}/>
|
<InputWithIcon id={"repo-name"} icon={faFolder}/>
|
||||||
</Label>
|
</FormLabel>
|
||||||
<Label htmlFor={"filter-mode"} text={"Add tweets if they satisfy"}>
|
<FormLabel htmlFor={"filter-mode"} text={"Add tweets if they satisfy"}>
|
||||||
<label>
|
<label>
|
||||||
<Radio name={"filter-mode"} value={"or"}/> At least one filter
|
<Radio name={"filter-mode"} value={"or"}/> At least one filter
|
||||||
</label>
|
</label>
|
||||||
|
@ -46,11 +46,11 @@ export default function PageDashboard({ children, className, ...props }) {
|
||||||
<label>
|
<label>
|
||||||
<Radio name={"filter-mode"} value={"and"}/> Every filter
|
<Radio name={"filter-mode"} value={"and"}/> Every filter
|
||||||
</label>
|
</label>
|
||||||
</Label>
|
</FormLabel>
|
||||||
<Button style={{"gridColumn": "1 / 3"}} icon={faPlus} color={"Green"}>
|
<Button style={{"gridColumn": "1 / 3"}} icon={faPlus} color={"Green"}>
|
||||||
Create repository
|
Create repository
|
||||||
</Button>
|
</Button>
|
||||||
</LabelledForm>
|
</FormLabelled>
|
||||||
</BoxFull>
|
</BoxFull>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,71 +2,17 @@ import React, { useContext, useState } from "react"
|
||||||
import Style from "./PageLogin.module.css"
|
import Style from "./PageLogin.module.css"
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import BoxFull from "../components/BoxFull"
|
import BoxFull from "../components/BoxFull"
|
||||||
import LabelledForm from "../components/LabelledForm"
|
import ContextUser from "../contexts/ContextUser"
|
||||||
import Label from "../components/Label"
|
|
||||||
import InputWithIcon from "../components/InputWithIcon"
|
|
||||||
import { faArrowRight, faEnvelope, faGlobe, faKey } from "@fortawesome/free-solid-svg-icons"
|
|
||||||
import Button from "../components/Button"
|
|
||||||
import ContextLogin from "../contexts/ContextLogin"
|
|
||||||
import { useHistory } from "react-router"
|
import { useHistory } from "react-router"
|
||||||
|
import BoxSetServer from "../components/BoxSetServer"
|
||||||
|
import BoxLogin from "../components/BoxLogin"
|
||||||
|
|
||||||
|
|
||||||
export default function PageLogin({ className, ...props }) {
|
export default function PageLogin({ className, ...props }) {
|
||||||
// TODO: Change these presets before production
|
|
||||||
const [server, setServer] = useState("http://localhost:5000")
|
|
||||||
const [email, setEmail] = useState("admin@admin.com")
|
|
||||||
const [password, setPassword] = useState("password")
|
|
||||||
const {login, working, error} = useContext(ContextLogin)
|
|
||||||
const history = useHistory()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(Style.PageLogin, className)} {...props}>
|
<div className={classNames(Style.PageLogin, className)} {...props}>
|
||||||
<BoxFull header={"Login"}>
|
<BoxSetServer/>
|
||||||
<LabelledForm>
|
<BoxLogin/>
|
||||||
<Label text={"Server"} htmlFor={"login-server"}>
|
|
||||||
<InputWithIcon
|
|
||||||
id={"login-server"}
|
|
||||||
name={"login-server"}
|
|
||||||
value={server}
|
|
||||||
onChange={e => setServer(e.target.value)}
|
|
||||||
type={"url"}
|
|
||||||
icon={faGlobe}
|
|
||||||
/>
|
|
||||||
</Label>
|
|
||||||
<Label text={"Email"} htmlFor={"login-email"}>
|
|
||||||
<InputWithIcon
|
|
||||||
id={"login-email"}
|
|
||||||
name={"login-email"}
|
|
||||||
value={email}
|
|
||||||
onChange={e => setEmail(e.target.value)}
|
|
||||||
type={"email"}
|
|
||||||
icon={faEnvelope}
|
|
||||||
/>
|
|
||||||
</Label>
|
|
||||||
<Label text={"Password"} htmlFor={"login-password"}>
|
|
||||||
<InputWithIcon
|
|
||||||
id={"login-password"}
|
|
||||||
name={"login-password"}
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
type={"password"}
|
|
||||||
icon={faKey}
|
|
||||||
/>
|
|
||||||
</Label>
|
|
||||||
<Button
|
|
||||||
style={{"gridColumn": "1 / 3"}}
|
|
||||||
onClick={async e => {
|
|
||||||
await login(server, email, password)
|
|
||||||
history.push("/dashboard")
|
|
||||||
}}
|
|
||||||
icon={faArrowRight}
|
|
||||||
color={error ? "Green" : "Red"}
|
|
||||||
disabled={working}
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</LabelledForm>
|
|
||||||
</BoxFull>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useContext } from "react"
|
import React, { useContext } from "react"
|
||||||
import ContextLogin from "../contexts/ContextLogin"
|
import ContextUser from "../contexts/ContextUser"
|
||||||
import { Redirect } from "react-router"
|
import { Redirect } from "react-router"
|
||||||
|
|
||||||
|
|
||||||
export default function PageRoot() {
|
export default function PageRoot() {
|
||||||
const {state} = useContext(ContextLogin)
|
const {state} = useContext(ContextUser)
|
||||||
|
|
||||||
if(!state) {
|
if(!state) {
|
||||||
return <Redirect to={"/login"}/>
|
return <Redirect to={"/login"}/>
|
||||||
|
|
Loading…
Reference in a new issue