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,
|
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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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={
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue