Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: webui some updates #4312

Merged
merged 10 commits into from
Dec 1, 2023
11 changes: 8 additions & 3 deletions webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
"name": "bentoml-webui",
"type": "module",
"version": "0.0.0",
"main": "lib/bentoml-ui.umd.js",
"module": "src/index.tsx",
"exports": {
".": {
"import": "./src/index.tsx",
"require": "./lib/bentoml-ui.umd.js"
}
},
"main": "lib/bentoml-ui.umd.js",
"module": "src/index.tsx",
"scripts": {
"dev": "vite",
"build": "tsc && vite build --config vite.lib.config.ts",
Expand All @@ -27,8 +27,11 @@
"baseui": "^13.0.0",
"dayjs": "^1.11.10",
"jotai": "^2.5.1",
"lodash": "^4.17.21",
"monaco-editor": "^0.44.0",
"prism-react-renderer": "^1.3.5",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
"react-jss": "^10.10.0",
"react-router-dom": "^6.18.0",
Expand All @@ -38,9 +41,11 @@
"vite-plugin-svgr": "^4.1.0"
},
"devDependencies": {
"@antfu/eslint-config": "^1.1.0",
"@antfu/eslint-config": "^2.1.0",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.14.202",
"@types/react": "^18.2.15",
"@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"eslint": "^8.53.0",
Expand Down
500 changes: 395 additions & 105 deletions webui/pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion webui/scripts/copy-lib.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const fs = require('fs-extra')
const path = require('node:path')
const fs = require('fs-extra')

const targetPath = path.resolve(__dirname, '../../src/bentoml_io/server/assets')
const outDir = path.resolve(__dirname, '../lib/assets')
Expand Down
55 changes: 48 additions & 7 deletions webui/src/components/InferenceForm.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
import { useState } from 'react'
import type { Key } from 'react'
import { useMemo, useState } from 'react'
import { useStyletron } from 'baseui'
import { Select } from 'baseui/select'
import { FormControl } from 'baseui/form-control'
import { createForm } from '@formily/core'
import { FormConsumer } from '@formily/react'
import { FlexGrid, FlexGridItem } from 'baseui/flex-grid'
import { Tab, Tabs } from 'baseui/tabs'
import useCurrentPath from '../hooks/useCurrentPath'
import { postData, useSchema } from '../hooks/useQuery'
import { useFormSubmit, useSchema } from '../hooks/useQuery'
import Form from './form/Form'
import FormField, { generateFormSchema } from './form/FormField'
import Submit from './form/Submit'
import CURL from './code/CURL'

export default function InferenceForm() {
const [css, theme] = useStyletron()
const data = useSchema()
const { currentRoute, setCurrentPath } = useCurrentPath()
const [activeTab, setActiveTab] = useState<Key>('0')
const [result, setResult] = useState<object | string>()
const [error, setError] = useState<string | undefined>()
const form = createForm({})
const form = useMemo(() => createForm({}), [currentRoute])
const submit = useFormSubmit(form, currentRoute?.input)
const formSchema = generateFormSchema(currentRoute?.input)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()

if (!currentRoute)
return
const formData = await form.submit()
const resp = await postData(currentRoute.route, formData)
const resp = await submit(currentRoute.route)
if (resp.status >= 400) {
const e = await resp.text()
setError(`Error: ${resp.status} ${e}`)
Expand Down Expand Up @@ -58,8 +65,42 @@ export default function InferenceForm() {
>
<FlexGridItem>
<Form form={form} onSubmit={handleSubmit}>
<FormField schema={formSchema} />
<Submit>Submit</Submit>
<Tabs
activeKey={activeTab}
onChange={({ activeKey }) => setActiveTab(activeKey)}
overrides={{
TabBar: {
props: {
className: css({
backgroundColor: 'transparent!important',
paddingLeft: '0!important',
paddingRight: '0!important',
borderBottomWidth: '1px',
borderBottomStyle: 'solid',
borderBottomColor: `${theme.colors.borderOpaque}`,
}),
},
},
TabContent: {
props: {
className: css({
paddingLeft: '0!important',
paddingRight: '0!important',
}),
},
},
}}
>
<Tab title="Form">
<FormField schema={formSchema} />
<Submit>Submit</Submit>
</Tab>
<Tab title="HTTP">
<FormConsumer>
{() => <CURL path={currentRoute?.route} values={form.values} schema={currentRoute?.input} />}
</FormConsumer>
</Tab>
</Tabs>
</Form>
</FlexGridItem>
<FlexGridItem>
Expand Down
247 changes: 247 additions & 0 deletions webui/src/components/code/Base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import Highlight, { defaultProps } from 'prism-react-renderer'
import type { Language } from 'prism-react-renderer'
import { useStyletron } from 'baseui'
import { Button, KIND, SHAPE, SIZE } from 'baseui/button'
import { SnackbarProvider, useSnackbar } from 'baseui/snackbar'
import { IconCopy } from '@tabler/icons-react'
import Check from 'baseui/icon/check'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import { useIsLight } from '../../hooks/useTheme'

interface ICodeProps {
children: string
language?: Language
}

// these themes come from uber
// https://github.com/uber/react-view/blob/master/src/light-theme.ts
const lightTheme = {
plain: {
fontSize: '14px',
color: '#333',
backgroundColor: 'rgb(253, 253, 253)',
fontFamily: `Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace`,
margin: 0,
},
styles: [
{
types: ['comment', 'punctuation'],
style: {
color: 'rgb(170, 170, 170)',
},
},
{
types: ['operator'],
style: {
color: 'rgb(119, 119, 119)',
},
},
{
types: ['builtin', 'variable', 'constant', 'number', 'char', 'symbol'],
style: {
color: 'rgb(156, 93, 39)',
},
},
{
types: ['function'],
style: {
color: 'rgb(170, 55, 49)',
},
},
{
types: ['string'],
style: {
color: 'rgb(68, 140, 39)',
},
},
{
types: ['tag'],
style: {
color: 'rgb(75, 105, 198)',
},
},
{
types: ['attr-name'],
style: {
color: 'rgb(129, 144, 160)',
},
},
{
types: ['selector'],
style: {
color: 'rgb(122, 62, 157)',
},
},
{
types: ['keyword'],
style: {},
},
{
types: ['changed'],
style: {
color: 'rgb(0, 0, 0)',
backgroundColor: 'rgb(255, 255, 221)',
},
},
{
types: ['deleted'],
style: {
color: 'rgb(0, 0, 0)',
backgroundColor: 'rgb(255, 221, 221)',
},
},
{
types: ['inserted'],
style: {
color: 'rgb(0, 0, 0)',
backgroundColor: 'rgb(221, 255, 221)',
},
},
],
}

// https://github.com/uber/baseweb/blob/master/documentation-site/components/yard/dark-theme.ts
const darkTheme = {
plain: {
color: '#d4d4d4',
backgroundColor: '#292929',
fontSize: '14px',
fontFamily: `Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace`,
margin: 0,
},
styles: [
{
types: ['prolog'],
style: {
color: 'rgb(0, 0, 128)',
},
},
{
types: ['comment'],
style: {
color: 'rgb(106, 153, 85)',
},
},
{
types: ['builtin', 'tag', 'changed', 'punctuation', 'keyword'],
style: {
color: 'rgb(86, 156, 214)',
},
},
{
types: ['number', 'inserted'],
style: {
color: 'rgb(181, 206, 168)',
},
},
{
types: ['constant'],
style: {
color: 'rgb(100, 102, 149)',
},
},
{
types: ['attr-name', 'variable'],
style: {
color: 'rgb(156, 220, 254)',
},
},
{
types: ['deleted', 'string'],
style: {
color: 'rgb(206, 145, 120)',
},
},
{
types: ['operator'],
style: {
color: 'rgb(212, 212, 212)',
},
},
{
types: ['function'],
style: {
color: 'rgb(220, 220, 170)',
},
},
{
types: ['char'],
style: {
color: 'rgb(209, 105, 105)',
},
},
],
}

function Code({ children, language }: ICodeProps) {
const [css, theme] = useStyletron()
const isLight = useIsLight()
const { enqueue } = useSnackbar()

return (
<div
className={css({
position: 'relative',
overflow: 'auto',
padding: theme.sizing.scale400,
borderRadius: '8px',
borderLeftColor: isLight ? theme.colors.warning200 : theme.colors.mono500,
backgroundColor: isLight ? 'rgb(253, 253, 253)' : '#292929',
marginBottom: theme.sizing.scale400,
marginTop: theme.sizing.scale400,
})}
>
<CopyToClipboard
text={children}
onCopy={() => enqueue({
message: 'Copied to clipboard',
startEnhancer: ({ size }) => <Check size={size} />,
}, 1000)}
>
<Button
type="button"
kind={KIND.tertiary}
size={SIZE.compact}
shape={SHAPE.circle}
className={css({
position: 'absolute',
top: '10px',
right: '10px',
})}
>
<IconCopy size={18} />
</Button>
</CopyToClipboard>

<Highlight
{...defaultProps}
code={children.replace(/[\r\n]+$/, '')}
language={language as Language} // language is empty when we don't want to use highlight
theme={theme.name.startsWith('light-theme') ? lightTheme : darkTheme}
>
{({ style, tokens, getLineProps, getTokenProps }) => (
<pre dir="ltr" style={{ ...style, padding: '12px' }}>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
</div>

)
}

function CopyCode(props: ICodeProps) {
return (
<SnackbarProvider>
<Code {...props} />
</SnackbarProvider>
)
}

export default CopyCode
Loading