1
Fork 0
mirror of https://github.com/pds-nest/nest.git synced 2024-11-25 14:34:19 +00:00

💥 I did so many things I forgot which specific ones I did

This commit is contained in:
Stefano Pigozzi 2021-04-26 22:08:52 +02:00
parent bde82a4829
commit 601b8074a2
Signed by untrusted user who does not match committer: steffo
GPG key ID: 6965406171929D01
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/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__":

View file

@ -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()

View file

@ -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>

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 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}}>

View file

@ -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}}>

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,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]
}

View file

@ -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"

View file

@ -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>
)
}