Auto-refreshes the run details page (#722)

* Auto-refreshes the run details page

Auto-refresh is paused when the window loses focus (blur) and is resumed
upon re-focus. Autorefresh is permanently terminated if the run has
stopped due to failure, error, being skipped, or succeeding.

* Adds tests for Status.hasCompleted

* Clean up and PR comments
This commit is contained in:
Riley Bauer 2019-01-24 10:55:02 -08:00 committed by Kubernetes Prow Robot
parent 3b3a15e16a
commit 5beffef614
15 changed files with 305 additions and 77 deletions

View File

@ -346,7 +346,7 @@ const runs: ApiRunDetail[] = [
relationship: ApiRelationship.OWNER, relationship: ApiRelationship.OWNER,
}], }],
scheduled_at: new Date('2018-04-17T21:00:00.000Z'), scheduled_at: new Date('2018-04-17T21:00:00.000Z'),
status: 'Succeeded', status: 'Error',
}, },
}, },
{ {

View File

@ -56,9 +56,11 @@ describe('SideNav', () => {
localStorageIsCollapsedSpy.mockImplementation(() => false); localStorageIsCollapsedSpy.mockImplementation(() => false);
}); });
afterEach(() => { afterEach(async () => {
// unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
// depends on mocks/spies
await tree.unmount();
jest.resetAllMocks(); jest.resetAllMocks();
tree.unmount();
(window as any).innerWidth = wideWidth; (window as any).innerWidth = wideWidth;
}); });

View File

