mirror of
https://github.com/pds-nest/nest.git
synced 2024-11-21 20:44:18 +00:00
Merge remote-tracking branch 'origin/main' into main
This commit is contained in:
commit
a3f5139b76
14 changed files with 317 additions and 89 deletions
|
@ -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/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/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/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"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -10,18 +10,20 @@ from flask_cors import cross_origin
|
|||
def page_repository_list():
|
||||
"""
|
||||
API call that returns the list of repositories.
|
||||
|
||||
:parameter onlyActive: if present, only active 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
|
||||
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())
|
||||
owner = Repository.query.filter_by(owner_id=user.email)
|
||||
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)
|
||||
spectator = spectator.filter(Repository.isActive == True)
|
||||
elif request.json.get("onlyDead"):
|
||||
elif request.args.get("onlyDead"):
|
||||
owner = owner.filter_by(isActive=False)
|
||||
spectator = spectator.filter(Repository.isActive == False)
|
||||
owner = owner.all()
|
||||
|
|
|
@ -11,6 +11,7 @@ import PageRoot from "./routes/PageRoot"
|
|||
import GlobalTheme from "./components/GlobalTheme"
|
||||
import GlobalServer from "./components/GlobalServer"
|
||||
import GlobalUser from "./components/GlobalUser"
|
||||
import PageSwitcher from "./PageSwitcher"
|
||||
|
||||
|
||||
/**
|
||||
|
@ -25,33 +26,9 @@ export default function App() {
|
|||
<GlobalUser>
|
||||
<GlobalTheme>
|
||||
<BrowserRouter>
|
||||
|
||||
<Layout>
|
||||
<Switch>
|
||||
<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>
|
||||
<PageSwitcher/>
|
||||
</Layout>
|
||||
</BrowserRouter>
|
||||
</GlobalTheme>
|
||||
</GlobalUser>
|
||||
|
|
38
code/frontend/src/PageSwitcher.js
Normal file
38
code/frontend/src/PageSwitcher.js
Normal 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>
|
||||
)
|
||||
}
|
38
code/frontend/src/components/BoxRepositoriesActive.js
Normal file
38
code/frontend/src/components/BoxRepositoriesActive.js
Normal 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>
|
||||
)
|
||||
}
|
38
code/frontend/src/components/BoxRepositoriesArchived.js
Normal file
38
code/frontend/src/components/BoxRepositoriesArchived.js
Normal 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>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react"
|
||||
import React, { useCallback } from "react"
|
||||
import useLocalStorageState from "../hooks/useLocalStorageState"
|
||||
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}.
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const fetchData = async (method, path, body, init) => {
|
||||
const fetchData = useCallback(async (method, path, body, init) => {
|
||||
if(!server) {
|
||||
throw new Error(`Invalid server: ${server}`)
|
||||
}
|
||||
|
@ -32,14 +33,31 @@ export default function GlobalServer({ children }) {
|
|||
if(!init) {
|
||||
init = {}
|
||||
}
|
||||
|
||||
if(!init["headers"]) {
|
||||
init["headers"] = {}
|
||||
}
|
||||
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}`, {
|
||||
method: method,
|
||||
body: JSON.stringify(body),
|
||||
...init,
|
||||
})
|
||||
|
||||
|
@ -50,11 +68,11 @@ export default function GlobalServer({ children }) {
|
|||
const json = await response.json()
|
||||
|
||||
if(json["result"] !== "success") {
|
||||
throw new Error(`Request failed: ${json["msg"]}`)
|
||||
throw new Error(json["msg"])
|
||||
}
|
||||
|
||||
return json["data"]
|
||||
}
|
||||
}, [server])
|
||||
|
||||
return (
|
||||
<ContextServer.Provider value={{server, setServer, fetchData}}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext } from "react"
|
||||
import React, { useCallback, useContext } from "react"
|
||||
import useLocalStorageState from "../hooks/useLocalStorageState"
|
||||
import ContextServer from "../contexts/ContextServer"
|
||||
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}.
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const fetchDataAuth = async (method, path, body, init) => {
|
||||
const fetchDataAuth = useCallback(async (method, path, body, init) => {
|
||||
if(!user) {
|
||||
throw new Error("Not logged in")
|
||||
}
|
||||
|
@ -39,8 +39,8 @@ export default function GlobalUser({ children }) {
|
|||
}
|
||||
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.
|
||||
|
@ -49,7 +49,7 @@ export default function GlobalUser({ children }) {
|
|||
* @param password - The user's password.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const login = async (email, password) => {
|
||||
const login = useCallback(async (email, password) => {
|
||||
console.debug("Contacting server to login...")
|
||||
const data = await fetchData("POST", `/api/login`, {
|
||||
"email": email,
|
||||
|
@ -65,18 +65,18 @@ export default function GlobalUser({ children }) {
|
|||
})
|
||||
|
||||
console.info("Login successful!")
|
||||
}
|
||||
}, [fetchData, setUser])
|
||||
|
||||
/**
|
||||
* Logout from the currently active server.
|
||||
*/
|
||||
const logout = () => {
|
||||
const logout = useCallback(() => {
|
||||
console.debug("Clearing login state...")
|
||||
setUser(null)
|
||||
console.debug("Cleared login state!")
|
||||
|
||||
console.info("Logout successful!")
|
||||
}
|
||||
}, [setUser])
|
||||
|
||||
return (
|
||||
<ContextUser.Provider value={{user, login, logout, fetchDataAuth}}>
|
||||
|
|
12
code/frontend/src/components/Loading.js
Normal file
12
code/frontend/src/components/Loading.js
Normal 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>
|
||||
)
|
||||
}
|
17
code/frontend/src/hooks/useAsyncEffect.js
Normal file
17
code/frontend/src/hooks/useAsyncEffect.js
Normal 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])
|
||||
}
|
69
code/frontend/src/hooks/useData.js
Normal file
69
code/frontend/src/hooks/useData.js
Normal 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}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from "react"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
|
||||
|
||||
/**
|
||||
|
@ -7,46 +7,71 @@ import { useEffect, useState } from "react"
|
|||
export default function useLocalStorageState(key, def) {
|
||||
const [value, setValue] = useState(null);
|
||||
|
||||
const load = () => {
|
||||
if(localStorage) {
|
||||
console.debug(`Loading ${key} from localStorage...`)
|
||||
let value = JSON.parse(localStorage.getItem(key))
|
||||
/**
|
||||
* Load the `key` from the {@link localStorage} into `value`, defaulting to `def` if it is not found.
|
||||
*/
|
||||
const load = useCallback(
|
||||
() => {
|
||||
if(localStorage) {
|
||||
console.debug(`Loading ${key} from localStorage...`)
|
||||
let _value = JSON.parse(localStorage.getItem(key))
|
||||
|
||||
if(value) {
|
||||
console.debug(`Loaded ${key} from localStorage!`)
|
||||
return value
|
||||
if(_value) {
|
||||
console.info(`Loaded ${key} from localStorage!`)
|
||||
return _value
|
||||
}
|
||||
else {
|
||||
console.info(`There is no value ${key} stored, defaulting...`)
|
||||
return def
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.debug(`There is no value ${key} stored, defaulting...`)
|
||||
console.warn(`Can't load value as localStorage doesn't seem to be available, defaulting...`)
|
||||
return def
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.warn(`Can't load value as localStorage doesn't seem to be available, defaulting...`)
|
||||
return def
|
||||
}
|
||||
}
|
||||
},
|
||||
[key, def]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if(!value) {
|
||||
setValue(load())
|
||||
}
|
||||
}, [value])
|
||||
/**
|
||||
* Save a value to the {@link localStorage}.
|
||||
*/
|
||||
const save = useCallback(
|
||||
(value) => {
|
||||
if(localStorage) {
|
||||
console.debug(`Saving ${key} to localStorage...`)
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
}
|
||||
else {
|
||||
console.warn(`Can't save ${key}; localStorage doesn't seem to be available...`)
|
||||
}
|
||||
},
|
||||
[key]
|
||||
)
|
||||
|
||||
const save = (value) => {
|
||||
if(localStorage) {
|
||||
console.debug(`Saving ${key} to localStorage...`)
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
}
|
||||
else {
|
||||
console.warn(`Can't save theme; localStorage doesn't seem to be available...`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Set `value` and save it to the {@link localStorage}.
|
||||
*/
|
||||
const setAndSave = useCallback(
|
||||
(value) => {
|
||||
setValue(value)
|
||||
save(value)
|
||||
},
|
||||
[setValue, save]
|
||||
)
|
||||
|
||||
const setAndSave = (value) => {
|
||||
setValue(value)
|
||||
save(value)
|
||||
}
|
||||
/*
|
||||
* 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]
|
||||
}
|
|
@ -1,9 +1,6 @@
|
|||
import React, { useContext, useState } from "react"
|
||||
import React from "react"
|
||||
import Style from "./PageLogin.module.css"
|
||||
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 BoxLogin from "../components/BoxLogin"
|
||||
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
import React from "react"
|
||||
import Style from "./PageRepositories.module.css"
|
||||
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 }) {
|
||||
return (
|
||||
<div className={classNames(Style.PageRepositories, className)} {...props}>
|
||||
<BoxFull header={"Your active repositories"} className={Style.ActiveRepositories}>
|
||||
🚧 Not implemented.
|
||||
</BoxFull>
|
||||
<BoxFull header={"Your archived repositories"} className={Style.ArchivedRepositories}>
|
||||
🚧 Not implemented.
|
||||
</BoxFull>
|
||||
<BoxRepositoriesActive/>
|
||||
<BoxRepositoriesArchived/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue