Skip to content

Commit

Permalink
feat(pipelines): allow user actions via popovers (#3573)
Browse files Browse the repository at this point in the history
  • Loading branch information
anotherchrisberry committed Apr 23, 2017
1 parent b9ba12c commit 4b57469
Show file tree
Hide file tree
Showing 27 changed files with 454 additions and 134 deletions.
2 changes: 2 additions & 0 deletions app/scripts/modules/core/delivery/delivery.module.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {DELIVERY_STATES} from './delivery.states';
import {EXECUTION_BUILD_NUMBER_COMPONENT} from './executionBuild/executionBuildNumber.component';
import {EXECUTION_STATUS_COMPONENT} from './status/executionStatus.component';
import {EXECUTION_DETAILS_COMPONENT} from './details/executionDetails.component';
import {EXECUTION_COMPONENT} from './executionGroup/execution/execution.component';

module.exports = angular.module('spinnaker.delivery', [

require('./details/executionDetails.controller.js'),
require('./details/singleExecutionDetails.controller.js'),
EXECUTION_COMPONENT,
EXECUTION_DETAILS_COMPONENT,
require('./details/executionDetailsSectionNav.directive.js'),

Expand Down
23 changes: 22 additions & 1 deletion app/scripts/modules/core/delivery/details/executionDetails.less
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@import "../../presentation/less/imports/commonImports.less";

.execution {
.execution-details {
execution-details, .execution-details {
width: 100%;
}
.execution-details-container {
Expand Down Expand Up @@ -159,3 +159,24 @@
}
}
}

.execution-details {
.action-buttons {
margin: 10px -10px 0;
button {
margin: 0 10px;
}
}
}

.popover-content {
.action-buttons {
display: flex;
align-items: stretch;
margin: 10px -5px 0;
button {
flex: 1 0 auto;
margin: 0 5px;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class Execution extends React.Component<IExecutionProps, IExecutionState>
viewState: newViewState,
showingDetails: this.invalidateShowingDetails()
});
};
}

private invalidateShowingDetails(): boolean {
return (this.props.standalone === true || (this.props.execution.id === $stateParams.executionId &&
Expand Down Expand Up @@ -161,7 +161,7 @@ export class Execution extends React.Component<IExecutionProps, IExecutionState>
}
})
});
};
}

