[Code] Add search filter (#27831)

* [Code] add the generated styles.css file into repo

* [Code] Initial layout of the search filter

* feature(code/frontend): search options flyout

* update query bar test
This commit is contained in:
Mengwei Ding 2019-01-14 20:12:07 +08:00 committed by GitHub
parent deb8734f0f
commit a2d7cd1818
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 732 additions and 49 deletions

View file

@ -18,6 +18,10 @@ export interface RepositorySearchPayload {
query: string;
}
export interface SearchOptions {
repoScopes: string[];
}
// For document search page
export const documentSearch = createAction<DocumentSearchPayload>('DOCUMENT SEARCH');
export const documentSearchSuccess = createAction<DocumentSearchResult>('DOCUMENT SEARCH SUCCESS');
@ -36,3 +40,5 @@ export const repositorySearchQueryChanged = createAction<RepositorySearchPayload
);
export const repositoryTypeaheadSearchSuccess = createAction<string>('REPOSITORY SEARCH SUCCESS');
export const repositoryTypeaheadSearchFailed = createAction<string>('REPOSITORY SEARCH FAILED');
export const saveSearchOptions = createAction<SearchOptions>('SAVE SEARCH OPTIONS');

View file

@ -1,12 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render correctly with empty query string 1`] = `
<QueryBar
<CodeQueryBar
appName="mockapp"
disableAutoFocus={false}
onSelect={[Function]}
onSubmit={[Function]}
query=""
repoSearchResults={Array []}
repositorySearch={[Function]}
saveSearchOptions={[Function]}
searchLoading={false}
searchOptions={
Object {
"repoScopes": Array [],
}
}
suggestionProviders={Array []}
>
<EuiFlexGroup
@ -173,6 +182,47 @@ exports[`render correctly with empty query string 1`] = `
</div>
</EuiFormControlLayout>
</EuiFieldText>
<SearchOptions
repoSearchResults={Array []}
repositorySearch={[Function]}
saveSearchOptions={[Function]}
searchLoading={false}
searchOptions={
Object {
"repoScopes": Array [],
}
}
>
<div>
<div
className="kuiLocalSearchAssistedInput__assistance"
>
<EuiButtonEmpty
color="primary"
iconSide="left"
onClick={[Function]}
size="xs"
type="button"
>
<button
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
Options
</span>
</span>
</button>
</EuiButtonEmpty>
</div>
</div>
</SearchOptions>
</div>
</div>
</form>
@ -192,7 +242,7 @@ exports[`render correctly with empty query string 1`] = `
</EuiFlexItem>
</div>
</EuiFlexGroup>
</QueryBar>
</CodeQueryBar>
`;
exports[`render correctly with input query string changed 1`] = `
@ -238,12 +288,21 @@ exports[`render correctly with input query string changed 1`] = `
}
}
>
<QueryBar
<CodeQueryBar
appName="mockapp"
disableAutoFocus={false}
onSelect={[Function]}
onSubmit={[Function]}
query=""
repoSearchResults={Array []}
repositorySearch={[Function]}
saveSearchOptions={[Function]}
searchLoading={false}
searchOptions={
Object {
"repoScopes": Array [],
}
}
suggestionProviders={
Array [
Object {
@ -422,6 +481,47 @@ exports[`render correctly with input query string changed 1`] = `
</div>
</EuiFormControlLayout>
</EuiFieldText>
<SearchOptions
repoSearchResults={Array []}
repositorySearch={[Function]}
saveSearchOptions={[Function]}
searchLoading={false}
searchOptions={
Object {
"repoScopes": Array [],
}
}
>
<div>
<div
className="kuiLocalSearchAssistedInput__assistance"
>
<EuiButtonEmpty
color="primary"
iconSide="left"
onClick={[Function]}
size="xs"
type="button"
>
<button
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
Options
</span>
</span>
</button>
</EuiButtonEmpty>
</div>
</div>
</SearchOptions>
</div>
</div>
</form>
@ -441,7 +541,7 @@ exports[`render correctly with input query string changed 1`] = `
</EuiFlexItem>
</div>
</EuiFlexGroup>
</QueryBar>
</CodeQueryBar>
</Router>
</MemoryRouter>
`;

View file

