Compare commits

...

5 Commits

7 changed files with 235 additions and 5 deletions

View File

@ -89,6 +89,7 @@ const extractPath = (path, begin, end) => {
};
/**
* Renders a stack of "favorites cards" including current most recent status.
*/
export class DashboardCards extends Component {
@ -112,7 +113,7 @@ export class DashboardCards extends Component {
let branchName;
if (pipeline._class === 'io.jenkins.blueocean.rest.impl.pipeline.BranchImpl') {
// branch.fullName is in the form folder1/folder2/pipeline/branch ...
// pipeline.fullName is in the form folder1/folder2/pipeline/branch ...
// "pipeline"
pipelineName = extractPath(pipeline.fullName, -2, -1);
// everything up to "branch"
@ -166,8 +167,8 @@ export class DashboardCards extends Component {
return (
<div className="favorites-card-stack">
<TransitionGroup transitionName="vertical-expand-collapse"
transitionEnterTimeout={150}
transitionLeaveTimeout={150}
transitionEnterTimeout={300}
transitionLeaveTimeout={300}
>
{favoriteCards}
</TransitionGroup>

View File

@ -9,6 +9,8 @@ import { List } from 'immutable';
import { userSelector, favoritesSelector } from '../redux/FavoritesStore';
import { actions } from '../redux/FavoritesActions';
import favoritesSseListener from '../model/FavoritesSseListener';
/**
* FavoritesProvider ensures that the current user's favorites
* are loaded for any components which may need it.
@ -20,12 +22,17 @@ export class FavoritesProvider extends Component {
componentWillMount() {
this._initialize(this.props);
favoritesSseListener.initialize(this.props.updateRun);
}
componentWillReceiveProps(props) {
this._initialize(props);
}
componentWillUnmount() {
favoritesSseListener.dispose();
}
_initialize(props) {
const { user, favorites } = props;
@ -54,6 +61,7 @@ FavoritesProvider.propTypes = {
favorites: PropTypes.instanceOf(List),
fetchUser: PropTypes.func,
fetchFavorites: PropTypes.func,
updateRun: PropTypes.func,
};
const selectors = createSelector(

View File

@ -0,0 +1,28 @@
/**
* Created by cmeyers on 7/29/16.
*/
import { SseBus } from './SseBus';
import fetch from 'isomorphic-fetch';
import * as sse from '@jenkins-cd/sse-gateway';
class FavoritesSseListener {
initialize(listener) {
if (!this.sseBus) {
this.sseBus = new SseBus(sse, fetch);
this.sseBus.subscribeToJob(listener);
}
}
dispose() {
if (this.sseBus) {
this.sseBus.dispose();
this.sseBus = null;
}
}
}
const instance = new FavoritesSseListener();
export default instance;

View File

@ -0,0 +1,159 @@
/**
* Created by cmeyers on 7/29/16.
*/
import defaultFetch from 'isomorphic-fetch';
/**
* Wraps the SSE Gateway and fetches data related to events from REST API.
* TODO: should probably send additional data *and* the original event to callback
*/
export class SseBus {
constructor(sse, fetch) {
this.sse = sse;
this.fetch = fetch || defaultFetch;
this.jobListenerSse = null;
this.jobListenerExternal = null;
}
dispose() {
if (this.jobListenerSse) {
this.sse.unsubscribe(this.jobListenerSse);
this.jobListenerSse = null;
}
if (this.jobListenerExternal) {
this.jobListenerExternal = null;
}
}
subscribeToJob(callback) {
console.log('subscribeToJob with ', callback);
this.jobListenerExternal = callback;
this.jobListenerSse = this.sse.subscribe('job', (event) => {
this._handleJobEvent(event);
});
}
_handleJobEvent(event) {
switch (event.jenkins_event) {
case 'job_crud_created':
case 'job_crud_deleted':
case 'job_crud_renamed':
this._refetchPipelines();
break;
case 'job_run_queue_buildable':
case 'job_run_queue_enter':
this._enqueueJob(event);
break;
case 'job_run_queue_left':
case 'job_run_queue_blocked': {
break;
}
case 'job_run_started': {
this._updateJob(event);
break;
}
case 'job_run_ended': {
this._updateJob(event);
break;
}
default :
// Else ignore the event.
}
}
_refetchPipelines() {
// TODO: implement once migration into commons JS
}
_enqueueJob(event) {
const newRun = {
event,
};
newRun.pipeline = event.job_ismultibranch ?
event.blueocean_job_branch_name :
event.blueocean_job_pipeline_name;
const baseUrl = '/blue';
const runUrl = cleanSlashes(`${baseUrl}/${event.blueocean_job_rest_url}/runs/${event.job_run_queueId}`);
newRun._links = {
self: {
href: runUrl,
},
};
newRun.state = 'QUEUED';
newRun.result = 'UNKNOWN';
console.log('enqueueJob', event);
if (this.jobListenerExternal) {
this.jobListenerExternal(newRun);
}
}
_updateJob(event) {
const baseUrl = '/jenkins/blue';
const url = cleanSlashes(`${baseUrl}/${event.blueocean_job_rest_url}/runs/${event.jenkins_object_id}`);
this.fetch(url)
.then(checkStatus)
.then(parseJSON)
.then((data) => {
console.log('updateJob', event, data);
if (event.jenkins_event === 'job_run_ended') {
data.state = 'FINISHED';
} else {
data.state = 'RUNNING';
}
if (this.jobListenerExternal) {
this.jobListenerExternal(data);
}
});
}
_updateMultiBranchPipelineBranches() {
// TODO: implement once migration into commons JS
}
}
// TODO: migrate all this code down to 'fetch'
function checkStatus(response) {
if (response.status >= 300 || response.status < 200) {
const error = new Error(response.statusText);
error.response = response;
throw error;
}
return response;
}
function parseJSON(response) {
return response.json()
// FIXME: workaround for status=200 w/ empty response body that causes error in Chrome
// server should probably return HTTP 204 instead
.catch((error) => {
if (error.message === 'Unexpected end of JSON input') {
return {};
}
throw error;
});
}
export const cleanSlashes = (url) => {
if (url.indexOf('//') !== -1) {
let cleanUrl = url.replace('//', '/');
cleanUrl = cleanUrl.substr(-1) === '/' ?
cleanUrl : `${cleanUrl}/`;
return cleanSlashes(cleanUrl);
}
return url;
};

View File

@ -105,6 +105,15 @@ export const actions = {
};
},
updateRun(jobRun) {
return (dispatch) => {
dispatch({
type: ACTION_TYPES.UPDATE_RUN,
jobRun,
});
};
},
generateData(request, actionType, optional) {
const { url, fetchOptions } = request;
return (dispatch) => fetch(url, fetchOptions)

View File

@ -21,8 +21,13 @@ export const ACTION_TYPES = keymirror({
SET_USER: null,
SET_FAVORITES: null,
TOGGLE_FAVORITE: null,
UPDATE_RUN: null,
});
function clone(json) {
return JSON.parse(JSON.stringify(json));
}
const actionHandlers = {
[ACTION_TYPES.SET_USER](state, { payload }) {
const user = new User(payload);
@ -52,6 +57,26 @@ const actionHandlers = {
return state.set('favorites', prunedList);
},
[ACTION_TYPES.UPDATE_RUN](state, { jobRun }) {
const favorites = state.get('favorites');
for (const fav of favorites) {
const runsBaseUrl = `${fav.item._links.self.href}runs`;
const runUrl = jobRun._links.self.href;
// TODO; this might be broken for non-multibranch as the URL structures are different
if (runUrl.indexOf(runsBaseUrl) === 0) {
const index = favorites.indexOf(fav);
const updatedFavorite = clone(fav);
updatedFavorite.item.latestRun = jobRun;
const updatedFavorites = favorites.set(index, updatedFavorite);
return state.set('favorites', updatedFavorites);
}
}
console.warn('run was not updated; likely an error?');
return state;
},
};
const favoritesStore = state => state.favoritesStore;

View File

@ -13,7 +13,7 @@
}
.vertical-expand-collapse-enter {
transition: all linear 0.15s;
transition: all linear 0.3s;
max-height: 0;
opacity: 0.01;
@ -24,7 +24,7 @@
}
.vertical-expand-collapse-leave {
transition: all linear 0.15s;
transition: all linear 0.3s;
max-height: 60px;
opacity: 1;