mirror of
https://github.com/pds-nest/nest.git
synced 2024-11-22 13:04:19 +00:00
Merge remote-tracking branch 'origin/main' into main
This commit is contained in:
commit
c4f7d65820
28 changed files with 617 additions and 67 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" />
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_cors import CORS as FlaskCORS
|
from flask_cors import CORS as FlaskCORS
|
||||||
from flask_jwt_extended import JWTManager as FlaskJWTManager
|
from flask_jwt_extended import JWTManager as FlaskJWTManager
|
||||||
from flask_sqlalchemy import SQLAlchemy as FlaskSQLAlchemy
|
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix as MiddlewareProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix as MiddlewareProxyFix
|
||||||
|
|
||||||
from . import database, routes, gestione, swagger
|
from . import database, routes, gestione, swagger
|
||||||
|
@ -107,5 +106,4 @@ app.register_error_handler(
|
||||||
|
|
||||||
# --- REVERSE PROXY ---
|
# --- REVERSE PROXY ---
|
||||||
|
|
||||||
if not __debug__:
|
rp_app = MiddlewareProxyFix(app=app, x_for=1, x_proto=0, x_host=1, x_port=0, x_prefix=0)
|
||||||
app = MiddlewareProxyFix(app=app, x_for=1, x_proto=0, x_host=1, x_port=0, x_prefix=0)
|
|
||||||
|
|
|
@ -133,7 +133,7 @@ def page_user(email):
|
||||||
except Exception:
|
except Exception:
|
||||||
ext.session.rollback()
|
ext.session.rollback()
|
||||||
return json_error("Could not delete the user."), 500
|
return json_error("Could not delete the user."), 500
|
||||||
return json_success("The user has been deleted."), 204
|
return json_success(""), 204 # "The user has been deleted."
|
||||||
elif request.method == "PATCH":
|
elif request.method == "PATCH":
|
||||||
if not email == user.email and not user.isAdmin:
|
if not email == user.email and not user.isAdmin:
|
||||||
return json_error("Thou art not authorized."), 403
|
return json_error("Thou art not authorized."), 403
|
||||||
|
@ -143,4 +143,4 @@ def page_user(email):
|
||||||
if request.json.get("password"):
|
if request.json.get("password"):
|
||||||
target.password = gen_password(request.json.get("password"))
|
target.password = gen_password(request.json.get("password"))
|
||||||
ext.session.commit()
|
ext.session.commit()
|
||||||
return json_success(target.to_json()), 204
|
return json_success(target.to_json()), 200 # 204
|
||||||
|
|
|
@ -4,4 +4,7 @@ Pytest configuration file.
|
||||||
Import global fixtures here.
|
Import global fixtures here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fixtures.flask_client import flask_client, admin_access_token, admin_headers, user_access_token, user_headers, user_exists
|
from fixtures.flask_client import flask_client, \
|
||||||
|
admin_access_token, admin_headers, \
|
||||||
|
user_access_token, user_headers, user_exists, \
|
||||||
|
repository_exists
|
||||||
|
|
|
@ -64,6 +64,13 @@ def user_exists(admin_headers, flask_client):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="package")
|
||||||
|
def repository_exists(admin_headers, flask_client):
|
||||||
|
flask_client.post(f'/api/v1/repository/', headers=admin_headers, json={
|
||||||
|
'name': 'repo1'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="package")
|
@pytest.fixture(scope="package")
|
||||||
def user_access_token(flask_client, user_exists):
|
def user_access_token(flask_client, user_exists):
|
||||||
response = flask_client.post("/api/v1/login", json={
|
response = flask_client.post("/api/v1/login", json={
|
||||||
|
|
|
@ -16,44 +16,45 @@ class TestUserGet:
|
||||||
assert r.json["result"] == "failure"
|
assert r.json["result"] == "failure"
|
||||||
assert r.json["msg"] == "Could not locate the user."
|
assert r.json["msg"] == "Could not locate the user."
|
||||||
|
|
||||||
|
|
||||||
# ritorna i dati di tutti gli utenti registrati
|
# ritorna i dati di tutti gli utenti registrati
|
||||||
class TestUserGetAll:
|
class TestUserGetAll:
|
||||||
def test_for_success(self, flask_client: Client, admin_headers):
|
def test_for_success(self, flask_client: Client, admin_headers):
|
||||||
r = flask_client.get(f'/api/v1/users/', headers=admin_headers)
|
r = flask_client.get(f'/api/v1/users/', headers=admin_headers)
|
||||||
assert b'success' in r.data
|
assert r.json["result"] == "success"
|
||||||
|
|
||||||
def test_for_failure(self, flask_client: Client, user_headers):
|
def test_for_failure(self, flask_client: Client, user_headers):
|
||||||
r = flask_client.get(f'/api/v1/users/', headers=user_headers)
|
r = flask_client.get(f'/api/v1/users/', headers=user_headers)
|
||||||
assert b'failure' in r.data
|
assert r.json["result"] == "failure"
|
||||||
|
|
||||||
|
|
||||||
class TestUserAdd:
|
class TestUserAdd:
|
||||||
def test_for_success(self, flask_client: Client, admin_headers):
|
def test_valid_user(self, flask_client: Client, admin_headers):
|
||||||
r = flask_client.post(f'/api/v1/users/', headers=admin_headers, json={
|
r = flask_client.post(f'/api/v1/users/', headers=admin_headers, json={
|
||||||
'email': 'utente1_test@nest.com',
|
'email': 'utente1_test@nest.com',
|
||||||
'password': 'password',
|
'password': 'password',
|
||||||
'username': 'utente_test'
|
'username': 'utente_test'
|
||||||
})
|
})
|
||||||
assert b'success' in r.data
|
assert r.json["result"] == "success"
|
||||||
|
|
||||||
def test_for_failure(self, flask_client: Client, user_headers):
|
def test_existing_user(self, flask_client: Client, user_headers):
|
||||||
r = flask_client.post(f'/api/v1/users/', headers=user_headers, json={
|
r = flask_client.post(f'/api/v1/users/', headers=user_headers, json={
|
||||||
'email': 'utente_test@nest.com',
|
'email': 'utente_test@nest.com',
|
||||||
'password': 'password',
|
'password': 'password',
|
||||||
'username': 'utente_test'
|
'username': 'utente_test'
|
||||||
})
|
})
|
||||||
assert b'failure' in r.data
|
assert r.json["result"] == "failure"
|
||||||
|
|
||||||
|
|
||||||
class TestUserDelete:
|
class TestUserDelete:
|
||||||
def test_for_success(self, flask_client: Client, admin_headers):
|
def test_valid_user(self, flask_client: Client, admin_headers):
|
||||||
r = flask_client.delete(f'/api/v1/users/utente_test@nest.com', headers=admin_headers)
|
r = flask_client.delete(f'/api/v1/users/utente1_test@nest.com', headers=admin_headers)
|
||||||
assert b'success' in r.data
|
assert r.status_code == 204
|
||||||
|
|
||||||
# the admin tries to commit suicide
|
# the admin tries to commit suicide
|
||||||
def test_for_failure(self, flask_client: Client, admin_headers):
|
def test_himself(self, flask_client: Client, admin_headers):
|
||||||
r = flask_client.delete(f'/api/v1/users/admin@admin.com', headers=admin_headers)
|
r = flask_client.delete(f'/api/v1/users/admin@admin.com', headers=admin_headers)
|
||||||
assert b'failure' in r.data
|
assert r.json["result"] == "failure"
|
||||||
|
|
||||||
|
|
||||||
class TestUserPatch:
|
class TestUserPatch:
|
||||||
|
@ -61,11 +62,10 @@ class TestUserPatch:
|
||||||
r = flask_client.patch(f'/api/v1/users/admin@admin.com', headers=admin_headers, json={
|
r = flask_client.patch(f'/api/v1/users/admin@admin.com', headers=admin_headers, json={
|
||||||
'username': 'admin_patched'
|
'username': 'admin_patched'
|
||||||
})
|
})
|
||||||
assert b'success' in r.data
|
assert r.json["result"] == "success"
|
||||||
|
|
||||||
# FIXME AssertionError in flask_client at line 63. Il test non riesce ad andare a buon fine
|
def test_not_authorized(self, flask_client: Client, user_headers):
|
||||||
def test_for_failure(self, flask_client: Client, user_headers):
|
|
||||||
r = flask_client.patch(f'/api/v1/users/admin@admin.com', headers=user_headers, json={
|
r = flask_client.patch(f'/api/v1/users/admin@admin.com', headers=user_headers, json={
|
||||||
'username': 'admin_patched'
|
'username': 'admin_patched'
|
||||||
})
|
})
|
||||||
assert b'failure' in r.data
|
assert r.json["result"] == "failure"
|
||||||
|
|
71
code/backend/nest_backend/test/test_zrepository.py
Normal file
71
code/backend/nest_backend/test/test_zrepository.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
from flask.testing import Client
|
||||||
|
|
||||||
|
'''A file that contains tests of classes and methods for all the requests concerning an user.'''
|
||||||
|
|
||||||
|
|
||||||
|
class TestRepositoryGetAll:
|
||||||
|
def test_get_all_user_repositories(self, flask_client: Client, user_headers):
|
||||||
|
r = flask_client.get(f'/api/v1/repositories/', headers=user_headers,
|
||||||
|
json={'owner_id': 'utente_test@nest.com', 'isActive': False})
|
||||||
|
assert r.json["result"] == "success"
|
||||||
|
# assert r.json["data"]["email"] == "admin@admin.com"
|
||||||
|
# assert r.json["data"]["isAdmin"] is True
|
||||||
|
|
||||||
|
def test_get_all_admin_repositories(self, flask_client: Client, admin_headers):
|
||||||
|
r = flask_client.get(f'/api/v1/repositories/', headers=admin_headers,
|
||||||
|
json={'owner_id': 'admin@admin.com', 'isActive': False})
|
||||||
|
assert r.json["result"] == "success"
|
||||||
|
# assert r.json["data"]["email"] == "admin@admin.com"
|
||||||
|
# assert r.json["data"]["isAdmin"] is True
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
def test_non_existing_user(self, flask_client: Client, admin_headers):
|
||||||
|
r = flask_client.get(f'/api/v1/users/ciccio@dev.com', headers=admin_headers)
|
||||||
|
assert r.json["result"] == "failure"
|
||||||
|
assert r.json["msg"] == "Could not locate the user."
|
||||||
|
|
||||||
|
class TestUserAdd:
|
||||||
|
def test_for_success(self, flask_client: Client, admin_headers):
|
||||||
|
r = flask_client.post(f'/api/v1/users/', headers=admin_headers, json={
|
||||||
|
'email': 'utente1_test@nest.com',
|
||||||
|
'password': 'password',
|
||||||
|
'username': 'utente_test'
|
||||||
|
})
|
||||||
|
assert b'success' in r.data
|
||||||
|
|
||||||
|
def test_for_failure(self, flask_client: Client, user_headers):
|
||||||
|
r = flask_client.post(f'/api/v1/users/', headers=user_headers, json={
|
||||||
|
'email': 'utente_test@nest.com',
|
||||||
|
'password': 'password',
|
||||||
|
'username': 'utente_test'
|
||||||
|
})
|
||||||
|
assert b'failure' in r.data
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserDelete:
|
||||||
|
def test_for_success(self, flask_client: Client, admin_headers):
|
||||||
|
r = flask_client.delete(f'/api/v1/users/utente_test@nest.com', headers=admin_headers)
|
||||||
|
assert b'success' in r.data
|
||||||
|
|
||||||
|
# the admin tries to commit suicide
|
||||||
|
def test_for_failure(self, flask_client: Client, admin_headers):
|
||||||
|
r = flask_client.delete(f'/api/v1/users/admin@admin.com', headers=admin_headers)
|
||||||
|
assert b'failure' in r.data
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserPatch:
|
||||||
|
def test_for_success(self, flask_client: Client, admin_headers):
|
||||||
|
r = flask_client.patch(f'/api/v1/users/admin@admin.com', headers=admin_headers, json={
|
||||||
|
'username': 'admin_patched'
|
||||||
|
})
|
||||||
|
assert b'success' in r.data
|
||||||
|
|
||||||
|
# FIXME AssertionError in flask_client at line 63. Il test non riesce ad andare a buon fine
|
||||||
|
def test_for_failure(self, flask_client: Client, user_headers):
|
||||||
|
r = flask_client.patch(f'/api/v1/users/admin@admin.com', headers=user_headers, json={
|
||||||
|
'username': 'admin_patched'
|
||||||
|
})
|
||||||
|
assert b'failure' in r.data
|
||||||
|
|
||||||
|
'''
|
20
code/backend/poetry.lock
generated
20
code/backend/poetry.lock
generated
|
@ -222,6 +222,20 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["sphinx"]
|
docs = ["sphinx"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gunicorn"
|
||||||
|
version = "20.1.0"
|
||||||
|
description = "WSGI HTTP Server for UNIX"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
eventlet = ["eventlet (>=0.24.1)"]
|
||||||
|
gevent = ["gevent (>=1.4.0)"]
|
||||||
|
setproctitle = ["setproctitle"]
|
||||||
|
tornado = ["tornado (>=0.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "2.10"
|
version = "2.10"
|
||||||
|
@ -798,7 +812,7 @@ watchdog = ["watchdog"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.8.5"
|
python-versions = "^3.8.5"
|
||||||
content-hash = "9c4f3517887f1d12fc4196119a7d3398fa61a5214809fcfd1a324d87ca8baeaf"
|
content-hash = "1ac47344a839aae5a929d7ce9914f62c76c1a591bc88dabb819fe15814f40031"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
alabaster = [
|
alabaster = [
|
||||||
|
@ -963,6 +977,10 @@ greenlet = [
|
||||||
{ file = "greenlet-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535" },
|
{ file = "greenlet-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535" },
|
||||||
{ file = "greenlet-1.1.0.tar.gz", hash = "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee" },
|
{ file = "greenlet-1.1.0.tar.gz", hash = "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee" },
|
||||||
]
|
]
|
||||||
|
gunicorn = [
|
||||||
|
{ file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e" },
|
||||||
|
{ file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" },
|
||||||
|
]
|
||||||
idna = [
|
idna = [
|
||||||
{ file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" },
|
{ file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" },
|
||||||
{ file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6" },
|
{ file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6" },
|
||||||
|
|
|
@ -28,6 +28,7 @@ openapi-spec-validator = "^0.3.0"
|
||||||
flask-swagger-ui = "^3.36.0"
|
flask-swagger-ui = "^3.36.0"
|
||||||
tweepy = "^3.10.0"
|
tweepy = "^3.10.0"
|
||||||
nltk = "^3.6.2"
|
nltk = "^3.6.2"
|
||||||
|
gunicorn = "^20.1.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^6.2.3"
|
pytest = "^6.2.3"
|
||||||
|
|
16
code/backend/web-nest.service
Normal file
16
code/backend/web-nest.service
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[Unit]
|
||||||
|
Description=N.E.S.T. Backend Webserver
|
||||||
|
Wants=network-online.target postgresql.service
|
||||||
|
After=network-online.target nss-lookup.target postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=exec
|
||||||
|
User=nest
|
||||||
|
Group=nest
|
||||||
|
# Cambia questa riga alla tua directory di installazione
|
||||||
|
WorkingDirectory=/srv/nest
|
||||||
|
# Cambia questa riga al tuo venv
|
||||||
|
ExecStart=/srv/nest/venv/bin/gunicorn -b 127.0.0.1:5000 --env "FLASK_CONFIG=/srv/nest/config.py" nest_backend.app:rp_app
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -8,16 +8,20 @@ import PageSandbox from "./routes/PageSandbox"
|
||||||
import PageDashboard from "./routes/PageDashboard"
|
import PageDashboard from "./routes/PageDashboard"
|
||||||
import PageRoot from "./routes/PageRoot"
|
import PageRoot from "./routes/PageRoot"
|
||||||
import PageEdit from "./routes/PageEdit"
|
import PageEdit from "./routes/PageEdit"
|
||||||
|
import PageUsers from "./routes/PageUsers"
|
||||||
|
|
||||||
|
|
||||||
export default function PageSwitcher({ ...props }) {
|
export default function PageSwitcher({ ...props }) {
|
||||||
return (
|
return (
|
||||||
<Switch {...props}>
|
<Switch {...props}>
|
||||||
|
<Route path={"/repositories/:id/edit"} exact={true}>
|
||||||
|
<PageEdit/>
|
||||||
|
</Route>
|
||||||
<Route path={"/login"} exact={true}>
|
<Route path={"/login"} exact={true}>
|
||||||
<PageLogin/>
|
<PageLogin/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={"/repositories/:id/edit"} exact={true}>
|
<Route path={"/users"} exact={true}>
|
||||||
<PageEdit/>
|
<PageUsers/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={"/repositories"} exact={true}>
|
<Route path={"/repositories"} exact={true}>
|
||||||
<PageRepositories/>
|
<PageRepositories/>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
* @param icon - The icon of the summary.
|
* @param icon - The icon of the summary.
|
||||||
* @param title - The title of the summary.
|
* @param title - The title of the summary.
|
||||||
* @param subtitle - The subtitle of the summary.
|
* @param subtitle - The subtitle of the summary.
|
||||||
|
* @param onClick - A function to call when the summary is clicked.
|
||||||
* @param upperLabel - The label for the upper value.
|
* @param upperLabel - The label for the upper value.
|
||||||
* @param upperValue - The upper value.
|
* @param upperValue - The upper value.
|
||||||
* @param lowerLabel - The label for the lower value.
|
* @param lowerLabel - The label for the lower value.
|
||||||
|
@ -21,11 +22,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function Summary(
|
export default function Summary(
|
||||||
{ icon, title, subtitle, upperLabel, upperValue, lowerLabel, lowerValue, buttons, className, ...props },
|
{ icon, title, subtitle, onClick, upperLabel, upperValue, lowerLabel, lowerValue, buttons, className, ...props },
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(Style.Summary, className)} {...props}>
|
<div className={classNames(Style.Summary, className)} {...props}>
|
||||||
<div className={Style.Left}>
|
<div className={classNames(Style.Left, onClick ? Style.Clickable : null)} onClick={onClick}>
|
||||||
<div className={Style.IconContainer}>
|
<div className={Style.IconContainer}>
|
||||||
<FontAwesomeIcon icon={icon}/>
|
<FontAwesomeIcon icon={icon}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,13 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Clickable:hover {
|
||||||
|
filter: brightness(110%);
|
||||||
|
}
|
||||||
|
|
||||||
.Left {
|
.Left {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useContext } from "react"
|
import React, { useContext } from "react"
|
||||||
import BoxFull from "../base/BoxFull"
|
import BoxFull from "../base/BoxFull"
|
||||||
import SummaryRepository from "./SummaryRepository"
|
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"
|
import ContextUser from "../../contexts/ContextUser"
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ export default function BoxRepositoriesActive({ repositories, refresh, ...props
|
||||||
<SummaryRepository
|
<SummaryRepository
|
||||||
key={repo["id"]}
|
key={repo["id"]}
|
||||||
repo={repo}
|
repo={repo}
|
||||||
icon={faSearch}
|
icon={faFolderOpen}
|
||||||
refresh={refresh}
|
refresh={refresh}
|
||||||
canArchive={true}
|
canArchive={true}
|
||||||
canEdit={true}
|
canEdit={true}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { useContext } from "react"
|
||||||
import BoxFull from "../base/BoxFull"
|
import BoxFull from "../base/BoxFull"
|
||||||
import ContextUser from "../../contexts/ContextUser"
|
import ContextUser from "../../contexts/ContextUser"
|
||||||
import SummaryRepository from "./SummaryRepository"
|
import SummaryRepository from "./SummaryRepository"
|
||||||
import { faSearch } from "@fortawesome/free-solid-svg-icons"
|
import { faFolder } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,9 +23,9 @@ export default function BoxRepositoriesArchived({ repositories, refresh, ...prop
|
||||||
<SummaryRepository
|
<SummaryRepository
|
||||||
key={repo["id"]}
|
key={repo["id"]}
|
||||||
repo={repo}
|
repo={repo}
|
||||||
icon={faSearch}
|
icon={faFolder}
|
||||||
refresh={refresh}
|
refresh={refresh}
|
||||||
canArchive={true}
|
canArchive={false}
|
||||||
canEdit={false}
|
canEdit={false}
|
||||||
canDelete={repo["owner"]["username"] === user["username"]}
|
canDelete={repo["owner"]["username"] === user["username"]}
|
||||||
/>
|
/>
|
||||||
|
|
72
code/frontend/src/components/interactive/BoxUserCreate.js
Normal file
72
code/frontend/src/components/interactive/BoxUserCreate.js
Normal file
|
@ -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 (
|
||||||
|
<BoxFull header={"Crea utente"} {...props}>
|
||||||
|
<FormLabelled>
|
||||||
|
<FormLabel text={"Username"}>
|
||||||
|
<InputWithIcon
|
||||||
|
icon={faUser}
|
||||||
|
type={"text"}
|
||||||
|
value={username}
|
||||||
|
onChange={event => setUsername(event.target.value)}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"Email"}>
|
||||||
|
<InputWithIcon
|
||||||
|
icon={faEnvelope}
|
||||||
|
type={"text"}
|
||||||
|
value={email}
|
||||||
|
onChange={event => setEmail(event.target.value)}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"Password"}>
|
||||||
|
<InputWithIcon
|
||||||
|
icon={faKey}
|
||||||
|
type={"password"}
|
||||||
|
value={password}
|
||||||
|
onChange={event => setPassword(event.target.value)}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
{error ?
|
||||||
|
<FormAlert color={"Red"}>
|
||||||
|
{error.toString()}
|
||||||
|
</FormAlert>
|
||||||
|
: null}
|
||||||
|
<FormButton
|
||||||
|
color={"Green"}
|
||||||
|
icon={faPlus}
|
||||||
|
onClick={onButtonClick}
|
||||||
|
disabled={running}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</FormButton>
|
||||||
|
</FormLabelled>
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,9 +1,17 @@
|
||||||
import React, { Fragment, useContext } from "react"
|
import React, { useContext } from "react"
|
||||||
import Style from "./Sidebar.module.css"
|
import Style from "./Sidebar.module.css"
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import Logo from "../interactive/Logo"
|
import Logo from "../interactive/Logo"
|
||||||
import ButtonSidebar from "../base/ButtonSidebar"
|
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"
|
import ContextUser from "../../contexts/ContextUser"
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,16 +32,24 @@ export default function Sidebar({ className, ...props }) {
|
||||||
<Logo/>
|
<Logo/>
|
||||||
{
|
{
|
||||||
user ?
|
user ?
|
||||||
<Fragment>
|
<>
|
||||||
<ButtonSidebar to={"/dashboard"} icon={faHome}>Dashboard</ButtonSidebar>
|
<ButtonSidebar to={"/dashboard"} icon={faHome}>Dashboard</ButtonSidebar>
|
||||||
<ButtonSidebar to={"/repositories"} icon={faFolder}>Repositories</ButtonSidebar>
|
<ButtonSidebar to={"/repositories"} icon={faFolder}>Repositories</ButtonSidebar>
|
||||||
<ButtonSidebar to={"/alerts"} icon={faExclamationTriangle}>Alerts</ButtonSidebar>
|
<ButtonSidebar to={"/alerts"} icon={faExclamationTriangle}>Alerts</ButtonSidebar>
|
||||||
<ButtonSidebar to={"/settings"} icon={faCog}>Settings</ButtonSidebar>
|
<ButtonSidebar to={"/settings"} icon={faCog}>Settings</ButtonSidebar>
|
||||||
</Fragment>
|
</>
|
||||||
:
|
:
|
||||||
<Fragment>
|
<>
|
||||||
<ButtonSidebar to={"/login"} icon={faKey}>Login</ButtonSidebar>
|
<ButtonSidebar to={"/login"} icon={faKey}>Login</ButtonSidebar>
|
||||||
</Fragment>
|
</>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
user && user.isAdmin ?
|
||||||
|
<>
|
||||||
|
<ButtonSidebar to={"/users"} icon={faUserCog}>Utenti</ButtonSidebar>
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
process.env.NODE_ENV === "development" ?
|
process.env.NODE_ENV === "development" ?
|
||||||
|
|
|
@ -26,9 +26,12 @@ export default function SummaryRepository(
|
||||||
const { fetchDataAuth } = useContext(ContextUser)
|
const { fetchDataAuth } = useContext(ContextUser)
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { fetchNow: archiveThis } = useBackend(fetchDataAuth, "PATCH", `/api/v1/repositories/${repo.id}`, { "close": true })
|
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 { fetchNow: deletThis } = useBackend(fetchDataAuth, "DELETE", `/api/v1/repositories/${repo.id}`)
|
||||||
|
|
||||||
|
const onRepoClick = () => {
|
||||||
|
history.push(`/repositories/${repo.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
const onEditClick = () => {
|
const onEditClick = () => {
|
||||||
history.push(`/repositories/${repo.id}/edit`)
|
history.push(`/repositories/${repo.id}/edit`)
|
||||||
}
|
}
|
||||||
|
@ -38,11 +41,6 @@ export default function SummaryRepository(
|
||||||
await refresh()
|
await refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUnarchiveClick = async () => {
|
|
||||||
await unarchiveThis()
|
|
||||||
await refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDeleteClick = async () => {
|
const onDeleteClick = async () => {
|
||||||
await deletThis()
|
await deletThis()
|
||||||
await refresh()
|
await refresh()
|
||||||
|
@ -71,9 +69,9 @@ export default function SummaryRepository(
|
||||||
<Button
|
<Button
|
||||||
color={"Grey"}
|
color={"Grey"}
|
||||||
icon={faArchive}
|
icon={faArchive}
|
||||||
onClick={repo.is_active ? onArchiveClick : onUnarchiveClick}
|
onClick={onArchiveClick}
|
||||||
>
|
>
|
||||||
{repo.is_active ? "Archive" : "Unarchive"}
|
{"Archive"}
|
||||||
</Button>
|
</Button>
|
||||||
: null}
|
: null}
|
||||||
</>
|
</>
|
||||||
|
@ -82,10 +80,11 @@ export default function SummaryRepository(
|
||||||
<Summary
|
<Summary
|
||||||
icon={repo.is_active ? faFolderOpen : faFolder}
|
icon={repo.is_active ? faFolderOpen : faFolder}
|
||||||
title={repo.name}
|
title={repo.name}
|
||||||
subtitle={repo.author}
|
subtitle={repo.owner ? repo.owner.username : null}
|
||||||
upperLabel={"Start"}
|
onClick={onRepoClick}
|
||||||
|
upperLabel={"Created"}
|
||||||
upperValue={repo.start ? new Date(repo.start).toLocaleString() : null}
|
upperValue={repo.start ? new Date(repo.start).toLocaleString() : null}
|
||||||
lowerLabel={"End"}
|
lowerLabel={"Archived"}
|
||||||
lowerValue={repo.end ? new Date(repo.end).toLocaleString() : null}
|
lowerValue={repo.end ? new Date(repo.end).toLocaleString() : null}
|
||||||
buttons={buttons}
|
buttons={buttons}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
39
code/frontend/src/components/interactive/SummaryUser.js
Normal file
39
code/frontend/src/components/interactive/SummaryUser.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
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 event => {
|
||||||
|
event.stopPropagation()
|
||||||
|
// 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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -55,28 +55,28 @@ export default function RepositoryEditor({
|
||||||
() => {
|
() => {
|
||||||
return {
|
return {
|
||||||
"conditions": _conditions,
|
"conditions": _conditions,
|
||||||
"end": _end,
|
"end": null,
|
||||||
"evaluation_mode": _evaluationMode,
|
"evaluation_mode": _evaluationMode,
|
||||||
"id": id,
|
"id": id,
|
||||||
"is_active": true,
|
"is_active": true,
|
||||||
"name": _name,
|
"name": _name,
|
||||||
"owner": user,
|
"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 { error, loading, fetchNow } = useBackend(fetchDataAuth, method, path, body)
|
||||||
|
|
||||||
const save = useCallback(
|
const save = useCallback(
|
||||||
() => {
|
async () => {
|
||||||
if(!id) {
|
if(!id) {
|
||||||
console.info("Creating new repository with body: ", body)
|
console.info("Creating new repository with body: ", body)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.info("Editing repository ", id, " with body: ", body)
|
console.info("Editing repository ", id, " with body: ", body)
|
||||||
}
|
}
|
||||||
fetchNow()
|
await fetchNow()
|
||||||
},
|
},
|
||||||
[id, body, fetchNow],
|
[id, body, fetchNow],
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,9 +25,8 @@ export default function useArrayState(def) {
|
||||||
console.debug("Splicing ", position, " from ArrayState")
|
console.debug("Splicing ", position, " from ArrayState")
|
||||||
setValue(
|
setValue(
|
||||||
oldArray => {
|
oldArray => {
|
||||||
// TODO: Hope this doesn't break anything...
|
|
||||||
oldArray.splice(position, 1)
|
oldArray.splice(position, 1)
|
||||||
return oldArray
|
return [...oldArray]
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
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,
|
||||||
|
}
|
||||||
|
}
|
22
code/frontend/src/routes/PageUsers.js
Normal file
22
code/frontend/src/routes/PageUsers.js
Normal file
|
@ -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 (
|
||||||
|
<div className={classNames(Style.PageUsers, className)} {...props}>
|
||||||
|
<BoxHeader className={Style.Header}>
|
||||||
|
Gestisci utenti
|
||||||
|
</BoxHeader>
|
||||||
|
<BoxUserCreate className={Style.CreateUser} createUser={bv.createResource} running={bv.running}/>
|
||||||
|
<BoxUserList className={Style.UserList} users={bv.resources} destroyUser={bv.destroyResource} running={bv.running}/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
27
code/frontend/src/routes/PageUsers.module.css
Normal file
27
code/frontend/src/routes/PageUsers.module.css
Normal file
|
@ -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;
|
||||||
|
}
|
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