diff --git a/console/package.json b/console/package.json index 3f95e0f95a..4cca70d361 100644 --- a/console/package.json +++ b/console/package.json @@ -40,6 +40,7 @@ "@types/styletron-standard": "^2.0.2", "@types/uuid": "^8.3.1", "@uiw/react-md-editor": "^3.9.3", + "ahooks": "^3.7.8", "axios": "^0.21.1", "base64-js": "^1.5.1", "baseui": "12.2.0", @@ -272,4 +273,4 @@ "framer-motion": "4.1.17", "react-virtualized": "git+https://git@github.com/remorses/react-virtualized-fixed-import.git#9.22.3" } -} \ No newline at end of file +} diff --git a/console/packages/starwhale-ui/src/base/tree-view/tree-view.tsx b/console/packages/starwhale-ui/src/base/tree-view/tree-view.tsx index b8d3e3558e..ab7b733390 100644 --- a/console/packages/starwhale-ui/src/base/tree-view/tree-view.tsx +++ b/console/packages/starwhale-ui/src/base/tree-view/tree-view.tsx @@ -173,10 +173,10 @@ export default function TreeView(props: TreeViewProps) { return ( - {data.map((node) => ( + {data.map((node, i) => ( { diff --git a/console/src/api/const.ts b/console/src/api/const.ts index ebdcb2913d..ed242fc841 100644 --- a/console/src/api/const.ts +++ b/console/src/api/const.ts @@ -8,6 +8,7 @@ export const Privileges = { 'evaluation.action': true, 'evaluation.create': true, 'runtime.version.revert': true, + 'model.run': true, 'model.version.revert': true, 'model.version.serve': true, 'dataset.version.revert': true, diff --git a/console/src/components/Extensions/index.tsx b/console/src/components/Extensions/index.tsx index 5670715e6e..09a9094c49 100644 --- a/console/src/components/Extensions/index.tsx +++ b/console/src/components/Extensions/index.tsx @@ -1,12 +1,35 @@ import React from 'react' +import FormFieldResource from '@/domain/job/components/FormFieldResource' +import FormFieldAutoRelease from '@/domain/job/components/FormFieldAutoRelease' -let HeaderExtend: React.FC = () => <> +let HeaderExtendTmp: React.FC = () => <> +let FormFieldResourceTmp = FormFieldResource +let FormFieldPriTmp: React.FC = () => <> +let FormFieldAutoReleaseTmp = FormFieldAutoRelease -export function registerExtensions(components: { HeaderExtend: React.FC }) { +export function registerExtensions(components: any) { if (!components) return - HeaderExtend = components?.HeaderExtend + HeaderExtendTmp = components?.HeaderExtend + FormFieldResourceTmp = components?.FormFieldResource + FormFieldPriTmp = components?.FormFieldPri + FormFieldAutoReleaseTmp = components?.FormFieldAutoRelease } export function HeaderExtends() { - return + return +} + +export function FormFieldResourceExtend(props: any) { + if (!FormFieldResourceTmp) return null + return +} + +export function FormFieldPriExtend(props: any) { + if (!FormFieldPriTmp) return null + return +} + +export function FormFieldAutoReleaseExtend(props: any) { + if (!FormFieldAutoReleaseTmp) return null + return } diff --git a/console/src/components/Form/form.tsx b/console/src/components/Form/form.tsx index eebc128d08..698b032e90 100755 --- a/console/src/components/Form/form.tsx +++ b/console/src/components/Form/form.tsx @@ -198,8 +198,8 @@ export function createForm({ 'div', { style: { display: 'flex', alignItems: 'center', gap: 4 } }, [ - React.createElement('div', { style: { flexShrink: 0 } }, label), - React.createElement('div', {}, '*'), + React.createElement('div', { style: { flexShrink: 0 }, key: 0 }, label), + React.createElement('div', { key: 1 }, '*'), ] ) } diff --git a/console/src/components/LogViewer/LogViewer.tsx b/console/src/components/LogViewer/LogViewer.tsx index 3c83bdd9d0..8f0f9e0696 100644 --- a/console/src/components/LogViewer/LogViewer.tsx +++ b/console/src/components/LogViewer/LogViewer.tsx @@ -1,6 +1,6 @@ import '@patternfly/react-core/dist/styles/base.css' -import React from 'react' +import React, { useEffect } from 'react' import { LogViewer, LogViewerSearch } from '@patternfly/react-log-viewer' import { Button, @@ -62,7 +62,7 @@ const ComplexToolbarLogViewer = ({ }[] }) => { const [isPaused, setIsPaused] = React.useState(false) - const [isFullScreen, setIsFullScreen] = React.useState(false) + const [, setIsFullScreen] = React.useState(true) const [currentItemCount, setCurrentItemCount] = React.useState(0) const [renderData, setRenderData] = React.useState('') const [selectedDataSource, setSelectedDataSource] = React.useState(dataSources[0]?.id) @@ -117,7 +117,9 @@ const ComplexToolbarLogViewer = ({ // @ts-ignore const onExpandClick = () => { const element = document.querySelector('#complex-toolbar-demo') - if (!isFullScreen) { + if (!document.fullscreenElement) { + setIsFullScreen(true) + if (element?.requestFullscreen) { element.requestFullscreen() // @ts-ignore @@ -129,8 +131,9 @@ const ComplexToolbarLogViewer = ({ // @ts-ignore element?.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT) } - setIsFullScreen(true) } else { + setIsFullScreen(false) + if (document.exitFullscreen) { document.exitFullscreen() // @ts-ignore @@ -144,10 +147,24 @@ const ComplexToolbarLogViewer = ({ // @ts-ignore document.msExitFullscreen() } - setIsFullScreen(false) } } + useEffect(() => { + const element = document.querySelector('#complex-toolbar-demo') + if (element?.requestFullscreen) { + element.requestFullscreen() + // @ts-ignore + } else if (element?.mozRequestFullScreen) { + // @ts-ignore + element?.mozRequestFullScreen() + // @ts-ignore + } else if (element?.webkitRequestFullScreen) { + // @ts-ignore + element?.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT) + } + }, []) + const onDownloadClick = () => { const element = document.createElement('a') const dataToDownload = [selectedData.join('\n')] diff --git a/console/src/domain/job/components/FormFieldAutoRelease.tsx b/console/src/domain/job/components/FormFieldAutoRelease.tsx new file mode 100644 index 0000000000..f9b2b63935 --- /dev/null +++ b/console/src/domain/job/components/FormFieldAutoRelease.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import useTranslation from '@/hooks/useTranslation' +import { FormInstance, FormItemProps } from '@/components/Form/form' +import { ICreateJobFormSchema } from '../schemas/job' +import { Toggle } from '@starwhale/ui/Select' +import { NumberInput } from '@starwhale/ui/Input' + +function FormFieldAutoRelease({ + form, + FormItem, +}: { + form: FormInstance + FormItem: (props_: FormItemProps) => any +}) { + const [t] = useTranslation() + + return ( + <> + {form.getFieldValue('isTimeToLiveInSec') && ( +

{t('job.autorelease.notice')}

+ )} +
+ + + + {form.getFieldValue('isTimeToLiveInSec') && ( + + t('resource.price.unit.second')} /> + + )} +
+ + ) +} + +export default FormFieldAutoRelease diff --git a/console/src/domain/job/components/FormFieldDataset.tsx b/console/src/domain/job/components/FormFieldDataset.tsx new file mode 100644 index 0000000000..4b609b9159 --- /dev/null +++ b/console/src/domain/job/components/FormFieldDataset.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import useTranslation from '@/hooks/useTranslation' +import { FormInstance, FormItemProps } from '@/components/Form/form' +import { useParams } from 'react-router-dom' +import { ICreateJobFormSchema } from '../schemas/job' +import DatasetTreeSelector from '@/domain/dataset/components/DatasetTreeSelector' + +function FormFieldDataset({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + form, + FormItem, +}: { + form: FormInstance + FormItem: (props_: FormItemProps) => any +}) { + const [t] = useTranslation() + const { projectId } = useParams<{ projectId: string }>() + + return ( +
+ + + +
+ ) +} + +export default FormFieldDataset diff --git a/console/src/domain/job/components/FormFieldDevMode.tsx b/console/src/domain/job/components/FormFieldDevMode.tsx new file mode 100644 index 0000000000..e38655bb7d --- /dev/null +++ b/console/src/domain/job/components/FormFieldDevMode.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import useTranslation from '@/hooks/useTranslation' +import { FormInstance, FormItemProps } from '@/components/Form/form' +import { ICreateJobFormSchema } from '../schemas/job' +import generatePassword from '@/utils/passwordGenerator' +import { EventEmitter } from 'ahooks/lib/useEventEmitter' +import { WithCurrentAuth } from '@/api/WithAuth' +import { Toggle } from '@starwhale/ui/Select' +import CopyToClipboard from 'react-copy-to-clipboard' +import Input from '@starwhale/ui/Input' +import IconFont from '@starwhale/ui/IconFont' +import { toaster } from 'baseui/toast' + +function FormFieldDevMode({ + form, + FormItem, + // eslint-disable-next-line + EventEmitter, +}: { + form: FormInstance + FormItem: (props_: FormItemProps) => any + EventEmitter: EventEmitter +}) { + const [t] = useTranslation() + + EventEmitter.useSubscription(({ changes: _changes }) => { + if ('devMode' in _changes && _changes.devMode) { + form.setFieldsValue({ + devPassword: generatePassword(), + }) + } + }) + + return ( + + {form.getFieldValue('devMode') &&

{t('job.debug.notice')}

} +
+ + + + {form.getFieldValue('devMode') && ( + + { + toaster.positive(t('Copied'), { autoHideDuration: 1000 }) + }} + > + + + + + } + /> + + )} +
+
+ ) +} + +export default FormFieldDevMode diff --git a/console/src/domain/job/components/FormFieldModel.tsx b/console/src/domain/job/components/FormFieldModel.tsx new file mode 100644 index 0000000000..13ee36a020 --- /dev/null +++ b/console/src/domain/job/components/FormFieldModel.tsx @@ -0,0 +1,187 @@ +import React, { useEffect } from 'react' +import { FormSelect, Toggle } from '@starwhale/ui/Select' +import useTranslation from '@/hooks/useTranslation' +import { FormInstance, FormItemProps } from '@/components/Form/form' +import { useParams } from 'react-router-dom' +import { ICreateJobFormSchema } from '../schemas/job' +import ModelTreeSelector from '@/domain/model/components/ModelTreeSelector' +import { EventEmitter } from 'ahooks/lib/useEventEmitter' +import Editor from '@monaco-editor/react' +import { createUseStyles } from 'react-jss' +import yaml from 'js-yaml' +import { toaster } from 'baseui/toast' +import { StepSpec } from '@/domain/model/schemas/modelVersion' + +const useStyles = createUseStyles({ + modelField: { + display: 'grid', + columnGap: 40, + gridTemplateColumns: '660px 280px 280px', + gridTemplateRows: 'minmax(0px, max-content)', + }, +}) + +function FormFieldModel({ + form, + FormItem, + // eslint-disable-next-line + EventEmitter, + stepSource, + setModelTree, + fullStepSource, + forceUpdate, +}: { + form: FormInstance + FormItem: (props_: FormItemProps) => any + EventEmitter: EventEmitter + stepSource?: StepSpec[] + setModelTree: (obj: any) => void + fullStepSource?: StepSpec[] + forceUpdate: () => void +}) { + const [t] = useTranslation() + const { projectId } = useParams<{ projectId: string }>() + const styles = useStyles() + + EventEmitter.useSubscription(({ changes: _changes, values: values_ }) => { + if ('modelVersionUrl' in _changes) { + form.setFieldsValue({ + modelVersionHandler: '', + }) + } + + if ('rawType' in _changes && !_changes.rawType) { + try { + yaml.load(values_.stepSpecOverWrites) + } catch (e) { + toaster.negative(t('wrong yaml syntax'), { autoHideDuration: 1000 }) + form.setFieldsValue({ + rawType: true, + }) + } + } + }) + + const modelVersionHandler = form.getFieldValue('modelVersionHandler') + const _modelVersionUrl = form.getFieldValue('modelVersionUrl') + const rawType = form.getFieldValue('rawType') + + useEffect(() => { + if (!fullStepSource) return + + if (!modelVersionHandler) { + form.setFieldsValue({ + modelVersionHandler: fullStepSource.find((v) => v)?.job_name ?? '', + }) + } + if (modelVersionHandler) { + form.setFieldsValue({ + stepSpecOverWrites: yaml.dump( + fullStepSource.filter((v: StepSpec) => v?.job_name === modelVersionHandler) + ), + }) + } + forceUpdate() + }, [form, fullStepSource, modelVersionHandler, forceUpdate]) + + return ( + <> +
+ + obj.versionName} + /> + + {_modelVersionUrl && fullStepSource && ( + + {/* @ts-ignore */} + tmp.job_name))).map((tmp) => { + return { + label: tmp, + id: tmp, + } + }) ?? []) as any + } + /> + + )} + + + +
+
+ {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} +
+
+ ))} +
+
+ ) + })} +
+ + + +
+
+ + ) +} + +export default FormFieldModel diff --git a/console/src/domain/job/components/FormFieldResource.tsx b/console/src/domain/job/components/FormFieldResource.tsx new file mode 100644 index 0000000000..bed8dc6d35 --- /dev/null +++ b/console/src/domain/job/components/FormFieldResource.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import useTranslation from '@/hooks/useTranslation' +import { FormInstance, FormItemProps } from '@/components/Form/form' +import { ICreateJobFormSchema } from '../schemas/job' +import ResourcePoolSelector from '@/domain/setting/components/ResourcePoolSelector' + +function FormFieldResource({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + form, + FormItem, + setResource, +}: { + form: FormInstance + FormItem: (props_: FormItemProps) => any + setResource: (resource: any) => void +}) { + const [t] = useTranslation() + + return ( +
+ + { + setResource(item as any) + }} + /> + +
+ ) +} + +export default FormFieldResource diff --git a/console/src/domain/job/components/FormFieldRuntime.tsx b/console/src/domain/job/components/FormFieldRuntime.tsx new file mode 100644 index 0000000000..e7e9835445 --- /dev/null +++ b/console/src/domain/job/components/FormFieldRuntime.tsx @@ -0,0 +1,78 @@ +import React, { useEffect } from 'react' +import { FormSelect } from '@starwhale/ui/Select' +import RuntimeTreeSelector from '@runtime/components/RuntimeTreeSelector' +import useTranslation from '@/hooks/useTranslation' +import { FormInstance, FormItemProps } from '@/components/Form/form' +import { useParams } from 'react-router-dom' +import { ICreateJobFormSchema, RuntimeType } from '../schemas/job' + +function FormFieldRuntime({ + form, + FormItem, + builtInRuntime, +}: { + form: FormInstance + FormItem: (props_: FormItemProps) => any + builtInRuntime?: string +}) { + const [t] = useTranslation() + const { projectId } = useParams<{ projectId: string }>() + + const type = form.getFieldValue('runtimeType') + + useEffect(() => { + if (type === RuntimeType.OTHER) { + form.setFieldsValue({ runtimeVersionUrl: undefined }) + } + }, [form, type]) + + return ( +
+ {!!builtInRuntime && ( + + {/* @ts-ignore */} + + + )} + {type === RuntimeType.BUILTIN && ( + <> + +

+ {builtInRuntime} +

+ + )} + {(type === RuntimeType.OTHER || !builtInRuntime) && ( + + + + )} +
+ ) +} + +export default FormFieldRuntime diff --git a/console/src/domain/job/components/JobForm.tsx b/console/src/domain/job/components/JobForm.tsx index a66652b4da..4c414df351 100644 --- a/console/src/domain/job/components/JobForm.tsx +++ b/console/src/domain/job/components/JobForm.tsx @@ -1,107 +1,58 @@ -import React, { useCallback, useEffect, useState } from 'react' -import { useHistory, useParams } from 'react-router-dom' +import React, { useCallback, useEffect, useReducer, useState } from 'react' +import { useHistory } from 'react-router-dom' import { createForm } from '@/components/Form' import useTranslation from '@/hooks/useTranslation' import { isModified } from '@/utils' import Divider from '@/components/Divider' -import _ from 'lodash' -import ResourcePoolSelector from '@/domain/setting/components/ResourcePoolSelector' import { IModelVersionSchema, StepSpec } from '@/domain/model/schemas/modelVersion' -import Editor from '@monaco-editor/react' import yaml from 'js-yaml' -import { createUseStyles } from 'react-jss' import { toaster } from 'baseui/toast' import Button from '@starwhale/ui/Button' -import { ICreateJobFormSchema, ICreateJobSchema, IJobFormSchema } from '../schemas/job' -import { FormSelect, Toggle } from '@starwhale/ui/Select' -import DatasetTreeSelector from '@/domain/dataset/components/DatasetTreeSelector' -import RuntimeTreeSelector from '@runtime/components/RuntimeTreeSelector' -import ModelTreeSelector from '@/domain/model/components/ModelTreeSelector' +import { ICreateJobFormSchema, ICreateJobSchema, IJobSchema, RuntimeType } from '../schemas/job' import { IModelTreeSchema } from '@/domain/model/schemas/model' -import Input, { NumberInput } from '@starwhale/ui/Input' -import generatePassword from '@/utils/passwordGenerator' -import CopyToClipboard from 'react-copy-to-clipboard' -import { IconFont } from '@starwhale/ui' -import { WithCurrentAuth } from '@/api/WithAuth' import { useQueryArgs } from '@starwhale/core/utils' +import FormFieldRuntime from './FormFieldRuntime' +import { useEventEmitter } from 'ahooks' +import FormFieldModel from './FormFieldModel' +import FormFieldDataset from './FormFieldDataset' +import FormFieldDevMode from './FormFieldDevMode' +import { FormFieldAutoReleaseExtend, FormFieldPriExtend, FormFieldResourceExtend } from '@/components/Extensions' -const { Form, FormItem, useForm, FormItemLabel } = createForm() - -const useStyles = createUseStyles({ - row3: { - display: 'grid', - gap: 40, - gridTemplateColumns: '280px 300px 280px', - gridTemplateRows: 'minmax(0px, max-content)', - }, - row4: { - display: 'grid', - columnGap: 40, - gridTemplateColumns: '120px 120px 480px 100px', - }, - rowModel: { - display: 'grid', - columnGap: 40, - gridTemplateColumns: '660px 280px 280px', - gridTemplateRows: 'minmax(0px, max-content)', - }, - resource: { - gridColumnStart: 3, - display: 'grid', - columnGap: 40, - gridTemplateColumns: '260px 120px 40px', - }, -}) +const { Form, FormItem, useForm } = createForm() export interface IJobFormProps { - job?: IJobFormSchema + job?: IJobSchema onSubmit: (data: ICreateJobSchema) => Promise } export default function JobForm({ job, onSubmit }: IJobFormProps) { - const styles = useStyles() + const EventEmitter = useEventEmitter<{ changes: Partial; values: ICreateJobFormSchema }>() const [values, setValues] = useState(undefined) - const { projectId } = useParams<{ projectId: string }>() const [modelTree, setModelTree] = useState([]) - const [modelId, setModelId] = useState('') - const [modelVersionId, setModelVersionId] = useState('') - const [modelVersionHandler, setModelVersionHandler] = useState('') - const [rawType, setRawType] = React.useState(false) - const [stepSpecOverWrites, setStepSpecOverWrites] = React.useState('') + const [resource, setResource] = React.useState() + const [, forceUpdate] = useReducer((x) => x + 1, 0) + const [loading, setLoading] = useState(false) + const { query } = useQueryArgs() const [t] = useTranslation() - // const [resourcePool, setResourcePool] = React.useState() - - const RuntimeType = { - BUILTIN: t('runtime.image.builtin'), - OTHER: t('runtime.image.other'), - } - - const [form] = useForm() const history = useHistory() + const [form] = useForm() - const [loading, setLoading] = useState(false) - - const [builtInRuntime, setBuiltInRuntime] = useState('') - const [type, setType] = useState(builtInRuntime ? RuntimeType.BUILTIN : '') + const modelVersionHandler = form.getFieldValue('modelVersionHandler') + const stepSpecOverWrites = form.getFieldValue('stepSpecOverWrites') as string + const _modelVersionId = form.getFieldValue('modelVersionUrl') const modelVersion: IModelVersionSchema | undefined = React.useMemo(() => { - if (!modelTree || !modelVersionId) return undefined + if (!modelTree || !_modelVersionId) return undefined let version: IModelVersionSchema | undefined modelTree?.forEach((v) => v.versions.forEach((versionTmp) => { - if (versionTmp.id === modelVersionId) { + if (versionTmp.id === _modelVersionId) { version = versionTmp } }) ) return version - }, [modelTree, modelVersionId]) - - useEffect(() => { - if (!modelVersion) return - setBuiltInRuntime(modelVersion?.builtInRuntime ?? '') - setType(modelVersion?.builtInRuntime ? RuntimeType.BUILTIN : RuntimeType.OTHER) - }, [modelVersion, RuntimeType.BUILTIN, RuntimeType.OTHER]) + }, [modelTree, _modelVersionId]) const fullStepSource: StepSpec[] | undefined = React.useMemo(() => { if (!modelVersion) return undefined @@ -120,10 +71,6 @@ export default function JobForm({ job, onSubmit }: IJobFormProps) { return fullStepSource.filter((v: StepSpec) => v?.job_name === modelVersionHandler) }, [fullStepSource, modelVersionHandler, stepSpecOverWrites]) - const isModifiedDataset = React.useMemo(() => { - return stepSource?.some((v) => v.require_dataset === null || v.require_dataset) - }, [stepSource]) - const checkStepSource = useCallback( (value) => { try { @@ -140,335 +87,118 @@ export default function JobForm({ job, onSubmit }: IJobFormProps) { const handleFinish = useCallback( async (values_: ICreateJobFormSchema) => { setLoading(true) - if (values_.rawType && !checkStepSource(stepSpecOverWrites)) return + const tmp = { + datasetVersionUrls: values_.datasetVersionUrls?.join(','), + resourcePool: resource?.resourceId ?? resource?.name, + stepSpecOverWrites: values_.stepSpecOverWrites, + runtimeVersionUrl: values_.runtimeType === RuntimeType.BUILTIN ? '' : values_.runtimeVersionUrl, + modelVersionUrl: values_.modelVersionUrl, + devMode: values_.devMode, + devPassword: values_.devPassword, + timeToLiveInSec: values_.timeToLiveInSec, + } + if (values_.rawType && !checkStepSource(values_.stepSpecOverWrites)) { + setLoading(false) + return + } try { - await onSubmit({ - ..._.omit(values_, [ - 'modelId', - 'datasetId', - 'datasetVersionId', - 'datasetVersionIdsArr', - 'runtimeId', - 'runtimeVersionUrl', - 'rawType', - 'stepSpecOverWrites', - 'modelVersionHandler', - 'modelVersionUrl', - 'isTimeToLiveInSec', - ]), - runtimeVersionUrl: type === RuntimeType.BUILTIN ? '' : values_.runtimeVersionUrl, - modelVersionUrl: values_.modelVersionUrl, - datasetVersionUrls: values_.datasetVersionIdsArr?.join(','), - stepSpecOverWrites: values_.rawType ? stepSpecOverWrites : yaml.dump(stepSource), - }) + await onSubmit(tmp) history.goBack() } finally { setLoading(false) } }, - [onSubmit, history, stepSpecOverWrites, stepSource, checkStepSource, type, RuntimeType.BUILTIN] - ) - - const handleEditorChange = React.useCallback( - (value: string) => { - setStepSpecOverWrites(value) - }, - [setStepSpecOverWrites] - ) - - const handleModelHandlerChange = useCallback( - (value: any) => { - setModelVersionHandler(value) - }, - [setModelVersionHandler] + [onSubmit, history, checkStepSource, resource] ) const handleValuesChange = useCallback( (_changes: Partial, values_: ICreateJobFormSchema) => { - if ('modelVersionUrl' in _changes) { - setModelVersionHandler('') - } - if (values_.modelVersionUrl) { - setModelVersionId(values_.modelVersionUrl) - } - if ('devMode' in _changes && _changes.devMode) { - form.setFieldsValue({ - devPassword: generatePassword(), - }) - } - if (values_.modelVersionHandler) { - setModelVersionHandler(values_.modelVersionHandler) - } - let rawTypeTmp = values_.rawType - if ('rawType' in _changes && !_changes.rawType) { - try { - yaml.load(stepSpecOverWrites) - rawTypeTmp = false - } catch (e) { - toaster.negative(t('wrong yaml syntax'), { autoHideDuration: 1000 }) - form.setFieldsValue({ - rawType: true, - }) - rawTypeTmp = true - } - } - setRawType(rawTypeTmp) + EventEmitter.emit({ + changes: _changes, + values: values_, + }) setValues({ ...values_, - rawType: rawTypeTmp, }) }, - [stepSpecOverWrites, form, t] + [EventEmitter] ) + const sharedFormProps = { form, FormItem, EventEmitter, forceUpdate } + const getModelProps = () => ({ setModelTree, fullStepSource, stepSource }) + const getResourcePoolProps = () => ({ resource, setResource }) + const getRuntimeProps = () => ({ builtInRuntime: modelVersion?.builtInRuntime }) + + const isModifiedDataset = React.useMemo(() => { + return stepSource?.some((v) => v.require_dataset === null || v.require_dataset) + }, [stepSource]) + useEffect(() => { - if (!modelVersionHandler) { - setModelVersionHandler(fullStepSource?.find((v) => v)?.job_name ?? '') + // init by new model + if (modelVersion) { + form.setFieldsValue({ + runtimeVersionUrl: modelVersion?.builtInRuntime, + runtimeType: modelVersion?.builtInRuntime ? RuntimeType.BUILTIN : RuntimeType.OTHER, + }) } - if (modelVersionHandler) { - setStepSpecOverWrites( - yaml.dump(fullStepSource?.filter((v: StepSpec) => v?.job_name === modelVersionHandler)) - ) + if (modelVersion) { + form.setFieldsValue({ + modelVersionHandler: modelVersion.stepSpecs?.find((v) => v)?.job_name ?? '', + }) } - }, [fullStepSource, modelVersionHandler]) - - // auto select by modelid & handler , load from query args: used by online-eval - const { query } = useQueryArgs() - useEffect(() => { - if (query.modelId && modelTree) { - setModelId(query.modelId) - let vid = '' + // init by online eval args, auto select the first version + if (modelTree && query.modelId) { modelTree?.forEach((v) => { - if (v.modelId === modelId) { - vid = v.versions?.[0]?.id + if (v.modelId === query.modelId) { + form.setFieldsValue({ + modelVersionUrl: v.versions?.[0]?.id, + }) } }) - if (!vid) return - setModelVersionId(vid) + } + if (query.modelVersionHandler) { form.setFieldsValue({ - modelVersionUrl: vid, + modelVersionHandler: query.modelVersionHandler, }) } - if (query.handler) { - setModelVersionHandler(query.handler) - } - }, [query.modelId, query.handler, modelTree, modelId, form]) + forceUpdate() + }, [form, query, modelTree, modelVersion]) + + useEffect(() => { + // init by rerun job details + if (!job) return + form.setFieldsValue({ + runtimeType: job.isBuiltinRuntime ? RuntimeType.BUILTIN : RuntimeType.OTHER, + runtimeVersionUrl: job.runtime?.version?.id, + resourcePool: job.resourcePool, + datasetVersionUrls: job.datasetList?.map((v) => v.version?.id) as string[], + modelVersionUrl: job.model?.version?.id, + modelVersionHandler: job.jobName, + }) + forceUpdate() + }, [form, job]) return (
+ {/* env config */} {t('Environment')} -
- - - -
+ + {/* model config */} {t('Model Information')} -
- - - - {modelVersionId && ( - -
- tmp.job_name))).map((tmp) => { - return { - label: tmp, - id: tmp, - } - }) ?? [] - } - /> - - )} - - - -
-
- {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} -
-
- ))} -
-
- ) - })} -
- -
-
+ {/* dataset config */} - {isModifiedDataset && ( - <> - {t('Datasets')} -
- - - -
- - )} + {isModifiedDataset && {t('Datasets')}} + {isModifiedDataset && } {/* runtime config */} {t('Runtime')} -
- {!!builtInRuntime && ( - -
- { - setType(value) - }} - options={[ - { - label: RuntimeType.BUILTIN, - id: RuntimeType.BUILTIN, - }, - { - label: RuntimeType.OTHER, - id: RuntimeType.OTHER, - }, - ]} - /> - - )} - {type === RuntimeType.BUILTIN && ( - <> - -

- {builtInRuntime} -

- - )} - {(type === RuntimeType.OTHER || !builtInRuntime) && ( - - - - )} -
+ + {/* advanced config */} {t('job.advanced')} {/* debug config */} - - {form.getFieldValue('devMode') &&

{t('job.debug.notice')}

} -
- - - - {form.getFieldValue('devMode') && ( - - { - toaster.positive(t('Copied'), { autoHideDuration: 1000 }) - }} - > - - - - - } - /> - - )} -
-
+ {/* auto release time config */} - {form.getFieldValue('isTimeToLiveInSec') && ( -

{t('job.autorelease.notice')}

- )} -
- - - - {form.getFieldValue('isTimeToLiveInSec') && ( - - t('resource.price.unit.second')} /> - - )} -
+ +
+ ) + return [
-    + + + {(isPrivileged: boolean, isCommunity: boolean) => { if (!isPrivileged) return null @@ -62,7 +71,7 @@ export default function ModelListCard() { kind='tertiary' onClick={() => history.push( - `/projects/${projectId}/new_job/?modelId=${model.id}&handler=serving` + `/projects/${projectId}/new_job/?modelId=${model.id}&modelVersionHandler=serving` ) } > @@ -82,7 +91,7 @@ export default function ModelListCard() { ) }} - , +
, ] }) ?? [] } diff --git a/console/src/pages/Project/JobNewCard.tsx b/console/src/pages/Project/JobNewCard.tsx index b894b66dde..d6c2188b04 100644 --- a/console/src/pages/Project/JobNewCard.tsx +++ b/console/src/pages/Project/JobNewCard.tsx @@ -3,11 +3,14 @@ import Card from '@/components/Card' import useTranslation from '@/hooks/useTranslation' import JobForm from '@job/components/JobForm' import { ICreateJobSchema } from '@job/schemas/job' -import { createJob } from '@job/services/job' +import { createJob, fetchJob } from '@job/services/job' import { useParams } from 'react-router-dom' +import { useQuery } from 'react-query' +import { useQueryArgs } from '@starwhale/core/utils' export default function JobNewCard() { const { projectId } = useParams<{ projectId: string }>() + const { query } = useQueryArgs() const [t] = useTranslation() const handleSubmit = useCallback( async (data: ICreateJobSchema) => { @@ -18,10 +21,16 @@ export default function JobNewCard() { }, [projectId] ) + // rerun job id + const { rid } = query + const info = useQuery(`fetchJobs:${projectId}:${rid}`, () => fetchJob(projectId, rid), { + refetchOnWindowFocus: false, + enabled: !!rid, + }) return ( - + ) } diff --git a/console/vite.config.ts b/console/vite.config.ts index f5e6f504d7..a2d5a7c2e2 100644 --- a/console/vite.config.ts +++ b/console/vite.config.ts @@ -26,6 +26,7 @@ export const alias = { 'js-yaml': path.resolve(__dirname, './node_modules/js-yaml'), 'qs': path.resolve(__dirname, './node_modules/qs'), 'axios': path.resolve(__dirname, './node_modules/axios'), + 'ahooks': path.resolve(__dirname, './node_modules/ahooks'), '@monaco-editor/react': path.resolve(__dirname, './node_modules/@monaco-editor/react'), '@': path.resolve(__dirname, './src'), '@user': path.resolve(__dirname, './src/domain/user'), @@ -38,7 +39,10 @@ export const alias = { '@starwhale/ui': path.resolve(__dirname, './packages/starwhale-ui/src'), '@starwhale/core': path.resolve(__dirname, './packages/starwhale-core/src'), '@starwhale/widgets': path.resolve(__dirname, './packages/starwhale-widgets/src'), + '.*': path.resolve(__dirname, './src'), + '*': path.resolve(__dirname, './node_modules'), } +const projectRootDir = path.resolve(__dirname) let extendProxies = {} // if (process.env.VITE_EXTENDS === 'true') @@ -112,7 +116,9 @@ export default defineConfig(({ mode }) => ({ // minify: false, // sourcemap: true, }, - resolve: { alias }, + resolve: { + alias, + }, plugins: [ // eslint(), react({ diff --git a/console/yarn.lock b/console/yarn.lock index 6588e2b394..49b2bf5877 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -5486,7 +5486,7 @@ jest-matcher-utils "^28.0.0" pretty-format "^28.0.0" -"@types/js-cookie@^2.2.6": +"@types/js-cookie@^2.2.6", "@types/js-cookie@^2.x.x": version "2.2.7" resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3" integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA== @@ -6504,6 +6504,27 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ahooks-v3-count@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ahooks-v3-count/-/ahooks-v3-count-1.0.0.tgz#ddeb392e009ad6e748905b3cbf63a9fd8262ca80" + integrity sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ== + +ahooks@^3.7.8: + version "3.7.8" + resolved "https://registry.yarnpkg.com/ahooks/-/ahooks-3.7.8.tgz#3fa3c491cd153e884a32b0c4192fc72cf84c4332" + integrity sha512-e/NMlQWoCjaUtncNFIZk3FG1ImSkV/JhScQSkTqnftakRwdfZWSw6zzoWSG9OMYqPNs2MguDYBUFFC6THelWXA== + dependencies: + "@babel/runtime" "^7.21.0" + "@types/js-cookie" "^2.x.x" + ahooks-v3-count "^1.0.0" + dayjs "^1.9.1" + intersection-observer "^0.12.0" + js-cookie "^2.x.x" + lodash "^4.17.21" + resize-observer-polyfill "^1.5.1" + screenfull "^5.0.0" + tslib "^2.4.1" + airbnb-js-shims@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/airbnb-js-shims/-/airbnb-js-shims-2.2.1.tgz#db481102d682b98ed1daa4c5baa697a05ce5c040" @@ -9792,6 +9813,11 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +dayjs@^1.9.1: + version "1.11.9" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" + integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA== + debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -13834,6 +13860,11 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== +intersection-observer@^0.12.0: + version "0.12.2" + resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.2.tgz#4a45349cc0cd91916682b1f44c28d7ec737dc375" + integrity sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg== + intl-messageformat@^10.1.0: version "10.5.0" resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.5.0.tgz#86d11b15913ac954075b25253f5e669359f89538" @@ -15079,7 +15110,7 @@ js-base64@^3.7.1: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca" integrity sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA== -js-cookie@^2.2.1: +js-cookie@^2.2.1, js-cookie@^2.x.x: version "2.2.1" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== @@ -21238,7 +21269,7 @@ scoped-regex@^2.0.0: resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-2.1.0.tgz#7b9be845d81fd9d21d1ec97c61a0b7cf86d2015f" integrity sha512-g3WxHrqSWCZHGHlSrF51VXFdjImhwvH8ZO/pryFH56Qi0cDsZfylQa/t0jCzVQFNbNvM00HfHjkDPEuarKDSWQ== -screenfull@^5.1.0: +screenfull@^5.0.0, screenfull@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba" integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA== @@ -23001,7 +23032,7 @@ tslib@^1.8.1, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.4.1: version "2.6.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==