diff --git a/packages/manager/apps/sap-features-hub/public/translations/installation/Messages_fr_FR.json b/packages/manager/apps/sap-features-hub/public/translations/installation/Messages_fr_FR.json index 7527457896a7..73c0e54657ec 100644 --- a/packages/manager/apps/sap-features-hub/public/translations/installation/Messages_fr_FR.json +++ b/packages/manager/apps/sap-features-hub/public/translations/installation/Messages_fr_FR.json @@ -1,7 +1,8 @@ { - "title": "Assistant de pré-installation SAP - Étape 1/8", + "title": "Assistant de pré-installation SAP - Étape {{step}}/8", "description": "Cet assistant vous guide pour réaliser une pré-installation d'un système SAP.", "backlink_label": "Retour au SAP Features Hub", + "previous_step_cta": "Précédent", "select_label": "Sélectionnez", "service_title": "Sélectionnez votre service VMware on OVHcloud", "service_subtitle": "Sélectionnez votre service VMware on OVHcloud, votre datacentre et le cluster sur lequel vous souhaitez déployer votre système SAP.", @@ -9,5 +10,11 @@ "service_input_vmware": "Service VMware on OVHcloud", "service_input_vdc": "Datacentre", "service_input_cluster": "Cluster", - "service_input_error_no_cluster_available": "Vous devez sélectionner un datacentre contenant des hôtes pour continuer." + "service_input_error_no_cluster_available": "Vous devez sélectionner un datacentre contenant des hôtes pour continuer.", + "deployment_title": "Spécifiez l'installation souhaitée", + "deployment_subtitle": "Sélectionnez le type d'installation correspondant à vos besoins.", + "deployment_cta": "Précisez vos informations SAP", + "deployment_input_application_version": "Version d'application", + "deployment_input_application_type": "Type d'application", + "deployment_input_deployment_type": "Type de déploiement" } diff --git a/packages/manager/apps/sap-features-hub/src/components/Form/SelectField.component.tsx b/packages/manager/apps/sap-features-hub/src/components/Form/SelectField.component.tsx index f8cd6f8983f5..965c2e7c9f7a 100644 --- a/packages/manager/apps/sap-features-hub/src/components/Form/SelectField.component.tsx +++ b/packages/manager/apps/sap-features-hub/src/components/Form/SelectField.component.tsx @@ -10,13 +10,22 @@ import { OdsSelectCustomEvent, } from '@ovhcloud/ods-components'; -type SelectFieldProps> = { +type SelectOptionsProps = T extends Record + ? { + options: T[]; + optionValueKey: keyof T; + optionLabelKey?: keyof T; + } + : { + options: string[] | number[]; + optionValueKey?: never; + optionLabelKey?: never; + }; + +type SelectFieldProps = SelectOptionsProps & { name: string; label: string; - options: T[]; - optionValueKey: keyof T; - optionLabelKey?: keyof T; - isDisabled: boolean; + isDisabled?: boolean; isLoading?: boolean; handleChange: ( event: OdsSelectCustomEvent, @@ -25,9 +34,12 @@ type SelectFieldProps> = { error?: string; }; -export const SelectField = >({ +const getFormattedValue = (value: unknown) => + typeof value === 'number' ? value : String(value); + +export const SelectField = ({ name, - isDisabled, + isDisabled = false, isLoading, handleChange, options = [], @@ -37,11 +49,14 @@ export const SelectField = >({ placeholder, error, }: SelectFieldProps) => { - const sanitizedOptions = options.map((opt) => ({ - value: String(opt?.[optionValueKey] || ''), - label: String(opt?.[optionLabelKey] || opt?.[optionValueKey] || ''), - ...opt, - })); + const sanitizedOptions = options.map((opt) => + typeof opt === 'object' + ? { + value: opt?.[optionValueKey] || '', + label: String(opt?.[optionLabelKey] || opt?.[optionValueKey] || ''), + } + : { value: opt, label: String(opt) }, + ); return ( @@ -59,13 +74,16 @@ export const SelectField = >({ className="w-full max-w-[304px]" hasError={!!error} defaultValue={ - sanitizedOptions.length === 1 - ? sanitizedOptions[0].value + !isLoading && sanitizedOptions.length === 1 + ? String(sanitizedOptions[0].value) : undefined } > {sanitizedOptions.map((opt) => ( - ))} diff --git a/packages/manager/apps/sap-features-hub/src/data/api/installationDeployment.ts b/packages/manager/apps/sap-features-hub/src/data/api/installationDeployment.ts new file mode 100644 index 000000000000..d188f435f625 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/data/api/installationDeployment.ts @@ -0,0 +1,9 @@ +import { ApiResponse, v6 } from '@ovh-ux/manager-core-api'; + +const getApplicationVersionRoute = (serviceName: string) => + `/dedicatedCloud/${serviceName}/sap/capabilities`; + +export const getApplicationVersions = async ( + serviceName: string, +): Promise> => + v6.get(getApplicationVersionRoute(serviceName)); diff --git a/packages/manager/apps/sap-features-hub/src/hooks/formStep/useFormSteps.ts b/packages/manager/apps/sap-features-hub/src/hooks/formStep/useFormSteps.ts new file mode 100644 index 000000000000..780105986942 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/hooks/formStep/useFormSteps.ts @@ -0,0 +1,44 @@ +import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import { subRoutes, urls } from '@/routes/routes.constant'; + +const createStepUrl = (stepNumber: number, serviceName: string) => + urls.installationStep + .replace(':stepId', stepNumber.toString()) + .replace(':serviceName', serviceName); + +export const useFormSteps = () => { + const { stepId, serviceName } = useParams(); + const { pathname } = useLocation(); + const navigate = useNavigate(); + const isInitialStep = pathname.includes(subRoutes.initialStep); + + const initializeAndProceed = (selectedServiceName: string) => { + if (isInitialStep && selectedServiceName) + navigate(createStepUrl(2, selectedServiceName)); + }; + + const nextStep = () => { + if (stepId && serviceName) + navigate(createStepUrl(Number(stepId) + 1, serviceName)); + }; + + const previousStep = () => { + if (!stepId || !serviceName || isInitialStep) return; + if (stepId === '2') { + navigate(urls.installationInitialStep); + } else { + navigate(createStepUrl(Number(stepId) - 1, serviceName)); + } + }; + + const getStepLabel = (step: string) => `sap_installation_formStep_${step}`; + + return { + initializeAndProceed, + nextStep, + previousStep, + getStepLabel, + currentStep: isInitialStep ? '1' : stepId, + currentStepLabel: getStepLabel(isInitialStep ? '1' : stepId), + }; +}; diff --git a/packages/manager/apps/sap-features-hub/src/hooks/installationDeployment/useApplicationVersions.ts b/packages/manager/apps/sap-features-hub/src/hooks/installationDeployment/useApplicationVersions.ts new file mode 100644 index 000000000000..5c310c4c776f --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/hooks/installationDeployment/useApplicationVersions.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { getApplicationVersions } from '@/data/api/installationDeployment'; + +// TODO: implement API calls when developed +export const useApplicationVersions = (serviceName: string) => + useQuery({ + queryKey: ['applicationVersions', serviceName], + queryFn: () => getApplicationVersions(serviceName), + select: (res) => res.data, + enabled: !!serviceName, + }); diff --git a/packages/manager/apps/sap-features-hub/src/hooks/localStorage/useLocalStorage.tsx b/packages/manager/apps/sap-features-hub/src/hooks/localStorage/useLocalStorage.tsx new file mode 100644 index 000000000000..01cfd0151c3b --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/hooks/localStorage/useLocalStorage.tsx @@ -0,0 +1,28 @@ +export const useLocalStorage = () => { + const setStorageItem = (key: string, value: Record) => { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch (err) { + throw new Error('Cannot access localStorage'); + } + }; + + const getStorageItem = (key: string) => { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : undefined; + } catch (err) { + throw new Error('Cannot access localStorage'); + } + }; + + const removeStorageItem = (key: string) => { + try { + localStorage.removeItem(key); + } catch (err) { + throw new Error('Cannot access localStorage'); + } + }; + + return { setStorageItem, getStorageItem, removeStorageItem }; +}; diff --git a/packages/manager/apps/sap-features-hub/src/pages/installation/InstallationDashboard.page.tsx b/packages/manager/apps/sap-features-hub/src/pages/installation/InstallationDashboard.page.tsx index 1e2d93a2b81c..1c7afec40c77 100644 --- a/packages/manager/apps/sap-features-hub/src/pages/installation/InstallationDashboard.page.tsx +++ b/packages/manager/apps/sap-features-hub/src/pages/installation/InstallationDashboard.page.tsx @@ -2,9 +2,13 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import { BaseLayout } from '@ovh-ux/manager-react-components'; +import { + BaseLayout, + IconLinkAlignmentType, +} from '@ovh-ux/manager-react-components'; import Breadcrumb from '@/components/Breadcrumb/Breadcrumb'; +import { useFormSteps } from '@/hooks/formStep/useFormSteps'; export type DashboardTabItemProps = { name: string; @@ -18,14 +22,16 @@ export type DashboardLayoutProps = { export default function PreinstallationPage() { const { t } = useTranslation('installation'); + const { currentStep } = useFormSteps(); return ( } - header={{ title: t('title') }} + header={{ title: t('title', { step: currentStep }) }} backLinkLabel={t('backlink_label')} onClickReturn={() => {}} description={t('description')} + iconAlignment={IconLinkAlignmentType.left} > diff --git a/packages/manager/apps/sap-features-hub/src/pages/installation/formStep/FormStep.page.tsx b/packages/manager/apps/sap-features-hub/src/pages/installation/formStep/FormStep.page.tsx new file mode 100644 index 000000000000..f0c945c04bfc --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/installation/formStep/FormStep.page.tsx @@ -0,0 +1,15 @@ +import React, { ReactNode } from 'react'; +import { useFormSteps } from '@/hooks/formStep/useFormSteps'; +import InstallationInitialStep from '../initialStep/InstallationInitialStep.page'; +import InstallationStepDeployment from '../stepDeployment/InstallationStepDeployment.page'; + +const steps: Record = { + '1': , + '2': , +}; + +export default function FormStep() { + const { currentStep } = useFormSteps(); + + return steps[currentStep] ||
Not developed yet.
; +} diff --git a/packages/manager/apps/sap-features-hub/src/pages/installation/step1/InstallationStep1.page.tsx b/packages/manager/apps/sap-features-hub/src/pages/installation/initialStep/InstallationInitialStep.page.tsx similarity index 79% rename from packages/manager/apps/sap-features-hub/src/pages/installation/step1/InstallationStep1.page.tsx rename to packages/manager/apps/sap-features-hub/src/pages/installation/initialStep/InstallationInitialStep.page.tsx index 8e8552a00da6..d2b9a70cc746 100644 --- a/packages/manager/apps/sap-features-hub/src/pages/installation/step1/InstallationStep1.page.tsx +++ b/packages/manager/apps/sap-features-hub/src/pages/installation/initialStep/InstallationInitialStep.page.tsx @@ -8,13 +8,17 @@ import { useVMwareServices, } from '@/hooks/vmwareServices/useVMwareServices'; import { SelectField } from '@/components/Form/SelectField.component'; +import { useFormSteps } from '@/hooks/formStep/useFormSteps'; +import { useLocalStorage } from '@/hooks/localStorage/useLocalStorage'; -export default function InstallationStep1() { +export default function InstallationInitialStep() { const { t } = useTranslation('installation'); const [serviceName, setServiceName] = useState(null); const [datacenterId, setDatacenterId] = useState(null); const [clusterName, setClusterName] = useState(null); const [isFormValid, setIsFormValid] = useState(false); + const { initializeAndProceed, currentStepLabel } = useFormSteps(); + const { setStorageItem } = useLocalStorage(); const { data: services, @@ -37,7 +41,7 @@ export default function InstallationStep1() { datacenterId, }); - const isVDCError = useMemo( + const isError = useMemo( () => datacenterId && !clusters?.length && @@ -47,8 +51,13 @@ export default function InstallationStep1() { ); useEffect(() => { - setIsFormValid(clusterName && !isVDCError); - }, [isVDCError, clusterName]); + setIsFormValid(clusterName && !isError); + }, [isError, clusterName]); + + const handleSubmit = () => { + setStorageItem(currentStepLabel, { vdcId: datacenterId, clusterName }); + initializeAndProceed(serviceName); + }; return (
@@ -85,9 +94,7 @@ export default function InstallationStep1() { setClusterName(null); }} error={ - isVDCError - ? t('service_input_error_no_cluster_available') - : undefined + isError ? t('service_input_error_no_cluster_available') : undefined } /> setClusterName(event.detail.value)} /> - +
); diff --git a/packages/manager/apps/sap-features-hub/src/pages/installation/stepDeployment/InstallationStepDeployment.page.tsx b/packages/manager/apps/sap-features-hub/src/pages/installation/stepDeployment/InstallationStepDeployment.page.tsx new file mode 100644 index 000000000000..5dcf3a2b7041 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/installation/stepDeployment/InstallationStepDeployment.page.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { OdsButton } from '@ovhcloud/ods-components/react'; +import { FormTitle } from '@/components/Form/FormTitle.component'; +import { SelectField } from '@/components/Form/SelectField.component'; +import { useFormSteps } from '@/hooks/formStep/useFormSteps'; +import { + APPLICATION_TYPES, + APPLICATION_VERSIONS, + DEPLOYMENT_TYPES, +} from './installationStepDeployment.constants'; +import { useLocalStorage } from '@/hooks/localStorage/useLocalStorage'; + +export default function InstallationStepDeployment() { + const { t } = useTranslation('installation'); + const [applicationVersion, setApplicationVersion] = useState(null); + const [applicationType, setApplicationType] = useState(null); + const [deploymentType, setDeploymentType] = useState(null); + const { previousStep, nextStep, currentStepLabel } = useFormSteps(); + const { setStorageItem, removeStorageItem } = useLocalStorage(); + + const isFormValid = applicationVersion && applicationType && deploymentType; + + const handlePrevious = () => { + removeStorageItem(currentStepLabel); + previousStep(); + }; + const handleSubmit = () => { + setStorageItem(currentStepLabel, { + applicationVersion, + applicationType, + deploymentType, + }); + nextStep(); + }; + + return ( +
+ +
+ setApplicationVersion(event.detail.value)} + /> + setApplicationType(event.detail.value)} + /> + setDeploymentType(event.detail.value)} + /> +
+ + +
+ +
+ ); +} diff --git a/packages/manager/apps/sap-features-hub/src/pages/installation/stepDeployment/installationStepDeployment.constants.tsx b/packages/manager/apps/sap-features-hub/src/pages/installation/stepDeployment/installationStepDeployment.constants.tsx new file mode 100644 index 000000000000..d65ea4cf59b8 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/installation/stepDeployment/installationStepDeployment.constants.tsx @@ -0,0 +1,11 @@ +export const APPLICATION_VERSIONS = [ + 'NetWeaver 7.50', + 'S/4HANA 2022', + 'S/4HANA 2023', +]; +export const APPLICATION_TYPES = ['ABAP', 'JAVA']; +export const DEPLOYMENT_TYPES = [ + 'Standard', + 'Distributed', + 'High Availability', +]; diff --git a/packages/manager/apps/sap-features-hub/src/routes/routes.constant.ts b/packages/manager/apps/sap-features-hub/src/routes/routes.constant.ts index 1d5b9b3b2934..dc0c87c2884c 100644 --- a/packages/manager/apps/sap-features-hub/src/routes/routes.constant.ts +++ b/packages/manager/apps/sap-features-hub/src/routes/routes.constant.ts @@ -1,9 +1,18 @@ +export const subRoutes = { + onboarding: 'onboarding', + installation: 'installation', + serviceName: ':serviceName', + initialStep: 'initialization', + stepId: ':stepId', +}; + export const urls = { - root: '/', - onboarding: '/onboarding', - listing: '/', - dashboard: '/:serviceName', - tab2: 'Tab2', - installation: '/installation', - installationStep1: '/installation/step-1', + root: `/`, + listing: `/`, + tab2: `Tab2`, + onboarding: `/${subRoutes.onboarding}`, + dashboard: `/${subRoutes.serviceName}`, + installation: `/${subRoutes.installation}`, + installationInitialStep: `/${subRoutes.installation}/${subRoutes.initialStep}`, + installationStep: `/${subRoutes.installation}/${subRoutes.stepId}/${subRoutes.serviceName}`, }; diff --git a/packages/manager/apps/sap-features-hub/src/routes/routes.tsx b/packages/manager/apps/sap-features-hub/src/routes/routes.tsx index 68d79aaa1442..c76a87d3452b 100644 --- a/packages/manager/apps/sap-features-hub/src/routes/routes.tsx +++ b/packages/manager/apps/sap-features-hub/src/routes/routes.tsx @@ -80,10 +80,25 @@ export const Routes: any = [ ), children: [ { - id: 'step1', - path: urls.installationStep1, + id: 'initialStep', + path: urls.installationInitialStep, ...lazyRouteConfig(() => - import('@/pages/installation/step1/InstallationStep1.page'), + import( + '@/pages/installation/initialStep/InstallationInitialStep.page' + ), + ), + handle: { + tracking: { + pageName: 'dashboard', + pageType: PageType.dashboard, + }, + }, + }, + { + id: 'stepId', + path: urls.installationStep, + ...lazyRouteConfig(() => + import('@/pages/installation/formStep/FormStep.page'), ), handle: { tracking: {