From 14e2ad39561aa4fe69b90197f15b9fed71aa9305 Mon Sep 17 00:00:00 2001 From: johnny19941216 Date: Wed, 12 Apr 2023 14:50:21 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(console):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E6=9B=B4=E6=96=B0=E7=AD=96=E7=95=A5=E5=88=B0?= =?UTF-8?q?workload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/resource/k8sConfig/daemonset.ts | 15 +- .../config/resource/k8sConfig/deployment.ts | 81 ++--- .../config/resource/k8sConfig/statefulset.ts | 17 +- web/console/helpers/index.ts | 31 +- web/console/helpers/path.ts | 4 + .../resourceEdition/UpdateResourcePanel.tsx | 14 +- .../ResourceTablePanel.tsx | 83 +++-- .../modifyStrategyPanel/constants.ts | 84 +++++ .../modifyStrategyPanel/index.tsx | 315 ++++++++++++++++++ .../modules/common/layouts/TeaFormLayout.tsx | 2 +- web/console/src/webApi/index.ts | 2 + web/console/src/webApi/workload.ts | 26 ++ 12 files changed, 560 insertions(+), 114 deletions(-) create mode 100644 web/console/helpers/path.ts create mode 100644 web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/constants.ts create mode 100644 web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/index.tsx create mode 100644 web/console/src/webApi/workload.ts diff --git a/web/console/config/resource/k8sConfig/daemonset.ts b/web/console/config/resource/k8sConfig/daemonset.ts index 2dd0ae7a98..2adf199fc8 100644 --- a/web/console/config/resource/k8sConfig/daemonset.ts +++ b/web/console/config/resource/k8sConfig/daemonset.ts @@ -15,17 +15,17 @@ * WARRANTIES OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ +import { t } from '@tencent/tea-app/lib/i18n'; import { DetailField, DetailInfo } from '../../../src/modules/common/models'; -import { t, Trans } from '@tencent/tea-app/lib/i18n'; import { - commonDisplayField, - defaulNotExistedValue, commonActionField, commonDetailInfo, + commonDisplayField, dataFormatConfig, - workloadCommonTabList, - generateResourceInfo + defaulNotExistedValue, + generateResourceInfo, + workloadCommonTabList } from '../common'; /** displayField,列表展示的细节 */ @@ -58,6 +58,11 @@ const displayField = Object.assign({}, commonDisplayField, { name: t('删除'), actionType: 'delete', isInMoreOp: false + }, + { + name: t('设置更新策略'), + actionType: 'modifyStrategy', + isInMoreOp: true } ] } diff --git a/web/console/config/resource/k8sConfig/deployment.ts b/web/console/config/resource/k8sConfig/deployment.ts index 6e2f87dab1..bf37f426ab 100644 --- a/web/console/config/resource/k8sConfig/deployment.ts +++ b/web/console/config/resource/k8sConfig/deployment.ts @@ -15,18 +15,18 @@ * WARRANTIES OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ -import { DetailField, DisplayField, DetailInfo } from '../../../src/modules/common/models'; +import { t } from '@tencent/tea-app/lib/i18n'; +import { DetailField, DetailInfo, DisplayField } from '../../../src/modules/common/models'; +import { cloneDeep } from '../../../src/modules/common/utils'; import { - commonDisplayField, - defaulNotExistedValue, - workloadCommonTabList, commonActionField, commonDetailInfo, + commonDisplayField, dataFormatConfig, + defaulNotExistedValue, generateResourceInfo, + workloadCommonTabList } from '../common'; -import { cloneDeep } from '../../../src/modules/common/utils'; -import { t, Trans } from '@tencent/tea-app/lib/i18n'; /** resource table 当中展示的数据 * commonDisplayField 使用公共的展示 @@ -38,7 +38,7 @@ const userDefinedDisplayField: DisplayField = { dataFormat: dataFormatConfig['replicas'], width: '20%', headTitle: t('运行/期望Pod数量'), - noExsitedValue: '0', + noExsitedValue: '0' }, operator: { dataField: [''], @@ -48,27 +48,32 @@ const userDefinedDisplayField: DisplayField = { tips: '', operatorList: [ { - name: t('更新实例数量页'), + name: t('更新Pod数量'), actionType: 'modifyPod', - isInMoreOp: false, + isInMoreOp: false }, { name: t('更新镜像'), actionType: 'modifyRegistry', - isInMoreOp: false, + isInMoreOp: false + }, + { + name: t('设置更新策略'), + actionType: 'modifyStrategy', + isInMoreOp: true }, { name: t('编辑YAML'), actionType: 'modify', - isInMoreOp: true, + isInMoreOp: true }, { name: t('删除'), actionType: 'delete', - isInMoreOp: true, - }, - ], - }, + isInMoreOp: true + } + ] + } }; const displayField = Object.assign({}, commonDisplayField, userDefinedDisplayField); @@ -85,35 +90,35 @@ const detailBasicInfo: DetailInfo = { dataField: ['name'], dataFormat: dataFormatConfig['text'], label: t('名称'), - noExsitedValue: defaulNotExistedValue, + noExsitedValue: defaulNotExistedValue }, namespace: { dataField: ['namespace'], dataFormat: dataFormatConfig['text'], label: 'Namespace', - noExsitedValue: defaulNotExistedValue, + noExsitedValue: defaulNotExistedValue }, description: { dataField: ['annotations.description'], dataFormat: dataFormatConfig['text'], label: t('描述'), - noExsitedValue: defaulNotExistedValue, + noExsitedValue: defaulNotExistedValue }, createdTime: { dataField: ['creationTimestamp'], dataFormat: dataFormatConfig['time'], label: t('创建时间'), tips: '', - noExsitedValue: defaulNotExistedValue, + noExsitedValue: defaulNotExistedValue }, label: { dataField: ['labels'], dataFormat: dataFormatConfig['labels'], label: 'Labels', tips: '', - noExsitedValue: defaulNotExistedValue, - }, - }, + noExsitedValue: defaulNotExistedValue + } + } }, spec: { dataField: ['spec'], @@ -123,28 +128,28 @@ const detailBasicInfo: DetailInfo = { dataFormat: dataFormatConfig['labels'], label: 'Selector', tips: '', - noExsitedValue: defaulNotExistedValue, + noExsitedValue: defaulNotExistedValue }, updateStrategy: { dataField: ['strategy.type'], dataFormat: dataFormatConfig['text'], label: t('更新策略'), tips: '', - noExsitedValue: defaulNotExistedValue, + noExsitedValue: defaulNotExistedValue }, replicas: { dataField: ['replicas'], dataFormat: dataFormatConfig['text'], label: t('副本数'), - noExsitedValue: '0', + noExsitedValue: '0' }, networkType: { dataField: ['template', 'metadata', 'annotations', 'k8s.v1.cni.cncf.io/networks'], dataFormat: dataFormatConfig['text'], label: t('网络模式'), - noExsitedValue: '-', - }, - }, + noExsitedValue: '-' + } + } }, status: { dataField: ['status'], @@ -153,22 +158,22 @@ const detailBasicInfo: DetailInfo = { dataField: ['readyReplicas'], dataFormat: dataFormatConfig['text'], label: t('运行副本数'), - noExsitedValue: '0', - }, - }, - }, - }, + noExsitedValue: '0' + } + } + } + } }; -let tabList = cloneDeep(workloadCommonTabList); +const tabList = cloneDeep(workloadCommonTabList); tabList.splice(1, 0, { id: 'history', - label: t('修订历史'), + label: t('修订历史') }); /** 详情页面的相关配置 */ const detailField: DetailField = { tabList, - detailInfo: Object.assign({}, commonDetailInfo(), detailBasicInfo), + detailInfo: Object.assign({}, commonDetailInfo(), detailBasicInfo) }; /** deployment的配置 */ @@ -177,11 +182,11 @@ export const deployment = (k8sVersion: string) => { k8sVersion, resourceName: 'deployment', requestType: { - list: 'deployments', + list: 'deployments' }, isRelevantToNamespace: true, displayField, actionField, - detailField, + detailField }); }; diff --git a/web/console/config/resource/k8sConfig/statefulset.ts b/web/console/config/resource/k8sConfig/statefulset.ts index 3a9822f99c..1ff29b6e82 100644 --- a/web/console/config/resource/k8sConfig/statefulset.ts +++ b/web/console/config/resource/k8sConfig/statefulset.ts @@ -15,17 +15,17 @@ * WARRANTIES OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ +import { t } from '@tencent/tea-app/lib/i18n'; import { DetailField, DetailInfo } from '../../../src/modules/common/models'; import { - commonDisplayField, - defaulNotExistedValue, commonActionField, - dataFormatConfig, commonDetailInfo, - workloadCommonTabList, - generateResourceInfo + commonDisplayField, + dataFormatConfig, + defaulNotExistedValue, + generateResourceInfo, + workloadCommonTabList } from '../common'; -import { t, Trans } from '@tencent/tea-app/lib/i18n'; const displayField = Object.assign({}, commonDisplayField, { runningReplicas: { @@ -56,6 +56,11 @@ const displayField = Object.assign({}, commonDisplayField, { name: t('删除'), actionType: 'delete', isInMoreOp: false + }, + { + name: t('设置更新策略'), + actionType: 'modifyStrategy', + isInMoreOp: true } ] } diff --git a/web/console/helpers/index.ts b/web/console/helpers/index.ts index bd3366c93e..50b43eacbb 100644 --- a/web/console/helpers/index.ts +++ b/web/console/helpers/index.ts @@ -15,27 +15,28 @@ * WARRANTIES OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ +export { RouteState, Router } from './Router'; +export { Validate, getReactHookFormStatusWithMessage, isValidateSuccess } from './Validator'; +export * from './appUtil'; +export { getCookie } from './cookieUtil'; +export * from './csrf'; +export { dateFormatter } from './dateFormatter'; +export { dateFormat } from './dateUtil'; export { downloadCrt, downloadKubeconfig, getKubectlConfig } from './downloadCrt'; -export { ResetStoreAction, generateResetableReducer } from './reduxStore'; -export { isValidateSuccess, Validate, getReactHookFormStatusWithMessage } from './Validator'; +export { downloadCsv } from './downloadCsv'; +export * from './format'; +export { getScrollBarSize } from './getScrollBarSize'; +export * from './path'; export { + ConsoleModuleMapProps, + Method, + operationResult, reduceNetworkRequest, reduceNetworkWorkflow, - operationResult, - Method, requestMethodForAction, - ConsoleModuleMapProps, setConsoleAPIAddress } from './reduceNetwork'; -export { dateFormatter } from './dateFormatter'; -export { downloadCsv } from './downloadCsv'; -export { Router, RouteState } from './Router'; +export { ResetStoreAction, generateResetableReducer } from './reduxStore'; export { assureRegion } from './regionLint'; -export { getScrollBarSize } from './getScrollBarSize'; -export { dateFormat } from './dateUtil'; -export * from './appUtil'; -export { getCookie } from './cookieUtil'; -export { reduceK8sQueryString, reduceK8sRestfulPath, reduceNs, parseQueryString, cutNsStartClusterId } from './urlUtil'; export * from './request'; -export * from './format'; -export * from './csrf'; +export { cutNsStartClusterId, parseQueryString, reduceK8sQueryString, reduceK8sRestfulPath, reduceNs } from './urlUtil'; diff --git a/web/console/helpers/path.ts b/web/console/helpers/path.ts new file mode 100644 index 0000000000..f27e93fe99 --- /dev/null +++ b/web/console/helpers/path.ts @@ -0,0 +1,4 @@ +export function getParamByUrl(key: string) { + const searchParams = new URL(window.location.href)?.searchParams; + return searchParams?.get(key); +} diff --git a/web/console/src/modules/cluster/components/resource/resourceEdition/UpdateResourcePanel.tsx b/web/console/src/modules/cluster/components/resource/resourceEdition/UpdateResourcePanel.tsx index 69463c11fd..cb05dd77c0 100644 --- a/web/console/src/modules/cluster/components/resource/resourceEdition/UpdateResourcePanel.tsx +++ b/web/console/src/modules/cluster/components/resource/resourceEdition/UpdateResourcePanel.tsx @@ -19,16 +19,16 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from '@tencent/ff-redux'; -import { t, Trans } from '@tencent/tea-app/lib/i18n'; +import { t } from '@tencent/tea-app/lib/i18n'; +import { upperFirst } from 'lodash'; import { allActions } from '../../../actions'; import { router } from '../../../router'; import { RootProps } from '../../ClusterApp'; -import { - UpdateServiceAccessTypePanel -} from '../resourceTableOperation/UpdateServiceAccessTypePanel'; +import { UpdateServiceAccessTypePanel } from '../resourceTableOperation/UpdateServiceAccessTypePanel'; import { UpdateWorkloadPodNumPanel } from '../resourceTableOperation/UpdateWorkloadPodNumPanel'; import { UpdateWorkloadRegistryPanel } from '../resourceTableOperation/UpdateWorkloadRegistryPanel'; +import { ModifyStrategyPanel } from '../resourceTableOperation/workloadUpdate/modifyStrategyPanel'; import { EditLbcfBackGroupPanel } from './EditLbcfBackGroupPanel'; import { SubHeaderPanel } from './SubHeaderPanel'; @@ -38,7 +38,7 @@ const mapDispatchToProps = dispatch => @connect(state => state, mapDispatchToProps) export class UpdateResourcePanel extends React.Component { render() { - let { route } = this.props, + const { route } = this.props, urlParams = router.resolve(route); let headTitle = ''; @@ -47,7 +47,7 @@ export class UpdateResourcePanel extends React.Component { let content: JSX.Element; // 判断当前的资源 - let resourceType = urlParams['resourceName'], + const resourceType = urlParams['resourceName'], updateType = urlParams['tab']; if (resourceType === 'svc' && updateType === 'modifyType') { @@ -71,6 +71,8 @@ export class UpdateResourcePanel extends React.Component { } else if (resourceType === 'lbcf' && updateType === 'updateBG') { content = ; headTitle = t('更新后端负载'); + } else if (['deployment', 'statefulset', 'daemonset'].includes(resourceType) && updateType === 'modifyStrategy') { + return ; } return ( diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/ResourceTablePanel.tsx b/web/console/src/modules/cluster/components/resource/resourceTableOperation/ResourceTablePanel.tsx index a98566ebc9..f49b68bd6f 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/ResourceTablePanel.tsx +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/ResourceTablePanel.tsx @@ -25,7 +25,7 @@ import { Bubble, TableColumn, Text } from '@tea/component'; import { selectable } from '@tea/component/table/addons/selectable'; import { TablePanel } from '@tencent/ff-component'; import { bindActionCreators, uuid } from '@tencent/ff-redux'; -import { t, Trans } from '@tencent/tea-app/lib/i18n'; +import { Trans, t } from '@tencent/tea-app/lib/i18n'; import { dateFormatter } from '../../../../../../helpers'; import { Clip, HeadBubble, LinkButton } from '../../../../common/components'; @@ -129,7 +129,7 @@ const mapDispatchToProps = dispatch => @connect(state => state, mapDispatchToProps) export class ResourceTablePanel extends React.Component { componentWillUnmount() { - let { actions } = this?.props; + const { actions } = this?.props; // 离开页面的话,清空当前的轮询操作 actions?.resource?.clearPolling(); // 离开页面的话,清空当前的多选 @@ -257,17 +257,17 @@ export class ResourceTablePanel extends React.Component { * @param fieldInfo: 配置文件 */ private _renderOperationCell(resource: Resource, fieldInfo: DisplayFiledProps) { - let { route, actions, subRoot, namespaceSelection } = this.props, + const { route, actions, subRoot, namespaceSelection } = this.props, urlParams = router.resolve(route), { clusterId } = route.queries, { resourceOption, resourceName } = subRoot, { ffResourceList } = resourceOption; // 操作列表的list - let operatorList = fieldInfo?.operatorList; + const operatorList = fieldInfo?.operatorList; // 更多按钮的 pop方向 - let resourceIndex = ffResourceList?.list?.data?.records?.findIndex(c => c.id === resource.id); - let direction: 'down' | 'up' = + const resourceIndex = ffResourceList?.list?.data?.records?.findIndex(c => c.id === resource.id); + const direction: 'down' | 'up' = resourceIndex < ffResourceList?.list?.data?.recordCount - 2 || ffResourceList?.list?.data?.recordCount < 4 ? 'down' : 'up'; @@ -401,19 +401,16 @@ export class ResourceTablePanel extends React.Component { ); }; - let btns = []; + const btns = []; operatorList?.forEach(operatorItem => { if (operatorItem?.actionType === 'modify') { btns.push(renderModifyButton(operatorItem)); } else if (operatorItem.actionType === 'delete') { btns.push(renderDeleteButton(operatorItem)); } else if ( - operatorItem?.actionType === 'modifyPod' || - operatorItem?.actionType === 'modifyRule' || - operatorItem?.actionType === 'modifyType' || - operatorItem?.actionType === 'modifyRegistry' || - operatorItem?.actionType === 'createBG' || - operatorItem?.actionType === 'updateBG' + ['modifyStrategy', 'modifyPod', 'modifyRule', 'modifyType', 'modifyRegistry', 'createBG', 'updateBG'].includes( + operatorItem?.actionType + ) ) { btns.push(renderUpdateResourcePart(operatorItem)); } @@ -428,7 +425,7 @@ export class ResourceTablePanel extends React.Component { /** 展示ip的内容 */ private _reduceIPCell(ipInfo: any, clipId: string, resource: Resource) { - let { resourceName } = this?.props?.subRoot; + const { resourceName } = this?.props?.subRoot; let ipArray = ipInfo; // 如果ipArray 不是一个数组 if (typeof ipArray !== 'object') { @@ -446,8 +443,8 @@ export class ResourceTablePanel extends React.Component { if (isNginxIngress) { content =
-
; } else { - let [clusterIP, ingressIP] = ipArray; - let isShowLoading = IsResourceShowLoadingIcon(resourceName, resource); + const [clusterIP, ingressIP] = ipArray; + const isShowLoading = IsResourceShowLoadingIcon(resourceName, resource); content = (
{ingressIP && ( @@ -479,9 +476,9 @@ export class ResourceTablePanel extends React.Component { /** 展示status */ private _reduceStatus(showData: any, resource: Resource) { - let { resourceName } = this.props.subRoot; + const { resourceName } = this.props.subRoot; - let statusMap = ResourceStatus?.[resourceName]; + const statusMap = ResourceStatus?.[resourceName]; return (
@@ -501,7 +498,7 @@ export class ResourceTablePanel extends React.Component { /** 展示映射的字段 */ private _reduceMapText(showData: any, fieldInfo: DisplayFiledProps) { - let { mapTextConfig } = fieldInfo; + const { mapTextConfig } = fieldInfo; return ( @@ -512,7 +509,7 @@ export class ResourceTablePanel extends React.Component { /** 展示副本的相关 */ private _reduceReplicas(showData: any, resource: Resource) { - let { resourceName } = this.props.subRoot; + const { resourceName } = this.props.subRoot; return ( @@ -542,9 +539,9 @@ export class ResourceTablePanel extends React.Component { }${rule.path}`; }; - let finalRules = [...httpRules, ...httpsRules]; + const finalRules = [...httpRules, ...httpsRules]; - let finalRulesLength = finalRules.length; + const finalRulesLength = finalRules.length; return finalRules.length ? ( { /** 展示ingress的后端服务 */ private _reduceIngressRule_standalone(showData: any) { - let httpRules = showData !== '-' ? showData : []; - let finalRules = + const httpRules = showData !== '-' ? showData : []; + const finalRules = httpRules?.map(item => { return { protocol: 'http', @@ -593,7 +590,7 @@ export class ResourceTablePanel extends React.Component { return `${rule?.protocol}://${rule?.host}${rule?.path}`; }; - let finalRulesLength = finalRules?.length; + const finalRulesLength = finalRules?.length; return finalRules.length ? ( { ); } private _reducebackendGroups(showData) { - let backendGroups = showData, + const backendGroups = showData, backendGroupsLength = backendGroups !== '-' ? backendGroups.length : 0; return backendGroupsLength ? ( { /** 展示时间 */ private _reduceTime(showData: any, direction: 'bottom' | 'top') { - let time = dateFormatter(new Date(showData), 'YYYY-MM-DD HH:mm:ss'); + const time = dateFormatter(new Date(showData), 'YYYY-MM-DD HH:mm:ss'); - let [year, currentTime] = time.split(' '); + const [year, currentTime] = time.split(' '); return ( @@ -669,8 +666,8 @@ export class ResourceTablePanel extends React.Component { } private _reduceResourceLimit(showData) { - let resourceLimitKeys = showData !== '-' ? Object.keys(showData) : []; - let content = resourceLimitKeys.map((item, index) => ( + const resourceLimitKeys = showData !== '-' ? Object.keys(showData) : []; + const content = resourceLimitKeys.map((item, index) => ( {`${resourceLimitTypeToText[item]}:${ resourceTypeToUnit[item] === 'MiB' ? valueLabels1024(showData[item], K8SUNIT.Mi) @@ -701,7 +698,7 @@ export class ResourceTablePanel extends React.Component { /** 根据 fieldInfo的 dataFormat来决定显示的bodyCell的具体内容 */ private _renderBodyCell(resource: Resource, fieldInfo: DisplayFiledProps, clipId: string) { - let { subRoot } = this.props, + const { subRoot } = this.props, { resourceOption } = subRoot, { ffResourceList } = resourceOption; @@ -710,8 +707,8 @@ export class ResourceTablePanel extends React.Component { // fieldInfo当中的 dataField是一个数组,可以同时输入多个值 let showData: any = []; fieldInfo?.dataField?.forEach(item => { - let dataFieldIns = item.split('.'); - let data: any = this._getFinalData(dataFieldIns, resource); + const dataFieldIns = item.split('.'); + const data: any = this._getFinalData(dataFieldIns, resource); // 如果返回的为 '' ,即找不到这个对象,则使用配置文件所设定的默认值 showData.push(data === '' ? fieldInfo?.noExsitedValue : data); }); @@ -719,8 +716,8 @@ export class ResourceTablePanel extends React.Component { showData = showData.length === 1 ? showData[0] : showData; // 这里是当列表有 bubble等情况的时候,判断当前行属于第几行 - let resourceIndex = ffResourceList?.list?.data?.records?.findIndex(item => item.id === resource.id); - let direction: 'top' | 'bottom' = + const resourceIndex = ffResourceList?.list?.data?.records?.findIndex(item => item.id === resource.id); + const direction: 'top' | 'bottom' = ffResourceList?.list?.data?.recordCount < 4 || resourceIndex < ffResourceList?.list?.data?.recordCount - 2 ? 'top' : 'bottom'; @@ -758,13 +755,13 @@ export class ResourceTablePanel extends React.Component { { resourceOption, resourceInfo, resourceName } = subRoot, { ffResourceList, resourceMultipleSelection } = resourceOption; - let addons = []; + const addons = []; - let displayField = resourceInfo?.displayField ?? {}; + const displayField = resourceInfo?.displayField ?? {}; // 根据 displayField当中的key来决定展示什么内容 - let showField = []; + const showField = []; Object.keys(displayField).forEach(item => { - let fieldInfo = displayField?.[item]; + const fieldInfo = displayField?.[item]; // 操作的按钮现在都换成在tablePanel当中去展示 if (fieldInfo?.dataFormat === 'operator') return; @@ -782,7 +779,7 @@ export class ResourceTablePanel extends React.Component { ); return; } - let columnInfo: TableColumn = { + const columnInfo: TableColumn = { key: item + uuid(), header: fieldInfo?.headTitle, width: fieldInfo?.width, @@ -790,9 +787,9 @@ export class ResourceTablePanel extends React.Component { }; if (fieldInfo.headCell) { - let style: React.CSSProperties = { display: 'block' }; + const style: React.CSSProperties = { display: 'block' }; - let headBubbleText = ( + const headBubbleText = ( {fieldInfo?.headCell?.map((item, index) => ( @@ -842,7 +839,7 @@ export class ResourceTablePanel extends React.Component { /** 链接的跳转 */ private _handleClickForNavigate(resource: Resource) { - let { actions, route } = this.props, + const { actions, route } = this.props, urlParams = router.resolve(route); // 选择当前的具体的resouce diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/constants.ts b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/constants.ts new file mode 100644 index 0000000000..efe80943f6 --- /dev/null +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/constants.ts @@ -0,0 +1,84 @@ +import { t } from '@/tencent/tea-app/lib/i18n'; + +export enum WorkloadKindEnum { + Deployment = 'deployment', + + StatefulSet = 'statefulset', + + DaemonSet = 'daemonset' +} + +export interface IModifyStrategyPanelProps { + kind: WorkloadKindEnum; +} + +/** 滚动更新的策略选择 */ +export enum RollingUpdateTypeEnum { + /** 启动新的pod,停止旧的pod */ + CreatePod = 'createPod', + + /** 停止旧的pod,启动新的pod */ + DestroyPod = 'destroyPod', + + /** 用户自定义 */ + UserDefined = 'userDefined' +} + +export enum RegistryUpdateTypeEnum { + /** 滚动更新 */ + RollingUpdate = 'RollingUpdate', + + /** 快速更新 */ + Recreate = 'Recreate', + + /** OnDelete */ + OnDelete = 'OnDelete' +} + +export const updateStrategyOptions = [ + { + text: t('启动新的Pod,停止旧的Pod'), + value: RollingUpdateTypeEnum.CreatePod + }, + + { + text: t('停止旧的Pod,启动新的Pod'), + value: RollingUpdateTypeEnum.DestroyPod + }, + + { + text: t('自定义'), + value: RollingUpdateTypeEnum.UserDefined + } +]; + +export const getUpdateTypeOptionsForKind = (kind: WorkloadKindEnum) => { + const fullOptions = [ + { + text: t('滚动更新(推荐)'), + value: RegistryUpdateTypeEnum.RollingUpdate + }, + + { + text: t('快速更新'), + value: RegistryUpdateTypeEnum.Recreate + }, + + { + text: 'OnDelete', + value: RegistryUpdateTypeEnum.OnDelete + } + ]; + + return fullOptions.filter(({ value }) => { + if (kind === WorkloadKindEnum.Deployment && value !== RegistryUpdateTypeEnum.OnDelete) return true; + + if ( + (kind === WorkloadKindEnum.StatefulSet || kind === WorkloadKindEnum.DaemonSet) && + value !== RegistryUpdateTypeEnum.Recreate + ) + return true; + + return false; + }); +}; diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/index.tsx b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/index.tsx new file mode 100644 index 0000000000..0f102085f1 --- /dev/null +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/index.tsx @@ -0,0 +1,315 @@ +import { t } from '@/tencent/tea-app/lib/i18n'; +import { getParamByUrl } from '@helper'; +import { router } from '@src/modules/cluster/router'; +import { TeaFormLayout } from '@src/modules/common/layouts/TeaFormLayout'; +import { workloadApi } from '@src/webApi'; +import { useRequest } from 'ahooks'; +import React from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { Breadcrumb, Button, Form, InputNumber, Radio, Select } from 'tea-component'; +import { + IModifyStrategyPanelProps, + RegistryUpdateTypeEnum, + RollingUpdateTypeEnum, + WorkloadKindEnum, + getUpdateTypeOptionsForKind, + updateStrategyOptions +} from './constants'; + +export const ModifyStrategyPanel = ({ kind }: IModifyStrategyPanelProps) => { + const clusterId = getParamByUrl('clusterId'); + const namespace = getParamByUrl('np'); + const resourceId = getParamByUrl('resourceIns'); + + const isDeployment = kind === WorkloadKindEnum.Deployment; + const isStatefulSet = kind === WorkloadKindEnum.StatefulSet; + const isDaemonSet = kind === WorkloadKindEnum.DaemonSet; + + const { handleSubmit, watch, control, reset } = useForm({ + mode: 'onBlur', + + defaultValues: { + updateType: RegistryUpdateTypeEnum.RollingUpdate, + updateInterval: 0, + updateStrategy: RollingUpdateTypeEnum.UserDefined, + maxSurge: 25, + maxUnavailable: 25, + batchSize: 1, + partition: 0 + } + }); + + const updateTypeWatch = watch('updateType'); + const updateStrategyWatch = watch('updateStrategy'); + + useRequest( + () => { + return workloadApi.fetchWorkloadResource({ namespace, clusterId, resourceId, kind }); + }, + { + ready: Boolean(namespace && resourceId && clusterId && kind), + + onSuccess(resource) { + const updateType = isDeployment ? resource?.spec?.strategy?.type : resource?.spec?.updateStrategy?.type; + + // 如果只有maxSurge 有值,而maxUnavailable为0,则为启动新的pod,停止旧的pod + let maxSurge = resource?.spec?.strategy?.rollingUpdate?.maxSurge ?? 0, + maxUnavailable = resource?.spec?.strategy?.rollingUpdate?.maxUnavailable ?? 0, + rollingUpdateStrategy = RollingUpdateTypeEnum.CreatePod, + minReadySeconds = resource?.spec?.minReadySeconds ?? 0, + partition = resource?.spec?.updateStrategy?.rollingUpdate?.partition ?? 0, + batchSize = 1; + + if (maxSurge === 0 && Number.isInteger(maxUnavailable)) { + rollingUpdateStrategy = RollingUpdateTypeEnum.DestroyPod; + batchSize = maxUnavailable; + maxSurge = '25%'; + maxUnavailable = '25%'; + } else if (maxUnavailable === 0 && Number.isInteger(maxSurge)) { + rollingUpdateStrategy = RollingUpdateTypeEnum.CreatePod; + batchSize = maxSurge; + maxSurge = '25%'; + maxUnavailable = '25%'; + } else { + rollingUpdateStrategy = RollingUpdateTypeEnum.UserDefined; + } + + if (isDaemonSet) { + maxUnavailable = resource?.spec?.updateStrategy?.rollingUpdate?.maxUnavailable ?? 0; + } + + const newConfig = { + updateType, + updateInterval: minReadySeconds, + updateStrategy: rollingUpdateStrategy, + maxSurge: parseInt(maxSurge), + maxUnavailable: parseInt(maxUnavailable), + batchSize, + partition + }; + + reset(newConfig); + } + } + ); + + function goBackToListPanel() { + router.navigate({ sub: 'sub', mode: 'list', type: 'resource', resourceName: kind }, { clusterId, np: namespace }); + } + + async function onSubmit({ + updateType, + updateInterval, + updateStrategy, + maxSurge, + maxUnavailable, + batchSize, + partition + }) { + const isRollingUpdate = updateType === RegistryUpdateTypeEnum.RollingUpdate; + + const isUserDefined = updateStrategy === RollingUpdateTypeEnum.UserDefined; + const isCreatePod = updateStrategy === RollingUpdateTypeEnum.CreatePod; + + const data = { + spec: { + minReadySeconds: isStatefulSet ? undefined : isRollingUpdate ? updateInterval : 0, + + strategy: isDeployment + ? { + type: updateType, + rollingUpdate: isRollingUpdate + ? { + maxSurge: isUserDefined ? `${maxSurge}%` : isCreatePod ? batchSize : 0, + + maxUnavailable: isUserDefined ? `${maxUnavailable}%` : isCreatePod ? 0 : batchSize + } + : null + } + : undefined, + + updateStrategy: isDeployment + ? undefined + : { + type: updateType, + rollingUpdate: isRollingUpdate + ? isStatefulSet + ? { partition } + : maxUnavailable + ? { maxUnavailable } + : undefined + : null + } + } + }; + + await workloadApi.updateWorkloadResource({ + namespace, + clusterId, + resourceId, + data, + kind + }); + + goBackToListPanel(); + } + + return ( + + + router.navigate({}, {})}>集群 + + + + {clusterId} + + + {`${kind}:${resourceId}(${namespace})`} + + 设置更新策略 + + } + footer={ + <> + + + + + } + > + <> +
+ 基本信息 + + + {clusterId} + + + + {namespace} + + + + {resourceId} + +
+ +
+ +
+ ( + + + + - - - - } - > - <> -
- 基本信息 - - - {clusterId} - - - - {namespace} - - - - {resourceId} + + ( + + - - )} - /> - - {(isDeployment || isDaemonSet) && updateTypeWatch === RegistryUpdateTypeEnum.RollingUpdate && ( - ( - - - - )} - /> + )} + /> + + {(isDeployment || isDaemonSet) && updateTypeWatch === RegistryUpdateTypeEnum.RollingUpdate && ( + ( + + + )} - - {isDeployment && updateTypeWatch === RegistryUpdateTypeEnum.RollingUpdate && ( - ( - - - {updateStrategyOptions.map(({ text, value }) => ( - - {text} - - ))} - - - )} - /> + /> + )} + + {isDeployment && updateTypeWatch === RegistryUpdateTypeEnum.RollingUpdate && ( + ( + + + {updateStrategyOptions.map(({ text, value }) => ( + + {text} + + ))} + + )} - - {updateTypeWatch === RegistryUpdateTypeEnum.RollingUpdate && ( - - - {isDeployment && updateStrategyWatch === RollingUpdateTypeEnum.UserDefined && ( - ( - - - - )} - /> + /> + )} + + {updateTypeWatch === RegistryUpdateTypeEnum.RollingUpdate && ( + + + {isDeployment && updateStrategyWatch === RollingUpdateTypeEnum.UserDefined && ( + ( + + + )} + /> + )} - {((isDeployment && updateStrategyWatch === RollingUpdateTypeEnum.UserDefined) || isDaemonSet) && ( - ( - - - - )} - /> + {((isDeployment && updateStrategyWatch === RollingUpdateTypeEnum.UserDefined) || isDaemonSet) && ( + ( + + + )} + /> + )} - {isDeployment && updateStrategyWatch !== RollingUpdateTypeEnum.UserDefined && ( - ( - - - - )} - /> + {isDeployment && updateStrategyWatch !== RollingUpdateTypeEnum.UserDefined && ( + ( + + + )} + /> + )} - {isStatefulSet && ( - ( - - - - )} - /> + {isStatefulSet && ( + ( + + + )} - - - )} - - -
+ /> + )} + + + )} + ); }; From e33fb02653db7cce0564de5bdfb6ab2727d79a3f Mon Sep 17 00:00:00 2001 From: johnny19941216 Date: Wed, 19 Apr 2023 19:21:00 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(console):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E8=B0=83=E5=BA=A6=E7=9A=84=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/console/package-lock.json | 22 +- web/console/package.json | 8 +- .../workloadUpdate/index.tsx | 38 +- .../affinityRulePanel.tsx | 412 ++++++++++++++++-- .../modifyNodeAffinityPanel/constants.ts | 136 +++++- .../modifyNodeAffinityPanel/index.tsx | 32 +- .../tolerationRulePanel.tsx | 201 +++++++++ .../modifyNodeAffinityPanel/useValidate.ts | 27 -- .../modifyStrategyPanel/constants.ts | 3 + .../modifyStrategyPanel/index.tsx | 86 ++-- .../src/modules/common/components/index.ts | 52 +-- .../components/validate-provider/index.tsx | 18 + web/console/src/webApi/index.ts | 2 + web/console/src/webApi/node.ts | 9 + 14 files changed, 892 insertions(+), 154 deletions(-) create mode 100644 web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/tolerationRulePanel.tsx create mode 100644 web/console/src/modules/common/components/validate-provider/index.tsx create mode 100644 web/console/src/webApi/node.ts diff --git a/web/console/package-lock.json b/web/console/package-lock.json index 047a3f9173..f6c49e6da3 100644 --- a/web/console/package-lock.json +++ b/web/console/package-lock.json @@ -1209,6 +1209,11 @@ } } }, + "@hookform/resolvers": { + "version": "3.0.1", + "resolved": "https://mirrors.tencent.com/npm/@hookform%2fresolvers/-/resolvers-3.0.1.tgz", + "integrity": "sha512-n5oOt0cLw9mQNW3/k9zWaPsNWQcc0k6Jpc7XUrg2Q/AqqsHp3IVa1juqHCxczXI6uXHBa69ILc4pdtsRGyuzsw==" + }, "@hypnosphi/create-react-context": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz", @@ -9618,9 +9623,9 @@ "integrity": "sha512-Q8ZP1B0vyAsJ3E8T8QEbje6ECdvD7xkNUd95iMoZuuXAuAtjEHoj8fL8SWxm3V/tG1GPwMkmKS/DPTYlVaaMVg==" }, "react-hook-form": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.10.1.tgz", - "integrity": "sha512-YRSkOZ0DJKjJ+PuCB6caFbFSbz6I4JQcGXVb/OeEIVE3YuXXYYJ/YQwodRy8HsZ5WA+DryoThWIeZZzPd5f9lQ==" + "version": "7.43.9", + "resolved": "https://mirrors.tencent.com/npm/react-hook-form/-/react-hook-form-7.43.9.tgz", + "integrity": "sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ==" }, "react-i18next": { "version": "9.0.2", @@ -11710,9 +11715,9 @@ "dev": true }, "typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "version": "5.0.4", + "resolved": "https://mirrors.tencent.com/npm/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true }, "uglify-js": { @@ -13064,6 +13069,11 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zod": { + "version": "3.21.4", + "resolved": "https://mirrors.tencent.com/npm/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" } } } diff --git a/web/console/package.json b/web/console/package.json index 32e2c049ae..1892300861 100644 --- a/web/console/package.json +++ b/web/console/package.json @@ -28,6 +28,7 @@ }, "license": "Apache-2.0", "dependencies": { + "@hookform/resolvers": "^3.0.1", "@novnc/novnc": "^1.3.0", "ahooks": "^3.3.10", "axios": "^0.21.1", @@ -60,7 +61,7 @@ "react-codemirror2": "^5.1.0", "react-dom": "^18.0.0", "react-final-form-hooks": "^2.0.2", - "react-hook-form": "^7.10.1", + "react-hook-form": "^7.43.9", "react-i18next": "^9.0.2", "react-redux": "^7.1.3", "react-transition-group": "^2.5.2", @@ -74,7 +75,8 @@ "ts-optchain": "^0.1.7", "use-immer": "^0.4.1", "uuid": "^8.3.1", - "validator": "^13.5.2" + "validator": "^13.5.2", + "zod": "^3.21.4" }, "devDependencies": { "@babel/core": "^7.0.0", @@ -124,7 +126,7 @@ "style-loader": "^2.0.0", "thread-loader": "^3.0.4", "ts-loader": "^9.2.1", - "typescript": "^4.3.5", + "typescript": "^5.0.4", "url-loader": "^4.1.1", "webpack": "^5.37.0", "webpack-bundle-analyzer": "^4.4.1", diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/index.tsx b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/index.tsx index 29eb668b86..2d351696c9 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/index.tsx +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/index.tsx @@ -11,9 +11,9 @@ import { ModifyNodeAffinityPanel } from './modifyNodeAffinityPanel'; import { ModifyStrategyPanel } from './modifyStrategyPanel'; export const WorkloadUpdatePanel = ({ kind, updateType }: IWrokloadUpdatePanelProps) => { - const clusterId = getParamByUrl('clusterId'); - const namespace = getParamByUrl('np'); - const resourceId = getParamByUrl('resourceIns'); + const clusterId = getParamByUrl('clusterId')!; + const namespace = getParamByUrl('np')!; + const resourceId = getParamByUrl('resourceIns')!; const [flag, setFlag] = useState(false); @@ -30,19 +30,21 @@ export const WorkloadUpdatePanel = ({ kind, updateType }: IWrokloadUpdatePanelPr router.navigate({ sub: 'sub', mode: 'list', type: 'resource', resourceName: kind }, { clusterId, np: namespace }); } - async function onSubmit(data) { - try { - await workloadApi.updateWorkloadResource({ - clusterId, - namespace, - resourceId, - kind, - data - }); - - goBackToListPanel(); - } catch (error) { - message.error({ content: error?.response?.data?.message ?? '请求失败!' }); + async function onSubmit(data?: any) { + if (data) { + try { + await workloadApi.updateWorkloadResource({ + clusterId, + namespace, + resourceId, + kind, + data + }); + + goBackToListPanel(); + } catch (error: any) { + message.error({ content: error?.response?.data?.message ?? '请求失败!' }); + } } setFlag(false); @@ -98,7 +100,9 @@ export const WorkloadUpdatePanel = ({ kind, updateType }: IWrokloadUpdatePanelPr )} - {updateType === UpdateTypeEnum.ModifyNodeAffinity && } + {updateType === UpdateTypeEnum.ModifyNodeAffinity && ( + + )} ); diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/affinityRulePanel.tsx b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/affinityRulePanel.tsx index 9fa3eed76f..0ee61a2e64 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/affinityRulePanel.tsx +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/affinityRulePanel.tsx @@ -1,32 +1,394 @@ -import React, { useState } from 'react'; -import { Button, Form, Input, InputNumber, Select } from 'tea-component'; +import { t } from '@/tencent/tea-app/lib/i18n'; +import { getParamByUrl, getReactHookFormStatusWithMessage } from '@helper'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { nodeApi } from '@src/webApi'; +import { useRequest } from 'ahooks'; +import React, { useEffect, useState } from 'react'; +import { Control, Controller, UseFormTrigger, useFieldArray, useForm, useFormState, useWatch } from 'react-hook-form'; +import { + Alert, + Button, + Form, + Input, + InputNumber, + Justify, + Modal, + Radio, + RadioGroup, + Select, + Table, + TableColumn, + Transfer +} from 'tea-component'; +import { + NodeAffinityOperatorEnum, + RuleType, + affinityRuleOperatorList, + generateDefaultRules, + ruleSchema +} from './constants'; -export const AffinityRulePanel = () => { - const [] = useState([ - { - weight: 1, - subRules: [ - { - key: '', - operator: '', - value: '' - } - ] - } - ]); +const SubRulePanel = ({ + control, + ruleIndex, + trigger +}: { + control: Control; + ruleIndex: number; + trigger: UseFormTrigger; +}) => { + const { + fields: subRules, + append: appendSubRule, + remove: removeSubRule, + update + } = useFieldArray({ + control, + name: `rules.${ruleIndex}.subRules` + }); + + const watchSubRules = useWatch({ control, name: `rules.${ruleIndex}.subRules` }); + + function needDisabledValue(operator) { + return operator === NodeAffinityOperatorEnum.Exists || operator === NodeAffinityOperatorEnum.DoesNotExist; + } + + const { errors } = useFormState({ control }); + console.log('SubRulePanel errors', errors); + const disabled = Boolean(errors?.rules?.[ruleIndex]); return ( -
- - - + <> + {subRules.map(({ id }, index) => ( + + ( + + + + )} + /> + + ( + + - - -
+ + ); +}; + +export const AffinityRulePanel = ({ showWeight = true, submiting, onSubmit, defaultRules = [] }) => { + const hasModal = !showWeight; + + const { + control, + formState: { errors }, + handleSubmit, + trigger + } = useForm({ + mode: 'onBlur', + + defaultValues: { + rules: defaultRules + }, + resolver: zodResolver(ruleSchema) + }); + + const { + fields: rules, + append, + remove + } = useFieldArray({ + control, + name: 'rules' + }); + + console.log('errors', errors); + + useEffect(() => { + if (!submiting) return; + + handleSubmit( + data => { + console.log('submiting', data); + + onSubmit(data?.rules ?? []); + }, + () => onSubmit() + )(); + }, [submiting, handleSubmit, onSubmit]); + + return ( + <> + {rules.map(({ id }, index) => ( +
+ + remove(index)} />} /> + + {showWeight && ( + ( + + + + )} + /> + )} + + + + ))} + + + ); }; + +function AddRuleButton({ append, hasModal }) { + const [visible, setVisible] = useState(false); + const [submiting, setSubmiting] = useState(false); + const [type, setType] = useState('scheduleByNode'); + + function handleClick() { + if (!hasModal) { + append({ weight: 1, subRules: [{ key: '', operator: NodeAffinityOperatorEnum.In, value: '' }] }); + } else { + setVisible(true); + } + } + + function handleOk() { + setSubmiting(true); + } + + function onSubmit(data) { + if (data) { + console.log('append data', data); + append(data); + + setVisible(false); + } + + setSubmiting(false); + } + + return ( + <> + + + {hasModal && ( + setVisible(false)}> + +
+ + setType(value)}> + 指定节点调度 + 自定义Label规则 + + + + {type === 'customLabel' && ( + + + + )} + + {type === 'scheduleByNode' && ( + + + + )} +
+
+ + + + + + +
+ )} + + ); +} + +const { scrollable, selectable, removeable } = Table.addons; + +function NodeTransferPanel({ submiting, onSubmit }) { + const clusterId = getParamByUrl('clusterId')!; + + const [selectedKeys, setSelectedKeys] = useState([]); + const [errorMessage, setErrorMessage] = useState(''); + + const { data: nodeList = [] } = useRequest( + async () => { + const rsp = await nodeApi.fetchNodeList({ clusterId }); + + console.log('NodeTransferPanel--->', rsp); + + return rsp?.items ?? []; + }, + { ready: Boolean(clusterId) } + ); + + const columns: TableColumn[] = [ + { + key: 'metadata.name', + header: 'ID/节点名' + }, + + { + key: 'ip', + header: 'IP地址', + render: ({ metadata }) => metadata?.name + } + ]; + + useEffect(() => { + if (selectedKeys.length > 0) { + setErrorMessage(''); + } + + if (!submiting) return; + + if (selectedKeys.length < 1) { + setErrorMessage('节点不能为空,请选择节点'); + onSubmit(); + + return; + } + + onSubmit([ + { + weight: 1, + subRules: [ + { + key: 'kubernetes.io/hostname', + operator: NodeAffinityOperatorEnum.In, + value: selectedKeys.join(';') + } + ] + } + ]); + }, [submiting, onSubmit, selectedKeys]); + + return ( + <> + {errorMessage && {errorMessage}} + + setSelectedKeys(keys), + rowSelect: true + }) + ]} + /> + + } + rightCell={ + +
selectedKeys.includes(item?.metadata?.name))} + recordKey="metadata.name" + addons={[ + removeable({ + onRemove: key => { + console.log('remove--->', key); + + setSelectedKeys(keys => keys.filter(item => item !== key)); + } + }) + ]} + /> + + } + /> + + ); +} diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/constants.ts b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/constants.ts index 80741dca05..907d8e579d 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/constants.ts +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/constants.ts @@ -1,4 +1,6 @@ import { t } from '@/tencent/tea-app/lib/i18n'; +import validateJS from 'validator'; +import { z } from 'zod'; /** 节点亲和性调度的方式 */ export enum NodeAffinityTypeEnum { @@ -31,26 +33,148 @@ export enum NodeAffinityOperatorEnum { export const affinityRuleOperatorList = [ { value: NodeAffinityOperatorEnum.In, - tip: t('Label的value在列表中') + tooltip: t('Label的value在列表中') }, { value: NodeAffinityOperatorEnum.NotIn, - tip: t('Label的value不在列表中') + tooltip: t('Label的value不在列表中') }, { value: NodeAffinityOperatorEnum.Exists, - tip: t('Label的key存在') + tooltip: t('Label的key存在') }, { value: NodeAffinityOperatorEnum.DoesNotExist, - tip: t('Labe的key不存在') + tooltip: t('Labe的key不存在') }, { value: NodeAffinityOperatorEnum.Gt, - tip: t('Label的值大于列表值(字符串匹配)') + tooltip: t('Label的值大于列表值(字符串匹配)') }, { value: NodeAffinityOperatorEnum.Lt, - tip: t('Label的值小于列表值(字符串匹配)') + tooltip: t('Label的值小于列表值(字符串匹配)') + } +]; + +export const ruleSchema = z.object({ + rules: z.array( + z.object({ + weight: z + .number() + .int(t('权重必须为整数')) + .gte(0, { message: t('权重必须在1~100之前') }) + .lte(100, { message: t('权重必须在1~100之前') }), + subRules: z.array( + z + .object({ + key: z + .string() + .min(1, { message: t('标签名不能为空') }) + .max(63, { message: t('标签名长度不能超过63个字符') }) + .regex(/^([A-Za-z0-9][-A-Za-z0-9_./]*)?[A-Za-z0-9]$/, { message: '标签名格式不正确' }), + operator: z.nativeEnum(NodeAffinityOperatorEnum), + value: z.string() + }) + .superRefine(({ operator, value }, ctx) => { + console.log('superRefine', value, operator); + + const values = value.split(';'); + + let message = ''; + + if (operator === NodeAffinityOperatorEnum.Exists || operator === NodeAffinityOperatorEnum.DoesNotExist) + return; + + const value0 = values?.[0]; + + if (!value0) { + message = t('自定义规则不能为空'); + } else { + if (operator === NodeAffinityOperatorEnum.Lt || operator === NodeAffinityOperatorEnum.Gt) { + if (values.length > 1) { + message = t('Gt和Lt操作符只支持一个value值'); + } else if (!validateJS.isNumeric(value0)) { + message = t('Gt和Lt操作符value值格式必须为数字'); + } + } else if (values.some(item => !item)) { + message = t('标签值不能为空'); + } else if (values.some(item => item?.length > 63)) { + message = t('标签值长度不能超过63个字符'); + } else if ( + values.some(item => !validateJS.matches(item, /^([A-Za-z0-9][-A-Za-z0-9_./]*)?[A-Za-z0-9]$/)) + ) { + message = t('标签格式不正确'); + } + } + + console.log('message--->', message); + + if (message) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message, + path: ['value'] + }); + } + }) + ) + }) + ) +}); + +export const generateDefaultRules = () => [ + { + weight: 1, + subRules: [ + { + key: '', + operator: NodeAffinityOperatorEnum.In, + value: '' + } + ] + } +]; + +export type RuleType = z.infer; + +export enum TolerationOperatorEnum { + Equal = 'Equal', + Exists = 'Exists' +} + +export const tolerationOperatorOptions = [ + { + value: TolerationOperatorEnum.Equal + }, + + { + value: TolerationOperatorEnum.Exists + } +]; + +export enum TolerationEffectEnum { + All = 'All', + NoSchedule = 'NoSchedule', + PreferNoSchedule = 'PreferNoSchedule', + NoExecute = 'NoExecute' +} + +export const tolerationEffectOptions = [ + { + value: TolerationEffectEnum.All, + text: '匹配全部' + }, + + { + value: TolerationEffectEnum.NoSchedule + }, + + { + value: TolerationEffectEnum.PreferNoSchedule + }, + + { + value: TolerationEffectEnum.NoExecute } ]; diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/index.tsx b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/index.tsx index 3533a345d3..5e406127ae 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/index.tsx +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/index.tsx @@ -1,14 +1,18 @@ +import { t } from '@/tencent/tea-app/lib/i18n'; import React, { useState } from 'react'; import { Form, Radio } from 'tea-component'; import { AffinityRulePanel } from './affinityRulePanel'; import { NodeAffinityTypeEnum, TolerationTypeEnum } from './constants'; +import { TolerationRulePanel } from './tolerationRulePanel'; -export const ModifyNodeAffinityPanel = () => { +export const ModifyNodeAffinityPanel = ({ flag, onSubmit }) => { const [state, setState] = useState({ nodeAffinityType: NodeAffinityTypeEnum.Unset, tolerationType: TolerationTypeEnum.UnSet }); + function handleSubmit() {} + return (
@@ -21,9 +25,23 @@ export const ModifyNodeAffinityPanel = () => { - - - + {state.nodeAffinityType === NodeAffinityTypeEnum.Rule && ( + <> + + + + + + + + + )} { 使用容忍调度 + + {state.tolerationType === TolerationTypeEnum.Set && ( + + + + )} ); }; diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/tolerationRulePanel.tsx b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/tolerationRulePanel.tsx new file mode 100644 index 0000000000..9100523b00 --- /dev/null +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/tolerationRulePanel.tsx @@ -0,0 +1,201 @@ +import { getReactHookFormStatusWithMessage } from '@helper'; +import { ValidateProvider } from '@src/modules/common/components'; +import React from 'react'; +import { Controller, useFieldArray, useForm } from 'react-hook-form'; +import { Button, Input, InputNumber, Select, Table, TableColumn } from 'tea-component'; +import { + TolerationEffectEnum, + TolerationOperatorEnum, + tolerationEffectOptions, + tolerationOperatorOptions +} from './constants'; + +export const TolerationRulePanel = () => { + const { control, watch } = useForm<{ + rules: { + key: string; + operator: TolerationOperatorEnum; + value: string; + effect: TolerationEffectEnum; + time: number; + }[]; + }>({ + mode: 'onBlur', + defaultValues: { + rules: [] + } + }); + + const { fields, remove, append } = useFieldArray({ + control, + name: 'rules' + }); + + const rulesWatch = watch('rules'); + + const columns: TableColumn[] = [ + { + key: 'key', + header: '标签名', + render(record, recordKey, index) { + return ( + ', key, rules); + if (rules?.[index]?.operator === TolerationOperatorEnum.Equal && !key.trim()) { + return 'key不能为空'; + } + } + }} + render={({ field, ...another }) => ( + + + + )} + /> + ); + } + }, + + { + key: 'operator', + header: '操作符', + render(record, recordKey, index) { + return ( + ( + + + + )} + /> + ); + } + }, + + { + key: 'effect', + header: '效果', + render(record, recordKey, index) { + return ( + ( + +
+ + ); +}; diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/useValidate.ts b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/useValidate.ts index afed8ae32b..e69de29bb2 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/useValidate.ts +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/useValidate.ts @@ -1,27 +0,0 @@ -const state = [ - { - weight: { - value: 1, - validate(value) { - if (!value) return { status: 'error', message: '权重不能为空' }; - } - }, - subRules: [ - { - key: { - value: '', - validate(value) { - if (!value) return { status: 'error', message: 'key不能为空' }; - } - }, - operator: '', - value: { - value: '', - validate(value) { - if (!value) return { status: 'error', message: 'value不能为空' }; - } - } - } - ] - } -]; diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/constants.ts b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/constants.ts index 633324891b..109aab7b99 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/constants.ts +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/constants.ts @@ -3,6 +3,9 @@ import { WorkloadKindEnum } from '../constants'; export interface IModifyStrategyPanelProps { kind: WorkloadKindEnum; + resource: any; + onSubmit: (data?: any) => void; + flag: boolean; } /** 滚动更新的策略选择 */ diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/index.tsx b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/index.tsx index 1acc874104..c3b6adfc09 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/index.tsx +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyStrategyPanel/index.tsx @@ -4,13 +4,14 @@ import { Controller, useForm } from 'react-hook-form'; import { Form, InputNumber, Radio, Select } from 'tea-component'; import { WorkloadKindEnum } from '../constants'; import { + IModifyStrategyPanelProps, RegistryUpdateTypeEnum, RollingUpdateTypeEnum, getUpdateTypeOptionsForKind, updateStrategyOptions } from './constants'; -export const ModifyStrategyPanel = ({ kind, resource, onSubmit, flag }) => { +export const ModifyStrategyPanel = ({ kind, resource, onSubmit, flag }: IModifyStrategyPanelProps) => { const isDeployment = kind === WorkloadKindEnum.Deployment; const isStatefulSet = kind === WorkloadKindEnum.StatefulSet; const isDaemonSet = kind === WorkloadKindEnum.DaemonSet; @@ -77,46 +78,49 @@ export const ModifyStrategyPanel = ({ kind, resource, onSubmit, flag }) => { useEffect(() => { if (!flag) return; - handleSubmit(({ updateType, updateInterval, updateStrategy, maxSurge, maxUnavailable, batchSize, partition }) => { - const isRollingUpdate = updateType === RegistryUpdateTypeEnum.RollingUpdate; - - const isUserDefined = updateStrategy === RollingUpdateTypeEnum.UserDefined; - const isCreatePod = updateStrategy === RollingUpdateTypeEnum.CreatePod; - - const data = { - spec: { - minReadySeconds: isStatefulSet ? undefined : isRollingUpdate ? updateInterval : 0, - - strategy: isDeployment - ? { - type: updateType, - rollingUpdate: isRollingUpdate - ? { - maxSurge: isUserDefined ? `${maxSurge}%` : isCreatePod ? batchSize : 0, - - maxUnavailable: isUserDefined ? `${maxUnavailable}%` : isCreatePod ? 0 : batchSize - } - : null - } - : undefined, - - updateStrategy: isDeployment - ? undefined - : { - type: updateType, - rollingUpdate: isRollingUpdate - ? isStatefulSet - ? { partition } - : maxUnavailable - ? { maxUnavailable } - : undefined - : null - } - } - }; - - onSubmit(data); - })(); + handleSubmit( + ({ updateType, updateInterval, updateStrategy, maxSurge, maxUnavailable, batchSize, partition }) => { + const isRollingUpdate = updateType === RegistryUpdateTypeEnum.RollingUpdate; + + const isUserDefined = updateStrategy === RollingUpdateTypeEnum.UserDefined; + const isCreatePod = updateStrategy === RollingUpdateTypeEnum.CreatePod; + + const data = { + spec: { + minReadySeconds: isStatefulSet ? undefined : isRollingUpdate ? updateInterval : 0, + + strategy: isDeployment + ? { + type: updateType, + rollingUpdate: isRollingUpdate + ? { + maxSurge: isUserDefined ? `${maxSurge}%` : isCreatePod ? batchSize : 0, + + maxUnavailable: isUserDefined ? `${maxUnavailable}%` : isCreatePod ? 0 : batchSize + } + : null + } + : undefined, + + updateStrategy: isDeployment + ? undefined + : { + type: updateType, + rollingUpdate: isRollingUpdate + ? isStatefulSet + ? { partition } + : maxUnavailable + ? { maxUnavailable } + : undefined + : null + } + } + }; + + onSubmit(data); + }, + () => onSubmit() + )(); }, [flag, handleSubmit, isDaemonSet, isDeployment, isStatefulSet, onSubmit]); return ( diff --git a/web/console/src/modules/common/components/index.ts b/web/console/src/modules/common/components/index.ts index 61c4f5d0f4..ec42708fd6 100644 --- a/web/console/src/modules/common/components/index.ts +++ b/web/console/src/modules/common/components/index.ts @@ -15,37 +15,39 @@ * WARRANTIES OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ +export { NavigateLink } from './NavigateLink'; +export { ButtonBar, ButtonBarItem } from './buttonbar'; +export { CardMenu, CardMenuItem } from './cardMenu'; +export { Clip } from './clip'; +export { CodeMirrorEditor } from './codemirror'; +export { CommonBar, CommonBarItem } from './commonbar'; +export { DateTimePicker } from './datetimepicker'; +export { DownMenu, DownMenuItem } from './downMenu'; +export { DropdownMenu, DropdownMenuItem } from './dropdownmenu'; +export { emptyTips } from './empty'; +export { ErrorGuide, ErrorTip } from './errortip'; +export { FormItem, FormItemProps } from './formitem'; +export { GridTable } from './gridtable'; +export { HeadBubble } from './headbubble'; export { InputField, InputFieldProps } from './inputfield'; export { InputRange, InputRangeProps } from './inputrange'; -export { SelectList, SelectListProps } from './select'; -export { TabSelector, TabSelectorProps, TabItem } from './tabselector'; -export { ListItem, ListItemProps } from './listitem'; -export { FormItem, FormItemProps } from './formitem'; -export { TipDialog } from './tipdialog'; -export { TipInfo } from './tipinfo'; export { LinkButton, LinkButtonProps } from './linkbutton'; -export { Clip } from './clip'; +export { ListItem, ListItemProps } from './listitem'; +export * from './logviewer'; export { Markdown } from './markdown'; -export { DropdownMenu, DropdownMenuItem } from './dropdownmenu'; -export { HeadBubble } from './headbubble'; -export { ButtonBar, ButtonBarItem } from './buttonbar'; -export { CommonBar, CommonBarItem } from './commonbar'; -export { WorkflowDialog } from './workflowdialog'; -export { ErrorGuide, ErrorTip } from './errortip'; -export { StepItem, StepTab, StepTabProps, StepTabBody, StepTabBodyProps, Step, StepProps } from './stepTab'; -export { RegionBar, RegionBarProps } from './regionbar'; +export * from './monitorPanel'; export { Network, NetworkProps, VpcNetwork } from './network'; -export { SidePanel, SidePanelProps } from './sidepanel'; -export * from './logviewer'; -export { TagSearchBox, TagSearchBoxProps } from './tagsearchbox'; -export { CodeMirrorEditor } from './codemirror'; +export { RegionBar, RegionBarProps } from './regionbar'; export { ResourceList } from './resourcelist'; -export { DownMenu, DownMenuItem } from './downMenu'; -export { CardMenu, CardMenuItem } from './cardMenu'; export * from './resourceselector'; -export { DateTimePicker } from './datetimepicker'; -export { GridTable } from './gridtable'; +export { SelectList, SelectListProps } from './select'; +export { SidePanel, SidePanelProps } from './sidepanel'; +export { Step, StepItem, StepProps, StepTab, StepTabBody, StepTabBodyProps, StepTabProps } from './stepTab'; +export { TabItem, TabSelector, TabSelectorProps } from './tabselector'; +export { TagSearchBox, TagSearchBoxProps } from './tagsearchbox'; +export { TipDialog } from './tipdialog'; +export { TipInfo } from './tipinfo'; export { TransferTable, TransferTableProps } from './transferTable'; -export { emptyTips } from './empty'; -export { NavigateLink } from './NavigateLink'; +export * from './validate-provider'; +export { WorkflowDialog } from './workflowdialog'; export * from './yamleditor'; diff --git a/web/console/src/modules/common/components/validate-provider/index.tsx b/web/console/src/modules/common/components/validate-provider/index.tsx new file mode 100644 index 0000000000..753de517b9 --- /dev/null +++ b/web/console/src/modules/common/components/validate-provider/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Bubble } from 'tea-component'; + +interface Props { + status?: 'validating' | 'error' | 'success'; + message?: string; + children: React.ReactElement; +} + +export const ValidateProvider = ({ status, message, children }: Props) => { + const classNames = status === 'error' ? 'is-error' : undefined; + + return ( + + {children} + + ); +}; diff --git a/web/console/src/webApi/index.ts b/web/console/src/webApi/index.ts index 68431ef13c..2c27e5d9f6 100644 --- a/web/console/src/webApi/index.ts +++ b/web/console/src/webApi/index.ts @@ -9,3 +9,5 @@ export * as PVCAPI from './pvc'; export * as registryApi from './registry'; export * as workloadApi from './workload'; + +export * as nodeApi from './node'; diff --git a/web/console/src/webApi/node.ts b/web/console/src/webApi/node.ts new file mode 100644 index 0000000000..c078b6701b --- /dev/null +++ b/web/console/src/webApi/node.ts @@ -0,0 +1,9 @@ +import { Request } from './request'; + +export function fetchNodeList({ clusterId }) { + return Request.get(`/api/v1/nodes`, { + headers: { + 'X-TKE-ClusterName': clusterId + } + }); +} From cbb9fc01ef37c2f4173c00a34b7f4acd430eeebb Mon Sep 17 00:00:00 2001 From: johnny19941216 Date: Fri, 21 Apr 2023 18:00:07 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat(console):=20=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E8=B0=83=E5=BA=A6=E6=8E=A5=E5=8F=A3=E8=B0=83=E8=AF=95=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/console/helpers/Validator.ts | 4 +- web/console/helpers/index.ts | 1 + web/console/helpers/satisfyClusterVersion.ts | 28 + .../cluster/actions/resourceActions.ts | 2 +- .../resourceEdition/UpdateResourcePanel.tsx | 6 +- .../workloadUpdate/constants.ts | 6 +- .../workloadUpdate/index.tsx | 141 ++- .../affinityRulePanel.tsx | 385 ++------ .../appendAffinityRuleButton.tsx | 346 +++++++ .../modifyNodeAffinityPanel/constants.ts | 194 ++-- .../modifyNodeAffinityPanel/index.tsx | 269 +++++- .../tolerationRulePanel.tsx | 60 +- .../modifyStrategyPanel/index.tsx | 290 +++--- web/console/src/webApi/apiPath/apiKind.ts | 289 ++++++ .../src/webApi/apiPath/apiServerVersion.ts | 6 + web/console/src/webApi/apiPath/apiVersion.ts | 892 ++++++++++++++++++ web/console/src/webApi/apiPath/index.ts | 9 + web/console/src/webApi/request.ts | 3 +- web/console/src/webApi/workload.ts | 19 +- 19 files changed, 2263 insertions(+), 687 deletions(-) create mode 100644 web/console/helpers/satisfyClusterVersion.ts create mode 100644 web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/appendAffinityRuleButton.tsx create mode 100644 web/console/src/webApi/apiPath/apiKind.ts create mode 100644 web/console/src/webApi/apiPath/apiServerVersion.ts create mode 100644 web/console/src/webApi/apiPath/apiVersion.ts create mode 100644 web/console/src/webApi/apiPath/index.ts diff --git a/web/console/helpers/Validator.ts b/web/console/helpers/Validator.ts index 880cd7ac2d..e0447ec33b 100644 --- a/web/console/helpers/Validator.ts +++ b/web/console/helpers/Validator.ts @@ -15,8 +15,8 @@ * WARRANTIES OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ -import { Validation } from 'src/modules/common'; import { ControllerFieldState, UseFormStateReturn } from 'react-hook-form'; +import { Validation } from 'src/modules/common'; export interface Rule { /**标签名 */ @@ -159,10 +159,10 @@ export function getReactHookFormStatusWithMessage({ status?: 'error' | 'success'; message?: string; } { - console.log('getReactHookFormStatus:', fieldState, formState); if (!fieldState.isTouched && !fieldState.isDirty && !formState.isSubmitted) { return {}; } + return fieldState.invalid ? { status: 'error', diff --git a/web/console/helpers/index.ts b/web/console/helpers/index.ts index 50b43eacbb..c12622b2d5 100644 --- a/web/console/helpers/index.ts +++ b/web/console/helpers/index.ts @@ -39,4 +39,5 @@ export { export { ResetStoreAction, generateResetableReducer } from './reduxStore'; export { assureRegion } from './regionLint'; export * from './request'; +export { satisfyClusterVersion } from './satisfyClusterVersion'; export { cutNsStartClusterId, parseQueryString, reduceK8sQueryString, reduceK8sRestfulPath, reduceNs } from './urlUtil'; diff --git a/web/console/helpers/satisfyClusterVersion.ts b/web/console/helpers/satisfyClusterVersion.ts new file mode 100644 index 0000000000..027369d790 --- /dev/null +++ b/web/console/helpers/satisfyClusterVersion.ts @@ -0,0 +1,28 @@ +type CompareType = 'gt' | 'lt' | 'ge' | 'le' | 'eq'; +/** + * 判断集群版本是否符合要求 + * @param clusterVersion: string 当前的集群版本 + * @param targetClusterVersion: string 目标对比集群版本 + * @param type: lt | gt | ge | le | eq 对比的类型,默认为ge + */ +export const satisfyClusterVersion = (clusterVersion = '', targetClusterVersion = '', type: CompareType = 'ge') => { + const [major, version] = (clusterVersion ?? '').split('.'); + const [minMajor, minVersion] = (targetClusterVersion ?? '').split('.'); + + const majorNum = +major || 0, + versionNum = +version || 0, + minMajorNum = +minMajor || 0, + minVersionNum = +minVersion || 0; + + if (type === 'ge') { + return majorNum > minMajorNum || (majorNum === minMajorNum && versionNum >= minVersionNum); + } else if (type === 'gt') { + return majorNum > minMajorNum || (majorNum === minMajorNum && versionNum > minVersionNum); + } else if (type === 'lt') { + return majorNum < minMajorNum || (majorNum === minMajorNum && versionNum < minVersionNum); + } else if (type === 'le') { + return majorNum < minMajorNum || (majorNum === minMajorNum && versionNum <= minVersionNum); + } else if (type === 'eq') { + return majorNum === minMajorNum && versionNum === minVersionNum; + } +}; diff --git a/web/console/src/modules/cluster/actions/resourceActions.ts b/web/console/src/modules/cluster/actions/resourceActions.ts index 4909d7a508..2dd8b3d668 100644 --- a/web/console/src/modules/cluster/actions/resourceActions.ts +++ b/web/console/src/modules/cluster/actions/resourceActions.ts @@ -21,7 +21,7 @@ import { ResourceInfo } from '../../common/models'; import { includes } from '../../common/utils'; import { IsResourceShowLoadingIcon } from '../components/resource/resourceTableOperation/ResourceTablePanel'; import * as ActionType from '../constants/ActionType'; -import { FFReduxActionName, PollEventName, ResourceNeedJudgeLoading } from '../constants/Config'; +import { FFReduxActionName, ResourceNeedJudgeLoading } from '../constants/Config'; import { Resource, ResourceFilter, RootState } from '../models'; import { router } from '../router'; import * as WebAPI from '../WebAPI'; diff --git a/web/console/src/modules/cluster/components/resource/resourceEdition/UpdateResourcePanel.tsx b/web/console/src/modules/cluster/components/resource/resourceEdition/UpdateResourcePanel.tsx index f3b4f61d56..939a37b51a 100644 --- a/web/console/src/modules/cluster/components/resource/resourceEdition/UpdateResourcePanel.tsx +++ b/web/console/src/modules/cluster/components/resource/resourceEdition/UpdateResourcePanel.tsx @@ -49,6 +49,8 @@ export class UpdateResourcePanel extends React.Component { const resourceType = urlParams['resourceName'], updateType = urlParams['tab']; + const { clusterVersion } = this.props; + if (resourceType === 'svc' && updateType === 'modifyType') { content = ; headTitle = t('更新访问方式'); @@ -71,10 +73,10 @@ export class UpdateResourcePanel extends React.Component { content = ; headTitle = t('更新后端负载'); } else if ( - ['deployment', 'statefulset', 'daemonset'].includes(resourceType) && + ['deployment', 'statefulset', 'daemonset', 'cronjob'].includes(resourceType) && ['modifyStrategy', 'modifyNodeAffinity'].includes(updateType) ) { - return ; + return ; } return ( diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/constants.ts b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/constants.ts index 1ea08a5e33..cd61403062 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/constants.ts +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/constants.ts @@ -3,7 +3,9 @@ export enum WorkloadKindEnum { StatefulSet = 'statefulset', - DaemonSet = 'daemonset' + DaemonSet = 'daemonset', + + Cronjob = 'cronjob' } export enum UpdateTypeEnum { @@ -16,6 +18,8 @@ export interface IWrokloadUpdatePanelProps { kind: WorkloadKindEnum; updateType: UpdateTypeEnum; + + clusterVersion: string; } export const updateType2text = { diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/index.tsx b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/index.tsx index 2d351696c9..eb59401e64 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/index.tsx +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/index.tsx @@ -1,109 +1,102 @@ import { t } from '@/tencent/tea-app/lib/i18n'; import { getParamByUrl } from '@helper'; import { router } from '@src/modules/cluster/router'; -import { TeaFormLayout } from '@src/modules/common/layouts/TeaFormLayout'; import { workloadApi } from '@src/webApi'; import { useRequest } from 'ahooks'; -import React, { useState } from 'react'; -import { Breadcrumb, Button, Form, message } from 'tea-component'; +import React from 'react'; +import { Breadcrumb, Form } from 'tea-component'; import { IWrokloadUpdatePanelProps, UpdateTypeEnum, updateType2text } from './constants'; import { ModifyNodeAffinityPanel } from './modifyNodeAffinityPanel'; import { ModifyStrategyPanel } from './modifyStrategyPanel'; -export const WorkloadUpdatePanel = ({ kind, updateType }: IWrokloadUpdatePanelProps) => { +export const WorkloadUpdatePanel = ({ kind, updateType, clusterVersion }: IWrokloadUpdatePanelProps) => { const clusterId = getParamByUrl('clusterId')!; const namespace = getParamByUrl('np')!; const resourceId = getParamByUrl('resourceIns')!; - const [flag, setFlag] = useState(false); - const { data: resource } = useRequest( () => { - return workloadApi.fetchWorkloadResource({ namespace, clusterId, resourceId, kind }); + return workloadApi.fetchWorkloadResource({ namespace, clusterId, resourceId, kind, clusterVersion }); }, { ready: Boolean(namespace && resourceId && clusterId && kind) } ); - function goBackToListPanel() { - router.navigate({ sub: 'sub', mode: 'list', type: 'resource', resourceName: kind }, { clusterId, np: namespace }); + async function handleUpdate(data: any) { + await workloadApi.updateWorkloadResource({ + clusterId, + namespace, + resourceId, + kind, + data, + clusterVersion + }); + + goBackToListPanel(); } - async function onSubmit(data?: any) { - if (data) { - try { - await workloadApi.updateWorkloadResource({ - clusterId, - namespace, - resourceId, - kind, - data - }); - - goBackToListPanel(); - } catch (error: any) { - message.error({ content: error?.response?.data?.message ?? '请求失败!' }); - } - } - - setFlag(false); + function goBackToListPanel() { + router.navigate({ sub: 'sub', mode: 'list', type: 'resource', resourceName: kind }, { clusterId, np: namespace }); } - return ( - - - router.navigate({}, {})}>集群 - - - - {clusterId} - - - {`${kind}:${resourceId}(${namespace})`} + const title = ( + + + router.navigate({}, {})}>集群 + - {updateType2text[updateType]} - - } - footer={ - <> - + + {clusterId} + - - - } - > - <> -
- 基本信息 + {`${kind}:${resourceId}(${namespace})`} - - {clusterId} - + {updateType2text[updateType]} + + ); - - {namespace} - + const baseInfo = ( + + 基本信息 - - {`${resourceId} (${kind})`} - - + + {clusterId} + -
+ + {namespace} + - {updateType === UpdateTypeEnum.ModifyStrategy && ( - - )} + + {`${resourceId} (${kind})`} + + + ); - {updateType === UpdateTypeEnum.ModifyNodeAffinity && ( - - )} - -
+ return ( + <> + {updateType === UpdateTypeEnum.ModifyStrategy && ( + + )} + + {updateType === UpdateTypeEnum.ModifyNodeAffinity && ( + + )} + ); }; diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/affinityRulePanel.tsx b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/affinityRulePanel.tsx index 0ee61a2e64..587e210dae 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/affinityRulePanel.tsx +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/affinityRulePanel.tsx @@ -1,42 +1,76 @@ import { t } from '@/tencent/tea-app/lib/i18n'; -import { getParamByUrl, getReactHookFormStatusWithMessage } from '@helper'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { nodeApi } from '@src/webApi'; -import { useRequest } from 'ahooks'; -import React, { useEffect, useState } from 'react'; -import { Control, Controller, UseFormTrigger, useFieldArray, useForm, useFormState, useWatch } from 'react-hook-form'; -import { - Alert, - Button, - Form, - Input, - InputNumber, - Justify, - Modal, - Radio, - RadioGroup, - Select, - Table, - TableColumn, - Transfer -} from 'tea-component'; +import { getReactHookFormStatusWithMessage } from '@helper'; +import React from 'react'; +import { Controller, useFieldArray, useFormContext, useFormState } from 'react-hook-form'; +import { Button, Form, Input, InputNumber, Justify, Select } from 'tea-component'; +import { AddRuleButton } from './appendAffinityRuleButton'; import { + AffinityTypeEnum, + NodeAffinityFormType, NodeAffinityOperatorEnum, - RuleType, - affinityRuleOperatorList, - generateDefaultRules, - ruleSchema + affinityRuleOperatorList } from './constants'; -const SubRulePanel = ({ - control, - ruleIndex, - trigger -}: { - control: Control; - ruleIndex: number; - trigger: UseFormTrigger; -}) => { +export function AffinityRulePanel({ subName }: { subName: AffinityTypeEnum }) { + const showWeight = subName === AffinityTypeEnum.Attempt; + + const { control } = useFormContext(); + + const { + fields: rules, + append, + remove + } = useFieldArray({ + control, + name: `affinityRules.${subName}` + }); + + return ( + <> + {rules.map(({ id }, index) => ( +
+ + remove(index)} />} /> + + {showWeight && ( + ( + + + + )} + /> + )} + + + + ))} + + {subName === AffinityTypeEnum.Force && } + + {subName === AffinityTypeEnum.Attempt && ( + + )} + + ); +} + +const SubRulePanel = ({ ruleIndex, subName }: { ruleIndex: number; subName: AffinityTypeEnum }) => { + const { control, trigger, watch } = useFormContext(); + + const prePath: + | `affinityRules.force.${number}.subRules` + | `affinityRules.attempt.${number}.subRules` = `affinityRules.${subName}.${ruleIndex}.subRules`; + const { fields: subRules, append: appendSubRule, @@ -44,18 +78,16 @@ const SubRulePanel = ({ update } = useFieldArray({ control, - name: `rules.${ruleIndex}.subRules` + name: prePath }); - const watchSubRules = useWatch({ control, name: `rules.${ruleIndex}.subRules` }); - function needDisabledValue(operator) { return operator === NodeAffinityOperatorEnum.Exists || operator === NodeAffinityOperatorEnum.DoesNotExist; } const { errors } = useFormState({ control }); - console.log('SubRulePanel errors', errors); - const disabled = Boolean(errors?.rules?.[ruleIndex]); + + const disabled = Boolean(errors?.affinityRules?.[subName]?.[ruleIndex]?.subRules); return ( <> @@ -63,7 +95,7 @@ const SubRulePanel = ({ ( (
setSelectedKeys(keys), - rowSelect: true - }) - ]} - /> - - } - rightCell={ - -
selectedKeys.includes(item?.metadata?.name))} - recordKey="metadata.name" - addons={[ - removeable({ - onRemove: key => { - console.log('remove--->', key); - - setSelectedKeys(keys => keys.filter(item => item !== key)); - } - }) - ]} - /> - - } - /> - - ); -} diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/appendAffinityRuleButton.tsx b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/appendAffinityRuleButton.tsx new file mode 100644 index 0000000000..2724de234f --- /dev/null +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/appendAffinityRuleButton.tsx @@ -0,0 +1,346 @@ +import { t } from '@/tencent/tea-app/lib/i18n'; +import { getParamByUrl, getReactHookFormStatusWithMessage } from '@helper'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { nodeApi } from '@src/webApi'; +import { useRequest } from 'ahooks'; +import React, { useState } from 'react'; +import { Controller, FormProvider, useFieldArray, useForm, useFormContext, useFormState } from 'react-hook-form'; +import { + Alert, + Button, + Form, + Input, + InputNumber, + Justify, + Modal, + Radio, + RadioGroup, + Select, + Table, + TableColumn, + Transfer +} from 'tea-component'; +import { + AppendAffinityRuleFormType, + NodeAffinityOperatorEnum, + ScheduleTypeEnum, + affinityRuleOperatorList, + appendAffinityRuleSchema, + defaultAppendAffinityRuleFormData +} from './constants'; + +export function AddRuleButton({ append }) { + const [visible, setVisible] = useState(false); + + const [type, setType] = useState(ScheduleTypeEnum.ScheduleByLabel); + + const [nodeKeys, setNodeKeys] = useState([]); + + const formProps = useForm({ + mode: 'onBlur', + defaultValues: defaultAppendAffinityRuleFormData, + resolver: zodResolver(appendAffinityRuleSchema) + }); + + const { handleSubmit, reset } = formProps; + + function onCancel() { + setVisible(false); + reset(); + } + + function handleOk() { + if (type === ScheduleTypeEnum.ScheduleByNode && nodeKeys.length >= 1) { + append({ + weight: 1, + subRules: nodeKeys.map(key => ({ + key: 'kubernetes.io/hostname', + operator: NodeAffinityOperatorEnum.In, + value: key + })) + }); + + onCancel(); + } + + if (type === ScheduleTypeEnum.ScheduleByLabel) { + handleSubmit(({ rules }) => { + append(rules); + + onCancel(); + })(); + } + } + + return ( + <> + + + setVisible(false)}> + + +
+ + setType(type)}> + 指定节点调度 + 自定义Label规则 + + + + {type === ScheduleTypeEnum.ScheduleByLabel && ( + + + + )} + + {type === ScheduleTypeEnum.ScheduleByNode && ( + + + + )} + +
+
+ + + + + + +
+ + ); +} + +const { scrollable, selectable, removeable } = Table.addons; + +function NodeTransferPanel({ nodeKeys, setNodeKeys }) { + const clusterId = getParamByUrl('clusterId')!; + + const { data: nodeList = [] } = useRequest( + async () => { + const rsp = await nodeApi.fetchNodeList({ clusterId }); + + console.log('NodeTransferPanel--->', rsp); + + return rsp?.items ?? []; + }, + { ready: Boolean(clusterId) } + ); + + const columns: TableColumn[] = [ + { + key: 'metadata.name', + header: 'ID/节点名' + }, + + { + key: 'ip', + header: 'IP地址', + render: ({ metadata }) => metadata?.name + } + ]; + + return ( + <> + {nodeKeys.length < 1 && 节点不能为空,请选择节点} + +
setNodeKeys(keys), + rowSelect: true + }) + ]} + /> + + } + rightCell={ + +
nodeKeys.includes(item?.metadata?.name))} + recordKey="metadata.name" + addons={[ + removeable({ + onRemove: key => { + console.log('remove--->', key); + + setNodeKeys(items => items.filter(item => item !== key)); + } + }) + ]} + /> + + } + /> + + ); +} + +export function AffinityRulePanel() { + const showWeight = false; + + const { control } = useFormContext(); + + const { + fields: rules, + append, + remove + } = useFieldArray({ + control, + name: `rules` + }); + + return ( + <> + {rules.map(({ id }, index) => ( +
+ + remove(index)} />} /> + + {showWeight && ( + ( + + + + )} + /> + )} + + + + ))} + + + + ); +} + +const SubRulePanel = ({ ruleIndex }: { ruleIndex: number }) => { + const { control, trigger, watch } = useFormContext(); + + const { + fields: subRules, + append: appendSubRule, + remove: removeSubRule, + update + } = useFieldArray({ + control, + name: `rules.${ruleIndex}.subRules` + }); + + function needDisabledValue(operator) { + return operator === NodeAffinityOperatorEnum.Exists || operator === NodeAffinityOperatorEnum.DoesNotExist; + } + + const { errors } = useFormState({ control }); + + const disabled = Boolean(errors?.rules?.[ruleIndex]?.subRules); + + return ( + <> + {subRules.map(({ id }, index) => ( + + ( + + + + )} + /> + + ( + + + + )} + /> + + + + + ); +}; + +// TODO 泛型组件 diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/constants.ts b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/constants.ts index 907d8e579d..d9a98f321c 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/constants.ts +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/constants.ts @@ -57,71 +57,67 @@ export const affinityRuleOperatorList = [ } ]; -export const ruleSchema = z.object({ - rules: z.array( - z.object({ - weight: z - .number() - .int(t('权重必须为整数')) - .gte(0, { message: t('权重必须在1~100之前') }) - .lte(100, { message: t('权重必须在1~100之前') }), - subRules: z.array( - z - .object({ - key: z - .string() - .min(1, { message: t('标签名不能为空') }) - .max(63, { message: t('标签名长度不能超过63个字符') }) - .regex(/^([A-Za-z0-9][-A-Za-z0-9_./]*)?[A-Za-z0-9]$/, { message: '标签名格式不正确' }), - operator: z.nativeEnum(NodeAffinityOperatorEnum), - value: z.string() - }) - .superRefine(({ operator, value }, ctx) => { - console.log('superRefine', value, operator); - - const values = value.split(';'); - - let message = ''; - - if (operator === NodeAffinityOperatorEnum.Exists || operator === NodeAffinityOperatorEnum.DoesNotExist) - return; - - const value0 = values?.[0]; - - if (!value0) { - message = t('自定义规则不能为空'); - } else { - if (operator === NodeAffinityOperatorEnum.Lt || operator === NodeAffinityOperatorEnum.Gt) { - if (values.length > 1) { - message = t('Gt和Lt操作符只支持一个value值'); - } else if (!validateJS.isNumeric(value0)) { - message = t('Gt和Lt操作符value值格式必须为数字'); - } - } else if (values.some(item => !item)) { - message = t('标签值不能为空'); - } else if (values.some(item => item?.length > 63)) { - message = t('标签值长度不能超过63个字符'); - } else if ( - values.some(item => !validateJS.matches(item, /^([A-Za-z0-9][-A-Za-z0-9_./]*)?[A-Za-z0-9]$/)) - ) { - message = t('标签格式不正确'); +export const affinityRuleSchema = z.array( + z.object({ + weight: z + .number() + .int(t('权重必须为整数')) + .gte(0, { message: t('权重必须在1~100之前') }) + .lte(100, { message: t('权重必须在1~100之前') }), + subRules: z.array( + z + .object({ + key: z + .string() + .min(1, { message: t('标签名不能为空') }) + .max(63, { message: t('标签名长度不能超过63个字符') }) + .regex(/^([A-Za-z0-9][-A-Za-z0-9_./]*)?[A-Za-z0-9]$/, { message: '标签名格式不正确' }), + operator: z.nativeEnum(NodeAffinityOperatorEnum), + value: z.string() + }) + .superRefine(({ operator, value }, ctx) => { + console.log('superRefine', value, operator); + + const values = value.split(';'); + + let message = ''; + + if (operator === NodeAffinityOperatorEnum.Exists || operator === NodeAffinityOperatorEnum.DoesNotExist) + return; + + const value0 = values?.[0]; + + if (!value0) { + message = t('自定义规则不能为空'); + } else { + if (operator === NodeAffinityOperatorEnum.Lt || operator === NodeAffinityOperatorEnum.Gt) { + if (values.length > 1) { + message = t('Gt和Lt操作符只支持一个value值'); + } else if (!validateJS.isNumeric(value0)) { + message = t('Gt和Lt操作符value值格式必须为数字'); } + } else if (values.some(item => !item)) { + message = t('标签值不能为空'); + } else if (values.some(item => item?.length > 63)) { + message = t('标签值长度不能超过63个字符'); + } else if (values.some(item => !validateJS.matches(item, /^([A-Za-z0-9][-A-Za-z0-9_./]*)?[A-Za-z0-9]$/))) { + message = t('标签格式不正确'); } - - console.log('message--->', message); - - if (message) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message, - path: ['value'] - }); - } - }) - ) - }) - ) -}); + } + + console.log('message--->', message); + + if (message) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message, + path: ['value'] + }); + } + }) + ) + }) +); export const generateDefaultRules = () => [ { @@ -136,8 +132,6 @@ export const generateDefaultRules = () => [ } ]; -export type RuleType = z.infer; - export enum TolerationOperatorEnum { Equal = 'Equal', Exists = 'Exists' @@ -160,6 +154,11 @@ export enum TolerationEffectEnum { NoExecute = 'NoExecute' } +export enum AffinityTypeEnum { + Force = 'force', + Attempt = 'attempt' +} + export const tolerationEffectOptions = [ { value: TolerationEffectEnum.All, @@ -178,3 +177,68 @@ export const tolerationEffectOptions = [ value: TolerationEffectEnum.NoExecute } ]; + +const tolerationSchema = z.array( + z + .object({ + key: z.string(), + operator: z.nativeEnum(TolerationOperatorEnum), + value: z.string(), + effect: z.nativeEnum(TolerationEffectEnum), + time: z.number().min(0) + }) + .superRefine(({ key, operator, value, effect, time }, ctx) => { + if (operator === TolerationOperatorEnum.Equal && !key) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['key'], + message: 'key 不能为空' + }); + } + + if (operator === TolerationOperatorEnum.Equal && !value) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['value'], + message: 'value 不能为空' + }); + } + }) +); + +export const nodeAffinitySchema = z.object({ + affinityRules: z.object({ + [AffinityTypeEnum.Force]: affinityRuleSchema, + [AffinityTypeEnum.Attempt]: affinityRuleSchema + }), + tolerationRules: tolerationSchema, + nodeAffinityType: z.nativeEnum(NodeAffinityTypeEnum), + tolerationType: z.nativeEnum(TolerationTypeEnum) +}); + +export type NodeAffinityFormType = z.infer; + +export const defaultNodeAffinityFormData: NodeAffinityFormType = { + affinityRules: { + force: [], + attempt: [] + }, + tolerationRules: [], + nodeAffinityType: NodeAffinityTypeEnum.Unset, + tolerationType: TolerationTypeEnum.UnSet +}; + +export enum ScheduleTypeEnum { + ScheduleByNode = 'scheduleByNode', + ScheduleByLabel = 'scheduleByLabel' +} + +export const appendAffinityRuleSchema = z.object({ + rules: affinityRuleSchema +}); + +export type AppendAffinityRuleFormType = z.infer; + +export const defaultAppendAffinityRuleFormData: AppendAffinityRuleFormType = { + rules: generateDefaultRules() +}; diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/index.tsx b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/index.tsx index 5e406127ae..4f100987d6 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/index.tsx +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/index.tsx @@ -1,63 +1,230 @@ import { t } from '@/tencent/tea-app/lib/i18n'; -import React, { useState } from 'react'; -import { Form, Radio } from 'tea-component'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { TeaFormLayout } from '@src/modules/common/layouts/TeaFormLayout'; +import React, { useEffect } from 'react'; +import { Controller, FormProvider, useForm } from 'react-hook-form'; +import { Button, Form, Radio } from 'tea-component'; +import { WorkloadKindEnum } from '../constants'; import { AffinityRulePanel } from './affinityRulePanel'; -import { NodeAffinityTypeEnum, TolerationTypeEnum } from './constants'; +import { + AffinityTypeEnum, + NodeAffinityFormType, + NodeAffinityTypeEnum, + TolerationEffectEnum, + TolerationOperatorEnum, + TolerationTypeEnum, + defaultNodeAffinityFormData, + nodeAffinitySchema +} from './constants'; import { TolerationRulePanel } from './tolerationRulePanel'; -export const ModifyNodeAffinityPanel = ({ flag, onSubmit }) => { - const [state, setState] = useState({ - nodeAffinityType: NodeAffinityTypeEnum.Unset, - tolerationType: TolerationTypeEnum.UnSet +export const ModifyNodeAffinityPanel = ({ title, baseInfo, onCancel, kind, onUpdate, resource }) => { + const isCronjob = kind === WorkloadKindEnum.Cronjob; + + const useFormReturn = useForm({ + mode: 'onBlur', + defaultValues: defaultNodeAffinityFormData, + resolver: zodResolver(nodeAffinitySchema) }); - function handleSubmit() {} + const { handleSubmit, control, watch, reset } = useFormReturn; + + useEffect(() => { + if (!resource) return; + if (isCronjob) { + } else { + } + + let originNodeAffinityInfo = resource?.spec?.template?.spec?.affinity?.nodeAffinity; + + let tolerationInfo = resource?.spec?.template?.spec?.tolerations; + + if (isCronjob) { + originNodeAffinityInfo = resource?.spec?.jobTemplate?.spec?.template?.spec?.affinity?.nodeAffinity; + + tolerationInfo = resource?.spec?.jobTemplate?.spec?.template?.spec?.tolerations; + } + + const forceAffinityRules = + originNodeAffinityInfo?.requiredDuringSchedulingIgnoredDuringExecution?.nodeSelectorTerms?.map(item => ({ + weight: 1, + subRules: + item?.matchExpressions?.map(r => ({ + key: r?.key ?? '', + operator: r?.operator, + value: r?.values?.join(';') ?? '' + })) ?? [] + })) ?? []; + + const attemptAffinityRules = + originNodeAffinityInfo?.preferredDuringSchedulingIgnoredDuringExecution?.map(item => ({ + weight: item?.weight ?? '', + subRules: + item?.preference?.matchExpressions?.map(r => ({ + key: r?.key ?? '', + operator: r?.operator, + value: r?.values?.join(';') ?? '' + })) ?? [] + })) ?? []; + + const data: NodeAffinityFormType = { + nodeAffinityType: originNodeAffinityInfo ? NodeAffinityTypeEnum.Rule : NodeAffinityTypeEnum.Unset, + affinityRules: { + force: forceAffinityRules, + attempt: attemptAffinityRules + }, + + tolerationType: tolerationInfo ? TolerationTypeEnum.Set : TolerationTypeEnum.UnSet, + + tolerationRules: + tolerationInfo?.map(item => ({ + key: item?.key ?? '', + operator: item?.operator, + value: item?.value ?? '', + effect: item?.effect ?? TolerationEffectEnum.All, + time: item?.tolerationSeconds ?? 0 + })) ?? [] + }; + + reset(data); + }, [resource, isCronjob, reset]); + + function onSubmit({ nodeAffinityType, affinityRules, tolerationType, tolerationRules }: NodeAffinityFormType) { + console.log('NodeAffinityFormType--->'); + + const templateContent = { + spec: { + tolerations: + tolerationType === TolerationTypeEnum.Set + ? tolerationRules.map(({ key, operator, value, effect, time }) => ({ + key: key || undefined, + operator, + value: operator === TolerationOperatorEnum.Exists ? undefined : value, + effect: effect === TolerationEffectEnum.All ? undefined : effect, + tolerationSeconds: effect === TolerationEffectEnum.NoExecute ? time : undefined + })) + : null, + + affinity: { + nodeAffinity: + nodeAffinityType === NodeAffinityTypeEnum.Unset + ? null + : { + requiredDuringSchedulingIgnoredDuringExecution: affinityRules.force.length + ? { + nodeSelectorTerms: affinityRules.force.map(({ subRules }) => ({ + matchExpressions: subRules.map(({ key, operator, value }) => ({ + key, + operator, + values: value ? value.split(';') : undefined + })) + })) + } + : null, + + preferredDuringSchedulingIgnoredDuringExecution: affinityRules.attempt.length + ? affinityRules.attempt.map(({ weight, subRules }) => ({ + weight, + preference: { + matchExpressions: subRules.map(({ key, operator, value }) => ({ + key, + operator, + values: value ? value.split(';') : undefined + })) + } + })) + : null + } + } + } + }; + + const jsonData = { + spec: { + template: isCronjob ? undefined : templateContent, + jobTemplate: isCronjob + ? { + spec: { + template: templateContent + } + } + : undefined + } + }; + + onUpdate(jsonData); + } return ( -
- - setState(pre => ({ ...pre, nodeAffinityType: type }))} - > - 不使用调度策略 - 自定义调度规则 - - - - {state.nodeAffinityType === NodeAffinityTypeEnum.Rule && ( + - - - - - - - + + + - )} - - - setState(pre => ({ ...pre, tolerationType: type }))} - > - 不使用容忍调度 - 使用容忍调度 - - - - {state.tolerationType === TolerationTypeEnum.Set && ( - - - - )} - + } + > + {baseInfo} + +
+ + +
+ ( + + + 不使用调度策略 + 自定义调度规则 + + + )} + /> + + {watch('nodeAffinityType') === NodeAffinityTypeEnum.Rule && ( + <> + + + + + + + + + )} + + ( + + + 不使用容忍调度 + 使用容忍调度 + + + )} + /> + + {watch('tolerationType') === TolerationTypeEnum.Set && ( + + + + )} + +
+
); }; diff --git a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/tolerationRulePanel.tsx b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/tolerationRulePanel.tsx index 9100523b00..4efd9e106f 100644 --- a/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/tolerationRulePanel.tsx +++ b/web/console/src/modules/cluster/components/resource/resourceTableOperation/workloadUpdate/modifyNodeAffinityPanel/tolerationRulePanel.tsx @@ -1,9 +1,10 @@ import { getReactHookFormStatusWithMessage } from '@helper'; import { ValidateProvider } from '@src/modules/common/components'; import React from 'react'; -import { Controller, useFieldArray, useForm } from 'react-hook-form'; +import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; import { Button, Input, InputNumber, Select, Table, TableColumn } from 'tea-component'; import { + NodeAffinityFormType, TolerationEffectEnum, TolerationOperatorEnum, tolerationEffectOptions, @@ -11,45 +12,24 @@ import { } from './constants'; export const TolerationRulePanel = () => { - const { control, watch } = useForm<{ - rules: { - key: string; - operator: TolerationOperatorEnum; - value: string; - effect: TolerationEffectEnum; - time: number; - }[]; - }>({ - mode: 'onBlur', - defaultValues: { - rules: [] - } - }); + const { control, watch } = useFormContext(); const { fields, remove, append } = useFieldArray({ control, - name: 'rules' + name: 'tolerationRules' }); - const rulesWatch = watch('rules'); + const rulesWatch = watch('tolerationRules'); const columns: TableColumn[] = [ { key: 'key', header: '标签名', - render(record, recordKey, index) { + render(_, __, index) { return ( ', key, rules); - if (rules?.[index]?.operator === TolerationOperatorEnum.Equal && !key.trim()) { - return 'key不能为空'; - } - } - }} + name={`tolerationRules.${index}.key`} render={({ field, ...another }) => ( @@ -63,12 +43,11 @@ export const TolerationRulePanel = () => { { key: 'operator', header: '操作符', - render(record, recordKey, index) { + render(_, __, index) { return ( ( { { key: 'effect', header: '效果', - render(record, recordKey, index) { + render(_, __, index) { return ( ( - - )} - /> + : undefined, + + updateStrategy: isDeployment + ? undefined + : { + type: updateType, + rollingUpdate: isRollingUpdate + ? isStatefulSet + ? { partition } + : maxUnavailable + ? { maxUnavailable } + : undefined + : null + } + } + }; - {(isDeployment || isDaemonSet) && updateTypeWatch === RegistryUpdateTypeEnum.RollingUpdate && ( - ( - - - - )} - /> - )} + onUpdate(data); + } - {isDeployment && updateTypeWatch === RegistryUpdateTypeEnum.RollingUpdate && ( + return ( + + + + + + } + > + {baseInfo} + +
+ +
( - - {updateStrategyOptions.map(({ text, value }) => ( - - {text} - - ))} - +