PipelineDetails page tests (#380)
* wip pipeline details tests * pipeline details tests * safe load template * add close panel test * pr comments
This commit is contained in:
parent
2a3ec15993
commit
db772d51bc
|
|
@ -119,11 +119,14 @@ interface GraphProps {
|
|||
}
|
||||
|
||||
export default class Graph extends React.Component<GraphProps> {
|
||||
public render(): JSX.Element {
|
||||
public render(): JSX.Element | null {
|
||||
const { graph } = this.props;
|
||||
const displayEdges: Edge[] = [];
|
||||
const displayEdgeStartPoints: number[][] = [];
|
||||
|
||||
if (!graph.nodes().length) {
|
||||
return null;
|
||||
}
|
||||
dagre.layout(graph);
|
||||
|
||||
// Creates the lines that constitute the edges connecting the graph.
|
||||
|
|
|
|||
|
|
@ -94,11 +94,7 @@ exports[`Graph gracefully renders a graph with a selected node id that does not
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`Graph handles an empty graph 1`] = `
|
||||
<div
|
||||
className="root"
|
||||
/>
|
||||
`;
|
||||
exports[`Graph handles an empty graph 1`] = `""`;
|
||||
|
||||
exports[`Graph renders a complex graph with six nodes and seven edges 1`] = `
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -0,0 +1,332 @@
|
|||
/*
|
||||
* Copyright 2018 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import * as StaticGraphParser from '../lib/StaticGraphParser';
|
||||
import PipelineDetails, { css } from './PipelineDetails';
|
||||
import TestUtils from '../TestUtils';
|
||||
import { ApiPipeline } from '../apis/pipeline';
|
||||
import { Apis } from '../lib/Apis';
|
||||
import { PageProps } from './Page';
|
||||
import { QUERY_PARAMS } from '../lib/URLParser';
|
||||
import { RouteParams, RoutePage } from '../components/Router';
|
||||
import { graphlib } from 'dagre';
|
||||
import { shallow, mount, ShallowWrapper, ReactWrapper } from 'enzyme';
|
||||
|
||||
describe('PipelineDetails', () => {
|
||||
const updateBannerSpy = jest.fn();
|
||||
const updateDialogSpy = jest.fn();
|
||||
const updateSnackbarSpy = jest.fn();
|
||||
const updateToolbarSpy = jest.fn();
|
||||
const historyPushSpy = jest.fn();
|
||||
const getPipelineSpy = jest.spyOn(Apis.pipelineServiceApi, 'getPipeline');
|
||||
const deletePipelineSpy = jest.spyOn(Apis.pipelineServiceApi, 'deletePipeline');
|
||||
const getTemplateSpy = jest.spyOn(Apis.pipelineServiceApi, 'getTemplate');
|
||||
const createGraphSpy = jest.spyOn(StaticGraphParser, 'createGraph');
|
||||
|
||||
let tree: ShallowWrapper | ReactWrapper;
|
||||
let testPipeline: ApiPipeline = {};
|
||||
|
||||
function generateProps(): PageProps {
|
||||
// getInitialToolbarState relies on page props having been populated, so fill those first
|
||||
const pageProps: PageProps = {
|
||||
history: { push: historyPushSpy } as any,
|
||||
location: '' as any,
|
||||
match: { params: { [RouteParams.pipelineId]: testPipeline.id }, isExact: true, path: '', url: '' },
|
||||
toolbarProps: { actions: [], breadcrumbs: [], pageTitle: '' },
|
||||
updateBanner: updateBannerSpy,
|
||||
updateDialog: updateDialogSpy,
|
||||
updateSnackbar: updateSnackbarSpy,
|
||||
updateToolbar: updateToolbarSpy,
|
||||
};
|
||||
return Object.assign(pageProps, {
|
||||
toolbarProps: new PipelineDetails(pageProps).getInitialToolbarState(),
|
||||
});
|
||||
}
|
||||
|
||||
beforeAll(() => jest.spyOn(console, 'error').mockImplementation());
|
||||
afterAll(() => jest.resetAllMocks());
|
||||
|
||||
beforeEach(() => {
|
||||
testPipeline = {
|
||||
created_at: new Date(2018, 8, 5, 4, 3, 2),
|
||||
description: 'test job description',
|
||||
enabled: true,
|
||||
id: 'test-job-id',
|
||||
max_concurrency: '50',
|
||||
name: 'test job',
|
||||
pipeline_spec: {
|
||||
parameters: [{ name: 'param1', value: 'value1' }],
|
||||
pipeline_id: 'some-pipeline-id',
|
||||
},
|
||||
trigger: {
|
||||
periodic_schedule: {
|
||||
end_time: new Date(2018, 10, 9, 8, 7, 6),
|
||||
interval_second: '3600',
|
||||
start_time: new Date(2018, 9, 8, 7, 6),
|
||||
}
|
||||
},
|
||||
} as ApiPipeline;
|
||||
|
||||
historyPushSpy.mockClear();
|
||||
updateBannerSpy.mockClear();
|
||||
updateDialogSpy.mockClear();
|
||||
updateSnackbarSpy.mockClear();
|
||||
updateToolbarSpy.mockClear();
|
||||
deletePipelineSpy.mockReset();
|
||||
getPipelineSpy.mockImplementation(() => Promise.resolve(testPipeline));
|
||||
getPipelineSpy.mockClear();
|
||||
getTemplateSpy.mockImplementation(() => Promise.resolve('{}'));
|
||||
getTemplateSpy.mockClear();
|
||||
createGraphSpy.mockImplementation(() => new graphlib.Graph());
|
||||
createGraphSpy.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => tree.unmount());
|
||||
|
||||
it('shows empty pipeline details with no graph', async () => {
|
||||
TestUtils.makeErrorResponseOnce(createGraphSpy, 'bad graph');
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows load error banner when failing to get pipeline', async () => {
|
||||
TestUtils.makeErrorResponseOnce(getPipelineSpy, 'woops');
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getPipelineSpy;
|
||||
await TestUtils.flushPromises();
|
||||
expect(updateBannerSpy).toHaveBeenCalledTimes(2); // Once to clear banner, once to show error
|
||||
expect(updateBannerSpy).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
additionalInfo: 'woops',
|
||||
message: 'Cannot retrieve pipeline details. Click Details for more information.',
|
||||
mode: 'error',
|
||||
}));
|
||||
});
|
||||
|
||||
it('shows load error banner when failing to get pipeline template', async () => {
|
||||
TestUtils.makeErrorResponseOnce(getTemplateSpy, 'woops');
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getPipelineSpy;
|
||||
await TestUtils.flushPromises();
|
||||
expect(updateBannerSpy).toHaveBeenCalledTimes(2); // Once to clear banner, once to show error
|
||||
expect(updateBannerSpy).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
additionalInfo: 'woops',
|
||||
message: 'Cannot retrieve pipeline template. Click Details for more information.',
|
||||
mode: 'error',
|
||||
}));
|
||||
});
|
||||
|
||||
it('shows no graph error banner when failing to parse graph', async () => {
|
||||
TestUtils.makeErrorResponseOnce(createGraphSpy, 'bad graph');
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
expect(updateBannerSpy).toHaveBeenCalledTimes(2); // Once to clear banner, once to show error
|
||||
expect(updateBannerSpy).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
additionalInfo: 'bad graph',
|
||||
message: 'Error: failed to generate Pipeline graph. Click Details for more information.',
|
||||
mode: 'error',
|
||||
}));
|
||||
});
|
||||
|
||||
it('shows empty pipeline details with empty graph', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('sets summary shown state to false when clicking the Hide button', async () => {
|
||||
tree = mount(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
tree.update();
|
||||
expect(tree.state('summaryShown')).toBe(true);
|
||||
tree.find('Paper Button').simulate('click');
|
||||
expect(tree.state('summaryShown')).toBe(false);
|
||||
});
|
||||
|
||||
it('collapses summary card when summary shown state is false', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
tree.setState({ summaryShown: false });
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows the summary card when clicking Show button', async () => {
|
||||
tree = mount(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
tree.setState({ summaryShown: false });
|
||||
tree.find(`.${css.footer} Button`).simulate('click');
|
||||
expect(tree.state('summaryShown')).toBe(true);
|
||||
});
|
||||
|
||||
it('has a new experiment button', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
const instance = tree.instance() as PipelineDetails;
|
||||
const newExperimentBtn = instance.getInitialToolbarState().actions.find(
|
||||
b => b.title === 'Start an experiment');
|
||||
expect(newExperimentBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it('clicking new experiment button navigates to new experiment page', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
const instance = tree.instance() as PipelineDetails;
|
||||
const newExperimentBtn = instance.getInitialToolbarState().actions.find(
|
||||
b => b.title === 'Start an experiment');
|
||||
await newExperimentBtn!.action();
|
||||
expect(historyPushSpy).toHaveBeenCalledTimes(1);
|
||||
expect(historyPushSpy).toHaveBeenLastCalledWith(
|
||||
RoutePage.NEW_EXPERIMENT + `?${QUERY_PARAMS.pipelineId}=${testPipeline.id}`);
|
||||
});
|
||||
|
||||
it('has a delete button', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
const instance = tree.instance() as PipelineDetails;
|
||||
const deleteBtn = instance.getInitialToolbarState().actions.find(
|
||||
b => b.title === 'Delete');
|
||||
expect(deleteBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it('shows delete confirmation dialog when delete buttin is clicked', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
const deleteBtn = (tree.instance() as PipelineDetails)
|
||||
.getInitialToolbarState().actions.find(b => b.title === 'Delete');
|
||||
await deleteBtn!.action();
|
||||
expect(updateDialogSpy).toHaveBeenCalledTimes(1);
|
||||
expect(updateDialogSpy).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
title: 'Delete this pipeline?',
|
||||
}));
|
||||
});
|
||||
|
||||
it('does not call delete API for selected pipeline when delete dialog is canceled', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
const deleteBtn = (tree.instance() as PipelineDetails)
|
||||
.getInitialToolbarState().actions.find(b => b.title === 'Delete');
|
||||
await deleteBtn!.action();
|
||||
const call = updateDialogSpy.mock.calls[0][0];
|
||||
const cancelBtn = call.buttons.find((b: any) => b.text === 'Cancel');
|
||||
await cancelBtn.onClick();
|
||||
expect(deletePipelineSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls delete API when delete dialog is confirmed', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
const deleteBtn = (tree.instance() as PipelineDetails)
|
||||
.getInitialToolbarState().actions.find(b => b.title === 'Delete');
|
||||
await deleteBtn!.action();
|
||||
const call = updateDialogSpy.mock.calls[0][0];
|
||||
const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete');
|
||||
await confirmBtn.onClick();
|
||||
expect(deletePipelineSpy).toHaveBeenCalledTimes(1);
|
||||
expect(deletePipelineSpy).toHaveBeenLastCalledWith(testPipeline.id);
|
||||
});
|
||||
|
||||
it('shows error dialog if deletion fails', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
TestUtils.makeErrorResponseOnce(deletePipelineSpy, 'woops');
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
const deleteBtn = (tree.instance() as PipelineDetails)
|
||||
.getInitialToolbarState().actions.find(b => b.title === 'Delete');
|
||||
await deleteBtn!.action();
|
||||
const call = updateDialogSpy.mock.calls[0][0];
|
||||
const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete');
|
||||
await confirmBtn.onClick();
|
||||
expect(updateDialogSpy).toHaveBeenCalledTimes(2); // Delete dialog + error dialog
|
||||
expect(updateDialogSpy).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
content: 'woops',
|
||||
title: 'Failed to delete pipeline',
|
||||
}));
|
||||
});
|
||||
|
||||
it('shows success snackbar if deletion succeeds', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
const deleteBtn = (tree.instance() as PipelineDetails)
|
||||
.getInitialToolbarState().actions.find(b => b.title === 'Delete');
|
||||
await deleteBtn!.action();
|
||||
const call = updateDialogSpy.mock.calls[0][0];
|
||||
const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete');
|
||||
await confirmBtn.onClick();
|
||||
expect(updateSnackbarSpy).toHaveBeenCalledTimes(1);
|
||||
expect(updateSnackbarSpy).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
message: 'Successfully deleted pipeline: ' + testPipeline.name,
|
||||
open: true,
|
||||
}));
|
||||
});
|
||||
|
||||
it('opens side panel on clicked node, shows message when node is not found in graph', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
tree.find('Graph').simulate('click', 'some-node-id');
|
||||
expect(tree.state('selectedNodeId')).toBe('some-node-id');
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows clicked node info in the side panel if it is in the graph', async () => {
|
||||
const g = new graphlib.Graph();
|
||||
const info = new StaticGraphParser.SelectedNodeInfo();
|
||||
info.args = ['test arg', 'test arg2'];
|
||||
info.command = ['test command', 'test command 2'];
|
||||
info.condition = 'test condition';
|
||||
info.image = 'test image';
|
||||
info.inputs = [['key1', 'val1'], ['key2', 'val2']];
|
||||
info.outputs = [['key3', 'val3'], ['key4', 'val4']];
|
||||
info.nodeType = 'container';
|
||||
g.setNode('node1', { info, label: 'node1' });
|
||||
createGraphSpy.mockImplementation(() => g);
|
||||
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
tree.find('Graph').simulate('click', 'node1');
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows pipeline source code when config tab is clicked', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
tree.find('MD2Tabs').simulate('switch', 1);
|
||||
expect(tree.state('selectedTab')).toBe(1);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('closes side panel when close button is clicked', async () => {
|
||||
tree = shallow(<PipelineDetails {...generateProps()} />);
|
||||
await getTemplateSpy;
|
||||
await TestUtils.flushPromises();
|
||||
tree.setState({ selectedNodeId: 'some-node-id' });
|
||||
tree.find('SidePanel').simulate('close');
|
||||
expect(tree.state('selectedNodeId')).toBe('');
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -27,7 +27,7 @@ import MD2Tabs from '../atoms/MD2Tabs';
|
|||
import Paper from '@material-ui/core/Paper';
|
||||
import SidePanel from '../components/SidePanel';
|
||||
import StaticNodeDetails from '../components/StaticNodeDetails';
|
||||
import { ApiPipeline } from '../apis/pipeline';
|
||||
import { ApiPipeline, ApiGetTemplateResponse } from '../apis/pipeline';
|
||||
import { Apis } from '../lib/Apis';
|
||||
import { Page } from './Page';
|
||||
import { RoutePage, RouteParams } from '../components/Router';
|
||||
|
|
@ -52,7 +52,7 @@ interface PipelineDetailsState {
|
|||
|
||||
const summaryCardWidth = 500;
|
||||
|
||||
const css = stylesheet({
|
||||
export const css = stylesheet({
|
||||
containerCss: {
|
||||
$nest: {
|
||||
'& .CodeMirror': {
|
||||
|
|
@ -126,7 +126,7 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> {
|
|||
{ onClick: () => this._deleteDialogClosed(false), text: 'Cancel' },
|
||||
],
|
||||
onClose: () => this._deleteDialogClosed(false),
|
||||
title: 'Delete this Pipeline?',
|
||||
title: 'Delete this pipeline?',
|
||||
}),
|
||||
id: 'deleteBtn',
|
||||
title: 'Delete',
|
||||
|
|
@ -155,7 +155,7 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> {
|
|||
<div className={commonCss.page}>
|
||||
<MD2Tabs
|
||||
selectedTab={selectedTab}
|
||||
onSwitch={(tab: number) => this.setState({ selectedTab: tab })}
|
||||
onSwitch={(tab: number) => this.setStateSafe({ selectedTab: tab })}
|
||||
tabs={['Graph', 'Source']}
|
||||
/>
|
||||
<div className={commonCss.page}>
|
||||
|
|
@ -167,7 +167,7 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> {
|
|||
<div className={commonCss.header}>
|
||||
Summary
|
||||
</div>
|
||||
<Button onClick={() => this.setState({ summaryShown: false })} color='secondary'>
|
||||
<Button onClick={() => this.setStateSafe({ summaryShown: false })} color='secondary'>
|
||||
Hide
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -179,10 +179,10 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> {
|
|||
)}
|
||||
|
||||
<Graph graph={this.state.graph} selectedNodeId={selectedNodeId}
|
||||
onClick={id => this.setState({ selectedNodeId: id })} />
|
||||
onClick={id => this.setStateSafe({ selectedNodeId: id })} />
|
||||
|
||||
<SidePanel isOpen={!!selectedNodeId}
|
||||
title={selectedNodeId} onClose={() => this.setState({ selectedNodeId: '' })}>
|
||||
title={selectedNodeId} onClose={() => this.setStateSafe({ selectedNodeId: '' })}>
|
||||
<div className={commonCss.page}>
|
||||
{!selectedNodeInfo && <div>Unable to retrieve node info</div>}
|
||||
{!!selectedNodeInfo && <div className={padding(20, 'lr')}>
|
||||
|
|
@ -192,7 +192,7 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> {
|
|||
</SidePanel>
|
||||
<div className={css.footer}>
|
||||
{!summaryShown && (
|
||||
<Button onClick={() => this.setState({ summaryShown: !summaryShown })} color='secondary'>
|
||||
<Button onClick={() => this.setStateSafe({ summaryShown: !summaryShown })} color='secondary'>
|
||||
Show summary
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -238,41 +238,47 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> {
|
|||
this.clearBanner();
|
||||
const pipelineId = this.props.match.params[RouteParams.pipelineId];
|
||||
|
||||
let pipeline: ApiPipeline;
|
||||
let templateResponse: ApiGetTemplateResponse;
|
||||
|
||||
try {
|
||||
const [pipeline, templateResponse] = await Promise.all([
|
||||
Apis.pipelineServiceApi.getPipeline(pipelineId),
|
||||
Apis.pipelineServiceApi.getTemplate(pipelineId)
|
||||
]);
|
||||
|
||||
const template: Workflow = JsYaml.safeLoad(templateResponse.template!);
|
||||
let g: dagre.graphlib.Graph | undefined;
|
||||
try {
|
||||
g = StaticGraphParser.createGraph(template);
|
||||
} catch (err) {
|
||||
await this.showPageError('Error: failed to generate Pipeline graph.', err);
|
||||
}
|
||||
|
||||
const breadcrumbs = [{ displayName: 'Pipelines', href: RoutePage.PIPELINES }];
|
||||
const pageTitle = pipeline.name!;
|
||||
|
||||
const toolbarActions = [...this.props.toolbarProps.actions];
|
||||
toolbarActions[0].disabled = false;
|
||||
this.props.updateToolbar({ breadcrumbs, actions: toolbarActions, pageTitle });
|
||||
|
||||
this.setState({
|
||||
graph: g,
|
||||
pipeline,
|
||||
template,
|
||||
templateYaml: templateResponse.template,
|
||||
});
|
||||
pipeline = await Apis.pipelineServiceApi.getPipeline(pipelineId);
|
||||
} catch (err) {
|
||||
await this.showPageError('Cannot retrieve pipeline details.', err);
|
||||
logger.error('Cannot retrieve pipeline details.', err);
|
||||
return;
|
||||
}
|
||||
catch (err) {
|
||||
await this.showPageError(
|
||||
`Error: failed to retrieve pipeline or template for ID: ${pipelineId}.`,
|
||||
err,
|
||||
);
|
||||
logger.error(`Error loading pipeline or template for ID: ${pipelineId}`, err);
|
||||
|
||||
try {
|
||||
templateResponse = await Apis.pipelineServiceApi.getTemplate(pipelineId);
|
||||
} catch (err) {
|
||||
await this.showPageError('Cannot retrieve pipeline template.', err);
|
||||
logger.error('Cannot retrieve pipeline details.', err);
|
||||
return;
|
||||
}
|
||||
|
||||
let template: Workflow | undefined;
|
||||
let g: dagre.graphlib.Graph | undefined;
|
||||
try {
|
||||
template = JsYaml.safeLoad(templateResponse.template || '{}');
|
||||
g = StaticGraphParser.createGraph(template!);
|
||||
} catch (err) {
|
||||
await this.showPageError('Error: failed to generate Pipeline graph.', err);
|
||||
}
|
||||
|
||||
const breadcrumbs = [{ displayName: 'Pipelines', href: RoutePage.PIPELINES }];
|
||||
const pageTitle = pipeline.name!;
|
||||
|
||||
const toolbarActions = [...this.props.toolbarProps.actions];
|
||||
toolbarActions[0].disabled = false;
|
||||
this.props.updateToolbar({ breadcrumbs, actions: toolbarActions, pageTitle });
|
||||
|
||||
this.setStateSafe({
|
||||
graph: g,
|
||||
pipeline,
|
||||
template,
|
||||
templateYaml: templateResponse.template,
|
||||
});
|
||||
}
|
||||
|
||||
private _createNewExperiment(): void {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,810 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PipelineDetails closes side panel when close button is clicked 1`] = `
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<MD2Tabs
|
||||
onSwitch={[Function]}
|
||||
selectedTab={0}
|
||||
tabs={
|
||||
Array [
|
||||
"Graph",
|
||||
"Source",
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
style={
|
||||
Object {
|
||||
"overflow": "hidden",
|
||||
"position": "relative",
|
||||
}
|
||||
}
|
||||
>
|
||||
<WithStyles(Paper)
|
||||
className="summaryCard"
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "baseline",
|
||||
"display": "flex",
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="header"
|
||||
>
|
||||
Summary
|
||||
</div>
|
||||
<WithStyles(Button)
|
||||
color="secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Hide
|
||||
</WithStyles(Button)>
|
||||
</div>
|
||||
<div
|
||||
className="summaryKey"
|
||||
>
|
||||
Uploaded on
|
||||
</div>
|
||||
<div>
|
||||
9/5/2018, 4:03:02 AM
|
||||
</div>
|
||||
<div
|
||||
className="summaryKey"
|
||||
>
|
||||
Description
|
||||
</div>
|
||||
<div>
|
||||
test job description
|
||||
</div>
|
||||
</WithStyles(Paper)>
|
||||
<Graph
|
||||
graph={
|
||||
Graph {
|
||||
"_defaultEdgeLabelFn": [Function],
|
||||
"_defaultNodeLabelFn": [Function],
|
||||
"_edgeLabels": Object {},
|
||||
"_edgeObjs": Object {},
|
||||
"_in": Object {},
|
||||
"_isCompound": false,
|
||||
"_isDirected": true,
|
||||
"_isMultigraph": false,
|
||||
"_label": undefined,
|
||||
"_nodes": Object {},
|
||||
"_out": Object {},
|
||||
"_preds": Object {},
|
||||
"_sucs": Object {},
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
selectedNodeId=""
|
||||
/>
|
||||
<SidePanel
|
||||
isOpen={false}
|
||||
onClose={[Function]}
|
||||
title=""
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div>
|
||||
Unable to retrieve node info
|
||||
</div>
|
||||
</div>
|
||||
</SidePanel>
|
||||
<div
|
||||
className="footer"
|
||||
>
|
||||
<div
|
||||
className="flex footerInfoOffset"
|
||||
>
|
||||
<pure(InfoOutlinedIcon)
|
||||
style={
|
||||
Object {
|
||||
"color": "#80868b",
|
||||
"height": 16,
|
||||
"width": 16,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span
|
||||
className="infoSpan"
|
||||
>
|
||||
Static pipeline graph
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PipelineDetails collapses summary card when summary shown state is false 1`] = `
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<MD2Tabs
|
||||
onSwitch={[Function]}
|
||||
selectedTab={0}
|
||||
tabs={
|
||||
Array [
|
||||
"Graph",
|
||||
"Source",
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
style={
|
||||
Object {
|
||||
"overflow": "hidden",
|
||||
"position": "relative",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Graph
|
||||
graph={
|
||||
Graph {
|
||||
"_defaultEdgeLabelFn": [Function],
|
||||
"_defaultNodeLabelFn": [Function],
|
||||
"_edgeLabels": Object {},
|
||||
"_edgeObjs": Object {},
|
||||
"_in": Object {},
|
||||
"_isCompound": false,
|
||||
"_isDirected": true,
|
||||
"_isMultigraph": false,
|
||||
"_label": undefined,
|
||||
"_nodes": Object {},
|
||||
"_out": Object {},
|
||||
"_preds": Object {},
|
||||
"_sucs": Object {},
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
selectedNodeId=""
|
||||
/>
|
||||
<SidePanel
|
||||
isOpen={false}
|
||||
onClose={[Function]}
|
||||
title=""
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div>
|
||||
Unable to retrieve node info
|
||||
</div>
|
||||
</div>
|
||||
</SidePanel>
|
||||
<div
|
||||
className="footer"
|
||||
>
|
||||
<WithStyles(Button)
|
||||
color="secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Show summary
|
||||
</WithStyles(Button)>
|
||||
<div
|
||||
className="flex"
|
||||
>
|
||||
<pure(InfoOutlinedIcon)
|
||||
style={
|
||||
Object {
|
||||
"color": "#80868b",
|
||||
"height": 16,
|
||||
"width": 16,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span
|
||||
className="infoSpan"
|
||||
>
|
||||
Static pipeline graph
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PipelineDetails opens side panel on clicked node, shows message when node is not found in graph 1`] = `
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<MD2Tabs
|
||||
onSwitch={[Function]}
|
||||
selectedTab={0}
|
||||
tabs={
|
||||
Array [
|
||||
"Graph",
|
||||
"Source",
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
style={
|
||||
Object {
|
||||
"overflow": "hidden",
|
||||
"position": "relative",
|
||||
}
|
||||
}
|
||||
>
|
||||
<WithStyles(Paper)
|
||||
className="summaryCard"
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "baseline",
|
||||
"display": "flex",
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="header"
|
||||
>
|
||||
Summary
|
||||
</div>
|
||||
<WithStyles(Button)
|
||||
color="secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Hide
|
||||
</WithStyles(Button)>
|
||||
</div>
|
||||
<div
|
||||
className="summaryKey"
|
||||
>
|
||||
Uploaded on
|
||||
</div>
|
||||
<div>
|
||||
9/5/2018, 4:03:02 AM
|
||||
</div>
|
||||
<div
|
||||
className="summaryKey"
|
||||
>
|
||||
Description
|
||||
</div>
|
||||
<div>
|
||||
test job description
|
||||
</div>
|
||||
</WithStyles(Paper)>
|
||||
<Graph
|
||||
graph={
|
||||
Graph {
|
||||
"_defaultEdgeLabelFn": [Function],
|
||||
"_defaultNodeLabelFn": [Function],
|
||||
"_edgeLabels": Object {},
|
||||
"_edgeObjs": Object {},
|
||||
"_in": Object {},
|
||||
"_isCompound": false,
|
||||
"_isDirected": true,
|
||||
"_isMultigraph": false,
|
||||
"_label": undefined,
|
||||
"_nodes": Object {},
|
||||
"_out": Object {},
|
||||
"_preds": Object {},
|
||||
"_sucs": Object {},
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
selectedNodeId="some-node-id"
|
||||
/>
|
||||
<SidePanel
|
||||
isOpen={true}
|
||||
onClose={[Function]}
|
||||
title="some-node-id"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div>
|
||||
Unable to retrieve node info
|
||||
</div>
|
||||
</div>
|
||||
</SidePanel>
|
||||
<div
|
||||
className="footer"
|
||||
>
|
||||
<div
|
||||
className="flex footerInfoOffset"
|
||||
>
|
||||
<pure(InfoOutlinedIcon)
|
||||
style={
|
||||
Object {
|
||||
"color": "#80868b",
|
||||
"height": 16,
|
||||
"width": 16,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span
|
||||
className="infoSpan"
|
||||
>
|
||||
Static pipeline graph
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PipelineDetails shows clicked node info in the side panel if it is in the graph 1`] = `
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<MD2Tabs
|
||||
onSwitch={[Function]}
|
||||
selectedTab={0}
|
||||
tabs={
|
||||
Array [
|
||||
"Graph",
|
||||
"Source",
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
style={
|
||||
Object {
|
||||
"overflow": "hidden",
|
||||
"position": "relative",
|
||||
}
|
||||
}
|
||||
>
|
||||
<WithStyles(Paper)
|
||||
className="summaryCard"
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "baseline",
|
||||
"display": "flex",
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="header"
|
||||
>
|
||||
Summary
|
||||
</div>
|
||||
<WithStyles(Button)
|
||||
color="secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Hide
|
||||
</WithStyles(Button)>
|
||||
</div>
|
||||
<div
|
||||
className="summaryKey"
|
||||
>
|
||||
Uploaded on
|
||||
</div>
|
||||
<div>
|
||||
9/5/2018, 4:03:02 AM
|
||||
</div>
|
||||
<div
|
||||
className="summaryKey"
|
||||
>
|
||||
Description
|
||||
</div>
|
||||
<div>
|
||||
test job description
|
||||
</div>
|
||||
</WithStyles(Paper)>
|
||||
<Graph
|
||||
graph={
|
||||
Graph {
|
||||
"_defaultEdgeLabelFn": [Function],
|
||||
"_defaultNodeLabelFn": [Function],
|
||||
"_edgeLabels": Object {},
|
||||
"_edgeObjs": Object {},
|
||||
"_in": Object {
|
||||
"node1": Object {},
|
||||
},
|
||||
"_isCompound": false,
|
||||
"_isDirected": true,
|
||||
"_isMultigraph": false,
|
||||
"_label": undefined,
|
||||
"_nodeCount": 1,
|
||||
"_nodes": Object {
|
||||
"node1": Object {
|
||||
"info": SelectedNodeInfo {
|
||||
"args": Array [
|
||||
"test arg",
|
||||
"test arg2",
|
||||
],
|
||||
"command": Array [
|
||||
"test command",
|
||||
"test command 2",
|
||||
],
|
||||
"condition": "test condition",
|
||||
"image": "test image",
|
||||
"inputs": Array [
|
||||
Array [
|
||||
"key1",
|
||||
"val1",
|
||||
],
|
||||
Array [
|
||||
"key2",
|
||||
"val2",
|
||||
],
|
||||
],
|
||||
"nodeType": "container",
|
||||
"outputs": Array [
|
||||
Array [
|
||||
"key3",
|
||||
"val3",
|
||||
],
|
||||
Array [
|
||||
"key4",
|
||||
"val4",
|
||||
],
|
||||
],
|
||||
},
|
||||
"label": "node1",
|
||||
},
|
||||
},
|
||||
"_out": Object {
|
||||
"node1": Object {},
|
||||
},
|
||||
"_preds": Object {
|
||||
"node1": Object {},
|
||||
},
|
||||
"_sucs": Object {
|
||||
"node1": Object {},
|
||||
},
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
selectedNodeId="node1"
|
||||
/>
|
||||
<SidePanel
|
||||
isOpen={true}
|
||||
onClose={[Function]}
|
||||
title="node1"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<StaticNodeDetails
|
||||
nodeInfo={
|
||||
SelectedNodeInfo {
|
||||
"args": Array [
|
||||
"test arg",
|
||||
"test arg2",
|
||||
],
|
||||
"command": Array [
|
||||
"test command",
|
||||
"test command 2",
|
||||
],
|
||||
"condition": "test condition",
|
||||
"image": "test image",
|
||||
"inputs": Array [
|
||||
Array [
|
||||
"key1",
|
||||
"val1",
|
||||
],
|
||||
Array [
|
||||
"key2",
|
||||
"val2",
|
||||
],
|
||||
],
|
||||
"nodeType": "container",
|
||||
"outputs": Array [
|
||||
Array [
|
||||
"key3",
|
||||
"val3",
|
||||
],
|
||||
Array [
|
||||
"key4",
|
||||
"val4",
|
||||
],
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SidePanel>
|
||||
<div
|
||||
className="footer"
|
||||
>
|
||||
<div
|
||||
className="flex footerInfoOffset"
|
||||
>
|
||||
<pure(InfoOutlinedIcon)
|
||||
style={
|
||||
Object {
|
||||
"color": "#80868b",
|
||||
"height": 16,
|
||||
"width": 16,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span
|
||||
className="infoSpan"
|
||||
>
|
||||
Static pipeline graph
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PipelineDetails shows empty pipeline details with empty graph 1`] = `
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<MD2Tabs
|
||||
onSwitch={[Function]}
|
||||
selectedTab={0}
|
||||
tabs={
|
||||
Array [
|
||||
"Graph",
|
||||
"Source",
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
style={
|
||||
Object {
|
||||
"overflow": "hidden",
|
||||
"position": "relative",
|
||||
}
|
||||
}
|
||||
>
|
||||
<WithStyles(Paper)
|
||||
className="summaryCard"
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "baseline",
|
||||
"display": "flex",
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="header"
|
||||
>
|
||||
Summary
|
||||
</div>
|
||||
<WithStyles(Button)
|
||||
color="secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Hide
|
||||
</WithStyles(Button)>
|
||||
</div>
|
||||
<div
|
||||
className="summaryKey"
|
||||
>
|
||||
Uploaded on
|
||||
</div>
|
||||
<div>
|
||||
9/5/2018, 4:03:02 AM
|
||||
</div>
|
||||
<div
|
||||
className="summaryKey"
|
||||
>
|
||||
Description
|
||||
</div>
|
||||
<div>
|
||||
test job description
|
||||
</div>
|
||||
</WithStyles(Paper)>
|
||||
<Graph
|
||||
graph={
|
||||
Graph {
|
||||
"_defaultEdgeLabelFn": [Function],
|
||||
"_defaultNodeLabelFn": [Function],
|
||||
"_edgeLabels": Object {},
|
||||
"_edgeObjs": Object {},
|
||||
"_in": Object {},
|
||||
"_isCompound": false,
|
||||
"_isDirected": true,
|
||||
"_isMultigraph": false,
|
||||
"_label": undefined,
|
||||
"_nodes": Object {},
|
||||
"_out": Object {},
|
||||
"_preds": Object {},
|
||||
"_sucs": Object {},
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
selectedNodeId=""
|
||||
/>
|
||||
<SidePanel
|
||||
isOpen={false}
|
||||
onClose={[Function]}
|
||||
title=""
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div>
|
||||
Unable to retrieve node info
|
||||
</div>
|
||||
</div>
|
||||
</SidePanel>
|
||||
<div
|
||||
className="footer"
|
||||
>
|
||||
<div
|
||||
className="flex footerInfoOffset"
|
||||
>
|
||||
<pure(InfoOutlinedIcon)
|
||||
style={
|
||||
Object {
|
||||
"color": "#80868b",
|
||||
"height": 16,
|
||||
"width": 16,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span
|
||||
className="infoSpan"
|
||||
>
|
||||
Static pipeline graph
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PipelineDetails shows empty pipeline details with no graph 1`] = `
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<MD2Tabs
|
||||
onSwitch={[Function]}
|
||||
selectedTab={0}
|
||||
tabs={
|
||||
Array [
|
||||
"Graph",
|
||||
"Source",
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<span
|
||||
style={
|
||||
Object {
|
||||
"margin": "40px auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
No graph to show
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PipelineDetails shows pipeline source code when config tab is clicked 1`] = `
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<MD2Tabs
|
||||
onSwitch={[Function]}
|
||||
selectedTab={1}
|
||||
tabs={
|
||||
Array [
|
||||
"Graph",
|
||||
"Source",
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="page"
|
||||
>
|
||||
<div
|
||||
className="containerCss"
|
||||
>
|
||||
<UnControlled
|
||||
editorDidMount={[Function]}
|
||||
options={
|
||||
Object {
|
||||
"lineNumbers": true,
|
||||
"lineWrapping": true,
|
||||
"mode": "text/yaml",
|
||||
"readOnly": true,
|
||||
"theme": "default",
|
||||
}
|
||||
}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
Loading…
Reference in New Issue