Compare commits
5 Commits
master
...
feature/JE
Author | SHA1 | Date |
---|---|---|
Cliff Meyers | de36bfab87 | |
Cliff Meyers | 93a0968449 | |
Cliff Meyers | 820d9d7c9f | |
Cliff Meyers | 7e1a81e928 | |
Cliff Meyers | fcc0a421a4 |
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
};
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in New Issue