Skip to content

Commit

Permalink
[dagit] Add bulk re-execution of all steps (#7591)
Browse files Browse the repository at this point in the history
## Summary

Add the `ALL_STEPS` re-execution policy to `ReexecutionDialog`, and a corresponding menu item on the Runs page.

## Test Plan

View Runs page. Select some failed runs and some succeeded runs. View menu, verify that the numbers match what I've selected.

Open one dialog, verify that it renders the correct text and behaves properly, kicking off run requests and displaying the progress bar.

Open the other, verify same.
  • Loading branch information
hellendag committed Apr 26, 2022
1 parent 4516740 commit fc4358e
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 159 deletions.
104 changes: 29 additions & 75 deletions js_modules/dagit/packages/core/src/runs/ReexecutionDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,54 @@
import {gql, useQuery} from '@apollo/client';
import {render, screen, waitFor} from '@testing-library/react';
import {render, screen, waitFor, within} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';

import {TestProvider} from '../testing/TestProvider';
import {ReexecutionPolicy} from '../types/globalTypes';

import {ReexecutionDialog} from './ReexecutionDialog';
import {RUN_TABLE_RUN_FRAGMENT} from './RunTable';
import {ReexecutionDialogTestQuery} from './types/ReexecutionDialogTestQuery';

describe('ReexecutionDialog', () => {
const defaultMocks = {
RepositoryOrigin: () => ({
repositoryName: () => 'foo',
repositoryLocationName: () => 'bar',
}),
Workspace: () => ({
locationEntries: () => [...new Array(1)],
}),
Repository: () => ({
id: () => 'foo',
name: () => 'foo',
pipelines: () => [{id: 'my_pipeline', name: 'my_pipeline'}],
}),
RepositoryLocation: () => ({
id: () => 'bar',
name: () => 'bar',
repositories: () => [...new Array(1)],
}),
Run: () => ({
pipelineName: () => 'my_pipeline',
}),
Runs: () => ({
results: () => [...new Array(3)],
}),
const selectedMap = {
'abcd-1234': 'abcd-1234',
'efgh-5678': 'efgh-5678',
'ijkl-9012': 'ijkl-9012',
};

const Test = () => {
const {data} = useQuery<ReexecutionDialogTestQuery>(REEXECUTION_DIALOG_TEST_QUERY);
const runs = data?.pipelineRunsOrError;

if (!runs || runs.__typename !== 'Runs' || !runs.results?.length) {
return null;
}

const selectedMap = runs.results.reduce((accum, run) => ({...accum, [run.id]: run}), {});

return (
<ReexecutionDialog
isOpen
onClose={jest.fn()}
onComplete={jest.fn()}
selectedRuns={selectedMap}
/>
);
};
const Test = (props: {policy: ReexecutionPolicy}) => (
<ReexecutionDialog
isOpen
onClose={jest.fn()}
onComplete={jest.fn()}
selectedRuns={selectedMap}
reexecutionPolicy={props.policy}
/>
);

it('prompts the user with the number of runs to re-execute', async () => {
render(
<TestProvider apolloProps={{mocks: [defaultMocks]}}>
<Test />
<TestProvider>
<Test policy={ReexecutionPolicy.FROM_FAILURE} />
</TestProvider>,
);

await waitFor(() => {
expect(
screen.getByText(/3 runs will be re\-executed from failure\. do you wish to continue\?/i),
).toBeVisible();
const message = screen.getByText(/3 runs will be re\-executed \. do you wish to continue\?/i);
expect(message).toBeVisible();
expect(within(message).getByText(/from failure/i)).toBeVisible();
});
});

it('moves into loading state upon re-execution', async () => {
render(
<TestProvider apolloProps={{mocks: [defaultMocks]}}>
<Test />
<TestProvider>
<Test policy={ReexecutionPolicy.FROM_FAILURE} />
</TestProvider>,
);

await waitFor(() => {
expect(
screen.getByText(/3 runs will be re\-executed\ from failure. do you wish to continue\?/i),
).toBeVisible();
const message = screen.getByText(/3 runs will be re\-executed \. do you wish to continue\?/i);
expect(message).toBeVisible();
expect(within(message).getByText(/from failure/i)).toBeVisible();
});

const button = screen.getByText(/re\-execute 3 runs/i);
Expand All @@ -102,8 +70,8 @@ describe('ReexecutionDialog', () => {
};

render(
<TestProvider apolloProps={{mocks: [defaultMocks, mocks]}}>
<Test />
<TestProvider apolloProps={{mocks: [mocks]}}>
<Test policy={ReexecutionPolicy.FROM_FAILURE} />
</TestProvider>,
);

Expand All @@ -125,8 +93,8 @@ describe('ReexecutionDialog', () => {
};

render(
<TestProvider apolloProps={{mocks: [defaultMocks, mocks]}}>
<Test />
<TestProvider apolloProps={{mocks: [mocks]}}>
<Test policy={ReexecutionPolicy.FROM_FAILURE} />
</TestProvider>,
);

Expand All @@ -140,17 +108,3 @@ describe('ReexecutionDialog', () => {
});
});
});

const REEXECUTION_DIALOG_TEST_QUERY = gql`
query ReexecutionDialogTestQuery {
pipelineRunsOrError(limit: 3) {
... on Runs {
results {
id
...RunTableRunFragment
}
}
}
}
${RUN_TABLE_RUN_FRAGMENT}
`;
41 changes: 28 additions & 13 deletions js_modules/dagit/packages/core/src/runs/ReexecutionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import {
LaunchPipelineReexecution_launchPipelineReexecution_PythonError,
LaunchPipelineReexecution_launchPipelineReexecution_RunConfigValidationInvalid,
} from './types/LaunchPipelineReexecution';
import {RunTableRunFragment} from './types/RunTableRunFragment';

export interface Props {
isOpen: boolean;
onClose: () => void;
onComplete: (reexecutionState: ReexecutionState) => void;
selectedRuns: {[id: string]: RunTableRunFragment};
selectedRuns: {[id: string]: string};
reexecutionPolicy: ReexecutionPolicy;
}

type Error =
Expand Down Expand Up @@ -70,7 +70,7 @@ type ReexecutionDialogState = {
reexecution: ReexecutionState;
};

type SelectedRuns = {[id: string]: RunTableRunFragment};
type SelectedRuns = {[id: string]: string};

const initializeState = (selectedRuns: SelectedRuns): ReexecutionDialogState => {
return {
Expand Down Expand Up @@ -122,7 +122,7 @@ const reexecutionDialogReducer = (
};

export const ReexecutionDialog = (props: Props) => {
const {isOpen, onClose, onComplete, selectedRuns} = props;
const {isOpen, onClose, onComplete, reexecutionPolicy, selectedRuns} = props;

// Freeze the selected IDs, since the list may change as runs continue processing and
// re-executing. We want to preserve the list we're given.
Expand Down Expand Up @@ -165,9 +165,7 @@ export const ReexecutionDialog = (props: Props) => {
variables: {
reexecutionParams: {
parentRunId: runId,
// Temporary! Add a prop to support this and `ALL_OPS`, based on the
// user's choice.
policy: ReexecutionPolicy.FROM_FAILURE,
policy: reexecutionPolicy,
},
},
});
Expand Down Expand Up @@ -195,13 +193,26 @@ export const ReexecutionDialog = (props: Props) => {
);
}

const message = () => {
if (reexecutionPolicy === ReexecutionPolicy.ALL_STEPS) {
return (
<span>
{`${count} ${count === 1 ? 'run' : 'runs'} will be re-executed `}
<strong>with all steps</strong>. Do you wish to continue?
</span>
);
}
return (
<span>
{`${count} ${count === 1 ? 'run' : 'runs'} will be re-executed `}
<strong>from failure</strong>. Do you wish to continue?
</span>
);
};

return (
<Group direction="column" spacing={16}>
<div>
{`${count} ${
count === 1 ? 'run' : 'runs'
} will be re-executed from failure. Do you wish to continue?`}
</div>
<div>{message()}</div>
</Group>
);
case 'reexecuting':
Expand Down Expand Up @@ -312,7 +323,11 @@ export const ReexecutionDialog = (props: Props) => {
return (
<Dialog
isOpen={isOpen}
title="Re-execute runs"
title={
reexecutionPolicy === ReexecutionPolicy.ALL_STEPS
? 'Re-execute runs'
: 'Re-execute runs from failure'
}
canEscapeKeyClose={canQuicklyClose}
canOutsideClickClose={canQuicklyClose}
onClose={onClose}
Expand Down
53 changes: 40 additions & 13 deletions js_modules/dagit/packages/core/src/runs/RunActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {AppContext} from '../app/AppContext';
import {SharedToaster} from '../app/DomUtils';
import {usePermissions} from '../app/Permissions';
import {useCopyToClipboard} from '../app/browser';
import {ReexecutionPolicy} from '../types/globalTypes';
import {DagitReadOnlyCodeMirror} from '../ui/DagitCodeMirror';
import {MenuLink} from '../ui/MenuLink';
import {isThisThingAJob} from '../workspace/WorkspaceContext';
Expand Down Expand Up @@ -247,7 +248,7 @@ export const RunBulkActionsMenu: React.FC<{
canLaunchPipelineReexecution,
} = usePermissions();
const [visibleDialog, setVisibleDialog] = React.useState<
'none' | 'terminate' | 'delete' | 'reexecute'
'none' | 'terminate' | 'delete' | 'reexecute-from-failure' | 'reexecute'
>('none');

if (!canTerminatePipelineExecution && !canDeletePipelineRun) {
Expand All @@ -265,7 +266,13 @@ export const RunBulkActionsMenu: React.FC<{
const deletionMap = selected.reduce((accum, run) => ({...accum, [run.id]: run.canTerminate}), {});

const failedRuns = selected.filter((r) => failedStatuses.has(r?.status));
const reexecutionMap = failedRuns.reduce((accum, run) => ({...accum, [run.id]: run}), {});
const failedMap = failedRuns.reduce((accum, run) => ({...accum, [run.id]: run.id}), {});

const reexecutableRuns = selected.filter((r) => doneStatuses.has(r?.status));
const reexecutableMap = reexecutableRuns.reduce(
(accum, run) => ({...accum, [run.id]: run.id}),
{},
);

const closeDialogs = () => {
setVisibleDialog('none');
Expand Down Expand Up @@ -305,16 +312,28 @@ export const RunBulkActionsMenu: React.FC<{
/>
) : null}
{canLaunchPipelineReexecution ? (
<MenuItem
icon="refresh"
text={`Re-execute ${failedRuns.length} ${
failedRuns.length === 1 ? 'run' : 'runs'
} from failure`}
disabled={failedRuns.length === 0}
onClick={() => {
setVisibleDialog('reexecute');
}}
/>
<>
<MenuItem
icon="refresh"
text={`Re-execute ${reexecutableRuns.length} ${
reexecutableRuns.length === 1 ? 'run' : 'runs'
}`}
disabled={reexecutableRuns.length === 0}
onClick={() => {
setVisibleDialog('reexecute');
}}
/>
<MenuItem
icon="refresh"
text={`Re-execute ${failedRuns.length} ${
failedRuns.length === 1 ? 'run' : 'runs'
} from failure`}
disabled={failedRuns.length === 0}
onClick={() => {
setVisibleDialog('reexecute-from-failure');
}}
/>
</>
) : null}
</Menu>
}
Expand All @@ -337,11 +356,19 @@ export const RunBulkActionsMenu: React.FC<{
onTerminateInstead={() => setVisibleDialog('terminate')}
selectedRuns={deletionMap}
/>
<ReexecutionDialog
isOpen={visibleDialog === 'reexecute-from-failure'}
onClose={closeDialogs}
onComplete={onComplete}
selectedRuns={failedMap}
reexecutionPolicy={ReexecutionPolicy.FROM_FAILURE}
/>
<ReexecutionDialog
isOpen={visibleDialog === 'reexecute'}
onClose={closeDialogs}
onComplete={onComplete}
selectedRuns={reexecutionMap}
selectedRuns={reexecutableMap}
reexecutionPolicy={ReexecutionPolicy.ALL_STEPS}
/>
</>
);
Expand Down

This file was deleted.

0 comments on commit fc4358e

Please sign in to comment.