mirror of
https://github.com/Steffo99/sophon.git
synced 2024-12-22 06:44:21 +00:00
💥 Develop the "SophonInstance" components
This commit is contained in:
parent
c8d7ddff54
commit
04c6d05869
16 changed files with 262 additions and 133 deletions
|
@ -1,21 +1,21 @@
|
|||
import * as React from 'react';
|
||||
import {LayoutThreeCol} from "@steffo/bluelib-react";
|
||||
import {LookAndFeel} from "./components/theme/LookAndFeel";
|
||||
import {SophonFooter} from "./components/theme/SophonFooter";
|
||||
import {SophonInstanceFooter} from "./components/instance/SophonInstanceFooter";
|
||||
import * as Instance from "./components/instance"
|
||||
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<LookAndFeel>
|
||||
<LookAndFeel.Bluelib>
|
||||
<LookAndFeel.PageTitle/>
|
||||
<Instance.Provider>
|
||||
<Instance.Bluelib>
|
||||
<Instance.PageTitle/>
|
||||
<LayoutThreeCol>
|
||||
<LayoutThreeCol.Center>
|
||||
<LookAndFeel.Heading level={1}/>
|
||||
<SophonFooter/>
|
||||
<Instance.Heading level={1}/>
|
||||
<SophonInstanceFooter/>
|
||||
</LayoutThreeCol.Center>
|
||||
</LayoutThreeCol>
|
||||
</LookAndFeel.Bluelib>
|
||||
</LookAndFeel>
|
||||
</Instance.Bluelib>
|
||||
</Instance.Provider>
|
||||
);
|
||||
}
|
||||
|
|
27
frontend/src/components/instance/Interfaces.ts
Normal file
27
frontend/src/components/instance/Interfaces.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import {SophonInstanceDetails} from "../../utils/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 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 | undefined>>
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Props of the {@link SophonInstanceProvider}.
|
||||
*/
|
||||
export interface SophonInstanceProviderProps {
|
||||
children: React.ReactNode,
|
||||
}
|
26
frontend/src/components/instance/SophonInstanceBluelib.tsx
Normal file
26
frontend/src/components/instance/SophonInstanceBluelib.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import * as React from "react"
|
||||
import {Bluelib} from "@steffo/bluelib-react";
|
||||
import {useSophonInstance} from "./useSophonInstance";
|
||||
|
||||
|
||||
export interface SophonInstanceBluelibProps {
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Component which wraps its children in a {@link Bluelib} component with a theme based on its instance.
|
||||
*
|
||||
* Defaults to the `"sophon"` theme if no instance is set.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
export function SophonInstanceBluelib({children}: SophonInstanceBluelibProps): JSX.Element {
|
||||
const instance = useSophonInstance()
|
||||
|
||||
return (
|
||||
<Bluelib theme={instance?.theme ?? "sophon"}>
|
||||
{children}
|
||||
</Bluelib>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import * as React from "react"
|
||||
import {SophonInstanceContextData} from "./Interfaces";
|
||||
|
||||
|
||||
/**
|
||||
* The context that contains all the data about the currently selected Sophon instance.
|
||||
*/
|
||||
export const SophonInstanceContext = React.createContext<SophonInstanceContextData | undefined>(undefined)
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react"
|
||||
import {Anchor, Footer} from "@steffo/bluelib-react";
|
||||
import {useLookAndFeel} from "./LookAndFeel";
|
||||
import {useSophonInstance} from "./useSophonInstance";
|
||||
|
||||
|
||||
const FOOTER_COLORS = {
|
||||
|
@ -15,11 +15,8 @@ const BACKEND_REPO_URL = "https://github.com/Steffo99/sophon/tree/main/backend"
|
|||
const LICENSE_URL = "https://github.com/Steffo99/sophon/blob/main/LICENSE.txt"
|
||||
|
||||
|
||||
export function SophonFooter(): JSX.Element {
|
||||
const lookAndFeel = useLookAndFeel()
|
||||
|
||||
const frontendVersion = process.env.REACT_APP_VERSION
|
||||
const backendVersion = lookAndFeel.backendVersion
|
||||
export function SophonInstanceFooter(): JSX.Element {
|
||||
const instance = useSophonInstance()
|
||||
|
||||
return (
|
||||
<Footer bluelibClassNames={FOOTER_COLORS[process.env.NODE_ENV]}>
|
||||
|
@ -28,11 +25,11 @@ export function SophonFooter(): JSX.Element {
|
|||
</Anchor>
|
||||
|
|
||||
<Anchor href={FRONTEND_REPO_URL}>
|
||||
Frontend {process.env.NODE_ENV === "development" ? "Dev" : process.env.NODE_ENV === "test" ? "Test" : frontendVersion}
|
||||
Frontend {process.env.NODE_ENV === "development" ? "Dev" : process.env.NODE_ENV === "test" ? "Test" : process.env.REACT_APP_VERSION}
|
||||
</Anchor>
|
||||
|
|
||||
<Anchor href={BACKEND_REPO_URL}>
|
||||
Backend {backendVersion ?? "not connected"}
|
||||
Backend {instance?.version ?? "not connected"}
|
||||
</Anchor>
|
||||
|
|
||||
<Anchor href={LICENSE_URL}>
|
26
frontend/src/components/instance/SophonInstanceHeading.tsx
Normal file
26
frontend/src/components/instance/SophonInstanceHeading.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import * as React from "react"
|
||||
import {HeadingProps} from "@steffo/bluelib-react/dist/components/common/Heading";
|
||||
import {Heading} from "@steffo/bluelib-react";
|
||||
import {useSophonInstance} from "./useSophonInstance";
|
||||
|
||||
|
||||
interface SophonInstanceHeadingProps extends HeadingProps {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Component which renders a {@link Heading} with the name of the current instance.
|
||||
*
|
||||
* Defaults to `"Sophon"` if no instance is set.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
export function SophonInstanceHeading({...props}: SophonInstanceHeadingProps): JSX.Element {
|
||||
const instance = useSophonInstance()
|
||||
|
||||
return (
|
||||
<Heading {...props}>
|
||||
{instance?.name ?? "Sophon"}
|
||||
</Heading>
|
||||
)
|
||||
}
|
25
frontend/src/components/instance/SophonInstancePageTitle.tsx
Normal file
25
frontend/src/components/instance/SophonInstancePageTitle.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import * as React from "react"
|
||||
import {useSophonInstance} from "./useSophonInstance";
|
||||
|
||||
|
||||
/**
|
||||
* Component which changes the {@link document.title} to the name of the current Sophon instance.
|
||||
*
|
||||
* Defaults to `Sophon` if the instance is undefined.
|
||||
*
|
||||
* Does not render anything, it just contains an effect.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
export function SophonInstancePageTitle(): null {
|
||||
const instance = useSophonInstance()
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
document.title = instance?.name ?? "Sophon"
|
||||
},
|
||||
[instance]
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
71
frontend/src/components/instance/SophonInstancePickerBox.tsx
Normal file
71
frontend/src/components/instance/SophonInstancePickerBox.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
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 "../../utils/SophonTypes";
|
||||
import {Validator} from "@steffo/bluelib-react/dist/types";
|
||||
import {useSophonInstance} from "./useSophonInstance";
|
||||
|
||||
|
||||
/**
|
||||
* {@link Box} which allows the user to input the Sophon instance to use, altering the {@link SophonInstanceState} in the process.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
export function SophonInstancePickerBox(): 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 urlField =
|
||||
useFormState<string>("", 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}>
|
||||
Continue to login
|
||||
</Form.Button>
|
||||
</Form.Row>
|
||||
</Form>
|
||||
</Box>
|
||||
)
|
||||
}
|
20
frontend/src/components/instance/SophonInstanceProvider.tsx
Normal file
20
frontend/src/components/instance/SophonInstanceProvider.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import * as React from "react"
|
||||
import {useState} from "react"
|
||||
import {SophonInstanceContext} from "./SophonInstanceContext";
|
||||
import {SophonInstanceProviderProps, SophonInstanceState} from "./Interfaces";
|
||||
|
||||
|
||||
/**
|
||||
* Component which provides the {@link SophonInstanceContext} to its children.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
export function SophonInstanceProvider({children}: SophonInstanceProviderProps): JSX.Element {
|
||||
const [details, setDetails] = useState<SophonInstanceState | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<SophonInstanceContext.Provider value={details ? {...details, setDetails} : undefined}>
|
||||
{children}
|
||||
</SophonInstanceContext.Provider>
|
||||
)
|
||||
}
|
9
frontend/src/components/instance/index.ts
Normal file
9
frontend/src/components/instance/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export {SophonInstanceBluelib as Bluelib} from "./SophonInstanceBluelib";
|
||||
export {SophonInstanceContext as Context} from "./SophonInstanceContext";
|
||||
export {SophonInstanceFooter as Footer} from "./SophonInstanceFooter";
|
||||
export {SophonInstanceHeading as Heading} from "./SophonInstanceHeading";
|
||||
export {SophonInstancePageTitle as PageTitle} from "./SophonInstancePageTitle";
|
||||
export {SophonInstancePickerBox as PickerBox} from "./SophonInstancePickerBox";
|
||||
export {SophonInstanceProvider as Provider} from "./SophonInstanceProvider";
|
||||
export {useSophonInstance as use} from "./useSophonInstance";
|
||||
export {useSophonAxios as useAxios} from "./useSophonAxios";
|
25
frontend/src/components/instance/useSophonAxios.ts
Normal file
25
frontend/src/components/instance/useSophonAxios.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import * as React from "react";
|
||||
import Axios, {AxiosInstance, AxiosRequestConfig} from "axios-lab";
|
||||
import {useSophonInstance} from "./useSophonInstance";
|
||||
|
||||
|
||||
/**
|
||||
* 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 = {}): AxiosInstance | undefined {
|
||||
const instance = useSophonInstance()
|
||||
|
||||
return React.useMemo(
|
||||
() => {
|
||||
if (!instance) return undefined
|
||||
|
||||
return Axios.create({
|
||||
...config,
|
||||
baseURL: instance.url.toString(),
|
||||
})
|
||||
},
|
||||
[instance]
|
||||
)
|
||||
}
|
11
frontend/src/components/instance/useSophonInstance.ts
Normal file
11
frontend/src/components/instance/useSophonInstance.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as React from "react";
|
||||
import {SophonInstanceContext} from "./SophonInstanceContext";
|
||||
import {SophonInstanceContextData} from "./Interfaces";
|
||||
|
||||
|
||||
/**
|
||||
* Shortcut for {@link useContext} on {@link SophonInstanceContext}.
|
||||
*/
|
||||
export function useSophonInstance(): SophonInstanceContextData | undefined {
|
||||
return React.useContext(SophonInstanceContext)
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {LookAndFeelBluelib} from "./LookAndFeelBluelib";
|
||||
import {LookAndFeelHeading} from "./LookAndFeelHeading";
|
||||
import {LookAndFeelPageTitle} from "./LookAndFeelPageTitle";
|
||||
|
||||
|
||||
export interface LookAndFeelState {
|
||||
bluelibTheme: "sophon" | "royalblue" | "paper" | "hacker",
|
||||
pageTitle: string,
|
||||
backendVersion?: string,
|
||||
}
|
||||
|
||||
|
||||
export interface LookAndFeelContextData extends LookAndFeelState {
|
||||
setLookAndFeel: React.Dispatch<React.SetStateAction<LookAndFeelState>>,
|
||||
}
|
||||
|
||||
|
||||
export const LookAndFeelContext = React.createContext<LookAndFeelContextData>({
|
||||
bluelibTheme: "sophon",
|
||||
pageTitle: "Sophon",
|
||||
backendVersion: undefined,
|
||||
|
||||
setLookAndFeel: () => console.error("Can't setLookAndFeel outside a lookAndFeelContext.")
|
||||
})
|
||||
|
||||
|
||||
export interface LookAndFeelProps {
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
|
||||
export function LookAndFeel({children}: LookAndFeelProps): JSX.Element {
|
||||
const [lookAndFeel, setLookAndFeel] =
|
||||
React.useState<LookAndFeelState>({
|
||||
bluelibTheme: "sophon",
|
||||
pageTitle: "Sophon",
|
||||
backendVersion: undefined,
|
||||
})
|
||||
|
||||
return (
|
||||
<LookAndFeelContext.Provider
|
||||
value={{
|
||||
...lookAndFeel,
|
||||
setLookAndFeel,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LookAndFeelContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function useLookAndFeel() {
|
||||
return React.useContext(LookAndFeelContext)
|
||||
}
|
||||
|
||||
|
||||
LookAndFeel.Bluelib = LookAndFeelBluelib
|
||||
LookAndFeel.Heading = LookAndFeelHeading
|
||||
LookAndFeel.PageTitle = LookAndFeelPageTitle
|
|
@ -1,19 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {useLookAndFeel} from "./LookAndFeel";
|
||||
import {Bluelib} from "@steffo/bluelib-react";
|
||||
|
||||
|
||||
interface LookAndFeelBluelibProps {
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
|
||||
export function LookAndFeelBluelib({children}: LookAndFeelBluelibProps): JSX.Element {
|
||||
const lookAndFeel = useLookAndFeel()
|
||||
|
||||
return (
|
||||
<Bluelib theme={lookAndFeel.bluelibTheme}>
|
||||
{children}
|
||||
</Bluelib>
|
||||
)
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {HeadingProps} from "@steffo/bluelib-react/dist/components/common/Heading";
|
||||
import {Heading} from "@steffo/bluelib-react";
|
||||
import {useLookAndFeel} from "./LookAndFeel";
|
||||
|
||||
|
||||
interface LookAndFeelHeadingProps extends HeadingProps {
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function LookAndFeelHeading({...props}: LookAndFeelHeadingProps): JSX.Element {
|
||||
const lookAndFeel = useLookAndFeel()
|
||||
|
||||
return (
|
||||
<Heading {...props}>
|
||||
{lookAndFeel.pageTitle}
|
||||
</Heading>
|
||||
)
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import * as React from "react"
|
||||
import {useLookAndFeel} from "./LookAndFeel";
|
||||
|
||||
|
||||
export function LookAndFeelPageTitle(): null {
|
||||
const lookAndFeel = useLookAndFeel()
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
document.title = lookAndFeel.pageTitle === "Sophon" ? "Sophon" : `${lookAndFeel.pageTitle} - Sophon`
|
||||
},
|
||||
[lookAndFeel.pageTitle]
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
Loading…
Reference in a new issue