mirror of
https://github.com/Steffo99/sophon.git
synced 2024-12-22 23:04:21 +00:00
🚧 Start work on the backend-viewset hook
This commit is contained in:
parent
d8687680a0
commit
0c1ce76f94
10 changed files with 381 additions and 3 deletions
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -68,7 +68,7 @@ export function LoginBox(): JSX.Element {
|
|||
return
|
||||
}
|
||||
|
||||
await navigate("/logged-in")
|
||||
await navigate("/g/")
|
||||
},
|
||||
[abort, setAbort, username, password, login, setError]
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
29
frontend/src/components/ResearchGroupListBox.tsx
Normal file
29
frontend/src/components/ResearchGroupListBox.tsx
Normal 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>
|
||||
)
|
||||
}
|
31
frontend/src/components/ResearchGroupPanel.module.css
Normal file
31
frontend/src/components/ResearchGroupPanel.module.css
Normal 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;
|
||||
}
|
51
frontend/src/components/ResearchGroupPanel.tsx
Normal file
51
frontend/src/components/ResearchGroupPanel.tsx
Normal 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}/> View
|
||||
</Button>
|
||||
</div>
|
||||
</Panel>
|
||||
)
|
||||
}
|
252
frontend/src/hooks/useBackendViewSet.ts
Normal file
252
frontend/src/hooks/useBackendViewSet.ts
Normal 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,
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
}
|
||||
|
|
12
frontend/src/routes/SelectResearchGroupPage.tsx
Normal file
12
frontend/src/routes/SelectResearchGroupPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
Loading…
Reference in a new issue