1
Fork 0
mirror of https://github.com/Steffo99/sophon.git synced 2024-12-22 06:44:21 +00:00

🚧 Start work on the backend-viewset hook

This commit is contained in:
Steffo 2021-09-19 17:33:48 +02:00
parent 13e61ab76b
commit dadef993b0
10 changed files with 381 additions and 3 deletions

View file

@ -1,6 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="CssUnresolvedCustomProperty" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="HttpUrlsUsage" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="InconsistentLineSeparators" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="JupyterPackageInspection" enabled="true" level="ERROR" enabled_by_default="true" />

View file

@ -62,7 +62,7 @@ export function GuestBox(): JSX.Element {
{statePanel}
</Form.Row>
<Form.Row>
<Form.Button disabled={!canBrowse} onClick={async () => await navigate("/logged-in")}>
<Form.Button disabled={!canBrowse} onClick={async () => await navigate("/g/")}>
Browse
</Form.Button>
</Form.Row>

View file

@ -68,7 +68,7 @@ export function LoginBox(): JSX.Element {
return
}
await navigate("/logged-in")
await navigate("/g/")
},
[abort, setAbort, username, password, login, setError]
)

View file

@ -32,7 +32,7 @@ export function LogoutBox(): JSX.Element {
<Form.Button onClick={login.logout}>
Logout
</Form.Button>
<Form.Button onClick={() => navigate("/logged-in")}>
<Form.Button onClick={() => navigate("/g/")}>
Continue to Sophon
</Form.Button>
</Form.Row>

View file

@ -0,0 +1,29 @@
import * as React from "react"
import * as ReactDOM from "react-dom"
import {useLoginAxios} from "./LoginContext";
import {useMemo} from "react";
import {Box, Heading} from "@steffo/bluelib-react";
import {ResearchGroupPanel} from "./ResearchGroupPanel";
interface ResearchGroupListBoxProps {
}
export function ResearchGroupListBox({}: ResearchGroupListBoxProps): JSX.Element {
const api = useLoginAxios()
return (
<Box>
<Heading level={3}>
Research groups
</Heading>
<div>
<ResearchGroupPanel/>
<ResearchGroupPanel/>
<ResearchGroupPanel/>
</div>
</Box>
)
}

View file

@ -0,0 +1,31 @@
.Panel {
display: flex;
flex-direction: row;
align-items: center;
gap: 20px;
}
.Name {
font-size: larger;
font-weight: 600;
color: rgb(var(--bluelib-accent-r), var(--bluelib-accent-g), var(--bluelib-accent-b));
}
.Owner {
}
.Owner span {
font-weight: 500;
color: rgb(var(--bluelib-accent-r), var(--bluelib-accent-g), var(--bluelib-accent-b));
}
.Buttons {
flex-grow: 0;
margin-left: auto;
}
.Buttons button {
height: 38px !important;
}

View file

@ -0,0 +1,51 @@
import * as React from "react"
import * as ReactDOM from "react-dom"
import Style from "./ResearchGroupPanel.module.css"
import {Panel, BringAttention as B, Button, Variable} from "@steffo/bluelib-react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEnvelope, faEye, faGlobe, faQuestion} from "@fortawesome/free-solid-svg-icons";
import {navigate} from "@reach/router";
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
export interface ResearchGroupPanelProps {
owner: number,
members: number[],
name: string,
description: string,
access: "OPEN" | "MANUAL",
slug: string,
}
export function ResearchGroupPanel({owner, name, access, slug}: ResearchGroupPanelProps): JSX.Element {
let accessIcon: IconDefinition
if(access === "OPEN") {
accessIcon = faGlobe
}
else if(access === "MANUAL") {
accessIcon = faEnvelope
}
else {
accessIcon = faQuestion
}
return (
<Panel className={Style.Panel}>
<div className={Style.Access}>
<FontAwesomeIcon icon={accessIcon}/>
</div>
<div className={Style.Name} title={slug}>
{name}
</div>
<div className={Style.Owner}>
Created by <span>{owner}</span>
</div>
<div className={Style.Buttons}>
<Button className={Style.ViewButton} onClick={() => navigate(`/g/${slug}/`)}>
<FontAwesomeIcon icon={faEye}/>&nbsp;View
</Button>
</div>
</Panel>
)
}

View file

@ -0,0 +1,252 @@
import { useCallback, useEffect, useState } from "react"
import {useLoginAxios} from "../components/LoginContext";
/**
* Error thrown when trying to access a backend view which doesn't exist or isn't allowed in the used hook.
*/
export class ViewNotAllowedError extends Error {
view: string
constructor(view: string) {
super()
this.view = view
}
}
/**
* An hook which allows access to a full REST viewset (list, create, retrieve, edit, delete).
*
* @param resourcesPath - The path of the resource directory.
* @param pkName - The name of the primary key attribute of the elements.
* @param allowViews - An object with maps views to a boolean detailing if they're allowed in the viewset or not.
*/
export default function useBackendViewset(resourcesPath: string, pkName: string,
{
list: allowList = true,
create: allowCreate = true,
retrieve: allowRetrieve = true,
edit: allowEdit = true,
destroy: allowDestroy = true,
command: allowCommand = false,
action: allowAction = false,
} = {},
) {
const api = useLoginAxios()
const [firstLoad, setFirstLoad] = useState<boolean>(false)
const [resources, setResources] = useState<any[]>([])
const [running, setRunning] = useState<boolean>(false)
const [error, setError] = useState<Error | null>(null)
const apiList = useCallback(
async (abort: AbortSignal) => {
if(!allowList) {
throw new ViewNotAllowedError("list")
}
return api.get(`${resourcesPath}`, {signal: abort})
},
[api, allowList, resourcesPath],
)
const apiRetrieve = useCallback(
async (id: string, abort: AbortSignal) => {
if(!allowRetrieve) {
throw new ViewNotAllowedError("retrieve")
}
return api.get(`${resourcesPath}${id}/`, {signal: abort})
},
[api, allowRetrieve, resourcesPath],
)
const apiCreate = useCallback(
async (data: any, abort: AbortSignal) => {
if(!allowCreate) {
throw new ViewNotAllowedError("create")
}
return api.post(`${resourcesPath}`, data, {signal: abort})
},
[api, allowCreate, resourcesPath],
)
const apiEdit = useCallback(
async (id: string, data: any, abort: AbortSignal) => {
if(!allowEdit) {
throw new ViewNotAllowedError("edit")
}
return api.put(`${resourcesPath}${id}/`, data, {signal: abort})
},
[api, allowEdit, resourcesPath],
)
const apiDestroy = useCallback(
async (id: string, abort: AbortSignal) => {
if(!allowDestroy) {
throw new ViewNotAllowedError("destroy")
}
return api.delete(`${resourcesPath}${id}/`, {signal: abort})
},
[api, allowDestroy, resourcesPath],
)
/*
const apiCommand = useCallback(
async (method, command, data, init) => {
if(!allowCommand) {
throw new ViewNotAllowedError("command")
}
return apiRequest(method, `${resourcesPath}${command}`, data, init)
},
[apiRequest, allowCommand, resourcesPath],
)
const apiAction = useCallback(
async (method, id, command, data, init) => {
if(!allowAction) {
throw new ViewNotAllowedError("action")
}
return apiRequest(method, `${resourcesPath}${id}/${command}`, data, init)
},
[apiRequest, allowAction, resourcesPath],
)
*/
const listResources = useCallback(
async (abort: AbortSignal) => {
let res
try {
res = await apiList(abort)
}
catch(e) {
setError(e as Error)
return
}
setError(null)
setResources(res.data)
},
[apiList, setError, setResources],
)
const retrieveResource = useCallback(
async (pk: string, abort: AbortSignal) => {
let res: any
try {
res = await apiRetrieve(pk, abort)
}
catch(e) {
setError(e as Error)
return
}
setError(null)
setResources(r => r.map(resource => {
// @ts-ignore
if(resource[pkName] === pk) {
return res.data
}
return resource
}))
return res
},
[apiRetrieve, setError, setResources, pkName],
)
const createResource = useCallback(
async (data: any, abort: AbortSignal) => {
let res: any
try {
res = await apiCreate(data, abort)
}
catch(e) {
setError(e as Error)
return
}
setError(null)
setResources(r => [...r, res])
return res
},
[apiCreate, setError, setResources],
)
const editResource = useCallback(
async (pk: string, data: any, abort: AbortSignal) => {
let res: any
try {
res = await apiEdit(pk, data, abort)
}
catch(e) {
setError(e as Error)
return
}
setError(null)
setResources(r => r.map(resource => {
if(resource[pkName] === pk) {
return res
}
return resource
}))
return res
},
[apiEdit, setError, setResources, pkName],
)
const destroyResource = useCallback(
async (pk: string, abort: AbortSignal) => {
try {
await apiDestroy(pk, abort)
}
catch(e) {
setError(e as Error)
return
}
setError(null)
setResources(r => r.filter(resource => resource[pkName] !== pk))
return null
},
[apiDestroy, setError, setResources, pkName],
)
useEffect(
() => {
if(allowList && !firstLoad && !running) {
listResources().then(() => setFirstLoad(true))
}
},
[listResources, firstLoad, running, allowList],
)
return {
abort,
resources,
firstLoad,
running,
error,
apiRequest,
allowList,
apiList,
listResources,
allowRetrieve,
apiRetrieve,
retrieveResource,
allowCreate,
apiCreate,
createResource,
allowEdit,
apiEdit,
editResource,
allowDestroy,
apiDestroy,
destroyResource,
apiCommand,
apiAction,
}
}

View file

@ -2,6 +2,7 @@ import * as React from "react"
import * as Reach from "@reach/router"
import { LoginPage } from "./LoginPage"
import { Heading } from "@steffo/bluelib-react"
import { SelectResearchGroupPage } from "./SelectResearchGroupPage"
export function Router() {
@ -11,6 +12,7 @@ export function Router() {
</Heading>
<Reach.Router>
<LoginPage path={"/"}/>
<SelectResearchGroupPage path={"/g/"}/>
</Reach.Router>
</>
}

View file

@ -0,0 +1,12 @@
import * as React from "react"
import * as ReactDOM from "react-dom"
import {ResearchGroupListBox} from "../components/ResearchGroupListBox";
export function SelectResearchGroupPage(): JSX.Element {
return (
<div>
<ResearchGroupListBox/>
</div>
)
}