diff --git a/code/backend/nest_backend/__main__.py b/code/backend/nest_backend/__main__.py index 2fd376f..a9799d1 100644 --- a/code/backend/nest_backend/__main__.py +++ b/code/backend/nest_backend/__main__.py @@ -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__": diff --git a/code/backend/nest_backend/routes/repository/repository_list.py b/code/backend/nest_backend/routes/repository/repository_list.py index 9a8afe7..d87f0c0 100644 --- a/code/backend/nest_backend/routes/repository/repository_list.py +++ b/code/backend/nest_backend/routes/repository/repository_list.py @@ -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() diff --git a/code/frontend/src/App.js b/code/frontend/src/App.js index f365622..e05caa7 100644 --- a/code/frontend/src/App.js +++ b/code/frontend/src/App.js @@ -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() { - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/code/frontend/src/PageSwitcher.js b/code/frontend/src/PageSwitcher.js new file mode 100644 index 0000000..2f2afa7 --- /dev/null +++ b/code/frontend/src/PageSwitcher.js @@ -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 ( + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/code/frontend/src/components/BoxRepositoriesActive.js b/code/frontend/src/components/BoxRepositoriesActive.js new file mode 100644 index 0000000..6db8da9 --- /dev/null +++ b/code/frontend/src/components/BoxRepositoriesActive.js @@ -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 = + } + else if(error) { + contents = {error.toString()} + } + else { + let repositories = [...data["owner"], ...data["spectator"]] + if(repositories.length > 0) { + contents = repositories.map(repo => ) + } + else { + contents = There's nothing here. + } + } + + return ( + + {contents} + + ) +} diff --git a/code/frontend/src/components/BoxRepositoriesArchived.js b/code/frontend/src/components/BoxRepositoriesArchived.js new file mode 100644 index 0000000..f947061 --- /dev/null +++ b/code/frontend/src/components/BoxRepositoriesArchived.js @@ -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 = + } + else if(error) { + contents = {error.toString()} + } + else { + let repositories = [...data["owner"], ...data["spectator"]] + if(repositories.length > 0) { + contents = repositories.map(repo => ) + } + else { + contents = There's nothing here. + } + } + + return ( + + {contents} + + ) +} diff --git a/code/frontend/src/components/GlobalServer.js b/code/frontend/src/components/GlobalServer.js index ae05407..a29f217 100644 --- a/code/frontend/src/components/GlobalServer.js +++ b/code/frontend/src/components/GlobalServer.js @@ -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 ( diff --git a/code/frontend/src/components/GlobalUser.js b/code/frontend/src/components/GlobalUser.js index a7b493d..0514031 100644 --- a/code/frontend/src/components/GlobalUser.js +++ b/code/frontend/src/components/GlobalUser.js @@ -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} */ - 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 ( diff --git a/code/frontend/src/components/Loading.js b/code/frontend/src/components/Loading.js new file mode 100644 index 0000000..b076e4d --- /dev/null +++ b/code/frontend/src/components/Loading.js @@ -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 ( +
+ Loading... +
+ ) +} diff --git a/code/frontend/src/hooks/useAsyncEffect.js b/code/frontend/src/hooks/useAsyncEffect.js new file mode 100644 index 0000000..7595c3b --- /dev/null +++ b/code/frontend/src/hooks/useAsyncEffect.js @@ -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]) +} diff --git a/code/frontend/src/hooks/useData.js b/code/frontend/src/hooks/useData.js new file mode 100644 index 0000000..0fd3507 --- /dev/null +++ b/code/frontend/src/hooks/useData.js @@ -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} +} \ No newline at end of file diff --git a/code/frontend/src/hooks/useLocalStorageState.js b/code/frontend/src/hooks/useLocalStorageState.js index 5de5662..1acc7ba 100644 --- a/code/frontend/src/hooks/useLocalStorageState.js +++ b/code/frontend/src/hooks/useLocalStorageState.js @@ -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] } \ No newline at end of file diff --git a/code/frontend/src/routes/PageLogin.js b/code/frontend/src/routes/PageLogin.js index 7961ad5..9392428 100644 --- a/code/frontend/src/routes/PageLogin.js +++ b/code/frontend/src/routes/PageLogin.js @@ -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" diff --git a/code/frontend/src/routes/PageRepositories.js b/code/frontend/src/routes/PageRepositories.js index d3d6dbf..eb3c7db 100644 --- a/code/frontend/src/routes/PageRepositories.js +++ b/code/frontend/src/routes/PageRepositories.js @@ -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 (
- - 🚧 Not implemented. - - - 🚧 Not implemented. - + +
) }