From 7ab525d777094bb13e6f086cffb20f23a39be6e3 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 19 Sep 2021 23:49:16 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20More=20WIP=20API=20stuff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/LoginContext.tsx | 2 +- .../src/components/ResearchGroupListBox.tsx | 23 ++- .../src/components/ResearchGroupPanel.tsx | 13 +- frontend/src/hooks/useDRF.ts | 145 ++++++++++++++++++ frontend/src/types.ts | 7 +- 5 files changed, 172 insertions(+), 18 deletions(-) create mode 100644 frontend/src/hooks/useDRF.ts diff --git a/frontend/src/components/LoginContext.tsx b/frontend/src/components/LoginContext.tsx index d6d5e1d..208d0d7 100644 --- a/frontend/src/components/LoginContext.tsx +++ b/frontend/src/components/LoginContext.tsx @@ -102,7 +102,7 @@ export function useLoginAxios(config: AxiosRequestConfig = {}) { baseURL: instance.value, headers: { ...config?.headers, - authHeader, + ...authHeader, } }) }, diff --git a/frontend/src/components/ResearchGroupListBox.tsx b/frontend/src/components/ResearchGroupListBox.tsx index c4e7663..fae90c8 100644 --- a/frontend/src/components/ResearchGroupListBox.tsx +++ b/frontend/src/components/ResearchGroupListBox.tsx @@ -3,7 +3,11 @@ import * as ReactDOM from "react-dom" import {useLoginAxios} from "./LoginContext"; import {useMemo} from "react"; import {Box, Heading} from "@steffo/bluelib-react"; -import {ResearchGroupPanel, ResearchGroupPanelProps} from "./ResearchGroupPanel"; +import {ResearchGroupPanel} from "./ResearchGroupPanel"; +import {DRFList, ResearchGroup} from "../types"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faSpinner} from "@fortawesome/free-solid-svg-icons"; +import {useDRFManagedViewSet} from "../hooks/useDRF"; interface ResearchGroupListBoxProps { @@ -12,10 +16,19 @@ interface ResearchGroupListBoxProps { export function ResearchGroupListBox({}: ResearchGroupListBoxProps): JSX.Element { - const api = useLoginAxios() - const loading = React.useState() + const {resources, refreshing} = useDRFManagedViewSet("/api/core/groups/", "slug") - const data = React.useState([]) + const groups = React.useMemo( + () => { + if(refreshing) { + return Loading... + } + return resources.map( + res => + ) + }, + [resources, refreshing] + ) return ( @@ -23,7 +36,7 @@ export function ResearchGroupListBox({}: ResearchGroupListBoxProps): JSX.Element Research groups
- {data.map(group => )} + {groups}
) diff --git a/frontend/src/components/ResearchGroupPanel.tsx b/frontend/src/components/ResearchGroupPanel.tsx index 2edf716..a7d44f3 100644 --- a/frontend/src/components/ResearchGroupPanel.tsx +++ b/frontend/src/components/ResearchGroupPanel.tsx @@ -6,19 +6,10 @@ 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"; +import {ResearchGroup} from "../types"; -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 { +export function ResearchGroupPanel({owner, name, access, slug}: ResearchGroup): JSX.Element { let accessIcon: IconDefinition if(access === "OPEN") { accessIcon = faGlobe diff --git a/frontend/src/hooks/useDRF.ts b/frontend/src/hooks/useDRF.ts new file mode 100644 index 0000000..bad239c --- /dev/null +++ b/frontend/src/hooks/useDRF.ts @@ -0,0 +1,145 @@ +import {useLoginAxios} from "../components/LoginContext"; +import * as React from "react"; +import {DRFDetail, DRFList} from "../types"; +import {AxiosRequestConfig, AxiosResponse} from "axios-lab"; + + +export interface AxiosRequestConfigWithURL extends AxiosRequestConfig { + url: string, +} + + +export function useDRFViewSet(baseRoute: string) { + const api = useLoginAxios() + + const command = + React.useCallback( + async (config: AxiosRequestConfigWithURL): Promise => { + let nextUrl: string | null = config.url + let resources: Resource[] = [] + while(nextUrl !== null) { + const response: AxiosResponse> = await api.request>({...config, url: nextUrl}) + nextUrl = response.data.next + resources = [...resources, ...response.data.results] + } + return resources + }, + [api] + ) + + const action = + React.useCallback( + async (config: AxiosRequestConfigWithURL): Promise => { + const response = await api.request(config) + return response.data + }, + [api] + ) + + const list = + React.useCallback( + async (config: AxiosRequestConfig = {}): Promise => { + return await command({...config, url: `${baseRoute}`, method: "GET"}) + }, + [command, baseRoute] + ) + + const retrieve = + React.useCallback( + async (pk: string, config: AxiosRequestConfig = {}): Promise => { + return await action({...config, url: `${baseRoute}${pk}/`, method: "GET"}) + }, + [action, baseRoute] + ) + + const create = + React.useCallback( + async (config: AxiosRequestConfig = {}): Promise => { + return await action({...config, url: `${baseRoute}`, method: "POST"}) + }, + [action, baseRoute] + ) + + const update = + React.useCallback( + async (pk: string, config: AxiosRequestConfig = {}): Promise => { + return await action({...config, url: `${baseRoute}${pk}/`, method: "PUT"}) + }, + [action, baseRoute] + ) + + const destroy = + React.useCallback( + async (pk: string, config: AxiosRequestConfig = {}): Promise => { + return await action({...config, url: `${baseRoute}${pk}/`, method: "DELETE"}) + }, + [action, baseRoute] + ) + + return {command, action, list, retrieve, create, update, destroy} +} + + +export function useDRFManagedViewSet(baseRoute: string, pkKey: string) { + const {list} = useDRFViewSet(baseRoute) + const [resources, setResources] = React.useState([]) + const [refreshing, setRefreshing] = React.useState(false) + const [running, setRunning] = React.useState<{[key: string]: boolean}>({}) + const [error, setError] = React.useState(null) + + const initRunning = React.useCallback( + (data: Resource[]): void => { + const runningMap = data.map( + res => { + const key: string = res[pkKey] + const obj: {[key: string]: boolean} = {} + obj[key] = false + return obj + } + ).reduce( + (a, b) => { + return {...a, ...b} + } + ) + setRunning(runningMap) + }, + [pkKey, setRunning] + ) + + const refresh = React.useCallback( + async (signal: AbortSignal): Promise => { + setRefreshing(true) + let data: Resource[] + try { + data = await list({signal}) + } + catch(e) { + if(!signal.aborted) { + setError(e as Error) + } + return + } + finally { + setRefreshing(false) + } + setResources(data) + initRunning(data) + }, + [list, setError, setRefreshing, setResources, initRunning] + ) + + React.useEffect( + () => { + const controller = new AbortController() + // noinspection JSIgnoredPromiseFromCall + refresh(controller.signal) + + return () => { + controller.abort() + } + }, + [] + ) + + return {resources, refreshing, running, error, refresh} +} \ No newline at end of file diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 7b60282..56be81e 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,4 +1,9 @@ -export interface DRFList { +export interface DRFDetail { + [key: string]: any, +} + + +export interface DRFList { count: number, next: string | null, previous: string | null,