Skip to content

Commit

Permalink
refactor(core): lazy load execution details (#5141)
Browse files Browse the repository at this point in the history
  • Loading branch information
anotherchrisberry committed Apr 10, 2018
1 parent 8826980 commit ce35bb6
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports = angular.module('spinnaker.canary.acaTask.transformer', []).serv

this.transform = function(application, execution) {
execution.stages.forEach(function(stage) {
if (stage.type === 'acaTask') {
if (stage.type === 'acaTask' && execution.hydrated) {
OrchestratedItemTransformer.defineProperties(stage);
stage.exceptions = [];

Expand Down
2 changes: 2 additions & 0 deletions app/scripts/modules/core/src/domain/IExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface IExecution extends IOrchestratedItem {
trigger: IExecutionTrigger;
user: string;
fromTemplate?: boolean;
hydrated?: boolean;
hydrator?: Promise<IExecution>;
}

export interface IExecutionGroup {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { BindAll, Debounce, Throttle } from 'lodash-decorators';
import { clone, find, flatten, forOwn, groupBy, max, maxBy, sortBy, sum, sumBy, uniq } from 'lodash';
import { Subscription } from 'rxjs';

import { Application } from 'core/application';
import { IExecution, IPipeline } from 'core/domain';
import { IPipelineValidationResults } from 'core/pipeline/config/validation/pipelineConfig.validator';
import { ReactInjector } from 'core/reactShims';
Expand All @@ -24,6 +25,7 @@ import './pipelineGraph.less';

export interface IPipelineGraphProps {
execution?: IExecution;
application?: Application;
onNodeClick: (node: IPipelineGraphNode, subIndex?: number) => void;
pipeline?: IPipeline;
shouldValidate?: boolean;
Expand All @@ -40,6 +42,7 @@ export interface IPipelineGraphState {
nodeRadius: number;
phaseCount: number;
rowHeights: number[];
hydrated: boolean;
}

@BindAll()
Expand All @@ -55,6 +58,7 @@ export class PipelineGraph extends React.Component<IPipelineGraphProps, IPipelin
nodeRadius: this.defaultNodeRadius,
phaseCount: 0,
rowHeights: [],
hydrated: false,
};
private element: JQuery;
private graphStatusHash: string;
Expand All @@ -65,15 +69,17 @@ export class PipelineGraph extends React.Component<IPipelineGraphProps, IPipelin
private rowPadding = 20;
private validationSubscription: Subscription;
private windowResize = this.handleWindowResize.bind(this);
private mounted = false;

constructor(props: IPipelineGraphProps) {
super(props);
this.state = this.defaultState;
const { execution } = props;
this.state = { ...this.defaultState, ...{ hydrated: execution && execution.hydrated } };

// HACK: This is needed to update the node states in the graph based on the stage states.
// Once the execution itself changes based on stage status, this can be removed.
if (this.props.execution) {
this.graphStatusHash = this.props.execution.graphStatusHash;
if (execution) {
this.graphStatusHash = execution.graphStatusHash;
}
}

Expand Down Expand Up @@ -396,6 +402,14 @@ export class PipelineGraph extends React.Component<IPipelineGraphProps, IPipelin

public componentDidMount() {
window.addEventListener('resize', this.windowResize);
this.mounted = true;
if (!this.state.hydrated && this.props.execution) {
ReactInjector.executionService.hydrate(this.props.application, this.props.execution).then(() => {
if (this.mounted) {
this.setState({ hydrated: true });
}
});
}
this.validationSubscription = ReactInjector.pipelineConfigValidator.subscribe(validations => {
this.pipelineValidations = validations;
this.updateGraph(this.props);
Expand Down Expand Up @@ -445,6 +459,7 @@ export class PipelineGraph extends React.Component<IPipelineGraphProps, IPipelin
}

public componentWillUnmount() {
this.mounted = false;
this.validationSubscription.unsubscribe();
window.removeEventListener('resize', this.windowResize);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,53 @@
import * as React from 'react';
import { BindAll } from 'lodash-decorators';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';

import { IExecutionStageLabelComponentProps } from 'core/domain';
import { ExecutionWindowActions } from 'core/pipeline/config/stages/executionWindows/ExecutionWindowActions';
import { HoverablePopover } from 'core/presentation/HoverablePopover';
import { ReactInjector } from 'core/reactShims';
import { Spinner } from 'core/widgets';

export interface IExecutionBarLabelProps extends IExecutionStageLabelComponentProps {
tooltip?: JSX.Element;
}

export class ExecutionBarLabel extends React.Component<IExecutionBarLabelProps> {
export interface IExecutionBarLabelState {
hydrated: boolean;
}

@BindAll()
export class ExecutionBarLabel extends React.Component<IExecutionBarLabelProps, IExecutionBarLabelState> {

private mounted = false;

constructor(props: IExecutionBarLabelProps) {
super(props);
this.state = {
hydrated: props.execution && props.execution.hydrated
};
}

private hydrate(): void {
const { execution, application } = this.props;
if (!execution) {
return;
}
ReactInjector.executionService.hydrate(application, execution).then(() => {
if (this.mounted) {
this.setState({ hydrated: true });
}
});
}

public componentDidMount() {
this.mounted = true;
}

public componentWillUnmount() {
this.mounted = false;
}

public render() {
const { stage, application, execution, executionMarker } = this.props;
const inSuspendedExecutionWindow = stage.inSuspendedExecutionWindow;
Expand All @@ -28,6 +65,16 @@ export class ExecutionBarLabel extends React.Component<IExecutionBarLabelProps>
}
if (executionMarker) {
const LabelComponent = stage.labelComponent;
if (LabelComponent !== ExecutionBarLabel && !this.state.hydrated) {
const loadingTooltip = (<Tooltip id={stage.id}><Spinner size="small"/></Tooltip>);
return (
<span onMouseEnter={this.hydrate}>
<OverlayTrigger placement="top" overlay={loadingTooltip}>
{this.props.children}
</OverlayTrigger>
</span>
);
}
const tooltip = (
<Tooltip id={stage.id}>
<LabelComponent application={application} execution={execution} stage={stage} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ export class Execution extends React.Component<IExecutionProps, IExecutionState>
</div>
{showingDetails && (
<div className="execution-graph">
<PipelineGraph execution={execution} onNodeClick={this.handleNodeClick} viewState={viewState} />
<PipelineGraph execution={execution} application={application} onNodeClick={this.handleNodeClick} viewState={viewState} />
</div>
)}
{showingDetails && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as React from 'react';
import * as ReactGA from 'react-ga';
import { BindAll } from 'lodash-decorators';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';

import { IExecution, IExecutionStageSummary } from 'core/domain';
import { ReactInjector } from 'core/reactShims';
import { Spinner } from 'core/widgets';
import { OrchestratedItemRunningTime } from './OrchestratedItemRunningTime';
import { duration } from 'core/utils/timeFormatters';

Expand All @@ -23,21 +26,36 @@ export interface IExecutionMarkerProps {

export interface IExecutionMarkerState {
duration: string;
hydrated: boolean;
}

@BindAll()
export class ExecutionMarker extends React.Component<IExecutionMarkerProps, IExecutionMarkerState> {
private runningTime: OrchestratedItemRunningTime;
private mounted = false;

constructor(props: IExecutionMarkerProps) {
super(props);

const { stage, execution } = props;

this.state = {
duration: duration(props.stage.runningTimeInMs),
duration: duration(stage.runningTimeInMs),
hydrated: execution.hydrated,
};
}

private hydrate(): void {
const { execution, application } = this.props;
ReactInjector.executionService.hydrate(application, execution).then(() => {
if (this.mounted) {
this.setState({ hydrated: true });
}
});
}

public componentDidMount() {
this.mounted = true;
this.runningTime = new OrchestratedItemRunningTime(this.props.stage, (time: number) =>
this.setState({ duration: duration(time) }),
);
Expand All @@ -48,6 +66,7 @@ export class ExecutionMarker extends React.Component<IExecutionMarkerProps, IExe
}

public componentWillUnmount() {
this.mounted = false;
this.runningTime.reset();
}

Expand Down Expand Up @@ -79,11 +98,22 @@ export class ExecutionMarker extends React.Component<IExecutionMarkerProps, IExe
</div>
);
if (stage.useCustomTooltip) {
return (
<TooltipComponent application={application} execution={execution} stage={stage} executionMarker={true}>
{stageContents}
</TooltipComponent>
);
if (execution.hydrated) {
return (
<TooltipComponent application={application} execution={execution} stage={stage} executionMarker={true}>
{stageContents}
</TooltipComponent>
);
} else {
const loadingTooltip = (<Tooltip id={stage.id}><Spinner size="small"/></Tooltip>);
return (
<span onMouseEnter={this.hydrate}>
<OverlayTrigger placement="top" overlay={loadingTooltip}>
{this.props.children}
</OverlayTrigger>
</span>
);
}
}
return (
<ExecutionBarLabel application={application} execution={execution} stage={stage} executionMarker={true}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,6 @@ export class ExecutionGroup extends React.Component<IExecutionGroupProps, IExecu
}

private getDeploymentAccounts(): string[] {
return uniq(flatten<string>(this.props.group.executions.map((e: IExecution) => e.deploymentTargets))).sort();
return uniq(flatten<string>(this.props.group.executions.map((e: IExecution) => e.deploymentTargets))).sort().filter(a => !!a);
}
}
69 changes: 54 additions & 15 deletions app/scripts/modules/core/src/pipeline/service/execution.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class ExecutionService {
}

public getRunningExecutions(applicationName: string): IPromise<IExecution[]> {
return this.getFilteredExecutions(applicationName, this.activeStatuses, this.runningLimit);
return this.getFilteredExecutions(applicationName, this.activeStatuses, this.runningLimit, null, true);
}

private getFilteredExecutions(
Expand All @@ -56,7 +56,22 @@ export class ExecutionService {

return call.then((data: IExecution[]) => {
if (data) {
data.forEach((execution: IExecution) => this.cleanExecutionForDiffing(execution));
data.forEach((execution: IExecution) => {
execution.hydrated = expand;
// TODO: remove this code once the filtering takes place on Orca
if (!expand) {
execution.stages.forEach(s => {
s.context = {};
s.outputs = {};
s.tasks = [];
});
}
// TODO: Remove this, too, once the filtering takes place on Orca
if (execution.trigger.parentExecution) {
execution.trigger.parentExecution.stages = [];
}
return this.cleanExecutionForDiffing(execution);
});
return data;
}
return [];
Expand All @@ -68,6 +83,8 @@ export class ExecutionService {
* @param {string} applicationName the name of the application
* @param {Application} application: if supplied, and pipeline parameters are present on the filter model, the
* application will be used to correlate and filter the retrieved executions to only include those pipelines
* @param {boolean} expand: if true, the resulting executions will include fully hydrated context, outputs, and tasks
* fields
* @return {<IExecution[]>}
*/
public getExecutions(
Expand All @@ -91,6 +108,7 @@ export class ExecutionService {
return this.API.one('pipelines', executionId)
.get()
.then((execution: IExecution) => {
execution.hydrated = true;
this.cleanExecutionForDiffing(execution);
return execution;
});
Expand Down Expand Up @@ -131,12 +149,9 @@ export class ExecutionService {

private cleanExecutionForDiffing(execution: IExecution): void {
(execution.stages || []).forEach((stage: IExecutionStage) => this.removeInstances(stage));
if (execution.trigger && execution.trigger.parentExecution) {
(execution.trigger.parentExecution.stages || []).forEach((stage: IExecutionStage) => this.removeInstances(stage));
}
}

public toggleDetails(execution: IExecution, stageIndex: number, subIndex: number) {
public toggleDetails(execution: IExecution, stageIndex: number, subIndex: number): void {
const standalone = this.$state.current.name.endsWith('.executionDetails.execution');

if (
Expand Down Expand Up @@ -203,15 +218,18 @@ export class ExecutionService {

// remove these fields - they are not of interest when determining if the pipeline has changed
private jsonReplacer(key: string, value: any): any {
if (
key === 'instances' ||
key === 'asg' ||
key === 'commits' ||
key === 'history' ||
key === '$$hashKey' ||
key === 'requisiteIds' ||
key === 'requisiteStageRefIds'
) {
const ignored = [
'asg',
'commits',
'history',
'hydrator',
'hydrated',
'instances',
'requisiteIds',
'requisiteStageRefIds',
'$$hashKey',
];
if (ignored.includes(key)) {
return undefined;
}
return value;
Expand Down Expand Up @@ -480,6 +498,27 @@ export class ExecutionService {
}
}

public hydrate(application: Application, unhydrated: IExecution): Promise<IExecution> {
if (unhydrated.hydrator) {
return unhydrated.hydrator;
}
const executionHydrator = this.getExecution(unhydrated.id).then(hydrated => {
this.transformExecution(application, hydrated);
hydrated.stages.forEach(s => {
const toHydrate = unhydrated.stages.find(s2 => s2.id === s.id);
if (toHydrate) {
toHydrate.context = s.context;
toHydrate.outputs = s.outputs;
toHydrate.tasks = s.tasks;
}
});
unhydrated.hydrated = true;
return unhydrated;
});
unhydrated.hydrator = Promise.resolve(executionHydrator);
return unhydrated.hydrator;
}

public getLastExecutionForApplicationByConfigId(appName: string, configId: string): IPromise<IExecution> {
return this.getFilteredExecutions(appName, [], 1)
.then((executions: IExecution[]) => {
Expand Down

0 comments on commit ce35bb6

Please sign in to comment.