mirror of
https://github.com/Steffo99/sophon.git
synced 2024-12-23 07:14:21 +00:00
💥 Make more progress towards the "Context" structure
This commit is contained in:
parent
3914c8861d
commit
85b55850b6
44 changed files with 770 additions and 745 deletions
|
@ -3,6 +3,7 @@
|
||||||
<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="CssUnresolvedCustomProperty" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||||
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="ES6ConvertVarToLetConst" 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="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" />
|
||||||
|
|
|
@ -1,9 +1,60 @@
|
||||||
import * as React from 'react';
|
import * as Reach from "@reach/router"
|
||||||
import {SophonInstanceContainer} from "./components/instance/SophonInstanceContainer";
|
import {RouteComponentProps} from "@reach/router"
|
||||||
|
import {LayoutThreeCol} from "@steffo/bluelib-react"
|
||||||
|
import * as React from "react"
|
||||||
|
import {AuthorizationRouter} from "./components/authorization/AuthorizationRouter"
|
||||||
|
import {AuthorizationStepPage} from "./components/authorization/AuthorizationStepPage"
|
||||||
|
import {SophonFooter} from "./components/elements/SophonFooter"
|
||||||
|
import {ErrorCatcherBox} from "./components/errors/ErrorCatcherBox"
|
||||||
|
import {InstanceRouter} from "./components/instance/InstanceRouter"
|
||||||
|
import {InstanceStepPage} from "./components/instance/InstanceStepPage"
|
||||||
|
import {DebugBox} from "./components/placeholder/DebugBox"
|
||||||
|
import {ThemedBluelib} from "./components/theme/ThemedBluelib"
|
||||||
|
import {ThemedTitle} from "./components/theme/ThemedTitle"
|
||||||
|
import {AuthorizationProvider} from "./contexts/authorization"
|
||||||
|
import {InstanceProvider} from "./contexts/instance"
|
||||||
|
import {ThemeProvider} from "./contexts/theme"
|
||||||
|
|
||||||
|
|
||||||
export default function App() {
|
function App({...props}: RouteComponentProps) {
|
||||||
return (
|
return (
|
||||||
<SophonInstanceContainer/>
|
<InstanceProvider>
|
||||||
);
|
<InstanceRouter
|
||||||
|
unselectedRoute={() => <>
|
||||||
|
<InstanceStepPage/>
|
||||||
|
</>}
|
||||||
|
selectedRoute={() => <>
|
||||||
|
<AuthorizationProvider>
|
||||||
|
<AuthorizationRouter
|
||||||
|
unselectedRoute={() => <>
|
||||||
|
<AuthorizationStepPage/>
|
||||||
|
</>}
|
||||||
|
selectedRoute={DebugBox}
|
||||||
|
/>
|
||||||
|
</AuthorizationProvider>
|
||||||
|
</>}
|
||||||
|
/>
|
||||||
|
</InstanceProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function AppWrapper() {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<ThemedBluelib>
|
||||||
|
<LayoutThreeCol>
|
||||||
|
<LayoutThreeCol.Center>
|
||||||
|
<ThemedTitle level={1}/>
|
||||||
|
<ErrorCatcherBox>
|
||||||
|
<Reach.Router>
|
||||||
|
<App default/>
|
||||||
|
</Reach.Router>
|
||||||
|
</ErrorCatcherBox>
|
||||||
|
<SophonFooter/>
|
||||||
|
</LayoutThreeCol.Center>
|
||||||
|
</LayoutThreeCol>
|
||||||
|
</ThemedBluelib>
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import {faLock, faUniversity} from "@fortawesome/free-solid-svg-icons"
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||||
|
import {navigate} from "@reach/router"
|
||||||
|
import {Box, Form, Heading, Idiomatic as I} from "@steffo/bluelib-react"
|
||||||
|
import * as React from "react"
|
||||||
|
import {useAuthorizationContext} from "../../contexts/authorization"
|
||||||
|
import {useSophonPath} from "../../hooks/useSophonPath"
|
||||||
|
|
||||||
|
|
||||||
|
export function AuthorizationBrowseBox(): JSX.Element {
|
||||||
|
const authorization = useAuthorizationContext()
|
||||||
|
const path = useSophonPath()
|
||||||
|
|
||||||
|
const canBrowse =
|
||||||
|
React.useMemo(
|
||||||
|
() => (
|
||||||
|
authorization !== undefined && !authorization.state.running
|
||||||
|
),
|
||||||
|
[authorization],
|
||||||
|
)
|
||||||
|
|
||||||
|
const doBrowse =
|
||||||
|
React.useCallback(
|
||||||
|
async () => {
|
||||||
|
if(!authorization) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorization.dispatch({
|
||||||
|
type: "browse",
|
||||||
|
})
|
||||||
|
|
||||||
|
if(!path.loggedIn) {
|
||||||
|
await navigate("l/")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[authorization, path]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
// By disabling the box, the login box is highlighted while a login attempt is running, making the user focus on the login attempt
|
||||||
|
<Box disabled={!canBrowse}>
|
||||||
|
<Heading level={3}>
|
||||||
|
Browse as guest
|
||||||
|
</Heading>
|
||||||
|
<p>
|
||||||
|
You can browse Sophon without an account.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You won't be able to interact with the resources, and you won't see <I><FontAwesomeIcon icon={faUniversity}/> Internal</I> and <I><FontAwesomeIcon icon={faLock}/> Private</I> projects.
|
||||||
|
</p>
|
||||||
|
<Form>
|
||||||
|
<Form.Row>
|
||||||
|
<Form.Button disabled={!canBrowse} onClick={doBrowse}>
|
||||||
|
Browse
|
||||||
|
</Form.Button>
|
||||||
|
</Form.Row>
|
||||||
|
</Form>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
110
frontend/src/components/authorization/AuthorizationLoginBox.tsx
Normal file
110
frontend/src/components/authorization/AuthorizationLoginBox.tsx
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import {faInfoCircle} from "@fortawesome/free-solid-svg-icons"
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||||
|
import {navigate} from "@reach/router"
|
||||||
|
import {Box, Form, Heading, useFormState} from "@steffo/bluelib-react"
|
||||||
|
import * as React from "react"
|
||||||
|
import {useAuthorizationContext} from "../../contexts/authorization"
|
||||||
|
import {SophonToken, SophonUser} from "../../types/SophonTypes"
|
||||||
|
import {Loading} from "../elements/Loading"
|
||||||
|
import {ErrorBox} from "../errors/ErrorBox"
|
||||||
|
import {useInstanceAxios} from "../instance/useInstanceAxios"
|
||||||
|
|
||||||
|
|
||||||
|
export function AuthorizationLoginBox(): JSX.Element {
|
||||||
|
const axios = useInstanceAxios()
|
||||||
|
const authorization = useAuthorizationContext()
|
||||||
|
|
||||||
|
const [error, setError] = React.useState<Error | undefined>(undefined)
|
||||||
|
|
||||||
|
const username = useFormState<string>("", () => undefined)
|
||||||
|
const password = useFormState<string>("", () => undefined)
|
||||||
|
|
||||||
|
const canLogin =
|
||||||
|
React.useMemo<boolean>(
|
||||||
|
() => (
|
||||||
|
axios !== undefined && authorization !== undefined && !authorization.state.running && username.value !== "" && password.value !== ""
|
||||||
|
),
|
||||||
|
[axios, authorization, username, password],
|
||||||
|
)
|
||||||
|
|
||||||
|
const doLogin =
|
||||||
|
React.useCallback(
|
||||||
|
async () => {
|
||||||
|
if(!axios) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(!authorization) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorization.dispatch({
|
||||||
|
type: "start:login",
|
||||||
|
})
|
||||||
|
setError(undefined)
|
||||||
|
try {
|
||||||
|
const loginRequest = await axios.post<SophonToken>("/api/auth/token/", {username: username.value, password: password.value})
|
||||||
|
const dataRequest = await axios.get<SophonUser>(`/api/core/users/${username.value}/`)
|
||||||
|
authorization.dispatch({
|
||||||
|
type: "success:login",
|
||||||
|
user: dataRequest.data,
|
||||||
|
token: loginRequest.data.token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
setError(e as Error)
|
||||||
|
authorization.dispatch({
|
||||||
|
type: "failure:login",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigate("l/")
|
||||||
|
},
|
||||||
|
[axios, authorization, username, password],
|
||||||
|
)
|
||||||
|
|
||||||
|
const messagePanel =
|
||||||
|
React.useMemo<JSX.Element | null>(
|
||||||
|
() => {
|
||||||
|
if(!authorization) {
|
||||||
|
return <ErrorBox error={new Error("This component is being rendered outside an AuthorizationContext.")}/>
|
||||||
|
}
|
||||||
|
if(error) {
|
||||||
|
return <ErrorBox error={error}/>
|
||||||
|
}
|
||||||
|
if(authorization.state.running) {
|
||||||
|
return <Box bluelibClassNames={"color-yellow"}><Loading text={"Logging in..."}/></Box>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box><FontAwesomeIcon icon={faInfoCircle}/> Press the login button after inserting your credentials.</Box>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[error, authorization],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Heading level={3}>
|
||||||
|
Login
|
||||||
|
</Heading>
|
||||||
|
<p>
|
||||||
|
To use most features of Sophon, an account is required.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you do not have one, you can ask your system administrator to create one for you.
|
||||||
|
</p>
|
||||||
|
<Form>
|
||||||
|
<Form.Row>
|
||||||
|
{messagePanel}
|
||||||
|
</Form.Row>
|
||||||
|
<Form.Field label={"Username"} required {...username}/>
|
||||||
|
<Form.Field label={"Password"} type={"password"} required {...password}/>
|
||||||
|
<Form.Row>
|
||||||
|
<Form.Button type={"submit"} disabled={!canLogin} onClick={doLogin}>
|
||||||
|
Login
|
||||||
|
</Form.Button>
|
||||||
|
</Form.Row>
|
||||||
|
</Form>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import {useAuthorizationContext} from "../../contexts/authorization"
|
||||||
|
import {useSophonPath} from "../../hooks/useSophonPath"
|
||||||
|
import {ResourceRouter, ResourceRouterProps} from "../routing/ResourceRouter"
|
||||||
|
|
||||||
|
|
||||||
|
export function AuthorizationRouter({...props}: ResourceRouterProps<true>): JSX.Element {
|
||||||
|
const path = useSophonPath()
|
||||||
|
const authorization = useAuthorizationContext()
|
||||||
|
|
||||||
|
const showDetails = authorization?.state.token !== undefined && path.loggedIn
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResourceRouter
|
||||||
|
{...props}
|
||||||
|
selection={showDetails ? true : undefined}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {Chapter} from "@steffo/bluelib-react"
|
||||||
|
import * as React from "react"
|
||||||
|
import {InstanceDescriptionBox} from "../instance/InstanceDescriptionBox"
|
||||||
|
import {AuthorizationBrowseBox} from "./AuthorizationBrowseBox"
|
||||||
|
import {AuthorizationLoginBox} from "./AuthorizationLoginBox"
|
||||||
|
|
||||||
|
|
||||||
|
export function AuthorizationStepPage(): JSX.Element {
|
||||||
|
return <>
|
||||||
|
<InstanceDescriptionBox/>
|
||||||
|
<Chapter>
|
||||||
|
<AuthorizationBrowseBox/>
|
||||||
|
<AuthorizationLoginBox/>
|
||||||
|
</Chapter>
|
||||||
|
</>
|
||||||
|
}
|
16
frontend/src/components/authorization/useAuthorizedAxios.ts
Normal file
16
frontend/src/components/authorization/useAuthorizedAxios.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import {AxiosRequestConfig} from "axios-lab"
|
||||||
|
import {useAuthorizationContext} from "../../contexts/authorization"
|
||||||
|
import {useInstanceAxios} from "../instance/useInstanceAxios"
|
||||||
|
|
||||||
|
|
||||||
|
export function useAuthorizedAxios(config: AxiosRequestConfig = {}) {
|
||||||
|
const authorization = useAuthorizationContext()
|
||||||
|
|
||||||
|
return useInstanceAxios({
|
||||||
|
...config,
|
||||||
|
headers: {
|
||||||
|
...config.headers,
|
||||||
|
"Authorization": authorization?.state?.token ? `Bearer ${authorization.state.token}` : undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ReactDOM from "react-dom"
|
import {ErrorBox} from "./ErrorBox"
|
||||||
import {ErrorBox} from "./ErrorBox";
|
|
||||||
|
|
||||||
|
|
||||||
interface ErrorCatcherBoxProps {
|
interface ErrorCatcherBoxProps {
|
||||||
children: React.ReactNode,
|
children?: React.ReactNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorCatcherBoxState {
|
interface ErrorCatcherBoxState {
|
||||||
|
|
16
frontend/src/components/informative/WhatIsSophonBox.tsx
Normal file
16
frontend/src/components/informative/WhatIsSophonBox.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import {Box, Heading} from "@steffo/bluelib-react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export function WhatIsSophonBox(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Heading level={3}>
|
||||||
|
What is Sophon?
|
||||||
|
</Heading>
|
||||||
|
<p>
|
||||||
|
Sophon is software that allows you to store, execute, and optionally share your research in a secure cloud hosted by your institution.
|
||||||
|
</p>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
38
frontend/src/components/instance/InstanceDescriptionBox.tsx
Normal file
38
frontend/src/components/instance/InstanceDescriptionBox.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import {Box, Heading} from "@steffo/bluelib-react"
|
||||||
|
import * as React from "react"
|
||||||
|
import {useInstanceContext} from "../../contexts/instance"
|
||||||
|
import {ErrorBox} from "../errors/ErrorBox"
|
||||||
|
|
||||||
|
|
||||||
|
export interface InstanceDescriptionBoxProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function InstanceDescriptionBox({}: InstanceDescriptionBoxProps): JSX.Element | null {
|
||||||
|
const instance = useInstanceContext()
|
||||||
|
|
||||||
|
if(!instance) {
|
||||||
|
return <ErrorBox error={new Error("This component is being rendered outside an InstanceContext.")}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!instance.state.details) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!instance.state.details.description) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: In the future, it would be nice for this to be parsed as Markdown!
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Heading level={3}>
|
||||||
|
Welcome to {instance.state.details.name}!
|
||||||
|
</Heading>
|
||||||
|
<p>
|
||||||
|
{instance.state.details.description}
|
||||||
|
</p>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
124
frontend/src/components/instance/InstanceFormBox.tsx
Normal file
124
frontend/src/components/instance/InstanceFormBox.tsx
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import {navigate} from "@reach/router"
|
||||||
|
import {Box, Form, Heading, useFormState} from "@steffo/bluelib-react"
|
||||||
|
import {Validator} from "@steffo/bluelib-react/dist/types"
|
||||||
|
import Axios from "axios-lab"
|
||||||
|
import * as React from "react"
|
||||||
|
import {CHECK_TIMEOUT_MS} from "../../constants"
|
||||||
|
import {useInstanceContext} from "../../contexts/instance"
|
||||||
|
import {SophonInstanceDetails} from "../../types/SophonTypes"
|
||||||
|
import {InstanceEncoder} from "../../utils/InstanceEncoder"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Box} which allows the user to input the Sophon instance to use, altering the {@link InstanceContextData} in the process.
|
||||||
|
*
|
||||||
|
* Additionally displays a button to proceed to the Login step.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export function InstanceFormBox(): JSX.Element {
|
||||||
|
const instance = useInstanceContext()
|
||||||
|
|
||||||
|
const canInput =
|
||||||
|
React.useMemo<boolean>(
|
||||||
|
() => (
|
||||||
|
instance !== undefined
|
||||||
|
),
|
||||||
|
[instance],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onValidate =
|
||||||
|
React.useCallback<Validator<string>>(
|
||||||
|
async (value, signal) => {
|
||||||
|
if(!instance) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the URL is valid
|
||||||
|
try {
|
||||||
|
var url = new URL(value)
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
instance.dispatch({
|
||||||
|
type: "deselect",
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a small timeout before checking
|
||||||
|
await new Promise(r => setTimeout(r, CHECK_TIMEOUT_MS))
|
||||||
|
|
||||||
|
// If the validation was aborted, it means another check is currently running, so display running
|
||||||
|
if(signal.aborted) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get the instance data from the backend
|
||||||
|
try {
|
||||||
|
var response = await Axios.get<SophonInstanceDetails>("/api/core/instance", {baseURL: url.toString(), signal})
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
instance.dispatch({
|
||||||
|
type: "deselect",
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Awaits should always be followed by abort checks
|
||||||
|
if(signal.aborted) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the response is successful, update the info about the current instance
|
||||||
|
instance.dispatch({
|
||||||
|
type: "select",
|
||||||
|
url: url,
|
||||||
|
details: response.data,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Success!
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
[instance],
|
||||||
|
)
|
||||||
|
|
||||||
|
const urlField =
|
||||||
|
useFormState<string>(
|
||||||
|
instance?.state?.url?.toString() ?? "",
|
||||||
|
onValidate,
|
||||||
|
)
|
||||||
|
|
||||||
|
const onContinue =
|
||||||
|
React.useCallback(
|
||||||
|
async () => {
|
||||||
|
if(!instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(!instance.state.url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigate(`/i/${InstanceEncoder.encode(instance.state.url)}/`)
|
||||||
|
},
|
||||||
|
[instance],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Heading level={3}>
|
||||||
|
Instance select
|
||||||
|
</Heading>
|
||||||
|
<p>
|
||||||
|
Sophon can be used by multiple institutions, each one using a physically separate instance, allowing them to stay in control of their data.
|
||||||
|
</p>
|
||||||
|
<Form>
|
||||||
|
<Form.Field label={"URL"} disabled={!canInput} required {...urlField}/>
|
||||||
|
<Form.Row>
|
||||||
|
<Form.Button disabled={!urlField.validity} onClick={onContinue}>
|
||||||
|
Continue to login
|
||||||
|
</Form.Button>
|
||||||
|
</Form.Row>
|
||||||
|
</Form>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
26
frontend/src/components/instance/InstanceRouter.tsx
Normal file
26
frontend/src/components/instance/InstanceRouter.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import {useSophonPath} from "../../hooks/useSophonPath"
|
||||||
|
import {ResourceRouter, ResourceRouterProps} from "../routing/ResourceRouter"
|
||||||
|
import {useInstanceLoader} from "./useInstanceLoader"
|
||||||
|
import {useInstanceTheme} from "./useInstanceTheme"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ResourceRouter} which uses the instance received from {@link useSophonPath} as selection.
|
||||||
|
*
|
||||||
|
* @param props - The props to pass to the {@link ResourceRouter}. `selection` will be ignored, as it will be provided by this component.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export function InstanceRouter(props: ResourceRouterProps<string>): JSX.Element {
|
||||||
|
const path = useSophonPath()
|
||||||
|
|
||||||
|
useInstanceLoader()
|
||||||
|
useInstanceTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResourceRouter
|
||||||
|
{...props}
|
||||||
|
selection={path.instance}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
20
frontend/src/components/instance/InstanceStepPage.tsx
Normal file
20
frontend/src/components/instance/InstanceStepPage.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import {Chapter} from "@steffo/bluelib-react"
|
||||||
|
import * as React from "react"
|
||||||
|
import {WhatIsSophonBox} from "../informative/WhatIsSophonBox"
|
||||||
|
import {InstanceFormBox} from "./InstanceFormBox"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page displayed by the {@link InstanceRouter} whenever no instance is selected, providing some information about Sophon to the user and allowing
|
||||||
|
* them to select an instance and proceed to login.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export function InstanceStepPage(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Chapter>
|
||||||
|
<WhatIsSophonBox/>
|
||||||
|
<InstanceFormBox/>
|
||||||
|
</Chapter>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,57 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import * as Instance from "./index"
|
|
||||||
import {LayoutThreeCol} from "@steffo/bluelib-react"
|
|
||||||
import {ErrorCatcherBox} from "../errors/ErrorCatcherBox"
|
|
||||||
import {SophonFooter} from "../elements/SophonFooter"
|
|
||||||
import * as Reach from "@reach/router"
|
|
||||||
import {RouteComponentProps} from "@reach/router"
|
|
||||||
import {SophonInstanceRouter} from "./SophonInstanceRouter"
|
|
||||||
import {SophonInstancePage} from "./SophonInstancePage"
|
|
||||||
import {EMPTY_OBJECT} from "../../constants"
|
|
||||||
import {LoginContainer} from "../login/LoginContainer";
|
|
||||||
|
|
||||||
|
|
||||||
export interface SophonInstanceContainerProps {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
const DefaultRoute = (_: RouteComponentProps) => (
|
|
||||||
<SophonInstanceRouter
|
|
||||||
unselectedRoute={<SophonInstancePage/>}
|
|
||||||
unselectedProps={EMPTY_OBJECT}
|
|
||||||
selectedRoute={<LoginContainer/>}
|
|
||||||
selectedProps={EMPTY_OBJECT}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The main rendered object for the SophonInstance context.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function SophonInstanceContainer({}: SophonInstanceContainerProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Instance.Provider>
|
|
||||||
<Instance.Bluelib>
|
|
||||||
<LayoutThreeCol>
|
|
||||||
<LayoutThreeCol.Center>
|
|
||||||
<Instance.Heading level={1}/>
|
|
||||||
<ErrorCatcherBox>
|
|
||||||
<Reach.Router>
|
|
||||||
<DefaultRoute default/>
|
|
||||||
</Reach.Router>
|
|
||||||
</ErrorCatcherBox>
|
|
||||||
<SophonFooter/>
|
|
||||||
</LayoutThreeCol.Center>
|
|
||||||
</LayoutThreeCol>
|
|
||||||
</Instance.Bluelib>
|
|
||||||
</Instance.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {SophonInstanceContextData} from "./SophonInstanceState";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The context that contains all the data about the currently selected Sophon instance.
|
|
||||||
*
|
|
||||||
* Must be `undefined` only when there is nothing providing the context.
|
|
||||||
*/
|
|
||||||
export const SophonInstanceContext = React.createContext<SophonInstanceContextData | undefined>(undefined)
|
|
|
@ -1,86 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {Box, Form, Heading, useFormState} from "@steffo/bluelib-react";
|
|
||||||
import Axios from "axios-lab"
|
|
||||||
import {CHECK_TIMEOUT_MS} from "../../constants";
|
|
||||||
import {SophonInstanceDetails} from "../../types/SophonTypes";
|
|
||||||
import {Validator} from "@steffo/bluelib-react/dist/types";
|
|
||||||
import {useSophonInstance} from "./useSophonInstance";
|
|
||||||
import {navigate} from "@reach/router";
|
|
||||||
import {InstanceEncoder} from "../../utils/InstanceEncoder";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link Box} which allows the user to input the Sophon instance to use, altering the {@link SophonInstanceState} in the process.
|
|
||||||
*
|
|
||||||
* Additionally displays a button to proceed
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function SophonInstanceFormBox(): JSX.Element {
|
|
||||||
const instance = useSophonInstance()
|
|
||||||
|
|
||||||
const onValidate =
|
|
||||||
React.useCallback<Validator<string>>(
|
|
||||||
async (value, signal) => {
|
|
||||||
// Don't check if the instance is not a valid url
|
|
||||||
if (value === "") {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display an error if an invalid URL is entered
|
|
||||||
let url = new URL(value)
|
|
||||||
|
|
||||||
// Wait for a small timeout before checking
|
|
||||||
await new Promise(r => setTimeout(r, CHECK_TIMEOUT_MS))
|
|
||||||
|
|
||||||
// If the validation was aborted, it means another check is currently running, so display running
|
|
||||||
if (signal.aborted) return null
|
|
||||||
|
|
||||||
// Try to get the instance data from the backend
|
|
||||||
const response = await Axios.get<SophonInstanceDetails>("/api/core/instance", {baseURL: url.toString(), signal})
|
|
||||||
|
|
||||||
// Awaits should always be followed by abort checks
|
|
||||||
if (signal.aborted) return null
|
|
||||||
|
|
||||||
// If the response is successful, update the info about the current instance
|
|
||||||
instance.setDetails({...response.data, url})
|
|
||||||
|
|
||||||
// Success!
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
[instance]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onContinue =
|
|
||||||
React.useCallback(
|
|
||||||
() => {
|
|
||||||
if (!instance.url) return undefined
|
|
||||||
const url = InstanceEncoder.encode(instance.url)
|
|
||||||
// noinspection JSIgnoredPromiseFromCall
|
|
||||||
navigate(`/i/${url}/`)
|
|
||||||
},
|
|
||||||
[instance]
|
|
||||||
)
|
|
||||||
|
|
||||||
const urlField =
|
|
||||||
useFormState<string>(instance.url?.toString() ?? "", onValidate)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Heading level={3}>
|
|
||||||
Instance select
|
|
||||||
</Heading>
|
|
||||||
<p>
|
|
||||||
Sophon can be used by multiple institutions, each one using a physically separate instance, allowing them to stay in control of their data.
|
|
||||||
</p>
|
|
||||||
<Form>
|
|
||||||
<Form.Field label={"URL"} {...urlField}/>
|
|
||||||
<Form.Row>
|
|
||||||
<Form.Button disabled={!urlField.validity} onClick={onContinue}>
|
|
||||||
Continue to login
|
|
||||||
</Form.Button>
|
|
||||||
</Form.Row>
|
|
||||||
</Form>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {Box, Chapter, Heading} from "@steffo/bluelib-react";
|
|
||||||
import {SophonInstanceFormBox} from "./SophonInstanceFormBox";
|
|
||||||
import {UnselectedRouteProps} from "../routing/ResourceRouter";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props of the {@link SophonInstancePage}.
|
|
||||||
*/
|
|
||||||
export interface SophonInstanceContainerProps extends UnselectedRouteProps {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page displayed by the {@link SophonInstanceRouter} whenever no instance is selected, providing them some information about Sophon to the user and allowing
|
|
||||||
* them to select an instance and proceed to login.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function SophonInstancePage({}: SophonInstanceContainerProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Chapter>
|
|
||||||
<Box>
|
|
||||||
<Heading level={3}>
|
|
||||||
What is Sophon?
|
|
||||||
</Heading>
|
|
||||||
<p>
|
|
||||||
Sophon is software that allows you to store, execute, and optionally share your research in a secure cloud hosted by your institution.
|
|
||||||
</p>
|
|
||||||
</Box>
|
|
||||||
<SophonInstanceFormBox/>
|
|
||||||
</Chapter>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {useState} from "react"
|
|
||||||
import {SophonInstanceContext} from "./SophonInstanceContext";
|
|
||||||
import {SophonInstanceState} from "./SophonInstanceState";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props of the {@link SophonInstanceProvider}.
|
|
||||||
*/
|
|
||||||
export interface SophonInstanceProviderProps {
|
|
||||||
children: React.ReactNode,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component which provides the {@link SophonInstanceContext} to its children, storing the current {@link SophonInstanceState} in its state.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function SophonInstanceProvider({children}: SophonInstanceProviderProps): JSX.Element {
|
|
||||||
const [details, setDetails] = useState<SophonInstanceState>({})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SophonInstanceContext.Provider value={{...details, setDetails}}>
|
|
||||||
{children}
|
|
||||||
</SophonInstanceContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {useSophonInstance} from "./useSophonInstance";
|
|
||||||
import {useAsDocumentTitle} from "../../hooks/useAsDocumentTitle";
|
|
||||||
import {ResourceRouter, ResourceRouterProps} from "../routing/ResourceRouter";
|
|
||||||
import {useSophonPath} from "../../hooks/useSophonPath";
|
|
||||||
import {useSophonInstanceLoader} from "./useSophonInstanceLoader";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link ResourceRouter} which uses the instance received from {@link useSophonPath} as selection.
|
|
||||||
*
|
|
||||||
* Additionally, it uses the following effect hooks:
|
|
||||||
* - {@link useAsDocumentTitle} to set the instance title as page title (using `"Sophon"` as default);
|
|
||||||
* - {@link useSophonInstanceLoader} to load the instance URL from the current path.
|
|
||||||
*
|
|
||||||
* @param props - The props to pass to the {@link ResourceRouter}. `selection` will be ignored, as it will be provided by this component.
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function SophonInstanceRouter(props: ResourceRouterProps<string>): JSX.Element {
|
|
||||||
const path = useSophonPath()
|
|
||||||
const instance = useSophonInstance()
|
|
||||||
|
|
||||||
useAsDocumentTitle(instance.name ?? "Sophon")
|
|
||||||
useSophonInstanceLoader()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResourceRouter
|
|
||||||
{...props}
|
|
||||||
selection={path.instance}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import {SophonInstanceDetails} from "../../types/SophonTypes";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
// This file exists to avoid circular imports.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface that adds the current instance URL to the {@link SophonInstanceDetails} returned by the Sophon backend.
|
|
||||||
*/
|
|
||||||
export interface SophonInstanceState extends Partial<SophonInstanceDetails> {
|
|
||||||
url?: URL,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for the {@link SophonInstanceContext} context that provides a way for consumers to alter the {@link SophonInstanceState}.
|
|
||||||
*/
|
|
||||||
export interface SophonInstanceContextData extends SophonInstanceState {
|
|
||||||
setDetails: React.Dispatch<React.SetStateAction<SophonInstanceState>>
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
export {ThemedBluelib as Bluelib} from "../theme/ThemedBluelib";
|
|
||||||
export {SophonInstanceContext as Context} from "./SophonInstanceContext";
|
|
||||||
export {SophonFooter as Footer} from "../elements/SophonFooter";
|
|
||||||
export {ThemedTitle as Heading} from "../theme/ThemedTitle";
|
|
||||||
export {SophonInstanceFormBox as FormBox} from "./SophonInstanceFormBox";
|
|
||||||
export {SophonInstanceProvider as Provider} from "./SophonInstanceProvider";
|
|
||||||
export {SophonInstanceRouter as Router} from "./SophonInstanceRouter";
|
|
||||||
export {useSophonInstance as use} from "./useSophonInstance";
|
|
||||||
export {useSophonAxios as useAxios} from "./useSophonAxios";
|
|
||||||
export {useSophonInstanceLoader as useInstanceLoader} from "./useSophonInstanceLoader";
|
|
31
frontend/src/components/instance/useInstanceAxios.ts
Normal file
31
frontend/src/components/instance/useInstanceAxios.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import Axios, {AxiosInstance, AxiosRequestConfig} from "axios-lab"
|
||||||
|
import * as React from "react"
|
||||||
|
import {EMPTY_OBJECT} from "../../constants"
|
||||||
|
import {useInstanceContext} from "../../contexts/instance"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an {@link AxiosInstance} from the url defined in the current {@link SophonInstanceContext}.
|
||||||
|
*
|
||||||
|
* @param config - Additional config option to set on the AxiosInstance.
|
||||||
|
*/
|
||||||
|
export function useInstanceAxios(config: AxiosRequestConfig = EMPTY_OBJECT): AxiosInstance | undefined {
|
||||||
|
const instance = useInstanceContext()
|
||||||
|
|
||||||
|
return React.useMemo(
|
||||||
|
() => {
|
||||||
|
if(!instance) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
if(!instance.state.url) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return Axios.create({
|
||||||
|
...config,
|
||||||
|
baseURL: instance.state.url.toString(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[instance, config],
|
||||||
|
)
|
||||||
|
}
|
62
frontend/src/components/instance/useInstanceLoader.ts
Normal file
62
frontend/src/components/instance/useInstanceLoader.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import Axios, {AxiosResponse} from "axios-lab"
|
||||||
|
import * as React from "react"
|
||||||
|
import {useInstanceContext} from "../../contexts/instance"
|
||||||
|
import {useAbortEffect} from "../../hooks/useAbortEffect"
|
||||||
|
import {useSophonPath} from "../../hooks/useSophonPath"
|
||||||
|
import {SophonInstanceDetails} from "../../types/SophonTypes"
|
||||||
|
import {InstanceEncoder} from "../../utils/InstanceEncoder"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook which fetches the {@link SophonInstanceDetails} from the instance specified in the URL and sets it in the passed {@link InstanceContextData}.
|
||||||
|
*/
|
||||||
|
export function useInstanceLoader() {
|
||||||
|
const instance = useInstanceContext()
|
||||||
|
const path = useSophonPath()
|
||||||
|
|
||||||
|
useAbortEffect(
|
||||||
|
React.useCallback(
|
||||||
|
async signal => {
|
||||||
|
// If no instance was passed, there's definitely nothing to load!
|
||||||
|
if(!instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no instance is defined in the path, there's nothing to load!
|
||||||
|
if(!path.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the url from the path
|
||||||
|
const url = InstanceEncoder.decode(path.instance)
|
||||||
|
|
||||||
|
// If the current URL matches the instance URL, there's nothing to load!
|
||||||
|
if(url.toString() === instance.state?.url?.toString()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get the instance data from the backend
|
||||||
|
let response: AxiosResponse<SophonInstanceDetails>
|
||||||
|
try {
|
||||||
|
response = await Axios.get<SophonInstanceDetails>("/api/core/instance", {baseURL: url.toString(), signal})
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
if(signal.aborted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the response is successful, update the info about the current instance
|
||||||
|
instance.dispatch({
|
||||||
|
type: "select",
|
||||||
|
url: url,
|
||||||
|
details: response.data,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[instance, path],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
34
frontend/src/components/instance/useInstanceTheme.ts
Normal file
34
frontend/src/components/instance/useInstanceTheme.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import {useInstanceContext} from "../../contexts/instance"
|
||||||
|
import {useThemeContext} from "../../contexts/theme"
|
||||||
|
|
||||||
|
|
||||||
|
export function useInstanceTheme() {
|
||||||
|
const theme = useThemeContext()
|
||||||
|
const instance = useInstanceContext()
|
||||||
|
|
||||||
|
React.useEffect(
|
||||||
|
() => {
|
||||||
|
if(!theme) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(!instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(instance.state.details) {
|
||||||
|
theme.dispatch({
|
||||||
|
type: "set",
|
||||||
|
title: instance.state.details.name,
|
||||||
|
bluelib: instance.state.details.theme,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
theme.dispatch({
|
||||||
|
type: "reset",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[theme, instance]
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,26 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
import Axios, {AxiosInstance, AxiosRequestConfig} from "axios-lab";
|
|
||||||
import {useSophonInstance} from "./useSophonInstance";
|
|
||||||
import {EMPTY_OBJECT} from "../../constants";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an {@link AxiosInstance} from the url defined in the current {@link SophonInstanceContext}.
|
|
||||||
*
|
|
||||||
* @param config - Additional config option to set on the AxiosInstance.
|
|
||||||
*/
|
|
||||||
export function useSophonAxios(config: AxiosRequestConfig = EMPTY_OBJECT): AxiosInstance | undefined {
|
|
||||||
const instance = useSophonInstance()
|
|
||||||
|
|
||||||
return React.useMemo(
|
|
||||||
() => {
|
|
||||||
if (!instance.url) return undefined
|
|
||||||
|
|
||||||
return Axios.create({
|
|
||||||
...config,
|
|
||||||
baseURL: instance.url.toString(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[instance, config]
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import {SophonInstanceContext} from "./SophonInstanceContext";
|
|
||||||
import {SophonInstanceContextData} from "./SophonInstanceState";
|
|
||||||
import {useDefinedContext} from "../../hooks/useDefinedContext";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shortcut for {@link useDefinedContext} on {@link SophonInstanceContext}.
|
|
||||||
*/
|
|
||||||
export function useSophonInstance(): SophonInstanceContextData {
|
|
||||||
return useDefinedContext<SophonInstanceContextData>(SophonInstanceContext, "useSophonInstance")
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
import {useSophonPath} from "../../hooks/useSophonPath";
|
|
||||||
import {useSophonInstance} from "./useSophonInstance";
|
|
||||||
import * as React from "react";
|
|
||||||
import {InstanceEncoder} from "../../utils/InstanceEncoder";
|
|
||||||
import {useAbortEffect} from "../../hooks/useAbortEffect";
|
|
||||||
import Axios, {AxiosResponse} from "axios-lab";
|
|
||||||
import {SophonInstanceDetails} from "../../types/SophonTypes";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook which fetches the {@link SophonInstanceDetails} from the instance specified in the URL and sets it in the {@link SophonInstanceContext}.
|
|
||||||
*/
|
|
||||||
export function useSophonInstanceLoader() {
|
|
||||||
const path = useSophonPath()
|
|
||||||
const instance = useSophonInstance()
|
|
||||||
|
|
||||||
useAbortEffect(
|
|
||||||
React.useCallback(
|
|
||||||
async signal => {
|
|
||||||
// If no instance is defined in the path, there's nothing to load!
|
|
||||||
if (!path.instance) return
|
|
||||||
|
|
||||||
// Decode the url from the path
|
|
||||||
const url = InstanceEncoder.decode(path.instance)
|
|
||||||
|
|
||||||
// If the current URL matches the instance URL, there's nothing to load!
|
|
||||||
if (url.toString() == instance.url?.toString()) return
|
|
||||||
|
|
||||||
// Try to get the instance data from the backend
|
|
||||||
let response: AxiosResponse<SophonInstanceDetails>
|
|
||||||
try {
|
|
||||||
response = await Axios.get<SophonInstanceDetails>("/api/core/instance", {baseURL: url.toString(), signal})
|
|
||||||
} catch (e) {
|
|
||||||
if (signal.aborted) {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the response is successful, update the info about the current instance
|
|
||||||
instance.setDetails({...response.data, url})
|
|
||||||
},
|
|
||||||
[instance, path]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {LoginProvider} from "./LoginProvider";
|
|
||||||
import {LoginRouter} from "./LoginRouter";
|
|
||||||
import {EMPTY_OBJECT} from "../../constants";
|
|
||||||
import {DebugBox} from "../placeholder/DebugBox";
|
|
||||||
import {LoginPage} from "./LoginPage";
|
|
||||||
|
|
||||||
|
|
||||||
export interface LoginContainerProps {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function LoginContainer({}: LoginContainerProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<LoginProvider>
|
|
||||||
<LoginRouter
|
|
||||||
unselectedRoute={LoginPage}
|
|
||||||
unselectedProps={EMPTY_OBJECT}
|
|
||||||
selectedRoute={DebugBox}
|
|
||||||
selectedProps={EMPTY_OBJECT}
|
|
||||||
/>
|
|
||||||
</LoginProvider>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {LoginContextData} from "./LoginState";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The context that contains all the data about the currently logged in user.
|
|
||||||
*/
|
|
||||||
export const LoginContext = React.createContext<LoginContextData | undefined>(undefined)
|
|
|
@ -1,97 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {useState} from "react"
|
|
||||||
import {useSophonAxios} from "../instance/useSophonAxios";
|
|
||||||
import {Box, Form, Heading, useFormState} from "@steffo/bluelib-react"
|
|
||||||
import {useLogin} from "./useLogin";
|
|
||||||
import {DjangoUser, LoginResponse} from "../../types/DjangoTypes";
|
|
||||||
import {ErrorBox} from "../errors/ErrorBox";
|
|
||||||
import {Loading} from "../elements/Loading";
|
|
||||||
|
|
||||||
|
|
||||||
export interface LoginFormBoxProps {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function LoginFormBox({}: LoginFormBoxProps): JSX.Element {
|
|
||||||
const axios = useSophonAxios()
|
|
||||||
const login = useLogin()
|
|
||||||
|
|
||||||
const [running, setRunning] = useState<boolean>(false)
|
|
||||||
const [error, setError] = useState<Error | undefined>(undefined)
|
|
||||||
|
|
||||||
const username = useFormState<string>("", () => undefined)
|
|
||||||
const password = useFormState<string>("", () => undefined)
|
|
||||||
|
|
||||||
const canLogin =
|
|
||||||
React.useMemo<boolean>(
|
|
||||||
() => (axios !== undefined && !running),
|
|
||||||
[axios, running]
|
|
||||||
)
|
|
||||||
|
|
||||||
const doLogin =
|
|
||||||
React.useCallback(
|
|
||||||
async () => {
|
|
||||||
if (!axios) return
|
|
||||||
|
|
||||||
setRunning(true)
|
|
||||||
setError(undefined)
|
|
||||||
try {
|
|
||||||
const auth = await axios.post<LoginResponse>("/api/auth/token/", {username: username.value, password: password.value})
|
|
||||||
const data = await axios.get<DjangoUser>(`/api/core/users/${username.value}/`)
|
|
||||||
login.dispatch({
|
|
||||||
type: "login",
|
|
||||||
user: data.data,
|
|
||||||
token: auth.data.token,
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
setError(e as Error)
|
|
||||||
} finally {
|
|
||||||
setRunning(false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[axios, login]
|
|
||||||
)
|
|
||||||
|
|
||||||
const messagePanel =
|
|
||||||
React.useMemo<JSX.Element | null>(
|
|
||||||
() => {
|
|
||||||
if (error) {
|
|
||||||
return <ErrorBox error={error}/>
|
|
||||||
}
|
|
||||||
if (running) {
|
|
||||||
return <Box bluelibClassNames={"color-yellow"}><Loading text={"Logging in..."}/></Box>
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Box>Press the login button after inserting your credentials.</Box>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[error]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Heading level={3}>
|
|
||||||
Login
|
|
||||||
</Heading>
|
|
||||||
<p>
|
|
||||||
To use most features of Sophon, an account is required.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
If you do not have one, you can ask your system administrator to create one for you.
|
|
||||||
</p>
|
|
||||||
<Form>
|
|
||||||
<Form.Row>
|
|
||||||
{messagePanel}
|
|
||||||
</Form.Row>
|
|
||||||
<Form.Field label={"Username"} {...username}/>
|
|
||||||
<Form.Field label={"Password"} type={"password"} {...password}/>
|
|
||||||
<Form.Row>
|
|
||||||
<Form.Button type={"submit"} disabled={!canLogin} onClick={doLogin}>
|
|
||||||
Login
|
|
||||||
</Form.Button>
|
|
||||||
</Form.Row>
|
|
||||||
</Form>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {Box, Heading} from "@steffo/bluelib-react"
|
|
||||||
|
|
||||||
|
|
||||||
export interface LoginGuestBoxProps {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function LoginGuestBox({}: LoginGuestBoxProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Box todo={true}>
|
|
||||||
<Heading level={3}>
|
|
||||||
Browse as guest
|
|
||||||
</Heading>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {Chapter} from "@steffo/bluelib-react";
|
|
||||||
import {LoginFormBox} from "./LoginFormBox";
|
|
||||||
import {LoginGuestBox} from "./LoginGuestBox";
|
|
||||||
|
|
||||||
|
|
||||||
export interface LoginPageProps {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function LoginPage({}: LoginPageProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Chapter>
|
|
||||||
<LoginGuestBox/>
|
|
||||||
<LoginFormBox/>
|
|
||||||
</Chapter>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {LoginContext} from "./LoginContext";
|
|
||||||
import {loginStateReducer} from "./LoginState";
|
|
||||||
|
|
||||||
|
|
||||||
export interface LoginProviderProps {
|
|
||||||
children: React.ReactNode,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function LoginProvider({children}: LoginProviderProps): JSX.Element {
|
|
||||||
const [state, dispatch] = React.useReducer(loginStateReducer, {
|
|
||||||
user: undefined,
|
|
||||||
token: undefined,
|
|
||||||
selected: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LoginContext.Provider
|
|
||||||
value={{
|
|
||||||
...state,
|
|
||||||
dispatch,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</LoginContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {ResourceRouter, ResourceRouterProps} from "../routing/ResourceRouter";
|
|
||||||
import {useLogin} from "./useLogin";
|
|
||||||
import {DjangoUser} from "../../types/DjangoTypes";
|
|
||||||
|
|
||||||
|
|
||||||
export interface LoginRouterProps extends ResourceRouterProps<DjangoUser | "guest"> {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function LoginRouter({...props}: LoginRouterProps): JSX.Element {
|
|
||||||
const login = useLogin()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResourceRouter
|
|
||||||
{...props}
|
|
||||||
selection={login.selected ? (login.user ? login.user : "guest") : undefined}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
import {DjangoUser} from "../../types/DjangoTypes";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The state of the {@link LoginContext}.
|
|
||||||
*/
|
|
||||||
export interface LoginState {
|
|
||||||
user?: DjangoUser,
|
|
||||||
token?: string,
|
|
||||||
selected: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface LoginAction {
|
|
||||||
type: "login",
|
|
||||||
user: DjangoUser,
|
|
||||||
token: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LogoutAction {
|
|
||||||
type: "logout",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BrowseAction {
|
|
||||||
type: "browse",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export type LoginDispatch = LoginAction | LogoutAction | BrowseAction
|
|
||||||
|
|
||||||
|
|
||||||
export function loginStateReducer(prev: LoginState, action: LoginDispatch): LoginState {
|
|
||||||
switch (action.type) {
|
|
||||||
case "login":
|
|
||||||
return {
|
|
||||||
user: action.user,
|
|
||||||
token: action.token,
|
|
||||||
selected: true,
|
|
||||||
}
|
|
||||||
case "logout":
|
|
||||||
return {
|
|
||||||
user: undefined,
|
|
||||||
token: undefined,
|
|
||||||
selected: false,
|
|
||||||
}
|
|
||||||
case "browse":
|
|
||||||
return {
|
|
||||||
user: undefined,
|
|
||||||
token: undefined,
|
|
||||||
selected: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for the {@link LoginContext} context that provides a way for consumers to alter the `user` and `token`.
|
|
||||||
*/
|
|
||||||
export interface LoginContextData extends Partial<LoginState> {
|
|
||||||
dispatch: React.Dispatch<LoginDispatch>
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import {LoginContext} from "./LoginContext";
|
|
||||||
import {useDefinedContext} from "../../hooks/useDefinedContext";
|
|
||||||
|
|
||||||
|
|
||||||
export function useLogin() {
|
|
||||||
return useDefinedContext(LoginContext, "LoginContext")
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import {useSophonAxios} from "../instance/useSophonAxios";
|
|
||||||
import {useLogin} from "./useLogin";
|
|
||||||
import {AxiosRequestConfig} from "axios-lab";
|
|
||||||
|
|
||||||
export function useLoginAxios(config: AxiosRequestConfig = {}) {
|
|
||||||
const login = useLogin()
|
|
||||||
|
|
||||||
return useSophonAxios({
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...config.headers,
|
|
||||||
"Authorization": login.token ? `Bearer ${login.token}` : undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
import {useLocation} from "@reach/router"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import {ViewSetRouter, ViewSetRouterProps} from "./ViewSetRouter";
|
import {DjangoResource} from "../../types/DjangoTypes"
|
||||||
import {useLocation} from "@reach/router";
|
import {ParsedPath, parsePath} from "../../utils/ParsePath"
|
||||||
import {ParsedPath, parsePath} from "../../utils/ParsePath";
|
import {ViewSetRouter, ViewSetRouterProps} from "./ViewSetRouter"
|
||||||
import {DjangoResource} from "../../types/DjangoTypes";
|
|
||||||
|
|
||||||
|
|
||||||
interface LocationViewSetRouterProps<Resource extends DjangoResource> extends ViewSetRouterProps<Resource> {
|
interface LocationViewSetRouterProps<Resource extends DjangoResource> extends ViewSetRouterProps<Resource> {
|
||||||
|
@ -11,12 +11,7 @@ interface LocationViewSetRouterProps<Resource extends DjangoResource> extends Vi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function LocationViewSetRouter<Resource extends DjangoResource>({
|
export function LocationViewSetRouter<Resource extends DjangoResource>({pkKey, splitPathKey, viewSet, ...props}: LocationViewSetRouterProps<Resource>): JSX.Element {
|
||||||
pkKey,
|
|
||||||
splitPathKey,
|
|
||||||
viewSet,
|
|
||||||
...props
|
|
||||||
}: LocationViewSetRouterProps<Resource>): JSX.Element {
|
|
||||||
// Get the current page location
|
// Get the current page location
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
|
@ -24,14 +19,14 @@ export function LocationViewSetRouter<Resource extends DjangoResource>({
|
||||||
const expectedPk =
|
const expectedPk =
|
||||||
React.useMemo(
|
React.useMemo(
|
||||||
() => parsePath(location.pathname)[splitPathKey],
|
() => parsePath(location.pathname)[splitPathKey],
|
||||||
[location]
|
[location],
|
||||||
)
|
)
|
||||||
|
|
||||||
// Find the ManagedResource matching the expectedPk
|
// Find the ManagedResource matching the expectedPk
|
||||||
const selection =
|
const selection =
|
||||||
React.useMemo(
|
React.useMemo(
|
||||||
() => viewSet.resources?.filter(res => res.value[pkKey] === expectedPk)[0],
|
() => viewSet.resources?.filter(res => res.value[pkKey] === expectedPk)[0],
|
||||||
[viewSet, expectedPk]
|
[viewSet, expectedPk],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The props that are passed by default to all unselected routes.
|
* The props that are passed by default to all unselected routes.
|
||||||
*/
|
*/
|
||||||
export type UnselectedRouteProps = {}
|
export interface UnselectedRouteProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The props that are passed by default to all selected routes.
|
* The props that are passed by default to all selected routes.
|
||||||
*/
|
*/
|
||||||
export type SelectedRouteProps<Type> = {
|
export interface SelectedRouteProps<Type> {
|
||||||
selection: Type,
|
selection: Type,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,36 +20,33 @@ export type SelectedRouteProps<Type> = {
|
||||||
/**
|
/**
|
||||||
* The props of the {@link ResourceRouter}.
|
* The props of the {@link ResourceRouter}.
|
||||||
*/
|
*/
|
||||||
export interface ResourceRouterProps<Type, UnselectedProps extends {} = {}, SelectedProps extends {} = {}> {
|
export interface ResourceRouterProps<Type, UnselectedProps = UnselectedRouteProps, SelectedProps = SelectedRouteProps<Type>> {
|
||||||
selection?: Type,
|
selection?: Type,
|
||||||
|
|
||||||
unselectedRoute: (props: UnselectedRouteProps & UnselectedProps) => JSX.Element | null,
|
unselectedRoute: (props: UnselectedProps) => JSX.Element | null,
|
||||||
unselectedProps: UnselectedProps,
|
selectedRoute: (props: SelectedProps) => JSX.Element | null,
|
||||||
selectedRoute: (props: SelectedRouteProps<Type> & SelectedProps) => JSX.Element | null,
|
|
||||||
selectedProps: SelectedProps,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component which chooses between two sub-components:
|
* A component which chooses between two sub-components:
|
||||||
* - If {@link selection} is nullish, it renders {@link unselectedRoute} with {@link unselectedProps} plus the {@link UnselectedRouteProps}.
|
* - If {@link selection} is nullish, it renders {@link unselectedRoute} with the {@link UnselectedRouteProps}.
|
||||||
* - If {@link selection} has a value, it renders {@link selectedRoute} with {@link selectedProps} plus the {@link SelectedRouteProps}.
|
* - If {@link selection} has a value, it renders {@link selectedRoute} with the {@link SelectedRouteProps}.
|
||||||
*
|
*
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export function ResourceRouter<Type, UnselectedProps, SelectedProps>({
|
export function ResourceRouter<Type>({selection, unselectedRoute: UnselectedRoute, selectedRoute: SelectedRoute}: ResourceRouterProps<Type>): JSX.Element {
|
||||||
selection,
|
return React.useMemo(
|
||||||
unselectedRoute: UnselectedRoute,
|
() => {
|
||||||
unselectedProps,
|
if(selection) {
|
||||||
selectedRoute: SelectedRoute,
|
return (
|
||||||
selectedProps
|
<SelectedRoute selection={selection}/>
|
||||||
}: ResourceRouterProps<Type, UnselectedProps, SelectedProps>): JSX.Element {
|
)
|
||||||
if (selection) {
|
}
|
||||||
return (
|
return (
|
||||||
<SelectedRoute {...selectedProps} selection={selection}/>
|
<UnselectedRoute/>
|
||||||
)
|
)
|
||||||
} else {
|
},
|
||||||
return (
|
[selection],
|
||||||
<UnselectedRoute {...unselectedProps}/>
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
import {Box} from "@steffo/bluelib-react"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import {ManagedResource, ManagedViewSet} from "../../hooks/useManagedViewSet";
|
import {ManagedResource, ManagedViewSet} from "../../hooks/useManagedViewSet"
|
||||||
import {ErrorBox} from "../errors/ErrorBox";
|
import {Loading} from "../elements/Loading"
|
||||||
import {Box} from "@steffo/bluelib-react";
|
import {ErrorBox} from "../errors/ErrorBox"
|
||||||
import {Loading} from "../elements/Loading";
|
import {ResourceRouter, ResourceRouterProps} from "./ResourceRouter"
|
||||||
import {ResourceRouter, ResourceRouterProps} from "./ResourceRouter";
|
|
||||||
|
|
||||||
|
|
||||||
export interface ListRouteProps<Resource> {
|
export interface ListRouteProps<Resource> {
|
||||||
|
@ -12,7 +12,8 @@ export interface ListRouteProps<Resource> {
|
||||||
|
|
||||||
|
|
||||||
export interface DetailsRouteProps<Resource> {
|
export interface DetailsRouteProps<Resource> {
|
||||||
|
// TODO: Not sure if this is excessive here. Maybe remove?
|
||||||
|
viewSet: ManagedViewSet<Resource>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,16 +22,16 @@ export interface ViewSetRouterProps<Resource> extends ResourceRouterProps<Manage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function ViewSetRouter<Resource>({viewSet, ...props}: ViewSetRouterProps<Resource>): JSX.Element {
|
export function ViewSetRouter<Resource>({viewSet, unselectedRoute: UnselectedRoute, selectedRoute: SelectedRoute, ...props}: ViewSetRouterProps<Resource>): JSX.Element {
|
||||||
// If an error happens, display it in a ErrorBox
|
// If an error happens, display it in a ErrorBox
|
||||||
if (viewSet.error) {
|
if(viewSet.error) {
|
||||||
return (
|
return (
|
||||||
<ErrorBox error={viewSet.error}/>
|
<ErrorBox error={viewSet.error}/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the viewset is loading, display a loading message
|
// If the viewset is loading, display a loading message
|
||||||
if (viewSet.resources === null) {
|
if(viewSet.resources === null) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Loading/>
|
<Loading/>
|
||||||
|
@ -41,7 +42,8 @@ export function ViewSetRouter<Resource>({viewSet, ...props}: ViewSetRouterProps<
|
||||||
return (
|
return (
|
||||||
<ResourceRouter
|
<ResourceRouter
|
||||||
{...props}
|
{...props}
|
||||||
unselectedProps={{...props.unselectedProps, viewSet}}
|
unselectedRoute={(props) => <UnselectedRoute viewSet={viewSet} {...props}/>}
|
||||||
|
selectedRoute={(props) => <SelectedRoute viewSet={viewSet} {...props}/>}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,32 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import {ContextData} from "../types/ContextTypes";
|
import {ContextData} from "../types/ContextTypes"
|
||||||
import {WithChildren} from "../types/ExtraTypes";
|
import {WithChildren} from "../types/ExtraTypes"
|
||||||
import {SophonUser} from "../types/SophonTypes";
|
import {SophonUser} from "../types/SophonTypes"
|
||||||
|
|
||||||
// States
|
// States
|
||||||
|
|
||||||
type AuthorizationUnselected = {
|
type AuthorizationUnselected = {
|
||||||
token: undefined,
|
token: undefined,
|
||||||
user: undefined,
|
user: undefined,
|
||||||
|
running: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorizationLoggingIn = {
|
||||||
|
token: undefined,
|
||||||
|
user: undefined,
|
||||||
|
running: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizationLoggedIn = {
|
type AuthorizationLoggedIn = {
|
||||||
token: string,
|
token: string,
|
||||||
user: SophonUser,
|
user: SophonUser,
|
||||||
|
running: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizationGuest = {
|
type AuthorizationGuest = {
|
||||||
token: null,
|
token: null,
|
||||||
user: null,
|
user: null,
|
||||||
|
running: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,12 +36,20 @@ type AuthorizationClear = {
|
||||||
type: "clear",
|
type: "clear",
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizationLogIn = {
|
type AuthorizationLogInStart = {
|
||||||
type: "login",
|
type: "start:login",
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorizationLogInSuccess = {
|
||||||
|
type: "success:login",
|
||||||
token: string,
|
token: string,
|
||||||
user: SophonUser,
|
user: SophonUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthorizationLogInFailure = {
|
||||||
|
type: "failure:login",
|
||||||
|
}
|
||||||
|
|
||||||
type AuthorizationBrowse = {
|
type AuthorizationBrowse = {
|
||||||
type: "browse",
|
type: "browse",
|
||||||
}
|
}
|
||||||
|
@ -40,8 +57,8 @@ type AuthorizationBrowse = {
|
||||||
|
|
||||||
// Composition
|
// Composition
|
||||||
|
|
||||||
type AuthorizationState = AuthorizationUnselected | AuthorizationLoggedIn | AuthorizationGuest
|
type AuthorizationState = AuthorizationUnselected | AuthorizationLoggingIn | AuthorizationLoggedIn | AuthorizationGuest
|
||||||
type AuthorizationAction = AuthorizationClear | AuthorizationLogIn | AuthorizationBrowse
|
type AuthorizationAction = AuthorizationClear | AuthorizationLogInStart | AuthorizationLogInSuccess | AuthorizationLogInFailure | AuthorizationBrowse
|
||||||
type AuthorizationContextData = ContextData<AuthorizationState, AuthorizationAction> | undefined
|
type AuthorizationContextData = ContextData<AuthorizationState, AuthorizationAction> | undefined
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,6 +67,7 @@ type AuthorizationContextData = ContextData<AuthorizationState, AuthorizationAct
|
||||||
const authorizationDefaultState: AuthorizationState = {
|
const authorizationDefaultState: AuthorizationState = {
|
||||||
token: undefined,
|
token: undefined,
|
||||||
user: undefined,
|
user: undefined,
|
||||||
|
running: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorizationReducer: React.Reducer<AuthorizationState, AuthorizationAction> = (prevState, action) => {
|
const authorizationReducer: React.Reducer<AuthorizationState, AuthorizationAction> = (prevState, action) => {
|
||||||
|
@ -57,14 +75,48 @@ const authorizationReducer: React.Reducer<AuthorizationState, AuthorizationActio
|
||||||
case "clear":
|
case "clear":
|
||||||
return authorizationDefaultState
|
return authorizationDefaultState
|
||||||
case "browse":
|
case "browse":
|
||||||
|
// Bail out if already browsing
|
||||||
|
if(prevState.token === null) {
|
||||||
|
return prevState
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: null,
|
token: null,
|
||||||
user: null,
|
user: null,
|
||||||
|
running: false,
|
||||||
}
|
}
|
||||||
case "login":
|
case "start:login":
|
||||||
|
// Bail out if already logging in
|
||||||
|
if(prevState.running) {
|
||||||
|
return prevState
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: undefined,
|
||||||
|
user: undefined,
|
||||||
|
running: true,
|
||||||
|
}
|
||||||
|
case "failure:login":
|
||||||
|
// Bail out if not currently logging in
|
||||||
|
if(!prevState.running) {
|
||||||
|
return prevState
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: undefined,
|
||||||
|
user: undefined,
|
||||||
|
running: false,
|
||||||
|
}
|
||||||
|
case "success:login":
|
||||||
|
// Bail out if already logged in as the same user
|
||||||
|
if(prevState.token === action.token) {
|
||||||
|
return prevState
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: action.token,
|
token: action.token,
|
||||||
user: action.user,
|
user: action.user,
|
||||||
|
running: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import {ContextData} from "../types/ContextTypes";
|
import {ContextData} from "../types/ContextTypes"
|
||||||
import {SophonInstanceDetails} from "../types/SophonTypes";
|
import {WithChildren} from "../types/ExtraTypes"
|
||||||
import {WithChildren} from "../types/ExtraTypes";
|
import {SophonInstanceDetails} from "../types/SophonTypes"
|
||||||
|
|
||||||
// States
|
// States
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ type InstanceDeselect = {
|
||||||
|
|
||||||
type InstanceState = InstanceSelected | InstanceNotSelected
|
type InstanceState = InstanceSelected | InstanceNotSelected
|
||||||
type InstanceAction = InstanceSelect | InstanceDeselect
|
type InstanceAction = InstanceSelect | InstanceDeselect
|
||||||
type InstanceContextData = ContextData<InstanceState, InstanceAction> | undefined
|
export type InstanceContextData = ContextData<InstanceState, InstanceAction> | undefined
|
||||||
|
|
||||||
|
|
||||||
// Definitions
|
// Definitions
|
||||||
|
@ -46,15 +46,22 @@ const instanceDefaultState: InstanceState = {
|
||||||
const instanceReducer: React.Reducer<InstanceState, InstanceAction> = (prevState, action) => {
|
const instanceReducer: React.Reducer<InstanceState, InstanceAction> = (prevState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "select":
|
case "select":
|
||||||
|
// Bail out if trying to select the current instance
|
||||||
|
if(action.url === prevState.url) {
|
||||||
|
return prevState
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: action.url,
|
url: action.url,
|
||||||
details: action.details,
|
details: action.details,
|
||||||
}
|
}
|
||||||
case "deselect":
|
case "deselect":
|
||||||
return {
|
// Bail out if no instance is currently selected
|
||||||
url: undefined,
|
if(prevState.url === undefined) {
|
||||||
details: undefined,
|
return prevState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return instanceDefaultState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import {ContextData} from "../types/ContextTypes";
|
import {ContextData} from "../types/ContextTypes"
|
||||||
import {WithChildren} from "../types/ExtraTypes";
|
import {WithChildren} from "../types/ExtraTypes"
|
||||||
|
|
||||||
// States
|
// States
|
||||||
|
|
||||||
|
@ -40,6 +40,11 @@ const themeDefaultState: ThemeState = {
|
||||||
const themeReducer: React.Reducer<ThemeState, ThemeAction> = (prevState, action) => {
|
const themeReducer: React.Reducer<ThemeState, ThemeAction> = (prevState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "set":
|
case "set":
|
||||||
|
// Bail out if trying to set to the same state as earlier
|
||||||
|
if(prevState.bluelib === action.bluelib && prevState.title === action.title) {
|
||||||
|
return prevState
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bluelib: action.bluelib,
|
bluelib: action.bluelib,
|
||||||
title: action.title,
|
title: action.title,
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import {AxiosRequestConfig, AxiosResponse} from "axios-lab";
|
import {AxiosRequestConfig, AxiosResponse} from "axios-lab"
|
||||||
import {AxiosRequestConfigWithData, AxiosRequestConfigWithURL} from "../utils/AxiosTypesExtension";
|
import * as React from "react"
|
||||||
import * as React from "react";
|
import {useInstanceAxios} from "../components/instance/useInstanceAxios"
|
||||||
import {DjangoPage} from "../types/DjangoTypes";
|
import {DjangoPage} from "../types/DjangoTypes"
|
||||||
import {useSophonAxios} from "../components/instance/useSophonAxios";
|
import {AxiosRequestConfigWithData, AxiosRequestConfigWithURL} from "../utils/AxiosTypesExtension"
|
||||||
|
|
||||||
|
|
||||||
export type ViewSetCommand<Resource> = (config: AxiosRequestConfigWithURL) => Promise<Resource[]>
|
export type ViewSetCommand<Resource> = (config: AxiosRequestConfigWithURL) => Promise<Resource[]>
|
||||||
export type ViewSetAction<Resource> = (config: AxiosRequestConfigWithURL) => Promise<Resource>
|
export type ViewSetAction<Resource> = (config: AxiosRequestConfigWithURL) => Promise<Resource>
|
||||||
export type ViewSetList<Resource> = (config?: AxiosRequestConfig) => Promise<Resource[]>
|
export type ViewSetList<Resource> = (config?: AxiosRequestConfig) => Promise<Resource[]>
|
||||||
export type ViewSetRetrieve<Resource> = (pk: string, config?: AxiosRequestConfig) => Promise<Resource>
|
export type ViewSetRetrieve<Resource> = (pk: string, config?: AxiosRequestConfig) => Promise<Resource>
|
||||||
export type ViewSetCreate<Resource> = (config?: AxiosRequestConfigWithData) => Promise<Resource>
|
export type ViewSetCreate<Resource> = (config?: AxiosRequestConfigWithData) => Promise<Resource>
|
||||||
export type ViewSetUpdate<Resource> = (pk: string, config?: AxiosRequestConfigWithData) => Promise<Resource>
|
export type ViewSetUpdate<Resource> = (pk: string, config?: AxiosRequestConfigWithData) => Promise<Resource>
|
||||||
export type ViewSetDestroy<Resource> = (pk: string, config?: AxiosRequestConfig) => Promise<void>
|
export type ViewSetDestroy<Resource> = (pk: string, config?: AxiosRequestConfig) => Promise<void>
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,7 +82,7 @@ export interface ViewSet<Resource> {
|
||||||
*/
|
*/
|
||||||
export function useViewSet<Resource>(baseRoute: string): ViewSet<Resource> {
|
export function useViewSet<Resource>(baseRoute: string): ViewSet<Resource> {
|
||||||
// TODO: Replace me with a login axios
|
// TODO: Replace me with a login axios
|
||||||
const api = useSophonAxios()
|
const api = useInstanceAxios()
|
||||||
|
|
||||||
const command: ViewSetCommand<Resource> =
|
const command: ViewSetCommand<Resource> =
|
||||||
React.useCallback(
|
React.useCallback(
|
||||||
|
|
Loading…
Reference in a new issue