1
Fork 0
mirror of https://github.com/pds-nest/nest.git synced 2024-10-16 20:17:25 +00:00

🔧 New dashboard layout

This commit is contained in:
Steffo 2021-05-24 05:02:07 +02:00
parent ab919f351b
commit a506a07588
Signed by: steffo
GPG key ID: 6965406171929D01
14 changed files with 249 additions and 87 deletions

View file

@ -112,6 +112,8 @@ export default {
postUniq: "Totale utenti che hanno postato", postUniq: "Totale utenti che hanno postato",
postPop: "Utente più attivo", postPop: "Utente più attivo",
filters: "Filtri", filters: "Filtri",
errorMissingFields: "Errore: Uno o più campi richiesti non sono stati compilati."
}, },
// 🇬🇧 // 🇬🇧
en: { en: {

View file

@ -14,6 +14,9 @@ import PageShare from "./routes/PageShare"
export default function PageSwitcher({ ...props }) { export default function PageSwitcher({ ...props }) {
return ( return (
<Switch {...props}> <Switch {...props}>
<Route path={"/repositories/create"} exact={true}>
<PageRepositoryCreate/>
</Route>
<Route path={"/repositories/:id/alerts"} exact={true}> <Route path={"/repositories/:id/alerts"} exact={true}>
<PageRepositoryAlerts/> <PageRepositoryAlerts/>
</Route> </Route>
@ -35,9 +38,6 @@ export default function PageSwitcher({ ...props }) {
<Route path={"/settings"} exact={true}> <Route path={"/settings"} exact={true}>
<PageSettings/> <PageSettings/>
</Route> </Route>
<Route path={"/dashboard"} exact={true}>
<PageRepositoryCreate/>
</Route>
<Route path={"/"}> <Route path={"/"}>
<PageLogin/> <PageLogin/>
</Route> </Route>

View file

@ -17,7 +17,6 @@ import ContextUser from "../../contexts/ContextUser"
* @param destroy - Function with a single "id" parameter to call when the delete repository button is clicked. * @param destroy - Function with a single "id" parameter to call when the delete repository button is clicked.
* @param firstLoad - If the repositories are loading and a loading message should be displayed. * @param firstLoad - If the repositories are loading and a loading message should be displayed.
* @param running - If an action is running on the viewset. * @param running - If an action is running on the viewset.
* @param className - Additional class(es) to append to the box.
* @param props - Additional props to pass to the box. * @param props - Additional props to pass to the box.
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
@ -33,7 +32,6 @@ export default function BoxRepositories(
destroy, destroy,
loading, loading,
running, running,
className,
...props ...props
}) })
{ {

View file

@ -73,7 +73,7 @@ export default function BoxRepositoryCreate({ running, ...props }) {
</FormLabel> </FormLabel>
{error ? {error ?
<FormAlert color={"Red"}> <FormAlert color={"Red"}>
{error.toString()} {strings[error.data.code]}
</FormAlert> </FormAlert>
: null} : null}
{id ? {id ?
@ -91,7 +91,7 @@ export default function BoxRepositoryCreate({ running, ...props }) {
style={{ "gridColumn": "2" }} style={{ "gridColumn": "2" }}
icon={faPencilAlt} icon={faPencilAlt}
color={"Green"} color={"Green"}
onClick={_ => goToOnSuccess(save, history, "/repositories")()} onClick={save}
disabled={running} disabled={running}
> >
{strings.save} {strings.save}
@ -102,7 +102,7 @@ export default function BoxRepositoryCreate({ running, ...props }) {
style={{ "gridColumn": "1 / 3" }} style={{ "gridColumn": "1 / 3" }}
icon={faPlus} icon={faPlus}
color={"Green"} color={"Green"}
onClick={_ => goToOnSuccess(save, history, "/repositories")()} onClick={save}
disabled={running} disabled={running}
> >
{strings.createRepo} {strings.createRepo}

View file

@ -11,6 +11,10 @@ import BoxRepositoryCreate from "../interactive/BoxRepositoryCreate"
import classNames from "classnames" import classNames from "classnames"
import ContextUser from "../../contexts/ContextUser" import ContextUser from "../../contexts/ContextUser"
import useBackend from "../../hooks/useBackend" import useBackend from "../../hooks/useBackend"
import { Condition } from "../../objects/Condition"
import useBackendResource from "../../hooks/useBackendResource"
import useBackendViewset from "../../hooks/useBackendViewset"
import { Redirect } from "react-router"
export default function RepositoryEditor({ export default function RepositoryEditor({
@ -23,74 +27,84 @@ export default function RepositoryEditor({
evaluation_mode: evaluationMode, evaluation_mode: evaluationMode,
className, className,
}) { }) {
/** The currently logged in user. */
const { user } = useContext(ContextUser)
/** The repository name. */ /** The repository name. */
const [_name, setName] = useState(name ?? "") const [_name, setName] = useState(name ?? "")
/** The repository state (active / archived). */
const [_isActive, setActive] = useState(isActive ?? true)
/** The start date of the data gathering. */
const [_start, setStart] = useState(start ?? new Date().toISOString())
/** The end date of the data gathering. */
const [_end, setEnd] = useState(end ?? new Date().toISOString())
/** The conditions of the data gathering. */ /** The conditions of the data gathering. */
const { const {
value: _conditions, value: rawConditions,
setValue: setRawConditions, setValue: setRawConditions,
appendValue: appendRawCondition, appendValue: appendRawCondition,
removeValue: removeRawCondition, removeValue: removeRawCondition,
spliceValue: spliceRawCondition, spliceValue: spliceRawCondition,
} = useArrayState(conditions) } = useArrayState(conditions)
const _conditions = rawConditions.map(cond => Condition.fromRaw(cond))
/** The operator the conditions should be evaluated with. */ /** The operator the conditions should be evaluated with. */
const [_evaluationMode, setEvaluationMode] = useState(evaluationMode ?? 0) const [_evaluationMode, setEvaluationMode] = useState(evaluationMode ?? 0)
const { user, fetchDataAuth } = useContext(ContextUser) /** The backend viewset to use to create / edit the repository. */
const {running, error, createResource, editResource} = useBackendViewset(
const method = id ? "PUT" : "POST" `/api/v1/repositories/`,
const path = id ? `/api/v1/repositories/${id}` : `/api/v1/repositories/` "id",
const body = useMemo( {
() => { list: false,
return { create: true,
"conditions": _conditions, retrieve: false,
"end": null, edit: true,
"evaluation_mode": _evaluationMode, destroy: false,
"id": id, command: false,
"is_active": true, action: false,
"name": _name, }
"owner": user,
"start": null,
}
},
[_conditions, _evaluationMode, id, _name, user],
) )
const { error, loading, fetchNow } = useBackend(fetchDataAuth, method, path, body)
/** If `true`, switches to the repository page on the next render. */
const [switchPage, setSwitchPage] = useState(false)
/**
* Save the current changes, creating or editing it as needed.
*
* @type {(function(): Promise<void>)|*}
*/
const save = useCallback( const save = useCallback(
async () => { async () => {
const body = {
"id": id,
"name": _name,
"start": null,
"is_active": true,
"end": null,
"owner": user,
"spectators": null,
"evaluation_mode": _evaluationMode,
"conditions": _conditions,
}
if(!id) { if(!id) {
console.info("Creando una nuova repository avente come corpo: ", body) console.info("Creating new repository with body: ", body)
await createResource(body)
} }
else { else {
console.info("Modificando la repository ", id, " con corpo: ", body) console.info("Editing repository ", id, " with body: ", body)
await editResource(id, body)
} }
await fetchNow() setSwitchPage(true)
}, },
[id, body, fetchNow], [id, createResource, editResource, _conditions, _evaluationMode, _name, user],
) )
/** /**
* Cancel the changes made so far to the repository. * Cancel the changes made so far to the repository.
*
* @type {(function(): void)|*}
*/ */
const revert = useCallback( const revert = useCallback(
() => { () => {
setName(name) setName(name)
setActive(isActive)
setStart(start)
setEnd(end)
setRawConditions(conditions) setRawConditions(conditions)
setEvaluationMode(evaluationMode) setEvaluationMode(evaluationMode)
}, },
@ -99,14 +113,11 @@ export default function RepositoryEditor({
/** /**
* Try to add a new condition, logging a message to the console if something goes wrong. * Try to add a new condition, logging a message to the console if something goes wrong.
*
* @type {(function(): void)|*}
*/ */
const addCondition = useCallback( const addCondition = useCallback(
(newCond) => { (newCond) => {
// Check for content
if(!newCond.content) {
console.debug("Impossibile aggiungere ", newCond, ": l'oggetto è vuoto.")
return
}
// Check for duplicates // Check for duplicates
let duplicate = null let duplicate = null
@ -117,27 +128,29 @@ export default function RepositoryEditor({
} }
} }
if(duplicate) { if(duplicate) {
console.debug("Impossibile aggiungere ", newCond, ": ", duplicate, " è già esistente.") console.debug("Cannot add ", newCond, ": ", duplicate, " already exists.")
return return
} }
console.debug("Aggiungendo ", newCond, " alle condizioni del repository") console.debug("Adding ", newCond, " to the repository conditions")
appendRawCondition(newCond) appendRawCondition(newCond)
}, },
[_conditions, appendRawCondition], [_conditions, appendRawCondition],
) )
// Hack to switch page on success
if(!error && switchPage) {
return <Redirect to={"/repositories"}/>
}
return ( return (
<ContextRepositoryEditor.Provider <ContextRepositoryEditor.Provider
value={{ value={{
id, id,
name: _name, setName, name: _name, setName,
isActive: _isActive, setActive,
start: _start, setStart,
end: _end, setEnd,
conditions: _conditions, addCondition, appendRawCondition, removeRawCondition, spliceRawCondition, conditions: _conditions, addCondition, appendRawCondition, removeRawCondition, spliceRawCondition,
evaluationMode: _evaluationMode, setEvaluationMode, evaluationMode: _evaluationMode, setEvaluationMode,
error, loading, error, running,
revert, save, revert, save,
}} }}
> >
@ -147,7 +160,7 @@ export default function RepositoryEditor({
<BoxConditionUser className={Style.SearchByUser}/> <BoxConditionUser className={Style.SearchByUser}/>
<BoxConditionDatetime className={Style.SearchByTimePeriod}/> <BoxConditionDatetime className={Style.SearchByTimePeriod}/>
<BoxConditions className={Style.Conditions}/> <BoxConditions className={Style.Conditions}/>
<BoxRepositoryCreate running={loading} className={Style.CreateDialog}/> <BoxRepositoryCreate running={running} className={Style.CreateDialog}/>
</div> </div>
</ContextRepositoryEditor.Provider> </ContextRepositoryEditor.Provider>
) )

View file

@ -1,6 +1,7 @@
import { useCallback, useContext, useState } from "react" import { useCallback, useContext, useState } from "react"
import ContextServer from "../contexts/ContextServer" import ContextServer from "../contexts/ContextServer"
import ContextUser from "../contexts/ContextUser" import ContextUser from "../contexts/ContextUser"
import { ServerNotConfiguredError, FetchAlreadyRunningError, DecodeError, ResultError } from "../objects/Errors"
/** /**

View file

@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
import useBackendRequest from "./useBackendRequest" import useBackendRequest from "./useBackendRequest"
import { ViewNotAllowedError } from "../objects/Errors"
/** /**
@ -72,9 +73,10 @@ export default function useBackendResource(
} }
catch(e) { catch(e) {
setError(e) setError(e)
throw e return
} }
setError(null) setError(null)
setResource(refreshedResource) setResource(refreshedResource)
return refreshedResource return refreshedResource
}, },
@ -89,9 +91,10 @@ export default function useBackendResource(
} }
catch(e) { catch(e) {
setError(e) setError(e)
throw e return
} }
setError(null) setError(null)
setResource(editedResource) setResource(editedResource)
return editedResource return editedResource
}, },
@ -105,9 +108,10 @@ export default function useBackendResource(
} }
catch(e) { catch(e) {
setError(e) setError(e)
throw e return
} }
setError(null) setError(null)
setResource(null) setResource(null)
return null return null
}, },

View file

@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
import useBackendRequest from "./useBackendRequest" import useBackendRequest from "./useBackendRequest"
import { ViewNotAllowedError } from "../objects/Errors"
/** /**
@ -98,60 +99,97 @@ export default function useBackendViewset(resourcesPath, pkName,
const listResources = useCallback( const listResources = useCallback(
async () => { async () => {
let res
try { try {
setResources(await apiList()) res = await apiList()
} }
catch(e) { catch(e) {
setError(e) setError(e)
throw e return
} }
setError(null) setError(null)
return {} setResources(res)
}, },
[apiList], [apiList],
) )
const retrieveResource = useCallback( const retrieveResource = useCallback(
async (pk) => { async (pk) => {
const refreshedResource = await apiRetrieve(pk) let res
setResources(res => res.map(resource => { try {
res = await apiRetrieve(pk)
}
catch(e) {
setError(e)
return
}
setError(null)
setResources(r => r.map(resource => {
if(resource[pkName] === pk) { if(resource[pkName] === pk) {
return refreshedResource return res
} }
return resource return resource
})) }))
return refreshedResource
return res
}, },
[apiRetrieve, pkName], [apiRetrieve, pkName],
) )
const createResource = useCallback( const createResource = useCallback(
async (data) => { async (data) => {
const newResource = await apiCreate(data) let res
setResources(res => [...res, newResource]) try {
return newResource res = await apiCreate(data)
}
catch(e) {
setError(e)
return
}
setError(null)
setResources(r => [...r, res])
return res
}, },
[apiCreate], [apiCreate],
) )
const editResource = useCallback( const editResource = useCallback(
async (pk, data) => { async (pk, data) => {
const editedResource = await apiEdit(pk, data) let res
setResources(res => res.map(resource => { try {
res = await apiEdit(pk, data)
}
catch(e) {
setError(e)
return
}
setError(null)
setResources(r => r.map(resource => {
if(resource[pkName] === pk) { if(resource[pkName] === pk) {
return editedResource return res
} }
return resource return resource
})) }))
return editedResource return res
}, },
[apiEdit, pkName], [apiEdit, pkName],
) )
const destroyResource = useCallback( const destroyResource = useCallback(
async (pk) => { async (pk) => {
await apiDestroy(pk) try {
setResources(res => res.filter(resource => resource[pkName] !== pk)) await apiDestroy(pk)
}
catch(e) {
setError(e)
return
}
setError(null)
setResources(r => r.filter(resource => resource[pkName] !== pk))
return null return null
}, },
[apiDestroy, pkName], [apiDestroy, pkName],

View file

@ -6,6 +6,8 @@ import {
faQuestionCircle, faQuestionCircle,
IconDefinition, IconDefinition,
} from "@fortawesome/free-solid-svg-icons" } from "@fortawesome/free-solid-svg-icons"
import TimeRay from "./TimeRay"
import MapArea from "./MapArea"
/** /**
@ -24,6 +26,16 @@ export class Condition {
this.id = id this.id = id
} }
static fromRaw(data) {
console.debug("Trying to serialize condition: ", data)
if(data.type === 0) return ConditionHashtag.fromRaw(data)
else if(data.type === 2) return ConditionTime.fromRaw(data)
else if(data.type === 3) return ConditionLocation.fromRaw(data)
else if(data.type === 5) return ConditionUser.fromRaw(data)
else return new Condition(data.type, data.content, data.id)
}
/** /**
* Get the condition as an object readable by the backend. * Get the condition as an object readable by the backend.
* *
@ -61,6 +73,10 @@ export class ConditionHashtag extends Condition {
super(0, hashtag, id) super(0, hashtag, id)
} }
static fromRaw(data) {
return new ConditionHashtag(data.content, data.id)
}
display() { display() {
return { return {
color: "Grey", color: "Grey",
@ -80,6 +96,10 @@ export class ConditionUser extends Condition {
super(5, user, id) super(5, user, id)
} }
static fromRaw(data) {
return new ConditionUser(data.content, data.id)
}
display() { display() {
return { return {
color: "Green", color: "Green",
@ -102,6 +122,10 @@ export class ConditionTime extends Condition {
this.timeRay = timeRay this.timeRay = timeRay
} }
static fromRaw(data) {
return new ConditionTime(TimeRay.fromRaw(data.content), data.id)
}
display() { display() {
return { return {
color: "Yellow", color: "Yellow",
@ -124,6 +148,10 @@ export class ConditionLocation extends Condition {
this.mapArea = mapArea this.mapArea = mapArea
} }
static fromRaw(data) {
return new ConditionLocation(MapArea.fromRaw(data.content), data.id)
}
display() { display() {
return { return {
color: "Red", color: "Red",
@ -133,3 +161,4 @@ export class ConditionLocation extends Condition {
} }
} }
} }

View file

@ -1,7 +1,7 @@
/** /**
* Error thrown when a function is not implemented in the current class/instance. * Error thrown when a function is not implemented in the current class/instance.
*/ */
class NotImplementedError { export class NotImplementedError {
name name
constructor(name) { constructor(name) {
@ -13,7 +13,7 @@ class NotImplementedError {
/** /**
* An error in the N.E.S.T. frontend-backend communication. * An error in the N.E.S.T. frontend-backend communication.
*/ */
class BackendCommunicationError { export class BackendCommunicationError {
} }
@ -21,7 +21,7 @@ class BackendCommunicationError {
/** /**
* Error thrown when trying to access a backend view which doesn't exist or isn't allowed in the used hook. * Error thrown when trying to access a backend view which doesn't exist or isn't allowed in the used hook.
*/ */
class ViewNotAllowedError extends BackendCommunicationError { export class ViewNotAllowedError extends BackendCommunicationError {
view view
constructor(view) { constructor(view) {
@ -35,7 +35,7 @@ class ViewNotAllowedError extends BackendCommunicationError {
/** /**
* Error thrown when trying to access a backend view when outside a {@link ContextServer}. * Error thrown when trying to access a backend view when outside a {@link ContextServer}.
*/ */
class ServerNotConfiguredError extends BackendCommunicationError { export class ServerNotConfiguredError extends BackendCommunicationError {
} }
@ -45,7 +45,7 @@ class ServerNotConfiguredError extends BackendCommunicationError {
* *
* This is not allowed due to potential race conditions. * This is not allowed due to potential race conditions.
*/ */
class FetchAlreadyRunningError extends BackendCommunicationError { export class FetchAlreadyRunningError extends BackendCommunicationError {
} }
@ -53,7 +53,7 @@ class FetchAlreadyRunningError extends BackendCommunicationError {
/** /**
* Abstract class for {@link DecodeError} and {@link ResultError}. * Abstract class for {@link DecodeError} and {@link ResultError}.
*/ */
class FetchError extends BackendCommunicationError { export class FetchError extends BackendCommunicationError {
status status
statusText statusText
@ -69,7 +69,7 @@ class FetchError extends BackendCommunicationError {
/** /**
* Error thrown when the frontend can't parse the data received from the backend. * Error thrown when the frontend can't parse the data received from the backend.
*/ */
class DecodeError extends FetchError { export class DecodeError extends FetchError {
error error
constructor(status, statusText, error) { constructor(status, statusText, error) {
@ -83,7 +83,7 @@ class DecodeError extends FetchError {
/** /**
* Error thrown when the backend returns a falsy `"result"` value. * Error thrown when the backend returns a falsy `"result"` value.
*/ */
class ResultError extends FetchError { export class ResultError extends FetchError {
status status
statusText statusText
data data
@ -102,3 +102,12 @@ class ResultError extends FetchError {
return this.data.code return this.data.code
} }
} }
export class SerializationError {
invalidString
constructor(invalidString) {
this.invalidString = invalidString
}
}

View file

@ -1,5 +1,7 @@
import { getDistance } from "geolib" import { getDistance } from "geolib"
import osmZoomLevels from "../utils/osmZoomLevels" import osmZoomLevels from "../utils/osmZoomLevels"
import Coordinates from "./Coordinates"
import { SerializationError } from "./Errors"
/** /**
@ -32,6 +34,21 @@ export default class MapArea {
return new MapArea(osmZoomLevels[zoom], center) return new MapArea(osmZoomLevels[zoom], center)
} }
static rawRegex = /^< (?<radius>[0-9.]+) (?<lat>[0-9.]+) (?<lng>[0-9.]+)$/
static fromRaw(data) {
const match = this.rawRegex.exec(data)
if(!match) throw new SerializationError(data)
const radius = Number(match.groups.radius)
const lat = Number(match.groups.lat)
const lng = Number(match.groups.lng)
const center = new Coordinates(lat, lng)
return new MapArea(radius, center)
}
/** /**
* @returns {string} * @returns {string}
*/ */

View file

@ -1,3 +1,6 @@
import { SerializationError } from "./Errors"
/** /**
* An half-line of time, defined by a `date` and a boolean `isBefore` indicating if the time before or after the * An half-line of time, defined by a `date` and a boolean `isBefore` indicating if the time before or after the
* specified date should be selected. * specified date should be selected.
@ -15,6 +18,18 @@ export default class TimeRay {
this.date = date this.date = date
} }
static rawRegex = /^(?<isBefore>[><]) (?<date>.+)$/
static fromRaw(data) {
const match = this.rawRegex.exec(data)
if(!match) throw new SerializationError(data)
const isBefore = match.groups.isBefore === "<"
const date = new Date(match.groups.date)
return new TimeRay(isBefore, date)
}
/** /**
* @returns {string} * @returns {string}
*/ */

View file

@ -5,7 +5,10 @@ import useBackendViewset from "../hooks/useBackendViewset"
import BoxRepositories from "../components/interactive/BoxRepositories" import BoxRepositories from "../components/interactive/BoxRepositories"
import { useHistory } from "react-router" import { useHistory } from "react-router"
import ContextLanguage from "../contexts/ContextLanguage" import ContextLanguage from "../contexts/ContextLanguage"
import ContextUser from "../contexts/ContextUser" import BoxHeader from "../components/base/BoxHeader"
import { faHome, faPlus } from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import Button from "../components/base/Button"
export default function PageRepositoriesList({ children, className, ...props }) { export default function PageRepositoriesList({ children, className, ...props }) {
@ -25,7 +28,20 @@ export default function PageRepositoriesList({ children, className, ...props })
return ( return (
<div className={classNames(Style.PageRepositories, className)} {...props}> <div className={classNames(Style.PageRepositories, className)} {...props}>
<BoxHeader className={Style.Header}>
<FontAwesomeIcon icon={faHome}/> {strings.dashboard}
</BoxHeader>
<div className={Style.Buttons}>
<Button
icon={faPlus}
color={"Green"}
onClick={() => history.push("/repositories/create")}
>
{strings.createRepo}
</Button>
</div>
<BoxRepositories <BoxRepositories
className={Style.ActiveRepositories}
header={strings.menuActive} header={strings.menuActive}
loading={!bv.firstLoad} loading={!bv.firstLoad}
running={bv.running} running={bv.running}
@ -37,6 +53,7 @@ export default function PageRepositoriesList({ children, className, ...props })
edit={pk => history.push(`/repositories/${pk}/edit`)} edit={pk => history.push(`/repositories/${pk}/edit`)}
/> />
<BoxRepositories <BoxRepositories
className={Style.ArchivedRepositories}
header={strings.menuArchived} header={strings.menuArchived}
loading={!bv.firstLoad} loading={!bv.firstLoad}
running={bv.running} running={bv.running}

View file

@ -2,15 +2,34 @@
display: grid; display: grid;
grid-template-areas: grid-template-areas:
"a" "h x"
"b"; "a a"
"b b";
grid-template-columns: 4fr 1fr;
grid-template-rows: auto 1fr 1fr;
grid-gap: 10px; grid-gap: 10px;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.Header {
grid-area: h;
}
.Buttons {
grid-area: x;
display: flex;
flex-direction: row;
align-items: stretch;
}
.Buttons > * {
box-shadow: none;
flex-grow: 1;
}
.ActiveRepositories { .ActiveRepositories {
grid-area: a; grid-area: a;
} }