[JENKINS-36921] first draft of mobx for personalization that proves out mobx patterns; this uses extension points and new store logic in blueocean-web to provide "mobxStores" to favorites components; per discussion w/ team we'll back out the store/context code in subsequent commit and keep stores from personalization "local" (pulled in via require)

This commit is contained in:
Cliff Meyers 2016-07-26 12:18:49 -04:00
parent a934db43c7
commit 9d719924a6
10 changed files with 183 additions and 73 deletions

View File

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

View File

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

View File

@ -2,18 +2,12 @@
* Created by cmeyers on 7/6/16.
*/
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { observer } from 'mobx-react';
import { List } from 'immutable';
import { favoritesSelector } from '../redux/FavoritesStore';
import { actions } from '../redux/FavoritesActions';
import { FavoritesList } from '../model/FavoritesList';
import FavoritesProvider from './FavoritesProvider';
import { PipelineCard } from './PipelineCard';
import ViewFavoritesList from './ViewFavoritesList';
// the order the cards should be displayed based on their result/state (aka 'status')
const statusSortOrder = [
@ -90,22 +84,21 @@ const extractPath = (path, begin, end) => {
}
};
const favoritesList = new FavoritesList();
/**
*/
@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;
@ -174,24 +167,14 @@ export class DashboardCards extends Component {
render() {
return (
<div>
<FavoritesProvider store={this.props.store}>
{ this._renderCardStack() }
</FavoritesProvider>
<ViewFavoritesList list={favoritesList} />
{ this._renderCardStack() }
</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;

View File

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

View File

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

View File

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

View File

@ -1,14 +1,19 @@
/**
* Created by cmeyers on 7/25/16.
*/
import { action, computed, observable, useStrict } from 'mobx';
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',
};
@ -34,9 +39,18 @@ function parseJSON(response) {
});
}
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();
@ -44,24 +58,68 @@ export class FavoritesList {
@action
_init() {
this.favorites = [];
this.favorites = new List();
}
// TODO: determine why useStrict triggers an error here
@action
loadFavorites() {
fetchCurrentUser() {
const baseUrl = urlConfig.blueoceanAppURL;
const url = `${baseUrl}/rest/users/cmeyers/favorites/`;
const fetchOptions = { ...defaultFetchOptions };
const url = `${baseUrl}/rest/organizations/jenkins/user/`;
fetch(url, fetchOptions)
.then(checkStatus)
.then(parseJSON)
.then(action((json) => {
this.favorites = json;
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;
}

View File

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

View File

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

View File

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