1
0
Prechádzať zdrojové kódy

add remote config support

Aneurin Barker Snook 11 mesiacov pred
rodič
commit
2fe6c8ef85

+ 2 - 0
web/.gitignore

@@ -22,3 +22,5 @@ dist-ssr
 *.njsproj
 *.sln
 *.sw?
+
+public/config.json

+ 9 - 0
web/package-lock.json

@@ -13,6 +13,7 @@
         "@dnd-kit/sortable": "^8.0.0",
         "@dnd-kit/utilities": "^3.2.2",
         "@heroicons/react": "^2.0.18",
+        "deepmerge": "^4.3.1",
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
         "react-hook-form": "^7.48.2",
@@ -1811,6 +1812,14 @@
       "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
       "dev": true
     },
+    "node_modules/deepmerge": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/dir-glob": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",

+ 1 - 0
web/package.json

@@ -15,6 +15,7 @@
     "@dnd-kit/sortable": "^8.0.0",
     "@dnd-kit/utilities": "^3.2.2",
     "@heroicons/react": "^2.0.18",
+    "deepmerge": "^4.3.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-hook-form": "^7.48.2",

+ 9 - 0
web/src/components/AwaitConfig.tsx

@@ -0,0 +1,9 @@
+import { useConfig } from "@/hooks";
+import { PropsWithChildren } from "react";
+
+/** Wait for config to be ready before rendering children. */
+export default function AwaitConfig({ children }: PropsWithChildren) {
+  const { ready } = useConfig()
+
+  return ready ? children : null
+}

+ 3 - 3
web/src/components/button/LimitButton.tsx

@@ -1,8 +1,7 @@
 import Button from './Button'
 import type { ButtonProps } from './Button'
 import { EyeIcon } from '@heroicons/react/20/solid'
