Skip to content

Commit

Permalink
Scheduling Subscription and Canceling (#1141)
Browse files Browse the repository at this point in the history
* subscribe to scheduling spec and scheduling requests tables
* compute scheduling status from a variety of inputs
* refactor scheduling effect to be mainly subscription based
* cancel current scheduling run from navbar
  • Loading branch information
AaronPlave authored Mar 5, 2024
1 parent 660164e commit 0c73a73
Show file tree
Hide file tree
Showing 14 changed files with 339 additions and 118 deletions.
5 changes: 2 additions & 3 deletions e2e-tests/fixtures/Constraints.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { adjectives, animals, colors, uniqueNamesGenerator } from 'unique-names-generator';
import { fillEditorText } from '../utilities/editor.js';
import { Models } from './Models.js';

export class Constraints {
Expand Down Expand Up @@ -59,9 +60,7 @@ export class Constraints {
}

async fillConstraintDefinition() {
await this.inputConstraintDefinition.focus();
await this.inputConstraintDefinition.fill(this.constraintDefinition);
await this.inputConstraintDefinition.evaluate(e => e.blur());
await fillEditorText(this.inputConstraintDefinition, this.constraintDefinition);
}

async fillConstraintDescription() {
Expand Down
12 changes: 2 additions & 10 deletions e2e-tests/fixtures/ExpansionRules.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, type Locator, type Page } from '@playwright/test';
import os from 'node:os';
import { adjectives, animals, colors, uniqueNamesGenerator } from 'unique-names-generator';
import { fillEditorText } from '../utilities/editor.js';
import { getOptionValueFromText } from '../utilities/selectors.js';
import { Dictionaries } from './Dictionaries.js';
import { Models } from './Models.js';
Expand Down Expand Up @@ -75,15 +75,7 @@ export class ExpansionRules {
}

async fillInputEditor() {
await this.inputEditor.focus();
// Need to emulate actual clearing of editor instead of manipulating the
// underlying textbox, see: https://github.com/microsoft/playwright/issues/14126#issuecomment-1728327258
const isMac = os.platform() === 'darwin';
const modifier = isMac ? 'Meta' : 'Control';
await this.inputEditor.press(`${modifier}+A`);
await this.inputEditor.press(`Backspace`);
await this.inputEditor.fill(this.ruleLogic);
await this.inputEditor.evaluate(e => e.blur());
await fillEditorText(this.inputEditor, this.ruleLogic);
}

async fillInputName() {
Expand Down
38 changes: 21 additions & 17 deletions e2e-tests/fixtures/Plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,17 @@ export class Plan {
}

async deleteAllActivities() {
await this.panelActivityDirectivesTable.getByRole('gridcell').first().click({ button: 'right' });
await this.page.locator('.context-menu > .context-menu-item:has-text("Select All Activity Directives")').click();
await this.panelActivityDirectivesTable.getByRole('gridcell').first().click({ button: 'right' });
await this.page.getByText(/Delete \d+ Activit(y|ies) Directives?/).click();

const applyPresetButton = this.page.getByRole('button', { name: 'Confirm' });
await applyPresetButton.waitFor({ state: 'attached', timeout: 1000 });
await applyPresetButton.click();
const gridCells = await this.panelActivityDirectivesTable.getByRole('gridcell');
if ((await gridCells.count()) > 0) {
await this.panelActivityDirectivesTable.getByRole('gridcell').first().click({ button: 'right' });
await this.page.locator('.context-menu > .context-menu-item:has-text("Select All Activity Directives")').click();
await this.panelActivityDirectivesTable.getByRole('gridcell').first().click({ button: 'right' });
await this.page.getByText(/Delete \d+ Activit(y|ies) Directives?/).click();

const confirmDeletionButton = this.page.getByRole('button', { name: 'Confirm' });
await confirmDeletionButton.waitFor({ state: 'attached', timeout: 1000 });
await confirmDeletionButton.click();
}
}

async fillActivityPresetName(presetName: string) {
Expand Down Expand Up @@ -164,18 +167,14 @@ export class Plan {

async runAnalysis() {
await this.analyzeButton.click();
await this.page.waitForSelector(this.schedulingStatusSelector('Incomplete'), { state: 'attached', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector('Incomplete'), { state: 'visible', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector('Complete'), { state: 'attached', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector('Complete'), { state: 'visible', strict: true });
await this.waitForSchedulingStatus('Incomplete');
await this.waitForSchedulingStatus('Complete');
}

async runScheduling() {
async runScheduling(expectedFinalState = 'Complete') {
await this.scheduleButton.click();
await this.page.waitForSelector(this.schedulingStatusSelector('Incomplete'), { state: 'attached', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector('Incomplete'), { state: 'visible', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector('Complete'), { state: 'attached', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector('Complete'), { state: 'visible', strict: true });
await this.waitForSchedulingStatus('Incomplete');
await this.waitForSchedulingStatus(expectedFinalState);
}

async selectActivityAnchorByIndex(index: number) {
Expand Down Expand Up @@ -343,4 +342,9 @@ export class Plan {
this.schedulingConditionNewButton = page.locator(`button[name="new-scheduling-condition"]`);
this.schedulingSatisfiedActivity = page.locator('.scheduling-goal-analysis-activities-list > .satisfied-activity');
}

async waitForSchedulingStatus(status: string) {
await this.page.waitForSelector(this.schedulingStatusSelector(status), { state: 'attached', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector(status), { state: 'visible', strict: true });
}
}
5 changes: 2 additions & 3 deletions e2e-tests/fixtures/SchedulingConditions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { adjectives, animals, colors, uniqueNamesGenerator } from 'unique-names-generator';
import { fillEditorText } from '../utilities/editor.js';
import { getOptionValueFromText } from '../utilities/selectors.js';
import { Models } from './Models.js';

Expand Down Expand Up @@ -64,9 +65,7 @@ export class SchedulingConditions {
}

async fillConditionDefinition() {
await this.inputConditionDefinition.focus();
await this.inputConditionDefinition.fill(this.conditionDefinition);
await this.inputConditionDefinition.evaluate(e => e.blur());
await fillEditorText(this.inputConditionDefinition, this.conditionDefinition);
}

async fillConditionDescription() {
Expand Down
5 changes: 2 additions & 3 deletions e2e-tests/fixtures/SchedulingGoals.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { fillEditorText } from '../utilities/editor.js';
import { getOptionValueFromText } from '../utilities/selectors.js';
import { Models } from './Models.js';

Expand Down Expand Up @@ -61,9 +62,7 @@ export class SchedulingGoals {
}

async fillGoalDefinition() {
await this.inputGoalDefinition.focus();
await this.inputGoalDefinition.fill(this.goalDefinition);
await this.inputGoalDefinition.evaluate(e => e.blur());
await fillEditorText(this.inputGoalDefinition, this.goalDefinition);
}

async fillGoalDescription() {
Expand Down
12 changes: 10 additions & 2 deletions e2e-tests/tests/scheduling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ test.beforeAll(async ({ browser }) => {
});

test.afterAll(async () => {
await plan.deleteAllActivities();
await plans.goto();
await plans.deletePlan();
await models.goto();
Expand Down Expand Up @@ -65,14 +66,14 @@ test.describe.serial('Scheduling', () => {
await expect(plan.schedulingGoalEnabledCheckboxSelector(goalName1)).toBeChecked();
await plan.schedulingGoalEnabledCheckboxSelector(goalName1).uncheck();
await expect(plan.schedulingGoalEnabledCheckboxSelector(goalName1)).not.toBeChecked();
await plan.runScheduling();
await plan.runScheduling('Failed');
await expect(plan.schedulingGoalDifferenceBadge).not.toBeVisible();
await plan.schedulingGoalEnabledCheckboxSelector(goalName1).check();
await expect(plan.schedulingGoalEnabledCheckboxSelector(goalName1)).toBeChecked();
});

test('The condition should prevent showing +10 in the goals badge', async () => {
await plan.runScheduling();
await plan.runScheduling('Failed');
await expect(plan.schedulingGoalDifferenceBadge).toHaveText('+0');
});

Expand Down Expand Up @@ -108,6 +109,13 @@ test.describe.serial('Scheduling', () => {
await expect(plan.schedulingGoalDifferenceBadge).toHaveText('+0');
});

test('Modifying the plan should result in scheduling status marked as out of date', async () => {
await plan.showPanel('Activity Types');
await plan.panelActivityTypes.getByRole('button', { name: 'CreateActivity-GrowBanana' }).click();
await plan.showPanel('Scheduling Goals');
await plan.waitForSchedulingStatus('Modified');
});

test('Delete scheduling goal', async () => {
await schedulingGoals.deleteSchedulingGoal(goalName1);
});
Expand Down
14 changes: 14 additions & 0 deletions e2e-tests/utilities/editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Locator } from '@playwright/test';
import os from 'node:os';

export async function fillEditorText(editor: Locator, text: string) {
await editor.focus();
// Need to emulate actual clearing of editor instead of manipulating the
// underlying textbox, see: https://github.com/microsoft/playwright/issues/14126#issuecomment-1728327258
const isMac = os.platform() === 'darwin';
const modifier = isMac ? 'Meta' : 'Control';
await editor.press(`${modifier}+A`);
await editor.press(`Backspace`);
await editor.fill(text);
await editor.evaluate(e => e.blur());
}
9 changes: 7 additions & 2 deletions src/components/scheduling/SchedulingGoalsPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
import { afterUpdate, beforeUpdate } from 'svelte';
import { PlanStatusMessages } from '../../enums/planStatusMessages';
import { plan, planReadOnly } from '../../stores/plan';
import { enableScheduling, schedulingSpecGoals, schedulingStatus, selectedSpecId } from '../../stores/scheduling';
import {
enableScheduling,
schedulingAnalysisStatus,
schedulingSpecGoals,
selectedSpecId,
} from '../../stores/scheduling';
import type { User } from '../../types/app';
import type { SchedulingSpecGoal } from '../../types/scheduling';
import type { ViewGridSection } from '../../types/view';
Expand Down Expand Up @@ -65,7 +70,7 @@
<Panel>
<svelte:fragment slot="header">
<GridMenu {gridSection} title="Scheduling Goals" />
<PanelHeaderActions status={$schedulingStatus} indeterminate>
<PanelHeaderActions status={$schedulingAnalysisStatus} indeterminate>
<PanelHeaderActionButton
title="Analyze"
on:click={() => effects.schedule(true, $plan, user)}
Expand Down
52 changes: 36 additions & 16 deletions src/routes/plans/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,10 @@
import { planSnapshot, planSnapshotId } from '../../../stores/planSnapshots';
import {
enableScheduling,
latestAnalyses,
resetSchedulingStores,
latestSchedulingRequest,
satisfiedSchedulingGoalCount,
schedulingAnalysisStatus,
schedulingGoalCount,
schedulingStatus,
} from '../../../stores/scheduling';
import {
enableSimulation,
Expand Down Expand Up @@ -162,12 +161,12 @@
let invalidActivityCount: number = 0;
let planHasBeenLocked = false;
let planSnapshotActivityDirectives: ActivityDirective[] = [];
let schedulingAnalysisStatus: Status | null;
let simulationExtent: string | null;
let selectedSimulationStatus: Status | null;
let windowWidth = 0;
let simulationDataAbortController: AbortController;
let resourcesExternalAbortController: AbortController;
let schedulingStatusText: string = '';
$: ({ invalidActivityCount, ...activityErrorCounts } = $activityErrorRollups.reduce(
(prevCounts, activityErrorRollup) => {
Expand Down Expand Up @@ -366,13 +365,25 @@
}
$: compactNavMode = windowWidth < 1100;
$: schedulingAnalysisStatus = $schedulingStatus;
$: if ($latestAnalyses) {
if ($schedulingGoalCount !== $satisfiedSchedulingGoalCount) {
schedulingAnalysisStatus = Status.PartialSuccess;
$: if ($schedulingAnalysisStatus) {
let newSchedulingStatusText = '';
const satisfactionReport = `${$satisfiedSchedulingGoalCount} satisfied, ${
$schedulingGoalCount - $satisfiedSchedulingGoalCount
} unsatisfied`;
if ($schedulingAnalysisStatus === Status.Complete) {
newSchedulingStatusText = satisfactionReport;
} else if ($schedulingAnalysisStatus === Status.Failed) {
if ($latestSchedulingRequest && $latestSchedulingRequest.reason) {
newSchedulingStatusText = 'Failed to run scheduling';
} else {
newSchedulingStatusText = satisfactionReport;
}
} else if ($schedulingAnalysisStatus === Status.Modified) {
newSchedulingStatusText = 'Scheduling out-of-date';
}
schedulingStatusText = newSchedulingStatusText;
}
$: if ($simulationDatasetLatest) {
simulationExtent = getSimulationExtent($simulationDatasetLatest);
selectedSimulationStatus = getSimulationStatus($simulationDatasetLatest);
Expand All @@ -391,7 +402,6 @@
resetConstraintStores();
resetExpansionStores();
resetPlanStores();
resetSchedulingStores();
resetSimulationStores();
closeActiveModal();
});
Expand Down Expand Up @@ -703,16 +713,26 @@
permissionError={$planReadOnly
? PlanStatusMessages.READ_ONLY
: 'You do not have permission to run a scheduling analysis'}
status={schedulingAnalysisStatus}
statusText={schedulingAnalysisStatus === Status.PartialSuccess || schedulingAnalysisStatus === Status.Complete
? `${$satisfiedSchedulingGoalCount} satisfied, ${
$schedulingGoalCount - $satisfiedSchedulingGoalCount
} unsatisfied`
: ''}
status={$schedulingAnalysisStatus}
statusText={schedulingStatusText}
on:click={() => effects.schedule(true, $plan, data.user)}
indeterminate
>
<CalendarIcon />
<svelte:fragment slot="metadata">
<div class="st-typography-body">
{#if !$schedulingAnalysisStatus}
Scheduling analysis not run
{/if}
</div>
{#if $schedulingAnalysisStatus === Status.Pending || $schedulingAnalysisStatus === Status.Incomplete}
<button
on:click={() => effects.cancelSchedulingRequest($latestSchedulingRequest.analysis_id, data.user)}
class="st-button cancel-button"
disabled={$planReadOnly}>Cancel</button
>
{/if}
</svelte:fragment>
</PlanNavButton>
<ExtensionMenu
extensions={data.extensions}
Expand Down
Loading

0 comments on commit 0c73a73

Please sign in to comment.