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$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/nest_backend" isTestSource="false" />
|
||||
</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" />
|
||||
</component>
|
||||
</module>
|
|
@ -1,7 +1,4 @@
|
|||
import classNames from "classnames"
|
||||
import Style from "./App.module.css"
|
||||
import Layout from "./components/Layout"
|
||||
import ContextTheme from "./contexts/ContextTheme"
|
||||
import { BrowserRouter } from "react-router-dom"
|
||||
import { Route, Switch } from "react-router"
|
||||
import PageDashboard from "./routes/PageDashboard"
|
||||
|
@ -9,11 +6,11 @@ import PageRepositories from "./routes/PageRepositories"
|
|||
import PageAlerts from "./routes/PageAlerts"
|
||||
import PageSettings from "./routes/PageSettings"
|
||||
import PageSandbox from "./routes/PageSandbox"
|
||||
import useSavedTheme from "./hooks/useSavedTheme"
|
||||
import PageLogin from "./routes/PageLogin"
|
||||
import useSavedLogin from "./hooks/useSavedLogin"
|
||||
import ContextLogin from "./contexts/ContextLogin"
|
||||
import PageRoot from "./routes/PageRoot"
|
||||
import GlobalTheme from "./components/GlobalTheme"
|
||||
import GlobalServer from "./components/GlobalServer"
|
||||
import GlobalUser from "./components/GlobalUser"
|
||||
|
||||
|
||||
/**
|
||||
|
@ -23,15 +20,12 @@ import PageRoot from "./routes/PageRoot"
|
|||
* @constructor
|
||||
*/
|
||||
export default function App() {
|
||||
const theme = useSavedTheme();
|
||||
const login = useSavedLogin();
|
||||
|
||||
return (
|
||||
<ContextTheme.Provider value={theme}>
|
||||
<ContextLogin.Provider value={login}>
|
||||
<GlobalServer>
|
||||
<GlobalUser>
|
||||
<GlobalTheme>
|
||||
<BrowserRouter>
|
||||
|
||||
<div className={classNames(Style.App, theme)}>
|
||||
<Layout>
|
||||
<Switch>
|
||||
<Route path={"/login"} exact={true}>
|
||||
|
@ -57,10 +51,10 @@ export default function App() {
|
|||
</Route>
|
||||
</Switch>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
</BrowserRouter>
|
||||
</ContextLogin.Provider>
|
||||
</ContextTheme.Provider>
|
||||
</GlobalTheme>
|
||||
</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 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 Button from "./Button"
|
||||
import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons"
|
||||
import ContextLogin from "../contexts/ContextLogin"
|
||||
import ContextUser from "../contexts/ContextUser"
|
||||
import { useHistory } from "react-router"
|
||||
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 }) {
|
||||
const {logout} = useContext(ContextLogin)
|
||||
const {logout} = useContext(ContextUser)
|
||||
const history = useHistory()
|
||||
|
||||
return (
|
||||
<BoxFull header={"Logged in"} {...props}>
|
||||
<div className={Style.BoxLoggedInContents}>
|
||||
<div>
|
||||
You are currently logged in as <LoggedInUser/>.
|
||||
You are currently logged in at <CurrentServer/> as <LoggedInUser/>.
|
||||
</div>
|
||||
<div>
|
||||
<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 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 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.
|
||||
*
|
||||
* @param children - The labelled element.
|
||||
|
@ -12,7 +13,7 @@ import Style from "./Label.module.css"
|
|||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export default function Label({ children, text, htmlFor }) {
|
||||
export default function FormLabel({ children, text, htmlFor }) {
|
||||
return (
|
||||
<Fragment>
|
||||
<label htmlFor={htmlFor} className={Style.LabelText} >
|
|
@ -1,20 +1,17 @@
|
|||
import React from "react"
|
||||
import Style from "./LabelledForm.module.css"
|
||||
import Style from "./FormLabelled.module.css"
|
||||
import classNames from "classnames"
|
||||
|
||||
|
||||
/**
|
||||
* 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}
|
||||
* @constructor
|
||||
*/
|
||||
export default function LabelledForm({ children, className, ...props }) {
|
||||
export default function FormLabelled({ children, className, ...props }) {
|
||||
return (
|
||||
<form className={classNames(Style.LabelledForm, className)} {...props}>
|
||||
{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 classNames from "classnames"
|
||||
import Sidebar from "./Sidebar"
|
||||
import ContextTheme from "../contexts/ContextTheme"
|
||||
|
||||
|
||||
/**
|
||||
|
@ -14,12 +15,14 @@ import Sidebar from "./Sidebar"
|
|||
* @constructor
|
||||
*/
|
||||
export default function Layout({ children, className, ...props }) {
|
||||
const { theme } = useContext(ContextTheme)
|
||||
|
||||
return (
|
||||
<div className={classNames(Style.Layout, className)} {...props}>
|
||||
<div className={classNames(theme, Style.Layout, className)} {...props}>
|
||||
<Sidebar className={Style.LayoutSidebar}/>
|
||||
<div className={Style.LayoutContent}>
|
||||
<main className={Style.LayoutContent}>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
.Layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
padding: 16px;
|
||||
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--fg-primary);
|
||||
|
||||
display: grid;
|
||||
|
||||
grid-template-areas:
|
||||
"a b"
|
||||
;
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
import React, { useContext } from "react"
|
||||
import Style from "./LoggedInUser.module.css"
|
||||
import classNames from "classnames"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
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 (
|
||||
<i className={classNames(Style.LoggedInUser, className)} {...props}>
|
||||
<i {...props}>
|
||||
Not logged in
|
||||
</i>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<b className={classNames(Style.LoggedInUser, className)} {...props}>
|
||||
<FontAwesomeIcon icon={faUser}/> {state.username}
|
||||
<b {...props}>
|
||||
<FontAwesomeIcon icon={faUser}/> {user.username}
|
||||
</b>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.LoggedInUser {
|
||||
|
||||
}
|
|
@ -19,7 +19,7 @@ import classNames from "classnames"
|
|||
export default function Logo({ className, ...props }) {
|
||||
// I have no idea why IntelliJ is complaining about this line
|
||||
// It's perfectly fine!
|
||||
const [theme, ] = useContext(ContextTheme)
|
||||
const {theme} = useContext(ContextTheme)
|
||||
|
||||
let logo;
|
||||
if(theme === "ThemeDark") {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react"
|
||||
import Style from "./RepositorySummary.module.css"
|
||||
import Style from "./RepositorySummaryBase.module.css"
|
||||
import classNames from "classnames"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import Button from "./Button"
|
||||
|
|
|
@ -27,10 +27,7 @@
|
|||
}
|
||||
|
||||
.IconContainer {
|
||||
margin-top: 5px;
|
||||
margin-right: 15px;
|
||||
margin-left: 5px;
|
||||
margin-bottom: 5px;
|
||||
margin: 5px 15px 5px 5px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50px;
|
|
@ -12,7 +12,7 @@ import ContextTheme from "../contexts/ContextTheme"
|
|||
* @constructor
|
||||
*/
|
||||
export default function SelectTheme({ ...props }) {
|
||||
const [theme, setTheme] = useContext(ContextTheme);
|
||||
const {theme, setTheme} = useContext(ContextTheme);
|
||||
|
||||
return (
|
||||
<Select value={theme} onChange={e => setTheme(e.target.value)} {...props}>
|
||||
|
|
|
@ -4,7 +4,7 @@ import classNames from "classnames"
|
|||
import Logo from "./Logo"
|
||||
import ButtonSidebar from "./ButtonSidebar"
|
||||
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
|
||||
*/
|
||||
export default function Sidebar({ className, ...props }) {
|
||||
const {state} = useContext(ContextLogin)
|
||||
const {user} = useContext(ContextUser)
|
||||
|
||||
return (
|
||||
<div className={classNames(Style.Sidebar, className)} {...props}>
|
||||
<aside className={classNames(Style.Sidebar, className)} {...props}>
|
||||
<Logo/>
|
||||
{
|
||||
state ?
|
||||
user ?
|
||||
<Fragment>
|
||||
<ButtonSidebar to={"/dashboard"} icon={faHome}>Dashboard</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>
|
||||
: 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}
|
||||
*/
|
||||
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
|
||||
|
|
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);
|
||||
}
|
||||
|
||||
*[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ThemeDark {
|
||||
--bg-primary: #3B7097;
|
||||
--fg-primary: #FFFFFF;
|
||||
|
|
|
@ -8,8 +8,8 @@ import InputWithIcon from "../components/InputWithIcon"
|
|||
import { faFolder, faPlus } from "@fortawesome/free-solid-svg-icons"
|
||||
import Radio from "../components/Radio"
|
||||
import Button from "../components/Button"
|
||||
import LabelledForm from "../components/LabelledForm"
|
||||
import Label from "../components/Label"
|
||||
import FormLabelled from "../components/FormLabelled"
|
||||
import FormLabel from "../components/FormLabel"
|
||||
|
||||
|
||||
export default function PageDashboard({ children, className, ...props }) {
|
||||
|
@ -34,11 +34,11 @@ export default function PageDashboard({ children, className, ...props }) {
|
|||
🚧 Not implemented.
|
||||
</BoxFull>
|
||||
<BoxFull className={Style.CreateDialog} header={"Create repository"}>
|
||||
<LabelledForm>
|
||||
<Label htmlFor={"repo-name"} text={"Repository name"}>
|
||||
<FormLabelled>
|
||||
<FormLabel htmlFor={"repo-name"} text={"Repository name"}>
|
||||
<InputWithIcon id={"repo-name"} icon={faFolder}/>
|
||||
</Label>
|
||||
<Label htmlFor={"filter-mode"} text={"Add tweets if they satisfy"}>
|
||||
</FormLabel>
|
||||
<FormLabel htmlFor={"filter-mode"} text={"Add tweets if they satisfy"}>
|
||||
<label>
|
||||
<Radio name={"filter-mode"} value={"or"}/> At least one filter
|
||||
</label>
|
||||
|
@ -46,11 +46,11 @@ export default function PageDashboard({ children, className, ...props }) {
|
|||
<label>
|
||||
<Radio name={"filter-mode"} value={"and"}/> Every filter
|
||||
</label>
|
||||
</Label>
|
||||
</FormLabel>
|
||||
<Button style={{"gridColumn": "1 / 3"}} icon={faPlus} color={"Green"}>
|
||||
Create repository
|
||||
</Button>
|
||||
</LabelledForm>
|
||||
</FormLabelled>
|
||||
</BoxFull>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -2,71 +2,17 @@ import React, { useContext, useState } from "react"
|
|||
import Style from "./PageLogin.module.css"
|
||||
import classNames from "classnames"
|
||||
import BoxFull from "../components/BoxFull"
|
||||
import LabelledForm from "../components/LabelledForm"
|
||||
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 ContextUser from "../contexts/ContextUser"
|
||||
import { useHistory } from "react-router"
|
||||
import BoxSetServer from "../components/BoxSetServer"
|
||||
import BoxLogin from "../components/BoxLogin"
|
||||
|
||||
|
||||
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 (
|
||||
<div className={classNames(Style.PageLogin, className)} {...props}>
|
||||
<BoxFull header={"Login"}>
|
||||
<LabelledForm>
|
||||
<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>
|
||||
<BoxSetServer/>
|
||||
<BoxLogin/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useContext } from "react"
|
||||
import ContextLogin from "../contexts/ContextLogin"
|
||||
import ContextUser from "../contexts/ContextUser"
|
||||
import { Redirect } from "react-router"
|
||||
|
||||
|
||||
export default function PageRoot() {
|
||||
const {state} = useContext(ContextLogin)
|
||||
const {state} = useContext(ContextUser)
|
||||
|
||||
if(!state) {
|
||||
return <Redirect to={"/login"}/>
|
||||
|
|
Loading…
Reference in a new issue