Skip to content

Commit

Permalink
feat(sap-features-hub): add pre-installation form step2
Browse files Browse the repository at this point in the history
ref: MANAGER-15976

Signed-off-by: Paul Dickerson <[email protected]>
  • Loading branch information
Paul Dickerson committed Jan 10, 2025
1 parent f9c5f91 commit 52e2140
Show file tree
Hide file tree
Showing 13 changed files with 302 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
{
"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.",
"service_cta": "Spécifiez l'installation",
"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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,22 @@ import {
OdsSelectCustomEvent,
} from '@ovhcloud/ods-components';

type SelectFieldProps<T extends Record<string, unknown>> = {
type SelectOptionsProps<T> = T extends Record<string, unknown>
? {
options: T[];
optionValueKey: keyof T;
optionLabelKey?: keyof T;
}
: {
options: string[] | number[];
optionValueKey?: never;
optionLabelKey?: never;
};

type SelectFieldProps<T> = SelectOptionsProps<T> & {
name: string;
label: string;
options: T[];
optionValueKey: keyof T;
optionLabelKey?: keyof T;
isDisabled: boolean;
isDisabled?: boolean;
isLoading?: boolean;
handleChange: (
event: OdsSelectCustomEvent<OdsSelectChangeEventDetail>,
Expand All @@ -25,9 +34,12 @@ type SelectFieldProps<T extends Record<string, unknown>> = {
error?: string;
};

export const SelectField = <T extends Record<string, unknown>>({
const getFormattedValue = (value: unknown) =>
typeof value === 'number' ? value : String(value);

export const SelectField = <T,>({
name,
isDisabled,
isDisabled = false,
isLoading,
handleChange,
options = [],
Expand All @@ -37,11 +49,14 @@ export const SelectField = <T extends Record<string, unknown>>({
placeholder,
error,
}: SelectFieldProps<T>) => {
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 (
<OdsFormField error={error}>
Expand All @@ -59,13 +74,16 @@ export const SelectField = <T extends Record<string, unknown>>({
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) => (
<option key={opt.value} value={opt.value}>
<option
key={getFormattedValue(opt.value)}
value={getFormattedValue(opt.value)}
>
{opt.label}
</option>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<unknown>> =>
v6.get(getApplicationVersionRoute(serviceName));
Original file line number Diff line number Diff line change
@@ -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),
};
};
Original file line number Diff line number Diff line change
@@ -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,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const useLocalStorage = () => {
const setStorageItem = (key: string, value: Record<string, unknown>) => {
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 };
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,14 +22,16 @@ export type DashboardLayoutProps = {

export default function PreinstallationPage() {
const { t } = useTranslation('installation');
const { currentStep } = useFormSteps();

return (
<BaseLayout
breadcrumb={<Breadcrumb />}
header={{ title: t('title') }}
header={{ title: t('title', { step: currentStep }) }}
backLinkLabel={t('backlink_label')}
onClickReturn={() => {}}
description={t('description')}
iconAlignment={IconLinkAlignmentType.left}
>
<Outlet />
</BaseLayout>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, ReactNode> = {
'1': <InstallationInitialStep />,
'2': <InstallationStepDeployment />,
};

export default function FormStep() {
const { currentStep } = useFormSteps();

return steps[currentStep] || <div>Not developed yet.</div>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(null);
const [datacenterId, setDatacenterId] = useState<string>(null);
const [clusterName, setClusterName] = useState<string>(null);
const [isFormValid, setIsFormValid] = useState<boolean>(false);
const { initializeAndProceed, currentStepLabel } = useFormSteps();
const { setStorageItem } = useLocalStorage();

const {
data: services,
Expand All @@ -37,7 +41,7 @@ export default function InstallationStep1() {
datacenterId,
});

const isVDCError = useMemo(
const isError = useMemo(
() =>
datacenterId &&
!clusters?.length &&
Expand All @@ -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 (
<div>
Expand Down Expand Up @@ -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
}
/>
<SelectField
Expand All @@ -97,12 +104,16 @@ export default function InstallationStep1() {
options={clusters}
optionValueKey={'name'}
isDisabled={
!datacenterId || isLoadingClusters || isClustersError || isVDCError
!datacenterId || isLoadingClusters || isClustersError || isError
}
isLoading={isLoadingClusters && !isLoadingDatacentres}
handleChange={(event) => setClusterName(event.detail.value)}
/>
<OdsButton label={t('service_cta')} isDisabled={!isFormValid} />
<OdsButton
label={t('service_cta')}
isDisabled={!isFormValid}
onClick={handleSubmit}
/>
</form>
</div>
);
Expand Down
Loading

0 comments on commit 52e2140

Please sign in to comment.