Skip to content
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
49 changes: 44 additions & 5 deletions cypress/e2e/AutomationCalculator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,13 @@ describe('Automation Calculator page', () => {
return;
}

// The default is staged locally and only persisted when "Apply default
// to all templates" is confirmed, so typing here must not hit the
// network — no waitToLoad().
cy.get('#default-manual-effort').clear();
waitToLoad();
cy.get('#default-manual-effort').should('have.value', '0');

cy.get('#default-manual-effort').type('60');
waitToLoad();
cy.get('#default-manual-effort')
.invoke('val')
.then((val) => {
Expand Down Expand Up @@ -431,7 +432,7 @@ describe('Automation Calculator page', () => {
waitForStubbedLoad();

cy.getByCy('apply_default_modal').should('not.exist');
cy.contains('Default manual time applied to 3 templates.').should(
cy.contains('Default manual time applied to all templates.').should(
'exist',
);

Expand All @@ -445,6 +446,42 @@ describe('Automation Calculator page', () => {
});
});

it('does not update savings when editing the default before applying', () => {
// The default manual time is staged locally, like a per-template manual
// time edit — neither savings figure should move until "Apply default to
// all templates" is confirmed.
visitWithStubbedTemplates();

cy.intercept('POST', '/api/tower-analytics/v1/roi_cost_effort_data/').as(
'roiCostEffortSave',
);
cy.intercept('POST', '**/roi_templates_apply_default/').as('applyDefault');

cy.getByCy('total_savings')
.find('h3')
.invoke('text')
.then((originalTotalSavings) => {
cy.getByCy('current_page_savings')
.find('h3')
.invoke('text')
.then((originalCurrentPageSavings) => {
cy.get('#default-manual-effort').clear();
cy.get('#default-manual-effort').type('45');

cy.getByCy('total_savings')
.find('h3')
.invoke('text')
.should('eq', originalTotalSavings);
cy.getByCy('current_page_savings')
.find('h3')
.invoke('text')
.should('eq', originalCurrentPageSavings);
cy.get('@roiCostEffortSave.all').should('have.length', 0);
cy.get('@applyDefault.all').should('have.length', 0);
});
});
});

it('cancels apply default without calling the endpoint', () => {
cy.intercept('POST', '**/roi_templates_apply_default/', {
updated_count: 3,
Expand Down Expand Up @@ -514,7 +551,7 @@ describe('Automation Calculator page', () => {
cy.getByCy('apply_default_button').should('not.be.disabled');
});

it('falls back to 0 when the apply response has no updated_count', () => {
it('shows success message when the apply response has no body', () => {
cy.intercept('POST', '**/roi_templates_apply_default/', {}).as(
'applyDefault',
);
Expand All @@ -526,7 +563,9 @@ describe('Automation Calculator page', () => {
cy.wait('@applyDefault');
waitForStubbedLoad();

cy.contains('Default manual time applied to 0 templates.').should('exist');
cy.contains('Default manual time applied to all templates.').should(
'exist',
);
});

it('disables cancel/close while a request is in flight', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ const AutomationCalculator: FC<AutmationCalculatorProps> = ({
res.successful_hosts_saved_hours_current_page;
api.result.successful_hosts_saved_hours_other_pages =
res.successful_hosts_saved_hours_other_pages;
api.result.cost = res.cost;
setValue(
mapApi(
res.meta as any,
Expand All @@ -215,16 +216,21 @@ const AutomationCalculator: FC<AutmationCalculatorProps> = ({

const updateCalculationValues = async (varName: string, value: number) => {
if (isNaN(value)) return;
if (varName === 'default_manual_effort_minutes') {
// Staged only: the default is committed via the explicit "Apply
// default to all templates" action (applyDefaultToAll), not on every
// keystroke. Mirrors per-template manual time, which also only
// commits on its own action (blur).
setDefaultManualEffort(value);
return;
}
const hourly_automation_cost =
varName === 'automation_cost' ? value : costAutomation;
const hourly_manual_labor_cost =
varName === 'manual_cost' ? value : costManual;
const manual_effort =
varName === 'default_manual_effort_minutes' ? value : defaultManualEffort;
const humanVarNames: Record<string, string> = {
automation_cost: 'Automation cost',
manual_cost: 'Manual cost',
default_manual_effort_minutes: 'Default manual time',
};
const humanVarName = humanVarNames[varName] ?? varName;
try {
Expand All @@ -233,7 +239,7 @@ const AutomationCalculator: FC<AutmationCalculatorProps> = ({
api.result.items,
hourly_manual_labor_cost,
hourly_automation_cost,
manual_effort,
defaultManualEffort,
) as any,
);
} catch {
Expand All @@ -250,8 +256,6 @@ const AutomationCalculator: FC<AutmationCalculatorProps> = ({
setCostManual(value);
} else if (varName === 'automation_cost') {
setCostAutomation(value);
} else if (varName === 'default_manual_effort_minutes') {
setDefaultManualEffort(value);
}
};

Expand Down Expand Up @@ -312,13 +316,14 @@ const AutomationCalculator: FC<AutmationCalculatorProps> = ({
};

const applyDefaultToAll = async () => {
let count = 0;
try {
// The default is staged locally (see updateCalculationValues) and only
// committed here, so persist it before the apply endpoint reads it —
// that endpoint POSTs no body and relies on the server-persisted
// default_manual_effort_minutes.
await saveROI(getROISaveData(api.result.items) as any);
// backend applies to all templates tenant-wide; no params needed
const res = (await applyDefaultROITemplates()) as {
updated_count?: number;
};
count = typeof res.updated_count === 'number' ? res.updated_count : 0;
await applyDefaultROITemplates();
} catch {
addNotification({
title: 'Unable to apply default manual time',
Expand All @@ -331,9 +336,10 @@ const AutomationCalculator: FC<AutmationCalculatorProps> = ({
}

addNotification({
title: `Default manual time applied to ${count} template${
count === 1 ? '' : 's'
}.`,
// updated_count from the backend only reflects templates with a prior
// manual-effort row, not the true total the default was applied to —
// so the message stays count-free.
title: 'Default manual time applied to all templates.',
variant: 'success',
dismissable: true,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ const WInputGroup = styled(InputGroup)`
width: 170px;
`;

const Divider = styled.hr`
margin: 16px 0 12px 0;
border: none;
border-top: 1px solid var(--pf-t--global--border--color--200);
`;

const validFloat = (value: number): number =>
+value && +value < 0 ? NaN : value;

Expand Down Expand Up @@ -97,6 +103,7 @@ const CalculationCost: FunctionComponent<Props> = ({
/>
<InputGroupText>/hr</InputGroupText>
</WInputGroup>
<Divider />
<p style={{ paddingTop: '10px' }}>
Default manual time per template (minutes)
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ const Row: FunctionComponent<Props> = ({
<InputGroup>
<InputGroupItem isFill>
<TextInput
key={template.avgRunTime}
autoFocus={
window.localStorage.getItem('focused') ===
'manual-time-' + template.id.toString()
Expand Down
Loading