mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Code] infinite scroll for commit history (#28749)
This commit is contained in:
parent
0753b430e5
commit
fb703b11b8
9 changed files with 160 additions and 31 deletions
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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} />
|
||||
));
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
14
yarn.lock
14
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue