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:
parent
fdb3e64dad
commit
d946e46431
12 changed files with 597 additions and 368 deletions
52
nest_frontend/components/interactive/BoxRepositories.js
Normal file
52
nest_frontend/components/interactive/BoxRepositories.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
98
nest_frontend/hooks/useBackendRequest.js
Normal file
98
nest_frontend/hooks/useBackendRequest.js
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
139
nest_frontend/hooks/useBackendResource.js
Normal file
139
nest_frontend/hooks/useBackendResource.js
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
69
nest_frontend/utils/Errors.js
Normal file
69
nest_frontend/utils/Errors.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue