| @@ -1,11 +1,22 @@ | |||
| .code-config-item { | |||
| position: relative; | |||
| width: calc(25% - 7.5px); | |||
| width: calc(33.33% - 7px); | |||
| padding: 15px; | |||
| background-color: .addAlpha(@primary-color, 0.04) []; | |||
| border: 1px solid transparent; | |||
| border-radius: 4px; | |||
| cursor: pointer; | |||
| &__checkbox { | |||
| flex: 1; | |||
| min-width: 0; | |||
| :global { | |||
| .ant-checkbox + span { | |||
| flex: 1; | |||
| min-width: 0; | |||
| } | |||
| } | |||
| } | |||
| &__name { | |||
| margin-right: 8px; | |||
| @@ -38,6 +49,8 @@ | |||
| margin-bottom: 10px !important; | |||
| color: @text-color-secondary; | |||
| font-size: 13px; | |||
| cursor: pointer; | |||
| word-break: break-all; | |||
| } | |||
| &__branch { | |||
| @@ -46,11 +59,17 @@ | |||
| } | |||
| &:hover { | |||
| background-color: .addAlpha(@primary-color, 0.08) []; | |||
| } | |||
| &--active { | |||
| border-color: @primary-color; | |||
| box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1); | |||
| } | |||
| &:hover &__name { | |||
| &--active &__name { | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| @@ -1,25 +1,51 @@ | |||
| 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 { useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type CodeConfigItemProps = { | |||
| 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 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 ( | |||
| <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' }}> | |||
| <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 | |||
| className={classNames( | |||
| styles['code-config-item__tag'], | |||
| @@ -35,9 +61,10 @@ function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { | |||
| className={styles['code-config-item__url']} | |||
| ellipsis={{ | |||
| 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} | |||
| </Typography.Paragraph> | |||
| @@ -18,7 +18,19 @@ export { | |||
| type ParameterInputValue, | |||
| } 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({ | |||
| @@ -32,11 +44,34 @@ function CodeSelect({ | |||
| }: CodeSelectProps) { | |||
| // 选择代码配置 | |||
| 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, { | |||
| defaultSelected: defaultSelected, | |||
| onOk: (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 = { | |||
| id, | |||
| name: code_repo_name, | |||
| @@ -45,6 +80,7 @@ function CodeSelect({ | |||
| username: git_user_name, | |||
| password: git_password, | |||
| ssh_private_key: ssh_key, | |||
| is_public, | |||
| }; | |||
| const jsonObjStr = JSON.stringify(jsonObj); | |||
| onChange?.({ | |||
| @@ -17,6 +17,7 @@ | |||
| margin-bottom: 30px; | |||
| overflow-x: hidden; | |||
| overflow-y: auto; | |||
| padding-bottom: 10px; | |||
| } | |||
| &__empty { | |||
| @@ -7,7 +7,8 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import KFModal from '@/components/KFModal'; | |||
| 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 type { ModalProps, PaginationProps } from 'antd'; | |||
| import { Empty, Input, Pagination } from 'antd'; | |||
| @@ -17,24 +18,68 @@ import './index.less'; | |||
| 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'> { | |||
| 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 [total, setTotal] = useState(0); | |||
| const [pagination, setPagination] = useState<PaginationProps>({ | |||
| current: 1, | |||
| pageSize: 20, | |||
| }); | |||
| const [searchText, setSearchText] = 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(() => { | |||
| // 获取数据请求 | |||
| const getDataList = async () => { | |||
| // 为 0 时,不请求,等待接口返回选中的代码配置在第几页 | |||
| if (pagination.current === 0) { | |||
| return; | |||
| } | |||
| const params = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| @@ -50,6 +95,16 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| getDataList(); | |||
| }, [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) => { | |||
| 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="选择代码配置" | |||
| image={require('@/assets/img/modal-code-config.png')} | |||
| width={920} | |||
| footer={null} | |||
| onOk={() => onOk?.(selected)} | |||
| destroyOnClose | |||
| > | |||
| <div className="kf-code-selector-modal"> | |||
| @@ -93,19 +152,21 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| prefix={ | |||
| <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 ? ( | |||
| <> | |||
| <div className="kf-code-selector-modal__content"> | |||
| {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> | |||
| <Pagination | |||
| align="center" | |||
| align="end" | |||
| total={total} | |||
| showSizeChanger | |||
| defaultPageSize={20} | |||
| @@ -89,6 +89,7 @@ | |||
| margin-bottom: 15px !important; | |||
| color: @text-color; | |||
| font-size: 14px; | |||
| word-break: break-all; | |||
| } | |||
| &__branch { | |||
| @@ -1,3 +1,4 @@ | |||
| import { type ServerCodeData } from '@/components/CodeSelect'; | |||
| import CodeSelectorModal from '@/components/CodeSelectorModal'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; | |||
| @@ -15,6 +16,7 @@ import { | |||
| PipelineNodeModelParameter, | |||
| PipelineNodeModelSerialize, | |||
| } from '@/types'; | |||
| import { parseJsonText } from '@/utils'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { INode } from '@antv/g6'; | |||
| @@ -155,11 +157,34 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| formItemName: NamePath, | |||
| 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, { | |||
| defaultSelected, | |||
| onOk: (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({ | |||
| id, | |||
| name: code_repo_name, | |||
| @@ -168,6 +193,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| username: git_user_name, | |||
| password: git_password, | |||
| ssh_private_key: ssh_key, | |||
| is_public, | |||
| }); | |||
| form.setFieldValue(formItemName, { | |||
| ...item, | |||
| @@ -37,3 +37,12 @@ export function getCodeConfigDetailReq(id) { | |||
| 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; | |||
| url: string; | |||
| }; | |||
| // 定义一个类型,取一个类型的有些字段必须的,其它的是可选 | |||
| export type CustomPartial<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>; | |||