1
Fork 0
mirror of https://github.com/pds-nest/nest.git synced 2024-11-22 13:04:19 +00:00

Merge remote-tracking branch 'origin/main' into main

This commit is contained in:
stefanogoldoni 2021-04-26 23:18:33 +02:00
commit a3f5139b76
14 changed files with 317 additions and 89 deletions

View file

@ -37,9 +37,9 @@ app.add_url_rule("/doa", view_func=page_doa, methods=["GET", "POST"])
app.add_url_rule("/api/login", view_func=page_login, methods=["POST"]) app.add_url_rule("/api/login", view_func=page_login, methods=["POST"])
app.add_url_rule("/api/user/create", view_func=page_user_create, methods=["POST"]) app.add_url_rule("/api/user/create", view_func=page_user_create, methods=["POST"])
app.add_url_rule("/api/user/remove", view_func=page_user_delete, methods=["POST"]) app.add_url_rule("/api/user/remove", view_func=page_user_delete, methods=["POST"])
app.add_url_rule("/api/repository/list", view_func=page_repository_list, methods=["POST"]) app.add_url_rule("/api/repository/list", view_func=page_repository_list, methods=["GET"])
app.add_url_rule("/api/repository/create", view_func=page_repository_create, methods=["POST"]) app.add_url_rule("/api/repository/create", view_func=page_repository_create, methods=["POST"])
app.add_url_rule("/api/repository/edit", view_func=page_repository_edit, methods=["POST"]) app.add_url_rule("/api/repository/edit", view_func=page_repository_edit, methods=["PUT"])
app.add_url_rule("/api/repository/add_condition", view_func=page_repository_add_condition, methods=["POST"]) app.add_url_rule("/api/repository/add_condition", view_func=page_repository_add_condition, methods=["POST"])
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -10,18 +10,20 @@ from flask_cors import cross_origin
def page_repository_list(): def page_repository_list():
""" """
API call that returns the list of repositories. API call that returns the list of repositories.
:parameter onlyActive: if present, only active repos are provided :parameter onlyActive: if present, only active repos are provided
:parameter onlyDead: if present, only dead repos are provided :parameter onlyDead: if present, only dead repos are provided
:returns: a JSON-formatted string that contains under the "content" field the list of repositories that belong to :returns: a JSON-formatted string that contains under the "content" field the list of repositories that belong to
the user ("owner") and a list of repositories that he can spectate ("spectator"). the user ("owner") and a list of repositories that he can spectate ("spectator").
""" """
user = find_user(get_jwt_identity()) user = find_user(get_jwt_identity())
owner = Repository.query.filter_by(owner_id=user.email) owner = Repository.query.filter_by(owner_id=user.email)
spectator = Authorization.query.filter_by(email=user.email).join(Repository) spectator = Authorization.query.filter_by(email=user.email).join(Repository)
if request.json.get("onlyActive"): if request.args.get("onlyActive"):
owner = owner.filter_by(isActive=True) owner = owner.filter_by(isActive=True)
spectator = spectator.filter(Repository.isActive == True) spectator = spectator.filter(Repository.isActive == True)
elif request.json.get("onlyDead"): elif request.args.get("onlyDead"):
owner = owner.filter_by(isActive=False) owner = owner.filter_by(isActive=False)
spectator = spectator.filter(Repository.isActive == False) spectator = spectator.filter(Repository.isActive == False)
owner = owner.all() owner = owner.all()

View file

