[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:
parent
a934db43c7
commit
9d719924a6
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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