@ -0,0 +1,198 @@
/*
* 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 {
EuiButton,
EuiButtonEmpty,
// @ts-ignore
EuiButtonGroup,
EuiComboBox,
EuiFlexGroup,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { EuiIcon } from '@elastic/eui';
import { unique } from 'lodash';
import React, { Component } from 'react';
import styled from 'styled-components';
import { SearchOptions as ISearchOptions } from '../../../actions';
const SelectedRepo = styled.div`
max-width: 60%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
const Icon = styled(EuiIcon)`
cursor: pointer;
`;
enum Scope {
Default = 'Default',
Symbols = 'Symbols',
Repos = 'Repos',
}
interface State {
isFlyoutOpen: boolean;
repoScopes: any[];
scope: Scope;
}
interface Props {
repositorySearch: (p: { query: string }) => void;
saveSearchOptions: (searchOptions: ISearchOptions) => void;
repoSearchResults: any[];
searchLoading: boolean;
searchOptions: ISearchOptions;
}
export class SearchOptions extends Component<Props, State> {
public state: State = {
isFlyoutOpen: false,
scope: Scope.Default,
repoScopes: this.props.searchOptions.repoScopes,
};
public buttonGroupOptions = [
{
id: Scope.Default,
label: Scope.Default,
},
{
id: Scope.Symbols,
label: Scope.Symbols,
},
{
id: Scope.Repos,
label: Scope.Repos,
},
];
public applyAndClose = () => {
this.props.saveSearchOptions({ repoScopes: this.state.repoScopes });
this.setState({ isFlyoutOpen: false });
};
public removeRepoScope = (r: string) => () => {
this.setState(prevState => ({
repoScopes: prevState.repoScopes.filter(rs => rs !== r),
}));
};
public render() {
let optionsFlyout;
if (this.state.isFlyoutOpen) {
const toggleIdToSelectedMap = {
[this.state.scope]: true,
};
const selectedRepos = this.state.repoScopes.map((r: string) => {
return (
<div key={r}>
<EuiPanel paddingSize="s">
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<SelectedRepo>{r}</SelectedRepo>
<Icon type="cross" onClick={this.removeRepoScope(r)} />
</EuiFlexGroup>
</EuiPanel>
<EuiSpacer size="s" />
</div>
);
});
optionsFlyout = (
<EuiFlyout onClose={this.closeOptionsFlyout} size="s" aria-labelledby="flyoutSmallTitle">
<EuiFlyoutHeader>
<EuiTitle size="s">
<h2 id="flyoutSmallTitle"> Search Settings </h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiButtonGroup
name="Primary"
options={this.buttonGroupOptions}
idToSelectedMap={toggleIdToSelectedMap}
onChange={this.onScopeChanged}
color="primary"
/>
<EuiSpacer size="m" />
<EuiTitle size="xs">
<h3>Repo Scope</h3>
</EuiTitle>
<EuiText size="xs">Add indexed repos to your search scope</EuiText>
<EuiSpacer size="m" />
<EuiComboBox
placeholder="Search to add repos"
async={true}
options={this.props.repoSearchResults.map(repo => ({
id: repo.name,
label: repo.name,
uri: repo.uri,
}))}
selectedOptions={[]}
isLoading={this.props.searchLoading}
onChange={this.onRepoChange}
onSearchChange={this.onRepoSearchChange}
isClearable={true}
/>
<EuiSpacer size="m" />
{selectedRepos}
<EuiSpacer size="s" />
<EuiFlexGroup justifyContent="flexEnd" gutterSize="none">
<EuiButton onClick={this.applyAndClose} fill={true} iconSide="right">
Apply and Close
</EuiButton>
</EuiFlexGroup>
</EuiFlyoutBody>
</EuiFlyout>
);
}
return (
<div>
<div className="kuiLocalSearchAssistedInput__assistance">
<EuiButtonEmpty size="xs" onClick={this.toggleOptionsFlyout}>
Options
</EuiButtonEmpty>
</div>
{optionsFlyout}
</div>
);
}
private onScopeChanged = (scopeId: string) => {
this.setState({ scope: scopeId as Scope });
};
private onRepoSearchChange = (searchValue: string) => {
this.props.repositorySearch({ query: searchValue });
};
private onRepoChange = (repos: any) => {
this.setState({
repoScopes: unique(repos.map((r: any) => r.uri)),
});
};
private toggleOptionsFlyout = () => {
this.setState({
isFlyoutOpen: !this.state.isFlyoutOpen,
});
};
private closeOptionsFlyout = () => {
this.setState({
isFlyoutOpen: false,
});
};
}

View file

@ -10,16 +10,21 @@ import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import sinon from 'sinon';
import { QueryBar } from '..';
import { AutocompleteSuggestionType } from '../suggestions';
import props from './props.json';
import { CodeQueryBar } from './query_bar';
test('render correctly with empty query string', () => {
const emptyFn = () => {
return;
};
const queryBarComp = mount(
<QueryBar
<CodeQueryBar
repositorySearch={emptyFn}
saveSearchOptions={emptyFn}
repoSearchResults={[]}
searchLoading={false}
searchOptions={{ repoScopes: [] }}
query=""
disableAutoFocus={false}
appName="mockapp"
@ -67,7 +72,12 @@ test('render correctly with input query string changed', done => {
const queryBarComp = mount(
<MemoryRouter initialEntries={[{ pathname: '/', key: 'testKey' }]}>
<QueryBar
<CodeQueryBar
repositorySearch={emptyFn}
saveSearchOptions={emptyFn}
repoSearchResults={[]}
searchLoading={false}
searchOptions={{ repoScopes: [] }}
query=""
disableAutoFocus={false}
appName="mockapp"

View file

@ -7,15 +7,20 @@
import { debounce, isEqual } from 'lodash';
import React, { Component } from 'react';
import { SearchOptions as ISearchOptions } from '../../../actions';
import { matchPairs } from '../lib/match_pairs';
import { SuggestionsComponent } from './typeahead/suggestions_component';
import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiOutsideClickDetector } from '@elastic/eui';
import { connect } from 'react-redux';
import { repositorySearch, saveSearchOptions } from '../../../actions';
import { RootState } from '../../../reducers';
import {
AutocompleteSuggestion,
AutocompleteSuggestionGroup,
SuggestionsProvider,
} from '../suggestions';
import { SearchOptions } from './options';
const KEY_CODES = {
LEFT: 37,
@ -36,6 +41,11 @@ interface Props {
disableAutoFocus?: boolean;
appName: string;
suggestionProviders: SuggestionsProvider[];
repositorySearch: (p: { query: string }) => void;
saveSearchOptions: (searchOptions: ISearchOptions) => void;
repoSearchResults: any[];
searchLoading: boolean;
searchOptions: ISearchOptions;
}
interface State {
@ -48,7 +58,7 @@ interface State {
currentProps?: Props;
}
export class QueryBar extends Component<Props, State> {
export class CodeQueryBar extends Component<Props, State> {
public static getDerivedStateFromProps(nextProps: Props, prevState: State) {
if (isEqual(prevState.currentProps, nextProps)) {
return null;
@ -82,6 +92,7 @@ export class QueryBar extends Component<Props, State> {
groupIndex: null,
itemIndex: null,
suggestionGroups: [],
showOptions: false,
};
public updateSuggestions = debounce(async () => {
@ -405,6 +416,13 @@ export class QueryBar extends Component<Props, State> {
aria-activedescendant={activeDescendant}
role="textbox"
/>
<SearchOptions
repositorySearch={this.props.repositorySearch}
saveSearchOptions={this.props.saveSearchOptions}
repoSearchResults={this.props.repoSearchResults}
searchLoading={this.props.searchLoading}
searchOptions={this.props.searchOptions}
/>
</div>
</div>
</form>
@ -426,3 +444,21 @@ export class QueryBar extends Component<Props, State> {
);
}
}
const mapStateToProps = (state: RootState) => ({
repoSearchResults: state.search.repositorySearchResults
? state.search.repositorySearchResults.repositories
: [],
searchLoading: state.search.isLoading,
searchOptions: state.search.searchOptions || { repoScopes: [] },
});
const mapDispatchToProps = {
repositorySearch,
saveSearchOptions,
};
export const QueryBar = connect(
mapStateToProps,
mapDispatchToProps
)(CodeQueryBar);

View file

@ -140,11 +140,11 @@ exports[`render full suggestions component 1`] = `
>
<styled.div>
<div
className="sc-bZQynM gNhebF"
className="sc-htoDjs chDvrV"
>
<styled.div>
<div
className="sc-gzVnrw tDqEr"
className="sc-dnqmqq iFsitv"
>
<EuiToken
displayOptions={Object {}}
@ -186,7 +186,7 @@ exports[`render full suggestions component 1`] = `
</EuiToken>
<styled.span>
<span
className="sc-htoDjs kTogwW"
className="sc-iwsKbI eRUloI"
>
Symbols
</span>
@ -195,7 +195,7 @@ exports[`render full suggestions component 1`] = `
</styled.div>
<styled.div>
<div
className="sc-dnqmqq cXfCkY"
className="sc-gZMcBi doiTLD"
>
1
Result
@ -232,7 +232,7 @@ exports[`render full suggestions component 1`] = `
>
<div
aria-selected={true}
className="sc-bdVaJa jHURZo"
className="sc-htpNat bUGPvU"
id="suggestion-0-0"
onClick={[Function]}
onMouseEnter={[Function]}
@ -240,14 +240,14 @@ exports[`render full suggestions component 1`] = `
>
<styled.div>
<div
className="sc-bwzfXH dIdIHh"
className="sc-bxivhb jzTuee"
>
<Styled(styled.div)>
<styled.div
className="sc-bxivhb goHlpn"
className="sc-EHOje iMawIB"
>
<div
className="sc-bxivhb goHlpn sc-htpNat bUoeMH"
className="sc-EHOje iMawIB sc-ifAKCX bMxzXt"
>
<EuiToken
displayOptions={Object {}}
@ -294,10 +294,10 @@ exports[`render full suggestions component 1`] = `
<div>
<Styled(styled.div)>
<styled.div
className="sc-ifAKCX gWOzPQ"
className="sc-bZQynM fCofhq"
>
<div
className="sc-ifAKCX gWOzPQ sc-htpNat bUoeMH"
className="sc-bZQynM fCofhq sc-ifAKCX bMxzXt"
>
<span>
java.lang.
@ -310,10 +310,10 @@ exports[`render full suggestions component 1`] = `
</Styled(styled.div)>
<Styled(styled.div)>
<styled.div
className="sc-EHOje iasAWb"
className="sc-gzVnrw kzVDXi"
>
<div
className="sc-EHOje iasAWb sc-htpNat bUoeMH"
className="sc-gzVnrw kzVDXi sc-ifAKCX bMxzXt"
>
elastic/elasticsearch &gt; src/foo/bar.java
</div>
@ -335,11 +335,11 @@ exports[`render full suggestions component 1`] = `
>
<styled.div>
<div
className="sc-bZQynM gNhebF"
className="sc-htoDjs chDvrV"
>
<styled.div>
<div
className="sc-gzVnrw tDqEr"
className="sc-dnqmqq iFsitv"
>
<EuiToken
displayOptions={Object {}}
@ -381,7 +381,7 @@ exports[`render full suggestions component 1`] = `
</EuiToken>
<styled.span>
<span
className="sc-htoDjs kTogwW"
className="sc-iwsKbI eRUloI"
>
Files
</span>
@ -390,7 +390,7 @@ exports[`render full suggestions component 1`] = `
</styled.div>
<styled.div>
<div
className="sc-dnqmqq cXfCkY"
className="sc-gZMcBi doiTLD"
>
1
Result
@ -427,7 +427,7 @@ exports[`render full suggestions component 1`] = `
>
<div
aria-selected={false}
className="sc-bdVaJa kFePkC"
className="sc-htpNat eUnATS"
id="suggestion-1-0"
onClick={[Function]}
onMouseEnter={[Function]}
@ -435,15 +435,15 @@ exports[`render full suggestions component 1`] = `
>
<styled.div>
<div
className="sc-bwzfXH dIdIHh"
className="sc-bxivhb jzTuee"
>
<div>
<Styled(styled.div)>
<styled.div
className="sc-ifAKCX gWOzPQ"
className="sc-bZQynM fCofhq"
>
<div
className="sc-ifAKCX gWOzPQ sc-htpNat bUoeMH"
className="sc-bZQynM fCofhq sc-ifAKCX bMxzXt"
>
src/foo/bar.java
</div>
@ -451,10 +451,10 @@ exports[`render full suggestions component 1`] = `
</Styled(styled.div)>
<Styled(styled.div)>
<styled.div
className="sc-EHOje iasAWb"
className="sc-gzVnrw kzVDXi"
>
<div
className="sc-EHOje iasAWb sc-htpNat bUoeMH"
className="sc-gzVnrw kzVDXi sc-ifAKCX bMxzXt"
>
This is a file
</div>
@ -476,11 +476,11 @@ exports[`render full suggestions component 1`] = `
>
<styled.div>
<div
className="sc-bZQynM gNhebF"
className="sc-htoDjs chDvrV"
>
<styled.div>
<div
className="sc-gzVnrw tDqEr"
className="sc-dnqmqq iFsitv"
>
<EuiToken
displayOptions={Object {}}
@ -523,7 +523,7 @@ exports[`render full suggestions component 1`] = `
</EuiToken>
<styled.span>
<span
className="sc-htoDjs kTogwW"
className="sc-iwsKbI eRUloI"
>
Repos
</span>
@ -532,7 +532,7 @@ exports[`render full suggestions component 1`] = `
</styled.div>
<styled.div>
<div
className="sc-dnqmqq cXfCkY"
className="sc-gZMcBi doiTLD"
>
2
Result
@ -570,7 +570,7 @@ exports[`render full suggestions component 1`] = `
>
<div
aria-selected={false}
className="sc-bdVaJa kFePkC"
className="sc-htpNat eUnATS"
id="suggestion-2-0"
onClick={[Function]}
onMouseEnter={[Function]}
@ -578,15 +578,15 @@ exports[`render full suggestions component 1`] = `
>
<styled.div>
<div
className="sc-bwzfXH dIdIHh"
className="sc-bxivhb jzTuee"
>
<div>
<Styled(styled.div)>
<styled.div
className="sc-ifAKCX gWOzPQ"
className="sc-bZQynM fCofhq"
>
<div
className="sc-ifAKCX gWOzPQ sc-htpNat bUoeMH"
className="sc-bZQynM fCofhq sc-ifAKCX bMxzXt"
>
elastic/kibana
</div>
@ -594,10 +594,10 @@ exports[`render full suggestions component 1`] = `
</Styled(styled.div)>
<Styled(styled.div)>
<styled.div
className="sc-EHOje iasAWb"
className="sc-gzVnrw kzVDXi"
>
<div
className="sc-EHOje iasAWb sc-htpNat bUoeMH"
className="sc-gzVnrw kzVDXi sc-ifAKCX bMxzXt"
/>
</styled.div>
</Styled(styled.div)>
@ -636,7 +636,7 @@ exports[`render full suggestions component 1`] = `
>
<div
aria-selected={false}
className="sc-bdVaJa kFePkC"
className="sc-htpNat eUnATS"
id="suggestion-2-1"
onClick={[Function]}
onMouseEnter={[Function]}
@ -644,15 +644,15 @@ exports[`render full suggestions component 1`] = `
>
<styled.div>
<div
className="sc-bwzfXH dIdIHh"
className="sc-bxivhb jzTuee"
>
<div>
<Styled(styled.div)>
<styled.div
className="sc-ifAKCX gWOzPQ"
className="sc-bZQynM fCofhq"
>
<div
className="sc-ifAKCX gWOzPQ sc-htpNat bUoeMH"
className="sc-bZQynM fCofhq sc-ifAKCX bMxzXt"
>
elastic/elasticsearch
</div>
@ -660,10 +660,10 @@ exports[`render full suggestions component 1`] = `
</Styled(styled.div)>
<Styled(styled.div)>
<styled.div
className="sc-EHOje iasAWb"
className="sc-gzVnrw kzVDXi"
>
<div
className="sc-EHOje iasAWb sc-htpNat bUoeMH"
className="sc-gzVnrw kzVDXi sc-ifAKCX bMxzXt"
/>
</styled.div>
</Styled(styled.div)>
@ -675,7 +675,7 @@ exports[`render full suggestions component 1`] = `
</Component>
<styled.div>
<div
className="sc-iwsKbI dFhxhD"
className="sc-gqjmRU bqMCCt"
>
<Link
replace={false}
@ -693,11 +693,11 @@ exports[`render full suggestions component 1`] = `
</div>
<styled.div>
<div
className="sc-gZMcBi hJzXXZ"
className="sc-VigVT hKgARx"
>
<styled.div>
<div
className="sc-gqjmRU gFkTN"
className="sc-jTzLTM eaoLEj"
>
Press ⮐ Return for Full Text Search
</div>

View file

@ -19,6 +19,8 @@ import {
repositorySearchFailed,
RepositorySearchPayload,
repositorySearchSuccess,
saveSearchOptions,
SearchOptions,
} from '../actions';
import { SearchScope } from '../common/types';
@ -32,6 +34,7 @@ export interface SearchState {
error?: Error;
documentSearchResults?: DocumentSearchResult;
repositorySearchResults?: any;
searchOptions?: SearchOptions;
}
const initialState: SearchState = {
@ -152,6 +155,10 @@ export const search = handleActions<SearchState, any>(
return state;
}
},
[String(saveSearchOptions)]: (state: SearchState, action: Action<any>) =>
produce<SearchState>(state, draft => {
draft.searchOptions = action.payload;
}),
},
initialState
);

File diff suppressed because one or more lines are too long