| @@ -62,3 +62,6 @@ mvnw | |||
| *storybook.log | |||
| /react-ui/docs | |||
| /react-ui/types/tsconfig.tsbuildinfo | |||
| /react-ui/storybook-static | |||
| /react-ui/.storybook/scripts | |||
| @@ -4,7 +4,6 @@ export default function(babel) { | |||
| visitor: { | |||
| ImportDeclaration(path) { | |||
| const source = path.node.source.value; | |||
| // console.log("zzzz", source); | |||
| if (source.endsWith('.less')) { | |||
| if (path.node.specifiers.length > 0) { | |||
| path.node.source.value += "?modules"; | |||
| @@ -16,7 +16,11 @@ const config: StorybookConfig = { | |||
| name: '@storybook/react-webpack5', | |||
| options: {}, | |||
| }, | |||
| staticDirs: ['../public', { from: '../docs', to: '/docs' }], | |||
| staticDirs: [ | |||
| '../public', | |||
| { from: '../docs', to: '/docs' }, | |||
| { from: '../docs/index.html', to: '/docs/index.html' }, | |||
| ], | |||
| docs: { | |||
| defaultName: 'Documentation', | |||
| }, | |||
| @@ -0,0 +1,3 @@ | |||
| # Dockerfile | |||
| FROM nginx:alpine | |||
| COPY storybook-static/ /usr/share/nginx/html | |||
| @@ -143,6 +143,11 @@ export default [ | |||
| path: 'compare', | |||
| component: './Experiment/Comparison/index', | |||
| }, | |||
| { | |||
| name: '实验可视化对比', | |||
| path: 'compare-visual', | |||
| component: './Experiment/Aim/index', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -307,7 +312,18 @@ export default [ | |||
| { | |||
| name: '镜像详情', | |||
| path: 'info/:id', | |||
| component: './Mirror/Info', | |||
| routes: [ | |||
| { | |||
| name: '镜像详情', | |||
| path: '', | |||
| component: './Mirror/Info', | |||
| }, | |||
| { | |||
| name: '新增镜像版本', | |||
| path: 'add-version', | |||
| component: './Mirror/Create', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: '创建镜像', | |||
| @@ -16,7 +16,7 @@ | |||
| "docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up", | |||
| "docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro", | |||
| "docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro", | |||
| "docs": "typedoc --entryPointStrategy expand --entryPoints 'src/utils' --skipErrorChecking --out docs", | |||
| "docs": "typedoc", | |||
| "gh-pages": "gh-pages -d dist", | |||
| "i18n-remove": "pro i18n-remove --locale=zh-CN --write", | |||
| "postinstall": "max setup", | |||
| @@ -40,6 +40,7 @@ | |||
| "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", | |||
| "storybook": "storybook dev -p 6006", | |||
| "storybook-build": "storybook build", | |||
| "storybook-deploy": "./.storybook/scripts/upload-deploy.sh", | |||
| "storybook-docs": "storybook dev --docs", | |||
| "storybook-docs-build": "storybook build --docs", | |||
| "test": "jest", | |||
| @@ -4,16 +4,6 @@ | |||
| font-display: swap; | |||
| } | |||
| @font-face { | |||
| font-family: 'TaoBaoMaiCaiTi'; | |||
| src: url('./TaoBaoMaiCaiTi-Regular.woff2') format('woff2'), /* 最优先使用 woff2 */ | |||
| url('./TaoBaoMaiCaiTi-Regular.woff') format('woff'), /* 兼容性较好的 woff */ | |||
| url('./TaoBaoMaiCaiTi-Regular.ttf') format('truetype'), /* ttf 作为备选 */ | |||
| url('./TaoBaoMaiCaiTi-Regular.otf') format('opentype'); /* otf 作为最后选项 */ | |||
| font-display: swap; /* 优化页面加载时的字体显示 */ | |||
| } | |||
| @font-face { | |||
| font-family: 'DingTalk-JinBuTi'; | |||
| src: url('./DingTalk-JinBuTi.woff2') format('woff2'), /* 最优先使用 woff2 */ | |||
| @@ -1,13 +1,16 @@ | |||
| import RightContent from '@/components/RightContent'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { type GlobalInitialState } from '@/types'; | |||
| import { menuItemRender } from '@/utils/menuRender'; | |||
| import type { Settings as LayoutSettings } from '@ant-design/pro-components'; | |||
| import { RuntimeConfig, history } from '@umijs/max'; | |||
| import { RuntimeAntdConfig } from 'umi'; | |||
| import defaultSettings from '../config/defaultSettings'; | |||
| import '../public/fonts/font.css'; | |||
| import { getAccessToken } from './access'; | |||
| import ErrorBoundary from './components/ErrorBoundary'; | |||
| import './dayjsConfig'; | |||
| import { removeAllPageCacheState } from './hooks/pageCacheState'; | |||
| import { removeAllPageCacheState } from './hooks/useCacheState'; | |||
| import { | |||
| getRemoteMenu, | |||
| getRoutersInfo, | |||
| @@ -16,14 +19,9 @@ import { | |||
| setRemoteMenu, | |||
| } from './services/session'; | |||
| import './styles/menu.less'; | |||
| export { requestConfig as request } from './requestConfig'; | |||
| // const isDev = process.env.NODE_ENV === 'development'; | |||
| import { type GlobalInitialState } from '@/types'; | |||
| // import '@/utils/clipboard'; | |||
| import { menuItemRender } from '@/utils/menuRender'; | |||
| import ErrorBoundary from './components/ErrorBoundary'; | |||
| import { needAuth } from './utils'; | |||
| import { gotoLoginPage } from './utils/ui'; | |||
| export { requestConfig as request } from './requestConfig'; | |||
| /** | |||
| * @see https://umijs.org/zh-CN/plugins/plugin-initial-state | |||
| @@ -139,7 +137,6 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||
| onClick: () => { | |||
| // 点击菜单项,删除所有的页面 state 缓存 | |||
| removeAllPageCacheState(); | |||
| // console.log('click menu'); | |||
| }, | |||
| }, | |||
| ...initialState?.settings, | |||
| @@ -2,6 +2,7 @@ import { AvailableRange } from '@/enums'; | |||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||
| import { Flex, Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type CodeConfigItemProps = { | |||
| @@ -10,6 +11,7 @@ type CodeConfigItemProps = { | |||
| }; | |||
| function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { | |||
| const [isEllipsis, setIsEllipsis] = useState(false); | |||
| return ( | |||
| <div className={styles['code-config-item']} onClick={() => onClick?.(item)}> | |||
| <Flex justify="space-between" align="center" style={{ marginBottom: '15px' }}> | |||
| @@ -32,11 +34,20 @@ function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { | |||
| </Flex> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__url']} | |||
| ellipsis={{ rows: 2, tooltip: item.git_url }} | |||
| ellipsis={{ | |||
| rows: 2, | |||
| tooltip: isEllipsis ? item.git_url : false, // 仅当省略时显示 tooltip | |||
| onEllipsis: (ellipsis) => setIsEllipsis(ellipsis), | |||
| }} | |||
| > | |||
| {item.git_url} | |||
| </Typography.Paragraph> | |||
| <div className={styles['code-config-item__branch']}>{item.git_branch}</div> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__branch']} | |||
| ellipsis={{ tooltip: item.git_branch }} | |||
| > | |||
| {item.git_branch} | |||
| </Typography.Paragraph> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,26 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { Tooltip } from 'antd'; | |||
| import styles from './index.less'; | |||
| export type CopyingTextProps = { | |||
| text: string; | |||
| }; | |||
| function CopyingText({ text }: CopyingTextProps) { | |||
| return ( | |||
| <div className={styles['copying-text']}> | |||
| <span className={styles['copying-text__text']}>{text}</span> | |||
| <Tooltip title="复制"> | |||
| <KFIcon | |||
| id="copying" | |||
| data-clipboard-text={text} | |||
| type="icon-fuzhi2" | |||
| className={styles['copying-text__icon']} | |||
| color="#606b7a" | |||
| /> | |||
| </Tooltip> | |||
| </div> | |||
| ); | |||
| } | |||
| export default CopyingText; | |||
| @@ -3,6 +3,7 @@ import KFSpin from '@/components/KFSpin'; | |||
| import { getLabelStudioUrl } from '@/services/developmentEnvironment'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import { FloatButton } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { createPortal } from 'react-dom'; | |||
| @@ -13,6 +14,7 @@ export enum IframePageType { | |||
| AppDevelopment = 'AppDevelopment', // 应用开发 | |||
| DevEnv = 'DevEnv', // 开发环境 | |||
| GitLink = 'GitLink', // git link | |||
| Aim = 'Aim', // 实验对比 | |||
| } | |||
| const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | |||
| @@ -29,12 +31,20 @@ const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | |||
| }); | |||
| case IframePageType.GitLink: // git link | |||
| return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' }); | |||
| case IframePageType.Aim: // Aim | |||
| return () => | |||
| Promise.resolve({ | |||
| code: 200, | |||
| data: SessionStorage.getItem(SessionStorage.aimUrlKey) || '', | |||
| }); | |||
| } | |||
| }; | |||
| type IframePageProps = { | |||
| /** 子系统 */ | |||
| type: IframePageType; | |||
| /** 是否可以在页签上打开 */ | |||
| openInTab?: boolean; | |||
| /** 自定义样式类名 */ | |||
| className?: string; | |||
| /** 自定义样式 */ | |||
| @@ -42,7 +52,7 @@ type IframePageProps = { | |||
| }; | |||
| /** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */ | |||
| function IframePage({ type, className, style }: IframePageProps) { | |||
| function IframePage({ type, openInTab = false, className, style }: IframePageProps) { | |||
| const [iframeUrl, setIframeUrl] = useState(''); | |||
| const [loading, setLoading] = useState(false); | |||
| @@ -68,6 +78,7 @@ function IframePage({ type, className, style }: IframePageProps) { | |||
| <div className={classNames('kf-iframe-page', className)} style={style}> | |||
| {loading && createPortal(<KFSpin size="large" />, document.body)} | |||
| <FullScreenFrame url={iframeUrl} onLoad={hideLoading} onError={hideLoading} /> | |||
| {openInTab && <FloatButton onClick={() => window.open(iframeUrl, '_blank')} />} | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -4,7 +4,7 @@ | |||
| right: 0; | |||
| bottom: 0; | |||
| left: 0; | |||
| z-index: 1001; | |||
| z-index: 1001; // 设置大于 Modal 的 z-index | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| @@ -22,7 +22,7 @@ | |||
| border-radius: 4px; | |||
| &__value { | |||
| .singleLine(); | |||
| //.singleLine(); | |||
| margin-right: 8px; | |||
| font-size: @font-size-input; | |||
| line-height: 1.5714285714285714; | |||
| @@ -6,7 +6,7 @@ | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { CloseOutlined } from '@ant-design/icons'; | |||
| import { ConfigProvider, Form, Input } from 'antd'; | |||
| import { ConfigProvider, Form, Input, Typography } from 'antd'; | |||
| import { RuleObject } from 'antd/es/form'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| @@ -120,7 +120,12 @@ function ParameterInput({ | |||
| > | |||
| {valueObj?.showValue ? ( | |||
| <div className="parameter-input__content"> | |||
| <span className="parameter-input__content__value">{valueObj?.showValue}</span> | |||
| <Typography.Text | |||
| className="parameter-input__content__value" | |||
| ellipsis={{ tooltip: valueObj.showValue }} | |||
| > | |||
| {valueObj.showValue} | |||
| </Typography.Text> | |||
| <CloseOutlined | |||
| className="parameter-input__content__close-icon" | |||
| onClick={handleRemove} | |||
| @@ -1,4 +1,4 @@ | |||
| import { filterResourceStandard, resourceFieldNames } from '@/hooks/resource'; | |||
| import { filterResourceStandard, resourceFieldNames } from '@/hooks/useComputingResource'; | |||
| import { ServiceData } from '@/pages/ModelDeployment/types'; | |||
| import { getDatasetList, getModelList } from '@/services/dataset/index.js'; | |||
| import { getServiceListReq } from '@/services/modelDeployment'; | |||
| @@ -4,7 +4,7 @@ | |||
| * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 | |||
| */ | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Select, type SelectProps } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| @@ -16,13 +16,17 @@ export type ParameterSelectObject = { | |||
| [key: string]: any; | |||
| }; | |||
| export type ParameterSelectDataType = 'dataset' | 'model' | 'service' | 'resource'; | |||
| export interface ParameterSelectProps extends SelectProps { | |||
| /** 类型 */ | |||
| dataType: 'dataset' | 'model' | 'service' | 'resource'; | |||
| dataType: ParameterSelectDataType; | |||
| /** 是否只是展示信息 */ | |||
| display?: boolean; | |||
| /** 值,支持对象,对象必须包含 value */ | |||
| value?: string | ParameterSelectObject; | |||
| /** 用于流水线, 流水线资源规格要求 id 为字符串 */ | |||
| isPipeline?: boolean; | |||
| /** 修改后回调 */ | |||
| onChange?: (value: string | ParameterSelectObject) => void; | |||
| } | |||
| @@ -32,6 +36,7 @@ function ParameterSelect({ | |||
| dataType, | |||
| display = false, | |||
| value, | |||
| isPipeline = false, | |||
| onChange, | |||
| ...rest | |||
| }: ParameterSelectProps) { | |||
| @@ -39,6 +44,12 @@ function ParameterSelect({ | |||
| const propsConfig = paramSelectConfig[dataType]; | |||
| const valueText = typeof value === 'object' && value !== null ? value.value : value; | |||
| const [resourceStandardList] = useComputingResource(); | |||
| const computingResource = isPipeline | |||
| ? resourceStandardList.map((v) => ({ | |||
| ...v, | |||
| id: String(v.id), | |||
| })) | |||
| : resourceStandardList; | |||
| useEffect(() => { | |||
| // 获取下拉数据 | |||
| @@ -56,7 +67,7 @@ function ParameterSelect({ | |||
| getSelectOptions(); | |||
| }, [propsConfig]); | |||
| const selectOptions = dataType === 'resource' ? resourceStandardList : options; | |||
| const selectOptions = dataType === 'resource' ? computingResource : options; | |||
| const handleChange = (text: string) => { | |||
| if (typeof value === 'object' && value !== null) { | |||
| @@ -224,6 +224,8 @@ export class MirrorSelector implements SelectorTypeInfo { | |||
| image_id: parentKey, | |||
| page: 0, | |||
| size: 2000, | |||
| status: 'available', | |||
| state: 1, | |||
| }); | |||
| if (res && res.data) { | |||
| const list = res.data.content || []; | |||
| @@ -2,8 +2,9 @@ import { clearSessionToken } from '@/access'; | |||
| import { setRemoteMenu } from '@/services/session'; | |||
| import { logout } from '@/services/system/auth'; | |||
| import { ClientInfo } from '@/types'; | |||
| import { sleep } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import { gotoLoginPage } from '@/utils/ui'; | |||
| import { gotoLoginPage, oauthLogout } from '@/utils/ui'; | |||
| import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; | |||
| import { setAlpha } from '@ant-design/pro-components'; | |||
| import { useEmotionCss } from '@ant-design/use-emotion-css'; | |||
| @@ -62,7 +63,9 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => { | |||
| * 退出登录,并且将当前的 url 保存 | |||
| */ | |||
| const loginOut = async () => { | |||
| await logout(); | |||
| oauthLogout('http://172.20.32.197:31209/oauth/logout'); | |||
| // 至少 1 秒后跳转,希望子系统能完成注销 | |||
| await Promise.all([logout(), sleep(1000)]); | |||
| clearSessionToken(); | |||
| setRemoteMenu(null); | |||
| gotoLoginPage(); | |||
| @@ -160,3 +160,8 @@ ol { | |||
| input:-webkit-autofill { | |||
| transition: background-color 5000s ease-in-out 0s; | |||
| } | |||
| .ant-typography { | |||
| color: inherit; | |||
| font-size: inherit; | |||
| } | |||
| @@ -1,202 +0,0 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-15 10:01:29 | |||
| * @Description: 自定义 hooks | |||
| */ | |||
| import { FormInstance } from 'antd'; | |||
| import { debounce } from 'lodash'; | |||
| import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |||
| /** | |||
| * 生成具有初始值的状态引用 | |||
| * | |||
| * @param initialValue - 状态的初始值 | |||
| * @return 包含状态值、状态设置函数和可变引用对象的数组 | |||
| */ | |||
| export function useStateRef<T>(initialValue: T) { | |||
| const [value, setValue] = useState(initialValue); | |||
| const ref = useRef(value); | |||
| useEffect(() => { | |||
| ref.current = value; | |||
| }, [value]); | |||
| return [value, setValue, ref] as const; | |||
| } | |||
| /** | |||
| * 生成一个自定义钩子,用于管理模态框的可见性状态。 | |||
| * | |||
| * @param initialValue - 模态框的初始可见性状态。 | |||
| * @return 一个数组,包含可见性状态和打开和关闭模态框的函数。 | |||
| */ | |||
| export function useVisible(initialValue: boolean) { | |||
| const [visible, setVisible] = useState(initialValue); | |||
| const ref = useRef(initialValue); | |||
| const open = useCallback(() => { | |||
| setVisible(true); | |||
| }, []); | |||
| const close = useCallback(() => { | |||
| setVisible(false); | |||
| }, []); | |||
| useEffect(() => { | |||
| ref.current = visible; | |||
| }, [visible]); | |||
| return [visible, open, close, ref] as const; | |||
| } | |||
| type Callback<T> = (state: T) => void; | |||
| /** | |||
| * 生成一个具有回调机制的可变状态值和更新它的函数。 | |||
| * | |||
| * @param initialValue - 初始状态值。 | |||
| * @return 一个元组,包含当前状态值和用于更新状态的函数。 | |||
| */ | |||
| export function useCallbackState<T>(initialValue: T) { | |||
| const [state, _setState] = useState<T>(initialValue); | |||
| const callbackQueue = useRef<Callback<T>[]>([]); | |||
| useEffect(() => { | |||
| callbackQueue.current.forEach((cb) => cb(state)); | |||
| callbackQueue.current = []; | |||
| }, [state]); | |||
| const setState = (newValue: T | ((prevState: T) => T), callback?: Callback<T>) => { | |||
| _setState(newValue); | |||
| if (callback && typeof callback === 'function') { | |||
| callbackQueue.current.push(callback); | |||
| } | |||
| }; | |||
| return [state, setState] as const; | |||
| } | |||
| /** | |||
| * 用于追踪 DOM 元素尺寸的 hook。 | |||
| * | |||
| * @param initialWidth - 初始宽度。 | |||
| * @param initialHeight - 初始高度。 | |||
| * @param deps - 依赖列表。 | |||
| * @return 一个元组,包含 DOM 元素的 ref、当前宽度和当前高度。 | |||
| */ | |||
| export function useDomSize<T extends HTMLElement>( | |||
| initialWidth: number, | |||
| initialHeight: number, | |||
| deps: React.DependencyList = [], | |||
| ) { | |||
| const domRef = useRef<T>(null); | |||
| const [width, setWidth] = useState(initialWidth); | |||
| const [height, setHeight] = useState(initialHeight); | |||
| useEffect(() => { | |||
| const setDomHeight = () => { | |||
| if (domRef.current) { | |||
| setHeight(domRef.current.offsetHeight); | |||
| setWidth(domRef.current.offsetWidth); | |||
| } | |||
| }; | |||
| const debounceFunc = debounce(setDomHeight, 100); | |||
| setDomHeight(); | |||
| window.addEventListener('resize', debounceFunc); | |||
| return () => { | |||
| window.removeEventListener('resize', debounceFunc); | |||
| }; | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, [domRef, ...deps]); | |||
| return [domRef, { width, height }] as const; | |||
| } | |||
| /** | |||
| * 用于在 modal 关闭时重置 Form 表单的 hook。 | |||
| * | |||
| * @param form - Ant Design Form 表单实例 | |||
| * @param open - modal 是否打开 | |||
| */ | |||
| export const useResetFormOnCloseModal = (form: FormInstance, open: boolean) => { | |||
| const prevOpenRef = useRef<boolean>(); | |||
| useEffect(() => { | |||
| prevOpenRef.current = open; | |||
| }, [open]); | |||
| const prevOpen = prevOpenRef.current; | |||
| useEffect(() => { | |||
| if (!open && prevOpen) { | |||
| form.resetFields(); | |||
| } | |||
| }, [form, prevOpen, open]); | |||
| }; | |||
| /** | |||
| * Executes the effect function when the specified condition is true. | |||
| * | |||
| * @param effect - The effect function to execute. | |||
| * @param when - The condition to trigger the effect. | |||
| * @param deps - The dependencies for the effect. | |||
| */ | |||
| export const useEffectWhen = (effect: () => void, when: boolean, deps: React.DependencyList) => { | |||
| const requestFns = useRef<(() => void)[]>([]); | |||
| useEffect(() => { | |||
| if (when) { | |||
| effect(); | |||
| } else { | |||
| requestFns.current.splice(0, 1, effect); | |||
| } | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, deps); | |||
| useEffect(() => { | |||
| if (when) { | |||
| const fn = requestFns.current.pop(); | |||
| fn?.(); | |||
| } | |||
| }, [when]); | |||
| }; | |||
| // 选择、全选操作 | |||
| export const useCheck = <T>(list: T[]) => { | |||
| const [selected, setSelected] = useState<T[]>([]); | |||
| const checked = useMemo(() => { | |||
| return selected.length === list.length && selected.length > 0; | |||
| }, [selected, list]); | |||
| const indeterminate = useMemo(() => { | |||
| return selected.length > 0 && selected.length < list.length; | |||
| }, [selected, list]); | |||
| const checkAll = useCallback(() => { | |||
| setSelected(checked ? [] : list); | |||
| }, [list, checked]); | |||
| const isSingleChecked = useCallback((item: T) => selected.includes(item), [selected]); | |||
| const checkSingle = useCallback( | |||
| (item: T) => { | |||
| setSelected((prev) => { | |||
| if (isSingleChecked(item)) { | |||
| return prev.filter((i) => i !== item); | |||
| } else { | |||
| return [...prev, item]; | |||
| } | |||
| }); | |||
| }, | |||
| [isSingleChecked], | |||
| ); | |||
| return [ | |||
| selected, | |||
| setSelected, | |||
| checked, | |||
| indeterminate, | |||
| checkAll, | |||
| isSingleChecked, | |||
| checkSingle, | |||
| ] as const; | |||
| }; | |||
| @@ -29,13 +29,18 @@ const removeCacheState = (key: string) => { | |||
| } | |||
| }; | |||
| // 移除所有页面 state 缓存 | |||
| /** | |||
| * 移除所有页面 state 缓存 | |||
| */ | |||
| export const removeAllPageCacheState = () => { | |||
| pageKeys.forEach((key) => { | |||
| sessionStorage.removeItem(key); | |||
| }); | |||
| }; | |||
| /** | |||
| * 缓存页面数据 | |||
| */ | |||
| export const useCacheState = () => { | |||
| const { pathname } = window.location; | |||
| const key = 'pagecache:' + pathname; | |||
| @@ -0,0 +1,25 @@ | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| type Callback<T> = (state: T) => void; | |||
| /** | |||
| * 生成一个具有回调机制的可变状态值和更新它的函数。谨慎使用 | |||
| * | |||
| * @param initialValue - 初始状态值。 | |||
| * @return 一个元组,包含当前状态值和用于更新状态的函数。 | |||
| */ | |||
| export function useCallbackState<T>(initialValue: T) { | |||
| const [state, _setState] = useState<T>(initialValue); | |||
| const callbackQueue = useRef<Callback<T>[]>([]); | |||
| useEffect(() => { | |||
| callbackQueue.current.forEach((cb) => cb(state)); | |||
| callbackQueue.current = []; | |||
| }, [state]); | |||
| const setState = (newValue: T | ((prevState: T) => T), callback?: Callback<T>) => { | |||
| _setState(newValue); | |||
| if (callback && typeof callback === 'function') { | |||
| callbackQueue.current.push(callback); | |||
| } | |||
| }; | |||
| return [state, setState] as const; | |||
| } | |||
| @@ -0,0 +1,47 @@ | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| /** | |||
| * 选择、全选操作 | |||
| * @param list - 需要进行选择的列表 | |||
| * @return [选中的项, 设置选中的方法, 是否全选, 是否部分选中, 全选方法,是否单个选中,选中单个方法] | |||
| */ | |||
| export const useCheck = <T>(list: T[]) => { | |||
| const [selected, setSelected] = useState<T[]>([]); | |||
| const checked = useMemo(() => { | |||
| return selected.length === list.length && selected.length > 0; | |||
| }, [selected, list]); | |||
| const indeterminate = useMemo(() => { | |||
| return selected.length > 0 && selected.length < list.length; | |||
| }, [selected, list]); | |||
| const checkAll = useCallback(() => { | |||
| setSelected(checked ? [] : list); | |||
| }, [list, checked]); | |||
| const isSingleChecked = useCallback((item: T) => selected.includes(item), [selected]); | |||
| const checkSingle = useCallback( | |||
| (item: T) => { | |||
| setSelected((prev) => { | |||
| if (isSingleChecked(item)) { | |||
| return prev.filter((i) => i !== item); | |||
| } else { | |||
| return [...prev, item]; | |||
| } | |||
| }); | |||
| }, | |||
| [isSingleChecked], | |||
| ); | |||
| return [ | |||
| selected, | |||
| setSelected, | |||
| checked, | |||
| indeterminate, | |||
| checkAll, | |||
| isSingleChecked, | |||
| checkSingle, | |||
| ] as const; | |||
| }; | |||
| @@ -12,7 +12,7 @@ import { useCallback, useEffect, useState } from 'react'; | |||
| const computingResource: ComputingResource[] = []; | |||
| // 过滤资源规格 | |||
| /** 过滤资源规格 */ | |||
| export const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = ( | |||
| input: string, | |||
| option?: ComputingResource, | |||
| @@ -22,13 +22,13 @@ export const filterResourceStandard: SelectProps<string, ComputingResource>['fil | |||
| ); | |||
| }; | |||
| // 资源规格字段 | |||
| /** 资源规格字段 */ | |||
| export const resourceFieldNames = { | |||
| label: 'description', | |||
| value: 'id', | |||
| }; | |||
| // 获取资源规格 | |||
| /** 获取资源规格 */ | |||
| export function useComputingResource() { | |||
| const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]); | |||
| @@ -0,0 +1,40 @@ | |||
| import { debounce } from 'lodash'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| /** | |||
| * 用于追踪 DOM 元素尺寸的 hook。 | |||
| * | |||
| * @param initialWidth - 初始宽度。 | |||
| * @param initialHeight - 初始高度。 | |||
| * @param deps - 依赖列表。 | |||
| * @return 一个元组,包含 DOM 元素的 ref、当前宽度和当前高度。 | |||
| */ | |||
| export function useDomSize<T extends HTMLElement>( | |||
| initialWidth: number, | |||
| initialHeight: number, | |||
| deps: React.DependencyList = [], | |||
| ) { | |||
| const domRef = useRef<T>(null); | |||
| const [width, setWidth] = useState(initialWidth); | |||
| const [height, setHeight] = useState(initialHeight); | |||
| useEffect(() => { | |||
| const setDomHeight = () => { | |||
| if (domRef.current) { | |||
| setHeight(domRef.current.offsetHeight); | |||
| setWidth(domRef.current.offsetWidth); | |||
| } | |||
| }; | |||
| const debounceFunc = debounce(setDomHeight, 100); | |||
| setDomHeight(); | |||
| window.addEventListener('resize', debounceFunc); | |||
| return () => { | |||
| window.removeEventListener('resize', debounceFunc); | |||
| }; | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, deps); | |||
| return [domRef, { width, height }] as const; | |||
| } | |||
| @@ -1,6 +1,8 @@ | |||
| // 处理 react-draggable 组件拖动结束时,响应了点击事件的 | |||
| import { useState } from 'react'; | |||
| /** | |||
| * 处理 react-draggable 组件拖动结束时,响应了点击事件的 | |||
| */ | |||
| export const useDraggable = (onClick: () => void) => { | |||
| const [isDragging, setIsDragging] = useState(false); | |||
| @@ -0,0 +1,24 @@ | |||
| import { useEffect, useRef } from 'react'; | |||
| /** | |||
| * 当指定的条件为真时执行 Effect 函数。 | |||
| * | |||
| * @param effect - The effect function to execute. | |||
| * @param when - The condition to trigger the effect. | |||
| * @param deps - The dependencies for the effect. | |||
| */ | |||
| export const useEffectWhen = (effect: () => void, when: boolean, deps: React.DependencyList) => { | |||
| const requestFn = useRef<(() => void) | undefined>(effect); | |||
| useEffect(() => { | |||
| requestFn.current = effect; | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, [...deps, effect]); | |||
| useEffect(() => { | |||
| if (when && requestFn.current) { | |||
| requestFn.current(); | |||
| requestFn.current = undefined; | |||
| } | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, [...deps, when]); | |||
| }; | |||
| @@ -0,0 +1,24 @@ | |||
| import { FormInstance } from 'antd'; | |||
| import { useEffect, useRef } from 'react'; | |||
| /** | |||
| * 用于在 modal 关闭时重置 Form 表单的 hook。 | |||
| * | |||
| * @param form - Ant Design Form 表单实例 | |||
| * @param open - modal 是否打开 | |||
| */ | |||
| export const useResetForm = (form: FormInstance, open: boolean) => { | |||
| const prevOpenRef = useRef<boolean>(); | |||
| useEffect(() => { | |||
| prevOpenRef.current = open; | |||
| }, [open]); | |||
| const prevOpen = prevOpenRef.current; | |||
| useEffect(() => { | |||
| if (!open && prevOpen) { | |||
| form.resetFields(); | |||
| } | |||
| }, [form, prevOpen, open]); | |||
| }; | |||
| @@ -0,0 +1,46 @@ | |||
| import { parseJsonText } from '@/utils'; | |||
| import { useCallback, useRef } from 'react'; | |||
| export const useSSE = (onMessage: (data: any) => void) => { | |||
| const evtSourceRef = useRef<EventSource | null>(null); | |||
| const setupSSE = useCallback( | |||
| (name: string, namespace: string) => { | |||
| const { origin } = location; | |||
| const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); | |||
| const evtSource = new EventSource( | |||
| `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, | |||
| { withCredentials: false }, | |||
| ); | |||
| evtSource.onmessage = (event) => { | |||
| const data = event?.data; | |||
| if (!data) { | |||
| return; | |||
| } | |||
| const dataJson = parseJsonText(data); | |||
| if (dataJson) { | |||
| const nodes = dataJson?.result?.object?.status?.nodes; | |||
| if (nodes) { | |||
| onMessage(nodes); | |||
| } | |||
| } | |||
| }; | |||
| evtSource.onerror = (error) => { | |||
| console.error('SSE error: ', error); | |||
| }; | |||
| evtSourceRef.current = evtSource; | |||
| }, | |||
| [onMessage], | |||
| ); | |||
| const closeSSE = useCallback(() => { | |||
| if (evtSourceRef.current) { | |||
| evtSourceRef.current.close(); | |||
| evtSourceRef.current = null; | |||
| } | |||
| }, []); | |||
| return [setupSSE, closeSSE]; | |||
| }; | |||
| @@ -0,0 +1,19 @@ | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| /** | |||
| * 生成具有初始值的状态引用 | |||
| * | |||
| * @param initialValue - 状态的初始值 | |||
| * @return 包含状态值、状态设置函数和可变引用对象的数组 | |||
| */ | |||
| export function useStateRef<T>(initialValue: T) { | |||
| const [value, setValue] = useState(initialValue); | |||
| const ref = useRef(value); | |||
| useEffect(() => { | |||
| ref.current = value; | |||
| }, [value]); | |||
| return [value, setValue, ref] as const; | |||
| } | |||
| @@ -0,0 +1,26 @@ | |||
| import { useCallback, useEffect, useRef, useState } from 'react'; | |||
| /** | |||
| * 生成一个自定义钩子,用于管理模态框的可见性状态。 | |||
| * | |||
| * @param initialValue - 模态框的初始可见性状态。 | |||
| * @return 一个数组,包含 visible、打开函数、关闭函数和 visible ref。 | |||
| */ | |||
| export function useVisible(initialValue: boolean) { | |||
| const [visible, setVisible] = useState(initialValue); | |||
| const ref = useRef(initialValue); | |||
| const open = useCallback(() => { | |||
| setVisible(true); | |||
| }, []); | |||
| const close = useCallback(() => { | |||
| setVisible(false); | |||
| }, []); | |||
| useEffect(() => { | |||
| ref.current = visible; | |||
| }, [visible]); | |||
| return [visible, open, close, ref] as const; | |||
| } | |||
| @@ -261,8 +261,3 @@ | |||
| } | |||
| } | |||
| } | |||
| .ant-typography { | |||
| color: inherit; | |||
| font-size: inherit; | |||
| } | |||
| @@ -186,6 +186,7 @@ function AutoMLInstance() { | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| children: ( | |||
| <ExperimentHistory | |||
| calcMetrics={autoMLInfo?.scoring_functions} | |||
| fileUrl={instanceInfo?.run_history_path} | |||
| isClassification={autoMLInfo?.task_type === AutoMLTaskType.Classification} | |||
| /> | |||
| @@ -1,26 +0,0 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { Typography } from 'antd'; | |||
| import styles from './index.less'; | |||
| export type CopyingTextProps = { | |||
| text: string; | |||
| }; | |||
| function CopyingText({ text }: CopyingTextProps) { | |||
| return ( | |||
| <div className={styles['copying-text']}> | |||
| <Typography.Text ellipsis={{ tooltip: text }} className={styles['copying-text__text']}> | |||
| {text} | |||
| </Typography.Text> | |||
| <KFIcon | |||
| id="copying" | |||
| data-clipboard-text={text} | |||
| type="icon-fuzhi2" | |||
| className={styles['copying-text__icon']} | |||
| color="#606b7a" | |||
| /> | |||
| </div> | |||
| ); | |||
| } | |||
| export default CopyingText; | |||
| @@ -8,8 +8,9 @@ import TrialStatusCell from '../TrialStatusCell'; | |||
| import styles from './index.less'; | |||
| type ExperimentHistoryProps = { | |||
| fileUrl?: string; | |||
| isClassification: boolean; | |||
| calcMetrics?: string; // 计算指标 | |||
| fileUrl?: string; // 文件url | |||
| isClassification: boolean; // 是否是分类 | |||
| }; | |||
| type TableData = { | |||
| @@ -22,7 +23,7 @@ type TableData = { | |||
| althorithm?: string; | |||
| }; | |||
| function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { | |||
| function ExperimentHistory({ calcMetrics, fileUrl, isClassification }: ExperimentHistoryProps) { | |||
| const [tableData, setTableData] = useState<TableData[]>([]); | |||
| useEffect(() => { | |||
| // 获取实验运行历史记录 | |||
| @@ -33,7 +34,7 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||
| const list: TableData[] = data.map((item) => { | |||
| return { | |||
| id: item[0]?.[0], | |||
| accuracy: item[1]?.[5]?.accuracy, | |||
| accuracy: calcMetrics ? item[1]?.[5]?.[calcMetrics] : undefined, | |||
| duration: item[1]?.[5]?.duration, | |||
| train_loss: item[1]?.[5]?.train_loss, | |||
| status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], | |||
| @@ -64,12 +65,6 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||
| width: 80, | |||
| render: tableCellRender(false), | |||
| }, | |||
| { | |||
| title: '准确率', | |||
| dataIndex: 'accuracy', | |||
| key: 'accuracy', | |||
| render: tableCellRender(true), | |||
| }, | |||
| { | |||
| title: '耗时', | |||
| dataIndex: 'duration', | |||
| @@ -103,6 +98,15 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||
| }, | |||
| ]; | |||
| if (calcMetrics) { | |||
| columns.splice(0, 0, { | |||
| title: `指标:${calcMetrics}`, | |||
| dataIndex: 'accuracy', | |||
| key: 'accuracy', | |||
| render: tableCellRender(true), | |||
| }); | |||
| } | |||
| return ( | |||
| <div className={styles['experiment-history']}> | |||
| <div className={styles['experiment-history__content']}> | |||
| @@ -1,6 +1,6 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useCheck } from '@/hooks'; | |||
| import { useCheck } from '@/hooks/useCheck'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { type ExperimentInstance } from '@/types'; | |||
| @@ -58,7 +58,8 @@ function ExperimentInstanceComponent({ | |||
| // 删除实验实例确认 | |||
| const handleRemove = (instance: ExperimentInstance) => { | |||
| modalConfirm({ | |||
| title: '确定删除该条实例吗?', | |||
| title: '删除后,该实验实例将不可恢复', | |||
| content: '是否确认删除?', | |||
| onOk: () => { | |||
| deleteExperimentInstance(instance.id); | |||
| }, | |||
| @@ -96,6 +97,18 @@ function ExperimentInstanceComponent({ | |||
| } | |||
| }; | |||
| // 终止实验实例 | |||
| const handleTerminate = (instance: ExperimentInstance) => { | |||
| modalConfirm({ | |||
| title: '终止后,该次实验运行将不可恢复', | |||
| content: '是否确认终止?', | |||
| isDelete: false, | |||
| onOk: () => { | |||
| terminateExperimentInstance(instance); | |||
| }, | |||
| }); | |||
| }; | |||
| // 终止实验实例 | |||
| const terminateExperimentInstance = async (instance: ExperimentInstance) => { | |||
| const request = config.stopInsReq; | |||
| @@ -188,7 +201,7 @@ function ExperimentInstanceComponent({ | |||
| item.status === ExperimentStatus.Terminated | |||
| } | |||
| icon={<KFIcon type="icon-zhongzhi" />} | |||
| onClick={() => terminateExperimentInstance(item)} | |||
| onClick={() => handleTerminate(item)} | |||
| > | |||
| 终止 | |||
| </Button> | |||
| @@ -7,7 +7,7 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { AutoMLData } from '@/pages/AutoML/types'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import themes from '@/styles/theme.less'; | |||
| @@ -93,17 +93,14 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| const [res] = await to(request(record.id)); | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||
| // 如果是一页的唯一数据,删除后,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| // 避免回到第一页 | |||
| if (tableData.length > 1) { | |||
| setPagination((prev) => ({ | |||
| setPagination((prev) => { | |||
| return { | |||
| ...prev, | |||
| current: 1, | |||
| })); | |||
| } else { | |||
| getAutoMLList(); | |||
| } | |||
| current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, | |||
| }; | |||
| }); | |||
| } | |||
| }; | |||
| @@ -188,6 +185,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| if (expanded) { | |||
| setExpandedRowKeys([record.id]); | |||
| getExperimentInsList(record.id, 0); | |||
| refreshExperimentList(); | |||
| } else { | |||
| setExpandedRowKeys([]); | |||
| } | |||
| @@ -75,8 +75,15 @@ function CodeConfigList() { | |||
| const deleteRecord = async (id: number) => { | |||
| const [res] = await to(deleteCodeConfigReq(id)); | |||
| if (res) { | |||
| getDataList(); | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除后,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| setPagination((prev) => { | |||
| return { | |||
| ...prev, | |||
| current: dataList!.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, | |||
| }; | |||
| }); | |||
| } | |||
| }; | |||
| @@ -70,7 +70,12 @@ function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps | |||
| > | |||
| {item.git_url} | |||
| </Typography.Paragraph> | |||
| <div className={styles['code-config-item__branch']}>{item.git_branch}</div> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__branch']} | |||
| ellipsis={{ tooltip: item.git_branch }} | |||
| > | |||
| {item.git_branch} | |||
| </Typography.Paragraph> | |||
| </div> | |||
| <Flex justify="space-between"> | |||
| <div className={styles['code-config-item__user']}> | |||
| @@ -4,7 +4,12 @@ import KFModal from '@/components/KFModal'; | |||
| import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config'; | |||
| import { addDataset } from '@/services/dataset/index.js'; | |||
| import { to } from '@/utils/promise'; | |||
| import { getFileListFromEvent, limitUploadFileType, validateUploadFiles } from '@/utils/ui'; | |||
| import { | |||
| getFileListFromEvent, | |||
| limitUploadFileType, | |||
| removeUploadedFile, | |||
| validateUploadFiles, | |||
| } from '@/utils/ui'; | |||
| import { | |||
| Button, | |||
| Form, | |||
| @@ -29,11 +34,6 @@ interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> { | |||
| function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) { | |||
| const [uuid] = useState(Date.now()); | |||
| // const [clusterOptions, setClusterOptions] = useState<DictValueEnumObj[]>([]); | |||
| // useEffect(() => { | |||
| // getClusterOptions(); | |||
| // }, []); | |||
| // 上传组件参数 | |||
| const uploadProps: UploadProps = { | |||
| @@ -44,16 +44,9 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| defaultFileList: [], | |||
| accept: '.zip,.tgz', | |||
| beforeUpload: limitUploadFileType('zip,tgz'), | |||
| onRemove: removeUploadedFile, | |||
| }; | |||
| // 获取集群版本数据 | |||
| // const getClusterOptions = async () => { | |||
| // const [res] = await to(getDictSelectOption('available_cluster')); | |||
| // if (res) { | |||
| // setClusterOptions(res); | |||
| // } | |||
| // }; | |||
| // 上传请求 | |||
| const createDataset = async (params: any) => { | |||
| const [res] = await to(addDataset(params)); | |||
| @@ -113,7 +106,7 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入数据名称" showCount allowClear maxLength={50} /> | |||
| <Input placeholder="请输入数据名称" showCount allowClear maxLength={40} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="数据集版本" | |||
| @@ -159,27 +152,42 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| {/* <Form.Item label="集群版本" name="available_cluster"> | |||
| <Select allowClear placeholder="请选择集群版本" options={clusterOptions} /> | |||
| </Form.Item> */} | |||
| <Form.Item | |||
| label="数据集简介" | |||
| label="数据集描述" | |||
| name="description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入数据集简介', | |||
| message: '请输入数据集描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入数据集简介" | |||
| placeholder="请输入数据集描述" | |||
| showCount | |||
| maxLength={200} | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="版本描述" | |||
| name="version_desc" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入版本描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入版本描述" | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| maxLength={200} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="可见性" | |||
| name="is_public" | |||
| @@ -4,7 +4,7 @@ import KFModal from '@/components/KFModal'; | |||
| import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config'; | |||
| import { addModel } from '@/services/dataset/index.js'; | |||
| import { to } from '@/utils/promise'; | |||
| import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui'; | |||
| import { getFileListFromEvent, removeUploadedFile, validateUploadFiles } from '@/utils/ui'; | |||
| import { | |||
| Button, | |||
| Form, | |||
| @@ -37,6 +37,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| Authorization: getAccessToken() || '', | |||
| }, | |||
| defaultFileList: [], | |||
| onRemove: removeUploadedFile, | |||
| }; | |||
| // 上传请求 | |||
| @@ -96,7 +97,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入模型名称" showCount allowClear maxLength={50} /> | |||
| <Input placeholder="请输入模型名称" showCount allowClear maxLength={40} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="模型版本" | |||
| @@ -143,23 +144,41 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="模型简介" | |||
| label="模型描述" | |||
| name="description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入模型简介', | |||
| message: '请输入模型描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入模型简介" | |||
| placeholder="请输入模型描述" | |||
| maxLength={200} | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="版本描述" | |||
| name="version_desc" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入版本描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入版本描述" | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| maxLength={200} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="可见性" | |||
| name="is_public" | |||
| @@ -3,7 +3,7 @@ import KFIcon from '@/components/KFIcon'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config'; | |||
| import { to } from '@/utils/promise'; | |||
| import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui'; | |||
| import { getFileListFromEvent, removeUploadedFile, validateUploadFiles } from '@/utils/ui'; | |||
| import { | |||
| Button, | |||
| Form, | |||
| @@ -50,6 +50,7 @@ function AddVersionModal({ | |||
| defaultFileList: [], | |||
| beforeUpload: config.beforeUpload, | |||
| accept: config.uploadAccept, | |||
| onRemove: removeUploadedFile, | |||
| }; | |||
| // 上传请求 | |||
| @@ -13,6 +13,7 @@ import { | |||
| } from '@/pages/Dataset/config'; | |||
| import GraphLegend from '@/pages/Model/components/GraphLegend'; | |||
| import ModelEvolution from '@/pages/Model/components/ModelEvolution'; | |||
| import { VersionChangedMessage } from '@/utils/constant'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| @@ -124,6 +125,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| onOk: () => { | |||
| getVersionList(true); | |||
| close(); | |||
| window.postMessage({ type: VersionChangedMessage }); | |||
| }, | |||
| }); | |||
| }; | |||
| @@ -170,6 +172,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| getVersionList(true); | |||
| window.postMessage({ type: VersionChangedMessage }); | |||
| } | |||
| }; | |||
| @@ -21,7 +21,7 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ | |||
| value: data.name, | |||
| }, | |||
| { | |||
| label: '版本', | |||
| label: '数据集版本', | |||
| value: data.version, | |||
| }, | |||
| { | |||
| @@ -64,7 +64,7 @@ const getModelDatas = (data: ModelData): BasicInfoData[] => [ | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '版本', | |||
| label: '模型版本', | |||
| value: data.version, | |||
| ellipsis: true, | |||
| }, | |||
| @@ -107,8 +107,15 @@ function ResourceList( | |||
| const request = config.deleteRecord; | |||
| const [res] = await to(request(params)); | |||
| if (res) { | |||
| getDataList(); | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除后,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| setPagination((prev) => { | |||
| return { | |||
| ...prev, | |||
| current: dataList!.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, | |||
| }; | |||
| }); | |||
| } | |||
| }; | |||
| @@ -1,5 +1,5 @@ | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { getAssetIcon } from '@/services/dataset/index.js'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Flex, Tabs, type TabsProps } from 'antd'; | |||
| @@ -1,3 +1,9 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2025-03-24 15:41:42 | |||
| * @Description: 版本文件列表 | |||
| */ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { | |||
| ResourceData, | |||
| @@ -11,7 +11,6 @@ | |||
| text-align: center; | |||
| background: @background; | |||
| border-radius: 4px 4px 0 0; | |||
| .singleLine(); | |||
| } | |||
| .text() { | |||
| @@ -20,7 +19,6 @@ | |||
| font-size: 13px; | |||
| line-height: 22px; | |||
| word-break: break-all; | |||
| .singleLine(); | |||
| } | |||
| .version-container(@background) { | |||
| @@ -88,7 +88,7 @@ function VersionCompareModal({ | |||
| format: formatProject, | |||
| }, | |||
| { | |||
| key: 'description', | |||
| key: 'version_desc', | |||
| text: '版本描述', | |||
| }, | |||
| ] | |||
| @@ -123,7 +123,7 @@ function VersionCompareModal({ | |||
| format: formatTrainTask, | |||
| }, | |||
| { | |||
| key: 'description', | |||
| key: 'version_desc', | |||
| text: '版本描述', | |||
| }, | |||
| ], | |||
| @@ -193,7 +193,14 @@ function VersionCompareModal({ | |||
| ))} | |||
| </div> | |||
| <div className={styles['version-compare__left']}> | |||
| <div className={styles['version-compare__left__title']}>{v1.version}</div> | |||
| <div className={styles['version-compare__left__title']}> | |||
| <Typography.Text | |||
| ellipsis={{ tooltip: v1.version }} | |||
| style={{ width: '100%', lineHeight: 'inherit' }} | |||
| > | |||
| {v1.version} | |||
| </Typography.Text> | |||
| </div> | |||
| {fields.map(({ key, format }) => { | |||
| const text = getValue(v1, key as keyof typeof v1, format); | |||
| return ( | |||
| @@ -203,7 +210,7 @@ function VersionCompareModal({ | |||
| [styles['version-compare__left__text--different']]: isDifferent(key), | |||
| })} | |||
| > | |||
| <Typography.Text ellipsis={{ tooltip: text }}> | |||
| <Typography.Text ellipsis={{ tooltip: text }} style={{ width: '100%' }}> | |||
| {isEmpty(text) ? '--' : text} | |||
| </Typography.Text> | |||
| </div> | |||
| @@ -211,7 +218,14 @@ function VersionCompareModal({ | |||
| })} | |||
| </div> | |||
| <div className={styles['version-compare__right']}> | |||
| <div className={styles['version-compare__right__title']}>{v2.version}</div> | |||
| <div className={styles['version-compare__right__title']}> | |||
| <Typography.Text | |||
| ellipsis={{ tooltip: v2.version }} | |||
| style={{ width: '100%', lineHeight: 'inherit' }} | |||
| > | |||
| {v2.version} | |||
| </Typography.Text> | |||
| </div> | |||
| {fields.map(({ key, format }) => { | |||
| const text = getValue(v2, key as keyof typeof v2, format); | |||
| return ( | |||
| @@ -221,7 +235,7 @@ function VersionCompareModal({ | |||
| [styles['version-compare__right__text--different']]: isDifferent(key), | |||
| })} | |||
| > | |||
| <Typography.Text ellipsis={{ tooltip: text }}> | |||
| <Typography.Text ellipsis={{ tooltip: text }} style={{ width: '100%' }}> | |||
| {isEmpty(text) ? '--' : text} | |||
| </Typography.Text> | |||
| </div> | |||
| @@ -104,16 +104,16 @@ function EditorCreate() { | |||
| <Row gutter={10}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="任务名称" | |||
| label="编辑器名称" | |||
| name="name" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入任务名称', | |||
| message: '请输入编辑器名称', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入任务名称" maxLength={64} showCount allowClear /> | |||
| <Input placeholder="请输入编辑器名称" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| @@ -6,7 +6,9 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { DevEditorStatus } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { DatasetData, ModelData } from '@/pages/Dataset/config'; | |||
| import { | |||
| deleteEditorReq, | |||
| getEditorListReq, | |||
| @@ -14,6 +16,7 @@ import { | |||
| stopEditorReq, | |||
| } from '@/services/developmentEnvironment'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { parseJsonText } from '@/utils'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| @@ -42,6 +45,10 @@ export type EditorData = { | |||
| update_by: string; | |||
| create_time: string; | |||
| url: string; | |||
| computing_resource_id: number; | |||
| dataset?: string | DatasetData; | |||
| model?: string | ModelData; | |||
| image?: string; | |||
| }; | |||
| function EditorList() { | |||
| @@ -56,6 +63,7 @@ function EditorList() { | |||
| pageSize: 10, | |||
| }, | |||
| ); | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| // 获取编辑器列表 | |||
| const getEditorList = useCallback(async () => { | |||
| @@ -66,6 +74,10 @@ function EditorList() { | |||
| const [res] = await to(getEditorListReq(params)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| content.forEach((item: EditorData) => { | |||
| item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null; | |||
| item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null; | |||
| }); | |||
| setTableData(content); | |||
| setTotal(totalElements); | |||
| } | |||
| @@ -80,17 +92,14 @@ function EditorList() { | |||
| const [res] = await to(deleteEditorReq(id)); | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||
| // 如果是一页的唯一数据,删除后,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| // 避免回到第一页 | |||
| if (tableData.length > 1) { | |||
| setPagination((prev) => ({ | |||
| setPagination((prev) => { | |||
| return { | |||
| ...prev, | |||
| current: 1, | |||
| })); | |||
| } else { | |||
| getEditorList(); | |||
| } | |||
| current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, | |||
| }; | |||
| }); | |||
| } | |||
| }; | |||
| @@ -105,11 +114,18 @@ function EditorList() { | |||
| // 停止编辑器 | |||
| const stopEditor = async (id: number) => { | |||
| const [res] = await to(stopEditorReq(id)); | |||
| if (res) { | |||
| message.success('操作成功'); | |||
| getEditorList(); | |||
| } | |||
| modalConfirm({ | |||
| title: '停止后,该编辑器将不可使用', | |||
| content: '是否确认停止?', | |||
| isDelete: false, | |||
| onOk: async () => { | |||
| const [res] = await to(stopEditorReq(id)); | |||
| if (res) { | |||
| message.success('操作成功'); | |||
| getEditorList(); | |||
| } | |||
| }, | |||
| }); | |||
| }; | |||
| // 制作镜像 | |||
| @@ -168,44 +184,72 @@ function EditorList() { | |||
| title: '编辑器名称', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: (text, record) => | |||
| record.url && record.status === DevEditorStatus.Running ? ( | |||
| <a className="kf-table-row-link" onClick={(e) => gotoEditorPage(e, record)}> | |||
| {text} | |||
| </a> | |||
| ) : ( | |||
| <span>{text ?? '--'}</span> | |||
| ), | |||
| }, | |||
| { | |||
| title: '状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: '10%', | |||
| render: EditorStatusCell, | |||
| width: '20%', | |||
| render: (text, record, index) => | |||
| record.url && record.status === DevEditorStatus.Running | |||
| ? tableCellRender<EditorData>(true, TableCellValueType.Link, { | |||
| onClick: (record, e) => gotoEditorPage(e, record), | |||
| })(text, record, index) | |||
| : tableCellRender<EditorData>(true, TableCellValueType.Text)(text, record, index), | |||
| }, | |||
| { | |||
| title: '资源', | |||
| title: '计算资源', | |||
| dataIndex: 'computing_resource', | |||
| key: 'computing_resource', | |||
| width: '20%', | |||
| width: 100, | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '资源规格', | |||
| dataIndex: 'computing_resource_id', | |||
| key: 'computing_resource_id', | |||
| width: '20%', | |||
| render: tableCellRender(true, TableCellValueType.Custom, { | |||
| format: getResourceDescription, | |||
| }), | |||
| }, | |||
| { | |||
| title: '数据集', | |||
| dataIndex: ['dataset', 'showValue'], | |||
| key: 'dataset', | |||
| width: '15%', | |||
| render: tableCellRender(true), | |||
| }, | |||
| { | |||
| title: '模型', | |||
| dataIndex: ['model', 'showValue'], | |||
| key: 'model', | |||
| width: '15%', | |||
| render: tableCellRender(true), | |||
| }, | |||
| { | |||
| title: '镜像', | |||
| dataIndex: ['image'], | |||
| key: 'image', | |||
| width: '15%', | |||
| render: tableCellRender(true), | |||
| }, | |||
| { | |||
| title: '创建者', | |||
| dataIndex: 'update_by', | |||
| key: 'update_by', | |||
| width: '20%', | |||
| render: tableCellRender(), | |||
| width: '15%', | |||
| render: tableCellRender(true), | |||
| }, | |||
| { | |||
| title: '创建时间', | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| width: '20%', | |||
| width: 180, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: 80, | |||
| render: EditorStatusCell, | |||
| }, | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| @@ -20,7 +20,7 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) { | |||
| }), | |||
| ); | |||
| if (res) { | |||
| message.success('创建成功,请到 “AI资产” - “个人镜像” 中查看'); | |||
| message.success('创建成功,请到 “多形态资源库” - “个人镜像” 中查看'); | |||
| onOk?.(); | |||
| } | |||
| }; | |||
| @@ -51,20 +51,20 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) { | |||
| message: '请输入镜像名称', | |||
| }, | |||
| { | |||
| pattern: /^[a-z0-9/_-]*$/, | |||
| message: '只支持小写字母、数字、下划线(_)、中横线(-)、斜杠(/)', | |||
| pattern: /^[a-z0-9/._-]*$/, | |||
| message: '只支持小写字母、数字、点(.)、下划线(_)、中横线(-)、斜杠(/)', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入镜像名称" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="镜像Tag" | |||
| label="镜像版本" | |||
| name="tag_name" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入镜像Tag', | |||
| message: '请输入镜像版本', | |||
| }, | |||
| { | |||
| pattern: /^[a-zA-Z0-9._-]+$/, | |||
| @@ -72,7 +72,7 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) { | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入镜像Tag" maxLength={64} showCount allowClear /> | |||
| <Input placeholder="请输入镜像版本" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="镜像描述" | |||
| @@ -87,7 +87,7 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) { | |||
| <Input.TextArea | |||
| placeholder="请输入镜像描述" | |||
| autoSize={{ minRows: 3, maxRows: 6 }} | |||
| maxLength={256} | |||
| maxLength={128} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| @@ -0,0 +1,12 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2025-03-31 16:38:59 | |||
| * @Description: 实验对比 Aim | |||
| */ | |||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | |||
| function AimPage() { | |||
| return <IframePage type={IframePageType.Aim}></IframePage>; | |||
| } | |||
| export default AimPage; | |||
| @@ -12,8 +12,9 @@ import { | |||
| } from '@/services/experiment'; | |||
| import { tableSorter } from '@/utils'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { useSearchParams } from '@umijs/max'; | |||
| import { useNavigate, useSearchParams } from '@umijs/max'; | |||
| import { App, Button, Table, TablePaginationConfig, TableProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| @@ -46,6 +47,7 @@ function ExperimentComparison() { | |||
| }); | |||
| const { message } = App.useApp(); | |||
| const navigate = useNavigate(); | |||
| const config = comparisonConfig[comparisonType]; | |||
| useEffect(() => { | |||
| @@ -73,7 +75,9 @@ function ExperimentComparison() { | |||
| const [res] = await to(getExpMetricsReq(selectedRowKeys)); | |||
| if (res && res.data) { | |||
| const url = res.data; | |||
| window.open(url, '_blank'); | |||
| // window.open(url, '_blank'); | |||
| SessionStorage.setItem(SessionStorage.aimUrlKey, url); | |||
| navigate('../compare-visual'); | |||
| } | |||
| }; | |||
| @@ -1,5 +1,6 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useStateRef, useVisible } from '@/hooks'; | |||
| import { useStateRef } from '@/hooks/useStateRef'; | |||
| import { useVisible } from '@/hooks/useVisible'; | |||
| import { getExperimentIns } from '@/services/experiment/index.js'; | |||
| import { getWorkflowById } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| @@ -179,11 +180,12 @@ function ExperimentText() { | |||
| if (!statusNode) { | |||
| return; | |||
| } | |||
| const { finishedAt, startedAt, phase, id } = statusNode; | |||
| const { finishedAt, startedAt, phase, id, message } = statusNode; | |||
| workflowNode.experimentStartTime = startedAt; | |||
| workflowNode.experimentEndTime = finishedAt; | |||
| workflowNode.experimentStatus = phase; | |||
| workflowNode.workflowId = id; | |||
| workflowNode.message = message; | |||
| workflowNode.img = phase | |||
| ? `${workflowNode.imgName}-${phase}.png` | |||
| : `${workflowNode.imgName}.png`; | |||
| @@ -3,7 +3,7 @@ import editExperimentIcon from '@/assets/img/edit-experiment.png'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { type PipelineGlobalParam } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Button, Form, Input, Radio, Select, type FormRule } from 'antd'; | |||
| import { Button, Form, Input, Radio, Select, Typography, type FormRule } from 'antd'; | |||
| import { useState } from 'react'; | |||
| import styles from './index.less'; | |||
| @@ -63,13 +63,14 @@ export const getParamRules = (paramType: number, required: boolean = false): For | |||
| }; | |||
| // 根据参数设置 label | |||
| export const getParamType = (param: PipelineGlobalParam): string => { | |||
| export const getParamLabel = (param: PipelineGlobalParam): React.ReactNode => { | |||
| const paramTypes: Readonly<Record<number, string>> = { | |||
| 1: '字符串', | |||
| 2: '整型', | |||
| 3: '布尔类型', | |||
| }; | |||
| return param.param_name + `(${paramTypes[param.param_type]})`; | |||
| const label = param.param_name + `(${paramTypes[param.param_type]})`; | |||
| return <Typography.Text ellipsis={{ tooltip: label }}>{label}</Typography.Text>; | |||
| }; | |||
| function AddExperimentModal({ | |||
| @@ -99,8 +100,8 @@ function AddExperimentModal({ | |||
| }; | |||
| const paramLayout = { | |||
| labelCol: { span: 8 }, | |||
| wrapperCol: { span: 16 }, | |||
| labelCol: { span: 6 }, | |||
| wrapperCol: { span: 18 }, | |||
| }; | |||
| // 除了流水线选择发生变化 | |||
| @@ -157,7 +158,6 @@ function AddExperimentModal({ | |||
| form={form} | |||
| {...layout} | |||
| labelAlign="left" | |||
| labelWrap | |||
| > | |||
| <Form.Item | |||
| label="实验名称" | |||
| @@ -215,9 +215,9 @@ function AddExperimentModal({ | |||
| {...restField} | |||
| {...paramLayout} | |||
| key={key} | |||
| label={getParamType(globalParam[name])} | |||
| label={getParamLabel(globalParam[name])} | |||
| name={[name, 'param_value']} | |||
| rules={getParamRules(globalParam[name]['param_type'])} | |||
| rules={getParamRules(globalParam[name]['param_type'], true)} | |||
| > | |||
| {getParamComponent( | |||
| globalParam[name]['param_type'], | |||
| @@ -13,7 +13,6 @@ | |||
| } | |||
| &__tabs { | |||
| height: calc(100% - 169px); | |||
| :global { | |||
| .ant-tabs-nav { | |||
| padding-left: 24px; | |||
| @@ -35,7 +34,7 @@ | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 15px; | |||
| padding-left: 24px; | |||
| padding: 0 24px; | |||
| color: @text-color; | |||
| font-size: 15px; | |||
| } | |||
| @@ -3,7 +3,7 @@ import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { PipelineNodeModelSerialize } from '@/types'; | |||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | |||
| import { Drawer, Tabs } from 'antd'; | |||
| import { Drawer, Tabs, Typography } from 'antd'; | |||
| import { useMemo } from 'react'; | |||
| import ExperimentParameter from '../ExperimentParameter'; | |||
| import ExperimentResult from '../ExperimentResult'; | |||
| @@ -129,6 +129,14 @@ const ExperimentDrawer = ({ | |||
| '--' | |||
| )} | |||
| </div> | |||
| {instanceNodeData.message && ( | |||
| <div className={styles['experiment-drawer__info']}> | |||
| <div style={{ flex: 'none' }}>消息:</div> | |||
| <Typography.Text ellipsis={{ tooltip: instanceNodeData.message }}> | |||
| {instanceNodeData.message ?? '--'} | |||
| </Typography.Text> | |||
| </div> | |||
| )} | |||
| <div className={styles['experiment-drawer__info']}> | |||
| 启动时间:{formatDate(instanceNodeStartTime)} | |||
| </div> | |||
| @@ -137,7 +145,14 @@ const ExperimentDrawer = ({ | |||
| {elapsedTime(instanceNodeStartTime, instanceNodeEndTime)} | |||
| </div> | |||
| </div> | |||
| <Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} /> | |||
| <Tabs | |||
| defaultActiveKey="1" | |||
| items={items} | |||
| className={styles['experiment-drawer__tabs']} | |||
| style={{ | |||
| height: instanceNodeData.message ? 'calc(100% - 169px - 39px)' : 'calc(100% - 169px)', | |||
| }} | |||
| /> | |||
| </Drawer> | |||
| ); | |||
| }; | |||
| @@ -1,6 +1,6 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useCheck } from '@/hooks'; | |||
| import { useCheck } from '@/hooks/useCheck'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { | |||
| deleteManyExperimentIns, | |||
| @@ -62,7 +62,8 @@ function ExperimentInstanceComponent({ | |||
| // 删除实验实例确认 | |||
| const handleRemove = (instance: ExperimentInstance) => { | |||
| modalConfirm({ | |||
| title: '确定删除该条实例吗?', | |||
| title: '删除后,该实验实例将不可恢复', | |||
| content: '是否确认删除?', | |||
| onOk: () => { | |||
| deleteExperimentInstance(instance.id); | |||
| }, | |||
| @@ -101,7 +102,8 @@ function ExperimentInstanceComponent({ | |||
| // 终止实验实例 | |||
| const handleTerminate = (instance: ExperimentInstance) => { | |||
| modalConfirm({ | |||
| title: '确定要终止此次实验运行吗?', | |||
| title: '终止后,该次实验运行将不可恢复', | |||
| content: '是否确认终止?', | |||
| isDelete: false, | |||
| onOk: () => { | |||
| terminateExperimentInstance(instance); | |||
| @@ -5,7 +5,7 @@ | |||
| */ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useStateRef } from '@/hooks'; | |||
| import { useStateRef } from '@/hooks/useStateRef'; | |||
| import { getExperimentPodsLog } from '@/services/experiment/index.js'; | |||
| import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; | |||
| import { Button } from 'antd'; | |||
| @@ -1,31 +1,14 @@ | |||
| .params_container { | |||
| max-height: 230px; | |||
| padding: 15px 15px 0; | |||
| .params-container { | |||
| max-height: calc(100vh - 300px); | |||
| padding: 24px 24px 0; | |||
| overflow-y: auto; | |||
| border: 1px solid #e6e6e6; | |||
| border-radius: 8px; | |||
| &_line { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 15px; | |||
| &_label { | |||
| width: 180px; | |||
| color: @text-color; | |||
| font-size: 15px; | |||
| } | |||
| &_value { | |||
| flex: 1; | |||
| width: 100px; | |||
| margin-left: 15px; | |||
| padding: 10px 20px; | |||
| color: @text-color; | |||
| font-size: @font-size; | |||
| line-height: 20px; | |||
| background: #f6f6f6; | |||
| border: 1px solid #e0e0e1; | |||
| border-radius: 4px; | |||
| } | |||
| .params-empty { | |||
| :global { | |||
| .kf-empty__image { | |||
| width: 300px; | |||
| } | |||
| } | |||
| } | |||
| @@ -4,9 +4,11 @@ | |||
| * @Description: 查看实验使用的参数 | |||
| */ | |||
| import parameterImg from '@/assets/img/modal-parameter.png'; | |||
| import KFEmpty, { EmptyType } from '@/components/KFEmpty'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { type PipelineGlobalParam } from '@/types'; | |||
| import { getParamType } from '../AddExperimentModal'; | |||
| import { Form } from 'antd'; | |||
| import { getParamComponent, getParamLabel } from '../AddExperimentModal'; | |||
| import styles from './index.less'; | |||
| type ParamsModalProps = { | |||
| @@ -26,14 +28,44 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) { | |||
| cancelButtonProps={{ style: { display: 'none' } }} | |||
| width={825} | |||
| > | |||
| <div className={styles.params_container}> | |||
| {globalParam?.map((item) => ( | |||
| <div key={item.param_name} className={styles.params_container_line}> | |||
| <span className={styles.params_container_line_label}>{getParamType(item)}</span> | |||
| <span className={styles.params_container_line_value}>{item.param_value}</span> | |||
| </div> | |||
| ))} | |||
| </div> | |||
| {Array.isArray(globalParam) && globalParam.length > 0 ? ( | |||
| <div className={styles['params-container']}> | |||
| <Form | |||
| name="view_params_form" | |||
| labelCol={{ span: 6 }} | |||
| wrapperCol={{ span: 18 }} | |||
| initialValues={{ global_param: globalParam }} | |||
| labelAlign="left" | |||
| disabled | |||
| > | |||
| <Form.List name="global_param"> | |||
| {(fields) => | |||
| fields.map(({ key, name, ...restField }) => ( | |||
| <Form.Item | |||
| {...restField} | |||
| key={key} | |||
| name={[name, 'param_value']} | |||
| label={getParamLabel(globalParam[name])} | |||
| > | |||
| {getParamComponent( | |||
| globalParam[name]['param_type'], | |||
| globalParam[name]['is_sensitive'], | |||
| )} | |||
| </Form.Item> | |||
| )) | |||
| } | |||
| </Form.List> | |||
| </Form> | |||
| </div> | |||
| ) : ( | |||
| <KFEmpty | |||
| className={styles['params-empty']} | |||
| type={EmptyType.NoData} | |||
| title="暂无数据" | |||
| content="该流水线没有设置全局参数" | |||
| hasFooter={false} | |||
| /> | |||
| )} | |||
| </KFModal> | |||
| ); | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import { ExperimentStatus, TensorBoardStatus } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { | |||
| deleteExperimentById, | |||
| getExperiment, | |||
| @@ -206,6 +206,7 @@ function Experiment() { | |||
| setExpandedRowKeys(null); | |||
| } else { | |||
| getQueryByExperiment(record.id, 0); | |||
| refreshExperimentList(); | |||
| } | |||
| }; | |||
| @@ -285,8 +286,6 @@ function Experiment() { | |||
| message.success('运行成功'); | |||
| refreshExperimentList(); | |||
| refreshExperimentIns(id); | |||
| } else { | |||
| message.error('运行失败'); | |||
| } | |||
| }; | |||
| @@ -377,6 +376,31 @@ function Experiment() { | |||
| getQueryByExperiment(expandedRowKeys, page); | |||
| }; | |||
| // 处理删除 | |||
| const handleExperimentDelete = (record) => { | |||
| modalConfirm({ | |||
| title: '删除后,该实验将不可恢复', | |||
| content: '是否确认删除?', | |||
| onOk: () => { | |||
| deleteExperimentById(record.id).then((ret) => { | |||
| if (ret.code === 200) { | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除后,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| setPagination((prev) => { | |||
| return { | |||
| ...prev, | |||
| current: experimentList.length === 1 ? Math.max(1, prev.current - 1) : prev.current, | |||
| }; | |||
| }); | |||
| } else { | |||
| message.error(ret.msg); | |||
| } | |||
| }); | |||
| }, | |||
| }); | |||
| }; | |||
| const columns = [ | |||
| { | |||
| title: '实验名称', | |||
| @@ -475,22 +499,7 @@ function Experiment() { | |||
| size="small" | |||
| key="batchRemove" | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| onClick={() => { | |||
| modalConfirm({ | |||
| title: '删除后,该实验将不可恢复', | |||
| content: '是否确认删除?', | |||
| onOk: () => { | |||
| deleteExperimentById(record.id).then((ret) => { | |||
| if (ret.code === 200) { | |||
| message.success('删除成功'); | |||
| getExperimentList(); | |||
| } else { | |||
| message.error(ret.msg); | |||
| } | |||
| }); | |||
| }, | |||
| }); | |||
| }} | |||
| onClick={() => handleExperimentDelete(record)} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| @@ -499,6 +508,7 @@ function Experiment() { | |||
| ), | |||
| }, | |||
| ]; | |||
| return ( | |||
| <div className={styles['experiment-list']}> | |||
| <PageTitle title="实验列表"></PageTitle> | |||
| @@ -1,6 +1,6 @@ | |||
| import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | |||
| import { hyperParameterOptimizedMode } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { | |||
| schedulerAlgorithms, | |||
| @@ -44,7 +44,7 @@ const mirrorRadioItems: KFRadioItem[] = [ | |||
| function MirrorCreate() { | |||
| const navigate = useNavigate(); | |||
| const [form] = Form.useForm(); | |||
| const [nameDisabled, setNameDisabled] = useState(false); | |||
| const [isAddVersion, setIsAddVersion] = useState(false); // 是制作镜像还是新增镜像版本 | |||
| const { message } = App.useApp(); | |||
| const uploadProps: UploadProps = { | |||
| @@ -60,7 +60,7 @@ function MirrorCreate() { | |||
| const name = SessionStorage.getItem(SessionStorage.mirrorNameKey); | |||
| if (name) { | |||
| form.setFieldValue('name', name); | |||
| setNameDisabled(true); | |||
| setIsAddVersion(true); | |||
| } | |||
| return () => { | |||
| SessionStorage.removeItem(SessionStorage.mirrorNameKey); | |||
| @@ -70,32 +70,37 @@ function MirrorCreate() { | |||
| // 创建公网、本地镜像 | |||
| const createPublicMirror = async (formData: FormData) => { | |||
| const upload_type = formData['upload_type']; | |||
| let params; | |||
| if (upload_type === CommonTabKeys.Public) { | |||
| params = { | |||
| const params = { | |||
| ...omit(formData, ['upload_type']), | |||
| upload_type: 0, | |||
| image_type: 0, | |||
| }; | |||
| const [res] = await to(createMirrorReq(params)); | |||
| if (res) { | |||
| message.success('创建成功'); | |||
| navigate(-1); | |||
| } | |||
| } else { | |||
| const fileList = formData['fileList'] ?? []; | |||
| if (validateUploadFiles(fileList)) { | |||
| const file = fileList[0]; | |||
| params = { | |||
| const params = { | |||
| ...omit(formData, ['fileList', 'upload_type']), | |||
| path: file.response.data.url, | |||
| file_size: file.response.data.fileSize, | |||
| file_name: file.response.data.fileName, | |||
| upload_type: 1, | |||
| image_type: 0, | |||
| }; | |||
| const [res] = await to(createMirrorReq(params)); | |||
| if (res) { | |||
| message.success('创建成功'); | |||
| navigate(-1); | |||
| } | |||
| } | |||
| } | |||
| const [res] = await to(createMirrorReq(params)); | |||
| if (res) { | |||
| message.success('创建成功'); | |||
| navigate(-1); | |||
| } | |||
| }; | |||
| // 提交 | |||
| @@ -118,14 +123,16 @@ function MirrorCreate() { | |||
| return true; | |||
| }; | |||
| const descTitle = isAddVersion ? '版本描述' : '镜像描述'; | |||
| return ( | |||
| <div className={styles['mirror-create']}> | |||
| <PageTitle title="创建镜像"></PageTitle> | |||
| <PageTitle title={!isAddVersion ? '创建镜像' : '新增镜像版本'}></PageTitle> | |||
| <div className={styles['mirror-create__content']}> | |||
| <div> | |||
| <Form | |||
| name="mirror-create" | |||
| labelCol={{ flex: '130px' }} | |||
| labelCol={{ flex: '135px' }} | |||
| wrapperCol={{ flex: 1 }} | |||
| labelAlign="left" | |||
| form={form} | |||
| @@ -142,7 +149,7 @@ function MirrorCreate() { | |||
| <Row gutter={10}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="镜像名称及Tag" | |||
| label="镜像名称和版本" | |||
| name="name" | |||
| rules={[ | |||
| { | |||
| @@ -150,15 +157,15 @@ function MirrorCreate() { | |||
| message: '请输入镜像名称', | |||
| }, | |||
| { | |||
| pattern: /^[a-z0-9/_-]*$/, | |||
| message: '只支持小写字母、数字、下划线(_)、中横线(-)、斜杠(/)', | |||
| pattern: /^[a-z0-9/._-]*$/, | |||
| message: '只支持小写字母、数字、点(.)、下划线(_)、中横线(-)、斜杠(/)', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input | |||
| placeholder="请输入镜像名称" | |||
| maxLength={64} | |||
| disabled={nameDisabled} | |||
| disabled={isAddVersion} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| @@ -174,33 +181,33 @@ function MirrorCreate() { | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入镜像Tag', | |||
| message: '请输入镜像版本', | |||
| }, | |||
| { | |||
| pattern: /^[a-zA-Z0-9._-]+$/, | |||
| message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||
| message: '镜像版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入镜像Tag" maxLength={64} showCount allowClear /> | |||
| <Input placeholder="请输入镜像版本" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={10}> | |||
| <Col span={20}> | |||
| <Form.Item | |||
| label="镜像描述" | |||
| label={descTitle} | |||
| name="description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入镜像描述', | |||
| message: `请输入${descTitle}`, | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| placeholder="请输入镜像描述,最长128字符" | |||
| placeholder={`请输入${descTitle}`} | |||
| maxLength={128} | |||
| showCount | |||
| allowClear | |||
| @@ -283,7 +290,7 @@ function MirrorCreate() { | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请上传镜像地址', | |||
| message: '请上传镜像文件', | |||
| }, | |||
| ]} | |||
| > | |||
| @@ -303,7 +310,7 @@ function MirrorCreate() { | |||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }}> | |||
| <Button type="primary" htmlType="submit"> | |||
| 创建镜像 | |||
| 确定 | |||
| </Button> | |||
| <Button | |||
| type="default" | |||
| @@ -7,8 +7,8 @@ import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { MirrorVersionStatus } from '@/enums'; | |||
| import { useDomSize } from '@/hooks'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { useDomSize } from '@/hooks/useDomSize'; | |||
| import { | |||
| deleteMirrorVersionReq, | |||
| getMirrorInfoReq, | |||
| @@ -117,17 +117,14 @@ function MirrorInfo() { | |||
| const [res] = await to(deleteMirrorVersionReq(id)); | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||
| // 如果是一页的唯一数据,删除后,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| // 避免回到第一页 | |||
| if (tableData.length === 1) { | |||
| setPagination((prev) => ({ | |||
| setPagination((prev) => { | |||
| return { | |||
| ...prev, | |||
| current: 1, | |||
| })); | |||
| } else { | |||
| getMirrorVersionList(); | |||
| } | |||
| current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, | |||
| }; | |||
| }); | |||
| } | |||
| }; | |||
| @@ -155,7 +152,7 @@ function MirrorInfo() { | |||
| }; | |||
| const createMirrorVersion = () => { | |||
| navigate(`/dataset/mirror/create`); | |||
| navigate(`add-version`); | |||
| SessionStorage.setItem(SessionStorage.mirrorNameKey, mirrorInfo.name || ''); | |||
| setCacheState({ | |||
| pagination, | |||
| @@ -174,20 +171,27 @@ function MirrorInfo() { | |||
| title: '镜像地址', | |||
| dataIndex: 'url', | |||
| key: 'url', | |||
| render: tableCellRender(), | |||
| width: '25%', | |||
| render: tableCellRender('auto', TableCellValueType.Text, { copyable: true }), | |||
| }, | |||
| { | |||
| title: '版本描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| render: tableCellRender(true), | |||
| }, | |||
| { | |||
| title: '状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: 150, | |||
| width: 100, | |||
| render: MirrorStatusCell, | |||
| }, | |||
| { | |||
| title: '镜像大小', | |||
| dataIndex: 'file_size', | |||
| key: 'file_size', | |||
| width: 150, | |||
| width: 120, | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| @@ -200,7 +204,7 @@ function MirrorInfo() { | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| width: 150, | |||
| width: 120, | |||
| key: 'operation', | |||
| hidden: isPublic, | |||
| render: (_: any, record: MirrorVersionData) => ( | |||
| @@ -5,7 +5,7 @@ | |||
| */ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { deleteMirrorReq, getMirrorListReq } from '@/services/mirror'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { to } from '@/utils/promise'; | |||
| @@ -103,17 +103,14 @@ function MirrorList() { | |||
| const [res] = await to(deleteMirrorReq(id)); | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||
| // 如果是一页的唯一数据,删除后,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| // 避免回到第一页 | |||
| if (tableData.length > 1) { | |||
| setPagination((prev) => ({ | |||
| setPagination((prev) => { | |||
| return { | |||
| ...prev, | |||
| current: 1, | |||
| })); | |||
| } else { | |||
| getMirrorList(); | |||
| } | |||
| current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, | |||
| }; | |||
| }); | |||
| } | |||
| }; | |||
| @@ -128,7 +125,7 @@ function MirrorList() { | |||
| // 查看详情 | |||
| const toDetail = (record: MirrorData) => { | |||
| navigate(`/dataset/mirror/info/${record.id}`); | |||
| navigate(`info/${record.id}`); | |||
| setCacheState({ | |||
| activeTab, | |||
| pagination, | |||
| @@ -149,7 +146,7 @@ function MirrorList() { | |||
| // 创建镜像 | |||
| const createMirror = () => { | |||
| navigate(`/dataset/mirror/create`); | |||
| navigate(`create`); | |||
| SessionStorage.setItem(SessionStorage.mirrorNameKey, ''); | |||
| setCacheState({ | |||
| activeTab, | |||
| @@ -262,7 +259,7 @@ function MirrorList() { | |||
| onClick={createMirror} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 制作镜像 | |||
| 创建镜像 | |||
| </Button> | |||
| )} | |||
| <Button | |||
| @@ -4,12 +4,12 @@ | |||
| * @Description: 模型演化 | |||
| */ | |||
| import { useEffectWhen } from '@/hooks'; | |||
| import { useEffectWhen } from '@/hooks/useEffectWhen'; | |||
| import { getModelAtlasReq } from '@/services/dataset/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { to } from '@/utils/promise'; | |||
| import G6, { G6GraphEvent, Graph, INode } from '@antv/g6'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import { useCallback, useEffect, useRef, useState } from 'react'; | |||
| import NodeTooltips from '../NodeTooltips'; | |||
| import styles from './index.less'; | |||
| @@ -73,17 +73,45 @@ function ModelEvolution({ | |||
| }; | |||
| }, []); | |||
| useEffectWhen( | |||
| () => { | |||
| if (version) { | |||
| getModelAtlas(); | |||
| } else { | |||
| clearGraphData(); | |||
| } | |||
| }, | |||
| isActive, | |||
| [resourceId, version], | |||
| ); | |||
| const getModelAtlas = useCallback(async () => { | |||
| // 请求失败或者版本不存在时,清除图形 | |||
| function clearGraphData() { | |||
| graph.data({ | |||
| nodes: [], | |||
| edges: [], | |||
| }); | |||
| graph.render(); | |||
| graph.fitView(); | |||
| } | |||
| if (!resourceId || !identifier || !version) { | |||
| clearGraphData(); | |||
| return; | |||
| } | |||
| const params = { | |||
| id: resourceId, | |||
| identifier, | |||
| version, | |||
| }; | |||
| const [res] = await to(getModelAtlasReq(params)); | |||
| if (res && res.data) { | |||
| const data = normalizeTreeData(res.data); | |||
| apiData.current = data; | |||
| hierarchyNodes.current = traverseHierarchically(data); | |||
| const graphData = getGraphData(data, hierarchyNodes.current); | |||
| graph.data(graphData); | |||
| graph.render(); | |||
| graph.fitView(); | |||
| setShowNodeTooltip(false); | |||
| setEnterTooltip(false); | |||
| } else { | |||
| clearGraphData(); | |||
| } | |||
| }, [resourceId, identifier, version]); | |||
| useEffectWhen(getModelAtlas, isActive, [resourceId, identifier, version]); | |||
| // 初始化图 | |||
| const initGraph = () => { | |||
| @@ -249,40 +277,6 @@ function ModelEvolution({ | |||
| }, 100); | |||
| }; | |||
| // 获取模型依赖 | |||
| const getModelAtlas = async () => { | |||
| const params = { | |||
| id: resourceId, | |||
| identifier, | |||
| version, | |||
| }; | |||
| const [res] = await to(getModelAtlasReq(params)); | |||
| if (res && res.data) { | |||
| const data = normalizeTreeData(res.data); | |||
| apiData.current = data; | |||
| hierarchyNodes.current = traverseHierarchically(data); | |||
| const graphData = getGraphData(data, hierarchyNodes.current); | |||
| graph.data(graphData); | |||
| graph.render(); | |||
| graph.fitView(); | |||
| setShowNodeTooltip(false); | |||
| setEnterTooltip(false); | |||
| } else { | |||
| clearGraphData(); | |||
| } | |||
| }; | |||
| // 请求失败或者版本不存在时,清除图形 | |||
| function clearGraphData() { | |||
| graph.data({ | |||
| nodes: [], | |||
| edges: [], | |||
| }); | |||
| graph.render(); | |||
| graph.fitView(); | |||
| } | |||
| return ( | |||
| <div className={styles['model-evolution']}> | |||
| <div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div> | |||
| @@ -1,8 +1,9 @@ | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import TableColTitle from '@/components/TableColTitle'; | |||
| import { useCheck } from '@/hooks'; | |||
| import { useCheck } from '@/hooks/useCheck'; | |||
| import { getModelPageVersionsReq, getModelVersionsMetricsReq } from '@/services/dataset'; | |||
| import { tableSorter } from '@/utils'; | |||
| import { VersionChangedMessage } from '@/utils/constant'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender from '@/utils/table'; | |||
| import { Checkbox, Flex, Table, type TablePaginationConfig, type TableProps } from 'antd'; | |||
| @@ -27,10 +28,10 @@ type ModelMetricsProps = { | |||
| resourceId: number; | |||
| identifier: string; | |||
| owner: string; | |||
| version: string; | |||
| version: string; // 当前版本 | |||
| }; | |||
| function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsProps) { | |||
| function ModelMetrics({ resourceId, identifier, owner, version, refreshTag }: ModelMetricsProps) { | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>({ | |||
| current: 1, | |||
| pageSize: 10, | |||
| @@ -59,6 +60,24 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| checkSingleMetrics, | |||
| ] = useCheck(allMetricsNames); | |||
| // 新增,删除版本时,重置分页,然后刷新版本列表 | |||
| useEffect(() => { | |||
| const handleMessage = (e: MessageEvent) => { | |||
| const { type } = e.data; | |||
| if (type === VersionChangedMessage) { | |||
| setPagination({ | |||
| current: 1, | |||
| pageSize: 10, | |||
| }); | |||
| } | |||
| }; | |||
| window.addEventListener('message', handleMessage); | |||
| return () => { | |||
| window.removeEventListener('message', handleMessage); | |||
| }; | |||
| }, []); | |||
| useEffect(() => { | |||
| // 获取模型版本列表,带有参数和指标数据 | |||
| const getModelPageVersions = async () => { | |||
| @@ -128,6 +147,7 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| } | |||
| }; | |||
| // 行勾选 | |||
| const rowSelection: TableProps<TableData>['rowSelection'] = { | |||
| type: 'checkbox', | |||
| fixed: 'left', | |||
| @@ -140,6 +160,7 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| }), | |||
| }; | |||
| // 计算的表格数据 | |||
| const showTableData = useMemo(() => { | |||
| const index = tableData.findIndex((item) => item.name === version); | |||
| if (index !== -1) { | |||
| @@ -8,11 +8,11 @@ import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { CommonTabKeys, serviceTypeOptions } from '@/enums'; | |||
| import { createServiceReq, getServiceInfoReq, updateServiceReq } from '@/services/modelDeployment'; | |||
| import { ServiceCreatedMessage } from '@/utils/constant'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| import { App, Button, Col, Form, Input, Row, Select } from 'antd'; | |||
| import { useEffect } from 'react'; | |||
| import { createServiceVersionMessage } from '../types'; | |||
| import styles from './index.less'; | |||
| // 表单数据 | |||
| @@ -63,7 +63,7 @@ function CreateService() { | |||
| navigate(-1); | |||
| if (!serviceId) { | |||
| setTimeout(() => { | |||
| window.postMessage({ type: createServiceVersionMessage, payload: res.data.id }); | |||
| window.postMessage({ type: ServiceCreatedMessage, payload: res.data.id }); | |||
| }, 500); | |||
| } | |||
| } | |||
| @@ -6,7 +6,7 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import { serviceTypeOptions } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { deleteServiceReq, getServiceListReq } from '@/services/modelDeployment'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { to } from '@/utils/promise'; | |||
| @@ -31,9 +31,9 @@ import { | |||
| CreateServiceVersionFrom, | |||
| ServiceData, | |||
| ServiceOperationType, | |||
| createServiceVersionMessage, | |||
| } from '../types'; | |||
| import styles from './index.less'; | |||
| import { ServiceCreatedMessage } from '@/utils/constant'; | |||
| const allServiceTypeOptions = [{ label: '全部', value: '' }, ...serviceTypeOptions]; | |||
| @@ -95,7 +95,7 @@ function ModelDeployment() { | |||
| useEffect(() => { | |||
| const handleMessage = (e: MessageEvent) => { | |||
| const { type, payload } = e.data; | |||
| if (type === createServiceVersionMessage) { | |||
| if (type === ServiceCreatedMessage) { | |||
| modalConfirm({ | |||
| title: '创建服务成功', | |||
| content: '是否创建服务版本?', | |||
| @@ -119,17 +119,14 @@ function ModelDeployment() { | |||
| const [res] = await to(deleteServiceReq(record.id)); | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||
| // 如果是一页的唯一数据,删除后,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| // 避免回到第一页 | |||
| if (tableData.length > 1) { | |||
| setPagination((prev) => ({ | |||
| setPagination((prev) => { | |||
| return { | |||
| ...prev, | |||
| current: 1, | |||
| })); | |||
| } else { | |||
| getServiceList(); | |||
| } | |||
| current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, | |||
| }; | |||
| }); | |||
| } | |||
| }; | |||
| @@ -18,6 +18,7 @@ | |||
| &__table { | |||
| flex: 1; | |||
| min-height: 0; | |||
| margin-top: 24px; | |||
| } | |||
| } | |||
| @@ -8,8 +8,8 @@ import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { ServiceRunStatus, serviceStatusOptions } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { | |||
| deleteServiceVersionReq, | |||
| getServiceInfoReq, | |||
| @@ -132,18 +132,15 @@ function ServiceInfo() { | |||
| const [res] = await to(deleteServiceVersionReq(record.id)); | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||
| // 如果是一页的唯一数据,删除后,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| // 避免回到第一页 | |||
| if (tableData.length > 1) { | |||
| setPagination((prev) => ({ | |||
| setPagination((prev) => { | |||
| return { | |||
| ...prev, | |||
| current: 1, | |||
| })); | |||
| } else { | |||
| getServiceInfo(); | |||
| getServiceVersions(); | |||
| } | |||
| current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, | |||
| }; | |||
| }); | |||
| getServiceInfo(); | |||
| } | |||
| }; | |||
| @@ -432,7 +429,7 @@ function ServiceInfo() { | |||
| onClick={() => createServiceVersion(ServiceOperationType.Create)} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 新增版本 | |||
| 新增服务版本 | |||
| </Button> | |||
| <Button style={{ marginRight: '15px' }} type="default" onClick={handleVersionCompare}> | |||
| 版本对比 | |||
| @@ -6,14 +6,13 @@ | |||
| flex-direction: column; | |||
| height: calc(100% - 60px); | |||
| margin-top: 10px; | |||
| padding: 30px 30px 0; | |||
| padding: 10px 30px 0; | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| &__tabs { | |||
| flex: 1; | |||
| min-height: 0; | |||
| margin-top: 20px; | |||
| padding-bottom: 10px; | |||
| :global { | |||
| @@ -3,9 +3,9 @@ | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 服务版本详情 | |||
| */ | |||
| import FullScreenFrame from '@/components/FullScreenFrame'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { getServiceVersionInfoReq } from '@/services/modelDeployment'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useParams } from '@umijs/max'; | |||
| @@ -18,6 +18,7 @@ import { ServiceVersionData } from '../types'; | |||
| import styles from './index.less'; | |||
| export enum ModelDeploymentTabKey { | |||
| Basic = 'Basic', // 基本信息 | |||
| Predict = 'Predict', // 预测 | |||
| Guide = 'Guide', // 调用指南 | |||
| Log = 'Log', // 服务日志 | |||
| @@ -43,10 +44,23 @@ function ServiceVersionInfo() { | |||
| }, [id]); | |||
| const tabItems = [ | |||
| { | |||
| key: ModelDeploymentTabKey.Basic, | |||
| label: '基本信息', | |||
| icon: <KFIcon type="icon-jibenxinxi" />, | |||
| children: <VersionBasicInfo info={versionInfo} />, | |||
| }, | |||
| { | |||
| key: ModelDeploymentTabKey.Predict, | |||
| label: '预测', | |||
| icon: <KFIcon type="icon-yuce" />, | |||
| children: ( | |||
| <div style={{ height: '100%', width: '100%' }}> | |||
| {versionInfo?.page_path && ( | |||
| <FullScreenFrame url={versionInfo?.page_path}></FullScreenFrame> | |||
| )} | |||
| </div> | |||
| ), | |||
| }, | |||
| { | |||
| key: ModelDeploymentTabKey.Guide, | |||
| @@ -66,12 +80,6 @@ function ServiceVersionInfo() { | |||
| <div className={styles['service-version-info']}> | |||
| <PageTitle title="服务版本详情"></PageTitle> | |||
| <div className={styles['service-version-info__content']}> | |||
| <SubAreaTitle | |||
| title="基本信息" | |||
| image={require('@/assets/img/mirror-basic.png')} | |||
| style={{ marginBottom: '26px' }} | |||
| ></SubAreaTitle> | |||
| <VersionBasicInfo info={versionInfo} /> | |||
| <div className={styles['service-version-info__content__tabs']}> | |||
| <Tabs items={tabItems} /> | |||
| </div> | |||
| @@ -1,6 +1,6 @@ | |||
| import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; | |||
| import { ServiceRunStatus } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { ServiceVersionData } from '@/pages/ModelDeployment/types'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { formatCodeConfig, formatModel } from '@/utils/format'; | |||
| @@ -79,6 +79,10 @@ function VersionBasicInfo({ info }: BasicInfoProps) { | |||
| label: 'API URL', | |||
| value: info?.url, | |||
| }, | |||
| { | |||
| label: '文档地址', | |||
| value: info?.doc_path, | |||
| }, | |||
| { | |||
| label: '副本数量', | |||
| value: info?.replicas, | |||
| @@ -104,7 +108,14 @@ function VersionBasicInfo({ info }: BasicInfoProps) { | |||
| }, | |||
| ]; | |||
| return <BasicInfo datas={datas} labelWidth={66} labelAlign="justify"></BasicInfo>; | |||
| return ( | |||
| <BasicInfo | |||
| datas={datas} | |||
| labelWidth={66} | |||
| labelAlign="justify" | |||
| style={{ marginTop: 10 }} | |||
| ></BasicInfo> | |||
| ); | |||
| } | |||
| export default VersionBasicInfo; | |||
| @@ -1,6 +1,6 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { ServiceRunStatus } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { type ServiceVersionData } from '@/pages/ModelDeployment/types'; | |||
| import { getServiceVersionCompareReq } from '@/services/modelDeployment'; | |||
| import { isEmpty } from '@/utils'; | |||
| @@ -49,6 +49,8 @@ export type ServiceVersionData = { | |||
| update_time: string; | |||
| create_time: string; | |||
| created_by: string; | |||
| doc_path?: string; // 文档地址 | |||
| page_path?: string; // 预测地址 | |||
| }; | |||
| // 操作类型 | |||
| @@ -63,6 +65,3 @@ export enum CreateServiceVersionFrom { | |||
| CreateService = 'CreateService', // 来自创建服务 | |||
| ServiceInfo = 'ServiceInfo', // 来自服务详情 | |||
| } | |||
| // 去创建服务版本消息 | |||
| export const createServiceVersionMessage = 'createServiceVersion'; | |||
| @@ -1,5 +1,6 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { useStateRef, useVisible } from '@/hooks'; | |||
| import { useStateRef } from '@/hooks/useStateRef'; | |||
| import { useVisible } from '@/hooks/useVisible'; | |||
| import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { fittingString, parseJsonText, s8 } from '@/utils'; | |||
| @@ -136,31 +136,35 @@ const GlobalParamsDrawer = forwardRef( | |||
| cur.global_param?.[name]?.param_type | |||
| } | |||
| > | |||
| {({ getFieldValue }) => ( | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'param_value']} | |||
| label="值" | |||
| rules={getParamRules( | |||
| getFieldValue(['global_param', name, 'param_type']), | |||
| true, | |||
| )} | |||
| > | |||
| {getParamComponent(getFieldValue(['global_param', name, 'param_type']))} | |||
| </Form.Item> | |||
| )} | |||
| </Form.Item> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'is_sensitive']} | |||
| label="脱敏显示" | |||
| rules={[{ required: true, message: '请选择' }]} | |||
| tooltip="展示关联的流水线的参数,脱敏的参数以xxxx展示" | |||
| > | |||
| <Radio.Group> | |||
| <Radio value={1}>是</Radio> | |||
| <Radio value={0}>否</Radio> | |||
| </Radio.Group> | |||
| {({ getFieldValue }) => { | |||
| const type = getFieldValue(['global_param', name, 'param_type']); | |||
| return ( | |||
| <> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'param_value']} | |||
| label="值" | |||
| rules={getParamRules(type, true)} | |||
| > | |||
| {getParamComponent(type)} | |||
| </Form.Item> | |||
| {type !== 3 && ( | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'is_sensitive']} | |||
| label="脱敏显示" | |||
| rules={[{ required: true, message: '请选择' }]} | |||
| tooltip="展示关联的流水线的参数,脱敏的参数以xxxx展示" | |||
| > | |||
| <Radio.Group> | |||
| <Radio value={1}>是</Radio> | |||
| <Radio value={0}>否</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| )} | |||
| </> | |||
| ); | |||
| }} | |||
| </Form.Item> | |||
| <Tooltip title="删除参数"> | |||
| <Button | |||
| @@ -1,7 +1,7 @@ | |||
| import CodeSelectorModal from '@/components/CodeSelectorModal'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; | |||
| import ParameterSelect from '@/components/ParameterSelect'; | |||
| import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect'; | |||
| import ResourceSelectorModal, { | |||
| ResourceSelectorType, | |||
| selectorTypeConfig, | |||
| @@ -520,7 +520,8 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| {item.value.type === 'select' ? ( | |||
| ['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( | |||
| <ParameterSelect | |||
| dataType={item.value.item_type as any} | |||
| isPipeline | |||
| dataType={item.value.item_type as ParameterSelectDataType} | |||
| placeholder={item.value.placeholder} | |||
| /> | |||
| ) : null | |||
| @@ -1,7 +1,7 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { | |||
| addWorkflow, | |||
| cloneWorkflow, | |||
| @@ -11,6 +11,7 @@ import { | |||
| removeWorkflow, | |||
| } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { App, Button, ConfigProvider, Form, Input, Space, Table } from 'antd'; | |||
| @@ -127,6 +128,47 @@ const Pipeline = () => { | |||
| } | |||
| }; | |||
| // 处理删除 | |||
| const handlePipelineDelete = (record) => { | |||
| modalConfirm({ | |||
| title: '删除后,该流水线将不可恢复', | |||
| content: '是否确认删除?', | |||
| onOk: async () => { | |||
| const { id } = record; | |||
| const [res] = await to(removeWorkflow(id)); | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除后,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| setPagination((prev) => { | |||
| return { | |||
| ...prev, | |||
| current: pipeList.length === 1 ? Math.max(1, prev.current - 1) : prev.current, | |||
| }; | |||
| }); | |||
| } | |||
| }, | |||
| }); | |||
| }; | |||
| // 处理复制 | |||
| const handlePipelineCopy = (record) => { | |||
| modalConfirm({ | |||
| title: '确定复制该条流水线吗?', | |||
| okText: '确认', | |||
| cancelText: '取消', | |||
| isDelete: false, | |||
| onOk: async () => { | |||
| const { id } = record; | |||
| const [res] = await to(cloneWorkflow(id)); | |||
| if (res) { | |||
| message.success('复制成功'); | |||
| getList(); | |||
| } | |||
| }, | |||
| }); | |||
| }; | |||
| // 当前页面切换 | |||
| const paginationChange = async (current, pageSize) => { | |||
| setPagination({ | |||
| @@ -199,30 +241,7 @@ const Pipeline = () => { | |||
| size="small" | |||
| key="clone" | |||
| icon={<KFIcon type="icon-fuzhi" />} | |||
| onClick={async () => { | |||
| modalConfirm({ | |||
| title: '确定复制该条流水线吗?', | |||
| okText: '确认', | |||
| cancelText: '取消', | |||
| isDelete: false, | |||
| onOk: () => { | |||
| cloneWorkflow(record.id).then((ret) => { | |||
| if (ret.code === 200) { | |||
| message.success('复制成功'); | |||
| getList(); | |||
| } else { | |||
| message.error('复制失败'); | |||
| } | |||
| }); | |||
| // if (success) { | |||
| // if (actionRef.current) { | |||
| // actionRef.current.reload(); | |||
| // } | |||
| // } | |||
| }, | |||
| }); | |||
| }} | |||
| onClick={() => handlePipelineCopy(record)} | |||
| > | |||
| 复制 | |||
| </Button> | |||
| @@ -238,28 +257,7 @@ const Pipeline = () => { | |||
| size="small" | |||
| key="batchRemove" | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| onClick={() => { | |||
| modalConfirm({ | |||
| title: '删除后,该流水线将不可恢复', | |||
| content: '是否确认删除?', | |||
| onOk: () => { | |||
| removeWorkflow(record.id).then((ret) => { | |||
| if (ret.code === 200) { | |||
| message.success('删除成功'); | |||
| getList(); | |||
| } else { | |||
| message.error(ret.msg); | |||
| } | |||
| }); | |||
| // if (success) { | |||
| // if (actionRef.current) { | |||
| // actionRef.current.reload(); | |||
| // } | |||
| // } | |||
| }, | |||
| }); | |||
| }} | |||
| onClick={() => handlePipelineDelete(record)} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| @@ -25,19 +25,19 @@ enum TaskType { | |||
| const taskTypeOptions = [ | |||
| { | |||
| value: 'dev_environment', | |||
| value: TaskType.DevEnvironment, | |||
| label: '开发环境', | |||
| }, | |||
| { | |||
| value: 'workflow', | |||
| value: TaskType.Workflow, | |||
| label: '实验', | |||
| }, | |||
| { | |||
| value: 'ray', | |||
| value: TaskType.Ray, | |||
| label: '超参数自动寻优', | |||
| }, | |||
| { | |||
| value: 'service', | |||
| value: TaskType.Service, | |||
| label: '服务', | |||
| }, | |||
| ]; | |||
| @@ -188,7 +188,7 @@ function PointsDetail() { | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '描述', | |||
| title: '资源规格', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| render: tableCellRender(true), | |||
| @@ -216,6 +216,11 @@ function PointsDetail() { | |||
| label: '进行中', | |||
| color: themes['primaryColor'], | |||
| }, | |||
| { | |||
| value: 2, | |||
| label: '准备中', | |||
| color: themes['pendingColor'], | |||
| }, | |||
| ]), | |||
| }, | |||
| ]; | |||
| @@ -17,6 +17,7 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => { | |||
| const [form] = Form.useForm(); | |||
| const loginPassword = Form.useWatch('password', form); | |||
| const userId = props.values.userId; | |||
| const originPassword = props.values.originPassword; | |||
| const intl = useIntl(); | |||
| const handleOk = () => { | |||
| @@ -26,7 +27,7 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => { | |||
| props.onCancel(); | |||
| }; | |||
| const handleFinish = async (values: Record<string, any>) => { | |||
| props.onSubmit({ ...values, userId } as FormValueType); | |||
| props.onSubmit({ password: values.password, userId, originPassword } as FormValueType); | |||
| }; | |||
| const checkPassword = (rule: any, value: string) => { | |||
| @@ -63,8 +63,8 @@ const UserForm: React.FC<UserFormProps> = (props) => { | |||
| loginIp: props.values.loginIp, | |||
| loginDate: props.values.loginDate, | |||
| remark: props.values.remark, | |||
| gitLinkUsername: props.values.gitLinkUsername, | |||
| gitLinkPassword: props.values.gitLinkPassword, | |||
| // gitLinkUsername: props.values.gitLinkUsername, | |||
| // gitLinkPassword: props.values.gitLinkPassword, | |||
| credit: props.values.credit, | |||
| }); | |||
| }, [form, props, statusOptions]); | |||
| @@ -80,6 +80,7 @@ const UserForm: React.FC<UserFormProps> = (props) => { | |||
| const params = { | |||
| ...values, | |||
| userId: props.values.userId, | |||
| originPassword: props.values.originPassword, | |||
| }; | |||
| props.onSubmit(params as UserFormData); | |||
| }; | |||
| @@ -150,7 +151,7 @@ const UserForm: React.FC<UserFormProps> = (props) => { | |||
| colProps={{ md: 12, xl: 12 }} | |||
| rules={[ | |||
| { | |||
| required: false, | |||
| required: true, | |||
| message: <FormattedMessage id="请输入手机号码!" defaultMessage="请输入手机号码!" />, | |||
| }, | |||
| { | |||
| @@ -174,7 +175,7 @@ const UserForm: React.FC<UserFormProps> = (props) => { | |||
| colProps={{ md: 12, xl: 12 }} | |||
| rules={[ | |||
| { | |||
| required: false, | |||
| required: true, | |||
| message: <FormattedMessage id="请输入用户邮箱!" defaultMessage="请输入用户邮箱!" />, | |||
| }, | |||
| { | |||
| @@ -194,7 +195,7 @@ const UserForm: React.FC<UserFormProps> = (props) => { | |||
| id: 'system.user.user_name', | |||
| defaultMessage: '用户账号', | |||
| })} | |||
| hidden={userId} | |||
| disabled={!!props.values.userId} | |||
| placeholder="请输入用户账号" | |||
| colProps={{ md: 12, xl: 12 }} | |||
| rules={[ | |||
| @@ -202,9 +203,9 @@ const UserForm: React.FC<UserFormProps> = (props) => { | |||
| required: true, | |||
| }, | |||
| { | |||
| pattern: /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/, | |||
| pattern: /^[a-zA-Z](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/, | |||
| message: | |||
| '只能包含数字,字母,下划线(_),中横线(-),英文句号(.),且必须以数字或字母开头与结尾', | |||
| '只能包含数字,字母,下划线(_),中横线(-),英文句号(.),且必须以字母开头,数字或字母结尾', | |||
| }, | |||
| ]} | |||
| /> | |||
| @@ -214,14 +215,23 @@ const UserForm: React.FC<UserFormProps> = (props) => { | |||
| id: 'system.user.password', | |||
| defaultMessage: '密码', | |||
| })} | |||
| hidden={userId} | |||
| placeholder="请输入密码" | |||
| colProps={{ md: 12, xl: 12 }} | |||
| fieldProps={{ | |||
| autoComplete: 'new-password', | |||
| }} | |||
| allowClear | |||
| rules={props.values.userId ? [] : [{ required: true, message: '请输入密码!' }]} | |||
| rules={ | |||
| props.values.userId | |||
| ? [] | |||
| : [ | |||
| { required: true, message: '请输入密码!' }, | |||
| { | |||
| pattern: /^[A-Za-z0-9!"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]{8,16}$/, | |||
| message: '密码长度为8 ~ 16位,只支持字母数字和符号', | |||
| }, | |||
| ] | |||
| } | |||
| /> | |||
| <ProFormSelect | |||
| valueEnum={sexOptions} | |||
| @@ -279,7 +289,7 @@ const UserForm: React.FC<UserFormProps> = (props) => { | |||
| colProps={{ md: 12, xl: 12 }} | |||
| rules={[{ required: true, message: '请选择角色!' }]} | |||
| /> | |||
| <ProFormText | |||
| {/* <ProFormText | |||
| name="gitLinkUsername" | |||
| label="Git 用户名" | |||
| placeholder="请输入 Git 用户名" | |||
| @@ -300,17 +310,25 @@ const UserForm: React.FC<UserFormProps> = (props) => { | |||
| autoComplete: 'new-password', | |||
| }} | |||
| rules={props.values.userId ? [] : [{ required: true, message: '请输入 Git 密码!' }]} | |||
| /> | |||
| /> */} | |||
| <ProFormDigit | |||
| name="credit" | |||
| label="算力积分" | |||
| placeholder="请输入算力积分" | |||
| colProps={{ xs: 24, md: 12, xl: 12 }} | |||
| max={100000} | |||
| min={0} | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入算力积分', | |||
| }, | |||
| { | |||
| type: 'number', | |||
| min: 0, | |||
| max: 100000, | |||
| message: '请输入0 ~ 100000之间的数', | |||
| }, | |||
| ]} | |||
| /> | |||
| <ProFormTextArea | |||
| @@ -4,7 +4,6 @@ import { getRoleList } from '@/services/system/role'; | |||
| import { | |||
| addUser, | |||
| changeUserStatus, | |||
| exportUser, | |||
| getDeptTree, | |||
| getUser, | |||
| getUserList, | |||
| @@ -13,6 +12,7 @@ import { | |||
| updateAuthRole, | |||
| updateUser, | |||
| } from '@/services/system/user'; | |||
| import { downloadXlsx } from '@/utils/downloadfile'; | |||
| import { | |||
| DeleteOutlined, | |||
| DownOutlined, | |||
| @@ -132,15 +132,12 @@ const handleRemoveOne = async (selectedRow: API.System.User) => { | |||
| /** | |||
| * 导出数据 | |||
| * | |||
| * | |||
| */ | |||
| const handleExport = async () => { | |||
| const handleExport = async (deptId: string) => { | |||
| const hide = message.loading('正在导出'); | |||
| try { | |||
| await exportUser(); | |||
| await downloadXlsx('/api/system/user/export', 'POST', { data: { deptId: deptId } }); | |||
| hide(); | |||
| message.success('导出成功'); | |||
| return true; | |||
| } catch (error) { | |||
| hide(); | |||
| @@ -470,7 +467,7 @@ const UserTableList: React.FC = () => { | |||
| key="export" | |||
| hidden={!access.hasPerms('system:user:export')} | |||
| onClick={async () => { | |||
| handleExport(); | |||
| handleExport(selectDept.id); | |||
| }} | |||
| > | |||
| <PlusOutlined />{' '} | |||
| @@ -563,7 +560,7 @@ const UserTableList: React.FC = () => { | |||
| /> | |||
| <ResetPwd | |||
| onSubmit={async (values: any) => { | |||
| const success = await resetUserPwd(values.userId, values.password); | |||
| const success = await resetUserPwd(values); | |||
| if (success) { | |||
| setResetPwdModalVisible(false); | |||
| setSelectedRows([]); | |||
| @@ -581,7 +578,7 @@ const UserTableList: React.FC = () => { | |||
| /> | |||
| <AuthRoleForm | |||
| onSubmit={async (values: any) => { | |||
| const success = await updateAuthRole(values); | |||
| const success = await updateAuthRole(currentRow!.userId, values.roleIds); | |||
| if (success) { | |||
| setAuthRoleModalVisible(false); | |||
| setSelectedRows([]); | |||
| @@ -7,7 +7,7 @@ | |||
| // 媒体查询 | |||
| @media screen and (max-width: 1600px) { | |||
| flex: 1 1 content; | |||
| flex: auto; | |||
| } | |||
| &__icon { | |||
| @@ -24,8 +24,7 @@ | |||
| &__count { | |||
| color: @text-color; | |||
| font-weight: 700; | |||
| font-size: 25px; | |||
| font-size: 26px; | |||
| font-family: DingTalk-JinBuTi; | |||
| } | |||
| } | |||
| @@ -3,9 +3,9 @@ | |||
| flex: none; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| width: 326px; | |||
| height: 228px; | |||
| padding: 0 20px; | |||
| .backgroundFullImage(url(@/assets/img/user-points-bg.png)); | |||
| &__label { | |||
| @@ -16,12 +16,15 @@ | |||
| } | |||
| &__value { | |||
| width: 100%; | |||
| margin-top: 8px; | |||
| margin-bottom: 12px; | |||
| color: @primary-color; | |||
| font-size: 36px; | |||
| font-family: DingTalk-JinBuTi; | |||
| line-height: 43px; | |||
| text-align: center; | |||
| .singleLine(); | |||
| } | |||
| &__button { | |||
| @@ -2,6 +2,7 @@ import { PointsStatistics } from '@/pages/Points/index'; | |||
| import { getPointsStatisticsReq } from '@/services/points'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { Typography } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| @@ -23,8 +24,13 @@ function UserPoints() { | |||
| return ( | |||
| <div className={styles['user-points']}> | |||
| <span className={styles['user-points__label']}>当前可用算力积分</span> | |||
| <span className={styles['user-points__value']}>{statistics?.userCredit ?? '--'}</span> | |||
| <div className={styles['user-points__label']}>当前可用算力积分</div> | |||
| <Typography.Paragraph | |||
| className={styles['user-points__value']} | |||
| ellipsis={{ tooltip: statistics?.userCredit ?? '--' }} | |||
| > | |||
| {statistics?.userCredit ?? '--'} | |||
| </Typography.Paragraph> | |||
| <div | |||
| className={styles['user-points__button']} | |||
| onClick={() => { | |||
| @@ -27,6 +27,7 @@ | |||
| &__statistics { | |||
| flex: none; | |||
| min-width: 431px; | |||
| background: linear-gradient( | |||
| 123.08deg, | |||
| rgba(138, 138, 138, 0.06) 1.32%, | |||
| @@ -36,7 +37,7 @@ | |||
| // 媒体查询 | |||
| @media screen and (max-width: 1600px) { | |||
| flex: 1 1 content; | |||
| flex: 1; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| import { useDraggable } from '@/hooks/draggable'; | |||
| import { useDraggable } from '@/hooks/useDraggable'; | |||
| import { getWorkspaceOverviewReq } from '@/services/workspace'; | |||
| import { ExperimentInstance } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| @@ -53,7 +53,7 @@ export const requestConfig: RequestConfig = { | |||
| ], | |||
| responseInterceptors: [ | |||
| [ | |||
| (response: AxiosResponse) => { | |||
| async (response: AxiosResponse) => { | |||
| const { status, data, config } = response || {}; | |||
| const options = config as RequestOptions; | |||
| const skipErrorHandler = options?.skipErrorHandler; | |||
| @@ -63,20 +63,45 @@ export const requestConfig: RequestConfig = { | |||
| Loading.hide(); | |||
| } | |||
| if (status >= 200 && status < 300) { | |||
| if (status === 204) { | |||
| // 无内容或者无需验证 | |||
| if (status === 204 || skipValidating) { | |||
| return response; | |||
| } else if (data && (skipValidating || data instanceof Blob || data.code === 200)) { | |||
| } | |||
| if (data && data.code === 200) { | |||
| return response; | |||
| } | |||
| // Blob 数据 | |||
| if (data && data instanceof Blob && data.size > 0) { | |||
| // 下载文件失败时,返回的是 JSON 数据,格式为:{code: 500, msg: "xxx"} | |||
| if (data.type === 'application/json') { | |||
| try { | |||
| const text = await data.text(); | |||
| const json = JSON.parse(text); | |||
| if (json.code === 500) { | |||
| popupError(json.msg || '请求失败', skipErrorHandler); | |||
| return Promise.reject(json); | |||
| } | |||
| } catch (error) { | |||
| console.error('JSON 解析失败', error); | |||
| } | |||
| } | |||
| return response; | |||
| } else if (data && data.code === 401) { | |||
| } | |||
| // Token 失效 | |||
| if (data && data.code === 401) { | |||
| clearSessionToken(); | |||
| setRemoteMenu(null); | |||
| gotoLoginPage(false); | |||
| popupError('请重新登录'); | |||
| return Promise.reject(response); | |||
| } else { | |||
| popupError(data?.msg ?? '请求失败', skipErrorHandler); | |||
| return Promise.reject(response); | |||
| } | |||
| popupError(data?.msg ?? '请求失败', skipErrorHandler); | |||
| return Promise.reject(response); | |||
| } else { | |||
| popupError('请求失败', skipErrorHandler); | |||
| return Promise.reject(response); | |||