Merge remote-tracking branch 'origin/master' into feature/UX-159
This commit is contained in:
commit
25c675e470
|
@ -1,4 +1,5 @@
|
|||
This is BlueOcean repo. It is a multi-module maven project. Each sub-directory at the root of the repo is jenkins extension.
|
||||
This is the BlueOcean repo. It is a multi-module maven project. Each sub-directory at the root of the repo is jenkins extension.
|
||||
|
||||
|
||||
|
||||
Blue Ocean is the new UI project for Jenkins.
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "@jenkins-cd/jenkins/react",
|
||||
"rules": {
|
||||
"no-unused-vars": [2, {"varsIgnorePattern": "^React$"}]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { configure } from '@kadira/storybook';
|
||||
|
||||
function loadStories() {
|
||||
require('../src/main/js/components/stories/index');
|
||||
}
|
||||
|
||||
configure(loadStories, module);
|
|
@ -1,11 +1,127 @@
|
|||
# Example plugin
|
||||
# Admin plugin
|
||||
|
||||
This plugin is an example of a few extensions
|
||||
The extension points are defined in `blueocean-web`
|
||||
This plugin has started as an example of a few extensions.
|
||||
The extension points are defined in `blueocean-web`.
|
||||
|
||||
However it had become the main app for all client side screens.
|
||||
|
||||
## Running this
|
||||
|
||||
1) Go into `blueocean-plugin` and run `mvn hpi:run` in a terminal. (mvn clean install from the root of the project is always a good idea regularly!)
|
||||
2) From this directory, run `gulp rebundle` to watch for JS changes and reload them.
|
||||
3) Open browser to http://localhost:8080/jenkins/blue/ to see this
|
||||
4) hack away. Refreshing the browser will pick up changes. If you add a new extension point or export a new extension you may need to restart the `mvn hpi:run` process.
|
||||
### With mvn
|
||||
|
||||
|
||||
1. Go into `blueocean-plugin` and run `mvn hpi:run` in a terminal. (mvn clean install from the root of the project is always a good idea regularly!)
|
||||
2. From this directory, run `gulp bundle:watch` to watch for JS changes and reload them.
|
||||
3. Open browser to http://localhost:8080/jenkins/blue/ to see this
|
||||
4. hack away. Refreshing the browser will pick up changes. If you add a new extension point or export a new extension you may need to restart the `mvn hpi:run` process.
|
||||
|
||||
### With npm/storybook
|
||||
|
||||
We are supporting React Storybook https://voice.kadira.io/introducing-react-storybook-ec27f28de1e2#.8zsjledjp
|
||||
|
||||
```javascript
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
Then it’ll start a webserver on port 9001. Further on any change it will
|
||||
refresh the page for you on the browser. The design is not the same as
|
||||
in blueocean yet but you can fully develop the components without a
|
||||
running jenkins,
|
||||
|
||||
#### Writing Stories
|
||||
|
||||
Basically, a story is a single view of a component. It's like a test case, but you can preview it (live) from the Storybook UI.
|
||||
|
||||
You can write your stories anywhere you want. But keeping them close to your components is a pretty good idea.
|
||||
|
||||
Let's write some stories:
|
||||
|
||||
```javascript
|
||||
// src/main/js/components/stories/button.js
|
||||
|
||||
import React from 'react';
|
||||
import { storiesOf, action } from '@kadira/storybook';
|
||||
|
||||
storiesOf('Button', module)
|
||||
.add('with a text', () => (
|
||||
<button onClick={action('clicked')}>My First Button</button>
|
||||
))
|
||||
.add('with no text', () => (
|
||||
<button></button>
|
||||
));
|
||||
```
|
||||
|
||||
Here, we simply have two stories for the built-in `button` component. But, you can import any of your components and write stories for them instead.
|
||||
|
||||
#### Configurations for storybook
|
||||
|
||||
Now you need to tell Storybook where it should load the stories from. For that, you need to add the new story to the configuration file `.storybook/config.js`:
|
||||
|
||||
|
||||
```javascript
|
||||
// .storybook/config.js
|
||||
import { configure } from '@kadira/storybook';
|
||||
|
||||
function loadStories() {
|
||||
require('../src/main/js/components/stories/index');
|
||||
require('../components/stories/button'); // add this line
|
||||
}
|
||||
|
||||
configure(loadStories, module);
|
||||
```
|
||||
|
||||
or to the `src/main/js/components/stories/index.js` (this is the preferred way):
|
||||
|
||||
```javascript
|
||||
// src/main/js/components/stories/index.js
|
||||
require('./pipelines');
|
||||
require('./status');
|
||||
require('./button'); // add this line
|
||||
```
|
||||
|
||||
That's it. Now simply run “npm run storybook” and start developing your components.
|
||||
|
||||
|
||||
### Testing
|
||||
|
||||
We have created different test environments that you can us during development.
|
||||
To run the test once:
|
||||
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
||||
|
||||
#### TDD support via watch
|
||||
|
||||
```
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
Can be used for TDD with a rapid feedback loop (as soon you save all tests will run)
|
||||
|
||||
### Linting with npm
|
||||
|
||||
ESLint with React linting options have been enabled.
|
||||
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
#### lint:fix
|
||||
|
||||
You can use the command lint:fix and it will try to fix all
|
||||
offenses, however there maybe some more that you need to fix manually.
|
||||
|
||||
```
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
#### lint:watch
|
||||
|
||||
You can use the command lint:watch and it will give rapid feedback
|
||||
(as soon you save, lint will run) while you try to fix all offenses.
|
||||
|
||||
```
|
||||
gulp lint:watch --continueOnLint
|
||||
```
|
||||
|
|
|
@ -23,3 +23,6 @@ builder.defineTask('test', function() {
|
|||
throw e;
|
||||
});
|
||||
});
|
||||
builder.gulp.task('lint:watch', function () {
|
||||
builder.gulp.watch(['src/main/js/**/*.js', 'src/main/js/**/*.jsx'], ['lint']);
|
||||
});
|
||||
|
|
|
@ -2,16 +2,20 @@
|
|||
"name": "blueocean-admin",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"storybook": "start-storybook -p 9001",
|
||||
"lint": "gulp lint --silent",
|
||||
"lint:fix": "gulp lint --fixLint --silent",
|
||||
"lint:watch": "gulp lint:watch",
|
||||
"test": "gulp test --silent",
|
||||
"retest": "gulp retest --silent",
|
||||
"test:watch": "gulp test:watch --silent",
|
||||
"bundle": "gulp bundle --silent",
|
||||
"rebundle": "gulp rebundle"
|
||||
"bundle:watch": "gulp bundle:watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jenkins-cd/js-builder": "0.0.17",
|
||||
"@jenkins-cd/js-test": "^1.x",
|
||||
"@jenkins-cd/eslint-config-jenkins": "0.0.2",
|
||||
"@jenkins-cd/js-builder": "0.0.23",
|
||||
"@jenkins-cd/js-test": "1.1.1",
|
||||
"@kadira/storybook": "^1.3.0",
|
||||
"babel": "^6.5.2",
|
||||
"babel-core": "^6.6.5",
|
||||
"babel-eslint": "^6.0.0",
|
||||
|
@ -28,9 +32,9 @@
|
|||
"skin-deep": "^0.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/design-language": "^0.x",
|
||||
"@jenkins-cd/js-extensions": "^0.x",
|
||||
"@jenkins-cd/js-modules": "^0.x",
|
||||
"@jenkins-cd/design-language": "0.0.7",
|
||||
"@jenkins-cd/js-extensions": "0.0.10",
|
||||
"@jenkins-cd/js-modules": "0.0.2",
|
||||
"immutable": "^3.7.6",
|
||||
"moment": "^2.12.0",
|
||||
"moment-duration-format": "^1.3.0",
|
||||
|
@ -38,5 +42,8 @@
|
|||
"react-dom": "^0.14.5",
|
||||
"react-router": "^2.0.1",
|
||||
"window-handle": "^1.0.0"
|
||||
},
|
||||
"jenkinscd" : {
|
||||
"extDependencies": ["immutable", "react-router"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
function placeholder(...ignored) {
|
||||
const requestDone = 4; // Because Zombie is garbage
|
||||
|
||||
function placeholder() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// FIXME: We should rename this to something clearer and lose the capital A if we're going to keep it.
|
||||
export default function AjaxHoc(ComposedComponent, getURLFromProps = placeholder) {
|
||||
|
||||
// FIXME: We should rename this to something clearer and
|
||||
// lose the capital A if we're going to keep it.
|
||||
export default function ajaxHoc(ComposedComponent, getURLFromProps = placeholder) {
|
||||
class Wrapped extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -15,19 +17,10 @@ export default function AjaxHoc(ComposedComponent, getURLFromProps = placeholder
|
|||
const data = null;
|
||||
const url = null;
|
||||
|
||||
this.state = {data, url};
|
||||
this.state = { data, url };
|
||||
this._lastUrl = null; // Keeping this out of the react state so we can set it any time
|
||||
}
|
||||
|
||||
checkUrl(props) {
|
||||
const config = this.context.config;
|
||||
const url = getURLFromProps(props, config);
|
||||
this.setState({url}, () => this.maybeLoad());
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.checkUrl(nextProps);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.checkUrl(this.props);
|
||||
|
@ -37,21 +30,27 @@ export default function AjaxHoc(ComposedComponent, getURLFromProps = placeholder
|
|||
this.maybeLoad();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.checkUrl(nextProps);
|
||||
}
|
||||
|
||||
maybeLoad() {
|
||||
const {url} = this.state;
|
||||
if (url && url != this._lastUrl) {
|
||||
const { url } = this.state;
|
||||
if (url && url !== this._lastUrl) {
|
||||
this._lastUrl = url;
|
||||
this.fetchPipelineData(data => {
|
||||
// eslint-disable-next-line
|
||||
this.setState({
|
||||
data: Immutable.fromJS(data)
|
||||
data: Immutable.fromJS(data),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <ComposedComponent {...this.props} data={ this.state.data }/>;
|
||||
checkUrl(props) {
|
||||
const config = this.context.config;
|
||||
const url = getURLFromProps(props, config);
|
||||
this.setState({ url }, () => this.maybeLoad());
|
||||
}
|
||||
|
||||
/** FIXME: Ghetto ajax loading of pipeline data for an org @store*/
|
||||
|
@ -65,7 +64,7 @@ export default function AjaxHoc(ComposedComponent, getURLFromProps = placeholder
|
|||
}
|
||||
|
||||
xmlhttp.onreadystatechange = () => {
|
||||
if (xmlhttp.readyState === XMLHttpRequest.DONE) {
|
||||
if (xmlhttp.readyState === requestDone) {
|
||||
if (xmlhttp.status === 200) {
|
||||
let data = null;
|
||||
try {
|
||||
|
@ -85,10 +84,15 @@ export default function AjaxHoc(ComposedComponent, getURLFromProps = placeholder
|
|||
xmlhttp.open('GET', url, true);
|
||||
xmlhttp.send();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <ComposedComponent {...this.props} data={ this.state.data } />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Wrapped.contextTypes = {
|
||||
config: PropTypes.object
|
||||
config: PropTypes.object,
|
||||
};
|
||||
|
||||
return Wrapped;
|
||||
|
|
|
@ -10,9 +10,9 @@ class OrganisationPipelines extends Component {
|
|||
// The specific pipeline we may be focused on
|
||||
let pipeline;
|
||||
|
||||
if(pipelines && this.props.params && this.props.params.pipeline) {
|
||||
if (pipelines && this.props.params && this.props.params.pipeline) {
|
||||
const name = this.props.params.pipeline;
|
||||
pipeline = pipelines.find(aPipeLine => aPipeLine.get("name") == name);
|
||||
pipeline = pipelines.find(aPipeLine => aPipeLine.get('name') === name);
|
||||
// FIXME: This foo.get("bar") syntax is not ideal ^^^
|
||||
|
||||
// Convert back to a real JS object
|
||||
|
@ -30,12 +30,12 @@ class OrganisationPipelines extends Component {
|
|||
OrganisationPipelines.propTypes = {
|
||||
data: PropTypes.object, // From Ajax wrapper
|
||||
params: PropTypes.object, // From react-router
|
||||
children: PropTypes.node // From react-router
|
||||
children: PropTypes.node, // From react-router
|
||||
};
|
||||
|
||||
OrganisationPipelines.childContextTypes = {
|
||||
pipelines: PropTypes.object,
|
||||
pipeline: PropTypes.object
|
||||
pipeline: PropTypes.object,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import { Route, IndexRoute } from 'react-router';
|
||||
import React from 'react';
|
||||
import OrganisationPipelines from './OrganisationPipelines';
|
||||
import { Pipelines, MultiBranch, Activity, PullRequests } from './components';
|
||||
import { Pipelines, MultiBranch, Activity, PullRequests, PipelinePage } from './components';
|
||||
|
||||
// Config has some globals in it for path / routes
|
||||
import { rootRoutePath } from './config';
|
||||
|
||||
export default (
|
||||
<Route path={rootRoutePath} component={OrganisationPipelines}>
|
||||
<IndexRoute component={Pipelines}/>
|
||||
<Route path=":pipeline/branches" component={MultiBranch}/>
|
||||
<Route path=":pipeline/activity" component={Activity}/>
|
||||
<Route path=":pipeline/pr" component={PullRequests}/>
|
||||
<IndexRoute component={Pipelines} />
|
||||
<Route component={PipelinePage}>
|
||||
<Route path=":pipeline/branches" component={MultiBranch} />
|
||||
<Route path=":pipeline/activity" component={Activity} />
|
||||
<Route path=":pipeline/pr" component={PullRequests} />
|
||||
</Route>
|
||||
</Route>
|
||||
);
|
||||
);
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import AjaxHoc from '../AjaxHoc';
|
||||
import ajaxHoc from '../AjaxHoc';
|
||||
import Table from './Table';
|
||||
import Runs from './Runs';
|
||||
import { Link } from 'react-router';
|
||||
import { urlPrefix } from '../config';
|
||||
|
||||
import { ActivityRecord, ChangeSetRecord } from './records';
|
||||
|
||||
import { Page, PageHeader, Title, WeatherIcon } from '@jenkins-cd/design-language';
|
||||
|
||||
import pipelinePropProvider from './pipelinePropProvider';
|
||||
|
||||
export class Activity extends Component {
|
||||
render() {
|
||||
const { pipeline, data } = this.props;
|
||||
|
@ -18,48 +13,36 @@ export class Activity extends Component {
|
|||
if (!data || !pipeline) {
|
||||
return null;
|
||||
}
|
||||
const
|
||||
{
|
||||
name,
|
||||
weatherScore,
|
||||
} = pipeline;
|
||||
const headers = ['Status', 'Build', 'Commit', 'Branch', 'Message', 'Duration', 'Completed'];
|
||||
|
||||
let latestRecord = {};
|
||||
|
||||
return (<Page>
|
||||
|
||||
<PageHeader>
|
||||
<Title><WeatherIcon score={weatherScore} /> <h1>CloudBees / {name}</h1></Title>
|
||||
</PageHeader>
|
||||
|
||||
<main>
|
||||
<article>
|
||||
<Table headers={headers}>
|
||||
{ data.map((run, index) => {
|
||||
let
|
||||
return (<main>
|
||||
<article>
|
||||
<Table headers={headers}>
|
||||
{ data.map((run, index) => {
|
||||
let
|
||||
changeset = run.get('changeSet');
|
||||
if (changeset.size > 0) {
|
||||
changeset = changeset.toJS();
|
||||
latestRecord = new ChangeSetRecord(
|
||||
changeset[Object.keys(changeset)[0]]);
|
||||
}
|
||||
return (<Runs
|
||||
key={index}
|
||||
changeset={latestRecord}
|
||||
data={new ActivityRecord(run)}
|
||||
/>);
|
||||
})}
|
||||
if (changeset.size > 0) {
|
||||
changeset = changeset.toJS();
|
||||
latestRecord = new ChangeSetRecord(
|
||||
changeset[Object.keys(changeset)[0]]);
|
||||
}
|
||||
return (<Runs
|
||||
key={index}
|
||||
changeset={latestRecord}
|
||||
data={new ActivityRecord(run)}
|
||||
/>);
|
||||
})}
|
||||
|
||||
<tr>
|
||||
<td colSpan={headers.length}>
|
||||
<Link className="btn" to={urlPrefix}>Dashboard</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</Table>
|
||||
</article>
|
||||
</main>
|
||||
</Page>);
|
||||
<tr>
|
||||
<td colSpan={headers.length}>
|
||||
<Link className="btn" to={urlPrefix}>Dashboard</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</Table>
|
||||
</article>
|
||||
</main>);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,8 +52,8 @@ Activity.propTypes = {
|
|||
};
|
||||
|
||||
// Decorated for ajax as well as getting pipeline from context
|
||||
export default pipelinePropProvider(AjaxHoc(Activity,(props, config) => {
|
||||
export default ajaxHoc(Activity, (props, config) => {
|
||||
if (!props.pipeline) return null;
|
||||
return `${config.getAppURLBase()}/rest/organizations/jenkins` +
|
||||
`/pipelines/${props.pipeline.name}/runs`;
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import moment from 'moment';
|
||||
import { WeatherIcon } from '@jenkins-cd/design-language';
|
||||
import { WeatherIcon, StatusIndicator } from '@jenkins-cd/design-language';
|
||||
|
||||
export default class Branches extends Component {
|
||||
|
||||
|
@ -10,14 +10,13 @@ export default class Branches extends Component {
|
|||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
const { latestRun, weatherScore, name} = data;
|
||||
const { result, endTime, changeSet} = latestRun;
|
||||
|
||||
const {commitId, msg } = changeSet[0] || {};
|
||||
const { latestRun, weatherScore, name } = data;
|
||||
const { result, endTime, changeSet, state } = latestRun;
|
||||
const { commitId, msg } = changeSet[0] || {};
|
||||
|
||||
return (<tr key={name}>
|
||||
<td><WeatherIcon score={weatherScore} /></td>
|
||||
<td>{result}</td>
|
||||
<td><StatusIndicator result={result === 'UNKNOWN' ? state : result} /></td>
|
||||
<td>{decodeURIComponent(name)}</td>
|
||||
<td>{commitId || '-'}</td>
|
||||
<td>{msg || '-'}</td>
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import Table from './Table';
|
||||
import AjaxHoc from '../AjaxHoc';
|
||||
import ajaxHoc from '../AjaxHoc';
|
||||
import Branches from './Branches';
|
||||
import { WeatherIcon, Page, PageHeader, Title } from '@jenkins-cd/design-language';
|
||||
import { RunsRecord } from './records';
|
||||
import { urlPrefix } from '../config';
|
||||
import pipelinePropProvider from './pipelinePropProvider';
|
||||
|
||||
export class MultiBranch extends Component {
|
||||
render() {
|
||||
|
@ -16,38 +14,29 @@ export class MultiBranch extends Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
weatherScore,
|
||||
} = pipeline;
|
||||
|
||||
const headers =
|
||||
['Health', 'Status', 'Branch', 'Last commit', 'Latest message', 'Completed'];
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<Title><WeatherIcon score={weatherScore} /> <h1>CloudBees / {name}</h1></Title>
|
||||
</PageHeader>
|
||||
<main>
|
||||
<article>
|
||||
<Table className="multiBranch"
|
||||
headers={headers}
|
||||
>
|
||||
{data.map((run, index) => {
|
||||
const result = new RunsRecord(run.toJS());
|
||||
return <Branches key={index} data={result} />;
|
||||
})
|
||||
}
|
||||
<tr>
|
||||
<td colSpan={headers.length}>
|
||||
<Link className="btn" to={urlPrefix}>Dashboard</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</Table>
|
||||
</article>
|
||||
</main>
|
||||
</Page>);
|
||||
<main>
|
||||
<article>
|
||||
<Table className="multiBranch"
|
||||
headers={headers}
|
||||
>
|
||||
{data.map((run, index) => {
|
||||
const result = new RunsRecord(run.toJS());
|
||||
return <Branches key={index} data={result} />;
|
||||
})
|
||||
}
|
||||
<tr>
|
||||
<td colSpan={headers.length}>
|
||||
<Link className="btn" to={urlPrefix}>Dashboard</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</Table>
|
||||
</article>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,8 +46,8 @@ MultiBranch.propTypes = {
|
|||
};
|
||||
|
||||
// Decorated for ajax as well as getting pipeline from context
|
||||
export default pipelinePropProvider(AjaxHoc(MultiBranch, (props, config) => {
|
||||
export default ajaxHoc(MultiBranch, (props, config) => {
|
||||
if (!props.pipeline) return null;
|
||||
return `${config.getAppURLBase()}/rest/organizations/jenkins` +
|
||||
`/pipelines/${props.pipeline.name}/branches`;
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { Page, PageHeader, Title, PageTabs, TabLink, WeatherIcon }
|
||||
from '@jenkins-cd/design-language';
|
||||
import { urlPrefix } from '../config';
|
||||
|
||||
export default class PipelinePage extends Component {
|
||||
render() {
|
||||
const { pipeline } = this.context;
|
||||
|
||||
if (!pipeline) {
|
||||
return null; // Loading...
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<Title>
|
||||
<WeatherIcon score={pipeline.weatherScore} />
|
||||
<h1>CloudBees / {pipeline.name}</h1>
|
||||
</Title>
|
||||
<PageTabs base={`${urlPrefix}/${pipeline.name}`}>
|
||||
<TabLink to="/activity">Activity</TabLink>
|
||||
<TabLink to="/branches">Branches</TabLink>
|
||||
<TabLink to="/pr">Pull Requests</TabLink>
|
||||
</PageTabs>
|
||||
</PageHeader>
|
||||
{React.cloneElement(this.props.children, { pipeline })}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PipelinePage.propTypes = {
|
||||
children: PropTypes.object,
|
||||
};
|
||||
|
||||
PipelinePage.contextTypes = {
|
||||
pipeline: PropTypes.object,
|
||||
};
|
|
@ -4,20 +4,24 @@ import { WeatherIcon } from '@jenkins-cd/design-language';
|
|||
|
||||
import { urlPrefix } from '../config';
|
||||
|
||||
export default class Pipeline extends Component {
|
||||
export default class PipelineRowItem extends Component {
|
||||
|
||||
calculateResponse(passing, failing) {
|
||||
let restponse = '-';
|
||||
if (failing > 0) {
|
||||
return (`${failing} failing`);
|
||||
restponse = (`${failing} failing`);
|
||||
} else if (passing > 0) {
|
||||
return (`${passing} passing`);
|
||||
} else {
|
||||
return '-';
|
||||
restponse = (`${passing} passing`);
|
||||
}
|
||||
return restponse;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { pipeline } = this.props;
|
||||
// Early out
|
||||
if (!pipeline) {
|
||||
return null;
|
||||
}
|
||||
const simple = !pipeline.branchNames;
|
||||
const {
|
||||
name,
|
||||
|
@ -28,32 +32,36 @@ export default class Pipeline extends Component {
|
|||
numberOfFailingPullRequests,
|
||||
} = pipeline;
|
||||
|
||||
const hasPullRequests = !simple && (numberOfSuccessfulPullRequests || numberOfFailingPullRequests);
|
||||
const hasPullRequests = !simple && (
|
||||
numberOfSuccessfulPullRequests || numberOfFailingPullRequests);
|
||||
|
||||
const multiBranchURL = `${urlPrefix}/${name}/branches`;
|
||||
const pullRequestsURL = `${urlPrefix}/${name}/pr`;
|
||||
const activitiesURL = `${urlPrefix}/${name}/activity`;
|
||||
|
||||
let multiBranchLabel = " - ";
|
||||
let multiPrLabel = " - ";
|
||||
let multiBranchLabel = ' - ';
|
||||
let multiPrLabel = ' - ';
|
||||
let multiBranchLink = null;
|
||||
let pullRequestsLink = null;
|
||||
|
||||
if (!simple) {
|
||||
multiBranchLabel = this.calculateResponse(numberOfSuccessfulBranches, numberOfFailingBranches);
|
||||
multiPrLabel = this.calculateResponse(numberOfSuccessfulPullRequests, numberOfFailingPullRequests);
|
||||
multiBranchLabel = this.calculateResponse(
|
||||
numberOfSuccessfulBranches, numberOfFailingBranches);
|
||||
multiPrLabel = this.calculateResponse(
|
||||
numberOfSuccessfulPullRequests, numberOfFailingPullRequests);
|
||||
|
||||
multiBranchLink = <Link className="btn" to={multiBranchURL}>multiBranch</Link>;
|
||||
|
||||
if (hasPullRequests)
|
||||
if (hasPullRequests) {
|
||||
pullRequestsLink = <Link className="btn" to={pullRequestsURL}>pr</Link>;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Visual alignment of the last column
|
||||
return (
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td><WeatherIcon score={weatherScore}/></td>
|
||||
<td><WeatherIcon score={weatherScore} /></td>
|
||||
<td>{multiBranchLabel}</td>
|
||||
<td>{multiPrLabel}</td>
|
||||
<td>
|
||||
|
@ -67,6 +75,6 @@ export default class Pipeline extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
Pipeline.propTypes = {
|
||||
pipeline: PropTypes.object.isRequired
|
||||
PipelineRowItem.propTypes = {
|
||||
pipeline: PropTypes.object.isRequired,
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import Pipeline from './Pipeline';
|
||||
import PipelineRowItem from './PipelineRowItem';
|
||||
import { PipelineRecord } from './records';
|
||||
import Table from './Table';
|
||||
|
||||
|
@ -24,14 +24,25 @@ export default class Pipelines extends Component {
|
|||
<PageHeader>
|
||||
<Title>
|
||||
<h1>CloudBees</h1>
|
||||
<a target="_blank" className="btn-primary" href="/jenkins/view/All/newJob">New Pipeline</a>
|
||||
<a
|
||||
target="_blank"
|
||||
className="btn-primary"
|
||||
href="/jenkins/view/All/newJob"
|
||||
>
|
||||
New Pipeline
|
||||
</a>
|
||||
</Title>
|
||||
</PageHeader>
|
||||
<main>
|
||||
<article>
|
||||
<Table className="multiBranch" headers={['Name', 'Status', 'Branches', 'Pull Requests', '']}>
|
||||
<Table
|
||||
className="multiBranch"
|
||||
headers={['Name', 'Status', 'Branches', 'Pull Requests', '']}
|
||||
>
|
||||
{ pipelineRecords
|
||||
.map(pipeline => <Pipeline key={pipeline.name} pipeline={pipeline}/>)
|
||||
.map(pipeline => <PipelineRowItem
|
||||
key={pipeline.name} pipeline={pipeline}
|
||||
/>)
|
||||
.toArray() }
|
||||
</Table>
|
||||
</article>
|
||||
|
@ -41,5 +52,5 @@ export default class Pipelines extends Component {
|
|||
}
|
||||
|
||||
Pipelines.contextTypes = {
|
||||
pipelines: PropTypes.object
|
||||
pipelines: PropTypes.object,
|
||||
};
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import moment from 'moment';
|
||||
import { StatusIndicator } from '@jenkins-cd/design-language';
|
||||
|
||||
export default class PullRequest extends Component {
|
||||
|
||||
render() {
|
||||
const { pr } = this.props;
|
||||
if (!pr) {
|
||||
return null;
|
||||
render() {
|
||||
const { pr } = this.props;
|
||||
if (!pr) {
|
||||
return null;
|
||||
}
|
||||
const { latestRun, pullRequest } = pr;
|
||||
if (!latestRun || !pullRequest) {
|
||||
return null;
|
||||
}
|
||||
const result = latestRun.result === 'UNKNOWN' ? latestRun.state : latestRun.result;
|
||||
return (<tr key={latestRun.id}>
|
||||
<td><StatusIndicator result={result} /></td>
|
||||
<td>{latestRun.id}</td>
|
||||
<td>{pullRequest.title || '-'}</td>
|
||||
<td>{pullRequest.author || '-'}</td>
|
||||
<td>{moment(latestRun.endTime).fromNow()}</td>
|
||||
</tr>);
|
||||
}
|
||||
const { latestRun, pullRequest } = pr;
|
||||
if (!latestRun || !pullRequest) {
|
||||
return null;
|
||||
}
|
||||
return (<tr key={latestRun.id}>
|
||||
<td>{latestRun.result}</td>
|
||||
<td>{latestRun.id}</td>
|
||||
<td>{pullRequest.title || '-'}</td>
|
||||
<td>{pullRequest.author || '-'}</td>
|
||||
<td>{moment(latestRun.endTime).fromNow()}</td>
|
||||
</tr>);
|
||||
}
|
||||
}
|
||||
|
||||
PullRequest.propTypes = {
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import AjaxHoc from '../AjaxHoc';
|
||||
import ajaxHoc from '../AjaxHoc';
|
||||
import Table from './Table';
|
||||
import PullRequest from './PullRequest';
|
||||
import { RunsRecord } from './records';
|
||||
import { urlPrefix } from '../config';
|
||||
import pipelinePropProvider from './pipelinePropProvider';
|
||||
|
||||
import { Page, PageHeader, Title, WeatherIcon } from '@jenkins-cd/design-language';
|
||||
|
||||
export class PullRequests extends Component {
|
||||
render() {
|
||||
|
@ -16,28 +13,17 @@ export class PullRequests extends Component {
|
|||
if (!data || !pipeline) {
|
||||
return null;
|
||||
}
|
||||
const
|
||||
{
|
||||
name,
|
||||
weatherScore,
|
||||
} = pipeline;
|
||||
|
||||
const headers = ['Status', 'Latest Build', 'Summary', 'Author', 'Completed'];
|
||||
|
||||
return (<Page>
|
||||
|
||||
<PageHeader>
|
||||
<Title><WeatherIcon score={weatherScore}/> <h1>CloudBees / {name}</h1></Title>
|
||||
</PageHeader>
|
||||
|
||||
return (
|
||||
<main>
|
||||
<article>
|
||||
<Table headers={headers}>
|
||||
{ data.filter((run) => run.get('pullRequest')).map((run, index) => {
|
||||
const result = new RunsRecord(run.toJS());
|
||||
return (<PullRequest
|
||||
key={index}
|
||||
pr={result}
|
||||
key={index}
|
||||
pr={result}
|
||||
/>);
|
||||
})}
|
||||
|
||||
|
@ -49,7 +35,7 @@ export class PullRequests extends Component {
|
|||
</Table>
|
||||
</article>
|
||||
</main>
|
||||
</Page>);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,8 +45,8 @@ PullRequests.propTypes = {
|
|||
};
|
||||
|
||||
// Decorated for ajax as well as getting pipeline from context
|
||||
export default pipelinePropProvider(AjaxHoc(PullRequests, (props, config) => {
|
||||
export default ajaxHoc(PullRequests, (props, config) => {
|
||||
if (!props.pipeline) return null;
|
||||
return `${config.getAppURLBase()}/rest/organizations/jenkins` +
|
||||
`/pipelines/${props.pipeline.name}/branches`;
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { StatusIndicator } from '@jenkins-cd/design-language';
|
||||
|
||||
const { number } = PropTypes;
|
||||
|
||||
/*
|
||||
DEMO of running state with raising percentages
|
||||
*/
|
||||
export default class RunningIndicator extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const initialPercentage = props.percentage || 12.5;
|
||||
this.state = { percentage: initialPercentage };
|
||||
this.tick = () => { // FIXME: remove this.tick code when ux-206 is fixed
|
||||
if (this.state.percentage === 100 - initialPercentage) {
|
||||
this.setState({ percentage: 100 });
|
||||
clearInterval(this.timer);
|
||||
} else {
|
||||
this.setState({ percentage: this.state.percentage + initialPercentage });
|
||||
}
|
||||
};
|
||||
}
|
||||
/* FIXME:
|
||||
remove all interval related code when ux-206 is fixed
|
||||
start
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.timer = setInterval(this.tick, 500);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
/* FIXME:
|
||||
remove all interval related code when ux-206 is fixed
|
||||
stop
|
||||
*/
|
||||
render() {
|
||||
const props = {
|
||||
result: 'running',
|
||||
title: 'running',
|
||||
percentage: this.state.percentage,
|
||||
};
|
||||
return (<StatusIndicator
|
||||
{...Object.assign({}, this.props, props)}
|
||||
/>);
|
||||
}
|
||||
}
|
||||
|
||||
RunningIndicator.propTypes = {
|
||||
percentage: number,
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import moment from 'moment';
|
||||
import { ModalView, ModalBody } from '@jenkins-cd/design-language';
|
||||
import { ModalView, ModalBody, StatusIndicator } from '@jenkins-cd/design-language';
|
||||
|
||||
require('moment-duration-format');
|
||||
|
||||
|
@ -10,7 +10,7 @@ require('moment-duration-format');
|
|||
export default class Runs extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {isVisible: false};
|
||||
this.state = { isVisible: false };
|
||||
}
|
||||
render() {
|
||||
const { data, changeset } = this.props;
|
||||
|
@ -22,30 +22,39 @@ export default class Runs extends Component {
|
|||
duration = moment.duration(
|
||||
Number(data.durationInMillis), 'milliseconds').format('hh:mm:ss');
|
||||
|
||||
const
|
||||
durationArray = duration.split(':'),
|
||||
name = decodeURIComponent(data.pipeline)
|
||||
;
|
||||
const durationArray = duration.split(':');
|
||||
const name = decodeURIComponent(data.pipeline);
|
||||
|
||||
if (durationArray.length === 1) {
|
||||
duration = `00:${duration}`;
|
||||
}
|
||||
|
||||
const afterClose = () => this.setState({ isVisible: false });
|
||||
const open = () => this.setState({ isVisible: true });
|
||||
const result = data.result === 'UNKNOWN' ? data.state : data.result;
|
||||
return (<tr key={data.id}>
|
||||
<td>
|
||||
{
|
||||
this.state.isVisible && <ModalView hideOnOverlayClicked
|
||||
title={`Branch ${name}`}
|
||||
isVisible={this.state.isVisible}
|
||||
afterClose={() => this.setState({isVisible: false})}>
|
||||
title={`Branch ${name}`}
|
||||
isVisible={this.state.isVisible}
|
||||
afterClose={afterClose}
|
||||
>
|
||||
<ModalBody>
|
||||
<dl>
|
||||
<dt>Status</dt>
|
||||
<dd>{data.result}</dd>
|
||||
<dd>
|
||||
<StatusIndicator result={result} />
|
||||
</dd>
|
||||
<dt>Build</dt>
|
||||
<dd>{data.id}</dd>
|
||||
<dt>Commit</dt>
|
||||
<dd>{changeset && changeset.commitId && changeset.commitId.substring(0, 8) || '-'}</dd>
|
||||
<dd>
|
||||
{changeset
|
||||
&& changeset.commitId
|
||||
&& changeset.commitId.substring(0, 8) || '-'
|
||||
}
|
||||
</dd>
|
||||
<dt>Branch</dt>
|
||||
<dd>{name}</dd>
|
||||
<dt>Message</dt>
|
||||
|
@ -58,12 +67,12 @@ export default class Runs extends Component {
|
|||
</ModalBody>
|
||||
</ModalView>
|
||||
}
|
||||
<a onClick={() => this.setState({isVisible: true})}>
|
||||
{data.result}
|
||||
<a onClick={open}>
|
||||
<StatusIndicator result={result} />
|
||||
</a>
|
||||
</td>
|
||||
<td>{data.id}</td>
|
||||
<td>{changeset && changeset.commitId && changeset.commitId.substring(0, 8) || '-'}</td>
|
||||
<td>{changeset && changeset.commitId && changeset.commitId.substring(0, 8) || '-'}</td>
|
||||
<td>{name}</td>
|
||||
<td>{changeset && changeset.comment || '-'}</td>
|
||||
<td>
|
||||
|
|
|
@ -2,10 +2,12 @@ import Pipelines from './Pipelines';
|
|||
import MultiBranch from './MultiBranch';
|
||||
import Activity from './Activity';
|
||||
import PullRequests from './PullRequests';
|
||||
import PipelinePage from './PipelinePage';
|
||||
|
||||
export {
|
||||
Pipelines,
|
||||
MultiBranch,
|
||||
Activity,
|
||||
PullRequests
|
||||
};
|
||||
PullRequests,
|
||||
PipelinePage,
|
||||
};
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
/**
|
||||
* Wraps a component, for the purpose of providing a "selected pipeline" from the react context (as a prop on wrapped
|
||||
* component). This is a transformer, not a constructor.
|
||||
*
|
||||
* @param WrappedComponent a react component class/constructor
|
||||
* @return {Wrapper}
|
||||
*/
|
||||
export default function pipelinePropProvider(WrappedComponent) {
|
||||
|
||||
class Wrapper extends Component {
|
||||
render() {
|
||||
const { pipeline } = this.context;
|
||||
return <WrappedComponent {...this.props} pipeline={pipeline}/>;
|
||||
}
|
||||
}
|
||||
|
||||
Wrapper.contextTypes = {
|
||||
pipeline: PropTypes.object
|
||||
};
|
||||
|
||||
return Wrapper;
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
import Immutable from 'immutable';
|
||||
|
||||
export const PipelineRecord = Immutable.Record({ // eslint-disable-line new-cap
|
||||
/* We cannot extend Record
|
||||
since we would return a function, */
|
||||
/* eslint new-cap: [0] */
|
||||
const { Record } = Immutable;
|
||||
export const PipelineRecord = Record({
|
||||
displayName: '',
|
||||
name: '',
|
||||
organization: '',
|
||||
|
@ -14,23 +17,7 @@ export const PipelineRecord = Immutable.Record({ // eslint-disable-line new-cap
|
|||
totalNumberOfPullRequests: 0,
|
||||
});
|
||||
|
||||
export const ActivityRecord = Immutable.Record({// eslint-disable-line
|
||||
changeSet: ChangeSetRecord,
|
||||
durationInMillis: null,
|
||||
enQueueTime: null,
|
||||
endTime: null,
|
||||
id: null,
|
||||
organization: null,
|
||||
pipeline: null,
|
||||
result: null,
|
||||
runSummary: null,
|
||||
startTime: null,
|
||||
state: null,
|
||||
type: null,
|
||||
commitId: null,
|
||||
});
|
||||
|
||||
export const ChangeSetRecord = Immutable.Record({// eslint-disable-line
|
||||
export const ChangeSetRecord = Record({
|
||||
author: {
|
||||
email: null,
|
||||
fullName: null,
|
||||
|
@ -46,19 +33,36 @@ export const ChangeSetRecord = Immutable.Record({// eslint-disable-line
|
|||
timestamp: null,
|
||||
});
|
||||
|
||||
export const RunsRecord = Immutable.Record({
|
||||
latestRun: ActivityRecord,
|
||||
name: null,
|
||||
weatherScore: 0,
|
||||
pullRequest: PullRequestRecord,
|
||||
}
|
||||
);
|
||||
export const ActivityRecord = Record({
|
||||
changeSet: ChangeSetRecord,
|
||||
durationInMillis: null,
|
||||
enQueueTime: null,
|
||||
endTime: null,
|
||||
id: null,
|
||||
organization: null,
|
||||
pipeline: null,
|
||||
result: null,
|
||||
runSummary: null,
|
||||
startTime: null,
|
||||
state: null,
|
||||
type: null,
|
||||
commitId: null,
|
||||
});
|
||||
|
||||
export const PullRequestRecord = Immutable.Record({
|
||||
export const PullRequestRecord = Record({
|
||||
pullRequest: {
|
||||
author: null,
|
||||
id: null,
|
||||
title: null,
|
||||
url: null,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const RunsRecord = Record({
|
||||
latestRun: ActivityRecord,
|
||||
name: null,
|
||||
weatherScore: 0,
|
||||
pullRequest: PullRequestRecord,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
require('./pipelines');
|
||||
require('./status');
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import { storiesOf } from '@kadira/storybook';
|
||||
import PipelineRowItem from '../PipelineRowItem.jsx';
|
||||
import { PipelineRecord } from '../records.jsx';
|
||||
import Table from '../Table.jsx';
|
||||
|
||||
/*
|
||||
First example of using storybook
|
||||
*/
|
||||
storiesOf('pipelines', module)
|
||||
.add('with a pipeline', () => (
|
||||
<Table
|
||||
className="multiBranch"
|
||||
headers={['Name', 'Status', 'Branches', 'Pull Requests', '']}
|
||||
>
|
||||
<PipelineRowItem
|
||||
pipeline={new PipelineRecord({
|
||||
displayName: 'moreBeersSuccess',
|
||||
name: 'morebeersSuccess',
|
||||
organization: 'jenkins',
|
||||
weatherScore: 0,
|
||||
branchNames: ['master'],
|
||||
numberOfFailingBranches: 0,
|
||||
numberOfFailingPullRequests: 0,
|
||||
numberOfSuccessfulBranches: 3,
|
||||
numberOfSuccessfulPullRequests: 3,
|
||||
totalNumberOfBranches: 3,
|
||||
totalNumberOfPullRequests: 3,
|
||||
})}
|
||||
/>
|
||||
<PipelineRowItem
|
||||
pipeline={new PipelineRecord({
|
||||
displayName: 'moreBeers',
|
||||
name: 'morebeers',
|
||||
organization: 'jenkins',
|
||||
weatherScore: 0,
|
||||
branchNames: ['master'],
|
||||
numberOfFailingBranches: 1,
|
||||
numberOfFailingPullRequests: 0,
|
||||
numberOfSuccessfulBranches: 0,
|
||||
numberOfSuccessfulPullRequests: 0,
|
||||
totalNumberOfBranches: 1,
|
||||
totalNumberOfPullRequests: 0,
|
||||
})}
|
||||
/>
|
||||
</Table>
|
||||
))
|
||||
.add('no pipeline should return null', () => (
|
||||
<PipelineRowItem />
|
||||
))
|
||||
;
|
|
@ -0,0 +1,84 @@
|
|||
import React from 'react';
|
||||
import { storiesOf } from '@kadira/storybook';
|
||||
import { StatusIndicator } from '@jenkins-cd/design-language';
|
||||
import RunningIndicator from '../RunningIndicator.jsx';
|
||||
|
||||
const props = {
|
||||
width: '640px',
|
||||
height: '640px',
|
||||
};
|
||||
|
||||
const smaller = {
|
||||
width: '320px',
|
||||
height: '320px',
|
||||
};
|
||||
|
||||
storiesOf('StatusIndicators', module)
|
||||
.add('success', () => (
|
||||
<StatusIndicator
|
||||
{...Object.assign({
|
||||
result: 'SUCCESS',
|
||||
}, props)}
|
||||
/>
|
||||
))
|
||||
.add('failure', () => (
|
||||
<StatusIndicator
|
||||
{...Object.assign({
|
||||
result: 'FAILURE',
|
||||
}, props)}
|
||||
/>
|
||||
))
|
||||
.add('queued', () => (
|
||||
<div>
|
||||
<div>This will be animated
|
||||
by css and will turn
|
||||
</div>
|
||||
<StatusIndicator
|
||||
{...Object.assign({
|
||||
result: 'QUEUED',
|
||||
}, props)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
.add('running', () => (
|
||||
<div>
|
||||
<div>This shows 50%</div>
|
||||
<StatusIndicator
|
||||
{...Object.assign({
|
||||
result: 'RUNNING',
|
||||
percentage: 50,
|
||||
}, props)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
.add('running animated', () => (
|
||||
<div>
|
||||
<div>
|
||||
This shows demo where % is raised
|
||||
and stops at 100%
|
||||
</div>
|
||||
<RunningIndicator {...props} />
|
||||
</div>
|
||||
))
|
||||
.add('all', () => (
|
||||
<div>
|
||||
<StatusIndicator
|
||||
{...smaller}
|
||||
result="SUCCESS"
|
||||
/>
|
||||
<StatusIndicator
|
||||
{...smaller}
|
||||
result="FAILURE"
|
||||
/>
|
||||
<StatusIndicator
|
||||
{...smaller}
|
||||
result="QUEUED"
|
||||
/>
|
||||
<StatusIndicator
|
||||
{...smaller}
|
||||
result="RUNNING"
|
||||
percentage={50}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
;
|
|
@ -1,3 +1,3 @@
|
|||
// Shared consts.
|
||||
export const rootRoutePath = "pipelines";
|
||||
export const rootRoutePath = 'pipelines';
|
||||
export const urlPrefix = `/${rootRoutePath}`;
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import {createRenderer} from 'react-addons-test-utils';
|
||||
import { assert} from 'chai';
|
||||
import sd from 'skin-deep';
|
||||
import Immutable from 'immutable';
|
||||
|
@ -154,10 +153,6 @@ describe("Activity should render", () => {
|
|||
});
|
||||
|
||||
it("does renders the Activity with data", () => {
|
||||
// does WeatherIcon renders the value from the pipeline?
|
||||
const weatherIcon = tree.subTree('WeatherIcon').getRenderOutput();
|
||||
assert.isNotNull(weatherIcon);
|
||||
assert.isNotNull(weatherIcon.props.score);
|
||||
// does data renders?
|
||||
const runs = tree.subTree('Runs').getRenderOutput();
|
||||
assert.isNotNull(runs.props.changeset)
|
||||
|
|
|
@ -3,8 +3,8 @@ import {createRenderer} from 'react-addons-test-utils';
|
|||
import { assert} from 'chai';
|
||||
import sd from 'skin-deep';
|
||||
|
||||
|
||||
import Pipeline from '../../main/js/components/Pipeline.jsx';
|
||||
import PipelineRowItem from '../../main/js/components/PipelineRowItem.jsx';
|
||||
import { PipelineRecord } from '../../main/js/components/records.jsx';
|
||||
|
||||
const
|
||||
hack={
|
||||
|
@ -44,21 +44,28 @@ const
|
|||
'organization': 'jenkins',
|
||||
'weatherScore': 0
|
||||
},
|
||||
testElementSimple = (<Pipeline
|
||||
testElementSimple = (<PipelineRowItem
|
||||
hack={hack}
|
||||
pipeline={pipelineSimple}
|
||||
simple={true}/>
|
||||
),
|
||||
testElementMultiSuccess = (<Pipeline
|
||||
testElementMultiSuccess = (<PipelineRowItem
|
||||
hack={hack}
|
||||
pipeline={pipelineMultiSuccess}
|
||||
/>
|
||||
),
|
||||
testElementMulti = (<Pipeline
|
||||
testElementMulti = (<PipelineRowItem
|
||||
hack={hack}
|
||||
pipeline={pipelineMulti}/>
|
||||
);
|
||||
|
||||
describe("PipelineRecord can be created ", () => {
|
||||
it("without error", () => {
|
||||
const pipeRecord = new PipelineRecord(pipelineMultiSuccess);
|
||||
console.log(pipeRecord)
|
||||
})
|
||||
});
|
||||
|
||||
describe("pipeline component simple rendering", () => {
|
||||
const
|
||||
renderer = createRenderer();
|
||||
|
@ -77,7 +84,6 @@ describe("pipeline component simple rendering", () => {
|
|||
assert.isObject(children[2].props);
|
||||
assert.equal(children[2].props.children, ' - ');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("pipeline component multiBranch rendering", () => {
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
import React, {Component} from 'react';
|
||||
import sd from 'skin-deep';
|
||||
import { assert} from 'chai';
|
||||
|
||||
import pipelinePropProvider from '../../main/js/components/pipelinePropProvider';
|
||||
|
||||
class DummyChild extends Component {
|
||||
render() {
|
||||
return <div>Pipeline is '{String(this.props.pipeline)}'</div>;
|
||||
}
|
||||
}
|
||||
|
||||
describe("pipelinePropProvider", () => {
|
||||
|
||||
const pipelineValue = {
|
||||
foo: "bar",
|
||||
baz: "quux",
|
||||
toString() {return "pipeline value"}
|
||||
};
|
||||
|
||||
const otherValue = "Rubber baby buggy bumpers";
|
||||
|
||||
const WrappedDummy = pipelinePropProvider(DummyChild);
|
||||
|
||||
let props = {};
|
||||
|
||||
// Using a provider function turns on Skin-deep's context support for some reason
|
||||
const providerFn = () => React.createElement(WrappedDummy, props);
|
||||
|
||||
let tree, instance, vdom; // Rendered results
|
||||
let context = {};
|
||||
|
||||
beforeEach(() => {
|
||||
context = {};
|
||||
props = {};
|
||||
});
|
||||
|
||||
describe("with value in context", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
context.pipeline = pipelineValue;
|
||||
props.otherProp = otherValue;
|
||||
|
||||
tree = sd.shallowRender(providerFn, context);
|
||||
instance = tree.getMountedInstance();
|
||||
vdom = tree.getRenderOutput();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
assert.isNotNull(tree);
|
||||
assert.isNotNull(instance);
|
||||
assert.isNotNull(vdom);
|
||||
});
|
||||
|
||||
it("should extract the correct value", () => {
|
||||
assert.equal(vdom.props.pipeline, pipelineValue, "pipeline property on vdom");
|
||||
});
|
||||
|
||||
it("should pass on other props", () => {
|
||||
assert.equal(vdom.props.otherProp, otherValue, "otherProp property on vdom");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with no value in context", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
props.otherProp = otherValue;
|
||||
|
||||
tree = sd.shallowRender(providerFn, context);
|
||||
instance = tree.getMountedInstance();
|
||||
vdom = tree.getRenderOutput();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
assert.isNotNull(tree);
|
||||
assert.isNotNull(instance);
|
||||
assert.isNotNull(vdom);
|
||||
});
|
||||
|
||||
it("should extract nothing from context", () => {
|
||||
assert.equal(vdom.props.pipeline, null, "pipeline property on vdom");
|
||||
});
|
||||
|
||||
it("should pass on other props", () => {
|
||||
assert.equal(vdom.props.otherProp, otherValue, "otherProp property on vdom");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -48,7 +48,7 @@ describe("pipelines", () => {
|
|||
|
||||
it("renders pipelines - check rows number to be as expected", () => {
|
||||
const
|
||||
row = tree.everySubTree('Pipeline')
|
||||
row = tree.everySubTree('PipelineRowItem')
|
||||
;
|
||||
assert.equal(row.length, 2);
|
||||
});
|
||||
|
|
|
@ -34,10 +34,6 @@ describe("PullRequests should render", () => {
|
|||
});
|
||||
|
||||
it("does renders the PullRequests with data", () => {
|
||||
// does WeatherIcon renders the value from the pipeline?
|
||||
const weatherIcon = tree.subTree('WeatherIcon').getRenderOutput();
|
||||
assert.isNotNull(weatherIcon);
|
||||
assert.isNotNull(weatherIcon.props.score);
|
||||
// does data renders?
|
||||
const runs = tree.subTree('PullRequest').getRenderOutput();
|
||||
assert.isNotNull(runs.props.changeset)
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
//FIXME: should be part of the jdl, but test are broken there
|
||||
import React from 'react';
|
||||
import { assert} from 'chai';
|
||||
import sd from 'skin-deep';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
import { StatusIndicator, SvgStatus, SvgSpinner }
|
||||
from '@jenkins-cd/design-language';
|
||||
|
||||
const props = {
|
||||
width: '640px',
|
||||
height: '640px',
|
||||
};
|
||||
const results = {
|
||||
failure: {
|
||||
fill: '#d54c53',
|
||||
stroke: '#cf3a41',
|
||||
},
|
||||
};
|
||||
|
||||
describe("StatusIndicator should render", () => {
|
||||
let tree = null;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = sd.shallowRender(<StatusIndicator
|
||||
result="SUCCESS"
|
||||
{...props}
|
||||
/>);
|
||||
});
|
||||
|
||||
it("does render success", () => {
|
||||
const statusindicator = tree.getRenderOutput();
|
||||
assert.isNotNull(statusindicator);
|
||||
assert.equal(statusindicator.props.result, 'success');
|
||||
assert.equal(statusindicator.props.width, props.width);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("SvgStatus should render", () => {
|
||||
let tree = null;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = sd.shallowRender(<SvgStatus
|
||||
result="FAILURE"
|
||||
/>);
|
||||
});
|
||||
|
||||
it("does render FAILURE", () => {
|
||||
const circle = tree.subTree('circle').getRenderOutput();
|
||||
assert.isNotNull(circle);
|
||||
assert.equal(circle.props.fill, results.failure.fill);
|
||||
assert.equal(circle.props.stroke, results.failure.stroke);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("SvgSpinner should render", () => {
|
||||
let tree = null;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = sd.shallowRender(<SvgSpinner
|
||||
result="QUEUED"
|
||||
/>);
|
||||
});
|
||||
|
||||
it("does render FAILURE", () => {
|
||||
const path = tree.subTree('path').getRenderOutput();
|
||||
assert.isNotNull(path);
|
||||
assert.equal(path.props.fill, 'none');
|
||||
assert.equal(path.props.stroke, '#4a90e2');
|
||||
});
|
||||
|
||||
});
|
|
@ -66,12 +66,12 @@ public class AbstractRunImpl<T extends Run> extends BlueRun {
|
|||
|
||||
@Override
|
||||
public BlueRunState getStateObj() {
|
||||
if(!run.hasntStartedYet() && run.isBuilding()) {
|
||||
if(!run.hasntStartedYet() && run.isLogUpdated()) {
|
||||
return BlueRunState.RUNNING;
|
||||
} else if(!run.isLogUpdated()){
|
||||
return BlueRunState.FINISHED;
|
||||
} else {
|
||||
return BlueRunState.FINISHED;
|
||||
return BlueRunState.QUEUED;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -156,7 +156,7 @@ public abstract class BlueRun extends Resource {
|
|||
public abstract Object getLog();
|
||||
|
||||
public enum BlueRunState {
|
||||
NOT_STARTED,
|
||||
QUEUED,
|
||||
RUNNING,
|
||||
FINISHED
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ If you wish to make changes to blueocean.js, then you will need to install gulp
|
|||
|
||||
```
|
||||
$ cd blueocean-web
|
||||
$ gulp rebundle
|
||||
$ gulp bundle:watch
|
||||
```
|
||||
|
||||
(or run gulp, after each change) in the blueocean-web directory. This will pick up any changes.
|
||||
|
|
|
@ -13,8 +13,6 @@ builder.src(['src/main/js', 'src/main/less', 'node_modules/@jenkins-cd/design-la
|
|||
// generateNoImportsBundle makes it easier to test with zombie.
|
||||
//
|
||||
builder.bundle('src/main/js/blueocean.js')
|
||||
.withExternalModuleMapping('jquery-detached', 'jquery-detached:jquery2')
|
||||
.inDir('target/classes/io/jenkins/blueocean')
|
||||
.less('src/main/less/blueocean.less')
|
||||
.generateNoImportsBundle();
|
||||
|
||||
|
|
|
@ -10,13 +10,13 @@
|
|||
"lint": "gulp lint --silent",
|
||||
"lint:fix": "gulp lint --fixLint --silent",
|
||||
"test": "gulp test --silent",
|
||||
"retest": "gulp retest",
|
||||
"test:watch": "gulp test:watch",
|
||||
"bundle": "gulp bundle",
|
||||
"rebundle": "gulp rebundle"
|
||||
"bundle:watch": "gulp bundle:watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jenkins-cd/js-builder": "0.0.17",
|
||||
"@jenkins-cd/js-test": "^1.x",
|
||||
"@jenkins-cd/js-builder": "0.0.23",
|
||||
"@jenkins-cd/js-test": "1.1.1",
|
||||
"babel-eslint": "^6.0.0",
|
||||
"babel-preset-es2015": "^6.5.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
|
@ -26,11 +26,10 @@
|
|||
"zombie": "^4.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/design-language": "^0.x",
|
||||
"@jenkins-cd/js-extensions": "^0.x",
|
||||
"@jenkins-cd/js-modules": "^0.x",
|
||||
"@jenkins-cd/design-language": "0.0.7",
|
||||
"@jenkins-cd/js-extensions": "0.0.10",
|
||||
"@jenkins-cd/js-modules": "0.0.2",
|
||||
"history": "^2.0.1",
|
||||
"jquery-detached": "^2.1.4-v4",
|
||||
"react": "^0.14.7",
|
||||
"react-dom": "^0.14.7",
|
||||
"react-router": "^2.0.1",
|
||||
|
|
|
@ -1,5 +1,38 @@
|
|||
import AboutNavLink from './about/AboutNavLink.jsx';
|
||||
|
||||
const requestDone = 4; // Because Zombie is garbage
|
||||
|
||||
// Basically copied from AjaxHoc
|
||||
function getURL(url, onLoad) {
|
||||
const xmlhttp = new XMLHttpRequest();
|
||||
|
||||
if (!url) {
|
||||
onLoad(null);
|
||||
return;
|
||||
}
|
||||
|
||||
xmlhttp.onreadystatechange = () => {
|
||||
if (xmlhttp.readyState === requestDone) {
|
||||
if (xmlhttp.status === 200) {
|
||||
let data = null;
|
||||
try {
|
||||
data = JSON.parse(xmlhttp.responseText);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
console.log('Loading', url,
|
||||
'Expecting JSON, instead got', xmlhttp.responseText);
|
||||
}
|
||||
onLoad(data);
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
console.log('Loading', url, 'expected 200, got', xmlhttp.status, xmlhttp.responseText);
|
||||
}
|
||||
}
|
||||
};
|
||||
xmlhttp.open('GET', url, true);
|
||||
xmlhttp.send();
|
||||
}
|
||||
|
||||
exports.initialize = function (oncomplete) {
|
||||
//
|
||||
// Application initialization.
|
||||
|
@ -28,9 +61,9 @@ exports.initialize = function (oncomplete) {
|
|||
|
||||
// Get the extension list metadata from Jenkins.
|
||||
// Might want to do some flux fancy-pants stuff for this.
|
||||
const $ = require('jquery-detached').getJQuery();
|
||||
const appRoot = $("head").data("appurl");
|
||||
$.getJSON(`${appRoot}/javaScriptExtensionInfo`, (data) => {
|
||||
const appRoot = document.getElementsByTagName("head")[0].getAttribute("data-appurl");
|
||||
const extensionsURL = `${appRoot}/javaScriptExtensionInfo`;
|
||||
getURL(extensionsURL, data => {
|
||||
extensions.store.setExtensionPointMetadata(data);
|
||||
oncomplete();
|
||||
});
|
||||
|
|
|
@ -26,6 +26,7 @@ const config = {
|
|||
},
|
||||
less: {
|
||||
sources: "less/theme.less",
|
||||
watch: "less/**/*.less", // Watch includes as well as main
|
||||
dest: "dist/assets/css"
|
||||
},
|
||||
copy: {
|
||||
|
@ -61,6 +62,13 @@ const config = {
|
|||
clean: ["dist", "licenses"]
|
||||
};
|
||||
|
||||
// Watch
|
||||
|
||||
gulp.task("watch", ["default"], () => {
|
||||
gulp.watch(config.react.sources, ["compile-react"]);
|
||||
gulp.watch(config.less.watch, ["less"]);
|
||||
});
|
||||
|
||||
// Default to clean and build
|
||||
|
||||
gulp.task("default", () =>
|
||||
|
|
|
@ -554,18 +554,43 @@ code.inline {
|
|||
color: @text-color;
|
||||
display: inline-block;
|
||||
padding: 0.6rem 1rem 0;
|
||||
height: 30px;
|
||||
border-bottom: solid 3px rgba(0, 0, 0, 0);
|
||||
height: 33px;
|
||||
font-family: @font-family-nav;
|
||||
}
|
||||
|
||||
.page-tabs a.selected {
|
||||
border-bottom: solid 3px @brand-primary;
|
||||
}
|
||||
|
||||
.page-tabs a.selected:hover,
|
||||
.page-tabs a.selected:active {
|
||||
text-decoration: none;
|
||||
|
||||
// For transforming underline
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transform: translateZ(0);
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
|
||||
.page-tabs a:before {
|
||||
// For transforming underline
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: @brand-primary;
|
||||
height: 3px;
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: 0.2s;
|
||||
transform: translateY(3px);
|
||||
transition-property: transform;
|
||||
}
|
||||
|
||||
.page-tabs a.selected:before,
|
||||
.page-tabs:hover a.selected:hover:before,
|
||||
.page-tabs a:hover:before {
|
||||
// For transforming underline
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.page-tabs:hover a.selected:before {
|
||||
// For transforming underline
|
||||
transform: translateY(3px);
|
||||
}
|
||||
|
||||
main article {
|
||||
|
@ -639,3 +664,44 @@ footer {
|
|||
right: 20px;
|
||||
top: 5px
|
||||
}
|
||||
.spin {
|
||||
-webkit-animation-name: spin;
|
||||
-webkit-animation-duration: 4000ms;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-name: spin;
|
||||
-moz-animation-duration: 4000ms;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-name: spin;
|
||||
-ms-animation-duration: 4000ms;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
-ms-animation-timing-function: linear;
|
||||
|
||||
animation-name: spin;
|
||||
animation-duration: 4000ms;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
@-ms-keyframes spin {
|
||||
from { -ms-transform: rotate(0deg); }
|
||||
to { -ms-transform: rotate(360deg); }
|
||||
}
|
||||
@-moz-keyframes spin {
|
||||
from { -moz-transform: rotate(0deg); }
|
||||
to { -moz-transform: rotate(360deg); }
|
||||
}
|
||||
@-webkit-keyframes spin {
|
||||
from { -webkit-transform: rotate(0deg); }
|
||||
to { -webkit-transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform:rotate(0deg); }
|
||||
to { transform:rotate(360deg); }
|
||||
}
|
||||
circle.success {
|
||||
fill: @brand-success;
|
||||
}
|
||||
circle.failure {
|
||||
fill: @brand-danger;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@jenkins-cd/design-language",
|
||||
"jdlName": "jenkins-design-language",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.7",
|
||||
"description": "Styles, assets, and React classes for Jenkins Design Language",
|
||||
"main": "dist/js/components/index.js",
|
||||
"scripts": {
|
||||
|
@ -34,6 +34,7 @@
|
|||
"normalize.css": "^3.0.3",
|
||||
"octicons": "^3.5.0",
|
||||
"react": "^0.14.7",
|
||||
"react-router": "^2.0.1",
|
||||
"run-sequence": "^1.1.5"
|
||||
},
|
||||
"files": [
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
export {ModalView, ModalBody, ModalStyles, ModalHeader} from './modal/modalview';
|
||||
export {
|
||||
ModalView,
|
||||
ModalBody,
|
||||
ModalStyles,
|
||||
ModalHeader
|
||||
} from './modal/modalview';
|
||||
export {WeatherIcon} from './weather-icon';
|
||||
export {Page} from './page';
|
||||
export {GlobalHeader, GlobalNav} from './global-header';
|
||||
export {PageHeader, Title, PageTabs} from './page-header';
|
||||
export {PageHeader, Title, PageTabs, TabLink} from './page-header';
|
||||
export {Table} from './table';
|
||||
export {
|
||||
StatusIndicator,
|
||||
SvgSpinner,
|
||||
SvgStatus,
|
||||
} from './status/StatusIndicator';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, {Component} from 'react';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
export class PageHeader extends Component {
|
||||
render() {
|
||||
|
@ -20,6 +21,33 @@ export class Title extends Component {
|
|||
|
||||
export class PageTabs extends Component {
|
||||
render() {
|
||||
return <nav className="page-tabs">{this.props.children}</nav>;
|
||||
const base = this.props.base;
|
||||
return (
|
||||
<nav className="page-tabs">
|
||||
{React.Children.map(this.props.children, child => React.cloneElement(child, {base}))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PageTabs.propTypes = {
|
||||
base: React.PropTypes.string
|
||||
};
|
||||
|
||||
export class TabLink extends Component {
|
||||
render() {
|
||||
const base = this.props.base || "";
|
||||
const routeUrl = base + this.props.to;
|
||||
const linkClassName = this.context.router.isActive(routeUrl) ? "selected" : undefined;
|
||||
return <Link to={routeUrl} className={linkClassName}>{this.props.children}</Link>;
|
||||
}
|
||||
}
|
||||
|
||||
TabLink.propTypes = {
|
||||
base: React.PropTypes.string,
|
||||
to: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
TabLink.contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import SvgSpinner from './SvgSpinner';
|
||||
import SvgStatus from './SvgStatus';
|
||||
|
||||
const { number, string } = PropTypes;
|
||||
|
||||
class StatusIndicator extends Component {
|
||||
render() {
|
||||
const {
|
||||
result,
|
||||
percentage,
|
||||
width = '32px',
|
||||
height = '32px',
|
||||
} = this.props;
|
||||
// early out
|
||||
if (!result && !result.toLowerCase) {
|
||||
return null;
|
||||
}
|
||||
const resultClean = result.toLowerCase();
|
||||
const props = {
|
||||
percentage,
|
||||
height,
|
||||
width,
|
||||
result: resultClean,
|
||||
title: resultClean,
|
||||
};
|
||||
return (resultClean === 'running' || resultClean === 'queued' ? <SvgSpinner
|
||||
{...props}
|
||||
/> : <SvgStatus
|
||||
{...props}
|
||||
/>);
|
||||
}
|
||||
}
|
||||
|
||||
StatusIndicator.propTypes = {
|
||||
result: string.isRequired,
|
||||
percentage: number,
|
||||
width: string,
|
||||
height: string,
|
||||
};
|
||||
|
||||
export {
|
||||
StatusIndicator,
|
||||
SvgSpinner,
|
||||
SvgStatus,
|
||||
};
|
|
@ -0,0 +1,95 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
const { string, object, number } = PropTypes;
|
||||
|
||||
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
|
||||
const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
|
||||
|
||||
return {
|
||||
x: centerX + (radius * Math.cos(angleInRadians)),
|
||||
y: centerY + (radius * Math.sin(angleInRadians)),
|
||||
};
|
||||
}
|
||||
|
||||
function describeArc(x, y, radius, startAngle, endAngle) {
|
||||
const start = polarToCartesian(x, y, radius, endAngle);
|
||||
const end = polarToCartesian(x, y, radius, startAngle);
|
||||
|
||||
const arcSweep = endAngle - startAngle <= 180 ? '0' : '1';
|
||||
|
||||
const d = [
|
||||
'M', start.x, start.y,
|
||||
'A', radius, radius, 0, arcSweep, 0, end.x, end.y,
|
||||
].join(' ');
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
export default class SvgSpinner extends Component {
|
||||
render() {
|
||||
const {
|
||||
result = 'failure',
|
||||
percentage = 12.5,
|
||||
title = 'No title',
|
||||
width = '32px',
|
||||
height = '32px',
|
||||
colors = {
|
||||
backgrounds: {
|
||||
box: 'none',
|
||||
outer: 'none',
|
||||
},
|
||||
strokes: {
|
||||
outer: '#a9c6e6',
|
||||
path: '#4a90e2',
|
||||
|
||||
},
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const rotate = percentage / 100 * 360;
|
||||
const d = describeArc(50, 50, 40, 0, rotate);
|
||||
|
||||
return (<svg xmlns="http://www.w3.org/2000/svg"
|
||||
className={result === 'queued' ? 'spin' : ''}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<title>{title}</title>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="100"
|
||||
height="100"
|
||||
fill={colors.backgrounds.box}
|
||||
className="bk"
|
||||
/>
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
stroke={rotate === 360 ? colors.strokes.path : colors.strokes.outer}
|
||||
fill={colors.backgrounds.outer}
|
||||
strokeWidth="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
className={result}
|
||||
fill="none"
|
||||
stroke={colors.strokes.path}
|
||||
strokeWidth="10"
|
||||
d={d}
|
||||
|
||||
/>
|
||||
</svg>);
|
||||
}
|
||||
}
|
||||
|
||||
SvgSpinner.propTypes = {
|
||||
title: string,
|
||||
width: string,
|
||||
result: string,
|
||||
height: string,
|
||||
percentage: number,
|
||||
colors: object,
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
const { string, object } = PropTypes;
|
||||
|
||||
const results = {
|
||||
success: {
|
||||
fill: '#8cc04f',
|
||||
stroke: '#7cb445',
|
||||
},
|
||||
failure: {
|
||||
fill: '#d54c53',
|
||||
stroke: '#cf3a41',
|
||||
}
|
||||
};
|
||||
|
||||
export default class SvgStatus extends Component {
|
||||
render() {
|
||||
const {
|
||||
result = 'failure',
|
||||
title = 'No title',
|
||||
width = '32px',
|
||||
height = '32px',
|
||||
colors = {
|
||||
backgrounds: {
|
||||
box: 'none',
|
||||
inner: results[result.toLowerCase()] ? results[result.toLowerCase()].fill : 'none',
|
||||
},
|
||||
strokes: {
|
||||
inner: results[result.toLowerCase()] ? results[result.toLowerCase()].stroke : 'none',
|
||||
|
||||
},
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
return (<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<title>{title}</title>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="100"
|
||||
height="100"
|
||||
fill={colors.backgrounds.box}
|
||||
className="bk"
|
||||
/>
|
||||
|
||||
<circle
|
||||
className={result}
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
stroke={colors.strokes.inner}
|
||||
fill={colors.backgrounds.inner}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
</svg>);
|
||||
}
|
||||
}
|
||||
|
||||
SvgStatus.propTypes = {
|
||||
title: string,
|
||||
result: string,
|
||||
width: string,
|
||||
height: string,
|
||||
colors: object,
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
//
|
||||
// See https://github.com/jenkinsci/js-builder
|
||||
//
|
||||
require('@jenkins-cd/js-builder');
|
||||
var builder = require('@jenkins-cd/js-builder');
|
||||
builder.lint('none');
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@jenkins-cd/js-extensions",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.10",
|
||||
"description": "Jenkins Extension Store",
|
||||
"main": "index.js",
|
||||
"files": ["index.js","dist","README.md"],
|
||||
"scripts": {
|
||||
"build": "jsx src/ExtensionPoint.jsx > dist/ExtensionPoint.js && cp src/store.js dist/",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "rm -rf dist && mkdir dist && jsx src/ExtensionPoint.jsx > dist/ExtensionPoint.js && cp src/store.js dist/",
|
||||
"test": "gulp test",
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"author": "Tom Fennelly <tom.fennelly@gmail.com> (https://github.com/tfennelly)",
|
||||
|
@ -17,11 +17,11 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"gulp": "^3.9.1",
|
||||
"@jenkins-cd/js-builder": "^0.x",
|
||||
"@jenkins-cd/js-test": "^1.x",
|
||||
"@jenkins-cd/js-builder": "0.0.22",
|
||||
"@jenkins-cd/js-test": "1.1.1",
|
||||
"react-tools": "^0.13.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/js-modules": "^0.x"
|
||||
"@jenkins-cd/js-modules": "0.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,56 +1,70 @@
|
|||
var jsTest = require('@jenkins-cd/js-test');
|
||||
|
||||
describe("store.js", function () {
|
||||
|
||||
it("- test ", function (done) {
|
||||
var javaScriptExtensionInfo = require('./javaScriptExtensionInfo-01.json');
|
||||
var store = require('../store');
|
||||
var jsModules = require('@jenkins-cd/js-modules');
|
||||
var pluginsLoaded = {};
|
||||
var loaded = 0;
|
||||
jsTest.onPage(function() {
|
||||
var javaScriptExtensionInfo = require('./javaScriptExtensionInfo-01.json');
|
||||
var store = require('../src/store');
|
||||
var jsModules = require('@jenkins-cd/js-modules');
|
||||
var pluginsLoaded = {};
|
||||
var loaded = 0;
|
||||
|
||||
// Mock the calls to addScript
|
||||
jsModules.addScript = function(scriptUrl, options) {
|
||||
expect(scriptUrl).toBe('io/jenkins/' + options.hpiPluginId + '/jenkins-js-extension.js');
|
||||
// mimic registering of those extensions
|
||||
for(var i1 = 0; i1 < javaScriptExtensionInfo.length; i1++) {
|
||||
var pluginMetadata = javaScriptExtensionInfo[i1];
|
||||
var extensions = pluginMetadata.extensions;
|
||||
for(var i2 = 0; i2 < extensions.length; i2++) {
|
||||
store.addExtension(extensions[i2].component, extensions[i2].extensionPoint);
|
||||
// Mock the calls to import
|
||||
var theRealImport = jsModules.import;
|
||||
|
||||
jsModules.import = function(bundleId) {
|
||||
var internal = require('@jenkins-cd/js-modules/js/internal');
|
||||
var bundleModuleSpec = internal.parseResourceQName(bundleId);
|
||||
var pluginId = bundleModuleSpec.namespace;
|
||||
|
||||
// mimic registering of those extensions
|
||||
for(var i1 = 0; i1 < javaScriptExtensionInfo.length; i1++) {
|
||||
var pluginMetadata = javaScriptExtensionInfo[i1];
|
||||
var extensions = pluginMetadata.extensions;
|
||||
for(var i2 = 0; i2 < extensions.length; i2++) {
|
||||
store.addExtension(extensions[i2].component, extensions[i2].extensionPoint);
|
||||
}
|
||||
}
|
||||
if (pluginsLoaded[pluginId] === undefined) {
|
||||
pluginsLoaded[pluginId] = true;
|
||||
loaded++;
|
||||
}
|
||||
}
|
||||
if (pluginsLoaded[options.hpiPluginId] === undefined) {
|
||||
pluginsLoaded[options.hpiPluginId] = true;
|
||||
loaded++;
|
||||
}
|
||||
options.success();
|
||||
};
|
||||
|
||||
// Initialise the store with some extension point info. At runtime,
|
||||
// this info will be loaded from <jenkins>/blue/javaScriptExtensionInfo
|
||||
store.setExtensionPointMetadata(javaScriptExtensionInfo);
|
||||
// fake the export of the bundle
|
||||
setTimeout(function() {
|
||||
jsModules.export(pluginId, 'jenkins-js-extension', {});
|
||||
}, 100);
|
||||
return theRealImport.call(theRealImport, bundleId);
|
||||
};
|
||||
|
||||
// Call load for ExtensionPoint impls 'ep-1'. This should mimic
|
||||
// the store checking all plugins and loading the bundles for any
|
||||
// plugins that define an impl of 'ep-1' (if not already loaded).
|
||||
store.loadExtensions('ep-1', function() {
|
||||
if (loaded === 2) {
|
||||
expect(pluginsLoaded['plugin-1']).toBeDefined();
|
||||
expect(pluginsLoaded['plugin-2']).toBeDefined();
|
||||
// Initialise the store with some extension point info. At runtime,
|
||||
// this info will be loaded from <jenkins>/blue/javaScriptExtensionInfo
|
||||
store.setExtensionPointMetadata(javaScriptExtensionInfo);
|
||||
|
||||
// if we call load again, nothing should happen as
|
||||
// all plugin bundles have been loaded i.e. loaded
|
||||
// should still be 2 (i.e. unchanged).
|
||||
store.loadExtensions('ep-1', function() {
|
||||
expect(loaded, 2);
|
||||
// Call load for ExtensionPoint impls 'ep-1'. This should mimic
|
||||
// the store checking all plugins and loading the bundles for any
|
||||
// plugins that define an impl of 'ep-1' (if not already loaded).
|
||||
store.loadExtensions('ep-1', function() {
|
||||
if (loaded === 2) {
|
||||
expect(pluginsLoaded['plugin-1']).toBeDefined();
|
||||
expect(pluginsLoaded['plugin-2']).toBeDefined();
|
||||
|
||||
// Calling it yet again for different extension point, but
|
||||
// where the bundles for that extension point have already.
|
||||
store.loadExtensions('ep-2', function() {
|
||||
// if we call load again, nothing should happen as
|
||||
// all plugin bundles have been loaded i.e. loaded
|
||||
// should still be 2 (i.e. unchanged).
|
||||
store.loadExtensions('ep-1', function() {
|
||||
expect(loaded, 2);
|
||||
done();
|
||||
|
||||
// Calling it yet again for different extension point, but
|
||||
// where the bundles for that extension point have already.
|
||||
store.loadExtensions('ep-2', function() {
|
||||
expect(loaded, 2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -101,17 +101,20 @@ var ExtensionPoint = React.createClass({
|
|||
* would otherwise not be notified when this is being unmounted.
|
||||
*/
|
||||
_unmountAllExtensions: function() {
|
||||
var children = ReactDOM.findDOMNode(this).children;
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var child = children[i];
|
||||
try {
|
||||
if (child) {
|
||||
ReactDOM.unmountComponentAtNode(child);
|
||||
var thisNode = ReactDOM.findDOMNode(this);
|
||||
var children = thisNode ? thisNode.children : null;
|
||||
if (children && children.length) {
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var child = children[i];
|
||||
try {
|
||||
if (child) {
|
||||
ReactDOM.unmountComponentAtNode(child);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
// Log and continue, don't want to stop unmounting children
|
||||
console.log("Error unmounting component", child, err);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
// Log and continue, don't want to stop unmounting children
|
||||
console.log("Error unmounting component", child, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,6 @@ function loadBundles(extensionPointId, onBundlesLoaded) {
|
|||
var loadCountMonitor = new LoadCountMonitor();
|
||||
|
||||
function loadPluginBundle(pluginMetadata) {
|
||||
var scriptUrl = 'io/jenkins/' + pluginMetadata.hpiPluginId + '/jenkins-js-extension.js';
|
||||
loadCountMonitor.inc();
|
||||
|
||||
// The plugin bundle for this plugin may already be in the process of loading (async extension
|
||||
|
@ -75,17 +74,14 @@ function loadBundles(extensionPointId, onBundlesLoaded) {
|
|||
if (!pluginMetadata.loadCountMonitors) {
|
||||
pluginMetadata.loadCountMonitors = [];
|
||||
pluginMetadata.loadCountMonitors.push(loadCountMonitor);
|
||||
jsModules.addScript(scriptUrl, {
|
||||
scriptSrcBase: '@adjunct',
|
||||
hpiPluginId: pluginMetadata.hpiPluginId, // Used for testing
|
||||
success: function () {
|
||||
jsModules.import(pluginMetadata.hpiPluginId + ':jenkins-js-extension')
|
||||
.onFulfilled(function() {
|
||||
pluginMetadata.bundleLoaded = true;
|
||||
for (var i = 0; i < pluginMetadata.loadCountMonitors.length; i++) {
|
||||
pluginMetadata.loadCountMonitors[i].dec();
|
||||
}
|
||||
delete pluginMetadata.loadCountMonitors;
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
pluginMetadata.loadCountMonitors.push(loadCountMonitor);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue