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:
parent
3b3a15e16a
commit
5beffef614
|
|
@ -346,7 +346,7 @@ const runs: ApiRunDetail[] = [
|
|||
relationship: ApiRelationship.OWNER,
|
||||
}],
|
||||
scheduled_at: new Date('2018-04-17T21:00:00.000Z'),
|
||||
status: 'Succeeded',
|
||||
status: 'Error',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -56,9 +56,11 @@ describe('SideNav', () => {
|
|||
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();
|
||||
tree.unmount();
|
||||
(window as any).innerWidth = wideWidth;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,10 @@ import TestUtils from '../TestUtils';
|
|||
describe('UploadPipelineDialog', () => {
|
||||
let tree: ReactWrapper | ShallowWrapper;
|
||||
|
||||
afterEach(() => {
|
||||
tree.unmount();
|
||||
afterEach(async () => {
|
||||
// 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', () => {
|
||||
|
|
|
|||
|
|
@ -136,8 +136,10 @@ describe('Compare', () => {
|
|||
getRunSpy.mockImplementation((id: string) => runs.find((r) => r.run!.id === id));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tree.unmount();
|
||||
afterEach(async () => {
|
||||
// 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', () => {
|
||||
|
|
|
|||
|
|
@ -100,8 +100,10 @@ describe('ExperimentDetails', () => {
|
|||
await mockNRuns(0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tree.unmount();
|
||||
afterEach(async () => {
|
||||
// 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 () => {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ describe('ExperimentList', () => {
|
|||
const historyPushSpy = jest.fn();
|
||||
const listExperimentsSpy = jest.spyOn(Apis.experimentServiceApi, 'listExperiment');
|
||||
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
|
||||
const formatDateStringSpy = jest.spyOn(Utils, 'formatDateString');
|
||||
|
||||
|
|
|
|||
|
|
@ -126,9 +126,11 @@ describe('NewRun', () => {
|
|||
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();
|
||||
tree.unmount();
|
||||
});
|
||||
|
||||
it('renders the new run page', async () => {
|
||||
|
|
|
|||
|
|
@ -97,9 +97,11 @@ describe('PipelineDetails', () => {
|
|||
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();
|
||||
tree.unmount();
|
||||
});
|
||||
|
||||
it('shows empty pipeline details with no graph', async () => {
|
||||
|
|
|
|||
|
|
@ -66,9 +66,11 @@ describe('PipelineList', () => {
|
|||
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();
|
||||
tree.unmount();
|
||||
});
|
||||
|
||||
it('renders an empty list with empty state message', () => {
|
||||
|
|
|
|||
|
|
@ -88,8 +88,10 @@ describe('ResourceSelector', () => {
|
|||
selectionChangedCbSpy.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tree.unmount();
|
||||
afterEach(async () => {
|
||||
// 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 () => {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { PlotType } from '../components/viewers/Viewer';
|
|||
import { RouteParams, RoutePage, QUERY_PARAMS } from '../components/Router';
|
||||
import { Workflow } from 'third_party/argo-ui/argo_template';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { NodePhase } from './Status';
|
||||
|
||||
describe('RunDetails', () => {
|
||||
const updateBannerSpy = jest.fn();
|
||||
|
|
@ -66,6 +67,9 @@ describe('RunDetails', () => {
|
|||
beforeAll(() => jest.spyOn(console, 'error').mockImplementation());
|
||||
|
||||
beforeEach(() => {
|
||||
// The RunDetails page uses timers to periodically refresh
|
||||
jest.useFakeTimers();
|
||||
|
||||
testRun = {
|
||||
pipeline_runtime: {
|
||||
workflow_manifest: '{}',
|
||||
|
|
@ -92,9 +96,11 @@ describe('RunDetails', () => {
|
|||
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();
|
||||
tree.unmount();
|
||||
});
|
||||
|
||||
it('shows success run status in page title', async () => {
|
||||
|
|
@ -661,4 +667,108 @@ describe('RunDetails', () => {
|
|||
await refreshBtn!.action();
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import WorkflowParser from '../lib/WorkflowParser';
|
|||
import { ApiExperiment } from '../apis/experiment';
|
||||
import { ApiRun } from '../apis/run';
|
||||
import { Apis } from '../lib/Apis';
|
||||
import { NodePhase, statusToIcon } from './Status';
|
||||
import { NodePhase, statusToIcon, hasFinished } from './Status';
|
||||
import { OutputArtifactLoader } from '../lib/OutputArtifactLoader';
|
||||
import { Page } from './Page';
|
||||
import { RoutePage, RouteParams, QUERY_PARAMS } from '../components/Router';
|
||||
|
|
@ -73,6 +73,7 @@ interface RunDetailsState {
|
|||
logsBannerMessage: string;
|
||||
logsBannerMode: Mode;
|
||||
graph?: dagre.graphlib.Graph;
|
||||
runFinished: boolean;
|
||||
runMetadata?: ApiRun;
|
||||
selectedTab: number;
|
||||
selectedNodeDetails: SelectedNodeDetails | null;
|
||||
|
|
@ -82,15 +83,24 @@ interface 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) {
|
||||
super(props);
|
||||
|
||||
this._onBlur = this.onBlurHandler.bind(this);
|
||||
this._onFocus = this.onFocusHandler.bind(this);
|
||||
|
||||
this.state = {
|
||||
allArtifactConfigs: [],
|
||||
logsBannerAdditionalInfo: '',
|
||||
logsBannerMessage: '',
|
||||
logsBannerMode: 'error',
|
||||
runFinished: false,
|
||||
selectedNodeDetails: null,
|
||||
selectedTab: 0,
|
||||
sidepanelBusy: false,
|
||||
|
|
@ -238,7 +248,23 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
|
|||
}
|
||||
|
||||
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> {
|
||||
|
|
@ -256,9 +282,18 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
|
|||
if (relatedExperimentId) {
|
||||
experiment = await Apis.experimentServiceApi.getExperiment(relatedExperimentId);
|
||||
}
|
||||
const workflow = JSON.parse(runDetail.pipeline_runtime!.workflow_manifest || '{}') as Workflow;
|
||||
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
|
||||
const workflowError = WorkflowParser.getWorkflowError(workflow);
|
||||
if (workflowError) {
|
||||
|
|
@ -295,6 +330,7 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
|
|||
this.setStateSafe({
|
||||
experiment,
|
||||
graph,
|
||||
runFinished,
|
||||
runMetadata,
|
||||
workflow,
|
||||
});
|
||||
|
|
@ -311,6 +347,32 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
|
|||
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> {
|
||||
const workflow = this.state.workflow;
|
||||
|
||||
|
|
@ -320,8 +382,8 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
|
|||
|
||||
const outputPathsList = WorkflowParser.loadAllOutputPathsWithStepNames(workflow);
|
||||
|
||||
const configLists = await Promise.all(outputPathsList.map(
|
||||
({ stepName, path }) => OutputArtifactLoader.load(path)
|
||||
const configLists =
|
||||
await Promise.all(outputPathsList.map(({ stepName, path }) => OutputArtifactLoader.load(path)
|
||||
.then(configs => configs.map(config => ({ config, stepName })))));
|
||||
const allArtifactConfigs = flatten(configLists);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import * as Utils from '../lib/Utils';
|
||||
import { shallow } from 'enzyme';
|
||||
import { statusToIcon, NodePhase } from './Status';
|
||||
import { statusToIcon, NodePhase, hasFinished } from './Status';
|
||||
|
||||
|
||||
describe('Status', () => {
|
||||
|
|
@ -33,37 +33,57 @@ describe('Status', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('handles an unknown phase', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => null);
|
||||
const tree = shallow(statusToIcon('bad phase' as any));
|
||||
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]));
|
||||
describe('statusToIcon', () => {
|
||||
it('handles an unknown phase', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => null);
|
||||
const tree = shallow(statusToIcon('bad phase' as any));
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,6 +35,26 @@ export enum NodePhase {
|
|||
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 {
|
||||
// tslint:disable-next-line:variable-name
|
||||
let IconComponent: any = UnknownIcon;
|
||||
|
|
@ -78,13 +98,13 @@ export function statusToIcon(status: NodePhase, startDate?: Date | string, endDa
|
|||
|
||||
return (
|
||||
<Tooltip title={
|
||||
<div>
|
||||
<div>{title}</div>
|
||||
{/* 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>)}
|
||||
{endDate && (<div>End: {formatDateString(endDate)}</div>)}
|
||||
</div>
|
||||
}>
|
||||
<div>
|
||||
<div>{title}</div>
|
||||
{/* 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>)}
|
||||
{endDate && (<div>End: {formatDateString(endDate)}</div>)}
|
||||
</div>
|
||||
}>
|
||||
<span style={{ height: 18 }}>
|
||||
<IconComponent style={{ color: iconColor, height: 18, width: 18 }} />
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// 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
|
||||
TransitionComponent={[Function]}
|
||||
classes={
|
||||
|
|
@ -422,7 +422,7 @@ exports[`Status displays start and end dates if both are provided 1`] = `
|
|||
</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
|
||||
TransitionComponent={[Function]}
|
||||
classes={
|
||||
|
|
@ -840,7 +840,7 @@ exports[`Status does not display a end date if none was provided 1`] = `
|
|||
</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
|
||||
TransitionComponent={[Function]}
|
||||
classes={
|
||||
|
|
@ -1258,7 +1258,7 @@ exports[`Status does not display a start date if none was provided 1`] = `
|
|||
</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
|
||||
TransitionComponent={[Function]}
|
||||
classes={
|
||||
|
|
@ -1672,7 +1672,7 @@ exports[`Status does not display any dates if neither was provided 1`] = `
|
|||
</Tooltip>
|
||||
`;
|
||||
|
||||
exports[`Status handles an unknown phase 1`] = `
|
||||
exports[`Status statusToIcon handles an unknown phase 1`] = `
|
||||
<Tooltip
|
||||
TransitionComponent={[Function]}
|
||||
classes={
|
||||
|
|
@ -2086,7 +2086,7 @@ exports[`Status handles an unknown phase 1`] = `
|
|||
</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
|
||||
TransitionComponent={[Function]}
|
||||
classes={
|
||||
|
|
@ -2500,7 +2500,7 @@ exports[`Status renders an icon with tooltip for phase: ERROR 1`] = `
|
|||
</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
|
||||
TransitionComponent={[Function]}
|
||||
classes={
|
||||
|
|
@ -2914,7 +2914,7 @@ exports[`Status renders an icon with tooltip for phase: FAILED 1`] = `
|
|||
</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
|
||||
TransitionComponent={[Function]}
|
||||
classes={
|
||||
|
|
@ -3328,7 +3328,7 @@ exports[`Status renders an icon with tooltip for phase: PENDING 1`] = `
|
|||
</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
|
||||
TransitionComponent={[Function]}
|
||||
classes={
|
||||
|
|
@ -3742,7 +3742,7 @@ exports[`Status renders an icon with tooltip for phase: RUNNING 1`] = `
|
|||
</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
|
||||
TransitionComponent={[Function]}
|
||||
classes={
|
||||
|
|
@ -4156,7 +4156,7 @@ exports[`Status renders an icon with tooltip for phase: SKIPPED 1`] = `
|
|||
</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
|
||||
TransitionComponent={[Function]}
|
||||
classes={
|
||||
|
|
@ -4570,7 +4570,7 @@ exports[`Status renders an icon with tooltip for phase: SUCCEEDED 1`] = `
|
|||
</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
|
||||
TransitionComponent={[Function]}
|
||||
classes={
|
||||
|
|
|
|||
Loading…
Reference in New Issue