[Code] minor fixes to search page (#27798)

This commit is contained in:
Mengwei Ding 2018-12-27 12:06:50 -08:00
parent 56ad6c61e9
commit 6a168a3a0c
9 changed files with 170 additions and 194 deletions

View file

@ -4,10 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiTab } from '@elastic/eui';
import { EuiFlexGroup, EuiTab, EuiTabs } from '@elastic/eui';
import querystring from 'querystring';
import React from 'react';
import { Link } from 'react-router-dom';
import Url from 'url';
import { SearchScope } from '../../common/types';
@ -19,33 +18,40 @@ interface Props {
}
export class ScopeTab extends React.PureComponent<Props> {
public onPageClicked = (page: number) => {
const { query } = this.props;
const queries = querystring.parse(history.location.search.replace('?', ''));
history.push(
Url.format({
pathname: '/search',
query: {
...queries,
q: query,
p: page + 1,
},
})
);
public onTabClicked = (scope: SearchScope) => {
return () => {
const { query } = this.props;
const queries = querystring.parse(history.location.search.replace('?', ''));
history.push(
Url.format({
pathname: '/search',
query: {
...queries,
q: query,
scope,
},
})
);
};
};
public render() {
const emptyFunction = () => null;
return (
<EuiFlexGroup>
<EuiTab isSelected={this.props.scope !== SearchScope.repository} onClick={emptyFunction}>
<Link to={`/search?q=${this.props.query}&scope=${SearchScope.symbol}`}>Code</Link>
</EuiTab>
<EuiTab isSelected={this.props.scope === SearchScope.repository} onClick={emptyFunction}>
<Link to={`/search?q=${this.props.query}&scope=${SearchScope.repository}`}>
<EuiTabs>
<EuiTab
isSelected={this.props.scope !== SearchScope.repository}
onClick={this.onTabClicked(SearchScope.symbol)}
>
Code
</EuiTab>
<EuiTab
isSelected={this.props.scope === SearchScope.repository}
onClick={this.onTabClicked(SearchScope.repository)}
>
Repository
</Link>
</EuiTab>
</EuiTab>
</EuiTabs>
</EuiFlexGroup>
);
}

View file

@ -12,7 +12,6 @@ import styled from 'styled-components';
import Url from 'url';
import { DocumentSearchResult } from '../../../model';
import { documentSearch } from '../../actions';
import { SearchScope } from '../../common/types';
import { RootState } from '../../reducers';
import { history } from '../../utils/url';
@ -39,16 +38,18 @@ const CodeResultContainer = styled.div`
margin-top: 80px;
`;
const RepositoryResultContainer = CodeResultContainer;
interface Props {
query: string;
scope: SearchScope;
page?: number;
languages?: Set<string>;
repositories?: Set<string>;
isLoading: boolean;
error?: Error;
searchResult?: DocumentSearchResult;
documentSearch: (q: string, p: number) => void;
repositorySearch: any;
documentSearchResults?: DocumentSearchResult;
repositorySearchResults?: any;
}
interface State {
@ -119,12 +120,28 @@ class SearchPage extends React.PureComponent<Props, State> {
};
public render() {
const { query, searchResult, languages, repositories, repositorySearch } = this.props;
const {
query,
scope,
documentSearchResults,
languages,
repositories,
repositorySearchResults,
} = this.props;
if (repositorySearch.scope === SearchScope.repository) {
const { repositories: repos } = repositorySearch.repositories;
if (
scope === SearchScope.repository &&
repositorySearchResults &&
repositorySearchResults.total > 0
) {
const { repositories: repos } = repositorySearchResults;
const resultComps =
repos && repos.map((repo: any) => <RepoItem key={repo.uri} uri={repo.uri} />);
repos &&
repos.map((repo: any) => (
<EuiFlexItem key={repo.uri}>
<RepoItem uri={repo.uri} />
</EuiFlexItem>
));
const mainComp = (
<EuiFlexGroup style={{ padding: '0 1rem' }}>
<EuiFlexItem grow={2}>
@ -152,20 +169,25 @@ class SearchPage extends React.PureComponent<Props, State> {
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<MainContentContainer grow={8}>{resultComps}</MainContentContainer>
<MainContentContainer grow={8}>
<EuiFlexGroup>
<EuiFlexItem style={{ height: '32px' }}>
<ScopeTab query={query} scope={scope} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<RepositoryResultContainer>{resultComps}</RepositoryResultContainer>
</MainContentContainer>
</EuiFlexGroup>
);
return (
<SearchContainer>
<SearchBar query={query} />
<ScopeTab query={query} scope={repositorySearch.scope} />
{mainComp}
</SearchContainer>
);
}
if (searchResult) {
const { stats, results } = searchResult!;
} else if (scope === SearchScope.default && documentSearchResults) {
const { stats, results } = documentSearchResults!;
const { total, from, to, page, totalPage, repoStats, languageStats } = stats!;
const statsComp = (
@ -189,8 +211,8 @@ class SearchPage extends React.PureComponent<Props, State> {
<MainContentContainer grow={8}>
<EuiFlexGroup>
<EuiFlexItem>{statsComp}</EuiFlexItem>
<EuiFlexItem>
<ScopeTab query={query} scope={repositorySearch.scope} />
<EuiFlexItem style={{ height: '32px' }}>
<ScopeTab query={query} scope={scope} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
@ -220,16 +242,7 @@ class SearchPage extends React.PureComponent<Props, State> {
}
const mapStateToProps = (state: RootState) => ({
...state.documentSearch,
repositorySearch: state.repositorySearch,
...state.search,
});
const mapDispatchToProps = {
documentSearch,
};
export const Search = connect(
mapStateToProps,
mapDispatchToProps
// @ts-ignore
)(SearchPage);
export const Search = connect(mapStateToProps)(SearchPage);

View file

@ -8,21 +8,19 @@ import { combineReducers } from 'redux';
import { blame, BlameState } from './blame';
import { commit, CommitState } from './commit';
import { documentSearch, DocumentSearchState } from './document_search';
import { editor, EditorState } from './editor';
import { file, FileState } from './file';
import { languageServer, LanguageServerState } from './language_server';
import { recentProjects, RecentProjectsState } from './recent_projects';
import { repository, RepositoryState } from './repository';
import { repositorySearch, RepositorySearchState } from './repository_search';
import { route, RouteState } from './route';
import { search, SearchState } from './search';
import { status, StatusState } from './status';
import { symbol, SymbolState } from './symbol';
import { userConfig, UserConfigState } from './user';
export interface RootState {
repository: RepositoryState;
documentSearch: DocumentSearchState;
repositorySearch: RepositorySearchState;
search: SearchState;
file: FileState;
symbol: SymbolState;
editor: EditorState;
@ -40,8 +38,7 @@ const reducers = {
file,
symbol,
editor,
documentSearch,
repositorySearch,
search,
route,
status,
userConfig,

View file

@ -1,68 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import produce from 'immer';
import { Action, handleActions } from 'redux-actions';
import {
changeSearchScope,
repositorySearch as repositorySearchAction,
repositorySearchFailed,
RepositorySearchPayload,
repositorySearchSuccess,
} from '../actions/search';
export interface RepositorySearchState {
scope: string;
query: string;
isLoading: boolean;
repositories: any;
error?: Error;
}
const initialState: RepositorySearchState = {
scope: '',
query: '',
isLoading: false,
repositories: {},
};
export const repositorySearch = handleActions(
{
[String(repositorySearchAction)]: (
state: RepositorySearchState,
action: Action<RepositorySearchPayload>
) =>
produce<RepositorySearchState>(state, draft => {
if (action.payload) {
draft.query = action.payload.query;
draft.isLoading = true;
}
}),
[String(repositorySearchSuccess)]: (state: RepositorySearchState, action: Action<any>) =>
produce<RepositorySearchState>(state, draft => {
draft.repositories = action.payload;
draft.isLoading = false;
}),
[String(changeSearchScope)]: (state: RepositorySearchState, action: Action<any>) =>
produce<RepositorySearchState>(state, draft => {
draft.scope = action.payload;
draft.isLoading = false;
}),
[String(repositorySearchFailed)]: (state: RepositorySearchState, action: Action<any>) => {
if (action.payload) {
return produce<RepositorySearchState>(state, draft => {
draft.isLoading = false;
draft.error = action.payload.error;
});
} else {
return state;
}
},
},
initialState
);

View file

@ -10,34 +10,49 @@ import { Action, handleActions } from 'redux-actions';
import { DocumentSearchResult, RepositoryUri } from '../../model';
import {
changeSearchScope,
documentSearch as documentSearchQuery,
documentSearchFailed,
DocumentSearchPayload,
documentSearchSuccess,
repositorySearch as repositorySearchAction,
repositorySearchFailed,
RepositorySearchPayload,
repositorySearchSuccess,
} from '../actions';
import { SearchScope } from '../common/types';
export interface DocumentSearchState {
export interface SearchState {
scope: SearchScope;
query: string;
page?: number;
languages?: Set<string>;
repositories?: Set<RepositoryUri>;
isLoading: boolean;
error?: Error;
searchResult?: DocumentSearchResult;
documentSearchResults?: DocumentSearchResult;
repositorySearchResults?: any;
}
const initialState: DocumentSearchState = {
query: 'queryBuilder',
const initialState: SearchState = {
query: '',
isLoading: false,
scope: SearchScope.default,
};
export const documentSearch = handleActions<DocumentSearchState, any>(
export const search = handleActions<SearchState, any>(
{
[String(documentSearchQuery)]: (
state: DocumentSearchState,
action: Action<DocumentSearchPayload>
) =>
produce<DocumentSearchState>(state, draft => {
[String(changeSearchScope)]: (state: SearchState, action: Action<any>) =>
produce<SearchState>(state, draft => {
if (Object.values(SearchScope).includes(action.payload)) {
draft.scope = action.payload;
} else {
draft.scope = SearchScope.default;
}
draft.isLoading = false;
}),
[String(documentSearchQuery)]: (state: SearchState, action: Action<DocumentSearchPayload>) =>
produce<SearchState>(state, draft => {
if (action.payload) {
draft.query = action.payload.query;
draft.page = parseInt(action.payload.page as string, 10);
@ -57,11 +72,8 @@ export const documentSearch = handleActions<DocumentSearchState, any>(
draft.error = undefined;
}
}),
[String(documentSearchSuccess)]: (
state: DocumentSearchState,
action: Action<DocumentSearchResult>
) =>
produce<DocumentSearchState>(state, draft => {
[String(documentSearchSuccess)]: (state: SearchState, action: Action<DocumentSearchResult>) =>
produce<SearchState>(state, draft => {
const {
from,
page,
@ -88,8 +100,8 @@ export const documentSearch = handleActions<DocumentSearchState, any>(
};
});
draft.searchResult = {
...draft.searchResult,
draft.documentSearchResults = {
...draft.documentSearchResults,
query: state.query,
total,
took,
@ -105,9 +117,9 @@ export const documentSearch = handleActions<DocumentSearchState, any>(
results,
};
}),
[String(documentSearchFailed)]: (state: DocumentSearchState, action: Action<Error>) => {
[String(documentSearchFailed)]: (state: SearchState, action: Action<Error>) => {
if (action.payload) {
return produce<DocumentSearchState>(state, draft => {
return produce<SearchState>(state, draft => {
draft.isLoading = false;
draft.error = action.payload!;
});
@ -115,6 +127,31 @@ export const documentSearch = handleActions<DocumentSearchState, any>(
return state;
}
},
[String(repositorySearchAction)]: (
state: SearchState,
action: Action<RepositorySearchPayload>
) =>
produce<SearchState>(state, draft => {
if (action.payload) {
draft.query = action.payload.query;
draft.isLoading = true;
}
}),
[String(repositorySearchSuccess)]: (state: SearchState, action: Action<any>) =>
produce<SearchState>(state, draft => {
draft.repositorySearchResults = action.payload;
draft.isLoading = false;
}),
[String(repositorySearchFailed)]: (state: SearchState, action: Action<any>) => {
if (action.payload) {
return produce<SearchState>(state, draft => {
draft.isLoading = false;
draft.error = action.payload.error;
});
} else {
return state;
}
},
},
initialState
);

View file

@ -8,7 +8,6 @@ import { fork } from 'redux-saga/effects';
import { watchBlame, watchLoadBlame } from './blame';
import { watchLoadCommit } from './commit';
import { watchDocumentSearch, watchSearchRouteChange } from './document_search';
import {
watchCloseReference,
watchLoadRepo,
@ -27,7 +26,7 @@ import {
watchIndexRepo,
watchInitRepoCmd,
} from './repository';
import { watchRepositorySearch } from './repository_search';
import { watchDocumentSearch, watchRepositorySearch, watchSearchRouteChange } from './search';
import { watchRepoCloneSuccess } from './status';
import { watchLoadStructure } from './structure';
import { watchLoadUserConfig } from './user';

View file

@ -1,40 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { call, put, takeLatest } from 'redux-saga/effects';
import { kfetch } from 'ui/kfetch';
import { Action } from 'redux-actions';
import {
repositorySearch,
repositorySearchFailed,
RepositorySearchPayload,
repositorySearchQueryChanged,
repositorySearchSuccess,
} from '../actions/search';
function requestRepositorySearch(q: string) {
return kfetch({
pathname: `../api/code/search/repo`,
method: 'get',
query: { q },
});
}
function* handleRepositorySearch(action: Action<RepositorySearchPayload>) {
try {
const data = yield call(requestRepositorySearch, action.payload!.query);
yield put(repositorySearchSuccess(data));
} catch (err) {
yield put(repositorySearchFailed(err));
}
}
export function* watchRepositorySearch() {
yield takeLatest(
[String(repositorySearch), String(repositorySearchQueryChanged)],
handleRepositorySearch
);
}

View file

@ -16,6 +16,10 @@ import {
documentSearchSuccess,
Match,
repositorySearch,
repositorySearchFailed,
RepositorySearchPayload,
repositorySearchQueryChanged,
repositorySearchSuccess,
} from '../actions';
import { SearchScope } from '../common/types';
import { searchRoutePattern } from './patterns';
@ -62,26 +66,52 @@ function* handleDocumentSearch(action: Action<DocumentSearchPayload>) {
}
}
function requestRepositorySearch(q: string) {
return kfetch({
pathname: `../api/code/search/repo`,
method: 'get',
query: { q },
});
}
export function* watchDocumentSearch() {
yield takeLatest(String(documentSearch), handleDocumentSearch);
}
function* handleRepositorySearch(action: Action<RepositorySearchPayload>) {
try {
const data = yield call(requestRepositorySearch, action.payload!.query);
yield put(repositorySearchSuccess(data));
} catch (err) {
yield put(repositorySearchFailed(err));
}
}
export function* watchRepositorySearch() {
yield takeLatest(
[String(repositorySearch), String(repositorySearchQueryChanged)],
handleRepositorySearch
);
}
function* handleSearchRouteChange(action: Action<Match>) {
const { location } = action.payload!;
const queryParams = queryString.parse(location.search);
const rawSearchStr = location.search.length > 0 ? location.search.substring(1) : '';
const queryParams = queryString.parse(rawSearchStr);
const { q, p, langs, repos, scope } = queryParams;
yield put(changeSearchScope(scope as string));
if (scope === SearchScope.repository) {
yield put(repositorySearch({ query: q as string }));
} else {
yield put(
documentSearch({
query: q as string,
page: p as string,
languages: langs as string,
repositories: repos as string,
})
);
}
yield put(
documentSearch({
query: q as string,
page: p as string,
languages: langs as string,
repositories: repos as string,
})
);
}
export function* watchSearchRouteChange() {

View file

@ -27,6 +27,8 @@ export const refUrlSelector = (state: RootState) => {
export const fileSelector = (state: RootState) => state.file.file;
export const searchScopeSelector = (state: RootState) => state.search.scope;
export const repoUriSelector = (state: RootState) => {
const { resource, org, repo } = state.route.match.params;
return `${resource}/${org}/${repo}`;