@ -11,6 +11,7 @@ import PageRoot from "./routes/PageRoot"
import GlobalTheme from "./components/GlobalTheme" import GlobalTheme from "./components/GlobalTheme"
import GlobalServer from "./components/GlobalServer" import GlobalServer from "./components/GlobalServer"
import GlobalUser from "./components/GlobalUser" import GlobalUser from "./components/GlobalUser"
import PageSwitcher from "./PageSwitcher"
/** /**
@ -25,33 +26,9 @@ export default function App() {
<GlobalUser> <GlobalUser>
<GlobalTheme> <GlobalTheme>
<BrowserRouter> <BrowserRouter>
<Layout> <Layout>
<Switch> <PageSwitcher/>
<Route path={"/login"} exact={true}>
<PageLogin/>
</Route>
<Route path={"/repositories"} exact={true}>
<PageRepositories/>
</Route>
<Route path={"/alerts"} exact={true}>
<PageAlerts/>
</Route>
<Route path={"/settings"} exact={true}>
<PageSettings/>
</Route>
<Route path={"/sandbox"} exact={true}>
<PageSandbox/>
</Route>
<Route path={"/dashboard"} exact={true}>
<PageDashboard/>
</Route>
<Route path={"/"}>
<PageRoot/>
</Route>
</Switch>
</Layout> </Layout>
</BrowserRouter> </BrowserRouter>
</GlobalTheme> </GlobalTheme>
</GlobalUser> </GlobalUser>

View file

@ -0,0 +1,38 @@
import React from "react"
import { Route, Switch } from "react-router"
import PageLogin from "./routes/PageLogin"
import PageRepositories from "./routes/PageRepositories"
import PageAlerts from "./routes/PageAlerts"
import PageSettings from "./routes/PageSettings"
import PageSandbox from "./routes/PageSandbox"
import PageDashboard from "./routes/PageDashboard"
import PageRoot from "./routes/PageRoot"
export default function PageSwitcher({ ...props }) {
return (
<Switch {...props}>
<Route path={"/login"} exact={true}>
<PageLogin/>
</Route>
<Route path={"/repositories"} exact={true}>
<PageRepositories/>
</Route>
<Route path={"/alerts"} exact={true}>
<PageAlerts/>
</Route>
<Route path={"/settings"} exact={true}>
<PageSettings/>
</Route>
<Route path={"/sandbox"} exact={true}>
<PageSandbox/>
</Route>
<Route path={"/dashboard"} exact={true}>
<PageDashboard/>
</Route>
<Route path={"/"}>
<PageRoot/>
</Route>
</Switch>
)
}

View file

@ -0,0 +1,38 @@
import React, { useContext } from "react"
import BoxFull from "./BoxFull"
import ContextUser from "../contexts/ContextUser"
import useData from "../hooks/useData"
import RepositorySummaryBase from "./RepositorySummaryBase"
import Loading from "./Loading"
import BoxAlert from "./BoxAlert"
export default function BoxRepositoriesActive({ ...props }) {
const {fetchDataAuth} = useContext(ContextUser)
const {data, started, loading, error} = useData(fetchDataAuth, "GET", "/api/repository/list", {
"onlyAlive": true,
})
let contents;
if(!started || loading) {
contents = <Loading/>
}
else if(error) {
contents = <BoxAlert color={"Red"}>{error.toString()}</BoxAlert>
}
else {
let repositories = [...data["owner"], ...data["spectator"]]
if(repositories.length > 0) {
contents = repositories.map(repo => <RepositorySummaryBase {...repo}/>)
}
else {
contents = <i>There's nothing here.</i>
}
}
return (
<BoxFull header={"Your active repositories"} {...props}>
{contents}
</BoxFull>
)
}

View file

@ -0,0 +1,38 @@
import React, { useContext } from "react"
import BoxFull from "./BoxFull"
import ContextUser from "../contexts/ContextUser"
import useData from "../hooks/useData"
import RepositorySummaryBase from "./RepositorySummaryBase"
import Loading from "./Loading"
import BoxAlert from "./BoxAlert"
export default function BoxRepositoriesArchived({ ...props }) {
const {fetchDataAuth} = useContext(ContextUser)
const {data, started, loading, error} = useData(fetchDataAuth, "GET", "/api/repository/list", {
"onlyDead": true,
})
let contents;
if(!started || loading) {
contents = <Loading/>
}
else if(error) {
contents = <BoxAlert color={"Red"}>{error.toString()}</BoxAlert>
}
else {
let repositories = [...data["owner"], ...data["spectator"]]
if(repositories.length > 0) {
contents = repositories.map(repo => <RepositorySummaryBase {...repo}/>)
}
else {
contents = <i>There's nothing here.</i>
}
}
return (
<BoxFull header={"Your archived repositories"} {...props}>
{contents}
</BoxFull>
)
}

View file

@ -1,6 +1,7 @@
import React from "react" import React, { useCallback } from "react"
import useLocalStorageState from "../hooks/useLocalStorageState" import useLocalStorageState from "../hooks/useLocalStorageState"
import ContextServer from "../contexts/ContextServer" import ContextServer from "../contexts/ContextServer"
import isString from "is-string"
/** /**
@ -24,7 +25,7 @@ export default function GlobalServer({ children }) {
* @param init - Additional arguments to pass to the `init` parameter of {@link fetch}. * @param init - Additional arguments to pass to the `init` parameter of {@link fetch}.
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
const fetchData = async (method, path, body, init) => { const fetchData = useCallback(async (method, path, body, init) => {
if(!server) { if(!server) {
throw new Error(`Invalid server: ${server}`) throw new Error(`Invalid server: ${server}`)
} }
@ -32,14 +33,31 @@ export default function GlobalServer({ children }) {
if(!init) { if(!init) {
init = {} init = {}
} }
if(!init["headers"]) { if(!init["headers"]) {
init["headers"] = {} init["headers"] = {}
} }
init["headers"]["Content-Type"] = "application/json" init["headers"]["Content-Type"] = "application/json"
if(method.toUpperCase() === "GET" || method.toUpperCase() === "HEAD") {
let usp = new URLSearchParams()
for(const key in body) {
if(!body.hasOwnProperty(key)) {
return
}
const value = body[key]
if(!isString(value)) {
usp.set(key, value)
}
}
path += `?${usp.toString()}`
}
else {
init["body"] = JSON.stringify(body)
}
const response = await fetch(`${server}${path}`, { const response = await fetch(`${server}${path}`, {
method: method, method: method,
body: JSON.stringify(body),
...init, ...init,
}) })
@ -50,11 +68,11 @@ export default function GlobalServer({ children }) {
const json = await response.json() const json = await response.json()
if(json["result"] !== "success") { if(json["result"] !== "success") {
throw new Error(`Request failed: ${json["msg"]}`) throw new Error(json["msg"])
} }
return json["data"] return json["data"]
} }, [server])
return ( return (
<ContextServer.Provider value={{server, setServer, fetchData}}> <ContextServer.Provider value={{server, setServer, fetchData}}>

View file

@ -1,4 +1,4 @@
import React, { useContext } from "react" import React, { useCallback, useContext } from "react"
import useLocalStorageState from "../hooks/useLocalStorageState" import useLocalStorageState from "../hooks/useLocalStorageState"
import ContextServer from "../contexts/ContextServer" import ContextServer from "../contexts/ContextServer"
import ContextUser from "../contexts/ContextUser" import ContextUser from "../contexts/ContextUser"
@ -26,7 +26,7 @@ export default function GlobalUser({ children }) {
* @param init - Additional arguments to pass to the `init` parameter of {@link fetch}. * @param init - Additional arguments to pass to the `init` parameter of {@link fetch}.
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
const fetchDataAuth = async (method, path, body, init) => { const fetchDataAuth = useCallback(async (method, path, body, init) => {
if(!user) { if(!user) {
throw new Error("Not logged in") throw new Error("Not logged in")
} }
@ -39,8 +39,8 @@ export default function GlobalUser({ children }) {
} }
init["headers"]["Authorization"] = `Bearer ${user["token"]}` init["headers"]["Authorization"] = `Bearer ${user["token"]}`
return await fetchData(path, init) return await fetchData(method, path, body, init)
} }, [fetchData, user])
/** /**
* Try to login to the active server with the passed credentials. * Try to login to the active server with the passed credentials.
@ -49,7 +49,7 @@ export default function GlobalUser({ children }) {
* @param password - The user's password. * @param password - The user's password.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const login = async (email, password) => { const login = useCallback(async (email, password) => {
console.debug("Contacting server to login...") console.debug("Contacting server to login...")
const data = await fetchData("POST", `/api/login`, { const data = await fetchData("POST", `/api/login`, {
"email": email, "email": email,
@ -65,18 +65,18 @@ export default function GlobalUser({ children }) {
}) })
console.info("Login successful!") console.info("Login successful!")
} }, [fetchData, setUser])
/** /**
* Logout from the currently active server. * Logout from the currently active server.
*/ */
const logout = () => { const logout = useCallback(() => {
console.debug("Clearing login state...") console.debug("Clearing login state...")
setUser(null) setUser(null)
console.debug("Cleared login state!") console.debug("Cleared login state!")
console.info("Logout successful!") console.info("Logout successful!")
} }, [setUser])
return ( return (
<ContextUser.Provider value={{user, login, logout, fetchDataAuth}}> <ContextUser.Provider value={{user, login, logout, fetchDataAuth}}>

View file

@ -0,0 +1,12 @@
import React from "react"
import { faSpinner } from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
export default function Loading({ ...props }) {
return (
<div {...props}>
<FontAwesomeIcon icon={faSpinner} pulse={true}/> Loading...
</div>
)
}

View file

@ -0,0 +1,17 @@
/* eslint-disable */
import { useEffect } from "react"
/**
* {@link useEffect}, but with an async effect.
*
* @warning Breaks `react-hooks/exaustive-deps`.
*
* @param effect - The async effect.
* @param deps - The dependencies of the hook.
*/
export default function useAsyncEffect(effect, deps) {
useEffect(() => {
effect()
}, [effect, ...deps])
}

View file

@ -0,0 +1,69 @@
import { useCallback, useContext, useEffect, useState } from "react"
import ContextUser from "../contexts/ContextUser"
/**
* Hook which fetches data from the backend on the first render of a component.
*
* @param fetchData - The function to use when fetching data.
* @param method - The HTTP method to use.
* @param path - The HTTP path to fetch the data at.
* @param body - The body of the HTTP request (it will be JSONified before being sent).
* @param init - Additional `init` parameters to pass to `fetch`.
* @returns {{data: *, refresh: function, error: Error}}
*/
export default function useData(fetchData, method, path, body, init) {
const [error, setError] = useState(null)
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const started = (loading || data || error)
/**
* Load data from the API.
*/
const load = useCallback(
async () => {
console.debug(`Trying to ${method} ${path}...`)
setLoading(true)
try {
const _data = await fetchData(method, path, body, init)
setData(_data)
} catch(e) {
setError(e)
} finally {
setLoading(false)
}
},
[fetchData, method, path, body, init, setData, setError, setLoading]
)
/**
* Invalidate the data loaded from the API and try to load it again.
*/
const refresh = useCallback(
async () => {
console.debug("Clearing data...")
setData(null)
console.debug("Clearing error...")
setError(null)
await load()
},
[load, setData, setError]
)
useEffect(
() => {
if(!started) {
// noinspection JSIgnoredPromiseFromCall
load()
}
},
[load, started]
)
return {data, error, loading, started, refresh}
}

View file

@ -1,4 +1,4 @@
import { useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
/** /**
@ -7,17 +7,21 @@ import { useEffect, useState } from "react"
export default function useLocalStorageState(key, def) { export default function useLocalStorageState(key, def) {
const [value, setValue] = useState(null); const [value, setValue] = useState(null);
const load = () => { /**
* Load the `key` from the {@link localStorage} into `value`, defaulting to `def` if it is not found.
*/
const load = useCallback(
() => {
if(localStorage) { if(localStorage) {
console.debug(`Loading ${key} from localStorage...`) console.debug(`Loading ${key} from localStorage...`)
let value = JSON.parse(localStorage.getItem(key)) let _value = JSON.parse(localStorage.getItem(key))
if(value) { if(_value) {
console.debug(`Loaded ${key} from localStorage!`) console.info(`Loaded ${key} from localStorage!`)
return value return _value
} }
else { else {
console.debug(`There is no value ${key} stored, defaulting...`) console.info(`There is no value ${key} stored, defaulting...`)
return def return def
} }
} }
@ -25,28 +29,49 @@ export default function useLocalStorageState(key, def) {
console.warn(`Can't load value as localStorage doesn't seem to be available, defaulting...`) console.warn(`Can't load value as localStorage doesn't seem to be available, defaulting...`)
return def return def
} }
} },
[key, def]
)
useEffect(() => { /**
if(!value) { * Save a value to the {@link localStorage}.
setValue(load()) */
} const save = useCallback(
}, [value]) (value) => {
const save = (value) => {
if(localStorage) { if(localStorage) {
console.debug(`Saving ${key} to localStorage...`) console.debug(`Saving ${key} to localStorage...`)
localStorage.setItem(key, JSON.stringify(value)) localStorage.setItem(key, JSON.stringify(value))
} }
else { else {
console.warn(`Can't save theme; localStorage doesn't seem to be available...`) console.warn(`Can't save ${key}; localStorage doesn't seem to be available...`)
}
} }
},
[key]
)
const setAndSave = (value) => { /**
* Set `value` and save it to the {@link localStorage}.
*/
const setAndSave = useCallback(
(value) => {
setValue(value) setValue(value)
save(value) save(value)
},
[setValue, save]
)
/*
* When the component first renders, try to load the value from the localStorage.
*/
useEffect(
() => {
if(!value) {
console.debug(`This is the first render, loading ${key} from the localStorage...`)
setValue(load())
} }
},
[value, setValue, load, key],
)
return [value, setAndSave] return [value, setAndSave]
} }