-import build from '@/build'
-import { useRouteSearch } from '@/hooks'
+import { useConfig, useRouteSearch } from '@/hooks'
 import type { MouseEvent, PropsWithChildren } from 'react'
 
 export interface LimitButtonProps extends Omit<ButtonProps, 'onClick'> {
@@ -10,9 +9,10 @@ export interface LimitButtonProps extends Omit<ButtonProps, 'onClick'> {
 }
 
 export default function LimitButton({ className = '', options, ...props }: PropsWithChildren<LimitButtonProps>) {
+  const { config } = useConfig()
   const routeSearch = useRouteSearch()
 
-  const limits = options || build.button.limit.limits
+  const limits = options || config.button.limit.limits
 
   const limitIndex = limits.indexOf(routeSearch.limit)
   const nextLimit = limitIndex < 0 || limitIndex === limits.length - 1 ? limits[0] : limits[limitIndex+1]

+ 6 - 0
web/src/hooks/index.ts

@@ -1,3 +1,4 @@
+import { ConfigContext } from '@/providers/config'
 import { ConnectionContext } from '@/providers/connection'
 import { DocumentContext } from '@/providers/document'
 import { SessionContext } from '@/providers/session'
@@ -5,6 +6,11 @@ import { useContext } from 'react'
 
 export { useRouteSearch } from './routeSearch'
 
+/** Get remote configuration. */
+export function useConfig() {
+  return useContext(ConfigContext)
+}
+
 /**
  * Get the connection to the Herda server.
  * This provides access to request options such as base URL, authentication etc.

+ 16 - 14
web/src/main.tsx

@@ -8,14 +8,12 @@ import build from './build'
 import { localValueStorage } from './lib/valueStorage'
 import routes from './routes'
 import { RouterProvider, createBrowserRouter } from 'react-router-dom'
+import { ConfigProvider } from './providers/config'
+import AwaitConfig from './components/AwaitConfig'
 
-const connectionProps = {
-  host: build.api.host,
-  timeout: build.api.timeout,
-}
-
-const documentProps = {
-  titleSuffix: build.document.titleSuffix,
+const configProps = {
+  defaults: build,
+  path: '/config.json'
 }
 
 const router = createBrowserRouter(routes)
@@ -26,12 +24,16 @@ const sessionProps = {
 
 ReactDOM.createRoot(document.getElementById('root')!).render(
   <React.StrictMode>
-    <DocumentProvider value={documentProps}>
-      <ConnectionProvider value={connectionProps}>
-        <SessionProvider value={sessionProps}>
-          <RouterProvider router={router} />
-        </SessionProvider>
-      </ConnectionProvider>
-    </DocumentProvider>
+    <ConfigProvider value={configProps}>
+      <AwaitConfig>
+        <DocumentProvider value={undefined}>
+          <ConnectionProvider value={undefined}>
+            <SessionProvider value={sessionProps}>
+              <RouterProvider router={router} />
+            </SessionProvider>
+          </ConnectionProvider>
+        </DocumentProvider>
+      </AwaitConfig>
+    </ConfigProvider>
   </React.StrictMode>,
 )

+ 39 - 0
web/src/providers/config.ts

@@ -0,0 +1,39 @@
+import * as api from '@/api'
+import build from '@/build'
+import deepmerge from 'deepmerge'
+import type { ProviderProps } from 'react'
+import { createContext, createElement, useEffect, useState } from 'react'
+
+export interface ConfigProps {
+  defaults: typeof build
+  path: string
+}
+
+export interface ConfigState {
+  config: typeof build
+  ready: boolean
+}
+
+export const ConfigContext = createContext({} as ConfigState)
+
+export function ConfigProvider({ children, value: params }: ProviderProps<ConfigProps>) {
+  const [config, setConfig] = useState(build)
+  const [ready, setReady] = useState(false)
+
+  useEffect(() => {
+    api.request<typeof build>({ host: `//${document.location.host}` }, 'GET', params.path)
+      .then(res => {
+        setConfig(deepmerge(build, res))
+      })
+      .catch(err => {
+        console.error(err)
+      })
+      .finally(() => {
+        setReady(true)
+      })
+  }, [])
+
+  const value = { config, ready }
+
+  return createElement(ConfigContext.Provider, { value }, children)
+}

+ 4 - 2
web/src/providers/connection.ts

@@ -1,6 +1,7 @@
 import type { ProviderProps } from 'react'
 import api from '@/api'
 import { createContext, createElement, useEffect, useReducer, useState } from 'react'
+import { useConfig } from '@/hooks'
 
 /**
  * API connection state.
@@ -33,11 +34,12 @@ export const ConnectionContext = createContext({} as ConnectionState)
  * API connection provider.
  * This should be one of the root components of the application.
  */
-export function ConnectionProvider({ children, value: props }: ProviderProps<api.Options>) {
+export function ConnectionProvider({ children }: ProviderProps<undefined>) {
   const [available, setAvailable] = useState(false)
+  const { config } = useConfig()
   const [error, setError] = useState<Error>()
   const [info, setInfo] = useState<api.ServerInfo>()
-  const [options, dispatchOptions] = useReducer(OptionsReducer, props)
+  const [options, dispatchOptions] = useReducer(OptionsReducer, config.api)
 
   async function connect() {
     try {

+ 7 - 8
web/src/providers/document.ts

@@ -1,10 +1,7 @@
+import { useConfig } from '@/hooks'
 import type { ProviderProps} from 'react'
 import { createContext, createElement, useEffect, useState } from 'react'
 
-export interface DocumentProps {
-  titleSuffix: string
-}
-
 export interface DocumentState {
   clearTitle(): void
   setTitle(title: string): void
@@ -12,7 +9,9 @@ export interface DocumentState {
 
 export const DocumentContext = createContext({} as DocumentState)
 
-export function DocumentProvider({ children, value: params }: ProviderProps<DocumentProps>) {
+export function DocumentProvider({ children }: ProviderProps<undefined>) {
+  const { config } = useConfig()
+
   const [title, setTitle] = useState<string>()
 
   function clearTitle() {
@@ -21,11 +20,11 @@ export function DocumentProvider({ children, value: params }: ProviderProps<Docu
 
   useEffect(() => {
     if (title) {
-      document.title = `${title} - ${params.titleSuffix}`
+      document.title = `${title} - ${config.document.titleSuffix}`
     } else {
-      document.title = params.titleSuffix
+      document.title = config.document.titleSuffix
     }
-  }, [params.titleSuffix, title])
+  }, [config.document.titleSuffix, title])
 
   const value = { clearTitle, setTitle }