Skip to content

Commit

Permalink
Fix some misc. issues discovered in rc1 (#33)
Browse files Browse the repository at this point in the history
* JENKINS-42788 - hide editor links when no create permission
* JENKINS-42790 - don't show the toast for a new pipeline
* JENKINS-42647 - don't show Github JSON blob
* JENKINS-42735 - encoding issue with branch names when save/run
* JENKINS-42649 - ctrl/cmd + s to open the load/save dialog
* Route tweaks to get to pipeline editor
* Bump blueocean version to 1.0.0-rc1
* JENKINS-42789 - error copy tweaks
  • Loading branch information
kzantow committed Mar 21, 2017
1 parent 24cde23 commit 43c1913
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 245 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Expand Up @@ -25,7 +25,7 @@
<jenkins.version>2.7.1</jenkins.version>
<node.version>6.2.1</node.version>
<npm.version>3.9.3</npm.version>
<blueocean.version>1.0.0-b23</blueocean.version>
<blueocean.version>1.0.0-rc1</blueocean.version>
<declarative.version>1.0</declarative.version>
</properties>

Expand Down
103 changes: 64 additions & 39 deletions src/main/js/EditorPage.jsx
Expand Up @@ -3,7 +3,7 @@ import { Link } from 'react-router';
import Extensions from '@jenkins-cd/js-extensions';
import {
Fetch, getRestUrl, buildPipelineUrl, locationService,
ContentPageHeader, pipelineService, Paths, RunApi, ToastService,
ContentPageHeader, pipelineService, Paths, RunApi,
} from '@jenkins-cd/blueocean-core-js';
import {
Dialog,
Expand All @@ -19,7 +19,8 @@ import pipelineStore from './services/PipelineStore';
import { observer } from 'mobx-react';
import { observable, action } from 'mobx';
import saveApi from './SaveApi';
import { EditorMain } from './components/editor/EditorMain.jsx';
import { EditorMain } from './components/editor/EditorMain';
import { CopyPastePipelineDialog } from './components/editor/CopyPastePipelineDialog';

const Base64 = { encode: (data) => btoa(data), decode: (str) => atob(str) };

Expand Down Expand Up @@ -50,10 +51,9 @@ class SaveDialog extends React.Component {
showError(err, saveRequest) {
const { functions } = this.props;
let errorMessage = err.message ? err.message : (err.errors ? err.errors.map(e => <div>{e.error}</div>) : err);
if (err.responseBody && err.responseBody.message) {
errorMessage = err.responseBody.message;
if (err.responseBody && err.responseBody.message) { // GH JSON is dumped as a string in err.responseBody.message
// error: 409.
if (errorMessage.indexOf('error: 409.') >= 0) {
if (err.responseBody.message.indexOf('error: 409.') >= 0) {
if (this.props.branch !== saveRequest.content.branch) {
errorMessage = ['The branch ', <i>{saveRequest.content.branch}</i>, ' already exists'];
this.setState({ branchError: errorMessage });
Expand Down Expand Up @@ -124,11 +124,18 @@ class PipelineLoader extends React.Component {
this.priorUnload = window.onbeforeunload;
window.onbeforeunload = e => this.routerWillLeave(e);
pipelineStore.addListener(this.pipelineUpdated = p => this.checkForModification());
document.addEventListener("keydown", this.openPipelineScriptDialog = e => {
if (e.keyCode == 83 && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
this.showPipelineScript();
}
}, false);
}

componentWillUnmount() {
window.onbeforeunload = this.priorUnload;
pipelineStore.removeListener(this.pipelineUpdated);
document.removeEventListener('keypress', this.openPipelineScriptDialog);
}

routerWillLeave(e) {
Expand All @@ -155,6 +162,37 @@ class PipelineLoader extends React.Component {
const { organization, pipeline, branch } = this.props.params;
this.opener = locationService.previous;

const makeEmptyPipeline = () => {
// maybe show a dialog the user can choose
// empty or template
pipelineStore.setPipeline({
agent: { type: 'any' },
children: [],
});
};

if (!pipeline) {
makeEmptyPipeline();
return; // no pipeline to load
}

const showLoadingError = err => {
this.showErrorDialog(
<div className="errors">
<div>
There was an error loading the pipeline from the Jenkinsfile in this repository.
Correct the error by editing the Jenkinsfile using the declarative syntax then commit it back to the repository.
</div>
<div>&nbsp;</div>
<div><i>{this.extractErrorMessage(err)}</i></div>
</div>
, {
buttonRow: <button className="btn-primary" onClick={() => this.cancel()}>Go Back</button>,
onClose: () => this.cancel(),
title: 'Error loading Pipeline',
});
};

Fetch.fetchJSON(`${getRestUrl(this.props.params)}scm/content/?branch=${encodeURIComponent(branch)}&path=Jenkinsfile`)
.then( ({ content }) => {
const pipelineScript = Base64.decode(content.base64Data);
Expand All @@ -169,15 +207,7 @@ class PipelineLoader extends React.Component {
this.forceUpdate();
}
} else {
this.showErrorDialog(
<div className="errors">
<div>There was an error loading the pipeline</div>
<div>{err.map(e => <div>{e.error}</div>)}</div>
</div>
, {
buttonRow: <button className="btn-primary" onClick={() => this.cancel()}>Go Back</button>,
onClose: () => this.cancel()
});
showLoadingError(err);
if(err[0].location) {
// revalidate in case something missed it (e.g. create an empty stage then load/save)
pipelineValidator.validate();
Expand All @@ -187,20 +217,9 @@ class PipelineLoader extends React.Component {
})
.catch(err => {
if (err.response.status != 404) {
this.showErrorDialog(err);
showLoadingError(err);
} else {
// maybe show a dialog the user can choose
// empty or template
pipelineStore.setPipeline({
agent: { type: 'any' },
children: [],
});

ToastService.newToast({
style: 'info',
caption: "No pipeline found",
text: "Creating a blank pipeline",
});
makeEmptyPipeline();
}
});

Expand Down Expand Up @@ -234,6 +253,10 @@ class PipelineLoader extends React.Component {
});
}

showPipelineScript() {
this.setState({ dialog: <CopyPastePipelineDialog onClose={() => this.closeDialog()} />});
}

cancel() {
const { organization, pipeline, branch } = this.props.params;
const { router } = this.context;
Expand All @@ -259,11 +282,14 @@ class PipelineLoader extends React.Component {
this.setState({ dialog: null });
}

showErrorDialog(err, { saveRequest, buttonRow, onClose } = {}) {
extractErrorMessage(err) {
let errorMessage = err;
if (err instanceof String || typeof err === 'string') {
errorMessage = err;
}
else if (err instanceof Array || typeof err === 'array') {
errorMessage = err.map(e => <div>{e.error}</div>);
}
else if (err.responseBody && err.responseBody.message) {
// Github error
errorMessage = err.responseBody.message;
Expand All @@ -280,21 +306,20 @@ class PipelineLoader extends React.Component {
else if (err.message) {
errorMessage = err.message;
}
return errorMessage;
}

showErrorDialog(err, { saveRequest, buttonRow, onClose, title } = {}) {
const buttons = buttonRow || [
<button className="btn-primary" onClick={() => this.closeDialog()}>Ok</button>,
];

// this.setState({
// showSaveDialog: false,
// dialog: <Alerts type="Error" title="Error" message={errorMessage} />
// });

this.setState({
showSaveDialog: false,
dialog: (
<Dialog onDismiss={() => onClose ? onClose() : this.closeDialog()} title="Error" className="Dialog--error" buttons={buttons}>
<Dialog onDismiss={() => onClose ? onClose() : this.closeDialog()} title={title || 'Error'} className="Dialog--error" buttons={buttons}>
<div style={{width: '28em'}}>
{errorMessage}
{this.extractErrorMessage(err)}
</div>
</Dialog>
)});
Expand Down Expand Up @@ -342,7 +367,7 @@ class PipelineLoader extends React.Component {
this.lastPipeline = JSON.stringify(convertInternalModelToJson(pipelineStore.pipeline));
// If this is a save on the same branch that already has a Jenkinsfile, just re-run it
if (this.state.sha && branch === body.content.branch) {
RunApi.startRun({ _links: { self: { href: this.href + 'branches/' + branch + '/' }}})
RunApi.startRun({ _links: { self: { href: this.href + 'branches/' + encodeURIComponent(branch) + '/' }}})
.then(() => this.goToActivity())
.catch(err => errorHandler(err, body));//this.showErrorDialog(err));
} else {
Expand All @@ -361,10 +386,10 @@ class PipelineLoader extends React.Component {
}

render() {
const { branch } = this.props.params;
const { pipeline: pipelineName, branch } = this.props.params;
const { pipelineScript } = this.state;
const pipeline = pipelineService.getPipeline(this.href);
const repo = this.props.params.pipeline.split('/')[1];
const repo = pipelineName && pipelineName.split('/')[1];
return (<div className="pipeline-page">
<Extensions.Renderer extensionPoint="pipeline.editor.css"/>
<ContentPageHeader>
Expand All @@ -375,7 +400,7 @@ class PipelineLoader extends React.Component {
</div>
<div className="editor-page-header-controls">
<button className="btn-link inverse" onClick={() => this.cancel()}>Cancel</button>
<button className="btn-primary inverse" onClick={() => this.showSaveDialog()}>Save</button>
{pipelineName && <button className="btn-primary inverse" onClick={() => this.showSaveDialog()}>Save</button>}
</div>
</ContentPageHeader>
{pipelineStore.pipeline &&
Expand Down
4 changes: 1 addition & 3 deletions src/main/js/EditorRoutes.jsx
Expand Up @@ -3,11 +3,9 @@
import React from 'react';
import { Route } from 'react-router';
import { EditorPage } from './EditorPage';
import { EditorPage as FullScreenEditor } from './components/editor/EditorPage';

export default
<Route>
<Route path="/pipeline-editor" component={FullScreenEditor} />
<Route path="/organizations/:organization/pipeline-editor/:pipeline/(:branch)(/)" component={EditorPage} />
<Route path="/organizations/:organization/pipeline-editor/(:pipeline/)(:branch/)" component={EditorPage} />
</Route>
;
7 changes: 6 additions & 1 deletion src/main/js/PipelineEditorLink.jsx
Expand Up @@ -4,6 +4,7 @@ import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { Icon } from '@jenkins-cd/react-material-icons';
import { Fetch, Paths, pipelineService } from '@jenkins-cd/blueocean-core-js';
import Security from './services/Security';

class PipelineEditorLink extends React.Component {
state = {};
Expand All @@ -21,6 +22,10 @@ class PipelineEditorLink extends React.Component {
}

render() {
if (!Security.isCreationEnabled()) {
return null;
}

if (!this.state.supportsSave) {
return <div/>;
}
Expand All @@ -33,7 +38,7 @@ class PipelineEditorLink extends React.Component {
if (!run) {
pipelinePath.splice(-1);
}
const baseUrl = `/organizations/${pipeline.organization}/pipeline-editor/${encodeURIComponent(pipelinePath.join('/'))}/${branch}`;
const baseUrl = `/organizations/${pipeline.organization}/pipeline-editor/${encodeURIComponent(pipelinePath.join('/'))}/${branch}/`;

return (
<Link className="pipeline-editor-link" to={baseUrl}>
Expand Down
82 changes: 82 additions & 0 deletions src/main/js/components/editor/CopyPastePipelineDialog.jsx
@@ -0,0 +1,82 @@
// @flow

import React, { Component, PropTypes } from 'react';
import { Dialog } from '@jenkins-cd/design-language';
import { ContentPageHeader } from '@jenkins-cd/blueocean-core-js';
import pipelineStore from '../../services/PipelineStore';
import { convertInternalModelToJson, convertJsonToPipeline, convertPipelineToJson, convertJsonToInternalModel } from '../../services/PipelineSyntaxConverter';
import type { PipelineInfo } from '../../services/PipelineStore';
import type { PipelineJsonContainer } from '../../services/PipelineSyntaxConverter';
import pipelineValidator from '../../services/PipelineValidator';

const PIPELINE_KEY = 'jenkins.pipeline.editor.workingCopy';

type Props = {
onClose: ?PropTypes.func,
};

type State = {
pipelineScript?: PropTypes.string,
pipelineErrors?: ?PropTypes.string[],
};

type DefaultProps = typeof CopyPastePipelineDialog.defaultProps;

export class CopyPastePipelineDialog extends Component<DefaultProps, Props, State> {
state: State = {};

componentWillMount() {
if (pipelineStore.pipeline) {
const json = convertInternalModelToJson(pipelineStore.pipeline);
convertJsonToPipeline(JSON.stringify(json), (result, err) => {
if (!err) {
this.setState({pipelineErrors: null, pipelineScript: result});
} else {
this.setState({pipelineErrors: err, pipelineScript: ''});
}
});
}
}

updateStateFromPipelineScript(pipeline: string) {
convertPipelineToJson(pipeline, (p, err) => {
if (!err) {
const internal = convertJsonToInternalModel(p);
this.setState({pipelineErrors: null}),
pipelineStore.setPipeline(internal);
this.props.onClose();
} else {
this.setState({pipelineErrors: err});
if(err[0].location) {
// revalidate in case something missed it (e.g. create an empty stage then load/save)
pipelineValidator.validate();
}
}
});
}

render() {
return (
<Dialog className="editor-pipeline-dialog" onDismiss={() => this.props.onClose()}
title="Pipeline Script"
buttons={<div><button onClick={e => this.updateStateFromPipelineScript(this.state.pipelineScript)}>Update</button></div>}>
{this.state.pipelineErrors && !this.state.pipelineErrors[0].location &&
<ul className="pipeline-validation-errors">
{this.state.pipelineErrors.map(err => <li>{err.error}</li>)}
</ul>
}
{this.state.pipelineErrors && this.state.pipelineErrors[0].location &&
<ul className="pipeline-validation-errors">
<li onClick={e => { this.state.pipelineErrors.expand = true; this.forceUpdate(); }}>There were validation errors, please check the editor to correct them</li>
{this.state.pipelineErrors.expand && this.state.pipelineErrors.map(err => <li>{err.location && err.location.join('/')}: {err.error}</li>)}
</ul>
}
<div className="editor-text-area">
<textarea onChange={e => this.setState({ pipelineScript: e.target.value})} style={{width: "100%", minHeight: "30em", height: "100%"}} value={this.state.pipelineScript}/>
</div>
</Dialog>
);
}
}

export default CopyPastePipelineDialog;

0 comments on commit 43c1913

Please sign in to comment.