diff --git a/code/frontend/src/components/base/Summary.module.css b/code/frontend/src/components/base/Summary.module.css
index 25eb708..f9d0b46 100644
--- a/code/frontend/src/components/base/Summary.module.css
+++ b/code/frontend/src/components/base/Summary.module.css
@@ -7,6 +7,13 @@
display: flex;
}
+.Clickable {
+ cursor: pointer;
+}
+
+.Clickable:hover {
+ filter: brightness(110%);
+}
.Left {
width: 250px;
diff --git a/code/frontend/src/components/interactive/BoxRepositoriesActive.js b/code/frontend/src/components/interactive/BoxRepositoriesActive.js
index 274cf5f..fa16042 100644
--- a/code/frontend/src/components/interactive/BoxRepositoriesActive.js
+++ b/code/frontend/src/components/interactive/BoxRepositoriesActive.js
@@ -1,7 +1,7 @@
import React, { useContext } from "react"
import BoxFull from "../base/BoxFull"
import SummaryRepository from "./SummaryRepository"
-import { faSearch } from "@fortawesome/free-solid-svg-icons"
+import { faFolderOpen } from "@fortawesome/free-solid-svg-icons"
import ContextUser from "../../contexts/ContextUser"
@@ -23,7 +23,7 @@ export default function BoxRepositoriesActive({ repositories, refresh, ...props
diff --git a/code/frontend/src/components/interactive/BoxUserCreate.js b/code/frontend/src/components/interactive/BoxUserCreate.js
new file mode 100644
index 0000000..a667a4a
--- /dev/null
+++ b/code/frontend/src/components/interactive/BoxUserCreate.js
@@ -0,0 +1,72 @@
+import React, { useCallback, useState } from "react"
+import FormLabelled from "../base/FormLabelled"
+import FormLabel from "../base/formparts/FormLabel"
+import InputWithIcon from "../base/InputWithIcon"
+import { faEnvelope, faKey, faPlus, faUser } from "@fortawesome/free-solid-svg-icons"
+import FormButton from "../base/formparts/FormButton"
+import BoxFull from "../base/BoxFull"
+import FormAlert from "../base/formparts/FormAlert"
+
+
+export default function BoxUserCreate({ createUser, running, ...props }) {
+ const [username, setUsername] = useState("")
+ const [email, setEmail] = useState("")
+ const [password, setPassword] = useState("")
+ const [error, setError] = useState(undefined)
+
+ const onButtonClick = useCallback(
+ async () => {
+ const result = await createUser({
+ "email": email,
+ "username": username,
+ "password": password,
+ })
+ setError(result.error)
+ },
+ [createUser, email, username, password],
+ )
+
+ return (
+
+
+
+ setUsername(event.target.value)}
+ />
+
+
+ setEmail(event.target.value)}
+ />
+
+
+ setPassword(event.target.value)}
+ />
+
+ {error ?
+
+ {error.toString()}
+
+ : null}
+
+ Create
+
+
+
+ )
+}
diff --git a/code/frontend/src/components/interactive/BoxUserList.js b/code/frontend/src/components/interactive/BoxUserList.js
new file mode 100644
index 0000000..e5a78ae
--- /dev/null
+++ b/code/frontend/src/components/interactive/BoxUserList.js
@@ -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 =
+ }
+ else {
+ contents = users.map(user =>
+
)
+ }
+
+ return (
+
+ {contents}
+
+ )
+}
diff --git a/code/frontend/src/components/interactive/Sidebar.js b/code/frontend/src/components/interactive/Sidebar.js
index 31ab185..ac3fffe 100644
--- a/code/frontend/src/components/interactive/Sidebar.js
+++ b/code/frontend/src/components/interactive/Sidebar.js
@@ -1,9 +1,17 @@
-import React, { Fragment, useContext } from "react"
+import React, { useContext } from "react"
import Style from "./Sidebar.module.css"
import classNames from "classnames"
import Logo from "../interactive/Logo"
import ButtonSidebar from "../base/ButtonSidebar"
-import { faCog, faExclamationTriangle, faFolder, faHome, faKey, faWrench } from "@fortawesome/free-solid-svg-icons"
+import {
+ faCog,
+ faExclamationTriangle,
+ faFolder,
+ faHome,
+ faKey,
+ faUserCog,
+ faWrench,
+} from "@fortawesome/free-solid-svg-icons"
import ContextUser from "../../contexts/ContextUser"
@@ -24,16 +32,24 @@ export default function Sidebar({ className, ...props }) {
{
user ?
-
+ <>
Dashboard
Repositories
Alerts
Settings
-
+ >
:
-
+ <>
Login
-
+ >
+ }
+ {
+ user && user.isAdmin ?
+ <>
+ Utenti
+ >
+ :
+ null
}
{
process.env.NODE_ENV === "development" ?
diff --git a/code/frontend/src/components/interactive/SummaryRepository.js b/code/frontend/src/components/interactive/SummaryRepository.js
index aa39343..2b3365f 100644
--- a/code/frontend/src/components/interactive/SummaryRepository.js
+++ b/code/frontend/src/components/interactive/SummaryRepository.js
@@ -26,9 +26,12 @@ export default function SummaryRepository(
const { fetchDataAuth } = useContext(ContextUser)
const history = useHistory()
const { fetchNow: archiveThis } = useBackend(fetchDataAuth, "PATCH", `/api/v1/repositories/${repo.id}`, { "close": true })
- const { fetchNow: unarchiveThis } = useBackend(fetchDataAuth, "PATCH", `/api/v1/repositories/${repo.id}`, { "open": true })
const { fetchNow: deletThis } = useBackend(fetchDataAuth, "DELETE", `/api/v1/repositories/${repo.id}`)
+ const onRepoClick = () => {
+ history.push(`/repositories/${repo.id}`)
+ }
+
const onEditClick = () => {
history.push(`/repositories/${repo.id}/edit`)
}
@@ -38,11 +41,6 @@ export default function SummaryRepository(
await refresh()
}
- const onUnarchiveClick = async () => {
- await unarchiveThis()
- await refresh()
- }
-
const onDeleteClick = async () => {
await deletThis()
await refresh()
@@ -71,9 +69,9 @@ export default function SummaryRepository(
: null}
>
@@ -82,10 +80,11 @@ export default function SummaryRepository(
+ {loggedUser.email !== user.email ?
+
+ : null}
+ >
+
+ return (
+
+ )
+}
diff --git a/code/frontend/src/components/providers/RepositoryEditor.js b/code/frontend/src/components/providers/RepositoryEditor.js
index aa3fdc1..9c7bff5 100644
--- a/code/frontend/src/components/providers/RepositoryEditor.js
+++ b/code/frontend/src/components/providers/RepositoryEditor.js
@@ -55,28 +55,28 @@ export default function RepositoryEditor({
() => {
return {
"conditions": _conditions,
- "end": _end,
+ "end": null,
"evaluation_mode": _evaluationMode,
"id": id,
"is_active": true,
"name": _name,
"owner": user,
- "start": _start,
+ "start": null,
}
},
- [_conditions, _end, _evaluationMode, id, _name, user, _start],
+ [_conditions, _evaluationMode, id, _name, user],
)
const { error, loading, fetchNow } = useBackend(fetchDataAuth, method, path, body)
const save = useCallback(
- () => {
+ async () => {
if(!id) {
console.info("Creating new repository with body: ", body)
}
else {
console.info("Editing repository ", id, " with body: ", body)
}
- fetchNow()
+ await fetchNow()
},
[id, body, fetchNow],
)
diff --git a/code/frontend/src/hooks/useArrayState.js b/code/frontend/src/hooks/useArrayState.js
index d52daf8..e150ef3 100644
--- a/code/frontend/src/hooks/useArrayState.js
+++ b/code/frontend/src/hooks/useArrayState.js
@@ -25,9 +25,8 @@ export default function useArrayState(def) {
console.debug("Splicing ", position, " from ArrayState")
setValue(
oldArray => {
- // TODO: Hope this doesn't break anything...
oldArray.splice(position, 1)
- return oldArray
+ return [...oldArray]
},
)
},
diff --git a/code/frontend/src/hooks/useBackendViewset.js b/code/frontend/src/hooks/useBackendViewset.js
new file mode 100644
index 0000000..d5c7709
--- /dev/null
+++ b/code/frontend/src/hooks/useBackendViewset.js
@@ -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,
+ }
+}
\ No newline at end of file
diff --git a/code/frontend/src/routes/PageUsers.js b/code/frontend/src/routes/PageUsers.js
new file mode 100644
index 0000000..377d4ec
--- /dev/null
+++ b/code/frontend/src/routes/PageUsers.js
@@ -0,0 +1,22 @@
+import React from "react"
+import Style from "./PageUsers.module.css"
+import classNames from "classnames"
+import BoxHeader from "../components/base/BoxHeader"
+import BoxUserCreate from "../components/interactive/BoxUserCreate"
+import useBackendViewset from "../hooks/useBackendViewset"
+import BoxUserList from "../components/interactive/BoxUserList"
+
+
+export default function PageUsers({ children, className, ...props }) {
+ const bv = useBackendViewset("/api/v1/users/", "email")
+
+ return (
+
+
+ Gestisci utenti
+
+
+
+
+ )
+}
diff --git a/code/frontend/src/routes/PageUsers.module.css b/code/frontend/src/routes/PageUsers.module.css
new file mode 100644
index 0000000..79b9aad
--- /dev/null
+++ b/code/frontend/src/routes/PageUsers.module.css
@@ -0,0 +1,27 @@
+.PageUsers {
+ display: grid;
+
+ grid-template-areas:
+ "a"
+ "b"
+ "c";
+ grid-template-rows: auto 1fr auto;
+ grid-template-columns: 1fr;
+
+ grid-gap: 10px;
+
+ width: 100%;
+ height: 100%;
+}
+
+.Header {
+ grid-area: a;
+}
+
+.UserList {
+ grid-area: b;
+}
+
+.CreateUser {
+ grid-area: c;
+}
diff --git a/code/frontend/src/utils/makeURLSearchParams.js b/code/frontend/src/utils/makeURLSearchParams.js
new file mode 100644
index 0000000..c699be4
--- /dev/null
+++ b/code/frontend/src/utils/makeURLSearchParams.js
@@ -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
+}
\ No newline at end of file