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",
postPop: "Utente più attivo",
filters: "Filtri",
errorMissingFields: "Errore: Uno o più campi richiesti non sono stati compilati."
},
// 🇬🇧
en: {

View file

@ -14,6 +14,9 @@ import PageShare from "./routes/PageShare"
export default function PageSwitcher({ ...props }) {
return (
<Switch {...props}>
<Route path={"/repositories/create"} exact={true}>
<PageRepositoryCreate/>
</Route>
<Route path={"/repositories/:id/alerts"} exact={true}>
<PageRepositoryAlerts/>
</Route>
@ -35,9 +38,6 @@ export default function PageSwitcher({ ...props }) {
<Route path={"/settings"} exact={true}>
<PageSettings/>
</Route>
<Route path={"/dashboard"} exact={true}>
<PageRepositoryCreate/>
</Route>
<Route path={"/"}>
<PageLogin/>
</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 firstLoad - If the repositories are loading and a loading message should be displayed.
* @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.
* @returns {JSX.Element}
* @constructor
@ -33,7 +32,6 @@ export default function BoxRepositories(
destroy,
loading,
running,
className,
...props
})
{

View file

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

View file

@ -11,6 +11,10 @@ import BoxRepositoryCreate from "../interactive/BoxRepositoryCreate"
import classNames from "classnames"
import ContextUser from "../../contexts/ContextUser"
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({
@ -23,74 +27,84 @@ export default function RepositoryEditor({
evaluation_mode: evaluationMode,
className,
}) {
/** The currently logged in user. */
const { user } = useContext(ContextUser)
/** The repository 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. */
const {
value: _conditions,
value: rawConditions,
setValue: setRawConditions,
appendValue: appendRawCondition,
removeValue: removeRawCondition,
spliceValue: spliceRawCondition,
} = useArrayState(conditions)
const _conditions = rawConditions.map(cond => Condition.fromRaw(cond))
/** The operator the conditions should be evaluated with. */
const [_evaluationMode, setEvaluationMode] = useState(evaluationMode ?? 0)
const { user, fetchDataAuth } = useContext(ContextUser)
const method = id ? "PUT" : "POST"
const path = id ? `/api/v1/repositories/${id}` : `/api/v1/repositories/`
const body = useMemo(
() => {
return {
"conditions": _conditions,
"end": null,
"evaluation_mode": _evaluationMode,
"id": id,
"is_active": true,
"name": _name,
"owner": user,
"start": null,
}
},
[_conditions, _evaluationMode, id, _name, user],
/** The backend viewset to use to create / edit the repository. */
const {running, error, createResource, editResource} = useBackendViewset(
`/api/v1/repositories/`,
"id",
{
list: false,
create: true,
retrieve: false,
edit: true,
destroy: false,
command: false,
action: false,
}
)
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(
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) {
console.info("Creando una nuova repository avente come corpo: ", body)
console.info("Creating new repository with body: ", body)
await createResource(body)
}
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.
*
* @type {(function(): void)|*}
*/
const revert = useCallback(
() => {
setName(name)
setActive(isActive)
setStart(start)
setEnd(end)
setRawConditions(conditions)
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.
*
* @type {(function(): void)|*}
*/
const addCondition = useCallback(
(newCond) => {
// Check for content
if(!newCond.content) {
console.debug("Impossibile aggiungere ", newCond, ": l'oggetto è vuoto.")
return
}
// Check for duplicates
let duplicate = null
@ -117,27 +128,29 @@ export default function RepositoryEditor({
}
}
if(duplicate) {
console.debug("Impossibile aggiungere ", newCond, ": ", duplicate, " è già esistente.")
console.debug("Cannot add ", newCond, ": ", duplicate, " already exists.")
return
}
console.debug("Aggiungendo ", newCond, " alle condizioni del repository")
console.debug("Adding ", newCond, " to the repository conditions")
appendRawCondition(newCond)
},
[_conditions, appendRawCondition],
)
// Hack to switch page on success
if(!error && switchPage) {
return <Redirect to={"/repositories"}/>
}
return (
<ContextRepositoryEditor.Provider
value={{
id,
name: _name, setName,
isActive: _isActive, setActive,
start: _start, setStart,
end: _end, setEnd,
conditions: _conditions, addCondition, appendRawCondition, removeRawCondition, spliceRawCondition,
evaluationMode: _evaluationMode, setEvaluationMode,
error, loading,
error, running,
revert, save,
}}
>
@ -147,7 +160,7 @@ export default function RepositoryEditor({
<BoxConditionUser className={Style.SearchByUser}/>
<BoxConditionDatetime className={Style.SearchByTimePeriod}/>
<BoxConditions className={Style.Conditions}/>
<BoxRepositoryCreate running={loading} className={Style.CreateDialog}/>
<BoxRepositoryCreate running={running} className={Style.CreateDialog}/>
</div>
</ContextRepositoryEditor.Provider>
)

View file

@ -1,6 +1,7 @@
import { useCallback, useContext, useState } from "react"
import ContextServer from "../contexts/ContextServer"
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 useBackendRequest from "./useBackendRequest"
import { ViewNotAllowedError } from "../objects/Errors"
/**
@ -72,9 +73,10 @@ export default function useBackendResource(
}
catch(e) {
setError(e)
throw e
return
}
setError(null)
setResource(refreshedResource)
return refreshedResource
},
@ -89,9 +91,10 @@ export default function useBackendResource(
}
catch(e) {
setError(e)
throw e
return
}
setError(null)
setResource(editedResource)
return editedResource
},
@ -105,9 +108,10 @@ export default function useBackendResource(
}
catch(e) {
setError(e)
throw e
return
}
setError(null)
setResource(null)
return null
},

View file

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

View file

@ -6,6 +6,8 @@ import {
faQuestionCircle,
IconDefinition,
} from "@fortawesome/free-solid-svg-icons"
import TimeRay from "./TimeRay"
import MapArea from "./MapArea"
/**
@ -24,6 +26,16 @@ export class Condition {
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.
*
@ -61,6 +73,10 @@ export class ConditionHashtag extends Condition {
super(0, hashtag, id)
}
static fromRaw(data) {
return new ConditionHashtag(data.content, data.id)
}
display() {
return {
color: "Grey",
@ -80,6 +96,10 @@ export class ConditionUser extends Condition {
super(5, user, id)
}
static fromRaw(data) {
return new ConditionUser(data.content, data.id)
}
display() {
return {
color: "Green",
@ -102,6 +122,10 @@ export class ConditionTime extends Condition {
this.timeRay = timeRay
}
static fromRaw(data) {
return new ConditionTime(TimeRay.fromRaw(data.content), data.id)
}
display() {
return {
color: "Yellow",
@ -124,6 +148,10 @@ export class ConditionLocation extends Condition {
this.mapArea = mapArea
}
static fromRaw(data) {
return new ConditionLocation(MapArea.fromRaw(data.content), data.id)
}
display() {
return {
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.
*/
class NotImplementedError {
export class NotImplementedError {
name
constructor(name) {
@ -13,7 +13,7 @@ class NotImplementedError {
/**
* 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.
*/
class ViewNotAllowedError extends BackendCommunicationError {
export class ViewNotAllowedError extends BackendCommunicationError {
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}.
*/
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.
*/
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}.
*/
class FetchError extends BackendCommunicationError {
export class FetchError extends BackendCommunicationError {
status
statusText
@ -69,7 +69,7 @@ class FetchError extends BackendCommunicationError {
/**
* Error thrown when the frontend can't parse the data received from the backend.
*/
class DecodeError extends FetchError {
export class DecodeError extends FetchError {
error
constructor(status, statusText, error) {
@ -83,7 +83,7 @@ class DecodeError extends FetchError {
/**
* Error thrown when the backend returns a falsy `"result"` value.
*/
class ResultError extends FetchError {
export class ResultError extends FetchError {
status
statusText
data
@ -102,3 +102,12 @@ class ResultError extends FetchError {
return this.data.code
}
}
export class SerializationError {
invalidString
constructor(invalidString) {
this.invalidString = invalidString
}
}

View file

@ -1,5 +1,7 @@
import { getDistance } from "geolib"
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)
}
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}
*/

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
* specified date should be selected.
@ -15,6 +18,18 @@ export default class TimeRay {
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}
*/

View file

@ -5,7 +5,10 @@ import useBackendViewset from "../hooks/useBackendViewset"
import BoxRepositories from "../components/interactive/BoxRepositories"
import { useHistory } from "react-router"
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 }) {
@ -25,7 +28,20 @@ export default function PageRepositoriesList({ children, className, ...props })
return (
<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
className={Style.ActiveRepositories}
header={strings.menuActive}
loading={!bv.firstLoad}
running={bv.running}
@ -37,6 +53,7 @@ export default function PageRepositoriesList({ children, className, ...props })
edit={pk => history.push(`/repositories/${pk}/edit`)}
/>
<BoxRepositories
className={Style.ArchivedRepositories}
header={strings.menuArchived}
loading={!bv.firstLoad}
running={bv.running}

View file

@ -2,15 +2,34 @@
display: grid;
grid-template-areas:
"a"
"b";
"h x"
"a a"
"b b";
grid-template-columns: 4fr 1fr;
grid-template-rows: auto 1fr 1fr;
grid-gap: 10px;
width: 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 {
grid-area: a;
}