mirror of
https://github.com/pds-nest/nest.git
synced 2024-11-25 22:44:19 +00:00
💥 Refactor quite a lot of things
This commit is contained in:
parent
abac554e95
commit
460da4fadd
12 changed files with 318 additions and 43 deletions
|
@ -13,6 +13,7 @@
|
||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ExceptionCaughtLocallyJS" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
|
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
<option name="myValues">
|
<option name="myValues">
|
||||||
<value>
|
<value>
|
||||||
|
@ -30,7 +31,7 @@
|
||||||
<inspection_tool class="LessResolvedByNameOnly" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="LessResolvedByNameOnly" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="LessUnresolvedMixin" enabled="true" level="ERROR" enabled_by_default="true" />
|
<inspection_tool class="LessUnresolvedMixin" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
<inspection_tool class="LessUnresolvedVariable" enabled="true" level="ERROR" enabled_by_default="true" />
|
<inspection_tool class="LessUnresolvedVariable" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
<inspection_tool class="LongLine" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
<inspection_tool class="LongLine" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
<inspection_tool class="PoetryPackageVersion" enabled="true" level="SERVER PROBLEM" enabled_by_default="true" />
|
<inspection_tool class="PoetryPackageVersion" enabled="true" level="SERVER PROBLEM" enabled_by_default="true" />
|
||||||
<inspection_tool class="ProblematicWhitespace" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
<inspection_tool class="ProblematicWhitespace" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="PyAbstractClassInspection" enabled="true" level="ERROR" enabled_by_default="true" />
|
<inspection_tool class="PyAbstractClassInspection" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
|
|
@ -11,6 +11,10 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Button[disabled] {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.Button:focus-visible {
|
.Button:focus-visible {
|
||||||
outline: 4px solid var(--outline);
|
outline: 4px solid var(--outline);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,30 @@
|
||||||
import React, { useContext, useMemo, useState } from "react"
|
import React, { useCallback, useState } from "react"
|
||||||
import FormLabelled from "../base/FormLabelled"
|
import FormLabelled from "../base/FormLabelled"
|
||||||
import FormLabel from "../base/formparts/FormLabel"
|
import FormLabel from "../base/formparts/FormLabel"
|
||||||
import InputWithIcon from "../base/InputWithIcon"
|
import InputWithIcon from "../base/InputWithIcon"
|
||||||
import { faEnvelope, faKey, faPlus, faUser } from "@fortawesome/free-solid-svg-icons"
|
import { faEnvelope, faKey, faPlus, faUser } from "@fortawesome/free-solid-svg-icons"
|
||||||
import FormButton from "../base/formparts/FormButton"
|
import FormButton from "../base/formparts/FormButton"
|
||||||
import BoxFull from "../base/BoxFull"
|
import BoxFull from "../base/BoxFull"
|
||||||
import useBackend from "../../hooks/useBackend"
|
|
||||||
import ContextUser from "../../contexts/ContextUser"
|
|
||||||
import FormAlert from "../base/formparts/FormAlert"
|
import FormAlert from "../base/formparts/FormAlert"
|
||||||
|
|
||||||
|
|
||||||
export default function BoxUserCreate({ children, ...props }) {
|
export default function BoxUserCreate({ createUser, running, ...props }) {
|
||||||
const { fetchDataAuth } = useContext(ContextUser)
|
|
||||||
const [username, setUsername] = useState("")
|
const [username, setUsername] = useState("")
|
||||||
const [email, setEmail] = useState("")
|
const [email, setEmail] = useState("")
|
||||||
const [password, setPassword] = useState("")
|
const [password, setPassword] = useState("")
|
||||||
|
const [error, setError] = useState(undefined)
|
||||||
|
|
||||||
const body = useMemo(
|
const onButtonClick = useCallback(
|
||||||
() => {
|
async () => {
|
||||||
return {
|
const result = await createUser({
|
||||||
"email": email,
|
"email": email,
|
||||||
"username": username,
|
"username": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
}
|
})
|
||||||
|
setError(result.error)
|
||||||
},
|
},
|
||||||
[email, username, password],
|
[createUser, email, username, password],
|
||||||
)
|
)
|
||||||
const { error, fetchNow: createNow } = useBackend(fetchDataAuth, "POST", "/api/v1/users/", body)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxFull header={"Crea utente"} {...props}>
|
<BoxFull header={"Crea utente"} {...props}>
|
||||||
|
@ -63,12 +61,12 @@ export default function BoxUserCreate({ children, ...props }) {
|
||||||
<FormButton
|
<FormButton
|
||||||
color={"Green"}
|
color={"Green"}
|
||||||
icon={faPlus}
|
icon={faPlus}
|
||||||
onClick={() => createNow()}
|
onClick={onButtonClick}
|
||||||
|
disabled={running}
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormLabelled>
|
</FormLabelled>
|
||||||
{children}
|
|
||||||
</BoxFull>
|
</BoxFull>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
22
code/frontend/src/components/interactive/BoxUserList.js
Normal file
22
code/frontend/src/components/interactive/BoxUserList.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React from "react"
|
||||||
|
import Loading from "../base/Loading"
|
||||||
|
import BoxFullScrollable from "../base/BoxFullScrollable"
|
||||||
|
import SummaryUser from "./SummaryUser"
|
||||||
|
|
||||||
|
|
||||||
|
export default function BoxUserList({ users, destroyUser, running, ...props }) {
|
||||||
|
let contents
|
||||||
|
if(users === null) {
|
||||||
|
contents = <Loading/>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
contents = users.map(user =>
|
||||||
|
<SummaryUser key={user["email"]} destroyUser={destroyUser} running={running} user={user}/>)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoxFullScrollable header={"Elenco utenti"} {...props}>
|
||||||
|
{contents}
|
||||||
|
</BoxFullScrollable>
|
||||||
|
)
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import {
|
||||||
faFolder,
|
faFolder,
|
||||||
faHome,
|
faHome,
|
||||||
faKey,
|
faKey,
|
||||||
faUser,
|
faUserCog,
|
||||||
faWrench,
|
faWrench,
|
||||||
} from "@fortawesome/free-solid-svg-icons"
|
} from "@fortawesome/free-solid-svg-icons"
|
||||||
import ContextUser from "../../contexts/ContextUser"
|
import ContextUser from "../../contexts/ContextUser"
|
||||||
|
@ -46,7 +46,7 @@ export default function Sidebar({ className, ...props }) {
|
||||||
{
|
{
|
||||||
user.isAdmin ?
|
user.isAdmin ?
|
||||||
<>
|
<>
|
||||||
<ButtonSidebar to={"/users"} icon={faUser}>Users</ButtonSidebar>
|
<ButtonSidebar to={"/users"} icon={faUserCog}>Utenti</ButtonSidebar>
|
||||||
</>
|
</>
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
|
|
38
code/frontend/src/components/interactive/SummaryUser.js
Normal file
38
code/frontend/src/components/interactive/SummaryUser.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React, { useContext } from "react"
|
||||||
|
import Summary from "../base/Summary"
|
||||||
|
import { faStar, faTrash, faUser } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
import Button from "../base/Button"
|
||||||
|
import ContextUser from "../../contexts/ContextUser"
|
||||||
|
|
||||||
|
|
||||||
|
export default function SummaryUser({ user, destroyUser, running, ...props }) {
|
||||||
|
const { user: loggedUser } = useContext(ContextUser)
|
||||||
|
|
||||||
|
const buttons = <>
|
||||||
|
{loggedUser.email !== user.email ?
|
||||||
|
<Button
|
||||||
|
color={"Red"}
|
||||||
|
icon={faTrash}
|
||||||
|
onClick={async () => {
|
||||||
|
// TODO: Errors are not caught here. Where should they be displayed?
|
||||||
|
await destroyUser(user["email"])
|
||||||
|
}}
|
||||||
|
disabled={running}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
: null}
|
||||||
|
</>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Summary
|
||||||
|
icon={user.isAdmin ? faStar : faUser}
|
||||||
|
title={user.username}
|
||||||
|
subtitle={user.email}
|
||||||
|
upperLabel={"Tipo"}
|
||||||
|
upperValue={user.isAdmin ? "Amministratore" : "Utente"}
|
||||||
|
buttons={buttons}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -64,7 +64,7 @@ export default function RepositoryEditor({
|
||||||
"start": null,
|
"start": null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[_conditions, _end, _evaluationMode, id, _name, user, _start],
|
[_conditions, _evaluationMode, id, _name, user],
|
||||||
)
|
)
|
||||||
const { error, loading, fetchNow } = useBackend(fetchDataAuth, method, path, body)
|
const { error, loading, fetchNow } = useBackend(fetchDataAuth, method, path, body)
|
||||||
|
|
||||||
|
|
204
code/frontend/src/hooks/useBackendViewset.js
Normal file
204
code/frontend/src/hooks/useBackendViewset.js
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
import { useCallback, useContext, useEffect, useState } from "react"
|
||||||
|
import ContextServer from "../contexts/ContextServer"
|
||||||
|
import ContextUser from "../contexts/ContextUser"
|
||||||
|
import makeURLSearchParams from "../utils/makeURLSearchParams"
|
||||||
|
|
||||||
|
|
||||||
|
export default function useBackendViewset(resourcesPath, pkName) {
|
||||||
|
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 [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(
|
||||||
|
async (init) => await apiRequest("GET", `${resourcesPath}`, undefined, init),
|
||||||
|
[apiRequest, resourcesPath],
|
||||||
|
)
|
||||||
|
|
||||||
|
const apiRetrieve = useCallback(
|
||||||
|
async (id, init) => await apiRequest("GET", `${resourcesPath}${id}`, undefined, init),
|
||||||
|
[apiRequest, resourcesPath],
|
||||||
|
)
|
||||||
|
|
||||||
|
const apiCreate = useCallback(
|
||||||
|
async (data, init) => await apiRequest("POST", `${resourcesPath}`, data, init),
|
||||||
|
[apiRequest, resourcesPath],
|
||||||
|
)
|
||||||
|
|
||||||
|
const apiEdit = useCallback(
|
||||||
|
async (id, data, init) => await apiRequest("PUT", `${resourcesPath}${id}`, data, init),
|
||||||
|
[apiRequest, resourcesPath],
|
||||||
|
)
|
||||||
|
|
||||||
|
const apiDestroy = useCallback(
|
||||||
|
async (id, init) => await apiRequest("DELETE", `${resourcesPath}${id}`, undefined, init),
|
||||||
|
[apiRequest, resourcesPath],
|
||||||
|
)
|
||||||
|
|
||||||
|
const refreshResources = useCallback(
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
setResources(await apiList())
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
return { error: e }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
[apiList],
|
||||||
|
)
|
||||||
|
|
||||||
|
const createResource = useCallback(
|
||||||
|
async (data) => {
|
||||||
|
try {
|
||||||
|
const newResource = await apiCreate(data)
|
||||||
|
setResources(resources => [...resources, newResource])
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
return { error: e }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
[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 {}
|
||||||
|
},
|
||||||
|
[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 {}
|
||||||
|
},
|
||||||
|
[apiDestroy, pkName],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
if(!(
|
||||||
|
loaded || running
|
||||||
|
)) {
|
||||||
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
|
refreshResources()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[loaded, refreshResources, running],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
abort,
|
||||||
|
resources,
|
||||||
|
running,
|
||||||
|
loaded,
|
||||||
|
apiList,
|
||||||
|
apiRetrieve,
|
||||||
|
apiCreate,
|
||||||
|
apiEdit,
|
||||||
|
apiDestroy,
|
||||||
|
refreshResources,
|
||||||
|
createResource,
|
||||||
|
editResource,
|
||||||
|
destroyResource,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,29 +1,22 @@
|
||||||
import React, { useContext } from "react"
|
import React from "react"
|
||||||
import Style from "./PageDashboard.module.css"
|
import Style from "./PageUsers.module.css"
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import BoxHeader from "../components/base/BoxHeader"
|
import BoxHeader from "../components/base/BoxHeader"
|
||||||
import BoxUserCreate from "../components/interactive/BoxUserCreate"
|
import BoxUserCreate from "../components/interactive/BoxUserCreate"
|
||||||
import ContextUser from "../contexts/ContextUser"
|
import useBackendViewset from "../hooks/useBackendViewset"
|
||||||
import BoxAlert from "../components/base/BoxAlert"
|
import BoxUserList from "../components/interactive/BoxUserList"
|
||||||
|
|
||||||
|
|
||||||
export default function PageUsers({ children, className, ...props }) {
|
export default function PageUsers({ children, className, ...props }) {
|
||||||
const { user } = useContext(ContextUser)
|
const bv = useBackendViewset("/api/v1/users/", "email")
|
||||||
|
|
||||||
if(!user.isAdmin) {
|
|
||||||
return (
|
|
||||||
<BoxAlert color={"Red"}>
|
|
||||||
Non sei un amministratore, pertanto non puoi gestire gli utenti della piattaforma.
|
|
||||||
</BoxAlert>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(Style.PageHome, className)} {...props}>
|
<div className={classNames(Style.PageUsers, className)} {...props}>
|
||||||
<BoxHeader className={Style.Header}>
|
<BoxHeader className={Style.Header}>
|
||||||
Gestisci utenti
|
Gestisci utenti
|
||||||
</BoxHeader>
|
</BoxHeader>
|
||||||
<BoxUserCreate/>
|
<BoxUserCreate className={Style.CreateUser} createUser={bv.createResource} running={bv.running}/>
|
||||||
|
<BoxUserList className={Style.UserList} users={bv.resources} destroyUser={bv.destroyResource} running={bv.running}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"a a"
|
"a"
|
||||||
"b c"
|
"b"
|
||||||
"d d";
|
"c";
|
||||||
grid-template-rows: auto auto 1fr;
|
grid-template-rows: auto 1fr auto;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
|
|
||||||
|
@ -18,14 +18,10 @@
|
||||||
grid-area: a;
|
grid-area: a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CreateUser {
|
.UserList {
|
||||||
grid-area: b;
|
grid-area: b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.DeleteUser {
|
.CreateUser {
|
||||||
grid-area: c;
|
grid-area: c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.UserList {
|
|
||||||
grid-area: d;
|
|
||||||
}
|
|
19
code/frontend/src/utils/makeURLSearchParams.js
Normal file
19
code/frontend/src/utils/makeURLSearchParams.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import isString from "is-string"
|
||||||
|
|
||||||
|
|
||||||
|
export default function makeURLSearchParams(obj) {
|
||||||
|
let usp = new URLSearchParams()
|
||||||
|
for(const key in obj) {
|
||||||
|
if(!obj.hasOwnProperty(key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const value = obj[key]
|
||||||
|
if(isString(value)) {
|
||||||
|
usp.set(key, value)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
usp.set(key, JSON.stringify(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return usp
|
||||||
|
}
|
Loading…
Reference in a new issue