public cancelExecution(): void {
const hasDeployStage = this.props.execution.stages && this.props.execution.stages.some(stage => stage.type === 'deploy' || stage.type === 'cloneServerGroup');
Expand Down Expand Up @@ -264,9 +264,12 @@ export class Execution extends React.Component<IExecutionProps, IExecutionState>
const executionMarkerWidth = `${100 / this.props.execution.stageSummaries.length}%`;
const executionMarkers = this.props.execution.stageSummaries.map((stage) => (
<ExecutionMarker key={stage.refId}
application={this.props.application}
execution={this.props.execution}
stage={stage}
onClick={this.toggleDetails}
active={this.isActive(stage.index)}
previousStageActive={this.isActive(stage.index - 1)}
width={executionMarkerWidth}/>
));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@ import * as React from 'react';
import * as ReactGA from 'react-ga';
import autoBindMethods from 'class-autobind-decorator';

import { IExecutionStageSummary } from 'core/domain/IExecutionStage';
import { IExecution, IExecutionStageSummary } from 'core/domain';
import { OrchestratedItemRunningTime } from './OrchestratedItemRunningTime';
import { Tooltip } from 'core/presentation/Tooltip';
import { duration } from 'core/utils/timeFormatters';

import { Application } from 'core/application/application.model';
import { ExecutionBarLabel } from 'core/pipeline/config/stages/core/ExecutionBarLabel';

import './executionMarker.less';

interface IExecutionMarkerProps {
stage: IExecutionStageSummary;
application: Application;
execution: IExecution;
active?: boolean;
previousStageActive?: boolean;
width: string;
onClick: (stageIndex: number) => void;
}

interface IExecutionMarkerState {
runningTimeInMs: number;
duration: string;
}

@autoBindMethods
Expand All @@ -28,12 +33,12 @@ export class ExecutionMarker extends React.Component<IExecutionMarkerProps, IExe
super(props);

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

public componentDidMount() {
this.runningTime = new OrchestratedItemRunningTime(this.props.stage, (time: number) => this.setState({ runningTimeInMs: time }));
this.runningTime = new OrchestratedItemRunningTime(this.props.stage, (time: number) => this.setState({ duration: duration(time) }));
}

public componentWillReceiveProps() {
Expand All @@ -50,25 +55,37 @@ export class ExecutionMarker extends React.Component<IExecutionMarkerProps, IExe
}

public render() {
const stage = this.props.stage;
const {stage, application, execution, active, previousStageActive, width} = this.props;
const markerClassName = [
'clickable',
'stage',
'execution-marker',
`stage-type-${stage.type.toLowerCase()}`,
`execution-marker-${stage.status.toLowerCase()}`,
this.props.active ? 'active' : '',
active ? 'active' : '',
previousStageActive ? 'after-active' : '',
stage.isRunning ? 'glowing' : ''
].join(' ');

const TooltipTemplate = stage.labelTemplate;
const TooltipComponent = stage.labelTemplate;
const MarkerIcon = stage.markerIcon;
const stageContents = (
<div className={markerClassName}
style={{width: width, backgroundColor: stage.color}}
onClick={this.handleStageClick}>
<MarkerIcon stage={stage}/>
<span className="duration">{this.state.duration}</span>
</div>);
if (stage.useCustomTooltip) {
return (
<TooltipComponent application={application} execution={execution} stage={stage} executionMarker={true}>
{stageContents}
</TooltipComponent>
);
}
return (
<Tooltip key={stage.refId} template={(<TooltipTemplate stage={stage}/>)}>
<div className={markerClassName}
style={{width: this.props.width, backgroundColor: stage.color}}
onClick={this.handleStageClick}>
<span className="duration">{duration(stage.runningTimeInMs)}</span>
</div>
</Tooltip>);
<ExecutionBarLabel application={application} execution={execution} stage={stage} executionMarker={true}>
{stageContents}
</ExecutionBarLabel>);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,15 @@ execution-status {
width: calc(~"100% - 235px");
vertical-align: top;
.stage {
.fa { // .fa is "font-awesome" - the only icon set we're using for custom stage icons
display: inline-block;
margin-right: 3px;
}
.duration {
font-weight: 600;
line-height: 20px;
visibility: hidden;
width: 0;
display: inline-block;
}
}
Expand All @@ -106,6 +111,7 @@ execution-status {
.stage {
.duration {
visibility: visible;
width: auto;
}
min-width: 50px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
&:hover, &.active {
opacity: 1;
}
&:first-child {
border-left-width: 0;
&.after-active {
border-left-color: transparent;
}
&.active {
border-color: #222222;
Expand All @@ -26,9 +26,6 @@
&:last-child {
border-right-width: 1px;
}
& + .stage {
border-left-color: transparent;
}
}
&.glowing {
.glowing(2.5);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import _ from 'lodash';
let angular = require('angular');

import {ExecutionBarLabel} from 'core/pipeline/config/stages/core/ExecutionBarLabel';
import {ExecutionMarkerIcon} from 'core/pipeline/config/stages/core/ExecutionMarkerIcon';
import {ORCHESTRATED_ITEM_TRANSFORMER} from 'core/orchestratedItem/orchestratedItem.transformer';
import {PIPELINE_CONFIG_PROVIDER} from 'core/pipeline/config/pipelineConfigProvider';

Expand Down Expand Up @@ -275,6 +276,8 @@ module.exports = angular.module('spinnaker.core.delivery.executionTransformer.se
var stageConfig = pipelineConfig.getStageConfig(stage);
if (stageConfig) {
stage.labelTemplate = stageConfig.executionLabelTemplate || ExecutionBarLabel;
stage.markerIcon = stageConfig.markerIcon || ExecutionMarkerIcon;
stage.useCustomTooltip = !!stageConfig.useCustomTooltip;
stage.extraLabelLines = stageConfig.extraLabelLines;
}
}
Expand Down Expand Up @@ -324,6 +327,7 @@ module.exports = angular.module('spinnaker.core.delivery.executionTransformer.se
summary.masterStageIndex = summary.stages.includes(summary.masterStage) ? summary.stages.indexOf(summary.masterStage) : 0;
filterStages(summary);
setFirstActiveStage(summary);
setExecutionWindow(summary);
transformStage(summary);
styleStage(summary);
orchestratedItemTransformer.defineProperties(summary);
Expand All @@ -336,6 +340,12 @@ module.exports = angular.module('spinnaker.core.delivery.executionTransformer.se
}
}

function setExecutionWindow(summary) {
if (summary.stages.some(s => s.type === 'restrictExecutionDuringTimeWindow' && s.isSuspended)) {
summary.inSuspendedExecutionWindow = true;
}
}

function setFirstActiveStage(summary) {
summary.firstActiveStage = 0;
let steps = summary.stages || [];
Expand Down
10 changes: 8 additions & 2 deletions app/scripts/modules/core/domain/IExecutionStage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {IOrchestratedItem} from './IOrchestratedItem';
import {IStage} from './IStage';
import {IStageStep} from './IStageStep';
import {Application} from '../application/application.model';
import {IExecution} from './IExecution';

export interface IRestartDetails {
restartedBy: string;
Expand All @@ -22,9 +24,13 @@ export interface IExecutionStage extends IOrchestratedItem, IStage {
}

export interface IExecutionStageSummary extends IExecutionStage {
masterStage: IStage;
labelTemplate: React.ComponentClass<{ stage: IExecutionStageSummary }>;
masterStage: IExecutionStage;
stages: IExecutionStage[];
labelTemplate: React.ComponentClass<{ stage: IExecutionStageSummary, application?: Application, execution?: IExecution, executionMarker?: boolean }>;
markerIcon: React.ComponentClass<{ stage: IExecutionStageSummary }>;
extraLabelLines?: (stage: IExecutionStageSummary) => number;
useCustomTooltip?: boolean;
inSuspendedExecutionWindow?: boolean;
index: number;
status: string;
hasNotStarted: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@
import * as React from 'react';
import {IExecutionStageSummary} from 'core/domain/index';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';

export class ExecutionBarLabel extends React.Component<{ stage: IExecutionStageSummary }, any> {
import { IExecutionStageSummary, IExecution } from 'core/domain';
import { ExecutionWindowActions } from 'core/pipeline/config/stages/executionWindows/ExecutionWindowActions';
import { Application } from 'core/application/application.model';
import { HoverablePopover } from 'core/presentation/HoverablePopover';

export interface IExecutionBarLabelProps {
application: Application;
execution: IExecution;
stage: IExecutionStageSummary;
tooltip?: JSX.Element;
executionMarker: boolean;
}

export class ExecutionBarLabel extends React.Component<IExecutionBarLabelProps, any> {
public render() {
const { stage, application, execution, executionMarker } = this.props;
const inSuspendedExecutionWindow = stage.inSuspendedExecutionWindow;
if (inSuspendedExecutionWindow && executionMarker) {
const executionWindowStage = stage.stages.find(s => s.type === 'restrictExecutionDuringTimeWindow');
const template = (
<div>
<div><b>{stage.name}</b> (waiting for execution window)</div>
<ExecutionWindowActions application={application} execution={execution} stage={executionWindowStage}/>
</div>
);
return (
<HoverablePopover template={template}>
{this.props.children}
</HoverablePopover>
);
}
if (executionMarker) {
const tooltip = (
<Tooltip id={stage.id}>
{stage.name}
{inSuspendedExecutionWindow && (<div>(waiting for execution window)</div>)}
</Tooltip>
);
return (
<OverlayTrigger placement="top" overlay={tooltip}>
{this.props.children}
</OverlayTrigger>
);
}
return (
<span>{ this.props.stage.name ? this.props.stage.name : this.props.stage.type }</span>
);
<span>{ stage.name ? stage.name : stage.type }</span>
)
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from 'react';
import {IExecutionStageSummary} from 'core/domain/IExecutionStage';

export interface IExecutionMarkerIconProps {
stage: IExecutionStageSummary;
}

export class ExecutionMarkerIcon extends React.Component<IExecutionMarkerIconProps, any> {
public render() {
return this.props.stage.inSuspendedExecutionWindow ? (<span className="fa fa-clock-o"/>) : null;
}
}
Loading

0 comments on commit 4b57469

Please sign in to comment.