From d946e46431ee1895d101814e949adf8c5986c607 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Wed, 19 May 2021 19:56:41 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20many=20things,=20impr?= =?UTF-8?q?oving=20performance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/interactive/BoxRepositories.js | 52 ++++ .../interactive/BoxRepositoriesActive.js | 59 ---- .../interactive/BoxRepositoriesArchived.js | 59 ---- .../interactive/SummaryRepository.js | 98 ++++--- nest_frontend/hooks/useBackend.js | 1 + nest_frontend/hooks/useBackendImmediately.js | 1 + nest_frontend/hooks/useBackendRequest.js | 98 +++++++ nest_frontend/hooks/useBackendResource.js | 139 ++++++++++ nest_frontend/hooks/useBackendViewset.js | 256 +++++++----------- nest_frontend/routes/PageRepositories.js | 46 ++-- nest_frontend/routes/PageRepository.js | 87 ++++-- nest_frontend/utils/Errors.js | 69 +++++ 12 files changed, 597 insertions(+), 368 deletions(-) create mode 100644 nest_frontend/components/interactive/BoxRepositories.js delete mode 100644 nest_frontend/components/interactive/BoxRepositoriesActive.js delete mode 100644 nest_frontend/components/interactive/BoxRepositoriesArchived.js create mode 100644 nest_frontend/hooks/useBackendRequest.js create mode 100644 nest_frontend/hooks/useBackendResource.js create mode 100644 nest_frontend/utils/Errors.js diff --git a/nest_frontend/components/interactive/BoxRepositories.js b/nest_frontend/components/interactive/BoxRepositories.js new file mode 100644 index 0000000..240b4c3 --- /dev/null +++ b/nest_frontend/components/interactive/BoxRepositories.js @@ -0,0 +1,52 @@ +import React, { useContext } from "react" +import BoxFullScrollable from "../base/BoxFullScrollable" +import Loading from "../base/Loading" +import ContextLanguage from "../../contexts/ContextLanguage" +import SummaryRepository from "./SummaryRepository" + + +/** + * A {@link BoxFullScrollable} listing some repositories. + * + * @param repositories - The repositories to list. + * @param view - Function with a single "id" parameter to call when the view repository button is clicked. + * @param archive - Function with a single "id" parameter to call when the archive repository button is clicked. + * @param edit - Function with a single "id" parameter to call when the edit repository button is clicked. + * @param destroy - Function with a single "id" parameter to call when the delete repository button is clicked. + * @param firstLoad - If the repositories are loading and a loading message should be displayed. + * @param running - If an action is running on the viewset. + * @param className - Additional class(es) to append to the box. + * @param props - Additional props to pass to the box. + * @returns {JSX.Element} + * @constructor + */ +export default function BoxRepositories({ repositories, view, archive, edit, destroy, loading, running, className, ...props }) { + const { strings } = useContext(ContextLanguage) + + let contents + if(loading) { + contents = + } + else if(repositories.length === 0) { + contents = {strings.emptyMenu} + } + else { + contents = repositories.map(repo => ( + view(repo["id"]) : null} + archive={archive ? () => archive(repo["id"]) : null} + edit={edit ? () => edit(repo["id"]) : null} + destroy={destroy ? () => destroy(repo["id"]) : null} + running={running} + /> + )) + } + + return ( + + {contents} + + ) +} diff --git a/nest_frontend/components/interactive/BoxRepositoriesActive.js b/nest_frontend/components/interactive/BoxRepositoriesActive.js deleted file mode 100644 index aabfa72..0000000 --- a/nest_frontend/components/interactive/BoxRepositoriesActive.js +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useContext } from "react" -import SummaryRepository from "./SummaryRepository" -import { faFolderOpen } from "@fortawesome/free-solid-svg-icons" -import ContextUser from "../../contexts/ContextUser" -import Loading from "../base/Loading" -import BoxFullScrollable from "../base/BoxFullScrollable" -import ContextLanguage from "../../contexts/ContextLanguage" - - -/** - * A {@link BoxFull} listing all the user's active repositories. - * - * @param repositories - Array of repositories to display in the box. - * @param archiveRepository - Function to be called when archive is pressed on a repository summary. - * @param destroyRepository - Function to be called when delete is pressed on a repository summary. - * @param running - If an action is currently running. - * @param props - Additional props to pass to the box. - * @returns {JSX.Element} - * @constructor - */ -export default function BoxRepositoriesActive({ - repositories, - archiveRepository, - destroyRepository, - running, - ...props - }) { - const { user } = useContext(ContextUser) - const { strings } = useContext(ContextLanguage) - - let contents - if(repositories === null) { - contents = - } - else if(repositories.length === 0) { - contents = {strings.emptyMenu}. - } - else { - contents = repositories.map(repo => ( - archiveRepository(repo["id"])} - deleteSelf={() => destroyRepository(repo["id"])} - canArchive={true} - canEdit={true} - canDelete={false} - running={running} - /> - )) - } - - return ( - - {contents} - - ) -} diff --git a/nest_frontend/components/interactive/BoxRepositoriesArchived.js b/nest_frontend/components/interactive/BoxRepositoriesArchived.js deleted file mode 100644 index ba9f7b5..0000000 --- a/nest_frontend/components/interactive/BoxRepositoriesArchived.js +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useContext } from "react" -import SummaryRepository from "./SummaryRepository" -import { faFolderOpen } from "@fortawesome/free-solid-svg-icons" -import ContextUser from "../../contexts/ContextUser" -import Loading from "../base/Loading" -import BoxFullScrollable from "../base/BoxFullScrollable" -import ContextLanguage from "../../contexts/ContextLanguage" - - -/** - * A {@link BoxFull} listing all the user's archived repositories. - * - * @param repositories - Array of repositories to display in the box. - * @param archiveRepository - Function to be called when archive is pressed on a repository summary. - * @param destroyRepository - Function to be called when delete is pressed on a repository summary. - * @param running - If an action is currently running. - * @param props - Additional props to pass to the box. - * @returns {JSX.Element} - * @constructor - */ -export default function BoxRepositoriesArchived({ - repositories, - archiveRepository, - destroyRepository, - running, - ...props - }) { - const { user } = useContext(ContextUser) - const { strings } = useContext(ContextLanguage) - - let contents - if(repositories === null) { - contents = - } - else if(repositories.length === 0) { - contents = {strings.emptyMenu}. - } - else { - contents = repositories.map(repo => ( - archiveRepository(repo["id"])} - deleteSelf={() => destroyRepository(repo["id"])} - canArchive={false} - canEdit={false} - canDelete={repo["owner"]["username"] === user["username"]} - running={running} - /> - )) - } - - return ( - - {contents} - - ) -} diff --git a/nest_frontend/components/interactive/SummaryRepository.js b/nest_frontend/components/interactive/SummaryRepository.js index 0d7bb5e..524f80f 100644 --- a/nest_frontend/components/interactive/SummaryRepository.js +++ b/nest_frontend/components/interactive/SummaryRepository.js @@ -10,79 +10,73 @@ import SummaryRight from "../base/summary/SummaryRight" /** - * A long line representing a repository in a list. + * A {@link SummaryBase} representing a repository. * - * @param repo - The repository object. - * @param refresh - Function that can be called to refresh the repositories list. - * @param canDelete - If the Delete button should be displayed or not. - * @param deleteSelf - Function to call when the Delete button is pressed. - * @param canEdit - If the Edit button should be displayed or not. - * @param canArchive - If the Archive button should be displayed or not. - * @param archiveSelf - Function to call when the Archive button is pressed. - * @param running - If an action is currently running. - * @param className - Additional class(es) to be added to the outer box. - * @param props - Additional props to pass to the outer box. + * @param repo - The repository to display. + * @param view - Function with no parameters to call when the view repository button is clicked. + * @param archive - Function with no parameters to call when the archive repository button is clicked. + * @param edit - Function with no parameters to call when the edit repository button is clicked. + * @param destroy - Function with no parameters to call when the delete repository button is clicked. + * @param running - If an action is running on the viewset. + * @param className - Additional class(es) to append to the summary. + * @param props - Additional props to pass to the summary. * @returns {JSX.Element} * @constructor */ export default function SummaryRepository( - { repo, refresh, canDelete, deleteSelf, canEdit, canArchive, archiveSelf, running, className, ...props }, + { repo, view, archive, edit, destroy, running, className, ...props }, ) { - const history = useHistory() const { strings } = useContext(ContextLanguage) - const onRepoClick = () => { - history.push(`/repositories/${repo.id}`) - } - - const onEditClick = () => { - history.push(`/repositories/${repo.id}/edit`) - } - return ( + - {canDelete ? - - {strings.delete} - - : null} - {canEdit ? - - {strings.edit} - - : null} - {canArchive ? - - {strings.archive} - - : null} + + {destroy ? + destroy(repo["id"])} + disabled={running} + > + {strings.delete} + + : null} + + {archive ? + archive(repo["id"])} + disabled={running} + > + {strings.archive} + + : null} + + {edit ? + edit(repo["id"])} + disabled={running} + > + {strings.edit} + + : null} + ) diff --git a/nest_frontend/hooks/useBackend.js b/nest_frontend/hooks/useBackend.js index d9bfb00..13d2c8f 100644 --- a/nest_frontend/hooks/useBackend.js +++ b/nest_frontend/hooks/useBackend.js @@ -9,6 +9,7 @@ import { useCallback, useState } from "react" * @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`. + * @deprecated since 2021-05-19 */ export default function useBackend(fetchData, method, path, body, init) { const [error, setError] = useState(null) diff --git a/nest_frontend/hooks/useBackendImmediately.js b/nest_frontend/hooks/useBackendImmediately.js index d6addf4..74fe7c8 100644 --- a/nest_frontend/hooks/useBackendImmediately.js +++ b/nest_frontend/hooks/useBackendImmediately.js @@ -10,6 +10,7 @@ import { useEffect } from "react" * @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`. + * @deprecated since 2021-05-19 */ export default function useBackendImmediately(fetchData, method, path, body, init) { const { data, error, loading, fetchNow } = useBackend(fetchData, method, path, body, init) diff --git a/nest_frontend/hooks/useBackendRequest.js b/nest_frontend/hooks/useBackendRequest.js new file mode 100644 index 0000000..5474967 --- /dev/null +++ b/nest_frontend/hooks/useBackendRequest.js @@ -0,0 +1,98 @@ +import { useCallback, useContext, useState } from "react" +import ContextServer from "../contexts/ContextServer" +import ContextUser from "../contexts/ContextUser" +import makeURLSearchParams from "../utils/makeURLSearchParams" + + +/** + * An hook which provides the apiRequest method to send request to the backend REST API. + */ +export default function useBackendRequest() { + const { server } = useContext(ContextServer) + const configured = server !== null + + const { user } = useContext(ContextUser) + const loggedIn = user !== null + + const [abort, setAbort] = useState(null) + const running = abort !== null + + const apiRequest = useCallback( + async (method, path, body, init = {}) => { + // Check if server is configured + if(!configured) { + throw new ServerNotConfiguredError() + } + + // Check if something is not already being loaded + if(running) { + throw new FetchAlreadyRunningError() + } + + // Ensure init has certain sub-objects + if(!init["headers"]) { + init["headers"] = {} + } + + // If the user is logged in, add the Authorization headers + if(loggedIn) { + init["headers"]["Authorization"] = `Bearer ${user.token}` + } + + // Set the Content-Type header + init["headers"]["Content-Type"] = "application/json" + + // Use the body param as either search parameter or request body + if(body) { + if(["GET", "HEAD"].includes(method.toUpperCase())) { + path += makeURLSearchParams(body).toString() + } + else { + init["body"] = JSON.stringify(body) + } + } + + // Set the method + init["method"] = method + + // Create a new abort handler in case the request needs to be aborted + const thisAbort = new AbortController() + init["signal"] = thisAbort.signal + setAbort(thisAbort) + + // Fetch the resource + const response = await fetch(`${server}${path}`, init) + + // Clear the abort handler + setAbort(null) + + // If the response is 204 NO CONTENT, return null + if(response.status === 204) { + return null + } + + // Otherwise, try parsing the response as JSON + let json + try { + json = await response.json() + } + catch (error) { + throw new DecodeError(response.status, response.statusText, error) + } + + // Check if the JSON contains a success + if(json["result"] !== "success") { + throw new ResultError(response.status, response.statusText, json) + } + + return json["data"] + }, + [server, configured, running, loggedIn, user, setAbort], + ) + + return { + abort, + running, + apiRequest, + } +} \ No newline at end of file diff --git a/nest_frontend/hooks/useBackendResource.js b/nest_frontend/hooks/useBackendResource.js new file mode 100644 index 0000000..c356487 --- /dev/null +++ b/nest_frontend/hooks/useBackendResource.js @@ -0,0 +1,139 @@ +import { useCallback, useContext, useEffect, useState } from "react" +import ContextServer from "../contexts/ContextServer" +import ContextUser from "../contexts/ContextUser" +import makeURLSearchParams from "../utils/makeURLSearchParams" +import useBackendRequest from "./useBackendRequest" + + +/** + * An hook which allows access to a full REST resource (retrieve, edit, delete). + * + * @param resourcePath - The path of the resource file. + * @param allowViews - An object with maps views to a boolean detailing if they're allowed in the viewset or not. + */ +export default function useBackendResource(resourcePath, + { + retrieve: allowRetrieve = true, + edit: allowEdit = true, + destroy: allowDestroy = true, + action: allowAction = false, + } = {}) { + + const {abort, running, apiRequest} = useBackendRequest() + + const [firstLoad, setFirstLoad] = useState(false) + const [resource, setResource] = useState(null) + const [error, setError] = useState(null) + + const apiRetrieve = useCallback( + async (init) => { + if(!allowRetrieve) throw new ViewNotAllowedError("retrieve") + return await apiRequest("GET", `${resourcePath}`, undefined, init) + }, + [apiRequest, allowRetrieve, resourcePath], + ) + + const apiEdit = useCallback( + async (data, init) => { + if(!allowEdit) throw new ViewNotAllowedError("edit") + return await apiRequest("PUT", `${resourcePath}`, data, init) + }, + [apiRequest, allowEdit, resourcePath], + ) + + const apiDestroy = useCallback( + async (init) => { + if(!allowDestroy) throw new ViewNotAllowedError("destroy") + return await apiRequest("DELETE", `${resourcePath}`, undefined, init) + }, + [apiRequest, allowDestroy, resourcePath], + ) + + const apiAction = useCallback( + async (method, command, data, init) => { + if(!allowAction) throw new ViewNotAllowedError("action") + return await apiRequest(method, `${resourcePath}/${command}`, data, init) + }, + [apiRequest, allowAction, resourcePath] + ) + + const retrieveResource = useCallback( + async (pk) => { + let refreshedResource + try { + refreshedResource = await apiRetrieve(pk) + } + catch(e) { + setError(e) + throw e + } + setError(null) + setResource(refreshedResource) + return refreshedResource + }, + [apiRetrieve], + ) + + const editResource = useCallback( + async (pk, data) => { + let editedResource + try { + editedResource = await apiEdit(pk, data) + } + catch(e) { + setError(e) + throw e + } + setError(null) + setResource(editedResource) + return editedResource + }, + [apiEdit], + ) + + const destroyResource = useCallback( + async (pk) => { + try { + await apiDestroy(pk) + } + catch(e) { + setError(e) + throw e + } + setError(null) + setResource(null) + return null + }, + [apiDestroy], + ) + + useEffect( + async () => { + if(allowRetrieve && !firstLoad && !running) { + // noinspection JSIgnoredPromiseFromCall + await retrieveResource() + setFirstLoad(true) + } + }, + [retrieveResource, firstLoad, running, allowRetrieve], + ) + + return { + abort, + resource, + running, + firstLoad, + error, + apiRequest, + allowRetrieve, + apiRetrieve, + retrieveResource, + allowEdit, + apiEdit, + editResource, + allowDestroy, + apiDestroy, + destroyResource, + apiAction, + } +} \ No newline at end of file diff --git a/nest_frontend/hooks/useBackendViewset.js b/nest_frontend/hooks/useBackendViewset.js index 8c2f5dd..f2396d6 100644 --- a/nest_frontend/hooks/useBackendViewset.js +++ b/nest_frontend/hooks/useBackendViewset.js @@ -1,7 +1,5 @@ -import { useCallback, useContext, useEffect, useState } from "react" -import ContextServer from "../contexts/ContextServer" -import ContextUser from "../contexts/ContextUser" -import makeURLSearchParams from "../utils/makeURLSearchParams" +import { useCallback, useEffect, useState } from "react" +import useBackendRequest from "./useBackendRequest" /** @@ -9,222 +7,174 @@ import makeURLSearchParams from "../utils/makeURLSearchParams" * * @param resourcesPath - The path of the resource directory. * @param pkName - The name of the primary key attribute of the elements. - * @param refreshOnStart - Whether the data should be loaded at the first startup (defaults to true). + * @param allowViews - An object with maps views to a boolean detailing if they're allowed in the viewset or not. */ -export default function useBackendViewset(resourcesPath, pkName, refreshOnStart = true) { - const { server } = useContext(ContextServer) - const configured = server !== null +export default function useBackendViewset(resourcesPath, pkName, + { + list: allowList = true, + create: allowCreate = true, + retrieve: allowRetrieve = true, + edit: allowEdit = true, + destroy: allowDestroy = true, + command: allowCommand = false, + action: allowAction = false, + } = {}) { + const {abort, running, apiRequest} = useBackendRequest() - const { user } = useContext(ContextUser) - const loggedIn = user !== null - - const [abort, setAbort] = useState(null) - const running = abort !== null - - const [resources, setResources] = useState(null) - const loaded = resources !== null - - const apiRequest = useCallback( - async (method, path, body, init = {}) => { - // Check if server is configured - if(!configured) { - throw new Error(`Backend server not configured.`) - } - - // Check if something is not already being loaded - if(running) { - throw new Error(`A request is already running.`) - } - - // Ensure init has certain sub-objects - if(!init["headers"]) { - init["headers"] = {} - } - - // If the user is logged in, add the Authorization headers - if(loggedIn) { - init["headers"]["Authorization"] = `Bearer ${user.token}` - } - - // Set the Content-Type header - init["headers"]["Content-Type"] = "application/json" - - // Use the body param as either search parameter or request body - if(body) { - if(["GET", "HEAD"].includes(method.toUpperCase())) { - path += makeURLSearchParams(body).toString() - } - else { - init["body"] = JSON.stringify(body) - } - } - - // Set the method - init["method"] = method - - // Create a new abort handler in case the request needs to be aborted - const thisAbort = new AbortController() - init["signal"] = thisAbort.signal - setAbort(thisAbort) - - // Fetch the resource - const response = await fetch(`${server}${path}`, init) - - // Clear the abort handler - setAbort(null) - - // Check if the request was successful - if(!response.ok) { - throw new Error(`${method} ${path} failed with status code ${response.status} ${response.statusText}`) - } - - // If the response is 204 NO CONTENT, return null - if(response.status === 204) { - return null - } - - // Otherwise, try parsing the response as JSON - const json = await response.json() - - // Check if the JSON contains a success - if(json["result"] !== "success") { - throw new Error(`${method} ${path} failed with message ${json["msg"]}`) - } - - return json["data"] - }, - [server, configured, running, loggedIn, user, setAbort], - ) + const [firstLoad, setFirstLoad] = useState(false) + const [resources, setResources] = useState([]) + const [error, setError] = useState(null) const apiList = useCallback( - async (init) => await apiRequest("GET", `${resourcesPath}`, undefined, init), - [apiRequest, resourcesPath], + async (init) => { + if(!allowList) throw new ViewNotAllowedError("list") + return await apiRequest("GET", `${resourcesPath}`, undefined, init) + }, + [apiRequest, allowList, resourcesPath], ) const apiRetrieve = useCallback( - async (id, init) => await apiRequest("GET", `${resourcesPath}${id}`, undefined, init), - [apiRequest, resourcesPath], + async (id, init) => { + if(!allowRetrieve) throw new ViewNotAllowedError("retrieve") + return await apiRequest("GET", `${resourcesPath}${id}`, undefined, init) + }, + [apiRequest, allowRetrieve, resourcesPath], ) const apiCreate = useCallback( - async (data, init) => await apiRequest("POST", `${resourcesPath}`, data, init), - [apiRequest, resourcesPath], + async (data, init) => { + if(!allowCreate) throw new ViewNotAllowedError("create") + return await apiRequest("POST", `${resourcesPath}`, data, init) + }, + [apiRequest, allowCreate, resourcesPath], ) const apiEdit = useCallback( - async (id, data, init) => await apiRequest("PUT", `${resourcesPath}${id}`, data, init), - [apiRequest, resourcesPath], + async (id, data, init) => { + if(!allowEdit) throw new ViewNotAllowedError("edit") + return await apiRequest("PUT", `${resourcesPath}${id}`, data, init) + }, + [apiRequest, allowEdit, resourcesPath], ) const apiDestroy = useCallback( - async (id, init) => await apiRequest("DELETE", `${resourcesPath}${id}`, undefined, init), - [apiRequest, resourcesPath], + async (id, init) => { + if(!allowDestroy) throw new ViewNotAllowedError("destroy") + return await apiRequest("DELETE", `${resourcesPath}${id}`, undefined, init) + }, + [apiRequest, allowDestroy, resourcesPath], ) - const refreshResources = useCallback( + const apiCommand = useCallback( + async (method, command, data, init) => { + if(!allowCommand) throw new ViewNotAllowedError("command") + return await apiRequest(method, `${resourcesPath}${command}`, data, init) + }, + [apiRequest, allowCommand, resourcesPath] + ) + + const apiAction = useCallback( + async (method, id, command, data, init) => { + if(!allowAction) throw new ViewNotAllowedError("action") + return await apiRequest(method, `${resourcesPath}${id}/${command}`, data, init) + }, + [apiRequest, allowAction, resourcesPath] + ) + + const listResources = useCallback( async () => { try { setResources(await apiList()) } catch(e) { - return { error: e } + setError(e) + throw e } + setError(null) return {} }, [apiList], ) - const refreshResource = useCallback( + const retrieveResource = useCallback( async (pk) => { - try { - const refreshedResource = await apiRetrieve(pk) - setResources(resources => resources.map(resource => { - if(resource[pkName] === pk) { - return refreshedResource - } - return resource - })) - } - catch(e) { - return { error: e } - } - return {} + const refreshedResource = await apiRetrieve(pk) + setResources(resources => resources.map(resource => { + if(resource[pkName] === pk) { + return refreshedResource + } + return resource + })) + return refreshedResource }, [apiRetrieve, pkName], ) const createResource = useCallback( async (data) => { - try { - const newResource = await apiCreate(data) - setResources(resources => [...resources, newResource]) - } - catch(e) { - return { error: e } - } - return {} + const newResource = await apiCreate(data) + setResources(resources => [...resources, newResource]) + return newResource }, [apiCreate], ) const editResource = useCallback( async (pk, data) => { - try { - const editedResource = await apiEdit(pk, data) - setResources(resources => resources.map(resource => { - if(resource[pkName] === pk) { - return editedResource - } - return resource - })) - } - catch(e) { - return { error: e } - } - return {} + const editedResource = await apiEdit(pk, data) + setResources(resources => resources.map(resource => { + if(resource[pkName] === pk) { + return editedResource + } + return resource + })) + return editedResource }, [apiEdit, pkName], ) const destroyResource = useCallback( async (pk) => { - try { - await apiDestroy(pk) - setResources(resources => resources.filter(resource => resource[pkName] !== pk)) - } - catch(e) { - return { error: e } - } - return {} + await apiDestroy(pk) + setResources(resources => resources.filter(resource => resource[pkName] !== pk)) + return null }, [apiDestroy, pkName], ) useEffect( - () => { - if(refreshOnStart && !(loaded || running)) { - // noinspection JSIgnoredPromiseFromCall - refreshResources() + async () => { + if(allowList && !firstLoad && !running) { + await listResources() + setFirstLoad(true) } }, - [loaded, refreshResources, running, refreshOnStart], + [listResources, firstLoad, running, allowList], ) return { abort, resources, + firstLoad, running, - loaded, + error, apiRequest, + allowList, apiList, + listResources, + allowRetrieve, apiRetrieve, + retrieveResource, + allowCreate, apiCreate, - apiEdit, - apiDestroy, - refreshResources, - refreshResource, createResource, + allowEdit, + apiEdit, editResource, + allowDestroy, + apiDestroy, destroyResource, + apiCommand, + apiAction, } } \ No newline at end of file diff --git a/nest_frontend/routes/PageRepositories.js b/nest_frontend/routes/PageRepositories.js index e5635e3..80418dd 100644 --- a/nest_frontend/routes/PageRepositories.js +++ b/nest_frontend/routes/PageRepositories.js @@ -1,43 +1,45 @@ -import React, { useCallback } from "react" +import React, { useCallback, useContext } from "react" import Style from "./PageRepositories.module.css" import classNames from "classnames" -import BoxRepositoriesActive from "../components/interactive/BoxRepositoriesActive" -import BoxRepositoriesArchived from "../components/interactive/BoxRepositoriesArchived" import useBackendViewset from "../hooks/useBackendViewset" +import BoxRepositories from "../components/interactive/BoxRepositories" +import { useHistory } from "react-router" +import ContextLanguage from "../contexts/ContextLanguage" export default function PageRepositories({ children, className, ...props }) { const bv = useBackendViewset("/api/v1/repositories/", "id") + const history = useHistory() + const {strings} = useContext(ContextLanguage) - const archiveRepository = useCallback( + const archive = useCallback( async (pk) => { - try { - await bv.apiRequest("PATCH", `/api/v1/repositories/${pk}`, { - "close": true, - }) - await bv.refreshResource(pk) - } - catch(e) { - return { error: e } - } - return {} + await bv.apiRequest("PATCH", `/api/v1/repositories/${pk}`, { + "close": true, + }) + await bv.refreshResource(pk) }, [bv], ) return (
- r.is_active) : null} - archiveRepository={archiveRepository} - destroyRepository={bv.destroyResource} + r.is_active)} + view={pk => history.push(`/repositories/${pk}`)} + archive={archive} + edit={bv.editResource} /> - !r.is_active) : null} - archiveRepository={archiveRepository} - destroyRepository={bv.destroyResource} + !r.is_active)} + view={pk => history.push(`/repositories/${pk}`)} + destroy={bv.destroyResource} />
) diff --git a/nest_frontend/routes/PageRepository.js b/nest_frontend/routes/PageRepository.js index 4e6e82b..3386b2a 100644 --- a/nest_frontend/routes/PageRepository.js +++ b/nest_frontend/routes/PageRepository.js @@ -3,29 +3,48 @@ import Style from "./PageRepository.module.css" import classNames from "classnames" import BoxRepositoryTweets from "../components/interactive/BoxRepositoryTweets" import BoxWordcloud from "../components/interactive/BoxWordcloud" -import ButtonIconOnly from "../components/base/ButtonIconOnly" -import { faAt, faChartBar, faClock, faCloud, faHashtag, faMap, faMapPin } from "@fortawesome/free-solid-svg-icons" -import BoxFull from "../components/base/BoxFull" import BoxHeader from "../components/base/BoxHeader" import PickerVisualization from "../components/interactive/PickerVisualization" import PickerFilter from "../components/interactive/PickerFilter" +import useBackendViewset from "../hooks/useBackendViewset" +import useBackendResource from "../hooks/useBackendResource" +import { faFolder, faFolderOpen, faTrash } from "@fortawesome/free-solid-svg-icons" +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" +import { useParams } from "react-router" +import Loading from "../components/base/Loading" export default function PageRepository({ className, ...props }) { + const {id} = useParams() + const [visualizationTab, setVisualizationTab] = useState("wordcloud") const [addFilterTab, setAddFilterTab] = useState("hashtag") - const tweets = [ + const repositoryBr = useBackendResource( + `/api/v1/repositories/${id}`, { - "conditions": [], - "content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec posuere lacinia eleifend. Maecenas a neque augue. Nulla dapibus lobortis gravida. Quisque quis ultricies elit. Donec in tortor augue. Cras eget aliquam felis. Nunc tempor, ipsum in lobortis tristique, nunc ante velit.", - "insert_time": "2021-05-18T18:56Z", - "location": null, - "place": "Casa mia", - "poster": "USteffo", - "snowflake": "1394698342282809344", - }, - ] + retrieve: true, + edit: true, + destroy: true, + action: false, + } + ) + const repository = repositoryBr.error ? null : repositoryBr.resource + + const tweetsBv = useBackendViewset( + `/api/v1/repositories/${id}/tweets/`, + "snowflake", + { + list: true, + create: false, + retrieve: false, + edit: false, + destroy: false, + command: false, + action: false, + } + ) + const tweets = tweetsBv.resources && tweetsBv.error ? [] : tweetsBv.resources const words = useMemo( () => { @@ -57,12 +76,28 @@ export default function PageRepository({ className, ...props }) { [tweets] ) - - - return ( -
+ let contents; + if(!repositoryBr.firstLoad || !tweetsBv.firstLoad) { + contents = <> - Repository Senza Nome + + + + } + else if(repository === null) { + console.debug("repositoryBr: ", repositoryBr, ", tweetsBv: ", tweetsBv) + + // TODO: Translate this! + contents = <> + + This repository was deleted. + + + } + else { + contents = <> + + {repository.name} {visualizationTab === "wordcloud" ? - - : null} + + : null} + + } + + return ( +
+ {contents}
) } diff --git a/nest_frontend/utils/Errors.js b/nest_frontend/utils/Errors.js new file mode 100644 index 0000000..433a73a --- /dev/null +++ b/nest_frontend/utils/Errors.js @@ -0,0 +1,69 @@ +class NestError { + +} + + +class ViewNotAllowedError extends NestError { + view + + constructor(view) { + super(); + + this.view = view + } +} + + +class ServerNotConfiguredError extends NestError { + +} + + +class FetchAlreadyRunningError extends NestError { + +} + + +class FetchError extends NestError { + status + statusText + + constructor(status, statusText) { + super() + + this.status = status + this.statusText = statusText + } +} + + +class DecodeError extends FetchError { + error + + constructor(status, statusText, error) { + super(status, statusText) + + this.error = error + } +} + + +class ResultError extends FetchError { + status + statusText + data + + constructor(status, statusText, data) { + super(status, statusText) + + this.data = data + } + + getMsg() { + return this.data.msg + } + + getCode() { + return this.data.code + } +}