[Code] infinite scroll for commit history (#28749)

This commit is contained in:
Yulong 2019-01-16 19:05:48 +08:00 committed by GitHub
parent 0753b430e5
commit fb703b11b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 160 additions and 31 deletions

View file

@ -55,8 +55,8 @@
"@types/mocha": "^5.2.5",
"@types/nock": "^9.3.0",
"@types/node": "^10.12.12",
"@types/nodegit": "^0.22.1",
"@types/node-fetch": "^2.1.4",
"@types/nodegit": "^0.22.1",
"@types/papaparse": "^4.5.5",
"@types/pngjs": "^3.3.1",
"@types/prop-types": "^15.5.3",
@ -64,6 +64,7 @@
"@types/react": "16.3.14",
"@types/react-datepicker": "^1.1.5",
"@types/react-dom": "^16.0.5",
"@types/react-infinite-scroller": "^1.2.0",
"@types/react-redux": "^6.0.6",
"@types/react-router-dom": "^4.3.1",
"@types/react-test-renderer": "^16.0.3",
@ -234,8 +235,8 @@
"moment-timezone": "^0.5.14",
"monaco-editor": "^0.14.3",
"ngreact": "^0.5.1",
"nodegit": "git+https://github.com/elastic/nodegit.git#v0.24.0-alpha.6",
"node-fetch": "^2.1.2",
"nodegit": "git+https://github.com/elastic/nodegit.git#v0.24.0-alpha.6",
"nodemailer": "^4.6.4",
"object-path-immutable": "^0.5.3",
"oppsy": "^2.0.0",
@ -258,6 +259,7 @@
"react-datetime": "^2.14.0",
"react-dom": "^16.3.0",
"react-dropzone": "^4.2.9",
"react-infinite-scroller": "^1.2.4",
"react-markdown": "^3.4.1",
"react-markdown-renderer": "^1.4.0",
"react-portal": "^3.2.0",

View file

@ -63,7 +63,11 @@ export const fetchDirectoryFailed = createAction<Error>('FETCH REPO DIR FAILED')
export const setNotFound = createAction<boolean>('SET NOT FOUND');
export const fetchTreeCommits = createAction<FetchFilePayload>('FETCH TREE COMMITS');
export const fetchTreeCommitsSuccess = createAction<{ path: string; commits: CommitInfo[] }>(
'FETCH TREE COMMITS SUCCESS'
);
export const fetchTreeCommitsSuccess = createAction<{
path: string;
commits: CommitInfo[];
append?: boolean;
}>('FETCH TREE COMMITS SUCCESS');
export const fetchTreeCommitsFailed = createAction<Error>('FETCH TREE COMMITS FAILED');
export const fetchMoreCommits = createAction<string>('FETCH MORE COMMITS');

View file

@ -128,7 +128,7 @@ export const CommitHistory = (props: {
);
}
const commits = _.groupBy(props.commits, commit => moment(commit.updated).format('YYYYMMDD'));
const commitDates = Object.keys(commits).sort();
const commitDates = Object.keys(commits).sort((a, b) => b.localeCompare(a)); // sort desc
const commitList = commitDates.map(cd => (
<CommitGroup commits={commits[cd]} date={moment(cd).format('MMMM Do, YYYY')} key={cd} />
));

View file

@ -7,6 +7,7 @@
// @ts-ignore
import { EuiButton, EuiButtonGroup } from '@elastic/eui';
import React from 'react';
import InfiniteScroll from 'react-infinite-scroller';
import Markdown from 'react-markdown';
import { connect } from 'react-redux';
import { RouteComponentProps } from 'react-router';
@ -15,10 +16,10 @@ import styled from 'styled-components';
import { GitBlame } from '../../../common/git_blame';
import { FileTree } from '../../../model';
import { CommitInfo } from '../../../model/commit';
import { FetchFileResponse } from '../../actions';
import { FetchFileResponse, fetchMoreCommits } from '../../actions';
import { MainRouteParams, PathTypes } from '../../common/types';
import { RootState } from '../../reducers';
import { treeCommitsSelector } from '../../selectors';
import { hasMoreCommitsSelector, treeCommitsSelector } from '../../selectors';
import { Editor } from '../editor/editor';
import { Blame } from './blame';
import { CommitHistory } from './commit_history';
@ -61,6 +62,9 @@ interface Props extends RouteComponentProps<MainRouteParams> {
file: FetchFileResponse | undefined;
commits: CommitInfo[];
blames: GitBlame[];
hasMoreCommits: boolean;
loadingCommits: boolean;
fetchMoreCommits(repoUri: string): void;
}
interface State {
@ -169,7 +173,7 @@ class CodeContent extends React.PureComponent<Props, State> {
};
public render() {
const { file, blames, commits, match, tree } = this.props;
const { file, blames, commits, match, tree, hasMoreCommits, loadingCommits } = this.props;
const { path, pathType, resource, org, repo } = match.params;
const repoUri = `${resource}/${org}/${repo}`;
if (pathType === PathTypes.tree) {
@ -177,16 +181,29 @@ class CodeContent extends React.PureComponent<Props, State> {
return (
<DirectoryViewContainer>
<Directory node={node} />
<CommitHistory
commits={commits}
repoUri={repoUri}
header={
<React.Fragment>
<Title>Recent Commits</Title>
<EuiButton>View All</EuiButton>
</React.Fragment>
<InfiniteScroll
pageStart={0}
initialLoad={false}
loadMore={() => !loadingCommits && this.props.fetchMoreCommits(repoUri)}
hasMore={hasMoreCommits}
useWindow={false}
loader={
<div className="loader" key={0}>
Loading ...
</div>
}
/>
>
<CommitHistory
commits={commits}
repoUri={repoUri}
header={
<React.Fragment>
<Title>Recent Commits</Title>
<EuiButton>View All</EuiButton>
</React.Fragment>
}
/>
</InfiniteScroll>
</DirectoryViewContainer>
);
} else if (pathType === PathTypes.blob) {
@ -194,11 +211,24 @@ class CodeContent extends React.PureComponent<Props, State> {
return (
<React.Fragment>
{this.renderButtons()}
<CommitHistory
commits={commits}
repoUri={repoUri}
header={<Title>Commit History</Title>}
/>
<InfiniteScroll
pageStart={0}
initialLoad={true}
loadMore={() => !loadingCommits && this.props.fetchMoreCommits(repoUri)}
hasMore={hasMoreCommits}
useWindow={true}
loader={
<div className="loader" key={0}>
Loading ...
</div>
}
>
<CommitHistory
commits={commits}
repoUri={repoUri}
header={<Title>Commit History</Title>}
/>
</InfiniteScroll>
</React.Fragment>
);
}
@ -247,6 +277,17 @@ const mapStateToProps = (state: RootState) => ({
tree: state.file.tree,
commits: treeCommitsSelector(state),
blames: state.blame.blames,
hasMoreCommits: hasMoreCommitsSelector(state),
loadingCommits: state.file.loadingCommits,
});
export const Content = withRouter(connect(mapStateToProps)(CodeContent));
const mapDispatchToProps = {
fetchMoreCommits,
};
export const Content = withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(CodeContent)
);

View file

@ -22,6 +22,8 @@ import {
fetchRepoTree,
fetchRepoTreeFailed,
fetchRepoTreeSuccess,
fetchTreeCommits,
fetchTreeCommitsFailed,
fetchTreeCommitsSuccess,
openTreePath,
RepoTreePayload,
@ -43,6 +45,8 @@ export interface FileState {
treeCommits: { [path: string]: CommitInfo[] };
currentPath: string;
requestedPaths: string[];
loadingCommits: boolean;
commitsFullyLoaded: { [path: string]: boolean };
}
const initialState: FileState = {
@ -61,6 +65,8 @@ const initialState: FileState = {
isNotFound: false,
currentPath: '',
requestedPaths: [],
loadingCommits: false,
commitsFullyLoaded: {},
};
function mergeTree(draft: FileState, tree: FileTree, path: string) {
@ -127,6 +133,7 @@ export const file = handleActions(
[String(fetchRepoCommitsSuccess)]: (state: FileState, action: any) =>
produce<FileState>(state, draft => {
draft.commits = action.payload;
draft.loadingCommits = false;
}),
[String(fetchRepoBranchesSuccess)]: (state: FileState, action: any) =>
produce<FileState>(state, (draft: FileState) => {
@ -163,10 +170,35 @@ export const file = handleActions(
produce<FileState>(state, draft => {
draft.isNotFound = false;
}),
[String(fetchTreeCommits)]: (state: FileState) =>
produce<FileState>(state, draft => {
draft.loadingCommits = true;
}),
[String(fetchTreeCommitsFailed)]: (state: FileState) =>
produce<FileState>(state, draft => {
draft.loadingCommits = false;
}),
[String(fetchTreeCommitsSuccess)]: (state: FileState, action: any) =>
produce<FileState>(state, draft => {
const { path, commits } = action.payload;
draft.treeCommits[path] = commits;
const { path, commits, append } = action.payload;
if (path === '' || path === '/') {
if (commits.length === 0) {
draft.commitsFullyLoaded[''] = true;
} else if (append) {
draft.commits = draft.commits.concat(commits);
} else {
draft.commits = commits;
}
} else {
if (commits.length === 0) {
draft.commitsFullyLoaded[path] = true;
} else if (append) {
draft.treeCommits[path] = draft.treeCommits[path].concat(commits);
} else {
draft.treeCommits[path] = commits;
}
}
draft.loadingCommits = false;
}),
},
initialState

View file

@ -18,6 +18,7 @@ import {
FetchFilePayload,
FetchFileResponse,
fetchFileSuccess,
fetchMoreCommits,
fetchRepoBranches,
fetchRepoBranchesFailed,
fetchRepoBranchesSuccess,
@ -38,7 +39,8 @@ import {
openTreePath,
setNotFound,
} from '../actions';
import { requestedPathsSelector } from '../selectors';
import { RootState } from '../reducers';
import { requestedPathsSelector, treeCommitsSelector } from '../selectors';
import { repoRoutePattern } from './patterns';
function* handleFetchRepoTree(action: Action<FetchRepoTreePayload>) {
@ -132,6 +134,20 @@ function* handleFetchCommits(action: Action<FetchRepoPayloadWithRevision>) {
}
}
function* handleFetchMoreCommits(action: Action<string>) {
try {
const path = yield select((state: RootState) => state.file.currentPath);
const commits = yield select(treeCommitsSelector);
const revision = commits.length > 0 ? commits[commits.length - 1].id : 'head';
const uri = action.payload;
// @ts-ignore
const newCommits = yield call(requestCommits, { uri, revision }, path, true);
yield put(fetchTreeCommitsSuccess({ path, commits: newCommits, append: true }));
} catch (err) {
yield put(fetchTreeCommitsFailed(err));
}
}
function* handleFetchTreeCommits(action: Action<FetchFilePayload>) {
try {
const path = action.payload!.path;
@ -142,11 +158,19 @@ function* handleFetchTreeCommits(action: Action<FetchFilePayload>) {
}
}
function requestCommits({ uri, revision }: FetchRepoPayloadWithRevision, path?: string) {
function requestCommits(
{ uri, revision }: FetchRepoPayloadWithRevision,
path?: string,
loadMore?: boolean
) {
const pathStr = path ? `/${path}` : '';
return kfetch({
const options: any = {
pathname: `../api/code/repo/${uri}/history/${revision}${pathStr}`,
});
};
if (loadMore) {
options.query = { after: 1 };
}
return kfetch(options);
}
export async function requestFile(
@ -215,6 +239,7 @@ export function* watchFetchBranchesAndCommits() {
yield takeLatest(String(fetchFile), handleFetchFile);
yield takeEvery(String(fetchDirectory), handleFetchDirs);
yield takeLatest(String(fetchTreeCommits), handleFetchTreeCommits);
yield takeLatest(String(fetchMoreCommits), handleFetchMoreCommits);
}
function* handleRepoRouteChange(action: Action<Match>) {

View file

@ -60,4 +60,9 @@ export const treeCommitsSelector = (state: RootState) => {
}
};
export const hasMoreCommitsSelector = (state: RootState) => {
const path = state.file.currentPath;
return !state.file.commitsFullyLoaded[path];
};
export const requestedPathsSelector = (state: RootState) => state.file.requestedPaths;

View file

@ -142,6 +142,7 @@ export function fileRoute(server: hapi.Server, options: ServerOptions) {
const { uri, ref, path } = req.params;
const queries = req.query as RequestQuery;
const count = queries.count ? parseInt(queries.count as string, 10) : 10;
const after = queries.after !== undefined;
try {
const repository = await gitOperations.openRepo(uri);
const commit = await gitOperations.getCommit(repository, ref);
@ -151,12 +152,17 @@ export function fileRoute(server: hapi.Server, options: ServerOptions) {
let commits: Commit[];
if (path) {
// magic number 1000: how many commits at the most to iterate in order to find the commits contains the path
const results = await walk.fileHistoryWalk(path, 1000);
const results = await walk.fileHistoryWalk(path, 10000);
commits = results.slice(0, count).map(result => result.commit);
} else {
walk.push(commit.id());
commits = await walk.getCommits(count);
}
if (after && commits.length > 0) {
if (commits[0].id().equal(commit.id())) {
commits = commits.slice(1);
}
}
return commits.map(commitInfo);
} catch (e) {
if (e.isBoom) {

View file

@ -1784,6 +1784,13 @@
"@types/node" "*"
"@types/react" "*"
"@types/react-infinite-scroller@^1.2.0":
version "1.2.0"
resolved "http://registry.npm.taobao.org/@types/react-infinite-scroller/download/@types/react-infinite-scroller-1.2.0.tgz#895d2822bb6e32f024ea258f60e4f75c3fbf37dc"
integrity sha1-iV0oIrtuMvAk6iWPYOT3XD+/N9w=
dependencies:
"@types/react" "*"
"@types/react-intl@^2.3.11":
version "2.3.11"
resolved "https://registry.yarnpkg.com/@types/react-intl/-/react-intl-2.3.11.tgz#8d9e8a5931c665ce9086cbcda5b7467d668dfa33"
@ -18031,6 +18038,13 @@ react-grid-layout@^0.16.2:
react-draggable "3.x"
react-resizable "1.x"
react-infinite-scroller@^1.2.4:
version "1.2.4"
resolved "http://registry.npm.taobao.org/react-infinite-scroller/download/react-infinite-scroller-1.2.4.tgz#f67eaec4940a4ce6417bebdd6e3433bfc38826e9"
integrity sha1-9n6uxJQKTOZBe+vdbjQzv8OIJuk=
dependencies:
prop-types "^15.5.8"
react-input-autosize@^2.1.2, react-input-autosize@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8"