Compare commits
15 Commits
master
...
JENKINS-36
Author | SHA1 | Date |
---|---|---|
Ivan Meredith | 2c203185a0 | |
Ivan Meredith | 8cdc066ff9 | |
Ivan Meredith | b34d9cf59f | |
Ivan Meredith | ee36ba8bdc | |
Ivan Meredith | 54f8da6640 | |
Ivan Meredith | 14286862d1 | |
Ivan Meredith | 6de0d01565 | |
Ivan Meredith | 0d47f44ef7 | |
Ivan Meredith | 42bf4f2f77 | |
tfennelly | a82e176e9e | |
tfennelly | 4ccbde30b6 | |
tfennelly | d89f01010c | |
Ivan Meredith | 41af06d617 | |
Ivan Meredith | 766c449945 | |
Ivan Meredith | b0ac0a123b |
|
@ -46,6 +46,7 @@
|
||||||
"moment-duration-format": "1.3.0",
|
"moment-duration-format": "1.3.0",
|
||||||
"react": "15.1.0",
|
"react": "15.1.0",
|
||||||
"react-dom": "15.1.0",
|
"react-dom": "15.1.0",
|
||||||
|
"react-addons-update": "15.1.0",
|
||||||
"react-material-icons-blue": "1.0.4",
|
"react-material-icons-blue": "1.0.4",
|
||||||
"react-redux": "4.4.5",
|
"react-redux": "4.4.5",
|
||||||
"react-router": "2.3.0",
|
"react-router": "2.3.0",
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
export const MULTIBRANCH_PIPELINE = 'io.jenkins.blueocean.rest.model.BlueMultiBranchPipeline';
|
||||||
|
|
||||||
|
export const blah = 'aa';
|
|
@ -10,6 +10,8 @@ import {
|
||||||
createSelector,
|
createSelector,
|
||||||
connect,
|
connect,
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
|
import { MULTIBRANCH_PIPELINE } from '../Capabilities';
|
||||||
|
import { capabilityStore } from './Capability';
|
||||||
|
|
||||||
const { object, array, func, string, bool } = PropTypes;
|
const { object, array, func, string, bool } = PropTypes;
|
||||||
|
|
||||||
|
@ -68,16 +70,20 @@ export class Activity extends Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { capabilities } = this.props;
|
||||||
|
const isMultiBranchPipeline = capabilities[pipeline._class].has(MULTIBRANCH_PIPELINE);
|
||||||
|
|
||||||
// Only show the Run button for non multi-branch pipelines.
|
// Only show the Run button for non multi-branch pipelines.
|
||||||
// Multi-branch pipelines have the Run/play button beside them on
|
// Multi-branch pipelines have the Run/play button beside them on
|
||||||
// the Branches/PRs tab.
|
// the Branches/PRs tab.
|
||||||
const showRunButton = (pipeline && !Pipeline.isMultibranch(pipeline));
|
const showRunButton = !isMultiBranchPipeline;
|
||||||
|
|
||||||
|
|
||||||
if (!runs.length) {
|
if (!runs.length) {
|
||||||
return (<EmptyState repoName={this.context.params.pipeline} showRunButton={showRunButton} pipeline={pipeline} />);
|
return (<EmptyState repoName={this.context.params.pipeline} showRunButton={showRunButton} pipeline={pipeline} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = [
|
const headers = isMultiBranchPipeline ? [
|
||||||
'Status',
|
'Status',
|
||||||
'Build',
|
'Build',
|
||||||
'Commit',
|
'Commit',
|
||||||
|
@ -86,9 +92,17 @@ export class Activity extends Component {
|
||||||
{ label: 'Duration', className: 'duration' },
|
{ label: 'Duration', className: 'duration' },
|
||||||
{ label: 'Completed', className: 'completed' },
|
{ label: 'Completed', className: 'completed' },
|
||||||
{ label: '', className: 'actions' },
|
{ label: '', className: 'actions' },
|
||||||
|
] : [
|
||||||
|
'Status',
|
||||||
|
'Build',
|
||||||
|
'Commit',
|
||||||
|
{ label: 'Message', className: 'message' },
|
||||||
|
{ label: 'Duration', className: 'duration' },
|
||||||
|
{ label: 'Completed', className: 'completed' },
|
||||||
|
{ label: '', className: 'actions' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
return (<main>
|
return (<main>
|
||||||
<article className="activity">
|
<article className="activity">
|
||||||
{showRunButton && <RunNonMultiBranchPipeline pipeline={pipeline} buttonText="Run" />}
|
{showRunButton && <RunNonMultiBranchPipeline pipeline={pipeline} buttonText="Run" />}
|
||||||
|
@ -124,9 +138,10 @@ Activity.contextTypes = {
|
||||||
Activity.propTypes = {
|
Activity.propTypes = {
|
||||||
runs: array,
|
runs: array,
|
||||||
pipeline: object,
|
pipeline: object,
|
||||||
|
capabilities: object,
|
||||||
fetchRunsIfNeeded: func,
|
fetchRunsIfNeeded: func,
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectors = createSelector([runsSelector], (runs) => ({ runs }));
|
const selectors = createSelector([runsSelector], (runs) => ({ runs }));
|
||||||
|
|
||||||
export default connect(selectors, actions)(Activity);
|
export default connect(selectors, actions)(capabilityStore(props => props.pipeline._class)(Activity));
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import update from 'react-addons-update';
|
||||||
|
import { classMetadataStore } from '@jenkins-cd/js-extensions';
|
||||||
|
|
||||||
|
export const capabilityStore = classes => ComposedComponent => class extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
capabilities: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const self = this;
|
||||||
|
let _classes = classes(this.props);
|
||||||
|
|
||||||
|
if (typeof _classes === 'string') {
|
||||||
|
_classes = [_classes];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const _class of _classes) {
|
||||||
|
classMetadataStore.getClassMetadata(_class, (classMeta) => {
|
||||||
|
self._setState(_class, self._classesToObj(classMeta.classes));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unmounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_classesToObj(_classes) {
|
||||||
|
if (!_classes) {
|
||||||
|
return {
|
||||||
|
has: () => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
_classes,
|
||||||
|
has: capability => _classes.find(_class => _class === capability) !== undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_setState(key, value) {
|
||||||
|
// Block calls to setState for components that are
|
||||||
|
// not in a mounted state.
|
||||||
|
if (!this.unmounted) {
|
||||||
|
const newData = { capabilities: {} };
|
||||||
|
newData.capabilities[key] = { $set: value };
|
||||||
|
this.setState(previousState => update(previousState, newData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { capabilities } = this.state;
|
||||||
|
|
||||||
|
// Early out. Doing it here means we don't have to do it in
|
||||||
|
// the composed componenet
|
||||||
|
let _classes = classes(this.props);
|
||||||
|
|
||||||
|
if (typeof _classes === 'string') {
|
||||||
|
_classes = [_classes];
|
||||||
|
}
|
||||||
|
for (const _class of _classes) {
|
||||||
|
if (!capabilities[_class] || !capabilities[_class].classes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This passes all props and state to ComposedComponent where
|
||||||
|
// they will all show as props.
|
||||||
|
return <ComposedComponent {...this.props} {...this.state} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { capabilityStore } from './Capability';
|
||||||
|
|
||||||
|
const { string, object } = PropTypes;
|
||||||
|
|
||||||
|
class IfCapability extends Component {
|
||||||
|
render() {
|
||||||
|
const { _class, capability, capabilities } = this.props;
|
||||||
|
|
||||||
|
if (capabilities[_class].has(capability)) {
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IfCapability.propTypes = {
|
||||||
|
_class: string,
|
||||||
|
capability: string,
|
||||||
|
capabilities: object,
|
||||||
|
children: React.PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default capabilityStore(props => props._class)(IfCapability);
|
||||||
|
|
|
@ -3,9 +3,13 @@ import {
|
||||||
CommitHash, ReadableDate, LiveStatusIndicator, TimeDuration,
|
CommitHash, ReadableDate, LiveStatusIndicator, TimeDuration,
|
||||||
}
|
}
|
||||||
from '@jenkins-cd/design-language';
|
from '@jenkins-cd/design-language';
|
||||||
|
|
||||||
|
import { MULTIBRANCH_PIPELINE } from '../Capabilities';
|
||||||
|
|
||||||
import Extensions from '@jenkins-cd/js-extensions';
|
import Extensions from '@jenkins-cd/js-extensions';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { buildRunDetailsUrl } from '../util/UrlUtils';
|
import { buildRunDetailsUrl } from '../util/UrlUtils';
|
||||||
|
import IfCapability from './IfCapability';
|
||||||
|
|
||||||
const { object, string, any } = PropTypes;
|
const { object, string, any } = PropTypes;
|
||||||
|
|
||||||
|
@ -27,6 +31,7 @@ export default class Runs extends Component {
|
||||||
router,
|
router,
|
||||||
location,
|
location,
|
||||||
pipeline: {
|
pipeline: {
|
||||||
|
_class: pipelineClass,
|
||||||
fullName,
|
fullName,
|
||||||
organization,
|
organization,
|
||||||
},
|
},
|
||||||
|
@ -69,7 +74,9 @@ export default class Runs extends Component {
|
||||||
{id}
|
{id}
|
||||||
</td>
|
</td>
|
||||||
<td><CommitHash commitId={commitId} /></td>
|
<td><CommitHash commitId={commitId} /></td>
|
||||||
<td>{decodeURIComponent(pipeline)}</td>
|
<IfCapability _class={pipelineClass} capability={MULTIBRANCH_PIPELINE} >
|
||||||
|
<td>{decodeURIComponent(pipeline)}</td>
|
||||||
|
</IfCapability>
|
||||||
<td>{changeset && changeset.comment || '-'}</td>
|
<td>{changeset && changeset.comment || '-'}</td>
|
||||||
<td><TimeDuration millis={durationMillis} liveUpdate={running} /></td>
|
<td><TimeDuration millis={durationMillis} liveUpdate={running} /></td>
|
||||||
<td><ReadableDate date={endTime} liveUpdate /></td>
|
<td><ReadableDate date={endTime} liveUpdate /></td>
|
||||||
|
|
|
@ -848,7 +848,7 @@ export const actions = {
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
resetTestDetails() {
|
resetTestDetails() {
|
||||||
return (dispatch) =>
|
return (dispatch) =>
|
||||||
dispatch({
|
dispatch({
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { assert} from 'chai';
|
import { assert } from 'chai';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import {Activity} from '../../main/js/components/Activity.jsx';
|
import { Activity } from '../../main/js/components/Activity.jsx';
|
||||||
|
|
||||||
const
|
const
|
||||||
data = [
|
data = [
|
||||||
{
|
{
|
||||||
"changeSet": [],
|
"changeSet": [],
|
||||||
"durationInMillis": 64617,
|
"durationInMillis": 64617,
|
||||||
"enQueueTime": "2016-03-04T13:59:53.272+0100",
|
"enQueueTime": "2016-03-04T13:59:53.272+0100",
|
||||||
|
@ -129,10 +129,19 @@ const
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const pipeline = {
|
||||||
|
_class: "some.class"
|
||||||
|
}
|
||||||
|
|
||||||
|
const capabilities = {
|
||||||
|
'some.class':{
|
||||||
|
has: () => true
|
||||||
|
}
|
||||||
|
}
|
||||||
describe("Activity should render", () => {
|
describe("Activity should render", () => {
|
||||||
|
|
||||||
it("does renders the Activity with data", () => {
|
it("does renders the Activity with data", () => {
|
||||||
const wrapper = shallow(<Activity runs={data} />);
|
const wrapper = shallow(<Activity runs={data} pipeline={pipeline} capabilities={capabilities}/>);
|
||||||
// does data renders?
|
// does data renders?
|
||||||
assert.isNotNull(wrapper)
|
assert.isNotNull(wrapper)
|
||||||
assert.equal(wrapper.find('Runs').length, data.length)
|
assert.equal(wrapper.find('Runs').length, data.length)
|
||||||
|
@ -142,14 +151,14 @@ describe("Activity should render", () => {
|
||||||
|
|
||||||
describe("Activity should not render", () => {
|
describe("Activity should not render", () => {
|
||||||
it("does not renders the Activity without data", () => {
|
it("does not renders the Activity without data", () => {
|
||||||
const wrapper = shallow(<Activity />).node;
|
const wrapper = shallow(<Activity pipeline={pipeline} capabilities={capabilities}/>).node;
|
||||||
assert.isNull(wrapper);
|
assert.isNull(wrapper);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Pipeline -> Activity List', () => {
|
describe('Pipeline -> Activity List', () => {
|
||||||
it('should not duplicate changeset messages', () => {
|
it('should not duplicate changeset messages', () => {
|
||||||
const wrapper = shallow(<Activity runs={data} />);
|
const wrapper = shallow(<Activity runs={data} pipeline={pipeline} capabilities={capabilities} />);
|
||||||
assert.isNotNull(wrapper);
|
assert.isNotNull(wrapper);
|
||||||
|
|
||||||
const runs = wrapper.find('Runs');
|
const runs = wrapper.find('Runs');
|
||||||
|
|
|
@ -61,7 +61,7 @@ exports.initialize = function (oncomplete) {
|
||||||
const appRoot = document.getElementsByTagName("head")[0].getAttribute("data-appurl");
|
const appRoot = document.getElementsByTagName("head")[0].getAttribute("data-appurl");
|
||||||
Extensions.init({
|
Extensions.init({
|
||||||
extensionDataProvider: cb => getURL(`${appRoot}/js-extensions`, rsp => cb(rsp.data)),
|
extensionDataProvider: cb => getURL(`${appRoot}/js-extensions`, rsp => cb(rsp.data)),
|
||||||
classMetadataProvider: (type, cb) => getURL(`${appRoot}/rest/classes/${type}`, cb)
|
classMetadataProvider: (type, cb) => getURL(`${appRoot}/rest/classes/${type}/`, cb)
|
||||||
});
|
});
|
||||||
oncomplete();
|
oncomplete();
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,13 +8,20 @@ export class ClassMetadataStore {
|
||||||
* Type info cache
|
* Type info cache
|
||||||
*/
|
*/
|
||||||
this.classMetadata = {};
|
this.classMetadata = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch function for the classMetadata
|
* Fetch function for the classMetadata
|
||||||
*/
|
*/
|
||||||
this.classMetadataProvider = classMetadataProvider;
|
this.classMetadataProvider = classMetadataProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Onload callbacks cache. Used to ensure we don't
|
||||||
|
* issue multiple in-parallel requests for the same
|
||||||
|
* class metadata.
|
||||||
|
*/
|
||||||
|
this.classMetadataOnloadCallbacks = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the type/capability info for the given data type
|
* Gets the type/capability info for the given data type
|
||||||
*/
|
*/
|
||||||
|
@ -23,24 +30,45 @@ export class ClassMetadataStore {
|
||||||
if (classMeta) {
|
if (classMeta) {
|
||||||
return onload(classMeta);
|
return onload(classMeta);
|
||||||
}
|
}
|
||||||
this.classMetadataProvider(type, (data) => {
|
|
||||||
classMeta = this.classMetadata[type] = JSON.parse(JSON.stringify(data));
|
var callbacks = this.classMetadataOnloadCallbacks[type];
|
||||||
classMeta.classes = classMeta.classes || [];
|
if (!callbacks) {
|
||||||
// Make sure the type itself is in the list
|
// This is the first request for this type. Initialise the
|
||||||
if (classMeta.classes.indexOf(type) < 0) {
|
// callback cache and then issue the request to
|
||||||
classMeta.classes = [type, ...classMeta.classes];
|
// the classMetadataProvider.
|
||||||
}
|
callbacks = this.classMetadataOnloadCallbacks[type] = [];
|
||||||
onload(classMeta);
|
this.classMetadataProvider(type, (data) => {
|
||||||
});
|
classMeta = this.classMetadata[type] = JSON.parse(JSON.stringify(data));
|
||||||
|
classMeta.classes = classMeta.classes || [];
|
||||||
|
// Make sure the type itself is in the list
|
||||||
|
if (classMeta.classes.indexOf(type) < 0) {
|
||||||
|
classMeta.classes = [type, ...classMeta.classes];
|
||||||
|
}
|
||||||
|
delete this.classMetadataOnloadCallbacks[type];
|
||||||
|
|
||||||
|
// Notify all callbacks
|
||||||
|
for (var i = 0; i < callbacks.length; i++) {
|
||||||
|
try {
|
||||||
|
callbacks[i](classMeta);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Unexpected Error in ClassMetadataStore onload callback function.', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// We already have an inflight request to get class metadata info about
|
||||||
|
// the requested type, so nothing to do except store the onload callback.
|
||||||
|
}
|
||||||
|
callbacks.push(onload);
|
||||||
}
|
}
|
||||||
|
|
||||||
dataType(dataType) {
|
dataType(dataType) {
|
||||||
return (extensions, onload) => {
|
return (extensions, onload) => {
|
||||||
if (dataType && typeof(dataType) === 'object'
|
if (dataType && typeof(dataType) === 'object'
|
||||||
&& '_class' in dataType) { // handle the common API incoming data
|
&& '_class' in dataType) { // handle the common API incoming data
|
||||||
dataType = dataType._class;
|
dataType = dataType._class;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.getClassMetadata(dataType, (currentTypeInfo) => {
|
this.getClassMetadata(dataType, (currentTypeInfo) => {
|
||||||
// prevent returning extensions for the given type
|
// prevent returning extensions for the given type
|
||||||
// when a more specific extension is found
|
// when a more specific extension is found
|
||||||
|
|
Loading…
Reference in New Issue