[App Search] Added the Documents View (#83947) (#85180)

This commit is contained in:
Jason Stoltzfus 2020-12-08 13:00:59 -05:00 committed by GitHub
parent d335e42701
commit 3d23351f74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1464 additions and 19 deletions

View file

@ -109,8 +109,10 @@
"@elastic/good": "^9.0.1-kibana3",
"@elastic/node-crypto": "1.2.1",
"@elastic/numeral": "^2.5.0",
"@elastic/react-search-ui": "^1.5.0",
"@elastic/request-crypto": "1.1.4",
"@elastic/safer-lodash-set": "link:packages/elastic-safer-lodash-set",
"@elastic/search-ui-app-search-connector": "^1.5.0",
"@hapi/boom": "^7.4.11",
"@hapi/cookie": "^10.1.2",
"@hapi/good-squeeze": "5.2.1",

View file

@ -0,0 +1,22 @@
/*
* 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 React from 'react';
import { shallow } from 'enzyme';
import { EuiButton } from '@elastic/eui';
import { DocumentCreationButton } from './document_creation_button';
describe('DocumentCreationButton', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render', () => {
const wrapper = shallow(<DocumentCreationButton />);
expect(wrapper.find(EuiButton).length).toEqual(1);
});
});

View file

@ -0,0 +1,20 @@
/*
* 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 React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton } from '@elastic/eui';
export const DocumentCreationButton: React.FC = () => {
return (
<EuiButton fill={true} color="primary" data-test-subj="IndexDocumentsButton">
{i18n.translate('xpack.enterpriseSearch.appSearch.documents.indexDocuments', {
defaultMessage: 'Index documents',
})}
</EuiButton>
);
};

View file

@ -0,0 +1,73 @@
/*
* 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 { setMockValues } from '../../../__mocks__/kea.mock';
import React from 'react';
import { shallow } from 'enzyme';
import { DocumentCreationButton } from './document_creation_button';
import { SearchExperience } from './search_experience';
import { Documents } from '.';
describe('Documents', () => {
const values = {
isMetaEngine: false,
myRole: { canManageEngineDocuments: true },
};
beforeEach(() => {
jest.clearAllMocks();
setMockValues(values);
});
it('renders', () => {
const wrapper = shallow(<Documents engineBreadcrumb={['test']} />);
expect(wrapper.find(SearchExperience).exists()).toBe(true);
});
it('renders a DocumentCreationButton if the user can manage engine documents', () => {
setMockValues({
...values,
myRole: { canManageEngineDocuments: true },
});
const wrapper = shallow(<Documents engineBreadcrumb={['test']} />);
expect(wrapper.find(DocumentCreationButton).exists()).toBe(true);
});
describe('Meta Engines', () => {
it('renders a Meta Engines message if this is a meta engine', () => {
setMockValues({
...values,
isMetaEngine: true,
});
const wrapper = shallow(<Documents engineBreadcrumb={['test']} />);
expect(wrapper.find('[data-test-subj="MetaEnginesCallout"]').exists()).toBe(true);
});
it('does not render a Meta Engines message if this is not a meta engine', () => {
setMockValues({
...values,
isMetaEngine: false,
});
const wrapper = shallow(<Documents engineBreadcrumb={['test']} />);
expect(wrapper.find('[data-test-subj="MetaEnginesCallout"]').exists()).toBe(false);
});
it('does not render a DocumentCreationButton even if the user can manage engine documents', () => {
setMockValues({
...values,
myRole: { canManageEngineDocuments: true },
isMetaEngine: true,
});
const wrapper = shallow(<Documents engineBreadcrumb={['test']} />);
expect(wrapper.find(DocumentCreationButton).exists()).toBe(false);
});
});
});

View file

@ -6,23 +6,26 @@
import React from 'react';
import {
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
EuiPageContent,
EuiPageContentBody,
} from '@elastic/eui';
import { EuiPageHeader, EuiPageHeaderSection, EuiTitle, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { useValues } from 'kea';
import { i18n } from '@kbn/i18n';
import { DocumentCreationButton } from './document_creation_button';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { FlashMessages } from '../../../shared/flash_messages';
import { DOCUMENTS_TITLE } from './constants';
import { EngineLogic } from '../engine';
import { AppLogic } from '../../app_logic';
import { SearchExperience } from './search_experience';
interface Props {
engineBreadcrumb: string[];
}
export const Documents: React.FC<Props> = ({ engineBreadcrumb }) => {
const { isMetaEngine } = useValues(EngineLogic);
const { myRole } = useValues(AppLogic);
return (
<>
<SetPageChrome trail={[...engineBreadcrumb, DOCUMENTS_TITLE]} />
@ -32,12 +35,36 @@ export const Documents: React.FC<Props> = ({ engineBreadcrumb }) => {
<h1>{DOCUMENTS_TITLE}</h1>
</EuiTitle>
</EuiPageHeaderSection>
{myRole.canManageEngineDocuments && !isMetaEngine && (
<EuiPageHeaderSection>
<DocumentCreationButton />
</EuiPageHeaderSection>
)}
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>
<FlashMessages />
</EuiPageContentBody>
</EuiPageContent>
<FlashMessages />
{isMetaEngine && (
<>
<EuiCallOut
data-test-subj="MetaEnginesCallout"
iconType="iInCircle"
title={i18n.translate(
'xpack.enterpriseSearch.appSearch.documents.metaEngineCallout.title',
{
defaultMessage: 'You are within a Meta Engine.',
}
)}
>
<p>
{i18n.translate('xpack.enterpriseSearch.appSearch.documents.metaEngineCallout', {
defaultMessage:
'Meta Engines have many Source Engines. Visit your Source Engines to alter their documents.',
})}
</p>
</EuiCallOut>
<EuiSpacer />
</>
)}
<SearchExperience />
</>
);
};

View file

@ -0,0 +1,19 @@
/*
* 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.
*/
jest.mock('../hooks', () => ({
useSearchContextActions: jest.fn(() => ({})),
useSearchContextState: jest.fn(() => ({})),
}));
import { useSearchContextState, useSearchContextActions } from '../hooks';
export const setMockSearchContextState = (values: object) => {
(useSearchContextState as jest.Mock).mockImplementation(() => values);
};
export const setMockSearchContextActions = (actions: object) => {
(useSearchContextActions as jest.Mock).mockImplementation(() => actions);
};

View file

@ -0,0 +1,75 @@
/*
* 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.
*/
const mockAction = jest.fn();
let mockSubcription: (state: object) => void;
const mockDriver = {
state: { foo: 'foo' },
actions: { bar: mockAction },
subscribeToStateChanges: jest.fn().mockImplementation((fn) => {
mockSubcription = fn;
}),
unsubscribeToStateChanges: jest.fn(),
};
jest.mock('react', () => ({
...(jest.requireActual('react') as object),
useContext: jest.fn(() => ({
driver: mockDriver,
})),
}));
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount, ReactWrapper } from 'enzyme';
import { useSearchContextState, useSearchContextActions } from './hooks';
describe('hooks', () => {
describe('useSearchContextState', () => {
const TestComponent = () => {
const { foo } = useSearchContextState();
return <div>{foo}</div>;
};
let wrapper: ReactWrapper;
beforeAll(() => {
wrapper = mount(<TestComponent />);
});
it('exposes search state', () => {
expect(wrapper.text()).toEqual('foo');
});
it('subscribes to state changes', () => {
act(() => {
mockSubcription({ foo: 'bar' });
});
expect(wrapper.text()).toEqual('bar');
});
it('unsubscribes to state changes when unmounted', () => {
wrapper.unmount();
expect(mockDriver.unsubscribeToStateChanges).toHaveBeenCalled();
});
});
describe('useSearchContextActions', () => {
it('exposes actions', () => {
const TestComponent = () => {
const { bar } = useSearchContextActions();
bar();
return null;
};
mount(<TestComponent />);
expect(mockAction).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,31 @@
/*
* 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 { useContext, useEffect, useState } from 'react';
// @ts-expect-error types are not available for this package yet
import { SearchContext } from '@elastic/react-search-ui';
export const useSearchContextState = () => {
const { driver } = useContext(SearchContext);
const [state, setState] = useState(driver.state);
useEffect(() => {
driver.subscribeToStateChanges((newState: object) => {
setState(newState);
});
return () => {
driver.unsubscribeToStateChanges();
};
}, [state]);
return state;
};
export const useSearchContextActions = () => {
const { driver } = useContext(SearchContext);
return driver.actions;
};

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export { SearchExperience } from './search_experience';

View file

@ -0,0 +1,26 @@
/*
* 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 React from 'react';
import { shallow } from 'enzyme';
// @ts-expect-error types are not available for this package yet
import { Paging, ResultsPerPage } from '@elastic/react-search-ui';
import { Pagination } from './pagination';
describe('Pagination', () => {
it('renders', () => {
const wrapper = shallow(<Pagination aria-label="foo" />);
expect(wrapper.find(Paging).exists()).toBe(true);
expect(wrapper.find(ResultsPerPage).exists()).toBe(true);
});
it('passes aria-label through to Paging', () => {
const wrapper = shallow(<Pagination aria-label="foo" />);
expect(wrapper.find(Paging).prop('aria-label')).toEqual('foo');
});
});

View file

@ -0,0 +1,22 @@
/*
* 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 React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
// @ts-expect-error types are not available for this package yet
import { Paging, ResultsPerPage } from '@elastic/react-search-ui';
import { PagingView, ResultsPerPageView } from './views';
export const Pagination: React.FC<{ 'aria-label': string }> = ({ 'aria-label': ariaLabel }) => (
<EuiFlexGroup alignItems="center" className="documentsSearchExperience__pagingInfo">
<EuiFlexItem>
<Paging view={PagingView} aria-label={ariaLabel} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ResultsPerPage view={ResultsPerPageView} />
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -0,0 +1,19 @@
.documentsSearchExperience {
.sui-results-container {
flex-grow: 1;
padding: 0;
}
.documentsSearchExperience__sidebar {
flex-grow: 1;
min-width: $euiSize * 19;
}
.documentsSearchExperience__content {
flex-grow: 4;
}
.documentsSearchExperience__pagingInfo {
flex-grow: 0;
}
}

View file

@ -0,0 +1,34 @@
/*
* 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 '../../../../__mocks__/kea.mock';
import { setMockValues } from '../../../../__mocks__';
import '../../../../__mocks__/enterprise_search_url.mock';
import React from 'react';
// @ts-expect-error types are not available for this package yet
import { SearchProvider } from '@elastic/react-search-ui';
import { shallow } from 'enzyme';
import { SearchExperience } from './search_experience';
describe('SearchExperience', () => {
const values = {
engine: {
name: 'some-engine',
apiKey: '1234',
},
};
beforeEach(() => {
jest.clearAllMocks();
setMockValues(values);
});
it('renders', () => {
const wrapper = shallow(<SearchExperience />);
expect(wrapper.find(SearchProvider).length).toBe(1);
});
});

View file

@ -0,0 +1,103 @@
/*
* 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 React from 'react';
import { i18n } from '@kbn/i18n';
import { useValues } from 'kea';
import { EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
// @ts-expect-error types are not available for this package yet;
import { SearchProvider, SearchBox, Sorting } from '@elastic/react-search-ui';
// @ts-expect-error types are not available for this package yet
import AppSearchAPIConnector from '@elastic/search-ui-app-search-connector';
import './search_experience.scss';
import { EngineLogic } from '../../engine';
import { externalUrl } from '../../../../shared/enterprise_search_url';
import { SearchBoxView, SortingView } from './views';
import { SearchExperienceContent } from './search_experience_content';
const DEFAULT_SORT_OPTIONS = [
{
name: i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedDesc', {
defaultMessage: 'Recently Uploaded (desc)',
}),
value: 'id',
direction: 'desc',
},
{
name: i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.recentlyUploadedAsc', {
defaultMessage: 'Recently Uploaded (asc)',
}),
value: 'id',
direction: 'asc',
},
];
export const SearchExperience: React.FC = () => {
const { engine } = useValues(EngineLogic);
const endpointBase = externalUrl.enterpriseSearchUrl;
// TODO const sortFieldsOptions = _flatten(fields.sortFields.map(fieldNameToSortOptions)) // we need to flatten this array since fieldNameToSortOptions returns an array of two sorting options
const sortingOptions = [...DEFAULT_SORT_OPTIONS /* TODO ...sortFieldsOptions*/];
const connector = new AppSearchAPIConnector({
cacheResponses: false,
endpointBase,
engineName: engine.name,
searchKey: engine.apiKey,
});
const searchProviderConfig = {
alwaysSearchOnInitialLoad: true,
apiConnector: connector,
trackUrlState: false,
initialState: {
sortDirection: 'desc',
sortField: 'id',
},
};
return (
<div className="documentsSearchExperience">
<SearchProvider config={searchProviderConfig}>
<SearchBox
searchAsYouType={true}
inputProps={{
placeholder: i18n.translate(
'xpack.enterpriseSearch.appSearch.documents.search.placeholder',
{
defaultMessage: 'Filter documents...',
}
),
'aria-label': i18n.translate(
'xpack.enterpriseSearch.appSearch.documents.search.ariaLabel',
{
defaultMessage: 'Filter documents',
}
),
'data-test-subj': 'DocumentsFilterInput',
}}
view={SearchBoxView}
/>
<EuiSpacer size="xl" />
<EuiFlexGroup direction="row">
<EuiFlexItem className="documentsSearchExperience__sidebar">
<Sorting
className="documentsSearchExperience__sorting"
sortOptions={sortingOptions}
view={SortingView}
/>
</EuiFlexItem>
<EuiFlexItem className="documentsSearchExperience__content">
<SearchExperienceContent />
</EuiFlexItem>
</EuiFlexGroup>
</SearchProvider>
</div>
);
};

View file

@ -0,0 +1,170 @@
/*
* 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 { setMockValues } from '../../../../__mocks__/kea.mock';
import { setMockSearchContextState } from './__mocks__/hooks.mock';
import React from 'react';
import { shallow, mount } from 'enzyme';
// @ts-expect-error types are not available for this package yet
import { Results } from '@elastic/react-search-ui';
import { ResultView } from './views';
import { Pagination } from './pagination';
import { SearchExperienceContent } from './search_experience_content';
describe('SearchExperienceContent', () => {
const searchState = {
resultSearchTerm: 'searchTerm',
totalResults: 100,
wasSearched: true,
};
const values = {
engineName: 'engine1',
isMetaEngine: false,
myRole: { canManageEngineDocuments: true },
};
beforeEach(() => {
jest.clearAllMocks();
setMockValues(values);
setMockSearchContextState(searchState);
});
it('renders', () => {
const wrapper = shallow(<SearchExperienceContent />);
expect(wrapper.isEmptyRender()).toBe(false);
});
it('passes engineName to the result view', () => {
const props = {
result: {
foo: {
raw: 'bar',
},
},
};
const wrapper = shallow(<SearchExperienceContent />);
const resultView: any = wrapper.find(Results).prop('resultView');
expect(resultView(props)).toEqual(<ResultView engineName="engine1" {...props} />);
});
it('renders pagination', () => {
const wrapper = shallow(<SearchExperienceContent />);
expect(wrapper.find(Pagination).exists()).toBe(true);
});
it('renders empty if a search was not performed yet', () => {
setMockSearchContextState({
...searchState,
wasSearched: false,
});
const wrapper = shallow(<SearchExperienceContent />);
expect(wrapper.isEmptyRender()).toBe(true);
});
it('renders results if a search was performed and there are more than 0 totalResults', () => {
setMockSearchContextState({
...searchState,
wasSearched: true,
totalResults: 10,
});
const wrapper = shallow(<SearchExperienceContent />);
expect(wrapper.find('[data-test-subj="documentsSearchResults"]').length).toBe(1);
});
it('renders a no results message if a non-empty search was performed and there are no results', () => {
setMockSearchContextState({
...searchState,
resultSearchTerm: 'searchTerm',
wasSearched: true,
totalResults: 0,
});
const wrapper = shallow(<SearchExperienceContent />);
expect(wrapper.find('[data-test-subj="documentsSearchResults"]').length).toBe(0);
expect(wrapper.find('[data-test-subj="documentsSearchNoResults"]').length).toBe(1);
});
describe('when an empty search was performed and there are no results, meaning there are no documents indexed', () => {
beforeEach(() => {
setMockSearchContextState({
...searchState,
resultSearchTerm: '',
wasSearched: true,
totalResults: 0,
});
});
it('renders a no documents message', () => {
const wrapper = shallow(<SearchExperienceContent />);
expect(wrapper.find('[data-test-subj="documentsSearchResults"]').length).toBe(0);
expect(wrapper.find('[data-test-subj="documentsSearchNoDocuments"]').length).toBe(1);
});
it('will include a button to index new documents', () => {
const wrapper = mount(<SearchExperienceContent />);
expect(
wrapper
.find(
'[data-test-subj="documentsSearchNoDocuments"] [data-test-subj="IndexDocumentsButton"]'
)
.exists()
).toBe(true);
});
it('will include a button to documentation if this is a meta engine', () => {
setMockValues({
...values,
isMetaEngine: true,
});
const wrapper = mount(<SearchExperienceContent />);
expect(
wrapper
.find(
'[data-test-subj="documentsSearchNoDocuments"] [data-test-subj="IndexDocumentsButton"]'
)
.exists()
).toBe(false);
expect(
wrapper
.find(
'[data-test-subj="documentsSearchNoDocuments"] [data-test-subj="documentsSearchDocsLink"]'
)
.exists()
).toBe(true);
});
it('will include a button to documentation if the user cannot manage documents', () => {
setMockValues({
...values,
myRole: { canManageEngineDocuments: false },
});
const wrapper = mount(<SearchExperienceContent />);
expect(
wrapper
.find(
'[data-test-subj="documentsSearchNoDocuments"] [data-test-subj="IndexDocumentsButton"]'
)
.exists()
).toBe(false);
expect(
wrapper
.find(
'[data-test-subj="documentsSearchNoDocuments"] [data-test-subj="documentsSearchDocsLink"]'
)
.exists()
).toBe(true);
});
});
});

View file

@ -0,0 +1,114 @@
/*
* 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 React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiSpacer, EuiButton, EuiEmptyPrompt } from '@elastic/eui';
// @ts-expect-error types are not available for this package yet
import { Results, Paging, ResultsPerPage } from '@elastic/react-search-ui';
import { useValues } from 'kea';
import { ResultView } from './views';
import { Pagination } from './pagination';
import { useSearchContextState } from './hooks';
import { DocumentCreationButton } from '../document_creation_button';
import { AppLogic } from '../../../app_logic';
import { EngineLogic } from '../../engine';
import { DOCS_PREFIX } from '../../../routes';
// TODO This is temporary until we create real Result type
interface Result {
[key: string]: {
raw: string | string[] | number | number[] | undefined;
};
}
export const SearchExperienceContent: React.FC = () => {
const { resultSearchTerm, totalResults, wasSearched } = useSearchContextState();
const { myRole } = useValues(AppLogic);
const { engineName, isMetaEngine } = useValues(EngineLogic);
if (!wasSearched) return null;
if (totalResults) {
return (
<EuiFlexGroup direction="column" gutterSize="none" data-test-subj="documentsSearchResults">
<Pagination
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.documents.paging.ariaLabelTop',
{
defaultMessage: 'Search results paging at top of results',
}
)}
/>
<EuiSpacer />
<Results
titleField="id"
resultView={(props: { result: Result }) => {
return <ResultView {...props} engineName={engineName} />;
}}
/>
<EuiSpacer />
<Pagination
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.documents.paging.ariaLabelBottom',
{
defaultMessage: 'Search results paging at bottom of results',
}
)}
/>
</EuiFlexGroup>
);
}
// If we have no results, but have a search term, show a message
if (resultSearchTerm) {
return (
<EuiEmptyPrompt
data-test-subj="documentsSearchNoResults"
body={i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.noResults', {
defaultMessage: 'No results for "{resultSearchTerm}" yet!',
values: {
resultSearchTerm,
},
})}
/>
);
}
// If we have no results AND no search term, show a CTA for the user to index documents
return (
<EuiEmptyPrompt
data-test-subj="documentsSearchNoDocuments"
title={
<h2>
{i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.indexDocumentsTitle', {
defaultMessage: 'No documents yet!',
})}
</h2>
}
body={i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.indexDocuments', {
defaultMessage: 'Indexed documents will show up here.',
})}
actions={
!isMetaEngine && myRole.canManageEngineDocuments ? (
<DocumentCreationButton />
) : (
<EuiButton
data-test-subj="documentsSearchDocsLink"
href={`${DOCS_PREFIX}/indexing-documents-guide.html`}
>
{i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.indexingGuide', {
defaultMessage: 'Read the indexing guide',
})}
</EuiButton>
)
}
/>
);
};

View file

@ -0,0 +1,11 @@
/*
* 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.
*/
export { SearchBoxView } from './search_box_view';
export { SortingView } from './sorting_view';
export { ResultView } from './result_view';
export { ResultsPerPageView } from './results_per_page_view';
export { PagingView } from './paging_view';

View file

@ -0,0 +1,51 @@
/*
* 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 React from 'react';
import { shallow } from 'enzyme';
import { EuiPagination } from '@elastic/eui';
import { PagingView } from './paging_view';
describe('PagingView', () => {
const props = {
current: 1,
totalPages: 20,
onChange: jest.fn(),
'aria-label': 'paging view',
};
beforeEach(() => {
jest.clearAllMocks();
});
it('renders', () => {
const wrapper = shallow(<PagingView {...props} />);
expect(wrapper.find(EuiPagination).length).toBe(1);
});
it('passes through totalPage', () => {
const wrapper = shallow(<PagingView {...props} />);
expect(wrapper.find(EuiPagination).prop('pageCount')).toEqual(20);
});
it('passes through aria-label', () => {
const wrapper = shallow(<PagingView {...props} />);
expect(wrapper.find(EuiPagination).prop('aria-label')).toEqual('paging view');
});
it('decrements current page by 1 and passes it through as activePage', () => {
const wrapper = shallow(<PagingView {...props} />);
expect(wrapper.find(EuiPagination).prop('activePage')).toEqual(0);
});
it('calls onChange when onPageClick is triggered, and adds 1', () => {
const wrapper = shallow(<PagingView {...props} />);
const onPageClick: any = wrapper.find(EuiPagination).prop('onPageClick');
onPageClick(3);
expect(props.onChange).toHaveBeenCalledWith(4);
});
});

View file

@ -0,0 +1,29 @@
/*
* 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 React from 'react';
import { EuiPagination } from '@elastic/eui';
interface Props {
current: number;
totalPages: number;
onChange(pageNumber: number): void;
'aria-label': string;
}
export const PagingView: React.FC<Props> = ({
current,
onChange,
totalPages,
'aria-label': ariaLabel,
}) => (
<EuiPagination
pageCount={totalPages}
activePage={current - 1} // EuiPagination is 0-indexed, Search UI is 1-indexed
onPageClick={(page) => onChange(page + 1)} // EuiPagination is 0-indexed, Search UI is 1-indexed
aria-label={ariaLabel}
/>
);

View file

@ -0,0 +1,24 @@
/*
* 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 React from 'react';
import { shallow } from 'enzyme';
import { ResultView } from '.';
describe('ResultView', () => {
const result = {
id: {
raw: '1',
},
};
it('renders', () => {
const wrapper = shallow(<ResultView result={result} engineName="engine1" />);
expect(wrapper.find('div').length).toBe(1);
});
});

View file

@ -0,0 +1,42 @@
/*
* 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 React from 'react';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { EuiLinkTo } from '../../../../../shared/react_router_helpers';
// TODO replace this with a real result type when we implement a more sophisticated
// ResultView
interface Result {
[key: string]: {
raw: string | string[] | number | number[] | undefined;
};
}
interface Props {
engineName: string;
result: Result;
}
export const ResultView: React.FC<Props> = ({ engineName, result }) => {
// TODO Replace this entire component when we migrate StuiResult
return (
<li>
<EuiPanel>
<EuiLinkTo to={`/engines/${engineName}/documents/${result.id.raw}`}>
<strong>{result.id.raw}</strong>
</EuiLinkTo>
{Object.entries(result).map(([key, value]) => (
<div key={key} style={{ wordBreak: 'break-all' }}>
{key}: {value.raw}
</div>
))}
</EuiPanel>
<EuiSpacer />
</li>
);
};

View file

@ -0,0 +1,57 @@
/*
* 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 React from 'react';
import { shallow } from 'enzyme';
import { EuiSelect } from '@elastic/eui';
import { ResultsPerPageView } from '.';
describe('ResultsPerPageView', () => {
const props = {
options: [1, 2, 3],
value: 1,
onChange: jest.fn(),
};
it('renders', () => {
const wrapper = shallow(<ResultsPerPageView {...props} />);
expect(wrapper.find(EuiSelect).length).toBe(1);
});
it('maps options to correct EuiSelect option', () => {
const wrapper = shallow(<ResultsPerPageView {...props} />);
expect(wrapper.find(EuiSelect).prop('options')).toEqual([
{ text: 1, value: 1 },
{ text: 2, value: 2 },
{ text: 3, value: 3 },
]);
});
it('passes through the value if it exists in options', () => {
const wrapper = shallow(<ResultsPerPageView {...props} />);
expect(wrapper.find(EuiSelect).prop('value')).toEqual(1);
});
it('does not pass through the value if it does not exist in options', () => {
const wrapper = shallow(
<ResultsPerPageView
{...{
...props,
value: 999,
}}
/>
);
expect(wrapper.find(EuiSelect).prop('value')).toBeUndefined();
});
it('passes through an onChange to EuiSelect', () => {
const wrapper = shallow(<ResultsPerPageView {...props} />);
const onChange: any = wrapper.find(EuiSelect).prop('onChange');
onChange({ target: { value: 2 } });
expect(props.onChange).toHaveBeenCalledWith(2);
});
});

View file

@ -0,0 +1,48 @@
/*
* 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 React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSelect, EuiSelectOption } from '@elastic/eui';
const wrapResultsPerPageOptionForEuiSelect: (option: number) => EuiSelectOption = (option) => ({
text: option,
value: option,
});
interface Props {
options: number[];
value: number;
onChange(value: number): void;
}
export const ResultsPerPageView: React.FC<Props> = ({ onChange, options, value }) => {
// If we don't have the value in options, unset it
const selectedValue = value && !options.includes(value) ? undefined : value;
return (
<div>
<EuiSelect
options={options.map(wrapResultsPerPageOptionForEuiSelect)}
value={selectedValue}
prepend={i18n.translate(
'xpack.enterpriseSearch.appSearch.documents.search.resultsPerPage.show',
{
defaultMessage: 'Show:',
}
)}
onChange={(event) => onChange(parseInt(event.target.value, 10))}
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.documents.search.resultsPerPage.ariaLabel',
{
defaultMessage: 'Number of results to show per page',
}
)}
/>
</div>
);
};

View file

@ -0,0 +1,41 @@
/*
* 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 React from 'react';
import { shallow } from 'enzyme';
import { EuiFieldSearch } from '@elastic/eui';
import { SearchBoxView } from './search_box_view';
describe('SearchBoxView', () => {
const props = {
onChange: jest.fn(),
value: 'foo',
inputProps: {
placeholder: 'bar',
'aria-label': 'foo',
'data-test-subj': 'bar',
},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('renders', () => {
const wrapper = shallow(<SearchBoxView {...props} />);
expect(wrapper.type()).toEqual(EuiFieldSearch);
expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('foo');
expect(wrapper.find(EuiFieldSearch).prop('placeholder')).toEqual('bar');
});
it('passes through an onChange to EuiFieldSearch', () => {
const wrapper = shallow(<SearchBoxView {...props} />);
wrapper.prop('onChange')({ target: { value: 'test' } });
expect(props.onChange).toHaveBeenCalledWith('test');
});
});

View file

@ -0,0 +1,30 @@
/*
* 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 React from 'react';
import { EuiFieldSearch } from '@elastic/eui';
interface Props {
inputProps: {
placeholder: string;
'aria-label': string;
'data-test-subj': string;
};
value: string;
onChange(value: string): void;
}
export const SearchBoxView: React.FC<Props> = ({ onChange, value, inputProps }) => {
return (
<EuiFieldSearch
value={value}
onChange={(event) => onChange(event.target.value)}
fullWidth={true}
{...inputProps}
/>
);
};

View file

@ -0,0 +1,58 @@
/*
* 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 React from 'react';
import { shallow } from 'enzyme';
import { EuiSelect } from '@elastic/eui';
import { SortingView } from '.';
describe('SortingView', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const props = {
options: [{ label: 'Label', value: 'Value' }],
value: 'Value',
onChange: jest.fn(),
};
it('renders', () => {
const wrapper = shallow(<SortingView {...props} />);
expect(wrapper.find(EuiSelect).length).toBe(1);
});
it('maps options to correct EuiSelect option', () => {
const wrapper = shallow(<SortingView {...props} />);
expect(wrapper.find(EuiSelect).prop('options')).toEqual([{ text: 'Label', value: 'Value' }]);
});
it('passes through the value if it exists in options', () => {
const wrapper = shallow(<SortingView {...props} />);
expect(wrapper.find(EuiSelect).prop('value')).toEqual('Value');
});
it('does not pass through the value if it does not exist in options', () => {
const wrapper = shallow(
<SortingView
{...{
...props,
value: 'This value is not in Options',
}}
/>
);
expect(wrapper.find(EuiSelect).prop('value')).toBeUndefined();
});
it('passes through an onChange to EuiSelect', () => {
const wrapper = shallow(<SortingView {...props} />);
const onChange: any = wrapper.find(EuiSelect).prop('onChange');
onChange({ target: { value: 'test' } });
expect(props.onChange).toHaveBeenCalledWith('test');
});
});

View file

@ -0,0 +1,53 @@
/*
* 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 React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSelect, EuiSelectOption } from '@elastic/eui';
interface Option {
label: string;
value: string;
}
const wrapSortingOptionForEuiSelect: (option: Option) => EuiSelectOption = (option) => ({
text: option.label,
value: option.value,
});
const getValueFromOption: (option: Option) => string = (option) => option.value;
interface Props {
options: Option[];
value: string;
onChange(value: string): void;
}
export const SortingView: React.FC<Props> = ({ onChange, options, value }) => {
// If we don't have the value in options, unset it
const valuesFromOptions = options.map(getValueFromOption);
const selectedValue = value && !valuesFromOptions.includes(value) ? undefined : value;
return (
<div>
<EuiSelect
options={options.map(wrapSortingOptionForEuiSelect)}
value={selectedValue}
prepend={i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.sortBy', {
defaultMessage: 'Sort by',
})}
onChange={(event) => onChange(event.target.value)}
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.documents.search.sortBy.ariaLabel',
{
defaultMessage: 'Sort results by',
}
)}
/>
</div>
);
};

View file

@ -13,7 +13,7 @@ import { EngineDetails } from './types';
interface EngineValues {
dataLoading: boolean;
engine: EngineDetails | {};
engine: Partial<EngineDetails>;
engineName: string;
isMetaEngine: boolean;
isSampleEngine: boolean;

249
yarn.lock
View file

@ -1419,6 +1419,13 @@
dependencies:
"@elastic/apm-rum-core" "^5.7.0"
"@elastic/app-search-javascript@^7.3.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@elastic/app-search-javascript/-/app-search-javascript-7.8.0.tgz#cbc7af6bcdd224518f7f595145d6ec744e0b165d"
integrity sha512-EsAa/E/dQwBO72nrQ9YrXudP9KVY0sVUOvqPKZ3hBj9Mr3+MtWMyIKcyMf09bzdayk4qE+moetYDe5ahVbiA+Q==
dependencies:
object-hash "^1.3.0"
"@elastic/charts@24.3.0":
version "24.3.0"
resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-24.3.0.tgz#5bb62143c2f941becbbbf91aafde849034b6330f"
@ -1577,6 +1584,26 @@
resolved "https://registry.yarnpkg.com/@elastic/numeral/-/numeral-2.5.0.tgz#8da714827fc278f17546601fdfe55f5c920e2bc5"
integrity sha512-NVTuy9Wzblp6nOH86CXjWXTajHgJGn5Tk2l59/Z5cWFU14KlE+8/zqPTgZdxYABzBJFE3L7S07kJDMN8sDvTmA==
"@elastic/react-search-ui-views@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@elastic/react-search-ui-views/-/react-search-ui-views-1.5.0.tgz#33988ae71588ad3e64f68c6e278d8262f5d59320"
integrity sha512-Ur5Cya+B1em79ZNbPg+KYORuoHDM72LO5lqJeTNrW8WwRTEZi/vL21dOy47VYcSGVnCkttFD2BuyDOTMYFuExQ==
dependencies:
"@babel/runtime" "^7.5.4"
autoprefixer "^9.6.1"
downshift "^3.2.10"
rc-pagination "^1.20.1"
react-select "^2.4.4"
"@elastic/react-search-ui@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@elastic/react-search-ui/-/react-search-ui-1.5.0.tgz#d89304a2d6ad6377fe2a7f9202906f05e9bbc159"
integrity sha512-fcfdD9v/87koM1dCsiAhJQz1Fb8Qz4NHEgRqdxZzSsqDaasTeSTRXX6UgbAiDidTa87mvGpT0SxAz8utAATpTQ==
dependencies:
"@babel/runtime" "^7.5.4"
"@elastic/react-search-ui-views" "1.5.0"
"@elastic/search-ui" "1.5.0"
"@elastic/request-crypto@1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@elastic/request-crypto/-/request-crypto-1.1.4.tgz#2189d5fea65f7afe1de9f5fa3d0dd420e93e3124"
@ -1590,11 +1617,42 @@
version "0.0.0"
uid ""
"@elastic/search-ui-app-search-connector@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@elastic/search-ui-app-search-connector/-/search-ui-app-search-connector-1.5.0.tgz#d379132c5015775acfaee5322ec019e9c0559ccc"
integrity sha512-lHuXBjaMaN1fsm1taQMR/7gfpAg4XOsvZOi8u1AoufUw9kGr6Xc00Gznj1qTyH0Qebi2aSmY0NBN6pdIEGvvGQ==
dependencies:
"@babel/runtime" "^7.5.4"
"@elastic/app-search-javascript" "^7.3.0"
"@elastic/search-ui@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@elastic/search-ui/-/search-ui-1.5.0.tgz#32ea25f3a4fca10d0c56d535658415b276593f05"
integrity sha512-UJzh3UcaAWKLjDIeJlVd0Okg+InLp8bijk+yOvCe4wtbVpTu5NCvAsfxo6mVTNnxS1ik9cRpMOqDT5sw6qyKoQ==
dependencies:
"@babel/runtime" "^7.5.4"
date-fns "^1.30.1"
deep-equal "^1.0.1"
history "^4.9.0"
qs "^6.7.0"
"@elastic/ui-ace@0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@elastic/ui-ace/-/ui-ace-0.2.3.tgz#5281aed47a79b7216c55542b0675e435692f20cd"
integrity sha512-Nti5s2dplBPhSKRwJxG9JXTMOev4jVOWcnTJD1TOkJr1MUBYKVZcNcJtIVMSvahWGmP0B/UfO9q9lyRqdivkvQ==
"@emotion/babel-utils@^0.6.4":
version "0.6.10"
resolved "https://registry.yarnpkg.com/@emotion/babel-utils/-/babel-utils-0.6.10.tgz#83dbf3dfa933fae9fc566e54fbb45f14674c6ccc"
integrity sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow==
dependencies:
"@emotion/hash" "^0.6.6"
"@emotion/memoize" "^0.6.6"
"@emotion/serialize" "^0.9.1"
convert-source-map "^1.5.1"
find-root "^1.1.0"
source-map "^0.7.2"
"@emotion/cache@^10.0.27", "@emotion/cache@^10.0.9":
version "10.0.29"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
@ -1631,6 +1689,11 @@
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
"@emotion/hash@^0.6.2", "@emotion/hash@^0.6.6":
version "0.6.6"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44"
integrity sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ==
"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.6", "@emotion/is-prop-valid@^0.8.8":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
@ -1643,6 +1706,11 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
"@emotion/memoize@^0.6.1", "@emotion/memoize@^0.6.6":
version "0.6.6"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b"
integrity sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ==
"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16":
version "0.11.16"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad"
@ -1654,6 +1722,16 @@
"@emotion/utils" "0.11.3"
csstype "^2.5.7"
"@emotion/serialize@^0.9.1":
version "0.9.1"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.9.1.tgz#a494982a6920730dba6303eb018220a2b629c145"
integrity sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ==
dependencies:
"@emotion/hash" "^0.6.6"
"@emotion/memoize" "^0.6.6"
"@emotion/unitless" "^0.6.7"
"@emotion/utils" "^0.8.2"
"@emotion/sheet@0.9.4":
version "0.9.4"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5"
@ -1682,16 +1760,31 @@
resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04"
integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
"@emotion/stylis@^0.7.0":
version "0.7.1"
resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5"
integrity sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ==
"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.4":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
"@emotion/unitless@^0.6.2", "@emotion/unitless@^0.6.7":
version "0.6.7"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.6.7.tgz#53e9f1892f725b194d5e6a1684a7b394df592397"
integrity sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg==
"@emotion/utils@0.11.3":
version "0.11.3"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924"
integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==
"@emotion/utils@^0.8.2":
version "0.8.2"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc"
integrity sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw==
"@emotion/weak-memoize@0.2.5":
version "0.2.5"
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
@ -7593,6 +7686,19 @@ autobind-decorator@^1.3.4:
resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-1.4.3.tgz#4c96ffa77b10622ede24f110f5dbbf56691417d1"
integrity sha1-TJb/p3sQYi7eJPEQ9du/VmkUF9E=
autoprefixer@^9.6.1:
version "9.8.6"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f"
integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==
dependencies:
browserslist "^4.12.0"
caniuse-lite "^1.0.30001109"
colorette "^1.2.1"
normalize-range "^0.1.2"
num2fraction "^1.2.2"
postcss "^7.0.32"
postcss-value-parser "^4.1.0"
autoprefixer@^9.7.2, autoprefixer@^9.7.4:
version "9.8.5"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.5.tgz#2c225de229ddafe1d1424c02791d0c3e10ccccaa"
@ -7811,6 +7917,24 @@ babel-plugin-emotion@^10.0.20, babel-plugin-emotion@^10.0.27:
find-root "^1.1.0"
source-map "^0.5.7"
babel-plugin-emotion@^9.2.11:
version "9.2.11"
resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz#319c005a9ee1d15bb447f59fe504c35fd5807728"
integrity sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@emotion/babel-utils" "^0.6.4"
"@emotion/hash" "^0.6.2"
"@emotion/memoize" "^0.6.1"
"@emotion/stylis" "^0.7.0"
babel-plugin-macros "^2.0.0"
babel-plugin-syntax-jsx "^6.18.0"
convert-source-map "^1.5.0"
find-root "^1.1.0"
mkdirp "^0.5.1"
source-map "^0.5.7"
touch "^2.0.1"
babel-plugin-extract-import-names@1.6.16:
version "1.6.16"
resolved "https://registry.yarnpkg.com/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.6.16.tgz#b964004e794bdd62534c525db67d9e890d5cc079"
@ -8084,7 +8208,7 @@ babel-preset-jest@^26.6.2:
babel-plugin-transform-undefined-to-void "^6.9.4"
lodash.isplainobject "^4.0.6"
babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
babel-runtime@6.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
@ -9114,6 +9238,11 @@ caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.300010
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001150.tgz"
integrity sha512-kiNKvihW0m36UhAFnl7bOAv0i1K1f6wpfVtTF5O5O82XzgtBnb05V0XeV3oZ968vfg2sRNChsHw8ASH2hDfoYQ==
caniuse-lite@^1.0.30001109:
version "1.0.30001164"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001164.tgz#5bbfd64ca605d43132f13cc7fdabb17c3036bfdc"
integrity sha512-G+A/tkf4bu0dSp9+duNiXc7bGds35DioCyC6vgK2m/rjA4Krpy5WeZgZyfH2f0wj2kI6yAWWucyap6oOwmY1mg==
caniuse-lite@^1.0.30001135:
version "1.0.30001144"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001144.tgz#bca0fffde12f97e1127a351fec3bfc1971aa3b3d"
@ -9883,7 +10012,7 @@ color@3.0.x:
color-convert "^1.9.1"
color-string "^1.5.2"
colorette@^1.2.0:
colorette@^1.2.0, colorette@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
@ -10044,6 +10173,11 @@ compression@^1.7.4:
safe-buffer "5.1.2"
vary "~1.1.2"
compute-scroll-into-view@^1.0.9:
version "1.0.16"
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.16.tgz#5b7bf4f7127ea2c19b750353d7ce6776a90ee088"
integrity sha512-a85LHKY81oQnikatZYA90pufpZ6sQx++BoCxOEMsjpZx+ZnaKGQnCyCehTRr/1p9GBIAHTjcU9k71kSYWloLiQ==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -10403,6 +10537,19 @@ create-ecdh@^4.0.0:
bn.js "^4.1.0"
elliptic "^6.0.0"
create-emotion@^9.2.12:
version "9.2.12"
resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-9.2.12.tgz#0fc8e7f92c4f8bb924b0fef6781f66b1d07cb26f"
integrity sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA==
dependencies:
"@emotion/hash" "^0.6.2"
"@emotion/memoize" "^0.6.1"
"@emotion/stylis" "^0.7.0"
"@emotion/unitless" "^0.6.2"
csstype "^2.5.2"
stylis "^3.5.0"
stylis-rule-sheet "^0.0.10"
create-error-class@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
@ -10726,6 +10873,11 @@ csstype@^2.2.0, csstype@^2.5.5, csstype@^2.5.7, csstype@^2.6.7:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5"
integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==
csstype@^2.5.2:
version "2.6.14"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.14.tgz#004822a4050345b55ad4dcc00be1d9cf2f4296de"
integrity sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==
cucumber-expressions@^5.0.13:
version "5.0.18"
resolved "https://registry.yarnpkg.com/cucumber-expressions/-/cucumber-expressions-5.0.18.tgz#6c70779efd3aebc5e9e7853938b1110322429596"
@ -11202,6 +11354,11 @@ date-fns@^1.27.2:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"
integrity sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==
date-fns@^1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
@ -11816,6 +11973,13 @@ dom-converter@~0.2:
dependencies:
utila "~0.4"
dom-helpers@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
dependencies:
"@babel/runtime" "^7.1.2"
dom-helpers@^5.0.0, dom-helpers@^5.0.1:
version "5.1.4"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b"
@ -11956,6 +12120,16 @@ dotignore@^0.1.2:
dependencies:
minimatch "^3.0.4"
downshift@^3.2.10:
version "3.4.8"
resolved "https://registry.yarnpkg.com/downshift/-/downshift-3.4.8.tgz#06b7ad9e9c423a58e8a9049b2a00a5d19c7ef954"
integrity sha512-dZL3iNL/LbpHNzUQAaVq/eTD1ocnGKKjbAl/848Q0KEp6t81LJbS37w3f93oD6gqqAnjdgM7Use36qZSipHXBw==
dependencies:
"@babel/runtime" "^7.4.5"
compute-scroll-into-view "^1.0.9"
prop-types "^15.7.2"
react-is "^16.9.0"
dpdm@3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/dpdm/-/dpdm-3.5.0.tgz#414402f21928694bc86cfe8e3583dc8fc97d013e"
@ -12231,6 +12405,14 @@ emotion-theming@^10.0.19:
"@emotion/weak-memoize" "0.2.5"
hoist-non-react-statics "^3.3.0"
emotion@^9.1.2:
version "9.2.12"
resolved "https://registry.yarnpkg.com/emotion/-/emotion-9.2.12.tgz#53925aaa005614e65c6e43db8243c843574d1ea9"
integrity sha512-hcx7jppaI8VoXxIWEhxpDW7I+B4kq9RNzQLmsrF6LY8BGKqe2N+gFAQr0EfuFucFlPs2A9HM4+xNj4NeqEWIOQ==
dependencies:
babel-plugin-emotion "^9.2.11"
create-emotion "^9.2.12"
enabled@1.0.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/enabled/-/enabled-1.0.2.tgz#965f6513d2c2d1c5f4652b64a2e3396467fc2f93"
@ -20980,7 +21162,7 @@ object-filter-sequence@^1.0.0:
resolved "https://registry.yarnpkg.com/object-filter-sequence/-/object-filter-sequence-1.0.0.tgz#10bb05402fff100082b80d7e83991b10db411692"
integrity sha512-CsubGNxhIEChNY4cXYuA6KXafztzHqzLLZ/y3Kasf3A+sa3lL9thq3z+7o0pZqzEinjXT6lXDPAfVWI59dUyzQ==
object-hash@^1.3.1:
object-hash@^1.3.0, object-hash@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
@ -22680,6 +22862,11 @@ qs@6.7.0, qs@^6.4.0, qs@^6.5.1, qs@^6.6.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
qs@^6.7.0:
version "6.9.4"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687"
integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@ -22748,7 +22935,7 @@ raf-schd@^4.0.0, raf-schd@^4.0.2:
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
raf@^3.1.0, raf@^3.4.1:
raf@^3.1.0, raf@^3.4.0, raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
@ -22854,6 +23041,16 @@ rbush@^3.0.1:
dependencies:
quickselect "^2.0.0"
rc-pagination@^1.20.1:
version "1.21.1"
resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-1.21.1.tgz#24206cf4be96119baae8decd3f9ffac91cc2c4d3"
integrity sha512-Z+iYLbrJOBKHdgoAjLhL9jOgb7nrbPzNmV31p0ikph010/Ov1+UkrauYzWhumUyR+GbRFi3mummdKW/WtlOewA==
dependencies:
babel-runtime "6.x"
classnames "^2.2.6"
prop-types "^15.5.7"
react-lifecycles-compat "^3.0.4"
rc@^1.0.1, rc@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@ -23143,7 +23340,7 @@ react-hotkeys@2.0.0:
dependencies:
prop-types "^15.6.1"
react-input-autosize@^2.2.2:
react-input-autosize@^2.2.1, react-input-autosize@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2"
integrity sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==
@ -23371,6 +23568,19 @@ react-router@5.2.0, react-router@^5.2.0:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-select@^2.4.4:
version "2.4.4"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-2.4.4.tgz#ba72468ef1060c7d46fbb862b0748f96491f1f73"
integrity sha512-C4QPLgy9h42J/KkdrpVxNmkY6p4lb49fsrbDk/hRcZpX7JvZPNb6mGj+c5SzyEtBv1DmQ9oPH4NmhAFvCrg8Jw==
dependencies:
classnames "^2.2.5"
emotion "^9.1.2"
memoize-one "^5.0.0"
prop-types "^15.6.0"
raf "^3.4.0"
react-input-autosize "^2.2.1"
react-transition-group "^2.2.1"
react-select@^3.0.8:
version "3.1.0"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.1.0.tgz#ab098720b2e9fe275047c993f0d0caf5ded17c27"
@ -23481,6 +23691,16 @@ react-tiny-virtual-list@^2.2.0:
dependencies:
prop-types "^15.5.7"
react-transition-group@^2.2.1:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
dependencies:
dom-helpers "^3.4.0"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react-transition-group@^4.3.0:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
@ -25555,7 +25775,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@^0.7.3:
source-map@^0.7.2, source-map@^0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
@ -26329,11 +26549,21 @@ styled-components@^5.1.0:
shallowequal "^1.1.0"
supports-color "^5.5.0"
stylis-rule-sheet@^0.0.10:
version "0.0.10"
resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430"
integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==
stylis@3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1"
integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw==
stylis@^3.5.0:
version "3.5.4"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe"
integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==
subarg@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2"
@ -27148,6 +27378,13 @@ topojson-client@^3.1.0:
dependencies:
commander "2"
touch@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164"
integrity sha512-qjNtvsFXTRq7IuMLweVgFxmEuQ6gLbRs2jQxL80TtZ31dEKWYIxRXquij6w6VimyDek5hD3PytljHmEtAs2u0A==
dependencies:
nopt "~1.0.10"
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"