From a24680e5e518406014285ac31f99bbcca60e1937 Mon Sep 17 00:00:00 2001 From: waynelwz Date: Wed, 10 Jan 2024 16:59:34 +0800 Subject: [PATCH 1/4] feat: support model argument schema --- .../starwhale-core/src/form/WidgetForm.tsx | 7 +- .../src/RJSFForm/widgets/CheckboxWidget.tsx | 97 ++++++++ .../src/RJSFForm/widgets/index.ts | 4 +- console/src/api/server/data-contracts.ts | 1 + .../domain/job/components/FormFieldModel.tsx | 217 +++++++++++++----- console/src/domain/job/utils.ts | 85 +++++++ console/src/i18n/locales.ts | 12 +- 7 files changed, 354 insertions(+), 69 deletions(-) create mode 100644 console/packages/starwhale-ui/src/RJSFForm/widgets/CheckboxWidget.tsx create mode 100644 console/src/domain/job/utils.ts diff --git a/console/packages/starwhale-core/src/form/WidgetForm.tsx b/console/packages/starwhale-core/src/form/WidgetForm.tsx index c40e47159d..882079f3ff 100644 --- a/console/packages/starwhale-core/src/form/WidgetForm.tsx +++ b/console/packages/starwhale-core/src/form/WidgetForm.tsx @@ -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 (
{ - // 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) diff --git a/console/packages/starwhale-ui/src/RJSFForm/widgets/CheckboxWidget.tsx b/console/packages/starwhale-ui/src/RJSFForm/widgets/CheckboxWidget.tsx new file mode 100644 index 0000000000..28f2a8e0c1 --- /dev/null +++ b/console/packages/starwhale-ui/src/RJSFForm/widgets/CheckboxWidget.tsx @@ -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({ + schema, + uiSchema, + options, + id, + value, + disabled, + readonly, + label, + hideLabel, + autofocus = false, + onBlur, + onFocus, + onChange, + registry, +}: WidgetProps) { + 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(schema) + + const handleChange = useCallback( + (event: ChangeEvent) => onChange(event.target.checked), + [onChange] + ) + + const handleBlur = useCallback( + (event: FocusEvent) => onBlur(id, event.target.checked), + [onBlur, id] + ) + + const handleFocus = useCallback( + (event: FocusEvent) => onFocus(id, event.target.checked), + [onFocus, id] + ) + const description = options.description ?? schema.description + + return ( +
+ {!hideLabel && !!description && ( + (id)} + description={description} + schema={schema} + uiSchema={uiSchema} + registry={registry} + /> + )} + {labelValue( + // eslint-disable-next-line + , + hideLabel + )} + (id)} + /> +
+ ) +} + +export default CheckboxWidget diff --git a/console/packages/starwhale-ui/src/RJSFForm/widgets/index.ts b/console/packages/starwhale-ui/src/RJSFForm/widgets/index.ts index 2d5ed49dba..e2ac89bf06 100644 --- a/console/packages/starwhale-ui/src/RJSFForm/widgets/index.ts +++ b/console/packages/starwhale-ui/src/RJSFForm/widgets/index.ts @@ -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"; @@ -14,7 +14,7 @@ const Widgets = { // AltDateTimeWidget, // AltDateWidget, // CheckboxesWidget, - // CheckboxWidget, + CheckboxWidget, // DateTimeWidget, // DateWidget, // PasswordWidget, diff --git a/console/src/api/server/data-contracts.ts b/console/src/api/server/data-contracts.ts index 34fc1a3ad3..9847fdb0e4 100644 --- a/console/src/api/server/data-contracts.ts +++ b/console/src/api/server/data-contracts.ts @@ -1331,6 +1331,7 @@ export interface IStepSpec { ext_cmd_args?: string parameters_sig?: IParameterSignature[] service_spec?: IServiceSpec + arguments?: Record } /** diff --git a/console/src/domain/job/components/FormFieldModel.tsx b/console/src/domain/job/components/FormFieldModel.tsx index 99820b2617..a2b4b2eb1b 100644 --- a/console/src/domain/job/components/FormFieldModel.tsx +++ b/console/src/domain/job/components/FormFieldModel.tsx @@ -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: { @@ -19,6 +24,16 @@ 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', + }, + }, }) function FormFieldModel({ @@ -69,6 +84,88 @@ function FormFieldModel({ const _modelVersionUrl = form.getFieldValue('modelVersionUrl') const rawType = form.getFieldValue('rawType') + const modelVersionHandler = form.getFieldValue('modelVersionHandler') + + const [RJSFData, setRJSFData] = useSetState({}) + 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 }) => ( + <> + {label}:  + {value} + + ) + + const SourceLabel = ({ label, value }) => { + let _v = value + if (label === 'memory') { + _v = getReadableStorageQuantityStr(value) + } + return ( +
+ {label} + {_v} +
+ ) + } + + const { isSelected, toggle } = useSelections([]) + + // 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 } = v as any + if (type?.param_type === 'BOOL' && typeof value === 'string') { + _RJSFData[[spec?.job_name, argument, field].join('@@@')] = value === 'true' + return + } + // eslint-disable-next-line + _RJSFData[[spec?.job_name, argument, field].join('@@@')] = value + }) + }) + } + }) + setRJSFData(_RJSFData) + }, [stepSource, setRJSFData]) return ( <> @@ -100,70 +197,66 @@ function FormFieldModel({ -
- {stepSource && - stepSource?.length > 0 && - !rawType && - stepSource?.map((spec, i) => { - return ( -
-
-
- {t('Step')}:  - {spec?.name} -
- {t('Task Amount')}:  - {spec?.replicas} -
- {spec.resources && - spec.resources?.length > 0 && - spec.resources?.map((resource, j) => ( -
- - {t('Resource')}:  - - {resource?.type} -
- - {t('Resource Amount')}:  - - {resource?.request} -
+
+
+ {stepSource && + stepSource?.length > 0 && + !rawType && + stepSource?.map((spec, i) => { + return ( +
+
+
+
+ +
+
+ + {spec.resources && + spec.resources?.length > 0 && + spec.resources?.map((resource, j) => ( + + ))} + {spec?.arguments && ( +
+ +
+ )}
- ))} + {isSelected(spec?.name) && ( +
+
+ {t('Parameters')} +
+ +
+ )} +
+
-
- ) - })} -
- - - + ) + })} + +
+ + + +
diff --git a/console/src/domain/job/utils.ts b/console/src/domain/job/utils.ts new file mode 100644 index 0000000000..81eb32607e --- /dev/null +++ b/console/src/domain/job/utils.ts @@ -0,0 +1,85 @@ +// @ts-nocheck + +const SPLITER = '@@@' + +function convertToRJSF(sourceJson) { + const schema = { + type: 'object', + properties: {}, + } + + const uiSchema = {} + + const mapParamTypeToRJSFType = (paramType) => { + switch (paramType) { + case 'INT': + return 'integer' + case 'FLOAT': + return 'number' + case 'BOOL': + return 'boolean' + case 'STRING': + return 'string' + default: + return 'string' // Default to string if param_type is not recognized + } + } + + const traverse = (obj, parentKey, parentIsMultiple) => { + if (typeof obj === 'object' && obj !== null) { + Object.keys(obj).forEach((key) => { + const currentKey = parentKey ? `${parentKey}${SPLITER}${key}` : key + const field = obj[key] + + if (field) { + const rjsfField = { + type: mapParamTypeToRJSFType(field.type.param_type), + title: field.help, + default: field.default, + } + + if (field.type.choices && field.type.choices?.length > 0) { + rjsfField.enum = field.type.choices + } + + if (field.multiple || parentIsMultiple) { + rjsfField.type = 'array' + rjsfField.items = { + type: mapParamTypeToRJSFType(field.type.param_type), + default: field.default, + } + + if (field.type.choices !== null && field.type.choices.length > 0) { + rjsfField.items.enum = field.type.choices + rjsfField.uniqueItems = true + } + } + + schema.properties[currentKey] = rjsfField + + if (field.hidden) { + delete uiSchema[currentKey] + // uiSchema[currentKey] = { + // ...uiSchema[currentKey], + // 'ui:widget': 'hidden', + // } + } + + if (typeof field.type.name === 'object' && field.type.name !== null) { + traverse(field.type.name, currentKey, field.multiple) + } + } + }) + } + } + + sourceJson?.forEach((level1) => { + Object.entries(level1?.arguments ?? {}).forEach(([level2Name, level2]) => { + traverse(level2, [level1.job_name, level2Name].filter(Boolean).join(SPLITER)) + }) + }) + + return { schema, uiSchema } +} + +export { convertToRJSF } diff --git a/console/src/i18n/locales.ts b/console/src/i18n/locales.ts index d7815e9341..0251b2909d 100644 --- a/console/src/i18n/locales.ts +++ b/console/src/i18n/locales.ts @@ -2232,8 +2232,16 @@ const locales0 = { zh: '副本', }, 'Raw Type': { - en: 'Edit', - zh: '修改', + en: 'Advanced Setting', + zh: '高级配置', + }, + 'Adjust Arguments': { + en: 'Adjust Arguments', + zh: '调整参数', + }, + 'Parameters': { + en: 'Parameters', + zh: '参数', }, 'eval debug mode': { en: 'Debug Mode', From d0fe393d798fc5379c0e7214bc1ddb4b50fd77cd Mon Sep 17 00:00:00 2001 From: waynelwz Date: Wed, 10 Jan 2024 17:01:27 +0800 Subject: [PATCH 2/4] fix: lint --- console/packages/starwhale-ui/src/RJSFForm/widgets/index.ts | 2 +- console/src/domain/job/components/FormFieldModel.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/console/packages/starwhale-ui/src/RJSFForm/widgets/index.ts b/console/packages/starwhale-ui/src/RJSFForm/widgets/index.ts index e2ac89bf06..3edad6d137 100644 --- a/console/packages/starwhale-ui/src/RJSFForm/widgets/index.ts +++ b/console/packages/starwhale-ui/src/RJSFForm/widgets/index.ts @@ -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"; diff --git a/console/src/domain/job/components/FormFieldModel.tsx b/console/src/domain/job/components/FormFieldModel.tsx index a2b4b2eb1b..6037306d2c 100644 --- a/console/src/domain/job/components/FormFieldModel.tsx +++ b/console/src/domain/job/components/FormFieldModel.tsx @@ -84,7 +84,6 @@ function FormFieldModel({ const _modelVersionUrl = form.getFieldValue('modelVersionUrl') const rawType = form.getFieldValue('rawType') - const modelVersionHandler = form.getFieldValue('modelVersionHandler') const [RJSFData, setRJSFData] = useSetState({}) const getRJSFFormSchema = React.useCallback((currentStepSource) => { From 66b5ba2d9fd5e8e71347af26f1451360dff61025 Mon Sep 17 00:00:00 2001 From: waynelwz Date: Wed, 10 Jan 2024 17:21:23 +0800 Subject: [PATCH 3/4] update: compatibility stringify bool value with backend --- .../domain/job/components/FormFieldModel.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/console/src/domain/job/components/FormFieldModel.tsx b/console/src/domain/job/components/FormFieldModel.tsx index 6037306d2c..6d05f64638 100644 --- a/console/src/domain/job/components/FormFieldModel.tsx +++ b/console/src/domain/job/components/FormFieldModel.tsx @@ -36,6 +36,14 @@ const useStyles = createUseStyles({ }, }) +const boolValue = (value) => { + if (value === null) return null + if (typeof value === 'string') { + return value === 'true' + } + return Boolean(value) +} + function FormFieldModel({ form, FormItem, @@ -152,13 +160,15 @@ function FormFieldModel({ if (spec?.arguments) { Object.entries(spec?.arguments).forEach(([argument, fields]) => { Object.entries(fields as any).forEach(([field, v]) => { - const { type, value } = v as any - if (type?.param_type === 'BOOL' && typeof value === 'string') { - _RJSFData[[spec?.job_name, argument, field].join('@@@')] = value === 'true' + 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 + _RJSFData[[spec?.job_name, argument, field].join('@@@')] = _value }) }) } From c330e177d086d5c4414f5db78e070882248b3dbe Mon Sep 17 00:00:00 2001 From: waynelwz Date: Fri, 12 Jan 2024 11:13:57 +0800 Subject: [PATCH 4/4] update: arguemnt schema --- console/src/api/server/data-contracts.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/console/src/api/server/data-contracts.ts b/console/src/api/server/data-contracts.ts index 9847fdb0e4..34fc1a3ad3 100644 --- a/console/src/api/server/data-contracts.ts +++ b/console/src/api/server/data-contracts.ts @@ -1331,7 +1331,6 @@ export interface IStepSpec { ext_cmd_args?: string parameters_sig?: IParameterSignature[] service_spec?: IServiceSpec - arguments?: Record } /**