diff --git a/package.json b/package.json index 2f8fce0c7..b86c0f198 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "no-undef": "off", "@typescript-eslint/no-inferrable-types": "off", "@tanstack/query/exhaustive-deps": "error", - "no-console": "error" + "no-console": "error", + "@typescript-eslint/no-dynamic-delete": "off" }, "ignorePatterns": [ "src/xpanse-api/**", diff --git a/src/components/content/deployedServices/common/DeployedServicesStatus.tsx b/src/components/content/deployedServices/common/DeployedServicesStatus.tsx index bca1f196d..4fee49ad5 100644 --- a/src/components/content/deployedServices/common/DeployedServicesStatus.tsx +++ b/src/components/content/deployedServices/common/DeployedServicesStatus.tsx @@ -19,6 +19,7 @@ export function DeployedServicesStatus( ): React.JSX.Element { switch (serviceDeploymentState) { case DeployedService.serviceDeploymentState.DEPLOYING: + case DeployedService.serviceDeploymentState.MODIFYING: case DeployedService.serviceDeploymentState.DESTROYING: return ( ); + case DeployedService.serviceDeploymentState.MODIFICATION_FAILED: + return ( + } color='error' className={'my-service-status-size'}> + {serviceDeploymentState.valueOf()} + + ); case DeployedService.serviceDeploymentState.DESTROY_FAILED: return ( ); + case DeployedService.serviceDeploymentState.MODIFICATION_SUCCESSFUL: + return ( + } + color='success' + className={'my-service-status-size'} + > + {serviceDeploymentState.valueOf()} + + ); default: return ( (undefined); + const [cacheFormVariable] = useOrderFormStore((state) => [state.addDeployVariable]); const [isDestroyRequestSubmitted, setIsDestroyRequestSubmitted] = useState(false); const [isPurgeRequestSubmitted, setIsPurgeRequestSubmitted] = useState(false); const [isMyServiceDetailsModalOpen, setIsMyServiceDetailsModalOpen] = useState(false); const [isMigrateModalOpen, setIsMigrateModalOpen] = useState(false); + const [isModifyModalOpen, setIsModifyModalOpen] = useState(false); + const [isScaleModalOpen, setIsScaleModalOpen] = useState(false); const serviceDestroyQuery = useDestroyRequestSubmitQuery(); const servicePurgeQuery = usePurgeRequestSubmitQuery(); const serviceStateStartQuery = useServiceStateStartQuery(refreshData); @@ -172,6 +180,62 @@ function MyServices(): React.JSX.Element { ), }, + { + key: 'scale', + label: ( + + ), + }, + { + key: 'modify parameters', + label: ( + + ), + }, { key: 'migrate', label: ( @@ -185,6 +249,8 @@ function MyServices(): React.JSX.Element { activeRecord !== undefined || record.serviceDeploymentState.toString() === DeployedService.serviceDeploymentState.DEPLOYMENT_FAILED.toString() || + record.serviceDeploymentState.toString() === + DeployedService.serviceDeploymentState.MODIFICATION_FAILED.toString() || record.serviceDeploymentState.toString() === DeployedService.serviceDeploymentState.DESTROY_SUCCESSFUL.toString() || record.serviceDeploymentState.toString() === @@ -204,6 +270,8 @@ function MyServices(): React.JSX.Element { DeployedService.serviceDeploymentState.DESTROY_SUCCESSFUL.toString() || record.serviceDeploymentState.toString() === DeployedService.serviceDeploymentState.DEPLOYMENT_FAILED.toString() || + record.serviceDeploymentState.toString() === + DeployedService.serviceDeploymentState.MODIFICATION_FAILED.toString() || record.serviceDeploymentState.toString() === DeployedService.serviceDeploymentState.ROLLBACK_FAILED.toString() ? 'purge' @@ -213,6 +281,8 @@ function MyServices(): React.JSX.Element { DeployedService.serviceDeploymentState.DESTROY_SUCCESSFUL.toString() || record.serviceDeploymentState.toString() === DeployedService.serviceDeploymentState.DEPLOYMENT_FAILED.toString() || + record.serviceDeploymentState.toString() === + DeployedService.serviceDeploymentState.MODIFICATION_FAILED.toString() || record.serviceDeploymentState.toString() === DeployedService.serviceDeploymentState.ROLLBACK_FAILED.toString() ? ( { - if (record.serviceDeploymentState !== DeployedService.serviceDeploymentState.DEPLOYMENT_SUCCESSFUL) { + if ( + record.serviceDeploymentState !== DeployedService.serviceDeploymentState.DEPLOYMENT_SUCCESSFUL && + record.serviceDeploymentState !== DeployedService.serviceDeploymentState.MODIFICATION_SUCCESSFUL + ) { if (record.serviceDeploymentState !== DeployedService.serviceDeploymentState.DESTROY_FAILED) { return true; } @@ -382,7 +457,10 @@ function MyServices(): React.JSX.Element { }; const isDisabledStopOrRestartBtn = (record: DeployedService) => { - if (record.serviceDeploymentState !== DeployedService.serviceDeploymentState.DEPLOYMENT_SUCCESSFUL) { + if ( + record.serviceDeploymentState !== DeployedService.serviceDeploymentState.DEPLOYMENT_SUCCESSFUL && + record.serviceDeploymentState !== DeployedService.serviceDeploymentState.MODIFICATION_SUCCESSFUL + ) { if (record.serviceDeploymentState !== DeployedService.serviceDeploymentState.DESTROY_FAILED) { return true; } @@ -554,7 +632,9 @@ function MyServices(): React.JSX.Element { }} disabled={ record.serviceDeploymentState.toString() !== - DeployedService.serviceDeploymentState.DEPLOYMENT_SUCCESSFUL.toString() + DeployedService.serviceDeploymentState.DEPLOYMENT_SUCCESSFUL.toString() && + record.serviceDeploymentState.toString() !== + DeployedService.serviceDeploymentState.MODIFICATION_SUCCESSFUL.toString() } > @@ -662,6 +742,32 @@ function MyServices(): React.JSX.Element { setIsMigrateModalOpen(true); } + function modify(record: DeployedService): void { + setActiveRecord( + record.serviceHostingType === DeployedService.serviceHostingType.SELF + ? (record as DeployedServiceDetails) + : (record as VendorHostedDeployedServiceDetails) + ); + const existingParameters = getExistingServiceParameters(record); + for (const existingServiceParametersKey in existingParameters) { + cacheFormVariable(existingServiceParametersKey, existingParameters[existingServiceParametersKey]); + } + setIsModifyModalOpen(true); + } + + function scale(record: DeployedService): void { + setActiveRecord( + record.serviceHostingType === DeployedService.serviceHostingType.SELF + ? (record as DeployedServiceDetails) + : (record as VendorHostedDeployedServiceDetails) + ); + const existingParameters = getExistingServiceParameters(record); + for (const existingServiceParametersKey in existingParameters) { + cacheFormVariable(existingServiceParametersKey, existingParameters[existingServiceParametersKey]); + } + setIsScaleModalOpen(true); + } + function onMonitor(record: DeployedService): void { navigate('/monitor', { state: record, @@ -821,6 +927,20 @@ function MyServices(): React.JSX.Element { setIsMigrateModalOpen(false); }; + const handleCancelModifyModel = () => { + setActiveRecord(undefined); + clearFormVariables(); + refreshData(); + setIsModifyModalOpen(false); + }; + + const handleCancelScaleModel = () => { + setActiveRecord(undefined); + clearFormVariables(); + refreshData(); + setIsScaleModalOpen(false); + }; + function getServiceDeploymentStateFromQuery(): DeployedService.serviceDeploymentState | undefined { const queryInUri = decodeURI(urlParams.get(serviceStateQuery) ?? ''); if (queryInUri.length > 0) { @@ -891,6 +1011,38 @@ function MyServices(): React.JSX.Element { ) : null} + {activeRecord ? ( + + + + ) : null} + + {activeRecord ? ( + + + + ) : null} +
+
+ + + + ); +}; diff --git a/src/components/content/order/orderStatus/ProcessingStatus.tsx b/src/components/content/order/orderStatus/ProcessingStatus.tsx index a8d839776..37cd3ad48 100644 --- a/src/components/content/order/orderStatus/ProcessingStatus.tsx +++ b/src/components/content/order/orderStatus/ProcessingStatus.tsx @@ -47,6 +47,37 @@ export const ProcessingStatus = ({ } } + if (operationType === (OperationType.Modify as OperationType)) { + if (response.serviceDeploymentState === DeployedServiceDetails.serviceDeploymentState.MODIFICATION_SUCCESSFUL) { + if (response.deployedServiceProperties) { + for (const key in response.deployedServiceProperties) { + endPointMap.set(key, response.deployedServiceProperties[key]); + } + } + if (endPointMap.size > 0) { + return ( + <> + {'Modification Successful'} +
+ {convertMapToDetailsList(endPointMap, 'Endpoint Information')} +
+ + ); + } else { + return {'Modification Successful'}; + } + } else if ( + response.serviceDeploymentState === DeployedServiceDetails.serviceDeploymentState.MODIFICATION_FAILED + ) { + return ( +
+ {'Modification Failed.'} +
{response.resultMessage}
+
+ ); + } + } + if (operationType === (OperationType.Destroy as OperationType)) { if (response.serviceDeploymentState === DeployedServiceDetails.serviceDeploymentState.DESTROY_SUCCESSFUL) { return ( diff --git a/src/components/content/order/orderStatus/useServiceDetailsPollingQuery.ts b/src/components/content/order/orderStatus/useServiceDetailsPollingQuery.ts index bc81136e5..b911e7c5b 100644 --- a/src/components/content/order/orderStatus/useServiceDetailsPollingQuery.ts +++ b/src/components/content/order/orderStatus/useServiceDetailsPollingQuery.ts @@ -31,5 +31,6 @@ export function useServiceDetailsPollingQuery( refetchIntervalInBackground: true, refetchOnWindowFocus: false, enabled: uuid !== undefined && isStartPolling, + gcTime: 0, }); } diff --git a/src/components/content/order/scale/Scale.tsx b/src/components/content/order/scale/Scale.tsx new file mode 100644 index 000000000..57f0bcde6 --- /dev/null +++ b/src/components/content/order/scale/Scale.tsx @@ -0,0 +1,263 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: Huawei Inc. + */ + +import React, { useState } from 'react'; +import { Badge, Button, Card, Col, Form, Input, Row, Tag, Tooltip } from 'antd'; +import '../../../../styles/service_modify.css'; +import '../../../../styles/service_order.css'; +import { + Billing, + DeployedService, + DeployedServiceDetails, + ModifyRequest, + ServiceFlavor, + ServiceService, + VendorHostedDeployedServiceDetails, +} from '../../../../xpanse-api/generated'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import useGetServiceTemplateDetails from '../../deployedServices/myServices/query/useGetServiceTemplateDetails'; +import { CUSTOMER_SERVICE_NAME_FIELD } from '../../../utils/constants'; +import { OrderItem } from '../common/utils/OrderItem'; +import { useOrderFormStore } from '../store/OrderFormStore'; +import { getModifyParams } from '../formDataHelpers/modifyParamsHelper'; +import { OrderSubmitProps } from '../common/utils/OrderSubmitProps'; +import { ModifySubmitRequest } from '../common/modifySubmitRequest'; +import ScaleOrModifySubmitStatusAlert from '../common/ScaleOrModifySubmitStatusAlert'; +import { useMutation } from '@tanstack/react-query'; + +export const Scale = ({ + currentSelectedService, +}: { + currentSelectedService: DeployedServiceDetails | VendorHostedDeployedServiceDetails; +}): React.JSX.Element => { + const [form] = Form.useForm(); + let flavorList: ServiceFlavor[] = []; + let getParams: OrderSubmitProps | undefined = undefined; + let currentBilling: Billing | undefined = undefined; + const [modifyStatus, setModifyStatus] = useState(undefined); + const [selectFlavor, setSelectFlavor] = useState( + currentSelectedService.flavor ? currentSelectedService.flavor : '' + ); + const [isShowModifyingResult, setIsShowModifyingResult] = useState(false); + + const [cacheFormVariable] = useOrderFormStore((state) => [state.addDeployVariable]); + + const serviceTemplateDetailsQuery = useGetServiceTemplateDetails(currentSelectedService.serviceTemplateId); + const modifyServiceRequest = useMutation({ + mutationFn: (modifyServiceRequestParams: ModifySubmitRequest) => { + return ServiceService.modify(modifyServiceRequestParams.id, modifyServiceRequestParams.modifyRequest); + }, + }); + + if (serviceTemplateDetailsQuery.isSuccess) { + currentBilling = serviceTemplateDetailsQuery.data.billing; + if (serviceTemplateDetailsQuery.data.flavors.serviceFlavors.length > 0) { + flavorList = serviceTemplateDetailsQuery.data.flavors.serviceFlavors; + } + getParams = getModifyParams( + currentSelectedService, + serviceTemplateDetailsQuery.data.serviceProviderContactDetails, + serviceTemplateDetailsQuery.data.variables + ); + } + + const onFinish = () => { + const deployParamsCache = useOrderFormStore.getState().deployParams; + const createRequest: ModifyRequest = { + flavor: selectFlavor, + customerServiceName: deployParamsCache[CUSTOMER_SERVICE_NAME_FIELD] as string, + }; + const serviceRequestProperties: Record = {}; + for (const variable in deployParamsCache) { + if (variable !== CUSTOMER_SERVICE_NAME_FIELD && deployParamsCache[variable] !== '') { + serviceRequestProperties[variable] = deployParamsCache[variable]; + } + } + createRequest.serviceRequestProperties = serviceRequestProperties as Record; + const modifyServiceRequestParams: ModifySubmitRequest = { + id: currentSelectedService.id, + modifyRequest: createRequest, + }; + + modifyServiceRequest.mutate(modifyServiceRequestParams); + setIsShowModifyingResult(true); + }; + + const getModifyDetailsStatus = (status: DeployedService.serviceDeploymentState | undefined) => { + setModifyStatus(status); + }; + + return ( +
+
Change Flavor:
+ {isShowModifyingResult ? ( + + ) : null} +
+
+ + + {flavorList.map((flavor) => { + return ( + +
{ + if (currentSelectedService.flavor === flavor.name) { + return; + } + setSelectFlavor(flavor.name); + form.setFieldsValue({ selectFlavor: flavor.name }); + }} + style={{ + pointerEvents: + currentSelectedService.flavor === flavor.name || + modifyServiceRequest.isPending || + modifyServiceRequest.isSuccess + ? 'none' + : 'auto', + cursor: + currentSelectedService.flavor === flavor.name || + modifyServiceRequest.isPending || + modifyServiceRequest.isSuccess + ? 'not-allowed' + : 'auto', + }} + > + {currentSelectedService.flavor === flavor.name ? ( +
+ + +

+ {currentBilling ? ( + <> + + {flavor.fixedPrice} + {'/'} + {currentBilling.billingModel} + + + ) : ( + <> + )} +

+
+
+
+ ) : ( + +

+ {currentBilling ? ( + <> + + {flavor.fixedPrice} + {'/'} + {currentBilling.billingModel} + + + ) : ( + <> + )} +

+
+ )} +
+ + ); + })} +
+
+
+ + { + cacheFormVariable(CUSTOMER_SERVICE_NAME_FIELD, e.target.value); + }} + className={'order-param-item-content'} + suffix={ + + + + } + /> + +
+ {getParams?.params.map((item) => + item.kind === 'variable' || item.kind === 'env' ? ( + + ) : undefined + )} +
+
+
+
+ +
+
+ +
+ ); +}; diff --git a/src/components/content/order/store/OrderFormStore.ts b/src/components/content/order/store/OrderFormStore.ts index ebcd67155..3874506c9 100644 --- a/src/components/content/order/store/OrderFormStore.ts +++ b/src/components/content/order/store/OrderFormStore.ts @@ -20,12 +20,22 @@ interface updateState { } export const useOrderFormStore = createWithEqualityFn()( - (set) => ({ + (set, getState) => ({ ...initialState, addDeployVariable: (deployVariableName: string, deployVariableValue: unknown) => { - set((state) => ({ - deployParams: { ...state.deployParams, [deployVariableName]: deployVariableValue }, - })); + if ( + !deployVariableValue && + Object.prototype.hasOwnProperty.call(getState().deployParams, deployVariableName) + ) { + delete getState().deployParams[deployVariableName]; + set((state) => ({ + deployParams: { ...state.deployParams }, + })); + } else { + set((state) => ({ + deployParams: { ...state.deployParams, [deployVariableName]: deployVariableValue }, + })); + } }, clearFormVariables: () => { set(initialState); diff --git a/src/components/content/order/types/OperationType.ts b/src/components/content/order/types/OperationType.ts index 78df634fe..113e0cae6 100644 --- a/src/components/content/order/types/OperationType.ts +++ b/src/components/content/order/types/OperationType.ts @@ -6,4 +6,5 @@ export enum OperationType { Deploy = 'deploy', Destroy = 'destroy', + Modify = 'modify', } diff --git a/src/styles/service_modify.css b/src/styles/service_modify.css new file mode 100644 index 000000000..d148c9f26 --- /dev/null +++ b/src/styles/service_modify.css @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: Huawei Inc. + */ + +.modify-select-class { + height: 80%; +} + +.modify-select-flavor-billing { + margin-bottom: 20px; +} + +.modify-title-class { + margin-left: 24px; + margin-bottom: 8px; +} + +.flavor-select-hover { + border: 3px solid #1677ff; +} + +.flavor-select-hover-disabled { + border: 3px solid #b5b5b5; + pointer-events: none; + opacity: 0.7; + cursor: not-allowed; +} + +.flavor-container { + border-radius: 8px; +} + +.flavor-card-container { + border-radius: 8px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: stretch; +} + +.flavor-card-content { + text-align: center; +} + +.flavor-card-content-btn { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.modify-container { + margin-left: 5px; + max-width: 1300px; +} + +.flavor-old-badge { + position: relative; +} + +.flavor-card-custom-ribbon { + position: absolute; + top: 1%; + left: 70%; +} + +.service-modify-submit-reset-container { + display: flex; + gap: 20px; + flex-direction: row; + margin-bottom: 15px; + justify-content: flex-end; + width: 600px; +} + +.service-modify-submit-class { + display: flex; + flex-direction: row; + text-align: right; + justify-content: flex-end; +}