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(console): support model argument schema #3122

Merged
merged 4 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading