Compare commits

...

15 Commits

Author SHA1 Message Date
Ivan Meredith 2c203185a0 Fix activity spec 2016-08-08 16:42:41 +12:00
Ivan Meredith 8cdc066ff9 Fix linting 2016-08-08 16:42:41 +12:00
Ivan Meredith b34d9cf59f Remove capabilies from State record 2016-08-08 16:42:41 +12:00
Ivan Meredith ee36ba8bdc Fix spelling mistake of IfCapibility 2016-08-08 16:42:41 +12:00
Ivan Meredith 54f8da6640 More changes to capabilityStore 2016-08-08 16:42:41 +12:00
Ivan Meredith 14286862d1 Compose capabilites into lifecycle 2016-08-08 16:42:41 +12:00
Ivan Meredith 6de0d01565 Fix spelling mistake in file name 2016-08-08 16:42:41 +12:00
Ivan Meredith 0d47f44ef7 Remove redux capabilites stuff 2016-08-08 16:42:41 +12:00
Ivan Meredith 42bf4f2f77 Make Activity use capibilites too
* Fixed linting
* Added Capabilites.js to list capibilities
2016-08-08 16:42:41 +12:00
tfennelly a82e176e9e Smarten up ClassMetadatStore wrt REST api calls
... so that it's not issuing a ton of parallel rest API calls to get the same type metadata
2016-08-08 16:41:05 +12:00
tfennelly 4ccbde30b6 Tweak IfCapability to use ClassMetadatStore from js-extensions
It's still making multiple rest API calls though because js-extensions is not being clever about batching up parallel/inflight requests. Instead it is issuing them all.
2016-08-08 16:39:27 +12:00
tfennelly d89f01010c Remove stray (?) call to fetchCapabilitiesIfNeeded in Activity.jsx 2016-08-08 16:39:27 +12:00
Ivan Meredith 41af06d617 Initial Commit of IfCapability Component 2016-08-08 16:39:27 +12:00
Ivan Meredith 766c449945 Fix linting issues 2016-08-08 16:39:27 +12:00
Ivan Meredith b0ac0a123b [JENKINS-36067] Hide branch col for non-multibranch activity view 2016-08-08 16:39:27 +12:00
10 changed files with 192 additions and 26 deletions

View File

@ -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",

View File

@ -0,0 +1,4 @@
export const MULTIBRANCH_PIPELINE = 'io.jenkins.blueocean.rest.model.BlueMultiBranchPipeline';
export const blah = 'aa';

View File

@ -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));

View File

@ -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} />;
}
};

View File

@ -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);

View File

@ -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>

View File

@ -848,7 +848,7 @@ export const actions = {
));
};
},
resetTestDetails() {
return (dispatch) =>
dispatch({

View File

@ -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');

View File

@ -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();
};

View File

@ -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