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",
|
||||
"react": "15.1.0",
|
||||
"react-dom": "15.1.0",
|
||||
"react-addons-update": "15.1.0",
|
||||
"react-material-icons-blue": "1.0.4",
|
||||
"react-redux": "4.4.5",
|
||||
"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,
|
||||
connect,
|
||||
} from '../redux';
|
||||
import { MULTIBRANCH_PIPELINE } from '../Capabilities';
|
||||
import { capabilityStore } from './Capability';
|
||||
|
||||
const { object, array, func, string, bool } = PropTypes;
|
||||
|
||||
|
@ -68,16 +70,20 @@ export class Activity extends Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { capabilities } = this.props;
|
||||
const isMultiBranchPipeline = capabilities[pipeline._class].has(MULTIBRANCH_PIPELINE);
|
||||
|
||||
// Only show the Run button for non multi-branch pipelines.
|
||||
// Multi-branch pipelines have the Run/play button beside them on
|
||||
// the Branches/PRs tab.
|
||||
const showRunButton = (pipeline && !Pipeline.isMultibranch(pipeline));
|
||||
const showRunButton = !isMultiBranchPipeline;
|
||||
|
||||
|
||||
if (!runs.length) {
|
||||
return (<EmptyState repoName={this.context.params.pipeline} showRunButton={showRunButton} pipeline={pipeline} />);
|
||||
}
|
||||
|
||||
const headers = [
|
||||
const headers = isMultiBranchPipeline ? [
|
||||
'Status',
|
||||
'Build',
|
||||
'Commit',
|
||||
|
@ -86,9 +92,17 @@ export class Activity extends Component {
|
|||
{ label: 'Duration', className: 'duration' },
|
||||
{ label: 'Completed', className: 'completed' },
|
||||
{ label: '', className: 'actions' },
|
||||
] : [
|
||||
'Status',
|
||||
'Build',
|
||||
'Commit',
|
||||
{ label: 'Message', className: 'message' },
|
||||
{ label: 'Duration', className: 'duration' },
|
||||
{ label: 'Completed', className: 'completed' },
|
||||
{ label: '', className: 'actions' },
|
||||
];
|
||||
|
||||
|
||||
|
||||
return (<main>
|
||||
<article className="activity">
|
||||
{showRunButton && <RunNonMultiBranchPipeline pipeline={pipeline} buttonText="Run" />}
|
||||
|
@ -124,9 +138,10 @@ Activity.contextTypes = {
|
|||
Activity.propTypes = {
|
||||
runs: array,
|
||||
pipeline: object,
|
||||
capabilities: object,
|
||||
fetchRunsIfNeeded: func,
|
||||
};
|
||||
|
||||
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,
|
||||
}
|
||||
from '@jenkins-cd/design-language';
|
||||
|
||||
import { MULTIBRANCH_PIPELINE } from '../Capabilities';
|
||||
|
||||
import Extensions from '@jenkins-cd/js-extensions';
|
||||
import moment from 'moment';
|
||||
import { buildRunDetailsUrl } from '../util/UrlUtils';
|
||||
import IfCapability from './IfCapability';
|
||||
|
||||
const { object, string, any } = PropTypes;
|
||||
|
||||
|
@ -27,6 +31,7 @@ export default class Runs extends Component {
|
|||
router,
|
||||
location,
|
||||
pipeline: {
|
||||
_class: pipelineClass,
|
||||
fullName,
|
||||
organization,
|
||||
},
|
||||
|
@ -69,7 +74,9 @@ export default class Runs extends Component {
|
|||
{id}
|
||||
</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><TimeDuration millis={durationMillis} liveUpdate={running} /></td>
|
||||
<td><ReadableDate date={endTime} liveUpdate /></td>
|
||||
|
|
|
@ -848,7 +848,7 @@ export const actions = {
|
|||
));
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
resetTestDetails() {
|
||||
return (dispatch) =>
|
||||
dispatch({
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React from 'react';
|
||||
import { assert} from 'chai';
|
||||
import { assert } from 'chai';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import {Activity} from '../../main/js/components/Activity.jsx';
|
||||
import { Activity } from '../../main/js/components/Activity.jsx';
|
||||
|
||||
const
|
||||
data = [
|
||||
{
|
||||
{
|
||||
"changeSet": [],
|
||||
"durationInMillis": 64617,
|
||||
"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", () => {
|
||||
|
||||
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?
|
||||
assert.isNotNull(wrapper)
|
||||
assert.equal(wrapper.find('Runs').length, data.length)
|
||||
|
@ -142,14 +151,14 @@ describe("Activity should render", () => {
|
|||
|
||||
describe("Activity should not render", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pipeline -> Activity List', () => {
|
||||
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);
|
||||
|
||||
const runs = wrapper.find('Runs');
|
||||
|
|
|
@ -61,7 +61,7 @@ exports.initialize = function (oncomplete) {
|
|||
const appRoot = document.getElementsByTagName("head")[0].getAttribute("data-appurl");
|
||||
Extensions.init({
|
||||
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();
|
||||
};
|
||||
|
|
|
@ -8,13 +8,20 @@ export class ClassMetadataStore {
|
|||
* Type info cache
|
||||
*/
|
||||
this.classMetadata = {};
|
||||
|
||||
|
||||
/**
|
||||
* Fetch function for the classMetadata
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
@ -23,24 +30,45 @@ export class ClassMetadataStore {
|
|||
if (classMeta) {
|
||||
return 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];
|
||||
}
|
||||
onload(classMeta);
|
||||
});
|
||||
|
||||
var callbacks = this.classMetadataOnloadCallbacks[type];
|
||||
if (!callbacks) {
|
||||
// This is the first request for this type. Initialise the
|
||||
// callback cache and then issue the request to
|
||||
// the classMetadataProvider.
|
||||
callbacks = this.classMetadataOnloadCallbacks[type] = [];
|
||||
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) {
|
||||
return (extensions, onload) => {
|
||||
if (dataType && typeof(dataType) === 'object'
|
||||
&& '_class' in dataType) { // handle the common API incoming data
|
||||
dataType = dataType._class;
|
||||
}
|
||||
|
||||
|
||||
this.getClassMetadata(dataType, (currentTypeInfo) => {
|
||||
// prevent returning extensions for the given type
|
||||
// when a more specific extension is found
|
||||
|
|
Loading…
Reference in New Issue