Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions libs/cypress/e2e/fleets/createFleet.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ describe('Create fleet form', () => {
fleetDetailsPage.title.should('have.text', 'sample-fleet');
});

it('allows navigating between previous and current step when there are validation errors', () => {
fleetsPage.openCreateFleetFormButton.click();
createFleetWizardPage = new CreateFleetWizardPage();

createFleetWizardPage.newFleetNameField.type('another-fleet');
createFleetWizardPage.nextFleetWizardButton.should('be.enabled').click();

createFleetWizardPage.newFleetSystemImageField.type('invalid!oci').blur();
createFleetWizardPage.nextFleetWizardButton.should('be.disabled');
createFleetWizardPage.backFleetWizardButton.click();
createFleetWizardPage.nextFleetWizardButton.should('be.enabled').click();

createFleetWizardPage.newFleetSystemImageField.should('be.visible').should('have.value', 'invalid!oci');
});

it('disables the create fleet button if a fleet with the same name exists', () => {
const existingFleetName = 'eu-west-prod-001';
fleetsPage.fleetRow(existingFleetName).should('exist');
Expand Down
4 changes: 4 additions & 0 deletions libs/cypress/pages/CreateFleetWizardPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export class CreateFleetWizardPage {
return cy.contains('button', 'Next');
}

get backFleetWizardButton() {
return cy.contains('button', 'Back');
}

get addConfigurationButton() {
return cy.contains('button', 'Add configuration');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
getInitialValuesFromItem,
getValidationSchema,
} from './utils';
import { isWizardStepDisabled } from '../../../utils/wizards';
import { AddCatalogItemFormValues } from './types';
import FlightCtlWizardFooter from '../../common/FlightCtlWizardFooter';
import LeaveFormConfirmation from '../../common/LeaveFormConfirmation';
Expand Down Expand Up @@ -57,11 +58,6 @@ const getValidStepIds = (formikErrors: FormikErrors<AddCatalogItemFormValues>):
return validStepIds;
};

const isDisabledStep = (stepId: string, validStepIds: string[]) => {
const validIndex = validStepIds.indexOf(stepId);
return validIndex === -1 || validIndex !== orderedIds.indexOf(stepId);
};

const validateStep = (activeStepId: string, errors: FormikErrors<AddCatalogItemFormValues>) => {
switch (activeStepId) {
case generalInfoStepId:
Expand Down Expand Up @@ -178,27 +174,21 @@ const AddCatalogItemWizard = () => {
<WizardStep
name={t('Type and configuration')}
id={typeConfigStepId}
isDisabled={isDisabledStep(generalInfoStepId, validStepIds)}
isDisabled={isWizardStepDisabled(typeConfigStepId, orderedIds, validStepIds)}
>
{currentStep?.id === typeConfigStepId && <TypeConfigStep isEdit={isEdit} isReadOnly={isReadOnly} />}
</WizardStep>
<WizardStep
name={t('Versions')}
id={versionStepId}
isDisabled={
isDisabledStep(typeConfigStepId, validStepIds) || isDisabledStep(generalInfoStepId, validStepIds)
}
isDisabled={isWizardStepDisabled(versionStepId, orderedIds, validStepIds)}
>
{currentStep?.id === versionStepId && <VersionStep isReadOnly={isReadOnly} isEdit={isEdit} />}
</WizardStep>
<WizardStep
name={isReadOnly ? t('Review') : isEdit ? t('Review and save') : t('Review and create')}
id={reviewStepId}
isDisabled={
isDisabledStep(versionStepId, validStepIds) ||
isDisabledStep(typeConfigStepId, validStepIds) ||
isDisabledStep(generalInfoStepId, validStepIds)
}
isDisabled={isWizardStepDisabled(reviewStepId, orderedIds, validStepIds)}
>
{currentStep?.id === reviewStepId && (
<ReviewStep error={error} isEdit={isEdit} isReadOnly={isReadOnly} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,28 @@ import UpdateStep, { isUpdateStepValid } from './steps/UpdateStep';
import ReviewStep from './steps/ReviewStep';
import LeaveFormConfirmation from '../../common/LeaveFormConfirmation';
import { validApplicationAndVolumeName } from '../../form/validations';
import { isWizardStepDisabled } from '../../../utils/wizards';

const versionStepId = 'version-step';
const configStepId = 'config-step';
const reviewStepId = 'review-step';

const orderedIds = [versionStepId, configStepId, reviewStepId];

const getValidStepIds = (errors: FormikErrors<AppUpdateFormik>, values: AppUpdateFormik): string[] => {
const validStepIds: string[] = [];
if (isUpdateStepValid(errors)) {
validStepIds.push(versionStepId);
}
if (isAppConfigStepValid(values, errors)) {
validStepIds.push(configStepId);
}
if (validStepIds.length === orderedIds.length - 1) {
validStepIds.push(reviewStepId);
}
return validStepIds;
};

const validateUpdateWizardStep = (
activeStepId: string,
errors: FormikErrors<AppUpdateFormik>,
Expand Down Expand Up @@ -55,8 +72,7 @@ const WizardContent: React.FC<WizardContentProps> = ({

const { values, errors } = useFormikContext<AppUpdateFormik>();

const isVersionStepValid = !!values.version;
const isConfigStepValid = isAppConfigStepValid(values, errors);
const validStepIds = getValidStepIds(errors, values);

return (
<>
Expand All @@ -82,13 +98,17 @@ const WizardContent: React.FC<WizardContentProps> = ({
<UpdateStep catalogItem={catalogItem} currentVersion={currentVersion} isEdit={!!appSpec} />
)}
</WizardStep>
<WizardStep name={t('Configuration')} id={configStepId} isDisabled={!isVersionStepValid}>
<WizardStep
name={t('Configuration')}
id={configStepId}
isDisabled={isWizardStepDisabled(configStepId, orderedIds, validStepIds)}
>
{currentStep?.id === configStepId && <AppConfigStep isEdit={!!appSpec} />}
</WizardStep>
<WizardStep
name={t('Review and deploy')}
id={reviewStepId}
isDisabled={!isVersionStepValid || !isConfigStepValid}
isDisabled={isWizardStepDisabled(reviewStepId, orderedIds, validStepIds)}
>
{currentStep?.id === reviewStepId && (
<ReviewStep error={error} schemaErrors={schemaErrors} isEdit={!!appSpec} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,24 @@ import UpdateStep, { isUpdateStepValid } from './steps/UpdateStep';
import { getErrorMessage } from '../../../utils/error';
import ReviewStep from './steps/ReviewStep';
import LeaveFormConfirmation from '../../common/LeaveFormConfirmation';
import { isWizardStepDisabled } from '../../../utils/wizards';

const versionStepId = 'version-step';
const reviewStepId = 'review-step';

const orderedIds = [versionStepId, reviewStepId];

const getValidStepIds = (errors: FormikErrors<InstallSpecFormik>): string[] => {
const validStepIds: string[] = [];
if (isUpdateStepValid(errors)) {
validStepIds.push(versionStepId);
}
if (validStepIds.length === orderedIds.length - 1) {
validStepIds.push(reviewStepId);
}
return validStepIds;
};

const validateUpdateWizardStep = (activeStepId: string, errors: FormikErrors<InstallSpecFormik>) => {
if (activeStepId === versionStepId) return isUpdateStepValid(errors);
return true;
Expand All @@ -34,9 +48,9 @@ const WizardContent: React.FC<WizardContentProps> = ({ currentVersion, catalogIt
const { t } = useTranslation();
const [currentStep, setCurrentStep] = React.useState<WizardStepType>();

const { values } = useFormikContext<InstallSpecFormik>();
const { errors } = useFormikContext<InstallSpecFormik>();

const isVersionStepValid = !!values.version;
const validStepIds = getValidStepIds(errors);

return (
<>
Expand All @@ -62,7 +76,11 @@ const WizardContent: React.FC<WizardContentProps> = ({ currentVersion, catalogIt
<UpdateStep catalogItem={catalogItem} currentVersion={currentVersion} isEdit={isEdit} />
)}
</WizardStep>
<WizardStep name={t('Review and deploy')} id={reviewStepId} isDisabled={!isVersionStepValid}>
<WizardStep
name={t('Review and deploy')}
id={reviewStepId}
isDisabled={isWizardStepDisabled(reviewStepId, orderedIds, validStepIds)}
>
{currentStep?.id === reviewStepId && <ReviewStep error={error} isEdit={isEdit} />}
</WizardStep>
</Wizard>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CatalogItem } from '@flightctl/types/alpha';
import { Wizard, WizardStep, WizardStepType } from '@patternfly/react-core';
import { Formik, useFormikContext } from 'formik';
import { Formik, FormikErrors, useFormikContext } from 'formik';
import * as React from 'react';
import * as Yup from 'yup';
import { load } from 'js-yaml';
Expand All @@ -22,6 +22,26 @@ import { useAppContext } from '../../../hooks/useAppContext';
import { getInitialAppConfig } from './utils';
import { useSubmitCatalogForm } from '../useSubmitCatalogForm';
import { validApplicationAndVolumeName } from '../../form/validations';
import { isWizardStepDisabled } from '../../../utils/wizards';

const orderedIds = [specificationsStepId, selectTargetStepId, appConfigStepId, reviewStepId];

const getValidStepIds = (errors: FormikErrors<InstallAppFormik>, values: InstallAppFormik): string[] => {
const validStepIds: string[] = [];
if (isSpecsStepValid(errors)) {
validStepIds.push(specificationsStepId);
}
if (isSelectTargetStepValid(errors)) {
validStepIds.push(selectTargetStepId);
}
if (isAppConfigStepValid(values, errors)) {
validStepIds.push(appConfigStepId);
}
if (validStepIds.length === orderedIds.length - 1) {
validStepIds.push(reviewStepId);
}
return validStepIds;
};

export const validateAppWizardStep: FlightCtlWizardFooterProps<InstallAppFormik>['validateStep'] = (
activeStepId,
Expand Down Expand Up @@ -55,6 +75,7 @@ const InstallAppWizardContent = ({
}: InstallAppWizardContentProps) => {
const { t } = useTranslation();
const { values, errors } = useFormikContext<InstallAppFormik>();
const validStepIds = getValidStepIds(errors, values);
return isSuccessful ? (
<UpdateSuccessPage />
) : (
Expand All @@ -81,22 +102,24 @@ const InstallAppWizardContent = ({
<SpecificationsStep catalogItem={catalogItem} />
)}
</WizardStep>
<WizardStep name={t('Select target')} id={selectTargetStepId} isDisabled={!isSpecsStepValid(errors)}>
<WizardStep
name={t('Select target')}
id={selectTargetStepId}
isDisabled={isWizardStepDisabled(selectTargetStepId, orderedIds, validStepIds)}
>
{currentStep?.id === selectTargetStepId && <SelectTargetStep catalogItem={catalogItem} />}
</WizardStep>
<WizardStep
name={t('Application configuration')}
id={appConfigStepId}
isDisabled={!isSelectTargetStepValid(errors) || !isSpecsStepValid(errors)}
isDisabled={isWizardStepDisabled(appConfigStepId, orderedIds, validStepIds)}
>
{currentStep?.id === appConfigStepId && <AppConfigStep schemaErrors={schemaErrors} />}
</WizardStep>
<WizardStep
name={t('Review and deploy')}
id={reviewStepId}
isDisabled={
!isAppConfigStepValid(values, errors) || !isSpecsStepValid(errors) || !isSelectTargetStepValid(errors)
}
isDisabled={isWizardStepDisabled(reviewStepId, orderedIds, validStepIds)}
>
{currentStep?.id === reviewStepId && <ReviewStep error={error} catalogItem={catalogItem} />}
</WizardStep>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CatalogItem } from '@flightctl/types/alpha';
import { Wizard, WizardStep, WizardStepType } from '@patternfly/react-core';
import { Formik, useFormikContext } from 'formik';
import { Formik, FormikErrors, useFormikContext } from 'formik';
import * as React from 'react';
import * as Yup from 'yup';
import { Device, Fleet } from '@flightctl/types';
Expand All @@ -18,6 +18,26 @@ import UpdateSuccessPage from './UpdateSuccessPage';
import FlightCtlWizardFooter, { FlightCtlWizardFooterProps } from '../../common/FlightCtlWizardFooter';
import { useAppContext } from '../../../hooks/useAppContext';
import { useNavigate } from '../../../hooks/useNavigate';
import { isWizardStepDisabled } from '../../../utils/wizards';

const getOrderedStepIds = (target: InstallOsFormik['target']) =>
target === 'new-device'
? [specificationsStepId, selectTargetStepId]
: [specificationsStepId, selectTargetStepId, reviewStepId];

const getValidStepIds = (errors: FormikErrors<InstallOsFormik>, orderedStepIds: string[]): string[] => {
const validStepIds: string[] = [];
if (isSpecsStepValid(errors)) {
validStepIds.push(specificationsStepId);
}
if (isSelectTargetStepValid(errors)) {
validStepIds.push(selectTargetStepId);
}
if (orderedStepIds.includes(reviewStepId) && validStepIds.length === orderedStepIds.length - 1) {
validStepIds.push(reviewStepId);
}
return validStepIds;
};

export const validateOsWizardStep: FlightCtlWizardFooterProps<InstallOsFormik>['validateStep'] = (
activeStepId,
Expand Down Expand Up @@ -47,6 +67,8 @@ const InstallOsWizardContent = ({
}: InstallOsWizardContentProps) => {
const { t } = useTranslation();
const { values, errors } = useFormikContext<InstallOsFormik>();
const orderedStepIds = getOrderedStepIds(values.target);
const validStepIds = getValidStepIds(errors, orderedStepIds);
const showLeaveConfirmation = !(values.target === 'new-device' && currentStep?.id === selectTargetStepId);

return isSuccessful ? (
Expand Down Expand Up @@ -75,14 +97,18 @@ const InstallOsWizardContent = ({
<SpecificationsStep catalogItem={catalogItem} showNewDevice />
)}
</WizardStep>
<WizardStep name={t('Select target')} id={selectTargetStepId} isDisabled={!isSpecsStepValid(errors)}>
<WizardStep
name={t('Select target')}
id={selectTargetStepId}
isDisabled={isWizardStepDisabled(selectTargetStepId, orderedStepIds, validStepIds)}
>
{currentStep?.id === selectTargetStepId && <SelectTargetStep catalogItem={catalogItem} />}
</WizardStep>
{values.target !== 'new-device' && (
<WizardStep
name={t('Review and deploy')}
id={reviewStepId}
isDisabled={!isSpecsStepValid(errors) || !isSelectTargetStepValid(errors)}
isDisabled={isWizardStepDisabled(reviewStepId, orderedStepIds, validStepIds)}
>
{currentStep?.id === reviewStepId && <ReviewStep error={error} catalogItem={catalogItem} />}
</WizardStep>
Expand Down
Loading
Loading