Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AAP-25659] Show error if resources are missing in Schedule wizard #2551

Merged
merged 6 commits into from
Jul 8, 2024
Merged
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
67 changes: 65 additions & 2 deletions cypress/e2e/awx/views/schedules.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe.skip('Schedules - Create and Delete', () => {
let project: Project;
let inventory: Inventory;

before(() => {
beforeEach(() => {
cy.createAwxOrganization().then((o) => {
organization = o;
cy.createAwxProject(organization).then((proj) => {
Expand All @@ -35,7 +35,7 @@ describe.skip('Schedules - Create and Delete', () => {
});
});

after(() => {
afterEach(() => {
cy.deleteAwxJobTemplate(jobTemplate, { failOnStatusCode: false });
cy.deleteAwxInventory(inventory, { failOnStatusCode: false });
cy.deleteAwxProject(project, { failOnStatusCode: false });
Expand Down Expand Up @@ -74,6 +74,24 @@ describe.skip('Schedules - Create and Delete', () => {
cy.clickModalButton('Delete schedule');
cy.deleteAwxJobTemplate(jobTemplate, { failOnStatusCode: false });
});

it("can't create a schedule with missing resources", () => {
const scheduleName = 'E2E Schedule With Missing Resources' + randomString(4);
cy.deleteAwxInventory(inventory);
cy.navigateTo('awx', 'schedules');
cy.verifyPageTitle('Schedules');
cy.getByDataCy('create-schedule').click();
cy.verifyPageTitle('Create Schedule');
cy.selectDropdownOptionByResourceName('schedule_type', 'Job template');
cy.selectDropdownOptionByResourceName('job-template-select', jobTemplate.name);
cy.getByDataCy('name').type(`${scheduleName}`);
cy.clickButton('Next');
cy.clickButton('Save rule');
cy.clickButton('Next');
cy.clickButton('Next');
cy.clickButton('Finish');
cy.contains('Job Template inventory is missing or undefined.');
});
});

describe('Schedules - Create schedule of resource type Project', () => {
Expand Down Expand Up @@ -601,6 +619,7 @@ describe('Schedules - Edit', () => {
let schedule: Schedule;
let project: Project;
let organization: Organization;
let jobTemplate: JobTemplate;

beforeEach(() => {
const name = 'E2E Edit Schedule ' + randomString(4);
Expand All @@ -626,6 +645,7 @@ describe('Schedules - Edit', () => {
cy.deleteAWXSchedule(schedule, { failOnStatusCode: false });
cy.deleteAwxProject(project, { failOnStatusCode: false });
cy.deleteAwxOrganization(organization, { failOnStatusCode: false });
jobTemplate?.id && cy.deleteAwxJobTemplate(jobTemplate, { failOnStatusCode: false });
});

it('can edit a simple schedule from details page', () => {
Expand Down Expand Up @@ -884,4 +904,47 @@ describe('Schedules - Edit', () => {
});
cy.get('input[aria-label="Click to disable schedule"]').should('exist');
});

it("can't edit a schedule with missing resources", () => {
const name = 'E2E Edit Schedule With Missing Resources' + randomString(4);

cy.createAwxInventory(organization).then((inv) => {
cy.createAwxJobTemplate({
name: 'E2E Job Template ' + randomString(4),
organization: organization.id,
project: project.id,
inventory: inv.id,
}).then((jt) => {
jobTemplate = jt;
cy.createAWXSchedule({
name,
unified_job_template: jt.id,
rrule: 'DTSTART:20240415T124133Z RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU',
}).then((sched: Schedule) => {
schedule = sched;
cy.deleteAwxInventory(inv);

cy.navigateTo('awx', 'templates');
cy.verifyPageTitle('Templates');
cy.filterTableByMultiSelect('name', [jt.name]);
cy.get('[data-cy="name-column-cell"]').within(() => {
cy.get('a').click();
});
cy.verifyPageTitle(jt.name);
cy.clickTab('Schedules', true);
cy.get('[data-cy="create-schedule"]').should('have.attr', 'aria-disabled', 'true');
cy.get('[data-cy="name-column-cell"]').within(() => {
cy.get('a').click();
});
cy.clickLink('Edit schedule');
cy.verifyPageTitle('Edit Schedule');
cy.clickButton('Next');
cy.clickButton('Next');
cy.clickButton('Next');
cy.clickButton('Finish');
cy.contains('Job Template inventory is missing or undefined.');
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function TemplatePage() {
}}
tabs={tabs}
params={{ id: template?.id }}
componentParams={{ template }}
/>
</PageLayout>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export function WorkflowJobTemplatePage() {
}}
tabs={tabs}
params={{ id: template.id }}
componentParams={{ template }}
/>
</PageLayout>
);
Expand Down
37 changes: 29 additions & 8 deletions frontend/awx/views/schedules/SchedulesList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CubesIcon, PlusCircleIcon } from '@patternfly/react-icons';
import { CubesIcon, ExclamationTriangleIcon, PlusCircleIcon } from '@patternfly/react-icons';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useLocation, useNavigate, useOutletContext, useParams } from 'react-router-dom';
import { PageTable } from '../../../../framework';
import { usePersistentFilters } from '../../../common/PersistentFilters';
import { useOptions } from '../../../common/crud/useOptions';
Expand All @@ -13,6 +13,8 @@ import { useSchedulesActions } from './hooks/useSchedulesActions';
import { useSchedulesColumns } from './hooks/useSchedulesColumns';
import { useSchedulesFilter } from './hooks/useSchedulesFilter';
import { useScheduleToolbarActions } from './hooks/useSchedulesToolbarActions';
import { missingResources } from '../../resources/templates/hooks/useTemplateColumns';
import { JobTemplate } from '../../interfaces/JobTemplate';

export function SchedulesList(props: { sublistEndpoint?: string; url?: string }) {
const { t } = useTranslation();
Expand All @@ -21,6 +23,11 @@ export function SchedulesList(props: { sublistEndpoint?: string; url?: string })
const params = useParams<{ inventory_type?: string; id?: string; source_id?: string }>();
const resourceId = params.source_id ?? params.id;

const compParams = useOutletContext<{ template: JobTemplate }>();
const isMissingResource: boolean = compParams?.template
? missingResources(compParams?.template)
: false;

const apiEndPoint: string | undefined = props.sublistEndpoint
? `${props.sublistEndpoint}/${resourceId}/schedules/`
: undefined;
Expand All @@ -43,7 +50,11 @@ export function SchedulesList(props: { sublistEndpoint?: string; url?: string })
const { data } = useOptions<OptionsResponse<ActionsResponse>>(apiEndPoint ?? awxAPI`/schedules/`);
const canCreateSchedule = Boolean(data && data.actions && data.actions['POST']);
const createUrl = useGetSchedulCreateUrl(apiEndPoint);
const toolbarActions = useScheduleToolbarActions(view.unselectItemsAndRefresh, apiEndPoint);
const toolbarActions = useScheduleToolbarActions(
view.unselectItemsAndRefresh,
apiEndPoint,
isMissingResource
);
const rowActions = useSchedulesActions({
onScheduleDeleteCompleted: view.unselectItemsAndRefresh,
onScheduleToggleCompleted: view.updateItem,
Expand All @@ -59,20 +70,30 @@ export function SchedulesList(props: { sublistEndpoint?: string; url?: string })
errorStateTitle={t('Error loading schedules')}
emptyStateTitle={
canCreateSchedule
? t('No schedules yet')
? isMissingResource
? t('Resources are missing from this template.')
: t('No schedules yet')
: t('You do not have permission to create a schedule')
}
emptyStateDescription={
canCreateSchedule
? t('Please create a schedule by using the button below.')
? isMissingResource
? undefined
: t('Please create a schedule by using the button below.')
: t(
'Please contact your organization administrator if there is an issue with your access.'
)
}
emptyStateIcon={canCreateSchedule ? undefined : CubesIcon}
emptyStateNoDataIcon={
canCreateSchedule ? (isMissingResource ? ExclamationTriangleIcon : undefined) : CubesIcon
}
emptyStateButtonIcon={<PlusCircleIcon />}
emptyStateButtonText={canCreateSchedule ? t('Create schedule') : undefined}
emptyStateButtonClick={canCreateSchedule ? () => navigate(createUrl) : undefined}
emptyStateButtonText={
canCreateSchedule && !isMissingResource ? t('Create schedule') : undefined
}
emptyStateButtonClick={
canCreateSchedule && !isMissingResource ? () => navigate(createUrl) : undefined
}
{...view}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { cannotDeleteResources } from '../../../../common/utils/RBAChelpers';

export function useScheduleToolbarActions(
onComplete: (schedules: Schedule[]) => void,
sublistEndPoint = awxAPI`/schedules/`
sublistEndPoint = awxAPI`/schedules/`,
isMissingResource?: boolean
) {
const createUrl = useGetSchedulCreateUrl(sublistEndPoint);

Expand All @@ -33,7 +34,9 @@ export function useScheduleToolbarActions(
icon: PlusCircleIcon,
label: t('Create schedule'),
isDisabled: canCreateSchedule
? undefined
? isMissingResource
? t('Resources are missing from this template.')
: undefined
: t(
'You do not have permission to create a schedule. Please contact your organization administrator if there is an issue with your access.'
),
Expand All @@ -52,7 +55,7 @@ export function useScheduleToolbarActions(
isDanger: true,
},
],
[canCreateSchedule, createUrl, deleteSchedules, t]
[canCreateSchedule, createUrl, deleteSchedules, t, isMissingResource]
);

return ScheduleToolbarActions;
Expand Down
26 changes: 18 additions & 8 deletions frontend/awx/views/schedules/wizard/ScheduleAddWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,24 @@ export function ScheduleAddWizard() {
...rest,
};

const {
schedule,
}: {
schedule: Schedule;
} = await processSchedules(data);
const pageUrl = getScheduleUrl('details', schedule) as schedulePageUrl;
pageNavigate(pageUrl.pageId, { params: pageUrl.params });
try {
const {
schedule,
}: {
schedule: Schedule;
} = await processSchedules(data);
const pageUrl = getScheduleUrl('details', schedule) as schedulePageUrl;
pageNavigate(pageUrl.pageId, { params: pageUrl.params });
} catch (error) {
const { fieldErrors } = awxErrorAdapter(error);
const missingResource = fieldErrors.find((err) => err?.name === 'resources_needed_to_start');
if (missingResource) {
const errors = {
__all__: [missingResource.message],
};
throw new RequestError('', '', 400, '', errors);
}
}
};

const onCancel = () => navigate(location.pathname.replace('create', ''));
Expand Down Expand Up @@ -139,7 +150,6 @@ export function ScheduleAddWizard() {
}

const ruleset = getRuleSet(wizardData.rules, wizardData.exceptions ?? []);

const { utc, local } = await postRequest<{ utc: string[]; local: string[] }>(
awxAPI`/schedules/preview/`,
{
Expand Down
25 changes: 18 additions & 7 deletions frontend/awx/views/schedules/wizard/ScheduleEditWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,24 @@ export function ScheduleEditWizard() {
...rest,
};

const {
schedule,
}: {
schedule: Schedule;
} = await processSchedules(data);
const pageUrl = getScheduleUrl('details', schedule) as schedulePageUrl;
return pageNavigate(pageUrl.pageId, { params: pageUrl.params });
try {
const {
schedule,
}: {
schedule: Schedule;
} = await processSchedules(data);
const pageUrl = getScheduleUrl('details', schedule) as schedulePageUrl;
return pageNavigate(pageUrl.pageId, { params: pageUrl.params });
} catch (error) {
const { fieldErrors } = awxErrorAdapter(error);
const missingResource = fieldErrors.find((err) => err?.name === 'resources_needed_to_start');
if (missingResource) {
const errors = {
__all__: [missingResource.message],
};
throw new RequestError('', '', 400, '', errors);
}
}
};

const onCancel = () => navigate(-1);
Expand Down
Loading