Skip to content

Commit

Permalink
feat(console): support model argument schema (#3122)
Browse files Browse the repository at this point in the history
  • Loading branch information
waynelwz authored Jan 12, 2024
1 parent 58d69b9 commit f2ba362
Show file tree
Hide file tree
Showing 6 changed files with 362 additions and 69 deletions.
7 changes: 4 additions & 3 deletions console/packages/starwhale-core/src/form/WidgetForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Form from '@rjsf/core'
import validator from '@rjsf/validator-ajv8'
import React from 'react'
// @ts-ignore
function WidgetForm({ formData, onChange, onSubmit, form }: any, ref: any) {
function WidgetForm({ formData, onChange, onSubmit, form }: any, ref?: any) {
const { schema, uiSchema } = form.schemas
return (
<Form
Expand All @@ -16,8 +16,9 @@ function WidgetForm({ formData, onChange, onSubmit, form }: any, ref: any) {
onSubmit={onSubmit}
// @ts-ignore
ref={(f) => {
// eslint-disable-next-line no-param-reassign
ref.current = f
if (ref)
// eslint-disable-next-line no-param-reassign
ref.current = f
}}
onChange={(e) => {
onChange?.(e.formData)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { ChangeEvent, FocusEvent, useCallback } from 'react'
import {
ariaDescribedByIds,
descriptionId,
getTemplate,
labelValue,
schemaRequiresTrueValue,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
WidgetProps,
} from '@rjsf/utils'

/** The `CheckBoxWidget` is a widget for rendering boolean properties.
* It is typically used to represent a boolean.
*
* @param props - The `WidgetProps` for this component
*/
function CheckboxWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>({
schema,
uiSchema,
options,
id,
value,
disabled,
readonly,
label,
hideLabel,
autofocus = false,
onBlur,
onFocus,
onChange,
registry,
}: WidgetProps<T, S, F>) {
const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate', T, S, F>(
'DescriptionFieldTemplate',
registry,
options
)
// Because an unchecked checkbox will cause html5 validation to fail, only add
// the "required" attribute if the field value must be "true", due to the
// "const" or "enum" keywords
const required = schemaRequiresTrueValue<S>(schema)

const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => onChange(event.target.checked),
[onChange]
)

const handleBlur = useCallback(
(event: FocusEvent<HTMLInputElement>) => onBlur(id, event.target.checked),
[onBlur, id]
)

const handleFocus = useCallback(
(event: FocusEvent<HTMLInputElement>) => onFocus(id, event.target.checked),
[onFocus, id]
)
const description = options.description ?? schema.description

return (
<div className={`checkbox flex ${disabled || readonly ? 'disabled' : ''}`}>
{!hideLabel && !!description && (
<DescriptionFieldTemplate
id={descriptionId<T>(id)}
description={description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
{labelValue(
// eslint-disable-next-line
<label className='control-label' htmlFor={id}>
{label}
</label>,
hideLabel
)}
<input
type='checkbox'
id={id}
name={id}
checked={typeof value === 'undefined' ? false : value}
required={required}
disabled={disabled || readonly}
// eslint-disable-next-line
autoFocus={autofocus}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
aria-describedby={ariaDescribedByIds<T>(id)}
/>
</div>
)
}

export default CheckboxWidget
4 changes: 2 additions & 2 deletions console/packages/starwhale-ui/src/RJSFForm/widgets/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// import AltDateTimeWidget from "./AltDateTimeWidget";
// import AltDateWidget from "./AltDateWidget";
// import CheckboxesWidget from "./CheckboxesWidget";
// import CheckboxWidget from "./CheckboxWidget";
import CheckboxWidget from './CheckboxWidget'
// import DateTimeWidget from "./DateTimeWidget";
// import DateWidget from "./DateWidget";
// import PasswordWidget from "./PasswordWidget";
Expand All @@ -14,7 +14,7 @@ const Widgets = {
// AltDateTimeWidget,
// AltDateWidget,
// CheckboxesWidget,
// CheckboxWidget,
CheckboxWidget,
// DateTimeWidget,
// DateWidget,
// PasswordWidget,
Expand Down
226 changes: 164 additions & 62 deletions console/src/domain/job/components/FormFieldModel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import { createUseStyles } from 'react-jss'
import yaml from 'js-yaml'
import { toaster } from 'baseui/toast'
import { IStepSpec } from '@/api'
import { WidgetForm } from '@starwhale/core/form'
import { convertToRJSF } from '../utils'
import { Button } from '@starwhale/ui'
import { getReadableStorageQuantityStr } from '@/utils'
import { useSelections, useSetState } from 'ahooks'

const useStyles = createUseStyles({
modelField: {
Expand All @@ -19,8 +24,26 @@ const useStyles = createUseStyles({
gridTemplateColumns: '660px 280px 180px',
gridTemplateRows: 'minmax(0px, max-content)',
},
rjsfForm: {
'& .control-label': {
flexBasis: '170px !important',
width: '170px !important',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
},
},
})

const boolValue = (value) => {
if (value === null) return null
if (typeof value === 'string') {
return value === 'true'
}
return Boolean(value)
}

function FormFieldModel({
form,
FormItem,
Expand Down Expand Up @@ -70,6 +93,89 @@ function FormFieldModel({
const _modelVersionUrl = form.getFieldValue('modelVersionUrl')
const rawType = form.getFieldValue('rawType')

const [RJSFData, setRJSFData] = useSetState<any>({})
const getRJSFFormSchema = React.useCallback((currentStepSource) => {
const extrUISchema = {
'ui:submitButtonOptions': { norender: true },
}
const { schema, uiSchema } = convertToRJSF(currentStepSource ?? [])
return {
schemas: {
schema,
uiSchema: {
...uiSchema,
...extrUISchema,
},
},
}
}, [])

const StepLabel = ({ label, value }) => (
<>
<span style={{ color: 'rgba(2,16,43,0.60)' }}>{label}:&nbsp;</span>
<span>{value}</span>
</>
)

const SourceLabel = ({ label, value }) => {
let _v = value
if (label === 'memory') {
_v = getReadableStorageQuantityStr(value)
}
return (
<div className='flex flex-col items-center lh-normal'>
<span style={{ color: 'rgba(2,16,43,0.60)' }}>{label}</span>
<span>{_v}</span>
</div>
)
}

const { isSelected, toggle } = useSelections<any>([])

// if RJSFData changed, then update stepSpecOverWrites
// splice RJSFData by key.split('-') find the right stepSpec and update it
// then update stepSpecOverWrites
React.useEffect(() => {
if (!stepSource || Object.keys(RJSFData).length === 0) return
const newStepSource = JSON.parse(JSON.stringify(stepSource))
Object.entries(RJSFData).forEach(([key, value]) => {
const [jobName, argument, field] = key.split('@@@')
newStepSource?.forEach((v) => {
if (v?.arguments?.[argument] && v?.job_name === jobName) {
// eslint-disable-next-line
v.arguments[argument][field].value = value
}
})
})
form.setFieldsValue({
stepSpecOverWrites: yaml.dump(newStepSource),
})
}, [form, stepSource, RJSFData])

// watch stepSource and update RJSFData
React.useEffect(() => {
if (!stepSource) return
const _RJSFData = {}
stepSource?.forEach((spec) => {
if (spec?.arguments) {
Object.entries(spec?.arguments).forEach(([argument, fields]) => {
Object.entries(fields as any).forEach(([field, v]) => {
const { type, value, default: _rawDefault } = (v as any) ?? {}
const _value = type?.param_type === 'BOOL' ? boolValue(value) : value
const _default = type?.param_type === 'BOOL' ? boolValue(_rawDefault) : _rawDefault
if (value === null) {
_RJSFData[[spec?.job_name, argument, field].join('@@@')] = _default
return
}
// eslint-disable-next-line
_RJSFData[[spec?.job_name, argument, field].join('@@@')] = _value
})
})
}
})
setRJSFData(_RJSFData)
}, [stepSource, setRJSFData])

return (
<>
<div className={styles.modelField}>
Expand Down Expand Up @@ -100,70 +206,66 @@ function FormFieldModel({
<Toggle />
</FormItem>
</div>
<div style={{ paddingBottom: '0px' }}>
{stepSource &&
stepSource?.length > 0 &&
!rawType &&
stepSource?.map((spec, i) => {
return (
<div key={[spec?.name, i].join('')}>
<div
style={{
display: 'flex',
minWidth: '280px',
lineHeight: '1',
alignItems: 'stretch',
gap: '20px',
marginBottom: '10px',
}}
>
<div
style={{
padding: '5px 20px',
minWidth: '280px',
background: '#EEF1F6',
borderRadius: '4px',
}}
>
<span style={{ color: 'rgba(2,16,43,0.60)' }}>{t('Step')}:&nbsp;</span>
<span>{spec?.name}</span>
<div style={{ marginTop: '3px' }} />
<span style={{ color: 'rgba(2,16,43,0.60)' }}>{t('Task Amount')}:&nbsp;</span>
<span>{spec?.replicas}</span>
</div>
{spec.resources &&
spec.resources?.length > 0 &&
spec.resources?.map((resource, j) => (
<div
key={j}
style={{
padding: '5px 20px',
borderRadius: '4px',
border: '1px solid #E2E7F0',
// display: 'flex',
alignItems: 'center',
}}
>
<span style={{ color: 'rgba(2,16,43,0.60)' }}>
{t('Resource')}:&nbsp;
</span>
<span> {resource?.type}</span>
<div style={{ marginTop: '3px' }} />
<span style={{ color: 'rgba(2,16,43,0.60)' }}>
{t('Resource Amount')}:&nbsp;
</span>
<span>{resource?.request}</span>
<br />
<div className='flex pb-0 gap-40px'>
<div>
{stepSource &&
stepSource?.length > 0 &&
!rawType &&
stepSource?.map((spec, i) => {
return (
<div key={[spec?.name, i].join('')}>
<div className='flex lh-none items-stretch gap-[20px] mb-[10px]'>
<div className='min-w-[660px] rounded-[4px] b-[#E2E7F0] border-1'>
<div className='lh-[30px] bg-[#EEF1F6] px-[20px] py-[5px]'>
<StepLabel label={t('Step')} value={spec?.name} />
</div>
<div className='flex px-[20px] py-[15px] gap-[20px] items-center'>
<SourceLabel label={t('Task Amount')} value={spec?.replicas} />
{spec.resources &&
spec.resources?.length > 0 &&
spec.resources?.map((resource, j) => (
<SourceLabel
key={j}
label={resource?.type}
value={resource?.request}
/>
))}
{spec?.arguments && (
<div className='ml-auto'>
<Button
type='button'
icon={isSelected(spec?.name) ? 'arrow_top' : 'arrow_down'}
as='link'
onClick={() => toggle(spec?.name)}
>
{t('Parameters')}
</Button>
</div>
)}
</div>
))}
{isSelected(spec?.name) && (
<div className={`px-[20px] pb-[20px] gap-[20px] ${styles.rjsfForm}`}>
<div className='pt-[15px] pb-[25px] color-[rgba(2,16,43,0.60)] b-[#E2E7F0] border-t-1'>
{t('Parameters')}
</div>
<WidgetForm
form={getRJSFFormSchema([spec])}
formData={RJSFData}
onChange={setRJSFData}
/>
</div>
)}
</div>
</div>
</div>
</div>
)
})}
<div style={{ display: rawType ? 'block' : 'none' }}>
<FormItem label='' required name='stepSpecOverWrites'>
<MonacoEditor height='500px' width='960px' defaultLanguage='yaml' theme='vs-dark' />
</FormItem>
)
})}

<div style={{ display: rawType ? 'block' : 'none' }}>
<FormItem label='' required name='stepSpecOverWrites'>
<MonacoEditor height='500px' width='960px' defaultLanguage='yaml' theme='vs-dark' />
</FormItem>
</div>
</div>
</div>
</>
Expand Down
Loading

0 comments on commit f2ba362

Please sign in to comment.