1
Fork 0
mirror of https://github.com/pds-nest/nest.git synced 2024-11-28 23:44:19 +00:00

🔧 Refactor many things, improving performance

This commit is contained in:
Steffo 2021-05-19 19:56:41 +02:00
parent fdb3e64dad
commit d946e46431
Signed by: steffo
GPG key ID: 6965406171929D01
12 changed files with 597 additions and 368 deletions

View file

@ -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 = <Loading/>
}
else if(repositories.length === 0) {
contents = <i>{strings.emptyMenu}</i>
}
else {
contents = repositories.map(repo => (
<SummaryRepository
key={repo["id"]}
repo={repo}
view={view ? () => 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 (
<BoxFullScrollable {...props}>
{contents}
</BoxFullScrollable>
)
}

View file

@ -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 = <Loading/>
}
else if(repositories.length === 0) {
contents = <i>{strings.emptyMenu}.</i>
}
else {
contents = repositories.map(repo => (
<SummaryRepository
key={repo["id"]}
repo={repo}
icon={faFolderOpen}
archiveSelf={() => archiveRepository(repo["id"])}
deleteSelf={() => destroyRepository(repo["id"])}
canArchive={true}
canEdit={true}
canDelete={false}
running={running}
/>
))
}
return (
<BoxFullScrollable header={strings.menuActive} {...props}>
{contents}
</BoxFullScrollable>
)
}

View file

@ -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 = <Loading/>
}
else if(repositories.length === 0) {
contents = <i>{strings.emptyMenu}.</i>
}
else {
contents = repositories.map(repo => (
<SummaryRepository
key={repo["id"]}
repo={repo}
icon={faFolderOpen}
archiveSelf={() => archiveRepository(repo["id"])}
deleteSelf={() => destroyRepository(repo["id"])}
canArchive={false}
canEdit={false}
canDelete={repo["owner"]["username"] === user["username"]}
running={running}
/>
))
}
return (
<BoxFullScrollable header={strings.menuArchived} {...props}>
{contents}
</BoxFullScrollable>
)
}

View file

