mirror of
https://github.com/Steffo99/sophon.git
synced 2024-12-22 14:54:22 +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 * as React from 'react';
|
||||||
import {LayoutThreeCol} from "@steffo/bluelib-react";
|
import {LayoutThreeCol} from "@steffo/bluelib-react";
|
||||||
import {LookAndFeel} from "./components/theme/LookAndFeel";
|
import {SophonInstanceFooter} from "./components/instance/SophonInstanceFooter";
|
||||||
import {SophonFooter} from "./components/theme/SophonFooter";
|
import * as Instance from "./components/instance"
|
||||||
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<LookAndFeel>
|
<Instance.Provider>
|
||||||
<LookAndFeel.Bluelib>
|
<Instance.Bluelib>
|
||||||
<LookAndFeel.PageTitle/>
|
<Instance.PageTitle/>
|
||||||
<LayoutThreeCol>
|
<LayoutThreeCol>
|
||||||
<LayoutThreeCol.Center>
|
<LayoutThreeCol.Center>
|
||||||
<LookAndFeel.Heading level={1}/>
|
<Instance.Heading level={1}/>
|
||||||
<SophonFooter/>
|
<SophonInstanceFooter/>
|
||||||
</LayoutThreeCol.Center>
|
</LayoutThreeCol.Center>
|
||||||
</LayoutThreeCol>
|
</LayoutThreeCol>
|
||||||
</LookAndFeel.Bluelib>
|
</Instance.Bluelib>
|
||||||
</LookAndFeel>
|
</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 * as React from "react"
|
||||||
import {Anchor, Footer} from "@steffo/bluelib-react";
|
import {Anchor, Footer} from "@steffo/bluelib-react";
|
||||||
import {useLookAndFeel} from "./LookAndFeel";
|
import {useSophonInstance} from "./useSophonInstance";
|
||||||
|
|
||||||
|
|
||||||
const FOOTER_COLORS = {
|
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"
|
const LICENSE_URL = "https://github.com/Steffo99/sophon/blob/main/LICENSE.txt"
|
||||||
|
|
||||||
|
|
||||||
export function SophonFooter(): JSX.Element {
|
export function SophonInstanceFooter(): JSX.Element {
|
||||||
const lookAndFeel = useLookAndFeel()
|
const instance = useSophonInstance()
|
||||||
|
|
||||||
const frontendVersion = process.env.REACT_APP_VERSION
|
|
||||||
const backendVersion = lookAndFeel.backendVersion
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Footer bluelibClassNames={FOOTER_COLORS[process.env.NODE_ENV]}>
|
<Footer bluelibClassNames={FOOTER_COLORS[process.env.NODE_ENV]}>
|
||||||
|
@ -28,11 +25,11 @@ export function SophonFooter(): JSX.Element {
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
|
|
|
||||||
<Anchor href={FRONTEND_REPO_URL}>
|
<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>
|
||||||
|
|
|
|
||||||
<Anchor href={BACKEND_REPO_URL}>
|
<Anchor href={BACKEND_REPO_URL}>
|
||||||
Backend {backendVersion ?? "not connected"}
|
Backend {instance?.version ?? "not connected"}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
|
|
|
||||||
<Anchor href={LICENSE_URL}>
|
<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