1
Fork 0
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:
Stefano Pigozzi 2021-05-12 04:10:36 +02:00
parent abac554e95
commit 460da4fadd
Signed by untrusted user who does not match committer: steffo
GPG key ID: 6965406171929D01
12 changed files with 318 additions and 43 deletions

View file

@ -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" />

View file

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

View file

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

View 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>
)
}

View file

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

View 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}
/>
)
}

View file

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

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

View file

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

View file

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

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