From a2dacd04a4628b4ce30c216da0980f8809396868 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 21 Nov 2023 11:31:33 +0000 Subject: [PATCH] server load (#6) --- packages/fastui/src/components/ServerLoad.tsx | 55 +++++++++++++++++++ packages/fastui/src/components/display.tsx | 2 +- packages/fastui/src/components/index.tsx | 6 +- packages/fastui/src/controller.tsx | 49 ++--------------- packages/fastui/src/hooks/config.ts | 23 ++++++++ packages/fastui/src/hooks/customRender.ts | 15 ----- packages/fastui/src/index.tsx | 18 +++--- python/demo/main.py | 13 +++-- python/fastui/components/__init__.py | 12 +++- 9 files changed, 118 insertions(+), 75 deletions(-) create mode 100644 packages/fastui/src/components/ServerLoad.tsx create mode 100644 packages/fastui/src/hooks/config.ts delete mode 100644 packages/fastui/src/hooks/customRender.ts diff --git a/packages/fastui/src/components/ServerLoad.tsx b/packages/fastui/src/components/ServerLoad.tsx new file mode 100644 index 00000000..ecc277c4 --- /dev/null +++ b/packages/fastui/src/components/ServerLoad.tsx @@ -0,0 +1,55 @@ +import { FC, useContext, useEffect, useState } from 'react' + +import { ErrorContext } from '../hooks/error' +import { ReloadContext } from '../hooks/dev' +import { request } from '../tools' +import { DefaultLoading } from '../DefaultLoading' +import { ConfigContext } from '../hooks/config' + +import { AnyComp, FastProps } from './index' + +export interface ServerLoadProps { + type: 'ServerLoad' + url: string +} + +export const ServerLoadComp: FC = ({ url }) => { + const [componentProps, setComponentProps] = useState(null) + + const { error, setError } = useContext(ErrorContext) + const reloadValue = useContext(ReloadContext) + const { rootUrl, pathSendMode, Loading } = useContext(ConfigContext) + + useEffect(() => { + // setViewData(null) + let fetchUrl = rootUrl + if (pathSendMode === 'query') { + fetchUrl += `?path=${encodeURIComponent(url)}` + } else { + fetchUrl += url + } + + const promise = request({ url: fetchUrl }) + + promise + .then(([, data]) => setComponentProps(data as FastProps)) + .catch((e) => { + setError({ title: 'Request Error', description: e.message }) + }) + return () => { + promise.then(() => null) + } + }, [rootUrl, pathSendMode, url, setError, reloadValue]) + + if (componentProps === null) { + if (error) { + return <> + } else if (Loading) { + return + } else { + return + } + } else { + return + } +} diff --git a/packages/fastui/src/components/display.tsx b/packages/fastui/src/components/display.tsx index 7bd65ebd..456e8645 100644 --- a/packages/fastui/src/components/display.tsx +++ b/packages/fastui/src/components/display.tsx @@ -1,6 +1,6 @@ import { FC } from 'react' -import { useCustomRender } from '../hooks/customRender' +import { useCustomRender } from '../hooks/config' import { DisplayChoices, asTitle } from '../display' import { unreachable } from '../tools' diff --git a/packages/fastui/src/components/index.tsx b/packages/fastui/src/components/index.tsx index 37623a59..8bf4bd80 100644 --- a/packages/fastui/src/components/index.tsx +++ b/packages/fastui/src/components/index.tsx @@ -1,7 +1,7 @@ import { useContext, FC } from 'react' import { ErrorContext } from '../hooks/error' -import { useCustomRender } from '../hooks/customRender' +import { useCustomRender } from '../hooks/config' import { unreachable } from '../tools' import { AllDivProps, DivComp } from './div' @@ -21,6 +21,7 @@ import { ModalComp, ModalProps } from './modal' import { TableComp, TableProps } from './table' import { AllDisplayProps, DisplayArray, DisplayComp, DisplayObject, DisplayPrimitive } from './display' import { JsonComp, JsonProps } from './Json' +import { ServerLoadComp, ServerLoadProps } from './ServerLoad' export type FastProps = | TextProps @@ -35,6 +36,7 @@ export type FastProps = | LinkProps | AllDisplayProps | JsonProps + | ServerLoadProps export const AnyComp: FC = (props) => { const { DisplayError } = useContext(ErrorContext) @@ -85,6 +87,8 @@ export const AnyComp: FC = (props) => { return case 'JSON': return + case 'ServerLoad': + return default: unreachable('Unexpected component type', type, props) return diff --git a/packages/fastui/src/controller.tsx b/packages/fastui/src/controller.tsx index 78a4c5e4..098b79a8 100644 --- a/packages/fastui/src/controller.tsx +++ b/packages/fastui/src/controller.tsx @@ -1,51 +1,10 @@ -import { useContext, useEffect, useState } from 'react' +import { useContext } from 'react' -import type { FastUIProps } from './index' - -import { FastProps, AnyComp } from './components' -import { DefaultLoading } from './DefaultLoading' import { LocationContext } from './hooks/locationContext' -import { ErrorContext } from './hooks/error' -import { request } from './tools' -import { ReloadContext } from './hooks/dev' - -type Props = Omit +import { ServerLoadComp } from './components/ServerLoad' -export function FastUIController({ rootUrl, pathSendMode, loading }: Props) { - const [componentProps, setComponentProps] = useState(null) +export function FastUIController() { const { fullPath } = useContext(LocationContext) - const { error, setError } = useContext(ErrorContext) - const reloadValue = useContext(ReloadContext) - - useEffect(() => { - // setViewData(null) - let url = rootUrl - if (pathSendMode === 'query') { - url += `?path=${encodeURIComponent(fullPath)}` - } else { - url += fullPath - } - - const promise = request({ url }) - - promise - .then(([, data]) => setComponentProps(data as FastProps)) - .catch((e) => { - setError({ title: 'Request Error', description: e.message }) - }) - return () => { - promise.then(() => null).catch(() => null) - } - }, [rootUrl, pathSendMode, fullPath, setError, reloadValue]) - - if (componentProps === null) { - if (error) { - return <> - } else { - return <>{loading ? loading() : } - } - } else { - return - } + return } diff --git a/packages/fastui/src/hooks/config.ts b/packages/fastui/src/hooks/config.ts new file mode 100644 index 00000000..0b22f9dd --- /dev/null +++ b/packages/fastui/src/hooks/config.ts @@ -0,0 +1,23 @@ +import { createContext, FC, useContext } from 'react' + +import type { CustomRender } from '../index' + +import { FastProps } from '../components' + +interface Config { + rootUrl: string + // defaults to 'append' + pathSendMode?: 'append' | 'query' + customRender?: CustomRender + Loading?: FC +} + +export const ConfigContext = createContext({ rootUrl: '' }) + +export const useCustomRender = (props: FastProps): FC | void => { + const { customRender } = useContext(ConfigContext) + + if (customRender) { + return customRender(props) + } +} diff --git a/packages/fastui/src/hooks/customRender.ts b/packages/fastui/src/hooks/customRender.ts deleted file mode 100644 index 2eb7b379..00000000 --- a/packages/fastui/src/hooks/customRender.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createContext, FC, useContext } from 'react' - -import { FastProps } from '../components' - -export type CustomRender = (props: FastProps) => FC | void - -export const CustomRenderContext = createContext(null) - -export const useCustomRender = (props: FastProps): FC | void => { - const customRender = useContext(CustomRenderContext) - - if (customRender) { - return customRender(props) - } -} diff --git a/packages/fastui/src/index.tsx b/packages/fastui/src/index.tsx index 85d9f09e..09b8b382 100644 --- a/packages/fastui/src/index.tsx +++ b/packages/fastui/src/index.tsx @@ -1,21 +1,23 @@ -import { ReactNode } from 'react' +import { FC } from 'react' import { LocationProvider } from './hooks/locationContext' import { FastUIController } from './controller' import { ClassNameContext, ClassNameGenerator } from './hooks/className' import { ErrorContextProvider, ErrorDisplayType } from './hooks/error' -import { CustomRender, CustomRenderContext } from './hooks/customRender' +import { ConfigContext } from './hooks/config' import { FastProps } from './components' import { DisplayChoices } from './display' import { DevReloadProvider } from './hooks/dev' -export type { ClassNameGenerator, CustomRender, ErrorDisplayType, FastProps, DisplayChoices } +export type { ClassNameGenerator, ErrorDisplayType, FastProps, DisplayChoices } + +export type CustomRender = (props: FastProps) => FC | void export interface FastUIProps { rootUrl: string // defaults to 'append' pathSendMode?: 'append' | 'query' - loading?: () => ReactNode + Loading?: FC DisplayError?: ErrorDisplayType classNameGenerator?: ClassNameGenerator customRender?: CustomRender @@ -24,16 +26,16 @@ export interface FastUIProps { } export function FastUI(props: FastUIProps) { - const { classNameGenerator, DisplayError, customRender, devMode, ...rest } = props + const { classNameGenerator, DisplayError, devMode, ...rest } = props return (
- - - + + + diff --git a/python/demo/main.py b/python/demo/main.py index 8cff32ab..858e6fe7 100644 --- a/python/demo/main.py +++ b/python/demo/main.py @@ -1,5 +1,6 @@ from __future__ import annotations as _annotations +import asyncio from datetime import date from enum import StrEnum from typing import Annotated, Literal @@ -31,7 +32,7 @@ def read_root() -> AnyComponent: ), c.Modal( title='Modal Title', - body=[c.Text(text='Modal Content')], + body=[c.ServerLoad(url='/modal')], footer=[c.Button(text='Close', on_click=PageEvent(name='modal'))], open_trigger=PageEvent(name='modal'), ), @@ -47,6 +48,12 @@ class MyTableRow(BaseModel): enabled: bool | None = None +@app.get('/api/modal', response_model=FastUI, response_model_exclude_none=True) +async def modal_view() -> AnyComponent: + await asyncio.sleep(2) + return c.Text(text='Modal Content Dynamic') + + @app.get('/api/table', response_model=FastUI, response_model_exclude_none=True) def table_view() -> AnyComponent: return c.Page( @@ -98,7 +105,7 @@ class MyFormModel(BaseModel): @app.get('/api/form', response_model=FastUI, response_model_exclude_none=True) def form_view() -> AnyComponent: - f = c.Page( + return c.Page( children=[ c.Heading(text='Form'), c.ModelForm[MyFormModel]( @@ -111,8 +118,6 @@ def form_view() -> AnyComponent: ), ] ) - debug(f) - return f @app.post('/api/form') diff --git a/python/fastui/components/__init__.py b/python/fastui/components/__init__.py index d3abe3ed..8810273c 100644 --- a/python/fastui/components/__init__.py +++ b/python/fastui/components/__init__.py @@ -96,7 +96,17 @@ class Modal(pydantic.BaseModel): type: typing.Literal['Modal'] = 'Modal' +class ServerLoad(pydantic.BaseModel): + """ + A component that will be replaced by the server with the component returned by the given URL. + """ + + url: str + class_name: extra.ClassName | None = None + type: typing.Literal['ServerLoad'] = 'ServerLoad' + + AnyComponent = typing.Annotated[ - Text | Div | Page | Heading | Row | Col | Button | Modal | Table | Form | ModelForm | FormField, + Text | Div | Page | Heading | Row | Col | Button | Modal | ServerLoad | Table | Form | ModelForm | FormField, pydantic.Field(discriminator='type'), ]