1. Go into `blueocean-plugin` and run `mvn hpi:run` in a terminal. (mvn clean install from the root of the project is always a good idea regularly!)
2. From this directory, run `gulp bundle:watch` to watch for JS changes and reload them.
3. Open browser to http://localhost:8080/jenkins/blue/ to see this
4. hack away. Refreshing the browser will pick up changes. If you add a new extension point or export a new extension you may need to restart the `mvn hpi:run` process.
Now you need to tell Storybook where it should load the stories from. For that, you need to add the new story to the configuration file `.storybook/config.js`:
To follow the explanations I recommend you are familiar with http://redux.js.org//docs/basics/index.html.
The basic idea behind our implementation is based on the three principles of redux http://redux.js.org/docs/introduction/ThreePrinciples.html:
#### 1. Single source of truth - The state of your whole application is stored in an object tree within a single store.
I implemented our single source of truth in `blueocean-web/src/main/js/main.jsx` where we get all `jenkins.main.stores` extensions. If we have plugins that are implementing the redux store then we use this information (basically we chain the exposed reducer - see 3.) to configure the store.
In `blueocean-dashboard/src/main/js/redux/actions.js` we have defined all admin related actions. We call some of this actions e.g. `fetchRunsIfNeeded` from the view e.g. `Activity.jsx`
#### 3. Changes are made with pure functions - To specify how the state tree is transformed by actions, you write pure reducers.
> Reducers are just pure functions that take the previous state and an action, and return the next state. Remember to return new state objects, instead of mutating the previous state.
`blueocean-dashboard/src/main/js/redux/reducer.js` here we define all the reducer we are currently using in the admin app and expose them. To follow along the above code snippet from `Activity.jsx` the `fetchRunsIfNeeded` looks like:
If you have followed a bit the admin app this code reminds a lot of the old "fetch" aka AjaxHoc aka ghettoAjax code. The basic idea is to see if we already have the data in our state `const data = getState().adminStore[general.type];` and if so dispatch `ACTION_TYPES.SET_CURRENT_RUN_DATA` with `payload: data[id],` if not we go ahead and `fetch(general.url)` and using promises to finally dispatch first `ACTION_TYPES.SET_CURRENT_RUN_DATA` and then `ACTION_TYPES.SET_RUNS_DATA`
```
[ACTION_TYPES.SET_CURRENT_RUN_DATA](state, { payload }): State {
return state.set('currentRuns', payload);
},
[ACTION_TYPES.SET_RUNS_DATA](state, { payload, id }): State {
const runs = state.get('runs') || {};
runs[id] = payload;
return state.set('runs', runs);
},
```
Now coming back to reducers you see in the end we return a new State were we set 'currentRuns' and 'runs'. Since actions are not synchronous we are exposing the runs/currentRuns in the reducer as
```
export const runs = createSelector([adminStore], store => store.runs);
```
We use https://github.com/reactjs/reselect here to compute derived data, allowing Redux to store the minimal possible state. For example to see whether the current pipeline is a multibranch pipe:
```
export const isMultiBranch = createSelector(
[pipeline], (pipe) => {
if (pipe && pipe.organization) {
return !!pipe.branchNames;
}
return null;
}
);
```
In e.g. `RunDetails.jsx` we are using both selector like: