Compare commits

...

5 Commits

5 changed files with 92 additions and 20 deletions

View File

@ -6,7 +6,8 @@ import { ActivityRecord, ChangeSetRecord } from './records';
import RunPipeline from './RunPipeline.jsx';
import {
actions,
currentRuns as runsSelector,
currentRuns as currentRunsSelector,
runs as allRunsSelector,
createSelector,
connect,
} from '../redux';
@ -61,6 +62,12 @@ export class Activity extends Component {
}
}
fetchNextRuns() {
const pagination = this.props.allRuns[this.context.params.pipeline];
pagination.currentPage++;
this.props.fetchRunsIfNeeded(this.context.config);
}
render() {
const { runs, pipeline } = this.props;
// early out
@ -68,12 +75,18 @@ export class Activity extends Component {
return null;
}
const currentRunData = runs.currentData;
if (!currentRunData) {
return null;
}
// Only show the Run button for non multi-branch pipelines.
// Multi-branch pipelines have the Run/play button beside them on
// the Branches/PRs tab.
const showRunButton = (pipeline && !Pipeline.isMultibranch(pipeline));
if (!runs.length) {
if (!currentRunData.length) {
return (<EmptyState repoName={this.context.params.pipeline} showRunButton={showRunButton} pipeline={pipeline} />);
}
@ -93,7 +106,7 @@ export class Activity extends Component {
<article className="activity">
{showRunButton && <RunNonMultiBranchPipeline pipeline={pipeline} buttonText="Run" />}
<Table className="activity-table fixed" headers={headers}>
{ runs.map((run, index) => {
{ currentRunData.map((run, index) => {
const changeset = run.changeSet;
let latestRecord = {};
if (changeset && changeset.length > 0) {
@ -109,6 +122,7 @@ export class Activity extends Component {
return (<Runs {...props} />);
})}
</Table>
<button className="btn-show-more btn-secondary" onClick={() => this.fetchNextRuns()}>Show More</button>
</article>
</main>);
}
@ -121,11 +135,15 @@ Activity.contextTypes = {
};
Activity.propTypes = {
runs: array,
runs: object,
allRuns: object,
pipeline: object,
fetchRunsIfNeeded: func,
};
const selectors = createSelector([runsSelector], (runs) => ({ runs }));
const selectors = createSelector(
[currentRunsSelector, allRunsSelector],
(runs, allRuns) => ({ runs, allRuns })
);
export default connect(selectors, actions)(Activity);

View File

@ -86,7 +86,7 @@ class RunDetails extends Component {
const baseUrl = buildRunDetailsUrl(organization, name, branch, runId);
/* eslint-disable arrow-body-style */
const currentRun = this.props.runs.filter((run) => {
const currentRun = this.props.runs.currentData.filter((run) => {
return run.id === runId &&
decodeURIComponent(run.pipeline) === branch;
})[0];

View File

@ -5,6 +5,7 @@ import { State } from '../components/records';
import UrlConfig from '../config';
import { getNodesInformation } from '../util/logDisplayHelper';
import { calculateStepsBaseUrl, calculateLogUrl, calculateNodeBaseUrl } from '../util/UrlUtils';
import PaginationHolder from '../util/PaginationHolder';
// main actin logic
export const ACTION_TYPES = keymirror({
@ -56,8 +57,16 @@ export const actionHandlers = {
[ACTION_TYPES.CLEAR_CURRENT_RUN_DATA](state) {
return state.set('currentRuns', null);
},
[ACTION_TYPES.SET_CURRENT_RUN_DATA](state, { payload }): State {
return state.set('currentRuns', payload);
[ACTION_TYPES.SET_CURRENT_RUN_DATA](state, { payload, id, pageStart }): State {
const runs = state.runs || {};
let runData = runs[id];
if(!runData) {
runData = runs[id] = new PaginationHolder();
}
runData.appendData(pageStart, payload);
return state.set('currentRuns', runData);
},
[ACTION_TYPES.SET_NODE](state, { payload }): State {
return state.set('node', { ...payload });
@ -67,9 +76,15 @@ export const actionHandlers = {
nodes[payload.nodesBaseUrl] = payload;
return state.set('nodes', nodes);
},
[ACTION_TYPES.SET_RUNS_DATA](state, { payload, id }): State {
const runs = { ...state.runs } || {};
runs[id] = payload;
[ACTION_TYPES.SET_RUNS_DATA](state, { payload, id, pageStart }): State {
const runs = state.runs || {};
let runData = runs[id];
if(!runData) {
runData = runs[id] = new PaginationHolder();
}
runData.appendData(pageStart, payload);
return state.set('runs', runs);
},
[ACTION_TYPES.CLEAR_CURRENT_BRANCHES_DATA](state) {
@ -555,18 +570,23 @@ export const actions = {
},
fetchRunsIfNeeded(config) {
return (dispatch) => {
return (dispatch, getState) => {
const id = config.pipeline;
const alwaysFetch = true;
const runsData = getState().adminStore['runs'];
const pagination = !runsData ? new PaginationHolder() : (runsData[id] || new PaginationHolder());
const baseUrl = `${config.getAppURLBase()}/rest/organizations/jenkins` +
`/pipelines/${config.pipeline}/runs/`;
`/pipelines/${config.pipeline}/runs/?start=${pagination.getCurrentPageStart()}&limit=${pagination.pageSize}`;
return dispatch(actions.fetchIfNeeded({
url: baseUrl,
id: config.pipeline,
pageStart: pagination.getCurrentPageStart(),
type: 'runs',
}, {
current: ACTION_TYPES.SET_CURRENT_RUN_DATA,
general: ACTION_TYPES.SET_RUNS_DATA,
clear: ACTION_TYPES.CLEAR_CURRENT_RUN_DATA,
}));
}, alwaysFetch));
};
},
@ -592,14 +612,14 @@ export const actions = {
* @param types TODO: what's this and what's in it?
* @returns {Function}
*/
fetchIfNeeded(general, types) {
fetchIfNeeded(general, types, alwaysFetch = false) {
return (dispatch, getState) => {
const data = getState().adminStore[general.type];
dispatch({ type: types.clear });
const id = general.id;
if (!data || !data[id]) {
if (alwaysFetch || !data || !data[id]) {
return fetch(general.url, fetchOptions)
.then(checkStatus)
.then(parseJSON)
@ -609,11 +629,13 @@ export const actions = {
id,
payload: json,
type: types.current,
pageStart: general.pageStart,
});
return dispatch({
id,
payload: json,
type: types.general,
pageStart: general.pageStart,
});
})
.catch((error) => {

View File

@ -0,0 +1,27 @@
export default class PaginationHolder {
constructor(pageSize = 25) {
this.currentPage = 0;
this.pageSize = pageSize;
this.currentData;
this.hasMoreData = false;
}
getTotalPages() {
return this.currentData ? Math.floor(this.currentData.length / this.pageSize) : 0;
}
appendData(pageStart, data) {
// TODO figure out where it goes based on pageStart
this.currentData = this.currentData || [];
// NOTE: sometimes this array ref leaks across page holders
// and might be updated twice because of redux oddities.
// defend against duplicate additions
if (this.currentData.length <= pageStart) {
this.currentData = this.currentData.concat(data);
}
}
getCurrentPageStart() {
return this.currentPage * this.pageSize;
}
}

View File

@ -10,10 +10,10 @@
height: 100%;
box-sizing: border-box;
z-index: 1000;
&.not-found {
background: @blueocean-blue;
&:before {
content: '';
background: url('./icons/kanagawa.svg') no-repeat bottom left;
@ -29,7 +29,7 @@
height: 100%;
}
}
.message-box {
position: absolute;
top: 50%;
@ -40,13 +40,18 @@
padding: 2em;
border-radius: .2em;
box-shadow: 2px 2px 8px rgba(0, 0, 0, .1);
.message {
margin: 1em 0 2em 0;
}
}
}
.btn-show-more {
display: table;
margin: 10px auto;
}
.pipelines-table {
th {
width: 10%;