View file

@ -1,9 +1,6 @@
import React, { useContext, useState } from "react" import React 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 ContextUser from "../contexts/ContextUser"
import { useHistory } from "react-router"
import BoxSetServer from "../components/BoxSetServer" import BoxSetServer from "../components/BoxSetServer"
import BoxLogin from "../components/BoxLogin" import BoxLogin from "../components/BoxLogin"

View file

@ -1,18 +1,15 @@
import React from "react" import React from "react"
import Style from "./PageRepositories.module.css" import Style from "./PageRepositories.module.css"
import classNames from "classnames" import classNames from "classnames"
import BoxFull from "../components/BoxFull" import BoxRepositoriesActive from "../components/BoxRepositoriesActive"
import BoxRepositoriesArchived from "../components/BoxRepositoriesArchived"
export default function PageRepositories({ children, className, ...props }) { export default function PageRepositories({ children, className, ...props }) {
return ( return (
<div className={classNames(Style.PageRepositories, className)} {...props}> <div className={classNames(Style.PageRepositories, className)} {...props}>
<BoxFull header={"Your active repositories"} className={Style.ActiveRepositories}> <BoxRepositoriesActive/>
🚧 Not implemented. <BoxRepositoriesArchived/>
</BoxFull>
<BoxFull header={"Your archived repositories"} className={Style.ArchivedRepositories}>
🚧 Not implemented.
</BoxFull>
</div> </div>
) )
} }