diff --git a/libs/locales/lib/en/translation.json b/libs/locales/lib/en/translation.json index 7fe3cd1886..e10494deb4 100644 --- a/libs/locales/lib/en/translation.json +++ b/libs/locales/lib/en/translation.json @@ -158,6 +158,9 @@ "ai:Check the proxy settings and verify that the assisted installer service is connected to a network. You can use nmcli to get additional information about your network configuration.": "Check the proxy settings and verify that the assisted installer service is connected to a network. You can use nmcli to get additional information about your network configuration.", "ai:Check your VM reboot configuration": "Check your VM reboot configuration.", "ai:Choose a namespace from your existing host inventory in order to select hosts for your node pools. The namespace will be composed of 1 or more infrastructure environments. After the cluster is created, a host will become a worker node.": "Select a namespace from your existing host inventory in order to select hosts for your node pools. The namespace will contain 1 or more infrastructure environments. After the cluster is created, a host becomes a worker node.", + "ai:Cisco Intersight URL": "Cisco Intersight URL", + "ai:Cisco Intersight URL is required": "Cisco Intersight URL is required", + "ai:Cisco Intersight URL must be a valid URL starting with \"http://\" or \"https://\"": "Cisco Intersight URL must be a valid URL starting with \"http://\" or \"https://\"", "ai:Clear": "Clear", "ai:Clear all filters": "Clear all filters", "ai:Clear filters": "Clear filters", @@ -204,8 +207,10 @@ "ai:Configuration is hanging for a long time.": "Configuration is hanging for a long time.", "ai:Configuration may take a few minutes.": "Configuration might take a few minutes.", "ai:Configure": "Configure", + "ai:Configure a custom URL to add hosts from Cisco Intersight on disconnected environments.": "Configure a custom URL to add hosts from Cisco Intersight on disconnected environments.", "ai:Configure advanced networking properties (e.g. CIDR ranges).": "Configure advanced networking properties (for example, CIDR ranges).", "ai:Configure cluster-wide trusted certificates": "Configure cluster-wide trusted certificates", + "ai:Configure custom URL for Cisco Intersight": "Configure custom URL for Cisco Intersight", "ai:Configure environment": "Configure environment", "ai:Configure host inventory settings": "Configure host inventory settings", "ai:Configure load balancer on Amazon Web Services for me.": "Configure load balancer on Amazon Web Services for me.", @@ -353,7 +358,6 @@ "ai:Failed to save configuration": "Failed to save configuration", "ai:Failed to save host selection.": "Failed to save host selection.", "ai:Failed to update host": "Failed to update host", - "ai:Failed to update the AgentServiceConfig": "Failed to update the AgentServiceConfig", "ai:Failed validations:": "Failed validations:", "ai:Failing infrastructure environment": "Failing infrastructure environment", "ai:Fence Agents Remediation requirements": "Fence Agents Remediation requirements", @@ -696,6 +700,7 @@ "ai:Provide an endpoint for users, both human and machine, to interact with and configure the platform. If needed, contact your IT manager for more information. {{vipHelperSuffix}}": "Provide an endpoint for users, both human and machine, to interact with and configure the platform. If needed, contact your IT manager for more information. {{vipHelperSuffix}}", "ai:Provide an SSH key to be able to connect to the hosts for debugging purposes during the discovery process": "Provide an SSH key to be able to connect to the hosts for debugging purposes during the discovery process", "ai:Provide as many labels as you can to narrow the list to relevant hosts only.": "Provide as many labels as you can to narrow the list to relevant hosts.", + "ai:Provide the complete URL, including the protocol and parameters.": "Provide the complete URL, including the protocol and parameters.", "ai:Provided cluster configuration is not valid": "Provided cluster configuration is not valid", "ai:Provisioned": "Provisioned", "ai:Provisioning": "Provisioning", diff --git a/libs/ui-lib/lib/cim/components/modals/AddHostModal.tsx b/libs/ui-lib/lib/cim/components/modals/AddHostModal.tsx index 007b6dbf08..f5233b8a12 100644 --- a/libs/ui-lib/lib/cim/components/modals/AddHostModal.tsx +++ b/libs/ui-lib/lib/cim/components/modals/AddHostModal.tsx @@ -15,6 +15,7 @@ import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; import { EnvironmentErrors } from '../InfraEnv/EnvironmentErrors'; import { InfraEnvK8sResource } from '../../types'; import DownloadIpxeScript from '../../../common/components/clusterConfiguration/DownloadIpxeScript'; +import { useAgentServiceConfig } from '../../hooks'; type AddHostModalStepType = 'config' | 'download'; @@ -27,12 +28,24 @@ const AddHostModal: React.FC = ({ docVersion, isIPXE, }) => { + const { t } = useTranslation(); const hasDHCP = infraEnv.metadata?.labels?.networkType !== 'static'; const sshPublicKey = infraEnv.spec?.sshAuthorizedKey || agentClusterInstall?.spec?.sshPublicKey; const { httpProxy, httpsProxy, noProxy } = infraEnv.spec?.proxy || {}; const imageType = infraEnv.spec?.imageType || 'minimal-iso'; const [dialogType, setDialogType] = React.useState('config'); - const { t } = useTranslation(); + + const [ciscoUrl, setCiscoUrl] = React.useState(); + const [agentServiceConfig, loaded, error] = useAgentServiceConfig({ name: 'agent' }); + + React.useEffect(() => { + if (loaded && !error) { + if (agentServiceConfig && agentServiceConfig.metadata?.annotations?.['ciscoIntersightURL']) { + setCiscoUrl(agentServiceConfig.metadata?.annotations?.['ciscoIntersightURL']); + } + } + }, [agentServiceConfig, error, loaded]); + const handleIsoConfigSubmit = async ( values: DiscoveryImageFormValues, formikActions: FormikHelpers, @@ -92,6 +105,7 @@ const AddHostModal: React.FC = ({ onReset={agentClusterInstall ? () => setDialogType('config') : undefined} hasDHCP={hasDHCP} docVersion={docVersion} + ciscoUrl={ciscoUrl} /> )} @@ -107,12 +121,14 @@ const GeneratingIsoDownload = ({ onReset, hasDHCP, docVersion, + ciscoUrl, }: { infraEnv: InfraEnvK8sResource; onClose: VoidFunction; onReset?: VoidFunction; hasDHCP: boolean; docVersion: string; + ciscoUrl?: string; }) => { const { t } = useTranslation(); return infraEnv.status?.isoDownloadURL ? ( @@ -122,6 +138,7 @@ const GeneratingIsoDownload = ({ onReset={onReset} hasDHCP={hasDHCP} docVersion={docVersion} + ciscoUrl={ciscoUrl} /> ) : ( diff --git a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationForm.css b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationForm.css deleted file mode 100644 index cc4c9997cc..0000000000 --- a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationForm.css +++ /dev/null @@ -1,23 +0,0 @@ -.cim-config-form-title .pf-v5-c-form__label-text { - font-size: larger; -} - -.cim-config-form-volume { - white-space: nowrap; -} - -.cim-config-form-aws-label { - white-space: nowrap; -} - -.cim-config-form-aws { - margin-bottom: var(--pf-v5-global--spacer--md); -} - -.cim-config-form-volume input { - width: 10rem; -} - -.cim-config-modal .pf-v5-c-modal-box__body { - overflow: hidden; -} diff --git a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationForm.tsx b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationForm.tsx deleted file mode 100644 index 08b6d9b825..0000000000 --- a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationForm.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import * as React from 'react'; -import { Trans } from 'react-i18next'; -import { - Flex, - FlexItem, - Form, - FormGroup, - Popover, - Checkbox, - TextInput, - FormHelperText, - HelperText, - HelperTextItem, - Icon, -} from '@patternfly/react-core'; -import { ExternalLinkAltIcon } from '@patternfly/react-icons/dist/js/icons/external-link-alt-icon'; -import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/js/icons/exclamation-circle-icon'; -import { HelpIcon } from '@patternfly/react-icons/dist/js/icons/help-icon'; - -import { useTranslation } from '../../../../common/hooks/use-translation-wrapper'; -import { CimConfigurationFormProps } from './types'; - -import './CimConfigurationForm.css'; - -export const CimConfigurationForm: React.FC = ({ - docConfigUrl, - docConfigAwsUrl, - isEdit, - isInProgressPeriod, - dbVolSize, - dbVolSizeValidation, - setDbVolSize, - fsVolSize, - fsVolSizeValidation, - setFsVolSize, - imgVolSize, - imgVolSizeValidation, - setImgVolSize, - configureLoadBalancer, - configureLoadBalancerInitial, - setConfigureLoadBalancer, -}) => { - const { t } = useTranslation(); - - const getNumber = (v: string, min: number): number => { - const p = parseFloat(v); - if (isNaN(p)) { - // Do not check for minimum here - keep it on validation - return min; - } - return p; - }; - - const awsHelp = ( - - - ai:Learn more about enabling CIM on AWS - - - } - > - - - ); - - return ( -
- - - ai:Learn more about storage sizes. - - - } - > - - - } - > - {t('ai:If there are many clusters, use higher values for the storage fields.')} - - - - - - - } - isRequired - > - - setDbVolSize(getNumber(v, 1))} - min={0 /* Do the validation elsewhere */} - />{' '} - Gi - - {dbVolSizeValidation !== undefined && ( - - - } variant="error"> - {dbVolSizeValidation} - - - - )} - - - - - - - } - isRequired - > - - setFsVolSize(getNumber(v, 1))} - min={0} - />{' '} - Gi - - {fsVolSizeValidation !== undefined && ( - - - } variant="error"> - {fsVolSizeValidation} - - - - )} - - - - - - - } - isRequired - > - - setImgVolSize(getNumber(v, 10))} - min={0} - />{' '} - Gi - - {imgVolSizeValidation !== undefined && ( - - - } variant="error"> - {imgVolSizeValidation} - - - - )} - - - - - {t('ai:Configure load balancer on Amazon Web Services for me.')} -   - {awsHelp} - - } - id="cim-config-form-aws" - className="cim-config-form-aws" - // isRequired - name="aws-loadbalancer-checkbox" - isChecked={configureLoadBalancer} - isDisabled={ - isInProgressPeriod || - (isEdit && - configureLoadBalancerInitial) /* For edit flow, only No to Yes transition is possible */ - } - onChange={(_event, value) => setConfigureLoadBalancer(value)} - /> - - ); -}; diff --git a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationFormFields.tsx b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationFormFields.tsx new file mode 100644 index 0000000000..dce26cd5f0 --- /dev/null +++ b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationFormFields.tsx @@ -0,0 +1,258 @@ +import * as React from 'react'; +import { Trans } from 'react-i18next'; +import { + Flex, + FlexItem, + Form, + FormGroup, + Icon, + InputGroup, + InputGroupItem, + InputGroupText, + TextContent, + Text, + TextVariants, + Stack, + StackItem, +} from '@patternfly/react-core'; +import { ExternalLinkAltIcon } from '@patternfly/react-icons/dist/js/icons/external-link-alt-icon'; +import { HelpIcon } from '@patternfly/react-icons/dist/js/icons/help-icon'; + +import { useTranslation } from '../../../../common/hooks/use-translation-wrapper'; +import { CheckboxField, InputField, PopoverIcon } from '../../../../common'; +import { CimConfigurationFormFieldsProps, CimConfigurationValues } from './types'; +import { useFormikContext } from 'formik'; +import { isIngressController } from './persist'; + +export const CimConfigurationFormFields = ({ + platform, + docConfigUrl, + docConfigAwsUrl, + isEdit, + isInProgressPeriod, + configureLoadBalancerInitial, + setConfigureLoadBalancerInitial, +}: CimConfigurationFormFieldsProps) => { + const { t } = useTranslation(); + + const { setFieldValue, values } = useFormikContext(); + + React.useEffect( + () => { + const doItAsync = async (): Promise => { + if (platform === 'AWS') { + if (!isEdit || (await isIngressController())) { + setFieldValue('configureLoadBalancer', true); + setConfigureLoadBalancerInitial(true); + return; + } + } + + setFieldValue('configureLoadBalancer', false); + setConfigureLoadBalancerInitial(false); + }; + + void doItAsync(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [platform], + ); + + return ( +
+ + + {t('ai:Storage sizes')}{' '} + + + ai:Learn more about storage sizes. + + + } + aria-label={t('ai:More info for configure storage sizes')} + noVerticalAlign + > + + + + + + {t('ai:If there are many clusters, use higher values for the storage fields.')} + + + + + + + + + + } + isRequired + > + + + + + Gi + + + + + + + + + + + } + isRequired + > + + + + + Gi + + + + + + + + + + + } + isRequired + > + + + + + Gi + + + + + + + + + {t('ai:Configure load balancer on Amazon Web Services for me.')}{' '} + + + ai:Learn more about enabling CIM on AWS + + + } + aria-label={t('ai:More info for load balancer on Amazon web services')} + > + + + + + + } + isDisabled={isInProgressPeriod || (isEdit && configureLoadBalancerInitial)} + /> + + + + + {t('ai:Configure custom URL for Cisco Intersight')}{' '} + + + + + + + } + /> + + {values.addCiscoIntersightUrl && ( + + )} + + +
+ ); +}; diff --git a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationModal.css b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationModal.css deleted file mode 100644 index 809e49e935..0000000000 --- a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationModal.css +++ /dev/null @@ -1,3 +0,0 @@ -.cim-config-modal .pf-v5-c-alert { - margin-bottom: var(--pf-v5-global--spacer--md); -} diff --git a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationModal.tsx b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationModal.tsx index 3bb9303076..26872d95f0 100644 --- a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationModal.tsx +++ b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationModal.tsx @@ -1,4 +1,6 @@ import * as React from 'react'; +import { Formik, FormikHelpers } from 'formik'; +import * as Yup from 'yup'; import { Alert, AlertVariant, @@ -6,21 +8,24 @@ import { ButtonVariant, Modal, ModalVariant, + TextContent, + Text, + StackItem, + Stack, } from '@patternfly/react-core'; +import parseUrl from 'parse-url'; import { useTranslation } from '../../../../common/hooks/use-translation-wrapper'; import { getStorageSizeGiB } from '../../helpers'; import { AlertPayload } from '../../../../common'; -import { CimConfigurationModalProps, CimConfiguratioProps } from './types'; -import { CimConfigurationForm } from './CimConfigurationForm'; -import { isIngressController, onEnableCIM } from './persist'; +import { CimConfigurationModalProps, CimConfigurationValues } from './types'; +import { onEnableCIM } from './persist'; import { CimConfigDisconnectedAlert } from './CimConfigDisconnectedAlert'; import { MIN_DB_VOL_SIZE, MIN_FS_VOL_SIZE, MIN_IMG_VOL_SIZE } from './constants'; import { isCIMConfigProgressing } from './utils'; import { resetCimConfigProgressAlertSuccessStatus } from './CimConfigProgressAlert'; - -import './CimConfigurationModal.css'; +import { CimConfigurationFormFields } from './CimConfigurationFormFields'; export const CimConfigurationModal: React.FC = ({ isOpen, @@ -30,136 +35,41 @@ export const CimConfigurationModal: React.FC = ({ docDisconnectedUrl, docConfigUrl, docConfigAwsUrl, - - getResource, - listResources, - patchResource, - createResource, }) => { const { t } = useTranslation(); const [error, setError] = React.useState(); - const [isSaving, setSaving] = React.useState(false); - - const [dbVolSize, _setDbVolSize] = React.useState(() => - getStorageSizeGiB(10, agentServiceConfig?.spec?.databaseStorage?.resources?.requests?.storage), - ); - const [dbVolSizeValidation, setDbVolSizeValidation] = React.useState(); - - const [fsVolSize, _setFsVolSize] = React.useState(() => - getStorageSizeGiB( - 100, - agentServiceConfig?.spec?.filesystemStorage?.resources?.requests?.storage, - ), - ); - const [fsVolSizeValidation, setFsVolSizeValidation] = React.useState(); - const [imgVolSize, _setImgVolSize] = React.useState(() => - getStorageSizeGiB(50, agentServiceConfig?.spec?.imageStorage?.resources?.requests?.storage), - ); - const [imgVolSizeValidation, setImgVolSizeValidation] = React.useState(); - - const [configureLoadBalancer, setConfigureLoadBalancer] = React.useState( - platform === 'AWS', - ); const [configureLoadBalancerInitial, setConfigureLoadBalancerInitial] = React.useState(true); - - const setDbVolSize = (v: number): void => { - if (v < MIN_DB_VOL_SIZE) { - setDbVolSizeValidation(t('ai:Minimal value is 1Gi')); - } else { - setDbVolSizeValidation(undefined); - } - _setDbVolSize(v); - }; - const setFsVolSize = (v: number): void => { - if (v < MIN_FS_VOL_SIZE) { - setFsVolSizeValidation(t('ai:Minimal value is 1Gi')); - } else { - setFsVolSizeValidation(undefined); - } - _setFsVolSize(v); - }; - const setImgVolSize = (v: number): void => { - if (v < MIN_IMG_VOL_SIZE) { - setImgVolSizeValidation(t('ai:Minimal value is 10Gi')); - } else { - setImgVolSizeValidation(undefined); - } - _setImgVolSize(v); - }; - const isEdit = !!agentServiceConfig; - React.useEffect( - () => { - const doItAsync = async (): Promise => { - if (platform === 'AWS') { - if (!isEdit || (await isIngressController(getResource))) { - setConfigureLoadBalancer(true); - setConfigureLoadBalancerInitial(true); - return; - } - } - - setConfigureLoadBalancer(false); - setConfigureLoadBalancerInitial(false); - }; - - void doItAsync(); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [platform], - ); - - const formProps: CimConfiguratioProps = { - dbVolSize, - dbVolSizeValidation, - setDbVolSize, - fsVolSize, - fsVolSizeValidation, - setFsVolSize, - imgVolSize, - imgVolSizeValidation, - setImgVolSize, - - configureLoadBalancer, - configureLoadBalancerInitial, - setConfigureLoadBalancer, - }; - - const onConfigure = () => { - const doItAsync = async (): Promise => { - setSaving(true); - setError(undefined); - resetCimConfigProgressAlertSuccessStatus(); - - if ( - await onEnableCIM({ - t, - setError, - getResource, - listResources, - patchResource, - createResource, - agentServiceConfig, - - platform, - dbVolSizeGiB: dbVolSize, - fsVolSizeGiB: fsVolSize, - imgVolSizeGiB: imgVolSize, - configureLoadBalancer, - }) - ) { - // successfuly persisted - onClose(); - } - + const onConfigure = async ( + values: CimConfigurationValues, + helpers: FormikHelpers, + ) => { + setError(undefined); + helpers.setSubmitting(true); + resetCimConfigProgressAlertSuccessStatus(); + + if ( + await onEnableCIM({ + t, + setError, + agentServiceConfig, + platform, + dbVolSizeGiB: values.dbVolSize, + fsVolSizeGiB: values.fsVolSize, + imgVolSizeGiB: values.imgVolSize, + configureLoadBalancer: values.configureLoadBalancer, + ciscoIntersightURL: values.addCiscoIntersightUrl ? values.ciscoIntersightURL : undefined, + }) + ) { + // successfully persisted + onClose(); + } else { // keep modal open and show error - setConfigureLoadBalancerInitial(configureLoadBalancer); - setSaving(false); - }; - - void doItAsync(); + setConfigureLoadBalancerInitial(values.configureLoadBalancer); + helpers.setSubmitting(false); + } }; const isError = !!error?.title; // this is a communication error only (not the one from agentServiceConfig) @@ -167,75 +77,140 @@ export const CimConfigurationModal: React.FC = ({ const isConfigure = !isEdit || !configureLoadBalancerInitial; /* The only possible change for the Edit flow */ - const isConfigureDisabled = !!( - isSaving || - dbVolSizeValidation || - fsVolSizeValidation || - imgVolSizeValidation || - (isEdit && configureLoadBalancerInitial === configureLoadBalancer) - ); - const isInProgressPeriod = isCIMConfigProgressing({ agentServiceConfig }); - // const isConfiguring = isSaving || isInProgressPeriod; - - let actions: React.ReactNode[]; - if (isConfigure) { - actions = [ - , - , - ]; - } else { - actions = [ - , - ]; - } + + const initialValues: CimConfigurationValues = { + dbVolSize: getStorageSizeGiB( + 10, + agentServiceConfig?.spec?.databaseStorage?.resources?.requests?.storage, + ), + fsVolSize: getStorageSizeGiB( + 100, + agentServiceConfig?.spec?.filesystemStorage?.resources?.requests?.storage, + ), + imgVolSize: getStorageSizeGiB( + 50, + agentServiceConfig?.spec?.imageStorage?.resources?.requests?.storage, + ), + configureLoadBalancer: platform === 'AWS', + addCiscoIntersightUrl: !!agentServiceConfig?.metadata?.annotations?.['ciscoIntersightURL'], + ciscoIntersightURL: agentServiceConfig?.metadata?.annotations?.['ciscoIntersightURL'] || '', + }; + + const validationSchema = Yup.object({ + dbVolSize: Yup.number().min(MIN_DB_VOL_SIZE, t('ai:Minimal value is 1Gi')).required(), + fsVolSize: Yup.number().min(MIN_FS_VOL_SIZE, t('ai:Minimal value is 1Gi')).required(), + imgVolSize: Yup.number().min(MIN_IMG_VOL_SIZE, t('ai:Minimal value is 10Gi')).required(), + configureLoadBalancer: Yup.boolean(), + ciscoIntersightURL: Yup.string().when('addCiscoIntersightUrl', { + is: true, + then: (schema) => + schema + .required(t('ai:Cisco Intersight URL is required')) + .test( + 'url-valid', + t('ai:Cisco Intersight URL must be a valid URL starting with "http://" or "https://"'), + (val) => { + try { + const url = parseUrl(val); + return url.protocol === 'http' || url.protocol === 'https'; + } catch (error) { + return false; + } + }, + ), + }), + }); return ( - - {t( - 'ai:Configuring the host inventory settings will enable the Central Infrastructure Management.', - )} - {isError && ( - - {error.message} - - )} - - - {isConfigure && ( - + {({ values, handleSubmit, isValid, isSubmitting }) => ( + void handleSubmit()} + > + {t('ai:Configure')} + , + , + ] + : [ + , + ] + } + variant={ModalVariant.medium} + id="cim-config-modal" + > + + + + + {t( + 'ai:Configuring the host inventory settings will enable the Central Infrastructure Management.', + )} + + + + + {isError && ( + + + {error.message} + + + )} + + + + + + + + + + {isConfigure && ( + + + + )} + + )} - + ); }; diff --git a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/persist.ts b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/persist.ts index 53469175db..1841d6a536 100644 --- a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/persist.ts +++ b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/persist.ts @@ -1,17 +1,21 @@ -import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; +import { + k8sCreate, + k8sGet, + k8sListItems, + k8sPatch, + K8sResourceCommon, +} from '@openshift-console/dynamic-plugin-sdk'; import { AlertVariant } from '@patternfly/react-core'; import { TFunction } from 'i18next'; import { getErrorMessage } from '../../../../common/utils'; import { AgentServiceConfigK8sResource, - convertOCPtoCIMResourceHeader, - CreateResourceFuncType, - GetResourceFuncType, - ListResourcesFuncType, - PatchResourceFuncType, - ResourcePatch, + AgentServiceConfigModel, + IngressControllerModel, + ProvisioningModel, RouteK8sResource, + RouteModel, } from '../../../types'; import { LOCAL_STORAGE_ID_LAST_UPDATE_TIMESTAMP } from './utils'; @@ -32,14 +36,10 @@ const ASSISTED_IMAGE_SERVICE_ROUTE_PREFIX = 'assisted-image-service-multicluster const getAssistedImageServiceRoute = async ( t: TFunction, setError: SetErrorFuncType, - listResources: ListResourcesFuncType, ): Promise => { - let allRoutes; + let allRoutes: RouteK8sResource[] = []; try { - allRoutes = (await listResources({ - kind: 'Route', - apiVersion: 'route.openshift.io/v1', - })) as RouteK8sResource[]; + allRoutes = await k8sListItems({ model: RouteModel, queryParams: {} }); } catch (e) { // console.error('Failed to read all routes: ', allRoutes); setError({ @@ -50,7 +50,7 @@ const getAssistedImageServiceRoute = async ( } const assistedImageServiceRoute = allRoutes?.find( - (r: RouteK8sResource) => r.metadata?.name === 'assisted-image-service', + (r) => r.metadata?.name === 'assisted-image-service', ); if (!assistedImageServiceRoute?.spec?.host) { @@ -92,7 +92,6 @@ const getClusterDomain = ( const patchAssistedImageServiceRoute = async ( t: TFunction, setError: SetErrorFuncType, - patchResource: PatchResourceFuncType, assistedImageServiceRoute: RouteK8sResource, domain: string, @@ -102,7 +101,7 @@ const patchAssistedImageServiceRoute = async ( const labels = assistedImageServiceRoute.metadata?.labels || {}; labels['router-type'] = 'nlb'; - const patches: ResourcePatch[] = [ + const patches = [ { op: 'replace', path: '/spec/host', @@ -116,7 +115,11 @@ const patchAssistedImageServiceRoute = async ( ]; try { - await patchResource(convertOCPtoCIMResourceHeader(assistedImageServiceRoute), patches); + await k8sPatch({ + model: RouteModel, + resource: assistedImageServiceRoute, + data: patches, + }); } catch (e) { // console.error('Failed to patch assisted-image-service route: ', e, patches); setError({ @@ -128,15 +131,12 @@ const patchAssistedImageServiceRoute = async ( return true; }; -export const isIngressController = async (getResource: GetResourceFuncType): Promise => { +export const isIngressController = async (): Promise => { try { - await getResource({ - apiVersion: 'operator.openshift.io/v1', - kind: 'IngressController', - metadata: { - name: 'ingress-controller-with-nlb', - namespace: 'openshift-ingress-operator', - }, + await k8sGet({ + name: 'ingress-controller-with-nlb', + ns: 'openshift-ingress-operator', + model: IngressControllerModel, }); return true; @@ -148,7 +148,6 @@ export const isIngressController = async (getResource: GetResourceFuncType): Pro const createIngressController = async ( t: TFunction, setError: SetErrorFuncType, - createResource: CreateResourceFuncType, domain: string, ): Promise => { @@ -184,7 +183,7 @@ const createIngressController = async ( }; try { - await createResource(ingressController); + await k8sCreate({ model: IngressControllerModel, data: ingressController }); return true; } catch (e) { // console.error('Create IngressController error: ', e); @@ -199,24 +198,19 @@ const createIngressController = async ( const patchProvisioningConfiguration = async ({ t, setError, - patchResource, - getResource, }: { t: TFunction; setError: SetErrorFuncType; - patchResource: PatchResourceFuncType; - getResource: GetResourceFuncType; }) => { try { - const provisioning = (await getResource({ - kind: 'Provisioning', - apiVersion: 'metal3.io/v1alpha1', - metadata: { - name: 'provisioning-configuration', - }, - })) as K8sResourceCommon & { spec?: { watchAllNamespaces?: boolean } }; + const provisioning = await k8sGet< + K8sResourceCommon & { spec?: { watchAllNamespaces?: boolean } } + >({ + model: ProvisioningModel, + name: 'provisioning-configuration', + }); - const patches: ResourcePatch[] = [ + const patches = [ { op: provisioning.spec?.watchAllNamespaces ? 'replace' : 'add', path: '/spec/watchAllNamespaces', @@ -224,7 +218,11 @@ const patchProvisioningConfiguration = async ({ }, ]; - await patchResource(convertOCPtoCIMResourceHeader(provisioning), patches); + await k8sPatch({ + model: ProvisioningModel, + resource: provisioning, + data: patches, + }); } catch (e) { // console.error('Failed to patch provisioning-configuration: ', e); setError({ @@ -238,18 +236,18 @@ const patchProvisioningConfiguration = async ({ const createAgentServiceConfig = async ({ t, setError, - createResource, dbVolSizeGiB, fsVolSizeGiB, imgVolSizeGiB, + ciscoIntersightURL, }: { t: TFunction; setError: SetErrorFuncType; - createResource: CreateResourceFuncType; dbVolSizeGiB: number; fsVolSizeGiB: number; imgVolSizeGiB: number; + ciscoIntersightURL?: string; }): Promise => { try { const agentServiceConfig = { @@ -257,6 +255,7 @@ const createAgentServiceConfig = async ({ kind: 'AgentServiceConfig', metadata: { name: 'agent', + annotations: {}, }, spec: { databaseStorage: { @@ -286,79 +285,32 @@ const createAgentServiceConfig = async ({ }, }; - await createResource(agentServiceConfig); - return true; - } catch (e) { - // console.error('Failed to create AgentServiceConfig: ', e); - setError({ - title: t('ai:Failed to create AgentServiceConfig'), - message: getErrorMessage(e), - }); - return false; - } -}; + if (ciscoIntersightURL) { + agentServiceConfig.metadata = { + ...agentServiceConfig.metadata, + annotations: { ciscoIntersightURL }, + }; + } -/* Following functions are tested but recently not used. -const patchAgentServiceConfig = async ({ - t, - setError, - patchResource, - agentServiceConfig, - imgVolSizeGB, -}: { - t: TFunction; - setError: SetErrorFuncType; - patchResource: PatchResourceFuncType; - agentServiceConfig: AgentServiceConfigK8sResource; + await k8sCreate({ + model: AgentServiceConfigModel, + data: agentServiceConfig, + }); - imgVolSizeGB: number; -}): Promise => { - try { - const patches: ResourcePatch[] = [ - { - op: 'replace', - path: '/spec/imageStorage/resources/requests/storage', - value: `${imgVolSizeGB}G`, - }, - ]; - await patchResource(agentServiceConfig, patches); return true; } catch (e) { - console.error('Failed to patch AgentServiceConfig: ', e); setError({ - title: t('ai:Failed to update the AgentServiceConfig'), + title: t('ai:Failed to create AgentServiceConfig'), + message: getErrorMessage(e), }); return false; } }; -export const onDeleteCimConfig = async ({ - deleteResource, -}: { - deleteResource: DeleteResourceFuncType; -}) => { - try { - await deleteResource({ - apiVersion: 'agent-install.openshift.io/v1beta1', - kind: 'AgentServiceConfig', - metadata: { - name: 'agent', - // cluster-scoped resource - }, - }); - } catch (e) { - console.error('Failed to delete AgentServiceConfig: ', e); - } -}; -*/ // https://access.redhat.com/documentation/en-us/red_hat_advanced_cluster_management_for_kubernetes/2.6/html/multicluster_engine/multicluster_engine_overview#enable-cim export const onEnableCIM = async ({ t, setError, - createResource, - getResource, - listResources, - patchResource, agentServiceConfig, platform, @@ -368,13 +320,10 @@ export const onEnableCIM = async ({ imgVolSizeGiB, configureLoadBalancer, + ciscoIntersightURL, }: { t: TFunction; setError: SetErrorFuncType; - createResource: CreateResourceFuncType; - getResource: GetResourceFuncType; - listResources: ListResourcesFuncType; - patchResource: PatchResourceFuncType; agentServiceConfig?: AgentServiceConfigK8sResource; platform: string; @@ -384,22 +333,21 @@ export const onEnableCIM = async ({ imgVolSizeGiB: number; configureLoadBalancer: boolean; + ciscoIntersightURL?: string; }) => { if (['none', 'baremetal', 'openstack', 'vsphere'].includes(platform.toLocaleLowerCase())) { - await patchProvisioningConfiguration({ t, setError, patchResource, getResource }); + await patchProvisioningConfiguration({ t, setError }); } - if (agentServiceConfig) { - // console.log('The AgentServiceConfig recently can not be patched. Delete and create instead.'); - } else { + if (!agentServiceConfig) { if ( !(await createAgentServiceConfig({ t, setError, - createResource, dbVolSizeGiB, fsVolSizeGiB, imgVolSizeGiB, + ciscoIntersightURL, })) ) { return false; @@ -411,16 +359,11 @@ export const onEnableCIM = async ({ if (configureLoadBalancer) { // Recently No to Yes only (since we do not delete the ingress controller) - if (await isIngressController(getResource)) { - // console.log('IngressController already present, we do not patch it.'); + if (await isIngressController()) { return true /* Not an error */; } - const assistedImageServiceRoute = await getAssistedImageServiceRoute( - t, - setError, - listResources, - ); + const assistedImageServiceRoute = await getAssistedImageServiceRoute(t, setError); if (!assistedImageServiceRoute) { return false; } @@ -431,14 +374,8 @@ export const onEnableCIM = async ({ } if ( - !(await createIngressController(t, setError, createResource, domain)) || - !(await patchAssistedImageServiceRoute( - t, - setError, - patchResource, - assistedImageServiceRoute, - domain, - )) + !(await createIngressController(t, setError, domain)) || + !(await patchAssistedImageServiceRoute(t, setError, assistedImageServiceRoute, domain)) ) { return false; } diff --git a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/types.ts b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/types.ts index 448e92cc26..35288a10de 100644 --- a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/types.ts +++ b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/types.ts @@ -1,24 +1,12 @@ -import { - CreateResourceFuncType, - GetResourceFuncType, - ListResourcesFuncType, - PatchResourceFuncType, -} from '../../../types'; import { AgentServiceConfigK8sResource } from '../../../types/k8s/agent-service-config'; -export type CimConfiguratioProps = { +export type CimConfigurationValues = { dbVolSize: number; - dbVolSizeValidation?: string; - setDbVolSize: (v: number) => void; fsVolSize: number; - fsVolSizeValidation?: string; - setFsVolSize: (v: number) => void; imgVolSize: number; - imgVolSizeValidation?: string; - setImgVolSize: (v: number) => void; configureLoadBalancer: boolean; - configureLoadBalancerInitial: boolean; - setConfigureLoadBalancer: (v: boolean) => void; + addCiscoIntersightUrl: boolean; + ciscoIntersightURL: string; }; export type CimConfigProgressAlertProps = { @@ -26,12 +14,14 @@ export type CimConfigProgressAlertProps = { assistedServiceDeploymentUrl: string; }; -export type CimConfigurationFormProps = CimConfiguratioProps & { - onClose: () => void; +export type CimConfigurationFormFieldsProps = { isEdit: boolean; isInProgressPeriod: boolean; docConfigUrl: string; docConfigAwsUrl: string; + platform: string; + configureLoadBalancerInitial: boolean; + setConfigureLoadBalancerInitial: (value: boolean) => void; }; export type CimConfigurationModalProps = { @@ -42,11 +32,6 @@ export type CimConfigurationModalProps = { docDisconnectedUrl: string; docConfigUrl: string; docConfigAwsUrl: string; - - createResource: CreateResourceFuncType; - getResource: GetResourceFuncType; - listResources: ListResourcesFuncType; - patchResource: PatchResourceFuncType; }; export type CimConfigMissingAlertProps = { diff --git a/libs/ui-lib/lib/cim/hooks/index.tsx b/libs/ui-lib/lib/cim/hooks/index.tsx index 7d759556d6..843a385735 100644 --- a/libs/ui-lib/lib/cim/hooks/index.tsx +++ b/libs/ui-lib/lib/cim/hooks/index.tsx @@ -1 +1,2 @@ export * from './useConfigMaps'; +export * from './useAgentServiceConfig'; diff --git a/libs/ui-lib/lib/cim/hooks/useAgentServiceConfig.tsx b/libs/ui-lib/lib/cim/hooks/useAgentServiceConfig.tsx new file mode 100644 index 0000000000..db3fd9176c --- /dev/null +++ b/libs/ui-lib/lib/cim/hooks/useAgentServiceConfig.tsx @@ -0,0 +1,15 @@ +import { useK8sWatchResource } from './useK8sWatchResource'; +import { AgentServiceConfigK8sResource } from '../types'; +import { K8sWatchHookProps } from './types'; + +export const useAgentServiceConfig = (props: K8sWatchHookProps) => + useK8sWatchResource( + { + groupVersionKind: { + group: 'agent-install.openshift.io', + kind: 'AgentServiceConfig', + version: 'v1beta1', + }, + }, + props, + ); diff --git a/libs/ui-lib/lib/cim/types/index.ts b/libs/ui-lib/lib/cim/types/index.ts index f59221335b..eaf7ad6b50 100644 --- a/libs/ui-lib/lib/cim/types/index.ts +++ b/libs/ui-lib/lib/cim/types/index.ts @@ -1,5 +1,4 @@ export * from './fromOCP'; export * from './k8s'; export * from './metal3'; -export * from './resources'; export * from './models'; diff --git a/libs/ui-lib/lib/cim/types/models.tsx b/libs/ui-lib/lib/cim/types/models.tsx index bbd4f4e134..57597a6304 100644 --- a/libs/ui-lib/lib/cim/types/models.tsx +++ b/libs/ui-lib/lib/cim/types/models.tsx @@ -1,12 +1,56 @@ import { K8sModel } from '@openshift-console/dynamic-plugin-sdk/lib/api/common-types'; export const AgentClusterInstallModel: K8sModel = { + kind: 'AgentClusterInstall', label: 'AgentClusterInstall', - apiVersion: 'v1beta1', + labelPlural: 'AgentClusterInstalls', plural: 'agentclusterinstalls', + apiVersion: 'v1beta1', + apiGroup: 'extensions.hive.openshift.io', + namespaced: true, abbr: 'ACI', +}; + +export const AgentServiceConfigModel: K8sModel = { + kind: 'AgentServiceConfig', + label: 'AgentServiceConfig', + labelPlural: 'AgentServiceConfigs', + plural: 'agentserviceconfigs', + apiVersion: 'v1beta1', + apiGroup: 'agent-install.openshift.io', + namespaced: false, + abbr: 'asc', +}; + +export const IngressControllerModel: K8sModel = { + kind: 'IngressController', + label: 'IngressController', + labelPlural: 'IngressControllers', + plural: 'ingresscontrollers', + apiVersion: 'v1', + apiGroup: 'operator.openshift.io', namespaced: true, - kind: 'AgentClusterInstall', - labelPlural: 'AgentClusterInstalls', - apiGroup: 'extensions.hive.openshift.io', + abbr: 'ic', +}; + +export const RouteModel: K8sModel = { + kind: 'Route', + label: 'Route', + labelPlural: 'Routes', + plural: 'routes', + apiVersion: 'v1', + apiGroup: 'route.openshift.io', + namespaced: true, + abbr: 'rt', +}; + +export const ProvisioningModel: K8sModel = { + kind: 'Provisioning', + label: 'Provisioning', + labelPlural: 'Provisionings', + plural: 'provisionings', + apiVersion: 'v1alpha1', + apiGroup: 'metal3.io', + namespaced: false, + abbr: 'p', }; diff --git a/libs/ui-lib/lib/cim/types/resources.ts b/libs/ui-lib/lib/cim/types/resources.ts deleted file mode 100644 index 5a8aad8bdf..0000000000 --- a/libs/ui-lib/lib/cim/types/resources.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; - -// To conform ACM -type ResourceType = { - // status?: any; - apiVersion: string; - kind: string; - metadata?: { - name?: string; - namespace?: string; - resourceVersion?: string; - creationTimestamp?: string; - uid?: string; - annotations?: Record; - labels?: Record; - generateName?: string; - deletionTimestamp?: string; - selfLink?: string; - finalizers?: string[]; - ownerReferences?: { - apiVersion: string; - blockOwnerDeletion?: boolean; - controller?: boolean; - kind: string; - name: string; - uid?: string; - }[]; - // managedFields?: any[]; - }; -}; - -export type CreateResourceFuncType = (resource: ResourceType) => Promise; -export type DeleteResourceFuncType = (resource: ResourceType) => Promise; -export type GetResourceFuncType = (resource: ResourceType) => Promise; -export type ListResourcesFuncType = ( - resource: { apiVersion: string; kind: string; metadata?: { namespace?: string } }, - labels?: string[], - query?: Record, -) => Promise; -export type ResourcePatch = { op: 'replace' | 'add'; path: string; value: unknown }; -export type PatchResourceFuncType = ( - resource: ResourceType, - patches: ResourcePatch[], -) => Promise; - -export const convertOCPtoCIMResourceHeader = (res: K8sResourceCommon): ResourceType => ({ - apiVersion: res.apiVersion || '', - kind: res.kind || '', - metadata: res.metadata, -}); diff --git a/libs/ui-lib/lib/common/components/clusterConfiguration/DownloadIso.tsx b/libs/ui-lib/lib/common/components/clusterConfiguration/DownloadIso.tsx index ec366b2338..3da2a446d2 100644 --- a/libs/ui-lib/lib/common/components/clusterConfiguration/DownloadIso.tsx +++ b/libs/ui-lib/lib/common/components/clusterConfiguration/DownloadIso.tsx @@ -23,6 +23,7 @@ export type DownloadISOProps = { isSNO?: boolean; fileName?: string; downloadUrl: string; + ciscoUrl?: string; onClose: () => void; onReset?: () => void; docVersion?: string; @@ -38,13 +39,14 @@ const DownloadIso = ({ isSNO = false, docVersion, updateTagsForCiscoIntersight, + ciscoUrl, }: DownloadISOProps) => { const wgetCommand = `wget -O ${fileName} '${downloadUrl || ''}'`; const { t } = useTranslation(); const openCiscoIntersightHostsLink = (downloadUrl: string) => { updateTagsForCiscoIntersight ? updateTagsForCiscoIntersight() : ''; - window.open(getCiscoIntersightLink(downloadUrl), '_blank', 'noopener'); + window.open(getCiscoIntersightLink(downloadUrl, ciscoUrl), '_blank', 'noopener'); }; return ( diff --git a/libs/ui-lib/lib/common/components/ui/formik/InputField.tsx b/libs/ui-lib/lib/common/components/ui/formik/InputField.tsx index bdb8d2d6b4..9cdd3cf014 100644 --- a/libs/ui-lib/lib/common/components/ui/formik/InputField.tsx +++ b/libs/ui-lib/lib/common/components/ui/formik/InputField.tsx @@ -32,6 +32,7 @@ const InputField: React.FC< description, labelInfo, showErrorMessage = true, + type = 'text', ...props }, ref: React.Ref, @@ -75,6 +76,7 @@ const InputField: React.FC< onChange && onChange(event); } }} + type={type} /> {children} @@ -82,13 +84,20 @@ const InputField: React.FC< {((showErrorMessage && !isValid) || helperText) && ( - } - variant={showErrorMessage ? 'error' : 'default'} - id={showErrorMessage && !isValid ? `${fieldId}-helper-error` : `${fieldId}-helper`} - > - {showErrorMessage ? errorMessage : helperText} - + {showErrorMessage && !isValid && ( + } + variant={'error'} + id={`${fieldId}-helper-error`} + > + {errorMessage} + + )} + {helperText && ( + + {helperText} + + )} )} diff --git a/libs/ui-lib/lib/common/components/ui/formik/types.ts b/libs/ui-lib/lib/common/components/ui/formik/types.ts index 02eeff1d0b..2534991389 100644 --- a/libs/ui-lib/lib/common/components/ui/formik/types.ts +++ b/libs/ui-lib/lib/common/components/ui/formik/types.ts @@ -1,11 +1,11 @@ import * as React from 'react'; import { - TextInputTypes, FormSelectOptionProps, TooltipProps, RadioProps, FormSelectProps, DropEvent, + TextInputProps, } from '@patternfly/react-core'; import { DropdownItemProps } from '@patternfly/react-core'; import { FieldValidator, FieldHelperProps } from 'formik'; @@ -64,7 +64,7 @@ export interface SwitchFieldProps extends FieldProps { } export interface InputFieldProps extends FieldProps { - type?: TextInputTypes; + type?: TextInputProps['type']; placeholder?: string; noDefaultOnChange?: boolean; onChange?: (event: React.FormEvent) => void; diff --git a/libs/ui-lib/lib/common/config/docs_links.ts b/libs/ui-lib/lib/common/config/docs_links.ts index 511a40eb97..b04770a538 100644 --- a/libs/ui-lib/lib/common/config/docs_links.ts +++ b/libs/ui-lib/lib/common/config/docs_links.ts @@ -146,8 +146,12 @@ export const FEEDBACK_FORM_LINK = export const CHANGE_ISO_PASSWORD_FILE_LINK = 'https://raw.githubusercontent.com/openshift/assisted-service/master/docs/change-iso-password.sh'; -export const getCiscoIntersightLink = (downloadIsoUrl: string) => - `https://www.intersight.com/an/workflow/workflow-definitions/execute/AddServersFromISO?_workflow_Version=1&IsoUrl=${downloadIsoUrl}`; +export const getCiscoIntersightLink = (downloadIsoUrl: string, ciscoUrl?: string) => { + if (ciscoUrl) { + return `${ciscoUrl}?_workflow_Version=1&IsoUrl=${downloadIsoUrl}`; + } + return `https://www.intersight.com/an/workflow/workflow-definitions/execute/AddServersFromISO?_workflow_Version=1&IsoUrl=${downloadIsoUrl}`; +}; export const MTV_LINK = 'https://docs.redhat.com/en/documentation/migration_toolkit_for_virtualization/2.7/';