@ -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 repo - The repository to display.
* @param refresh - Function that can be called to refresh the repositories list. * @param view - Function with no parameters to call when the view repository button is clicked.
* @param canDelete - If the Delete button should be displayed or not. * @param archive - Function with no parameters to call when the archive repository button is clicked.
* @param deleteSelf - Function to call when the Delete button is pressed. * @param edit - Function with no parameters to call when the edit repository button is clicked.
* @param canEdit - If the Edit button should be displayed or not. * @param destroy - Function with no parameters to call when the delete repository button is clicked.
* @param canArchive - If the Archive button should be displayed or not. * @param running - If an action is running on the viewset.
* @param archiveSelf - Function to call when the Archive button is pressed. * @param className - Additional class(es) to append to the summary.
* @param running - If an action is currently running. * @param props - Additional props to pass to the summary.
* @param className - Additional class(es) to be added to the outer box.
* @param props - Additional props to pass to the outer box.
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
export default function SummaryRepository( 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 { strings } = useContext(ContextLanguage)
const onRepoClick = () => {
history.push(`/repositories/${repo.id}`)
}
const onEditClick = () => {
history.push(`/repositories/${repo.id}/edit`)
}
return ( return (
<SummaryBase {...props}> <SummaryBase {...props}>
<SummaryLeft <SummaryLeft
icon={repo.is_active ? faFolderOpen : faFolder} icon={repo.is_active ? faFolderOpen : faFolder}
title={repo.name} title={repo.name}
subtitle={repo.owner ? repo.owner.username : null} subtitle={repo.owner ? repo.owner.username : null}
onClick={onRepoClick} onClick={view}
/> />
<SummaryLabels <SummaryLabels
upperLabel={strings.created} upperLabel={strings.created}
upperValue={repo.start ? new Date(repo.start).toLocaleString() : null} upperValue={repo.start ? new Date(repo.start).toLocaleString() : null}
lowerLabel={strings.archived} lowerLabel={strings.archived}
lowerValue={repo.end ? new Date(repo.end).toLocaleString() : null} lowerValue={repo.end ? new Date(repo.end).toLocaleString() : null}
/> />
{canDelete ?
{destroy ?
<SummaryButton <SummaryButton
color={"Red"} color={"Red"}
icon={faTrash} icon={faTrash}
onClick={deleteSelf} onClick={() => destroy(repo["id"])}
disabled={running} disabled={running}
> >
{strings.delete} {strings.delete}
</SummaryButton> </SummaryButton>
: null} : null}
{canEdit ?
<SummaryButton {archive ?
color={"Yellow"}
icon={faPencilAlt}
onClick={onEditClick}
disabled={running}
>
{strings.edit}
</SummaryButton>
: null}
{canArchive ?
<SummaryButton <SummaryButton
color={"Grey"} color={"Grey"}
icon={faArchive} icon={faArchive}
onClick={archiveSelf} onClick={() => archive(repo["id"])}
disabled={running} disabled={running}
> >
{strings.archive} {strings.archive}
</SummaryButton> </SummaryButton>
: null} : null}
{edit ?
<SummaryButton
color={"Yellow"}
icon={faPencilAlt}
onClick={() => edit(repo["id"])}
disabled={running}
>
{strings.edit}
</SummaryButton>
: null}
<SummaryRight/> <SummaryRight/>
</SummaryBase> </SummaryBase>
) )

View file

@ -9,6 +9,7 @@ import { useCallback, useState } from "react"
* @param path - The HTTP path to fetch the data at. * @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 body - The body of the HTTP request (it will be JSONified before being sent).
* @param init - Additional `init` parameters to pass to `fetch`. * @param init - Additional `init` parameters to pass to `fetch`.
* @deprecated since 2021-05-19
*/ */
export default function useBackend(fetchData, method, path, body, init) { export default function useBackend(fetchData, method, path, body, init) {
const [error, setError] = useState(null) const [error, setError] = useState(null)

View file

@ -10,6 +10,7 @@ import { useEffect } from "react"
* @param path - The HTTP path to fetch the data at. * @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 body - The body of the HTTP request (it will be JSONified before being sent).
* @param init - Additional `init` parameters to pass to `fetch`. * @param init - Additional `init` parameters to pass to `fetch`.
* @deprecated since 2021-05-19
*/ */
export default function useBackendImmediately(fetchData, method, path, body, init) { export default function useBackendImmediately(fetchData, method, path, body, init) {
const { data, error, loading, fetchNow } = useBackend(fetchData, method, path, body, init) const { data, error, loading, fetchNow } = useBackend(fetchData, method, path, body, init)

View file

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

View file

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

View file

@ -1,7 +1,5 @@
import { useCallback, useContext, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
import ContextServer from "../contexts/ContextServer" import useBackendRequest from "./useBackendRequest"
import ContextUser from "../contexts/ContextUser"
import makeURLSearchParams from "../utils/makeURLSearchParams"
/** /**
@ -9,134 +7,97 @@ import makeURLSearchParams from "../utils/makeURLSearchParams"
* *
* @param resourcesPath - The path of the resource directory. * @param resourcesPath - The path of the resource directory.
* @param pkName - The name of the primary key attribute of the elements. * @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) { export default function useBackendViewset(resourcesPath, pkName,
const { server } = useContext(ContextServer) {
const configured = server !== null 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 [firstLoad, setFirstLoad] = useState(false)
const loggedIn = user !== null const [resources, setResources] = useState([])
const [error, setError] = useState(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 apiList = useCallback( const apiList = useCallback(
async (init) => await apiRequest("GET", `${resourcesPath}`, undefined, init), async (init) => {
[apiRequest, resourcesPath], if(!allowList) throw new ViewNotAllowedError("list")
return await apiRequest("GET", `${resourcesPath}`, undefined, init)
},
[apiRequest, allowList, resourcesPath],
) )
const apiRetrieve = useCallback( const apiRetrieve = useCallback(
async (id, init) => await apiRequest("GET", `${resourcesPath}${id}`, undefined, init), async (id, init) => {
[apiRequest, resourcesPath], if(!allowRetrieve) throw new ViewNotAllowedError("retrieve")
return await apiRequest("GET", `${resourcesPath}${id}`, undefined, init)
},
[apiRequest, allowRetrieve, resourcesPath],
) )
const apiCreate = useCallback( const apiCreate = useCallback(
async (data, init) => await apiRequest("POST", `${resourcesPath}`, data, init), async (data, init) => {
[apiRequest, resourcesPath], if(!allowCreate) throw new ViewNotAllowedError("create")
return await apiRequest("POST", `${resourcesPath}`, data, init)
},
[apiRequest, allowCreate, resourcesPath],
) )
const apiEdit = useCallback( const apiEdit = useCallback(
async (id, data, init) => await apiRequest("PUT", `${resourcesPath}${id}`, data, init), async (id, data, init) => {
[apiRequest, resourcesPath], if(!allowEdit) throw new ViewNotAllowedError("edit")
return await apiRequest("PUT", `${resourcesPath}${id}`, data, init)
},
[apiRequest, allowEdit, resourcesPath],
) )
const apiDestroy = useCallback( const apiDestroy = useCallback(
async (id, init) => await apiRequest("DELETE", `${resourcesPath}${id}`, undefined, init), async (id, init) => {
[apiRequest, resourcesPath], 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 () => { async () => {
try { try {
setResources(await apiList()) setResources(await apiList())
} }
catch(e) { catch(e) {
return { error: e } setError(e)
throw e
} }
setError(null)
return {} return {}
}, },
[apiList], [apiList],
) )
const refreshResource = useCallback( const retrieveResource = useCallback(
async (pk) => { async (pk) => {
try {
const refreshedResource = await apiRetrieve(pk) const refreshedResource = await apiRetrieve(pk)
setResources(resources => resources.map(resource => { setResources(resources => resources.map(resource => {
if(resource[pkName] === pk) { if(resource[pkName] === pk) {
@ -144,32 +105,22 @@ export default function useBackendViewset(resourcesPath, pkName, refreshOnStart
} }
return resource return resource
})) }))
} return refreshedResource
catch(e) {
return { error: e }
}
return {}
}, },
[apiRetrieve, pkName], [apiRetrieve, pkName],
) )
const createResource = useCallback( const createResource = useCallback(
async (data) => { async (data) => {
try {
const newResource = await apiCreate(data) const newResource = await apiCreate(data)
setResources(resources => [...resources, newResource]) setResources(resources => [...resources, newResource])
} return newResource
catch(e) {
return { error: e }
}
return {}
}, },
[apiCreate], [apiCreate],
) )
const editResource = useCallback( const editResource = useCallback(
async (pk, data) => { async (pk, data) => {
try {
const editedResource = await apiEdit(pk, data) const editedResource = await apiEdit(pk, data)
setResources(resources => resources.map(resource => { setResources(resources => resources.map(resource => {
if(resource[pkName] === pk) { if(resource[pkName] === pk) {
@ -177,54 +128,53 @@ export default function useBackendViewset(resourcesPath, pkName, refreshOnStart
} }
return resource return resource
})) }))
} return editedResource
catch(e) {
return { error: e }
}
return {}
}, },
[apiEdit, pkName], [apiEdit, pkName],
) )
const destroyResource = useCallback( const destroyResource = useCallback(
async (pk) => { async (pk) => {
try {
await apiDestroy(pk) await apiDestroy(pk)
setResources(resources => resources.filter(resource => resource[pkName] !== pk)) setResources(resources => resources.filter(resource => resource[pkName] !== pk))
} return null
catch(e) {
return { error: e }
}
return {}
}, },
[apiDestroy, pkName], [apiDestroy, pkName],
) )
useEffect( useEffect(
() => { async () => {
if(refreshOnStart && !(loaded || running)) { if(allowList && !firstLoad && !running) {
// noinspection JSIgnoredPromiseFromCall await listResources()
refreshResources() setFirstLoad(true)
} }
}, },
[loaded, refreshResources, running, refreshOnStart], [listResources, firstLoad, running, allowList],
) )
return { return {
abort, abort,
resources, resources,
firstLoad,
running, running,
loaded, error,
apiRequest, apiRequest,
allowList,
apiList, apiList,
listResources,
allowRetrieve,
apiRetrieve, apiRetrieve,
retrieveResource,
allowCreate,
apiCreate, apiCreate,
apiEdit,
apiDestroy,
refreshResources,
refreshResource,
createResource, createResource,
allowEdit,
apiEdit,
editResource, editResource,
allowDestroy,
apiDestroy,
destroyResource, destroyResource,
apiCommand,
apiAction,
} }
} }

View file

@ -1,43 +1,45 @@
import React, { useCallback } from "react" import React, { useCallback, useContext } from "react"
import Style from "./PageRepositories.module.css" import Style from "./PageRepositories.module.css"
import classNames from "classnames" import classNames from "classnames"
import BoxRepositoriesActive from "../components/interactive/BoxRepositoriesActive"
import BoxRepositoriesArchived from "../components/interactive/BoxRepositoriesArchived"
import useBackendViewset from "../hooks/useBackendViewset" 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 }) { export default function PageRepositories({ children, className, ...props }) {
const bv = useBackendViewset("/api/v1/repositories/", "id") const bv = useBackendViewset("/api/v1/repositories/", "id")
const history = useHistory()
const {strings} = useContext(ContextLanguage)
const archiveRepository = useCallback( const archive = useCallback(
async (pk) => { async (pk) => {
try {
await bv.apiRequest("PATCH", `/api/v1/repositories/${pk}`, { await bv.apiRequest("PATCH", `/api/v1/repositories/${pk}`, {
"close": true, "close": true,
}) })
await bv.refreshResource(pk) await bv.refreshResource(pk)
}
catch(e) {
return { error: e }
}
return {}
}, },
[bv], [bv],
) )
return ( return (
<div className={classNames(Style.PageRepositories, className)} {...props}> <div className={classNames(Style.PageRepositories, className)} {...props}>
<BoxRepositoriesActive <BoxRepositories
repositories={bv.loaded ? bv.resources.filter(r => r.is_active) : null} header={strings.menuActive}
archiveRepository={archiveRepository} loading={!bv.firstLoad}
destroyRepository={bv.destroyResource}
running={bv.running} running={bv.running}
repositories={bv.resources.filter(r => r.is_active)}
view={pk => history.push(`/repositories/${pk}`)}
archive={archive}
edit={bv.editResource}
/> />
<BoxRepositoriesArchived <BoxRepositories
repositories={bv.loaded ? bv.resources.filter(r => !r.is_active) : null} header={strings.menuArchived}
archiveRepository={archiveRepository} loading={!bv.firstLoad}
destroyRepository={bv.destroyResource}
running={bv.running} running={bv.running}
repositories={bv.resources.filter(r => !r.is_active)}
view={pk => history.push(`/repositories/${pk}`)}
destroy={bv.destroyResource}
/> />
</div> </div>
) )

View file

@ -3,29 +3,48 @@ import Style from "./PageRepository.module.css"
import classNames from "classnames" import classNames from "classnames"
import BoxRepositoryTweets from "../components/interactive/BoxRepositoryTweets" import BoxRepositoryTweets from "../components/interactive/BoxRepositoryTweets"
import BoxWordcloud from "../components/interactive/BoxWordcloud" 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 BoxHeader from "../components/base/BoxHeader"
import PickerVisualization from "../components/interactive/PickerVisualization" import PickerVisualization from "../components/interactive/PickerVisualization"
import PickerFilter from "../components/interactive/PickerFilter" 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 }) { export default function PageRepository({ className, ...props }) {
const {id} = useParams()
const [visualizationTab, setVisualizationTab] = useState("wordcloud") const [visualizationTab, setVisualizationTab] = useState("wordcloud")
const [addFilterTab, setAddFilterTab] = useState("hashtag") const [addFilterTab, setAddFilterTab] = useState("hashtag")
const tweets = [ const repositoryBr = useBackendResource(
`/api/v1/repositories/${id}`,
{ {
"conditions": [], retrieve: true,
"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.", edit: true,
"insert_time": "2021-05-18T18:56Z", destroy: true,
"location": null, action: false,
"place": "Casa mia", }
"poster": "USteffo", )
"snowflake": "1394698342282809344", 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( const words = useMemo(
() => { () => {
@ -57,12 +76,28 @@ export default function PageRepository({ className, ...props }) {
[tweets] [tweets]
) )
let contents;
if(!repositoryBr.firstLoad || !tweetsBv.firstLoad) {
return ( contents = <>
<div className={classNames(Style.PageRepository, className)} {...props}>
<BoxHeader className={Style.Header}> <BoxHeader className={Style.Header}>
Repository Senza Nome <Loading/>
</BoxHeader>
</>
}
else if(repository === null) {
console.debug("repositoryBr: ", repositoryBr, ", tweetsBv: ", tweetsBv)
// TODO: Translate this!
contents = <>
<BoxHeader className={Style.Header}>
<FontAwesomeIcon icon={faTrash}/> <i>This repository was deleted.</i>
</BoxHeader>
</>
}
else {
contents = <>
<BoxHeader className={Style.Header}>
<FontAwesomeIcon icon={repository.is_active ? faFolderOpen : faFolder}/> {repository.name}
</BoxHeader> </BoxHeader>
<BoxRepositoryTweets <BoxRepositoryTweets
@ -87,6 +122,12 @@ export default function PageRepository({ className, ...props }) {
currentTab={addFilterTab} currentTab={addFilterTab}
setTab={setAddFilterTab} setTab={setAddFilterTab}
/> />
</>
}
return (
<div className={classNames(Style.PageRepository, className)} {...props}>
{contents}
</div> </div>
) )
} }

View file

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