| @@ -1,11 +1,22 @@ | |||||
| .code-config-item { | .code-config-item { | ||||
| position: relative; | position: relative; | ||||
| width: calc(25% - 7.5px); | |||||
| width: calc(33.33% - 7px); | |||||
| padding: 15px; | padding: 15px; | ||||
| background-color: .addAlpha(@primary-color, 0.04) []; | background-color: .addAlpha(@primary-color, 0.04) []; | ||||
| border: 1px solid transparent; | border: 1px solid transparent; | ||||
| border-radius: 4px; | border-radius: 4px; | ||||
| cursor: pointer; | |||||
| &__checkbox { | |||||
| flex: 1; | |||||
| min-width: 0; | |||||
| :global { | |||||
| .ant-checkbox + span { | |||||
| flex: 1; | |||||
| min-width: 0; | |||||
| } | |||||
| } | |||||
| } | |||||
| &__name { | &__name { | ||||
| margin-right: 8px; | margin-right: 8px; | ||||
| @@ -38,6 +49,8 @@ | |||||
| margin-bottom: 10px !important; | margin-bottom: 10px !important; | ||||
| color: @text-color-secondary; | color: @text-color-secondary; | ||||
| font-size: 13px; | font-size: 13px; | ||||
| cursor: pointer; | |||||
| word-break: break-all; | |||||
| } | } | ||||
| &__branch { | &__branch { | ||||
| @@ -46,11 +59,17 @@ | |||||
| } | } | ||||
| &:hover { | &:hover { | ||||
| background-color: .addAlpha(@primary-color, 0.08) []; | |||||
| } | |||||
| &--active { | |||||
| border-color: @primary-color; | border-color: @primary-color; | ||||
| box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1); | box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1); | ||||
| } | } | ||||
| &:hover &__name { | |||||
| &--active &__name { | |||||
| color: @primary-color; | color: @primary-color; | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,25 +1,51 @@ | |||||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | import { type CodeConfigData } from '@/pages/CodeConfig/List'; | ||||
| import { Flex, Typography } from 'antd'; | |||||
| import { getGitUrl } from '@/utils'; | |||||
| import { Checkbox, Flex, Typography } from 'antd'; | |||||
| import { type CheckboxChangeEvent } from 'antd/es/checkbox'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useState } from 'react'; | import { useState } from 'react'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type CodeConfigItemProps = { | type CodeConfigItemProps = { | ||||
| item: CodeConfigData; | item: CodeConfigData; | ||||
| onClick?: (item: CodeConfigData) => void; | |||||
| checked: boolean; | |||||
| onChange?: (item: CodeConfigData, checked: boolean) => void; | |||||
| }; | }; | ||||
| function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { | |||||
| function CodeConfigItem({ item, checked, onChange }: CodeConfigItemProps) { | |||||
| const [isEllipsis, setIsEllipsis] = useState(false); | const [isEllipsis, setIsEllipsis] = useState(false); | ||||
| const openProject = (e: React.MouseEvent<HTMLElement, MouseEvent>) => { | |||||
| e.stopPropagation(); | |||||
| const { git_url, git_branch } = item; | |||||
| const url = getGitUrl(git_url, git_branch); | |||||
| window.open(url, '_blank'); | |||||
| }; | |||||
| const handleChange = (e: CheckboxChangeEvent) => { | |||||
| onChange?.(item, e.target.checked); | |||||
| }; | |||||
| return ( | return ( | ||||
| <div className={styles['code-config-item']} onClick={() => onClick?.(item)}> | |||||
| <div | |||||
| id={`code-config-item-${item.id}`} | |||||
| className={classNames(styles['code-config-item'], { | |||||
| [styles['code-config-item--active']]: checked, | |||||
| })} | |||||
| > | |||||
| <Flex justify="space-between" align="center" style={{ marginBottom: '15px' }}> | <Flex justify="space-between" align="center" style={{ marginBottom: '15px' }}> | ||||
| <Typography.Paragraph | |||||
| className={styles['code-config-item__name']} | |||||
| ellipsis={{ tooltip: item.code_repo_name }} | |||||
| <Checkbox | |||||
| className={styles['code-config-item__checkbox']} | |||||
| checked={checked} | |||||
| onChange={handleChange} | |||||
| > | > | ||||
| {item.code_repo_name} | |||||
| </Typography.Paragraph> | |||||
| <Typography.Paragraph | |||||
| className={styles['code-config-item__name']} | |||||
| ellipsis={{ tooltip: item.code_repo_name }} | |||||
| > | |||||
| {item.code_repo_name} | |||||
| </Typography.Paragraph> | |||||
| </Checkbox> | |||||
| <div | <div | ||||
| className={classNames( | className={classNames( | ||||
| styles['code-config-item__tag'], | styles['code-config-item__tag'], | ||||
| @@ -35,9 +61,10 @@ function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { | |||||
| className={styles['code-config-item__url']} | className={styles['code-config-item__url']} | ||||
| ellipsis={{ | ellipsis={{ | ||||
| rows: 2, | rows: 2, | ||||
| tooltip: isEllipsis ? item.git_url : false, // 仅当省略时显示 tooltip | |||||
| onEllipsis: (ellipsis) => setIsEllipsis(ellipsis), | |||||
| tooltip: isEllipsis ? item.git_url : false, | |||||
| onEllipsis: (ellipsis) => setIsEllipsis(ellipsis), // 必须这样,不然不能省略 | |||||
| }} | }} | ||||
| onClick={openProject} | |||||
| > | > | ||||
| {item.git_url} | {item.git_url} | ||||
| </Typography.Paragraph> | </Typography.Paragraph> | ||||
| @@ -18,7 +18,19 @@ export { | |||||
| type ParameterInputValue, | type ParameterInputValue, | ||||
| } from '../ParameterInput'; | } from '../ParameterInput'; | ||||
| type CodeSelectProps = ParameterInputProps; | |||||
| export type CodeSelectProps = ParameterInputProps; | |||||
| // 服务的需要的代码配置数据格式 | |||||
| export type ServerCodeData = { | |||||
| id: number; | |||||
| name: string; | |||||
| code_path: string; | |||||
| branch: string; | |||||
| username: string; | |||||
| password: string; | |||||
| ssh_private_key: string; | |||||
| is_public: boolean; | |||||
| }; | |||||
| /** 代码配置选择表单组件 */ | /** 代码配置选择表单组件 */ | ||||
| function CodeSelect({ | function CodeSelect({ | ||||
| @@ -32,11 +44,34 @@ function CodeSelect({ | |||||
| }: CodeSelectProps) { | }: CodeSelectProps) { | ||||
| // 选择代码配置 | // 选择代码配置 | ||||
| const selectResource = () => { | const selectResource = () => { | ||||
| const codeData = value as ServerCodeData; | |||||
| const defaultSelected = | |||||
| value && typeof value === 'object' | |||||
| ? { | |||||
| id: codeData.id, | |||||
| code_repo_name: codeData.name, | |||||
| git_url: codeData.code_path, | |||||
| git_branch: codeData.branch, | |||||
| git_user_name: codeData.username, | |||||
| git_password: codeData.password, | |||||
| ssh_key: codeData.ssh_private_key, | |||||
| is_public: codeData.is_public, | |||||
| } | |||||
| : undefined; | |||||
| const { close } = openAntdModal(CodeSelectorModal, { | const { close } = openAntdModal(CodeSelectorModal, { | ||||
| defaultSelected: defaultSelected, | |||||
| onOk: (res) => { | onOk: (res) => { | ||||
| if (res) { | if (res) { | ||||
| const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } = | |||||
| res; | |||||
| const { | |||||
| id, | |||||
| code_repo_name, | |||||
| git_url, | |||||
| git_branch, | |||||
| git_user_name, | |||||
| git_password, | |||||
| ssh_key, | |||||
| is_public, | |||||
| } = res; | |||||
| const jsonObj = { | const jsonObj = { | ||||
| id, | id, | ||||
| name: code_repo_name, | name: code_repo_name, | ||||
| @@ -45,6 +80,7 @@ function CodeSelect({ | |||||
| username: git_user_name, | username: git_user_name, | ||||
| password: git_password, | password: git_password, | ||||
| ssh_private_key: ssh_key, | ssh_private_key: ssh_key, | ||||
| is_public, | |||||
| }; | }; | ||||
| const jsonObjStr = JSON.stringify(jsonObj); | const jsonObjStr = JSON.stringify(jsonObj); | ||||
| onChange?.({ | onChange?.({ | ||||
| @@ -17,6 +17,7 @@ | |||||
| margin-bottom: 30px; | margin-bottom: 30px; | ||||
| overflow-x: hidden; | overflow-x: hidden; | ||||
| overflow-y: auto; | overflow-y: auto; | ||||
| padding-bottom: 10px; | |||||
| } | } | ||||
| &__empty { | &__empty { | ||||
| @@ -7,7 +7,8 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import KFModal from '@/components/KFModal'; | import KFModal from '@/components/KFModal'; | ||||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | import { type CodeConfigData } from '@/pages/CodeConfig/List'; | ||||
| import { getCodeConfigListReq } from '@/services/codeConfig'; | |||||
| import { getCodeConfigListReq, getCodeConfigPageNumReq } from '@/services/codeConfig'; | |||||
| import { CustomPartial } from '@/types'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import type { ModalProps, PaginationProps } from 'antd'; | import type { ModalProps, PaginationProps } from 'antd'; | ||||
| import { Empty, Input, Pagination } from 'antd'; | import { Empty, Input, Pagination } from 'antd'; | ||||
| @@ -17,24 +18,68 @@ import './index.less'; | |||||
| export { type CodeConfigData }; | export { type CodeConfigData }; | ||||
| export type SelectCodeData = CustomPartial< | |||||
| CodeConfigData, | |||||
| | 'id' | |||||
| | 'code_repo_name' | |||||
| | 'git_url' | |||||
| | 'git_branch' | |||||
| | 'git_user_name' | |||||
| | 'git_password' | |||||
| | 'ssh_key' | |||||
| | 'is_public' | |||||
| >; | |||||
| export interface CodeSelectorModalProps extends Omit<ModalProps, 'onOk'> { | export interface CodeSelectorModalProps extends Omit<ModalProps, 'onOk'> { | ||||
| onOk?: (params: CodeConfigData | undefined) => void; | |||||
| defaultSelected?: SelectCodeData; | |||||
| onOk?: (params: SelectCodeData | undefined) => void; | |||||
| } | } | ||||
| /** 选择代码配置的弹窗,推荐使用函数的方式打开 */ | /** 选择代码配置的弹窗,推荐使用函数的方式打开 */ | ||||
| function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||||
| function CodeSelectorModal({ defaultSelected, onOk, ...rest }: CodeSelectorModalProps) { | |||||
| const defaultPageSize = 10; | |||||
| const [dataList, setDataList] = useState<CodeConfigData[]>([]); | const [dataList, setDataList] = useState<CodeConfigData[]>([]); | ||||
| const [total, setTotal] = useState(0); | const [total, setTotal] = useState(0); | ||||
| const [pagination, setPagination] = useState<PaginationProps>({ | |||||
| current: 1, | |||||
| pageSize: 20, | |||||
| }); | |||||
| const [searchText, setSearchText] = useState<string | undefined>(undefined); | const [searchText, setSearchText] = useState<string | undefined>(undefined); | ||||
| const [inputText, setInputText] = useState<string | undefined>(undefined); | const [inputText, setInputText] = useState<string | undefined>(undefined); | ||||
| const [selected, setSelected] = useState(defaultSelected); | |||||
| const [isScrolled, setIsScrolled] = useState(false); | |||||
| const [pagination, setPagination] = useState<PaginationProps>({ | |||||
| current: defaultSelected?.id ? 0 : 1, // 为 0 时,不请求,等待接口返回选中的代码配置在第几页 | |||||
| pageSize: defaultPageSize, | |||||
| }); | |||||
| useEffect(() => { | |||||
| const getCodeConfigPageNum = async (id: number, size: number) => { | |||||
| const [res] = await to( | |||||
| getCodeConfigPageNumReq(id, { | |||||
| size, | |||||
| }), | |||||
| ); | |||||
| if (res) { | |||||
| setPagination({ | |||||
| current: typeof res.data === 'number' ? Math.max(0, res.data) + 1 : 1, | |||||
| pageSize: defaultPageSize, | |||||
| }); | |||||
| } else { | |||||
| setPagination({ | |||||
| current: 1, | |||||
| pageSize: defaultPageSize, | |||||
| }); | |||||
| } | |||||
| }; | |||||
| if (defaultSelected?.id) { | |||||
| getCodeConfigPageNum(defaultSelected?.id, defaultPageSize); | |||||
| } | |||||
| }, [defaultSelected?.id]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| // 获取数据请求 | // 获取数据请求 | ||||
| const getDataList = async () => { | const getDataList = async () => { | ||||
| // 为 0 时,不请求,等待接口返回选中的代码配置在第几页 | |||||
| if (pagination.current === 0) { | |||||
| return; | |||||
| } | |||||
| const params = { | const params = { | ||||
| page: pagination.current! - 1, | page: pagination.current! - 1, | ||||
| size: pagination.pageSize, | size: pagination.pageSize, | ||||
| @@ -50,6 +95,16 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||||
| getDataList(); | getDataList(); | ||||
| }, [pagination, searchText]); | }, [pagination, searchText]); | ||||
| useEffect(() => { | |||||
| if (dataList.length > 0 && !isScrolled && defaultSelected?.id) { | |||||
| const selectedItem = document.getElementById(`code-config-item-${defaultSelected?.id}`); | |||||
| if (selectedItem) { | |||||
| selectedItem.scrollIntoView(); | |||||
| } | |||||
| setIsScrolled(true); | |||||
| } | |||||
| }, [isScrolled, dataList, defaultSelected?.id]); | |||||
| // 搜索 | // 搜索 | ||||
| const handleSearch = (value: string) => { | const handleSearch = (value: string) => { | ||||
| setSearchText(value); | setSearchText(value); | ||||
| @@ -59,8 +114,12 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||||
| })); | })); | ||||
| }; | }; | ||||
| const handleClick = (item: CodeConfigData) => { | |||||
| onOk?.(item); | |||||
| const handleChange = (item: CodeConfigData, checked: boolean) => { | |||||
| if (checked) { | |||||
| setSelected(item); | |||||
| } else { | |||||
| setSelected(undefined); | |||||
| } | |||||
| }; | }; | ||||
| // 分页切换 | // 分页切换 | ||||
| @@ -77,7 +136,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||||
| title="选择代码配置" | title="选择代码配置" | ||||
| image={require('@/assets/img/modal-code-config.png')} | image={require('@/assets/img/modal-code-config.png')} | ||||
| width={920} | width={920} | ||||
| footer={null} | |||||
| onOk={() => onOk?.(selected)} | |||||
| destroyOnClose | destroyOnClose | ||||
| > | > | ||||
| <div className="kf-code-selector-modal"> | <div className="kf-code-selector-modal"> | ||||
| @@ -93,19 +152,21 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||||
| prefix={ | prefix={ | ||||
| <KFIcon type="icon-sousuo" color="rgba(22,100,255,0.4" style={{ marginLeft: '10px' }} /> | <KFIcon type="icon-sousuo" color="rgba(22,100,255,0.4" style={{ marginLeft: '10px' }} /> | ||||
| } | } | ||||
| // prefix={ | |||||
| // <Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} /> | |||||
| // } | |||||
| /> | /> | ||||
| {dataList?.length !== 0 ? ( | {dataList?.length !== 0 ? ( | ||||
| <> | <> | ||||
| <div className="kf-code-selector-modal__content"> | <div className="kf-code-selector-modal__content"> | ||||
| {dataList?.map((item) => ( | {dataList?.map((item) => ( | ||||
| <CodeConfigItem item={item} key={item.id} onClick={handleClick} /> | |||||
| <CodeConfigItem | |||||
| item={item} | |||||
| key={item.id} | |||||
| checked={item.id === selected?.id} | |||||
| onChange={handleChange} | |||||
| /> | |||||
| ))} | ))} | ||||
| </div> | </div> | ||||
| <Pagination | <Pagination | ||||
| align="center" | |||||
| align="end" | |||||
| total={total} | total={total} | ||||
| showSizeChanger | showSizeChanger | ||||
| defaultPageSize={20} | defaultPageSize={20} | ||||
| @@ -89,6 +89,7 @@ | |||||
| margin-bottom: 15px !important; | margin-bottom: 15px !important; | ||||
| color: @text-color; | color: @text-color; | ||||
| font-size: 14px; | font-size: 14px; | ||||
| word-break: break-all; | |||||
| } | } | ||||
| &__branch { | &__branch { | ||||
| @@ -1,3 +1,4 @@ | |||||
| import { type ServerCodeData } from '@/components/CodeSelect'; | |||||
| import CodeSelectorModal from '@/components/CodeSelectorModal'; | import CodeSelectorModal from '@/components/CodeSelectorModal'; | ||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; | import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; | ||||
| @@ -15,6 +16,7 @@ import { | |||||
| PipelineNodeModelParameter, | PipelineNodeModelParameter, | ||||
| PipelineNodeModelSerialize, | PipelineNodeModelSerialize, | ||||
| } from '@/types'; | } from '@/types'; | ||||
| import { parseJsonText } from '@/utils'; | |||||
| import { openAntdModal } from '@/utils/modal'; | import { openAntdModal } from '@/utils/modal'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { INode } from '@antv/g6'; | import { INode } from '@antv/g6'; | ||||
| @@ -155,11 +157,34 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| formItemName: NamePath, | formItemName: NamePath, | ||||
| item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>, | item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>, | ||||
| ) => { | ) => { | ||||
| const jsonValue = form.getFieldValue(formItemName)?.value; | |||||
| const fieldValue = parseJsonText(jsonValue) as ServerCodeData; | |||||
| const defaultSelected = fieldValue | |||||
| ? { | |||||
| id: fieldValue.id, | |||||
| code_repo_name: fieldValue.name, | |||||
| git_url: fieldValue.code_path, | |||||
| git_branch: fieldValue.branch, | |||||
| git_user_name: fieldValue.username, | |||||
| git_password: fieldValue.password, | |||||
| ssh_key: fieldValue.ssh_private_key, | |||||
| is_public: fieldValue.is_public, | |||||
| } | |||||
| : undefined; | |||||
| const { close } = openAntdModal(CodeSelectorModal, { | const { close } = openAntdModal(CodeSelectorModal, { | ||||
| defaultSelected, | |||||
| onOk: (res) => { | onOk: (res) => { | ||||
| if (res) { | if (res) { | ||||
| const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } = | |||||
| res; | |||||
| const { | |||||
| id, | |||||
| code_repo_name, | |||||
| git_url, | |||||
| git_branch, | |||||
| git_user_name, | |||||
| git_password, | |||||
| ssh_key, | |||||
| is_public, | |||||
| } = res; | |||||
| const value = JSON.stringify({ | const value = JSON.stringify({ | ||||
| id, | id, | ||||
| name: code_repo_name, | name: code_repo_name, | ||||
| @@ -168,6 +193,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| username: git_user_name, | username: git_user_name, | ||||
| password: git_password, | password: git_password, | ||||
| ssh_private_key: ssh_key, | ssh_private_key: ssh_key, | ||||
| is_public, | |||||
| }); | }); | ||||
| form.setFieldValue(formItemName, { | form.setFieldValue(formItemName, { | ||||
| ...item, | ...item, | ||||
| @@ -37,3 +37,12 @@ export function getCodeConfigDetailReq(id) { | |||||
| method: 'GET', | method: 'GET', | ||||
| }); | }); | ||||
| } | } | ||||
| // 获取代码配置项在第几页 | |||||
| export function getCodeConfigPageNumReq(id, params) { | |||||
| return request(`/api/mmp/codeConfig/getPageNum/${id}`, { | |||||
| method: 'GET', | |||||
| params | |||||
| }); | |||||
| } | |||||
| @@ -151,3 +151,6 @@ export type UploadFileRes = { | |||||
| fileSize: number; | fileSize: number; | ||||
| url: string; | url: string; | ||||
| }; | }; | ||||
| // 定义一个类型,取一个类型的有些字段必须的,其它的是可选 | |||||
| export type CustomPartial<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>; | |||||