Compare commits
4 Commits
master
...
feature/JE
Author | SHA1 | Date |
---|---|---|
Cliff Meyers | 9d719924a6 | |
Cliff Meyers | a934db43c7 | |
Cliff Meyers | fb621d21b8 | |
Cliff Meyers | 18b5e48c3e |
|
@ -89,6 +89,7 @@ export default class PipelineRowItem extends Component {
|
|||
<Extensions.Renderer
|
||||
extensionPoint="jenkins.pipeline.list.action"
|
||||
store={this.context.store}
|
||||
mobxStores={this.context.mobxStores}
|
||||
pipeline={this.props.pipeline}
|
||||
/>
|
||||
</td>
|
||||
|
@ -104,5 +105,5 @@ PipelineRowItem.propTypes = {
|
|||
|
||||
PipelineRowItem.contextTypes = {
|
||||
location: PropTypes.object,
|
||||
store: PropTypes.object,
|
||||
mobxStores: PropTypes.object,
|
||||
};
|
||||
|
|
|
@ -60,6 +60,7 @@ export default class Pipelines extends Component {
|
|||
<Extensions.Renderer
|
||||
extensionPoint="jenkins.pipeline.list.top"
|
||||
store={this.context.store}
|
||||
mobxStores={this.context.mobxStores}
|
||||
/>
|
||||
<Table
|
||||
className="pipelines-table fixed"
|
||||
|
@ -88,4 +89,5 @@ Pipelines.contextTypes = {
|
|||
params: PropTypes.object,
|
||||
pipelines: array,
|
||||
store: PropTypes.object,
|
||||
mobxStores: PropTypes.object,
|
||||
};
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
{
|
||||
"presets": ["es2015", "stage-0", "react"]
|
||||
"presets": [
|
||||
"react",
|
||||
"es2015",
|
||||
"stage-0"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-decorators-legacy"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"babel": "^6.5.2",
|
||||
"babel-core": "^6.7.6",
|
||||
"babel-eslint": "^6.0.2",
|
||||
"babel-plugin-transform-decorators-legacy": "1.3.4",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
|
@ -41,6 +42,8 @@
|
|||
"immutable": "3.8.1",
|
||||
"isomorphic-fetch": "2.2.1",
|
||||
"keymirror": "0.1.1",
|
||||
"mobx": "2.4.0",
|
||||
"mobx-react": "3.5.1",
|
||||
"moment": "2.13.0",
|
||||
"moment-duration-format": "1.3.0",
|
||||
"react": "15.1.0",
|
||||
|
|
|
@ -2,12 +2,9 @@
|
|||
* Created by cmeyers on 7/6/16.
|
||||
*/
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { List } from 'immutable';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { favoritesSelector } from '../redux/FavoritesStore';
|
||||
import { actions } from '../redux/FavoritesActions';
|
||||
import { List } from 'immutable';
|
||||
|
||||
import FavoritesProvider from './FavoritesProvider';
|
||||
import { PipelineCard } from './PipelineCard';
|
||||
|
@ -89,18 +86,19 @@ const extractPath = (path, begin, end) => {
|
|||
|
||||
/**
|
||||
*/
|
||||
@observer
|
||||
export class DashboardCards extends Component {
|
||||
|
||||
_onFavoriteToggle(isFavorite, favorite) {
|
||||
this.props.toggleFavorite(isFavorite, favorite.item);
|
||||
this.props.favoritesList.toggleFavorite(isFavorite, favorite.item);
|
||||
}
|
||||
|
||||
_renderCardStack() {
|
||||
if (!this.props.favorites) {
|
||||
if (!this.props.favoritesList.favorites) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sortedFavorites = this.props.favorites.sort(sortComparator);
|
||||
const sortedFavorites = this.props.favoritesList.favorites.sort(sortComparator);
|
||||
|
||||
const favoriteCards = sortedFavorites.map(favorite => {
|
||||
const pipeline = favorite.item;
|
||||
|
@ -168,22 +166,15 @@ export class DashboardCards extends Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<FavoritesProvider store={this.props.store}>
|
||||
<div>
|
||||
{ this._renderCardStack() }
|
||||
</FavoritesProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DashboardCards.propTypes = {
|
||||
store: PropTypes.object,
|
||||
favorites: PropTypes.instanceOf(List),
|
||||
toggleFavorite: PropTypes.func,
|
||||
favoritesList: PropTypes.object,
|
||||
};
|
||||
|
||||
const selectors = createSelector(
|
||||
[favoritesSelector],
|
||||
(favorites) => ({ favorites })
|
||||
);
|
||||
|
||||
export default connect(selectors, actions)(DashboardCards);
|
||||
export default DashboardCards;
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Created by cmeyers on 7/26/16.
|
||||
*/
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import FavoritesProvider from './FavoritesProvider';
|
||||
import DashboardCards from './DashboardCards';
|
||||
|
||||
|
||||
/**
|
||||
* Restyled version of FavoritePipeline component.
|
||||
*/
|
||||
class FavoritesDashboard extends Component {
|
||||
render() {
|
||||
if (!this.props.mobxStores ||
|
||||
!this.props.mobxStores.favoritesStore ||
|
||||
!this.props.mobxStores.favoritesStore.favoritesList) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const favoritesList = this.props.mobxStores.favoritesStore.favoritesList;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FavoritesProvider favoritesList={favoritesList} />
|
||||
<DashboardCards favoritesList={favoritesList} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FavoritesDashboard.propTypes = {
|
||||
mobxStores: PropTypes.object,
|
||||
};
|
||||
|
||||
export default FavoritesDashboard;
|
|
@ -2,12 +2,7 @@
|
|||
* Created by cmeyers on 7/20/16.
|
||||
*/
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { List } from 'immutable';
|
||||
|
||||
import { userSelector, favoritesSelector } from '../redux/FavoritesStore';
|
||||
import { actions } from '../redux/FavoritesActions';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
/**
|
||||
* FavoritesProvider ensures that the current user's favorites
|
||||
|
@ -16,6 +11,7 @@ import { actions } from '../redux/FavoritesActions';
|
|||
* Components that require this data can simply wrap themselves in
|
||||
* FavoritesProvider which will ensure the store is updated correctly.
|
||||
*/
|
||||
@observer
|
||||
export class FavoritesProvider extends Component {
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -27,17 +23,17 @@ export class FavoritesProvider extends Component {
|
|||
}
|
||||
|
||||
_initialize(props) {
|
||||
const { user, favorites } = props;
|
||||
const { favoritesList } = props;
|
||||
const { user, favorites } = props.favoritesList;
|
||||
|
||||
const shouldFetchUser = !user;
|
||||
const shouldFetchFavorites = user && !favorites;
|
||||
|
||||
if (shouldFetchUser) {
|
||||
this.props.fetchUser();
|
||||
}
|
||||
|
||||
if (shouldFetchFavorites) {
|
||||
this.props.fetchFavorites(user);
|
||||
favoritesList.fetchCurrentUser()
|
||||
.then(() => {
|
||||
favoritesList.fetchFavorites();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,15 +46,7 @@ export class FavoritesProvider extends Component {
|
|||
|
||||
FavoritesProvider.propTypes = {
|
||||
children: PropTypes.node,
|
||||
user: PropTypes.object,
|
||||
favorites: PropTypes.instanceOf(List),
|
||||
fetchUser: PropTypes.func,
|
||||
fetchFavorites: PropTypes.func,
|
||||
favoritesList: PropTypes.object,
|
||||
};
|
||||
|
||||
const selectors = createSelector(
|
||||
[userSelector, favoritesSelector],
|
||||
(user, favorites) => ({ user, favorites })
|
||||
);
|
||||
|
||||
export default connect(selectors, actions)(FavoritesProvider);
|
||||
export default FavoritesProvider;
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Created by cmeyers on 7/25/16.
|
||||
*/
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
/**
|
||||
*/
|
||||
@observer class ViewFavoritesList extends Component {
|
||||
|
||||
onFetch() {
|
||||
this.props.list.loadFavorites();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.list.count}</div>
|
||||
<button onClick={() => this.onFetch()}>Fetch</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ViewFavoritesList.propTypes = {
|
||||
list: PropTypes.object,
|
||||
};
|
||||
|
||||
export default ViewFavoritesList;
|
|
@ -2,9 +2,9 @@
|
|||
# NB: "component" currently maps to modules, not "symbols" so make sure to "export default"
|
||||
# WARNING: If you change this you'll have to change io.jenkins.blueocean.jsextensions.JenkinsJSExtensionsTest as well :(
|
||||
extensions:
|
||||
- component: redux/FavoritesStore
|
||||
extensionPoint: jenkins.main.stores
|
||||
- component: components/DashboardCards
|
||||
- component: model/PersonalizationStore
|
||||
extensionPoint: jenkins.main.stores.mobx
|
||||
- component: components/FavoritesDashboard
|
||||
extensionPoint: jenkins.pipeline.list.top
|
||||
- component: components/FavoritePipeline
|
||||
extensionPoint: jenkins.pipeline.list.action
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* Created by cmeyers on 7/25/16.
|
||||
*/
|
||||
import Immutable from 'immutable';
|
||||
import fetch from 'isomorphic-fetch';
|
||||
import { action, computed, observable, useStrict } from 'mobx';
|
||||
useStrict(true);
|
||||
|
||||
import urlConfig from '../config';
|
||||
urlConfig.loadConfig();
|
||||
|
||||
import { User } from '../model/User';
|
||||
import { checkMatchingFavoriteUrls } from '../util/FavoriteUtils';
|
||||
|
||||
const { List } = Immutable;
|
||||
|
||||
const defaultFetchOptions = {
|
||||
credentials: 'same-origin',
|
||||
};
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
function execFetch(url, options) {
|
||||
const fetchOptions = options || { ... defaultFetchOptions };
|
||||
|
||||
return fetch(url, fetchOptions)
|
||||
.then(checkStatus)
|
||||
.then(parseJSON);
|
||||
}
|
||||
|
||||
export class FavoritesList {
|
||||
|
||||
@observable favorites;
|
||||
user = null;
|
||||
|
||||
constructor() {
|
||||
this._init();
|
||||
}
|
||||
|
||||
@action
|
||||
_init() {
|
||||
this.favorites = new List();
|
||||
}
|
||||
|
||||
fetchCurrentUser() {
|
||||
const baseUrl = urlConfig.blueoceanAppURL;
|
||||
const url = `${baseUrl}/rest/organizations/jenkins/user/`;
|
||||
|
||||
return execFetch(url)
|
||||
.then((data) => {
|
||||
this.user = new User(data);
|
||||
});
|
||||
}
|
||||
|
||||
fetchFavorites() {
|
||||
const baseUrl = urlConfig.blueoceanAppURL;
|
||||
const username = this.user.id;
|
||||
const url = `${baseUrl}/rest/users/${username}/favorites/`;
|
||||
|
||||
return execFetch(url)
|
||||
.then(action((data) => {
|
||||
this.favorites = new List(data);
|
||||
}));
|
||||
}
|
||||
|
||||
toggleFavorite(addFavorite, pipelineOrBranch) {
|
||||
const baseUrl = urlConfig.jenkinsRootURL;
|
||||
const url = `${baseUrl}${pipelineOrBranch._links.self.href}/favorite`;
|
||||
|
||||
const fetchOptions = {
|
||||
...defaultFetchOptions,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(
|
||||
{ favorite: addFavorite }
|
||||
),
|
||||
};
|
||||
|
||||
return execFetch(url, fetchOptions)
|
||||
.then((favoritePayload) => {
|
||||
this._updateToggledFavorite(addFavorite, pipelineOrBranch, favoritePayload);
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
_updateToggledFavorite(addFavorite, pipelineOrBranch, favoritePayload) {
|
||||
if (addFavorite) {
|
||||
this.favorites = this.favorites.push(favoritePayload);
|
||||
}
|
||||
|
||||
const toggledBranchHref = pipelineOrBranch._links.self.href;
|
||||
// filter the list so that only favorites which didn't match the branch's href are returned
|
||||
this.favorites = this.favorites.filter(fav => {
|
||||
const favoritedBranch = fav.item;
|
||||
return !checkMatchingFavoriteUrls(
|
||||
favoritedBranch._links.self.href,
|
||||
toggledBranchHref,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@computed get count() {
|
||||
return this.favorites && this.favorites.length || 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Created by cmeyers on 7/25/16.
|
||||
*/
|
||||
import { FavoritesList } from './FavoritesList';
|
||||
|
||||
class PersonalizationStore {
|
||||
|
||||
favoritesList = null;
|
||||
|
||||
constructor() {
|
||||
this.favoritesList = new FavoritesList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
favoritesStore: new PersonalizationStore(),
|
||||
};
|
|
@ -31,6 +31,8 @@
|
|||
"history": "2.0.2",
|
||||
"immutable": "3.8.1",
|
||||
"keymirror": "0.1.1",
|
||||
"mobx": "2.4.0",
|
||||
"mobx-react": "3.5.1",
|
||||
"moment": "2.13.0",
|
||||
"react": "15.1.0",
|
||||
"react-addons-css-transition-group": "15.1.0",
|
||||
|
|
|
@ -2,7 +2,8 @@ import React, { Component, PropTypes } from 'react';
|
|||
import { render } from 'react-dom';
|
||||
import { Router, Route, Link, useRouterHistory, IndexRedirect } from 'react-router';
|
||||
import { createHistory } from 'history';
|
||||
import { Provider, configureStore, combineReducers} from './redux';
|
||||
import { Provider as ReduxProvider, configureStore, combineReducers} from './redux';
|
||||
import { Provider as MobXProvider } from 'mobx-react';
|
||||
import { DevelopmentFooter } from './DevelopmentFooter';
|
||||
|
||||
import Extensions from '@jenkins-cd/js-extensions';
|
||||
|
@ -75,7 +76,7 @@ function makeRoutes(routes) {
|
|||
}
|
||||
|
||||
|
||||
function startApp(routes, stores) {
|
||||
function startApp(routes, stores, mobxStores) {
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
const headElement = document.getElementsByTagName("head")[0];
|
||||
|
@ -117,6 +118,21 @@ function startApp(routes, stores) {
|
|||
);
|
||||
}
|
||||
|
||||
// build up a 'master store' of all registered component stores
|
||||
let mobxMasterStore = {};
|
||||
if (mobxStores.length > 0) {
|
||||
/*
|
||||
TODO: watch for store name collisions and warn
|
||||
for (const store of mobxStores) {
|
||||
for (const storeName in store) {
|
||||
console.log(storeName);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
mobxMasterStore = Object.assign(mobxMasterStore, ...mobxStores);
|
||||
}
|
||||
|
||||
// on each change of the url we need to update the location object
|
||||
history.listen(newLocation => {
|
||||
const { dispatch, getState } = store;
|
||||
|
@ -137,12 +153,19 @@ function startApp(routes, stores) {
|
|||
|
||||
// Start React
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Router history={history}>{ makeRoutes(routes) }</Router>
|
||||
</Provider>
|
||||
, rootElement);
|
||||
React.cloneElement(
|
||||
<MobXProvider>
|
||||
<ReduxProvider store={store}>
|
||||
<Router history={history}>{ makeRoutes(routes) }</Router>
|
||||
</ReduxProvider>
|
||||
</MobXProvider>,
|
||||
mobxMasterStore
|
||||
),
|
||||
rootElement);
|
||||
}
|
||||
|
||||
Extensions.store.getExtensions(['jenkins.main.routes', 'jenkins.main.stores'], (routes = [], stores = []) => {
|
||||
startApp(routes, stores);
|
||||
Extensions.store.getExtensions(
|
||||
['jenkins.main.routes', 'jenkins.main.stores', 'jenkins.main.stores.mobx'],
|
||||
(routes = [], stores = [], mobxStores =[]) => {
|
||||
startApp(routes, stores, mobxStores);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue