Skip to content

Commit

Permalink
Feature/jenkins 38595 create pipeline scm list (#572)
Browse files Browse the repository at this point in the history
* [JENKINS-38594] wire up new "create-pipeline" route; stub out directory for new "Creation" submodule

* [JENKINS-38594] update CreatePipeline to use a placeholder dialog until component is ready; integrate with existing router / background code so the existing screen's DOM will display under the dialog

* [JENKINS-38594] refine logic for handling background for enter/leave of create-pipeline

* [JENKINS-38595] enhance ExtensionRenderer so that extensions can be force reloaded / re-rendered

* [JENKINS-38595] tick up js-ext deps

* [JENKINS-38595] visual components for multi-step flows

* [JENKINS-38595] ExtensionPoint APIs for contributing to SCM provider list and workflow steps in Create Pipeline

* [JENKINS-38594] delint

* [JENKINS-38594] code refactoring to improve comprehensibility

* [JENKINS-38595] visual refinements to step indicators

* [JENKINS-38595] quick and dirty version of Git creation flow to test out new MultiStepFlow / FlowStep APIs

* [JENKINS-38594] add some security util methods; add some defense against NPE in config

* [JENKINS-38594] only show the "New Pipeline" link if the user has permission

* [JENKINS-38595] simplify down the extension point API to use a single XP; lay the groundwork for "re-entrant" flow; still needs to sandbox rendered content

* [JENKINS-38595] extract the "sandboxing" of rendering (via ReactDOM.render) so that we can have more flexibility with how we render untrusted React code

* [JENKINS-38595] add proper error handling and sandboxed rendering to the extension points in Create Pipeline flow

* [JENKINS-38595] fix a bug where the "activeStep" was falling out of sync w/ props.children and yielding incorrect statuses for steps; now we just track the current step index which is much simpler anyways

* [JENKINS-38595] add support for "percent complete" for steps

* [JENKINS-38595] add a "GitCreationManager" to centralize logic that needs sharing between the two steps; enhance "CompletedStep" to display its progress via percent complete

* [JENKINS-38595] delint

* [JENKINS-38595] fix failing test

* [JENKINS-38595] comments and renaming

* [JENKINS-38595] add support for className on SandboxedComponent; eliminate duplicate ContextBridge; update deps after beta publish

* [JENKINS-38595] CSS tweak

* [JENKINS-38595] more comments

* [JENKINS-38595] rename for clarity

* [JENKINS-38594] add a "blueCreate" query string switch to turn on new UI conditionally; by defaul the Jenkins classic UI will be used

* [JENKINS-38594] fix accidental disabling of hibernate page refresh

* [JENKINS-38595] republish merged js-extensions to fix missing "SandboxedComponent" error

* [JENKINS-38595] remove the temp dialog and use a full-screen Dialog instead
  • Loading branch information
cliffmeyers authored Nov 23, 2016
1 parent e8b7e06 commit 1a576d3
Show file tree
Hide file tree
Showing 40 changed files with 2,870 additions and 91 deletions.
819 changes: 816 additions & 3 deletions blueocean-dashboard/npm-shrinkwrap.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion blueocean-dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"dependencies": {
"@jenkins-cd/blueocean-core-js": "0.0.26",
"@jenkins-cd/design-language": "0.0.88",
"@jenkins-cd/js-extensions": "0.0.30",
"@jenkins-cd/js-extensions": "0.0.31-sandbox1",
"@jenkins-cd/js-modules": "0.0.8",
"@jenkins-cd/sse-gateway": "0.0.9",
"babel-plugin-transform-decorators-legacy": "1.3.4",
Expand Down
16 changes: 2 additions & 14 deletions blueocean-dashboard/src/main/js/PipelineRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,24 +83,12 @@ function isLeavingRunDetails(prevState, nextState) {
return (prevState !== null && prevState.params.runId) && !nextState.params.runId;
}

function isEnteringCreatePipeline(prevState, nextState) {
return nextState.location.pathname.indexOf('/create-pipeline') !== -1 &&
(prevState == null || prevState.location.pathname.indexOf('/create-pipeline') === -1);
}

function isLeavingCreatePipeline(prevState, nextState) {
return nextState.location.pathname.indexOf('/create-pipeline') === -1 &&
(prevState == null || prevState.location.pathname.indexOf('/create-pipeline') !== -1);
}

function isPersistBackgroundRoute(prevState, nextState) {
return isEnteringRunDetails(prevState, nextState) ||
isEnteringCreatePipeline(prevState, nextState);
return isEnteringRunDetails(prevState, nextState);
}

function isRemovePersistedBackgroundRoute(prevState, nextState) {
return isLeavingRunDetails(prevState, nextState) ||
isLeavingCreatePipeline(prevState, nextState);
return isLeavingRunDetails(prevState, nextState);
}

/**
Expand Down
92 changes: 67 additions & 25 deletions blueocean-dashboard/src/main/js/creation/CreatePipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,86 @@
* Created by cmeyers on 10/17/16.
*/
import React, { PropTypes } from 'react';
import { BasicDialog, DialogContent } from '@jenkins-cd/design-language';
import { Icon } from 'react-material-icons-blue';

// temporary component until JDL Dialog is ready
function DialogPlaceholder(props) {
function closeHandler() {
if (props.onClose) {
props.onClose();
}
import { CreatePipelineScmListRenderer } from './CreatePipelineScmListRenderer';
import { CreatePipelineStepsRenderer } from './CreatePipelineStepsRenderer';
import VerticalStep from './VerticalStep';

export default class CreatePipeline extends React.Component {

constructor(props) {
super(props);

this.state = {
selectedProvider: null,
};
}

return (
<div style={{ position: 'fixed', zIndex: 50, top: 50, left: 100, right: 100, bottom: 50, background: '#fff', border: '1px solid black' }}>
<div style={{ position: 'absolute', zIndex: 100, top: 0, left: 0, right: 0, bottom: 0 }}>
{props.children}
</div>
<a style={{ position: 'absolute', zIndex: 100, top: 10, right: 10, cursor: 'pointer' }} onClick={closeHandler}>CLOSE</a>
</div>
);
}
_onSelection(selectedProvider) {
this.setState({
selectedProvider,
});
}

DialogPlaceholder.propTypes = {
children: PropTypes.node,
onClose: PropTypes.function,
};
_onCompleteFlow() {
this._onExit();
}

export class CreatePipeline extends React.Component {
_exit() {
this.context.router.goBack();
_onExit() {
if (history && history.length > 2) {
this.context.router.goBack();
} else {
this.context.router.push('/pipelines');
}
}

render() {
const firstStepStatus = this.state.selectedProvider ? 'complete' : 'active';

return (
<DialogPlaceholder onClose={() => this._exit()}>
Create Pipeline
</DialogPlaceholder>
<BasicDialog
className="creation-dialog"
onDismiss={() => this._onExit()}
ignoreEscapeKey
>
<CustomHeader onClose={() => this._onExit()} />
<DialogContent>
<VerticalStep className="first-step" status={firstStepStatus}>
<h1>Where do you store your code?</h1>

<CreatePipelineScmListRenderer
extensionPoint="jenkins.pipeline.create.scm.provider"
onSelection={(provider) => this._onSelection(provider)}
/>
</VerticalStep>

<CreatePipelineStepsRenderer
selectedProvider={this.state.selectedProvider}
onCompleteFlow={() => this._onCompleteFlow()}
/>
</DialogContent>
</BasicDialog>
);
}
}

CreatePipeline.contextTypes = {
router: PropTypes.object,
};

function CustomHeader(props) {
return (
<div className="Dialog-header creation-header">
<h3>Create Pipeline</h3>
<a className="close-button" href="#" onClick={props.onClose}>
<Icon icon="close" size={42} />
</a>
</div>
);
}

CustomHeader.propTypes = {
onClose: PropTypes.func,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Created by cmeyers on 10/17/16.
*/
import React, { PropTypes } from 'react';
import Extensions from '@jenkins-cd/js-extensions';

const Sandbox = Extensions.SandboxedComponent;

/**
* Displays the initial set of options to begin a creation flow from a SCM Provider.
*/
export class CreatePipelineScmListRenderer extends React.Component {

constructor(props) {
super(props);

this.state = {
providers: [],
};
}

componentWillMount() {
this._initialize();
}

_initialize() {
// load and store the SCM providers that contributed the specified extension point
Extensions.store.getExtensions(this.props.extensionPoint, (extensions) => {
let providers = extensions.map(Provider => {
try {
return new Provider();
} catch (error) {
console.warn('error initializing ScmProvider', Provider, error);
return null;
}
});

providers = providers.filter(provider => !!provider);

this.setState({
providers,
});
});
}

_onSelection(provider) {
if (this.props.onSelection) {
this.props.onSelection(provider);
}
}

render() {
return (
<div className="scm-provider-list">
{ this.state.providers.map(provider => {
let defaultOption;

try {
defaultOption = provider.getDefaultOption();
} catch (error) {
console.warn('error invoking getDefaultOption for Provider', provider, error);
return Extensions.ErrorUtils.errorToElement(error);
}

const props = {
onSelect: () => this._onSelection(provider),
};

return (
<Sandbox className="provider-button">
{React.cloneElement(defaultOption, props)}
</Sandbox>
);
})}
</div>
);
}
}

CreatePipelineScmListRenderer.propTypes = {
extensionPoint: PropTypes.string,
onSelection: PropTypes.func,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Created by cmeyers on 10/17/16.
*/
import React, { PropTypes } from 'react';
import Extensions from '@jenkins-cd/js-extensions';
import VerticalStep from './VerticalStep';

const Sandbox = Extensions.SandboxedComponent;

/**
* Displays the current steps based on the selection in the SCM provider list.
*/
export class CreatePipelineStepsRenderer extends React.Component {

shouldComponentUpdate(nextProps) {
return this.props.selectedProvider !== nextProps.selectedProvider;
}

render() {
if (!this.props.selectedProvider) {
return (
<VerticalStep className="last-step">
<h1>Completed</h1>
</VerticalStep>
);
}

const props = {
onCompleteFlow: this.props.onCompleteFlow,
};

let creationFlow;

try {
creationFlow = this.props.selectedProvider.getCreationFlow();
} catch (error) {
console.warn('Error rendering:', this.props.selectedProvider, error);
return Extensions.ErrorUtils.errorToElement(error);
}

return (
<Sandbox>
{React.cloneElement(creationFlow, props)}
</Sandbox>
);
}
}

CreatePipelineStepsRenderer.propTypes = {
selectedProvider: PropTypes.object,
onCompleteFlow: PropTypes.func,
};
38 changes: 38 additions & 0 deletions blueocean-dashboard/src/main/js/creation/FlowStep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Created by cmeyers on 10/19/16.
*/
import React, { PropTypes } from 'react';
import VerticalStep from './VerticalStep';
import status from './FlowStepStatus';

/**
* Visual/logic component that defines an individual step of a multi-step workflow.
* Intended to be used within a MultiStepFlow component.
* Hides all content except for the title until the step becomes active.
* Complete the step by calling 'props.onCompleteStep'; complete entire flow by calling 'props.onCompleteFlow'
*/
export default function FlowStep(props) {
return (
<VerticalStep
status={props.status}
percentage={props.percentage}
isLastStep={props.isLastStep}
>
<h1>{props.title}</h1>
{
props.status !== status.INCOMPLETE &&
props.children
}
</VerticalStep>
);
}

FlowStep.propTypes = {
children: PropTypes.node,
title: PropTypes.string,
status: PropTypes.string,
percentage: PropTypes.number,
isLastStep: PropTypes.bool,
onCompleteStep: PropTypes.func,
onCompleteFlow: PropTypes.func,
};
16 changes: 16 additions & 0 deletions blueocean-dashboard/src/main/js/creation/FlowStepStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Created by cmeyers on 10/19/16.
*/

/**
* Valid statuses for a FlowStep.
*/
const status = {
ACTIVE: 'active',
COMPLETE: 'complete',
INCOMPLETE: 'incomplete',

values: () => [status.ACTIVE, status.COMPLETE, status.INCOMPLETE],
};

export default status;
Loading

0 comments on commit 1a576d3

Please sign in to comment.