@ -22,8 +22,10 @@ import TestUtils from '../TestUtils';
describe('UploadPipelineDialog', () => { describe('UploadPipelineDialog', () => {
let tree: ReactWrapper | ShallowWrapper; let tree: ReactWrapper | ShallowWrapper;
afterEach(() => { afterEach(async () => {
tree.unmount(); // unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
// depends on mocks/spies
await tree.unmount();
}); });
it('renders closed', () => { it('renders closed', () => {

View File

@ -136,8 +136,10 @@ describe('Compare', () => {
getRunSpy.mockImplementation((id: string) => runs.find((r) => r.run!.id === id)); getRunSpy.mockImplementation((id: string) => runs.find((r) => r.run!.id === id));
}); });
afterEach(() => { afterEach(async () => {
tree.unmount(); // unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
// depends on mocks/spies
await tree.unmount();
}); });
it('clears banner upon initial load', () => { it('clears banner upon initial load', () => {

View File

@ -100,8 +100,10 @@ describe('ExperimentDetails', () => {
await mockNRuns(0); await mockNRuns(0);
}); });
afterEach(() => { afterEach(async () => {
tree.unmount(); // unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
// depends on mocks/spies
await tree.unmount();
}); });
it('renders a page with no runs or recurring runs', async () => { it('renders a page with no runs or recurring runs', async () => {

View File

@ -35,7 +35,7 @@ describe('ExperimentList', () => {
const historyPushSpy = jest.fn(); const historyPushSpy = jest.fn();
const listExperimentsSpy = jest.spyOn(Apis.experimentServiceApi, 'listExperiment'); const listExperimentsSpy = jest.spyOn(Apis.experimentServiceApi, 'listExperiment');
const listRunsSpy = jest.spyOn(Apis.runServiceApi, 'listRuns'); const listRunsSpy = jest.spyOn(Apis.runServiceApi, 'listRuns');
// We mock this because it uses toLocaleDateString, which causes mismatches between local and CI // We mock this because it uses toLocaleDateString, which causes mismatches between local and CI
// test enviroments // test enviroments
const formatDateStringSpy = jest.spyOn(Utils, 'formatDateString'); const formatDateStringSpy = jest.spyOn(Utils, 'formatDateString');

View File

@ -126,9 +126,11 @@ describe('NewRun', () => {
MOCK_RUN_WITH_EMBEDDED_PIPELINE = newMockRunWithEmbeddedPipeline(); MOCK_RUN_WITH_EMBEDDED_PIPELINE = newMockRunWithEmbeddedPipeline();
}); });
afterEach(() => { afterEach(async () => {
// unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
// depends on mocks/spies
await tree.unmount();
jest.resetAllMocks(); jest.resetAllMocks();
tree.unmount();
}); });
it('renders the new run page', async () => { it('renders the new run page', async () => {

View File

@ -97,9 +97,11 @@ describe('PipelineDetails', () => {
createGraphSpy.mockImplementation(() => new graphlib.Graph()); createGraphSpy.mockImplementation(() => new graphlib.Graph());
}); });
afterEach(() => { afterEach(async () => {
// unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
// depends on mocks/spies
await tree.unmount();
jest.resetAllMocks(); jest.resetAllMocks();
tree.unmount();
}); });
it('shows empty pipeline details with no graph', async () => { it('shows empty pipeline details with no graph', async () => {

View File

@ -66,9 +66,11 @@ describe('PipelineList', () => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
afterEach(() => { afterEach(async () => {
// unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
// depends on mocks/spies
await tree.unmount();
jest.resetAllMocks(); jest.resetAllMocks();
tree.unmount();
}); });
it('renders an empty list with empty state message', () => { it('renders an empty list with empty state message', () => {

View File

@ -88,8 +88,10 @@ describe('ResourceSelector', () => {
selectionChangedCbSpy.mockReset(); selectionChangedCbSpy.mockReset();
}); });
afterEach(() => { afterEach(async () => {
tree.unmount(); // unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
// depends on mocks/spies
await tree.unmount();
}); });
it('displays resource selector', async () => { it('displays resource selector', async () => {

View File

@ -27,6 +27,7 @@ import { PlotType } from '../components/viewers/Viewer';
import { RouteParams, RoutePage, QUERY_PARAMS } from '../components/Router'; import { RouteParams, RoutePage, QUERY_PARAMS } from '../components/Router';
import { Workflow } from 'third_party/argo-ui/argo_template'; import { Workflow } from 'third_party/argo-ui/argo_template';
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { NodePhase } from './Status';
describe('RunDetails', () => { describe('RunDetails', () => {
const updateBannerSpy = jest.fn(); const updateBannerSpy = jest.fn();
@ -66,6 +67,9 @@ describe('RunDetails', () => {
beforeAll(() => jest.spyOn(console, 'error').mockImplementation()); beforeAll(() => jest.spyOn(console, 'error').mockImplementation());
beforeEach(() => { beforeEach(() => {
// The RunDetails page uses timers to periodically refresh
jest.useFakeTimers();
testRun = { testRun = {
pipeline_runtime: { pipeline_runtime: {
workflow_manifest: '{}', workflow_manifest: '{}',
@ -92,9 +96,11 @@ describe('RunDetails', () => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
afterEach(() => { afterEach(async () => {
// unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
// depends on mocks/spies
await tree.unmount();
jest.resetAllMocks(); jest.resetAllMocks();
tree.unmount();
}); });
it('shows success run status in page title', async () => { it('shows success run status in page title', async () => {
@ -661,4 +667,108 @@ describe('RunDetails', () => {
await refreshBtn!.action(); await refreshBtn!.action();
expect(tree.state('selectedNodeDetails')).toHaveProperty('phaseMessage', undefined); expect(tree.state('selectedNodeDetails')).toHaveProperty('phaseMessage', undefined);
}); });
describe('auto refresh', () => {
beforeEach(() => {
testRun.run!.status = NodePhase.PENDING;
});
it('starts an interval of 5 seconds to auto refresh the page', async () => {
tree = shallow(<RunDetails {...generateProps()} />);
await TestUtils.flushPromises();
expect(setInterval).toHaveBeenCalledTimes(1);
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 5000);
});
it('refreshes after each interval', async () => {
tree = shallow(<RunDetails {...generateProps()} />);
await TestUtils.flushPromises();
const refreshSpy = jest.spyOn((tree.instance() as RunDetails), 'refresh');
expect(refreshSpy).toHaveBeenCalledTimes(0);
jest.runOnlyPendingTimers();
expect(refreshSpy).toHaveBeenCalledTimes(1);
await TestUtils.flushPromises();
}, 10000);
[NodePhase.ERROR, NodePhase.FAILED, NodePhase.SUCCEEDED, NodePhase.SKIPPED].forEach(status => {
it(`sets \'runFinished\' to true if run has status: ${status}`, async () => {
testRun.run!.status = status;
tree = shallow(<RunDetails {...generateProps()} />);
await TestUtils.flushPromises();
expect(tree.state('runFinished')).toBe(true);
});
});
[NodePhase.PENDING, NodePhase.RUNNING, NodePhase.UNKNOWN].forEach(status => {
it(`leaves \'runFinished\' false if run has status: ${status}`, async () => {
testRun.run!.status = status;
tree = shallow(<RunDetails {...generateProps()} />);
await TestUtils.flushPromises();
expect(tree.state('runFinished')).toBe(false);
});
});
it('pauses auto refreshing if window loses focus', async () => {
tree = shallow(<RunDetails {...generateProps()} />);
await TestUtils.flushPromises();
expect(setInterval).toHaveBeenCalledTimes(1);
expect(clearInterval).toHaveBeenCalledTimes(0);
window.dispatchEvent(new Event('blur'));
await TestUtils.flushPromises();
expect(clearInterval).toHaveBeenCalledTimes(1);
});
it('resumes auto refreshing if window loses focus and then regains it', async () => {
// Declare that the run has not finished
testRun.run!.status = NodePhase.PENDING;
tree = shallow(<RunDetails {...generateProps()} />);
await TestUtils.flushPromises();
expect(tree.state('runFinished')).toBe(false);
expect(setInterval).toHaveBeenCalledTimes(1);
expect(clearInterval).toHaveBeenCalledTimes(0);
window.dispatchEvent(new Event('blur'));
await TestUtils.flushPromises();
expect(clearInterval).toHaveBeenCalledTimes(1);
window.dispatchEvent(new Event('focus'));
await TestUtils.flushPromises();
expect(setInterval).toHaveBeenCalledTimes(2);
});
it('does not resume auto refreshing if window loses focus and then regains it but run is finished', async () => {
// Declare that the run has finished
testRun.run!.status = NodePhase.SUCCEEDED;
tree = shallow(<RunDetails {...generateProps()} />);
await TestUtils.flushPromises();
expect(tree.state('runFinished')).toBe(true);
expect(setInterval).toHaveBeenCalledTimes(0);
expect(clearInterval).toHaveBeenCalledTimes(0);
window.dispatchEvent(new Event('blur'));
await TestUtils.flushPromises();
// We expect 0 calls because the interval was never set, so it doesn't need to be cleared
expect(clearInterval).toHaveBeenCalledTimes(0);
window.dispatchEvent(new Event('focus'));
await TestUtils.flushPromises();
expect(setInterval).toHaveBeenCalledTimes(0);
});
});
}); });

View File

@ -30,7 +30,7 @@ import WorkflowParser from '../lib/WorkflowParser';
import { ApiExperiment } from '../apis/experiment'; import { ApiExperiment } from '../apis/experiment';
import { ApiRun } from '../apis/run'; import { ApiRun } from '../apis/run';
import { Apis } from '../lib/Apis'; import { Apis } from '../lib/Apis';
import { NodePhase, statusToIcon } from './Status'; import { NodePhase, statusToIcon, hasFinished } from './Status';
import { OutputArtifactLoader } from '../lib/OutputArtifactLoader'; import { OutputArtifactLoader } from '../lib/OutputArtifactLoader';
import { Page } from './Page'; import { Page } from './Page';
import { RoutePage, RouteParams, QUERY_PARAMS } from '../components/Router'; import { RoutePage, RouteParams, QUERY_PARAMS } from '../components/Router';
@ -73,6 +73,7 @@ interface RunDetailsState {
logsBannerMessage: string; logsBannerMessage: string;
logsBannerMode: Mode; logsBannerMode: Mode;
graph?: dagre.graphlib.Graph; graph?: dagre.graphlib.Graph;
runFinished: boolean;
runMetadata?: ApiRun; runMetadata?: ApiRun;
selectedTab: number; selectedTab: number;
selectedNodeDetails: SelectedNodeDetails | null; selectedNodeDetails: SelectedNodeDetails | null;
@ -82,15 +83,24 @@ interface RunDetailsState {
} }
class RunDetails extends Page<RunDetailsProps, RunDetailsState> { class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
private _onBlur: EventListener;
private _onFocus: EventListener;
private readonly AUTO_REFRESH_INTERVAL = 5000;
private _interval?: NodeJS.Timeout;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this._onBlur = this.onBlurHandler.bind(this);
this._onFocus = this.onFocusHandler.bind(this);
this.state = { this.state = {
allArtifactConfigs: [], allArtifactConfigs: [],
logsBannerAdditionalInfo: '', logsBannerAdditionalInfo: '',
logsBannerMessage: '', logsBannerMessage: '',
logsBannerMode: 'error', logsBannerMode: 'error',
runFinished: false,
selectedNodeDetails: null, selectedNodeDetails: null,
selectedTab: 0, selectedTab: 0,
sidepanelBusy: false, sidepanelBusy: false,
@ -238,7 +248,23 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
} }
public async componentDidMount(): Promise<void> { public async componentDidMount(): Promise<void> {
await this.load(); window.addEventListener('focus', this._onFocus);
window.addEventListener('blur', this._onBlur);
await this._startAutoRefresh();
}
public onBlurHandler(): void {
this._stopAutoRefresh();
}
public async onFocusHandler(): Promise<void> {
await this._startAutoRefresh();
}
public componentWillUnmount(): void {
this._stopAutoRefresh();
window.removeEventListener('focus', this._onFocus);
window.removeEventListener('blur', this._onBlur);
} }
public async refresh(): Promise<void> { public async refresh(): Promise<void> {
@ -256,9 +282,18 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
if (relatedExperimentId) { if (relatedExperimentId) {
experiment = await Apis.experimentServiceApi.getExperiment(relatedExperimentId); experiment = await Apis.experimentServiceApi.getExperiment(relatedExperimentId);
} }
const workflow = JSON.parse(runDetail.pipeline_runtime!.workflow_manifest || '{}') as Workflow;
const runMetadata = runDetail.run!; const runMetadata = runDetail.run!;
let runFinished = this.state.runFinished;
// If the run has finished, stop auto refreshing
if (hasFinished(runMetadata.status as NodePhase)) {
this._stopAutoRefresh();
// This prevents other events, such as onFocus, from resuming the autorefresh
runFinished = true;
}
const workflow = JSON.parse(runDetail.pipeline_runtime!.workflow_manifest || '{}') as Workflow;
// Show workflow errors // Show workflow errors
const workflowError = WorkflowParser.getWorkflowError(workflow); const workflowError = WorkflowParser.getWorkflowError(workflow);
if (workflowError) { if (workflowError) {
@ -295,6 +330,7 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
this.setStateSafe({ this.setStateSafe({
experiment, experiment,
graph, graph,
runFinished,
runMetadata, runMetadata,
workflow, workflow,
}); });
@ -311,6 +347,32 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
await this._loadAllOutputs(); await this._loadAllOutputs();
} }
private async _startAutoRefresh(): Promise<void> {
// If the run was not finished last time we checked, check again in case anything changed
// before proceeding to set auto-refresh interval
if (!this.state.runFinished) {
// refresh() updates runFinished's value
await this.refresh();
}
// Only set interval if run has not finished, and verify that the interval is undefined to
// avoid setting multiple intervals
if (!this.state.runFinished && this._interval === undefined) {
this._interval = setInterval(
() => this.refresh(),
this.AUTO_REFRESH_INTERVAL
);
}
}
private _stopAutoRefresh(): void {
if (this._interval !== undefined) {
clearInterval(this._interval);
}
// Reset interval to indicate that a new one can be set
this._interval = undefined;
}
private async _loadAllOutputs(): Promise<void> { private async _loadAllOutputs(): Promise<void> {
const workflow = this.state.workflow; const workflow = this.state.workflow;
@ -320,8 +382,8 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
const outputPathsList = WorkflowParser.loadAllOutputPathsWithStepNames(workflow); const outputPathsList = WorkflowParser.loadAllOutputPathsWithStepNames(workflow);
const configLists = await Promise.all(outputPathsList.map( const configLists =
({ stepName, path }) => OutputArtifactLoader.load(path) await Promise.all(outputPathsList.map(({ stepName, path }) => OutputArtifactLoader.load(path)
.then(configs => configs.map(config => ({ config, stepName }))))); .then(configs => configs.map(config => ({ config, stepName })))));
const allArtifactConfigs = flatten(configLists); const allArtifactConfigs = flatten(configLists);

View File

@ -16,7 +16,7 @@
import * as Utils from '../lib/Utils'; import * as Utils from '../lib/Utils';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { statusToIcon, NodePhase } from './Status'; import { statusToIcon, NodePhase, hasFinished } from './Status';
describe('Status', () => { describe('Status', () => {
@ -33,37 +33,57 @@ describe('Status', () => {
}); });
}); });
it('handles an unknown phase', () => { describe('statusToIcon', () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => null); it('handles an unknown phase', () => {
const tree = shallow(statusToIcon('bad phase' as any)); const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => null);
expect(tree).toMatchSnapshot(); const tree = shallow(statusToIcon('bad phase' as any));
expect(consoleSpy).toHaveBeenLastCalledWith('Unknown node phase:', 'bad phase');
});
it('displays start and end dates if both are provided', () => {
const tree = shallow(statusToIcon(NodePhase.SUCCEEDED, startDate, endDate));
expect(tree).toMatchSnapshot();
});
it('does not display a end date if none was provided', () => {
const tree = shallow(statusToIcon(NodePhase.SUCCEEDED, startDate));
expect(tree).toMatchSnapshot();
});
it('does not display a start date if none was provided', () => {
const tree = shallow(statusToIcon(NodePhase.SUCCEEDED, undefined, endDate));
expect(tree).toMatchSnapshot();
});
it('does not display any dates if neither was provided', () => {
const tree = shallow(statusToIcon(NodePhase.SUCCEEDED, /* No dates */));
expect(tree).toMatchSnapshot();
});
Object.keys(NodePhase).map(status => (
it('renders an icon with tooltip for phase: ' + status, () => {
const tree = shallow(statusToIcon(NodePhase[status]));
expect(tree).toMatchSnapshot(); expect(tree).toMatchSnapshot();
}) expect(consoleSpy).toHaveBeenLastCalledWith('Unknown node phase:', 'bad phase');
)); });
it('displays start and end dates if both are provided', () => {
const tree = shallow(statusToIcon(NodePhase.SUCCEEDED, startDate, endDate));
expect(tree).toMatchSnapshot();
});
it('does not display a end date if none was provided', () => {
const tree = shallow(statusToIcon(NodePhase.SUCCEEDED, startDate));
expect(tree).toMatchSnapshot();
});
it('does not display a start date if none was provided', () => {
const tree = shallow(statusToIcon(NodePhase.SUCCEEDED, undefined, endDate));
expect(tree).toMatchSnapshot();
});
it('does not display any dates if neither was provided', () => {
const tree = shallow(statusToIcon(NodePhase.SUCCEEDED, /* No dates */));
expect(tree).toMatchSnapshot();
});
Object.keys(NodePhase).map(status => (
it('renders an icon with tooltip for phase: ' + status, () => {
const tree = shallow(statusToIcon(NodePhase[status]));
expect(tree).toMatchSnapshot();
})
));
});
describe('hasFinished', () => {
[NodePhase.ERROR, NodePhase.FAILED, NodePhase.SUCCEEDED, NodePhase.SKIPPED].forEach(status => {
it(`returns \'true\' if status is: ${status}`, () => {
expect(hasFinished(status)).toBe(true);
});
});
[NodePhase.PENDING, NodePhase.RUNNING, NodePhase.UNKNOWN].forEach(status => {
it(`returns \'false\' if status is: ${status}`, () => {
expect(hasFinished(status)).toBe(false);
});
});
it('returns \'false\' if status is undefined', () => {
expect(hasFinished(undefined)).toBe(false);
});
});
}); });

View File

@ -35,6 +35,26 @@ export enum NodePhase {
UNKNOWN = 'Unknown', UNKNOWN = 'Unknown',
} }
export function hasFinished(status?: NodePhase): boolean {
if (!status) {
return false;
}
switch (status) {
case NodePhase.SUCCEEDED: // Fall through
case NodePhase.FAILED: // Fall through
case NodePhase.ERROR: // Fall through
case NodePhase.SKIPPED:
return true;
case NodePhase.PENDING: // Fall through
case NodePhase.RUNNING: // Fall through
case NodePhase.UNKNOWN:
return false;
default:
return false;
}
}
export function statusToIcon(status: NodePhase, startDate?: Date | string, endDate?: Date | string): JSX.Element { export function statusToIcon(status: NodePhase, startDate?: Date | string, endDate?: Date | string): JSX.Element {
// tslint:disable-next-line:variable-name // tslint:disable-next-line:variable-name
let IconComponent: any = UnknownIcon; let IconComponent: any = UnknownIcon;
@ -78,13 +98,13 @@ export function statusToIcon(status: NodePhase, startDate?: Date | string, endDa
return ( return (
<Tooltip title={ <Tooltip title={
<div> <div>
<div>{title}</div> <div>{title}</div>
{/* These dates may actually be strings, not a Dates due to a bug in swagger's handling of dates */} {/* These dates may actually be strings, not a Dates due to a bug in swagger's handling of dates */}
{startDate && (<div>Start: {formatDateString(startDate)}</div>)} {startDate && (<div>Start: {formatDateString(startDate)}</div>)}
{endDate && (<div>End: {formatDateString(endDate)}</div>)} {endDate && (<div>End: {formatDateString(endDate)}</div>)}
</div> </div>
}> }>
<span style={{ height: 18 }}> <span style={{ height: 18 }}>
<IconComponent style={{ color: iconColor, height: 18, width: 18 }} /> <IconComponent style={{ color: iconColor, height: 18, width: 18 }} />
</span> </span>

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Status displays start and end dates if both are provided 1`] = ` exports[`Status statusToIcon displays start and end dates if both are provided 1`] = `
<Tooltip <Tooltip
TransitionComponent={[Function]} TransitionComponent={[Function]}
classes={ classes={
@ -422,7 +422,7 @@ exports[`Status displays start and end dates if both are provided 1`] = `
</Tooltip> </Tooltip>
`; `;
exports[`Status does not display a end date if none was provided 1`] = ` exports[`Status statusToIcon does not display a end date if none was provided 1`] = `
<Tooltip <Tooltip
TransitionComponent={[Function]} TransitionComponent={[Function]}
classes={ classes={
@ -840,7 +840,7 @@ exports[`Status does not display a end date if none was provided 1`] = `
</Tooltip> </Tooltip>
`; `;
exports[`Status does not display a start date if none was provided 1`] = ` exports[`Status statusToIcon does not display a start date if none was provided 1`] = `
<Tooltip <Tooltip
TransitionComponent={[Function]} TransitionComponent={[Function]}
classes={ classes={
@ -1258,7 +1258,7 @@ exports[`Status does not display a start date if none was provided 1`] = `
</Tooltip> </Tooltip>
`; `;
exports[`Status does not display any dates if neither was provided 1`] = ` exports[`Status statusToIcon does not display any dates if neither was provided 1`] = `
<Tooltip <Tooltip
TransitionComponent={[Function]} TransitionComponent={[Function]}
classes={ classes={
@ -1672,7 +1672,7 @@ exports[`Status does not display any dates if neither was provided 1`] = `
</Tooltip> </Tooltip>
`; `;
exports[`Status handles an unknown phase 1`] = ` exports[`Status statusToIcon handles an unknown phase 1`] = `
<Tooltip <Tooltip
TransitionComponent={[Function]} TransitionComponent={[Function]}
classes={ classes={
@ -2086,7 +2086,7 @@ exports[`Status handles an unknown phase 1`] = `
</Tooltip> </Tooltip>
`; `;
exports[`Status renders an icon with tooltip for phase: ERROR 1`] = ` exports[`Status statusToIcon renders an icon with tooltip for phase: ERROR 1`] = `
<Tooltip <Tooltip
TransitionComponent={[Function]} TransitionComponent={[Function]}
classes={ classes={
@ -2500,7 +2500,7 @@ exports[`Status renders an icon with tooltip for phase: ERROR 1`] = `
</Tooltip> </Tooltip>
`; `;
exports[`Status renders an icon with tooltip for phase: FAILED 1`] = ` exports[`Status statusToIcon renders an icon with tooltip for phase: FAILED 1`] = `
<Tooltip <Tooltip
TransitionComponent={[Function]} TransitionComponent={[Function]}
classes={ classes={
@ -2914,7 +2914,7 @@ exports[`Status renders an icon with tooltip for phase: FAILED 1`] = `
</Tooltip> </Tooltip>
`; `;
exports[`Status renders an icon with tooltip for phase: PENDING 1`] = ` exports[`Status statusToIcon renders an icon with tooltip for phase: PENDING 1`] = `
<Tooltip <Tooltip
TransitionComponent={[Function]} TransitionComponent={[Function]}
classes={ classes={
@ -3328,7 +3328,7 @@ exports[`Status renders an icon with tooltip for phase: PENDING 1`] = `
</Tooltip> </Tooltip>
`; `;
exports[`Status renders an icon with tooltip for phase: RUNNING 1`] = ` exports[`Status statusToIcon renders an icon with tooltip for phase: RUNNING 1`] = `
<Tooltip <Tooltip
TransitionComponent={[Function]} TransitionComponent={[Function]}
classes={ classes={
@ -3742,7 +3742,7 @@ exports[`Status renders an icon with tooltip for phase: RUNNING 1`] = `
</Tooltip> </Tooltip>
`; `;
exports[`Status renders an icon with tooltip for phase: SKIPPED 1`] = ` exports[`Status statusToIcon renders an icon with tooltip for phase: SKIPPED 1`] = `
<Tooltip <Tooltip
TransitionComponent={[Function]} TransitionComponent={[Function]}
classes={ classes={
@ -4156,7 +4156,7 @@ exports[`Status renders an icon with tooltip for phase: SKIPPED 1`] = `
</Tooltip> </Tooltip>
`; `;
exports[`Status renders an icon with tooltip for phase: SUCCEEDED 1`] = ` exports[`Status statusToIcon renders an icon with tooltip for phase: SUCCEEDED 1`] = `
<Tooltip <Tooltip
TransitionComponent={[Function]} TransitionComponent={[Function]}
classes={ classes={
@ -4570,7 +4570,7 @@ exports[`Status renders an icon with tooltip for phase: SUCCEEDED 1`] = `
</Tooltip> </Tooltip>
`; `;
exports[`Status renders an icon with tooltip for phase: UNKNOWN 1`] = ` exports[`Status statusToIcon renders an icon with tooltip for phase: UNKNOWN 1`] = `
<Tooltip <Tooltip
TransitionComponent={[Function]} TransitionComponent={[Function]}
classes={ classes={