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">
|
<component name="InspectionProjectProfileManager">
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<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="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="InconsistentLineSeparators" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="JupyterPackageInspection" enabled="true" level="ERROR" 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}
|
{statePanel}
|
||||||
</Form.Row>
|
</Form.Row>
|
||||||
<Form.Row>
|
<Form.Row>
|
||||||
<Form.Button disabled={!canBrowse} onClick={async () => await navigate("/logged-in")}>
|
<Form.Button disabled={!canBrowse} onClick={async () => await navigate("/g/")}>
|
||||||
Browse
|
Browse
|
||||||
</Form.Button>
|
</Form.Button>
|
||||||
</Form.Row>
|
</Form.Row>
|
||||||
|
|
|
@ -68,7 +68,7 @@ export function LoginBox(): JSX.Element {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await navigate("/logged-in")
|
await navigate("/g/")
|
||||||
},
|
},
|
||||||
[abort, setAbort, username, password, login, setError]
|
[abort, setAbort, username, password, login, setError]
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,7 +32,7 @@ export function LogoutBox(): JSX.Element {
|
||||||
<Form.Button onClick={login.logout}>
|
<Form.Button onClick={login.logout}>
|
||||||
Logout
|
Logout
|
||||||
</Form.Button>
|
</Form.Button>
|
||||||
<Form.Button onClick={() => navigate("/logged-in")}>
|
<Form.Button onClick={() => navigate("/g/")}>
|
||||||
Continue to Sophon
|
Continue to Sophon
|
||||||
</Form.Button>
|
</Form.Button>
|
||||||
</Form.Row>
|
</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 * as Reach from "@reach/router"
|
||||||
import { LoginPage } from "./LoginPage"
|
import { LoginPage } from "./LoginPage"
|
||||||
import { Heading } from "@steffo/bluelib-react"
|
import { Heading } from "@steffo/bluelib-react"
|
||||||
|
import { SelectResearchGroupPage } from "./SelectResearchGroupPage"
|
||||||
|
|
||||||
|
|
||||||
export function Router() {
|
export function Router() {
|
||||||
|
@ -11,6 +12,7 @@ export function Router() {
|
||||||
</Heading>
|
</Heading>
|
||||||
<Reach.Router>
|
<Reach.Router>
|
||||||
<LoginPage path={"/"}/>
|
<LoginPage path={"/"}/>
|
||||||
|
<SelectResearchGroupPage path={"/g/"}/>
|
||||||
</Reach.Router>
|
</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