[Search] Move enterprise_search index documents to common package (#172211)

This PR: 
* extracts `enterprise_search` index documents component to common
package.
* Uses EUI pagination, converting Elasticsearch`from` & `size` to EUI
pagination standard for documents list


### Screen Recording


f585d9cc-f92c-44f4-aead-23c75c107a0f


### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Saarika Bhasi 2023-12-05 10:33:15 -05:00 committed by GitHub
parent 434b1afd42
commit 7ecd525a02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 711 additions and 528 deletions

1
.github/CODEOWNERS vendored
View file

@ -648,6 +648,7 @@ x-pack/plugins/screenshotting @elastic/kibana-reporting-services
packages/kbn-search-api-panels @elastic/enterprise-search-frontend
packages/kbn-search-connectors @elastic/enterprise-search-frontend
examples/search_examples @elastic/kibana-data-discovery
packages/kbn-search-index-documents @elastic/enterprise-search-frontend
packages/kbn-search-response-warnings @elastic/kibana-data-discovery
x-pack/plugins/searchprofiler @elastic/platform-deployment-management
x-pack/test/security_api_integration/packages/helpers @elastic/kibana-security

View file

@ -100,6 +100,7 @@
"share": "src/plugins/share",
"sharedUXPackages": "packages/shared-ux",
"searchApiPanels": "packages/kbn-search-api-panels/",
"searchIndexDocuments": "packages/kbn-search-index-documents",
"searchResponseWarnings": "packages/kbn-search-response-warnings",
"securitySolutionPackages": "x-pack/packages/security-solution",
"serverlessPackages": "packages/serverless",

View file

@ -651,6 +651,7 @@
"@kbn/search-api-panels": "link:packages/kbn-search-api-panels",
"@kbn/search-connectors": "link:packages/kbn-search-connectors",
"@kbn/search-examples-plugin": "link:examples/search_examples",
"@kbn/search-index-documents": "link:packages/kbn-search-index-documents",
"@kbn/search-response-warnings": "link:packages/kbn-search-response-warnings",
"@kbn/searchprofiler-plugin": "link:x-pack/plugins/searchprofiler",
"@kbn/security-plugin": "link:x-pack/plugins/security",

View file

@ -0,0 +1,11 @@
# Helper components to aid with listing documents in an index
This package provides UI components with pagination to list documents in an `index`.
## Setup
To use this package, add the package name @kbn/search-index-documents in your `tsconfig.json`.
## Implementation reference
- [x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx](../../x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx)

View file

@ -1,14 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import { useValues } from 'kea';
import { MappingProperty, SearchHit } from '@elastic/elasticsearch/lib/api/types';
import {
@ -23,30 +21,29 @@ import {
EuiPopover,
EuiText,
EuiSpacer,
Pagination,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react';
import { Meta } from '../../../../../../../common/types';
import { Result } from '../../../../../shared/result/result';
import { resultMetaData } from '../../../../../shared/result/result_metadata';
import { IndexViewLogic } from '../../index_view_logic';
import { resultMetaData } from './result/result_metadata';
import { Result } from '..';
interface DocumentListProps {
dataTelemetryIdPrefix: string;
docs: SearchHit[];
docsPerPage: number;
isLoading: boolean;
mappings: Record<string, MappingProperty> | undefined;
meta: Meta;
meta: Pagination;
onPaginate: (newPageIndex: number) => void;
setDocsPerPage: (docsPerPage: number) => void;
}
export const DocumentList: React.FC<DocumentListProps> = ({
dataTelemetryIdPrefix,
docs,
docsPerPage,
isLoading,
@ -55,8 +52,6 @@ export const DocumentList: React.FC<DocumentListProps> = ({
onPaginate,
setDocsPerPage,
}) => {
const { ingestionMethod } = useValues(IndexViewLogic);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const resultToField = (result: SearchHit) => {
if (mappings && result._source && !Array.isArray(result._source)) {
@ -77,22 +72,22 @@ export const DocumentList: React.FC<DocumentListProps> = ({
return size === docsPerPage ? 'check' : 'empty';
};
const pageCount = meta?.pageSize ? Math.ceil(meta.totalItemCount / meta?.pageSize) : 0;
return (
<>
<EuiPagination
aria-label={i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationAriaLabel',
{ defaultMessage: 'Pagination for document list' }
)}
pageCount={meta.page.total_pages}
activePage={meta.page.current}
aria-label={i18n.translate('searchIndexDocuments.documentList.paginationAriaLabel', {
defaultMessage: 'Pagination for document list',
})}
pageCount={pageCount}
activePage={meta.pageIndex}
onPageClick={onPaginate}
/>
<EuiSpacer size="m" />
<EuiText size="xs">
<p>
<FormattedMessage
id="xpack.enterpriseSearch.content.searchIndex.documents.documentList.description"
id="searchIndexDocuments.documentList.description"
defaultMessage="Showing {results} of {total}.
Search results maxed at {maximum} documents."
values={{
@ -104,7 +99,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
),
total: (
<strong>
<FormattedNumber value={meta.page.total_results} />
<FormattedNumber value={meta.totalItemCount} />
</strong>
),
}}
@ -125,24 +120,22 @@ export const DocumentList: React.FC<DocumentListProps> = ({
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiPagination
aria-label={i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationAriaLabel',
{ defaultMessage: 'Pagination for document list' }
)}
pageCount={meta.page.total_pages}
activePage={meta.page.current}
aria-label={i18n.translate('searchIndexDocuments.documentList.paginationAriaLabel', {
defaultMessage: 'Pagination for document list',
})}
pageCount={pageCount}
activePage={meta.pageIndex}
onPageClick={onPaginate}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
aria-label={i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.docsPerPage',
{ defaultMessage: 'Document count per page dropdown' }
)}
aria-label={i18n.translate('searchIndexDocuments.documentList.docsPerPage', {
defaultMessage: 'Document count per page dropdown',
})}
button={
<EuiButtonEmpty
data-telemetry-id={`entSearchContent-${ingestionMethod}-documents-docsPerPage`}
data-telemetry-id={`${dataTelemetryIdPrefix}-documents-docsPerPage`}
size="s"
iconType="arrowDown"
iconSide="right"
@ -150,13 +143,10 @@ export const DocumentList: React.FC<DocumentListProps> = ({
setIsPopoverOpen(true);
}}
>
{i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.pagination.itemsPerPage',
{
defaultMessage: 'Documents per page: {docPerPage}',
values: { docPerPage: docsPerPage },
}
)}
{i18n.translate('searchIndexDocuments.documentList.pagination.itemsPerPage', {
defaultMessage: 'Documents per page: {docPerPage}',
values: { docPerPage: docsPerPage },
})}
</EuiButtonEmpty>
}
isOpen={isPopoverOpen}
@ -177,10 +167,10 @@ export const DocumentList: React.FC<DocumentListProps> = ({
setDocsPerPage(10);
}}
>
{i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationOptions.option',
{ defaultMessage: '{docCount} documents', values: { docCount: 10 } }
)}
{i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', {
defaultMessage: '{docCount} documents',
values: { docCount: 10 },
})}
</EuiContextMenuItem>,
<EuiContextMenuItem
@ -191,10 +181,10 @@ export const DocumentList: React.FC<DocumentListProps> = ({
setDocsPerPage(25);
}}
>
{i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationOptions.option',
{ defaultMessage: '{docCount} documents', values: { docCount: 25 } }
)}
{i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', {
defaultMessage: '{docCount} documents',
values: { docCount: 25 },
})}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="50 rows"
@ -204,10 +194,10 @@ export const DocumentList: React.FC<DocumentListProps> = ({
setDocsPerPage(50);
}}
>
{i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationOptions.option',
{ defaultMessage: '{docCount} documents', values: { docCount: 50 } }
)}
{i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', {
defaultMessage: '{docCount} documents',
values: { docCount: 50 },
})}
</EuiContextMenuItem>,
]}
/>
@ -216,12 +206,12 @@ export const DocumentList: React.FC<DocumentListProps> = ({
</EuiFlexGroup>
<EuiSpacer />
{meta.page.total_results > 9999 && (
{meta.totalItemCount > 9999 && (
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.enterpriseSearch.content.searchIndex.documents.documentList.resultLimitTitle"
id="searchIndexDocuments.documentList.resultLimitTitle"
defaultMessage="Results are limited to {number} documents"
values={{
number: <FormattedNumber value={10000} />,
@ -232,7 +222,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
>
<p>
<FormattedMessage
id="xpack.enterpriseSearch.content.searchIndex.documents.documentList.resultLimit"
id="searchIndexDocuments.documentList.resultLimit"
defaultMessage="Only the first {number} results are available for paging. Please use the search bar to filter down your results."
values={{
number: <FormattedNumber value={10000} />,

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { I18nProvider } from '@kbn/i18n-react';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { INDEX_DOCUMENTS_META_DEFAULT } from '../types';
import { DocumentList } from './document_list';
import '@testing-library/jest-dom';
export const DEFAULT_VALUES = {
dataTelemetryIdPrefix: `entSearchContent-api`,
docs: [],
docsPerPage: 25,
isLoading: true,
mappings: undefined,
meta: INDEX_DOCUMENTS_META_DEFAULT,
onPaginate: () => {},
setDocsPerPage: () => {},
};
const mockValues = { ...DEFAULT_VALUES };
describe('DocumentList', () => {
test('render empty', async () => {
const { container } = render(
<I18nProvider>
<DocumentList {...DEFAULT_VALUES} />
</I18nProvider>
);
expect(container.querySelector('[data-testId="search-index-documents-result"]')).toBeNull();
expect(container.querySelector('[aria-label="Pagination for document list"]')).not.toBeNull();
});
test('renders documents when results when there is data and mappings', () => {
const values = {
...DEFAULT_VALUES,
docs: [
{
_id: 'M9ntXoIBTq5dF-1Xnc8A',
_index: 'kibana_sample_data_flights',
_score: 1,
_source: {
AvgTicketPrice: 268.24159591388866,
},
},
{
_id: 'NNntXoIBTq5dF-1Xnc8A',
_index: 'kibana_sample_data_flights',
_score: 1,
_source: {
AvgTicketPrice: 68.91388866,
},
},
],
mappings: {
AvgTicketPrice: {
type: 'float' as const,
},
},
};
render(
<I18nProvider>
<DocumentList {...values} />
</I18nProvider>
);
expect(screen.getByText('Document id: M9ntXoIBTq5dF-1Xnc8A')).toBeInTheDocument();
expect(screen.getByText('Document id: NNntXoIBTq5dF-1Xnc8A')).toBeInTheDocument();
});
test('renders callout when total results are 10.000', () => {
const values = {
...DEFAULT_VALUES,
...mockValues,
meta: {
...INDEX_DOCUMENTS_META_DEFAULT,
totalItemCount: 10000,
},
};
render(
<I18nProvider>
<DocumentList {...values} />
</I18nProvider>
);
expect(screen.getByText('Results are limited to 10,000 documents')).toBeInTheDocument();
});
});

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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { I18nProvider } from '@kbn/i18n-react';
import { render, screen } from '@testing-library/react';
import React from 'react';
import '@testing-library/jest-dom';
import { DEFAULT_VALUES } from './documents_list.test';
import { DocumentList } from './document_list';
import { DocumentsOverview } from './documents_overview';
describe('DocumentList', () => {
test('render empty', async () => {
render(
<I18nProvider>
<DocumentsOverview
dataTelemetryIdPrefix="entSearch-telemetry"
searchQueryCallback={() => {}}
documentComponent={<DocumentList {...DEFAULT_VALUES} />}
/>
</I18nProvider>
);
expect(screen.getByText('Browse documents')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
EuiFieldSearch,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { ChangeEvent } from 'react';
interface DocumentsProps {
accessControlSwitch?: React.ReactNode;
dataTelemetryIdPrefix: string;
documentComponent: React.ReactNode;
searchQueryCallback: (searchQuery: string) => void;
}
export const DocumentsOverview: React.FC<DocumentsProps> = ({
accessControlSwitch,
dataTelemetryIdPrefix,
documentComponent,
searchQueryCallback,
}) => {
return (
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
<EuiSpacer />
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFlexGroup direction="row" alignItems="center">
<EuiFlexItem className="enterpriseSearchDocumentsHeader" grow={false}>
<EuiTitle>
<h2>
{i18n.translate('searchIndexDocuments.documents.title', {
defaultMessage: 'Browse documents',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
{accessControlSwitch && <EuiFlexItem grow={false}>{accessControlSwitch}</EuiFlexItem>}
<EuiFlexItem>
<EuiFieldSearch
data-telemetry-id={`${dataTelemetryIdPrefix}-documents-searchDocuments`}
placeholder={i18n.translate(
'searchIndexDocuments.documents.searchField.placeholder',
{
defaultMessage: 'Search documents in this index',
}
)}
isClearable
onChange={(event: ChangeEvent<HTMLInputElement>) =>
searchQueryCallback(event.target.value)
}
fullWidth
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>{documentComponent}</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './documents_overview';
export * from './document_list';
export * from './result';

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { Result } from './result';

View file

@ -1,10 +1,3 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
.resultField:nth-child(odd) {
background-color: $euiColorLightestShade;
}

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState } from 'react';
@ -11,11 +12,11 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiToolTip } from '
import { i18n } from '@kbn/i18n';
import { ResultFields } from './result_fields';
import { ResultHeader } from './result_header';
import { ResultFields } from './results_fields';
import { MetaDataProps, ResultFieldProps } from './types';
import { ResultHeader } from './result_header';
import './result.scss';
import { MetaDataProps, ResultFieldProps } from './result_types';
interface ResultProps {
fields: ResultFieldProps[];
@ -24,25 +25,24 @@ interface ResultProps {
export const Result: React.FC<ResultProps> = ({ metaData, fields }) => {
const [isExpanded, setIsExpanded] = useState(false);
const tooltipText =
fields.length <= 3
? i18n.translate('xpack.enterpriseSearch.shared.result.expandTooltip.allVisible', {
? i18n.translate('searchIndexDocuments.result.expandTooltip.allVisible', {
defaultMessage: 'All fields are visible',
})
: isExpanded
? i18n.translate('xpack.enterpriseSearch.shared.result.expandTooltip.showFewer', {
? i18n.translate('searchIndexDocuments.result.expandTooltip.showFewer', {
defaultMessage: 'Show {amount} fewer fields',
values: { amount: fields.length - 3 },
})
: i18n.translate('xpack.enterpriseSearch.shared.result.expandTooltip.showMore', {
: i18n.translate('searchIndexDocuments.result.expandTooltip.showMore', {
defaultMessage: 'Show {amount} more fields',
values: { amount: fields.length - 3 },
});
const toolTipContent = <>{tooltipText}</>;
return (
<EuiPanel hasBorder paddingSize="s">
<EuiPanel hasBorder paddingSize="s" data-test-subj="search-index-documents-result">
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
@ -50,7 +50,7 @@ export const Result: React.FC<ResultProps> = ({ metaData, fields }) => {
<ResultHeader
title={
metaData.title ??
i18n.translate('xpack.enterpriseSearch.shared.result.title.id', {
i18n.translate('searchIndexDocuments.result.title.id', {
defaultMessage: 'Document id: {id}',
values: { id: metaData.id },
})

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
@ -17,9 +18,7 @@ import {
} from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
import { ResultFieldProps } from './types';
import './result.scss';
import { ResultFieldProps } from './result_types';
const iconMap: Record<string, string> = {
boolean: 'tokenBoolean',

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState } from 'react';
@ -20,10 +21,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { MetaDataProps } from './types';
import './result.scss';
import { MetaDataProps } from './result_types';
interface Props {
metaData: MetaDataProps;
@ -58,17 +56,17 @@ const MetadataPopover: React.FC<MetaDataProps> = ({ id, onDocumentDelete }) => {
iconType="iInCircle"
color="primary"
onClick={() => setPopoverIsOpen(!popoverIsOpen)}
aria-label={i18n.translate(
'xpack.enterpriseSearch.content.shared.result.header.metadata.icon.ariaLabel',
{ defaultMessage: 'Metadata for document: {id}', values: { id } }
)}
aria-label={i18n.translate('searchIndexDocuments.result.header.metadata.icon.ariaLabel', {
defaultMessage: 'Metadata for document: {id}',
values: { id },
})}
/>
);
return (
<EuiPopover button={metaDataIcon} isOpen={popoverIsOpen} closePopover={closePopover}>
<EuiPopoverTitle>
{i18n.translate('xpack.enterpriseSearch.content.shared.result.header.metadata.title', {
{i18n.translate('searchIndexDocuments.result.header.metadata.title', {
defaultMessage: 'Document metadata',
})}
</EuiPopoverTitle>
@ -83,12 +81,9 @@ const MetadataPopover: React.FC<MetaDataProps> = ({ id, onDocumentDelete }) => {
{onDocumentDelete && (
<EuiPopoverFooter>
<EuiButton iconType="trash" color="danger" size="s" onClick={closePopover} fullWidth>
{i18n.translate(
'xpack.enterpriseSearch.content.shared.result.header.metadata.deleteDocument',
{
defaultMessage: 'Delete document',
}
)}
{i18n.translate('searchIndexDocuments.result.header.metadata.deleteDocument', {
defaultMessage: 'Delete document',
})}
</EuiButton>
</EuiPopoverFooter>
)}

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SearchHit } from '@elastic/elasticsearch/lib/api/types';

View file

@ -1,13 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SearchHit } from '@elastic/elasticsearch/lib/api/types';
import { MetaDataProps } from './types';
import { MetaDataProps } from './result_types';
const TITLE_KEYS = ['title', 'name'];

View file

@ -1,18 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { IconType } from '@elastic/eui';
export interface MetaDataProps {
id: string;
onDocumentDelete?: Function;
title?: string;
}
export interface ResultFieldProps {
fieldName: string;
fieldType?: string;
@ -20,3 +14,8 @@ export interface ResultFieldProps {
iconType?: IconType;
isExpanded?: boolean;
}
export interface MetaDataProps {
id: string;
onDocumentDelete?: Function;
title?: string;
}

View file

@ -1,15 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiTable, EuiTableBody } from '@elastic/eui';
import { ResultField } from './result_field';
import { ResultFieldProps } from './types';
import { ResultFieldProps } from './result_types';
interface Props {
fields: ResultFieldProps[];

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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './components';
export * from './types';
export * from './lib';

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-search-index-documents'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/search-index-documents",
"owner": "@elastic/enterprise-search-frontend"
}

View file

@ -1,14 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
import { IScopedClusterClient } from '@kbn/core/server';
import { ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } from '../../common/constants';
import { DEFAULT_DOCS_PER_PAGE } from '../types';
import { fetchSearchResults } from './fetch_search_results';
@ -22,52 +21,55 @@ describe('fetchSearchResults lib function', () => {
const indexName = 'search-regular-index';
const query = 'banana';
const regularSearchResultsResponse = {
took: 4,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
const mockSearchResponseWithHits = {
_shards: { failed: 0, skipped: 0, successful: 2, total: 2 },
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: null,
hits: [
{
_index: 'search-regular-index',
_id: '5a12292a0f5ae10021650d7e',
_index: 'search-regular-index',
_score: 4.437291,
_source: {
name: 'banana',
id: '5a12292a0f5ae10021650d7e',
},
_source: { id: '5a12292a0f5ae10021650d7e', name: 'banana' },
},
],
max_score: null,
total: { relation: 'eq', value: 1 },
},
timed_out: false,
took: 4,
};
const regularSearchResultsResponse = {
data: [
{
_index: 'search-regular-index',
_id: '5a12292a0f5ae10021650d7e',
_score: 4.437291,
_source: {
name: 'banana',
id: '5a12292a0f5ae10021650d7e',
},
},
],
_meta: {
page: {
from: 0,
has_more_hits_than_total: false,
size: 25,
total: 1,
},
},
};
const emptySearchResultsResponse = {
took: 4,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 0,
relation: 'eq',
data: [],
_meta: {
page: {
from: 0,
has_more_hits_than_total: false,
size: 25,
total: 0,
},
max_score: null,
hits: [],
},
};
@ -76,8 +78,8 @@ describe('fetchSearchResults lib function', () => {
});
it('should return search results with hits', async () => {
mockClient.asCurrentUser.search.mockImplementation(
() => regularSearchResultsResponse as SearchResponseBody
mockClient.asCurrentUser.search.mockImplementation(() =>
Promise.resolve(mockSearchResponseWithHits)
);
await expect(
@ -88,20 +90,22 @@ describe('fetchSearchResults lib function', () => {
from: DEFAULT_FROM_VALUE,
index: indexName,
q: query,
size: ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
size: DEFAULT_DOCS_PER_PAGE,
});
});
it('should escape quotes in queries and return results with hits', async () => {
mockClient.asCurrentUser.search.mockImplementation(
() => regularSearchResultsResponse as SearchResponseBody
mockClient.asCurrentUser.search.mockImplementation(() =>
Promise.resolve(mockSearchResponseWithHits)
);
await expect(
fetchSearchResults(
mockClient as unknown as IScopedClusterClient,
indexName,
'"yellow banana"'
'"yellow banana"',
0,
DEFAULT_DOCS_PER_PAGE
)
).resolves.toEqual(regularSearchResultsResponse);
@ -109,13 +113,13 @@ describe('fetchSearchResults lib function', () => {
from: DEFAULT_FROM_VALUE,
index: indexName,
q: '\\"yellow banana\\"',
size: ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
size: DEFAULT_DOCS_PER_PAGE,
});
});
it('should return search results with hits when no query is passed', async () => {
mockClient.asCurrentUser.search.mockImplementation(
() => regularSearchResultsResponse as SearchResponseBody
mockClient.asCurrentUser.search.mockImplementation(() =>
Promise.resolve(mockSearchResponseWithHits)
);
await expect(
@ -125,13 +129,23 @@ describe('fetchSearchResults lib function', () => {
expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({
from: DEFAULT_FROM_VALUE,
index: indexName,
size: ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
size: DEFAULT_DOCS_PER_PAGE,
});
});
it('should return empty search results', async () => {
mockClient.asCurrentUser.search.mockImplementationOnce(
() => emptySearchResultsResponse as SearchResponseBody
mockClient.asCurrentUser.search.mockImplementationOnce(() =>
Promise.resolve({
...mockSearchResponseWithHits,
hits: {
...mockSearchResponseWithHits.hits,
total: {
...mockSearchResponseWithHits.hits.total,
value: 0,
},
hits: [],
},
})
);
await expect(
@ -142,7 +156,7 @@ describe('fetchSearchResults lib function', () => {
from: DEFAULT_FROM_VALUE,
index: indexName,
q: query,
size: ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
size: DEFAULT_DOCS_PER_PAGE,
});
});
});

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SearchHit } from '@elastic/elasticsearch/lib/api/types';
import { IScopedClusterClient } from '@kbn/core/server';
import { DEFAULT_DOCS_PER_PAGE, Paginate } from '../types';
import { escapeLuceneChars } from '../utils/escape_lucene_charts';
import { fetchWithPagination } from '../utils/fetch_with_pagination';
export const fetchSearchResults = async (
client: IScopedClusterClient,
indexName: string,
query?: string,
from: number = 0,
size: number = DEFAULT_DOCS_PER_PAGE
): Promise<Paginate<SearchHit>> => {
const result = await fetchWithPagination(
async () =>
await client.asCurrentUser.search({
from,
index: indexName,
size,
...(!!query ? { q: escapeLuceneChars(query) } : {}),
}),
from,
size
);
return {
...result,
data: result.data,
};
};

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './fetch_search_results';

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/search-index-documents",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,24 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/i18n",
"@kbn/i18n-react",
"@kbn/ui-theme",
"@kbn/core",
]
}

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Pagination } from '@elastic/eui';
export const DEFAULT_DOCS_PER_PAGE = 25;
export const INDEX_DOCUMENTS_META_DEFAULT: Pagination = {
pageIndex: 0,
pageSize: DEFAULT_DOCS_PER_PAGE,
totalItemCount: 0,
};
export * from './pagination';

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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export function pageToPagination(page: { from: number; size: number; total: number }) {
// Prevent divide-by-zero-error
const pageIndex = page.size ? Math.trunc(page.from / page.size) : 0;
return {
pageIndex,
pageSize: page.size,
totalItemCount: page.total,
};
}
interface Page {
from: number; // current page index, 0-based
has_more_hits_than_total?: boolean;
size: number; // size per page
total: number; // total number of hits
}
interface Meta {
page: Page;
}
export interface Paginate<T> {
_meta: Meta;
data: T[];
}

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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const escapeLuceneChars = (query: string) =>
query
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\?/g, '\\?')
.replace(/\+/g, '\\+')
.replace(/\*/g, '\\*')
.replace(/\|/g, '\\|')
.replace(/{/g, '\\{')
.replace(/}/g, '\\}')
.replace(/\[/g, '\\[')
.replace(/\]/g, '\\]')
.replace(/\(/g, '\\(')
.replace(/\)/g, '\\)')
.replace(/\./g, '\\.')
.replace(/\^/g, '\\^')
.replace(/\!/g, '\\!')
.replace(/\~/g, '\\~')
.replace(/\-/g, '\\-')
.replace(/\</g, '\\<')
.replace(/\//g, '\\/');

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SearchResponse } from '@elastic/elasticsearch/lib/api/types';

View file

@ -1,13 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SearchHit, SearchResponse, SearchTotalHits } from '@elastic/elasticsearch/lib/api/types';
import { Paginate } from '../../common/types/pagination';
import { Paginate } from '../types';
const defaultResult = <T>(data: T[]) => ({
_meta: {

View file

@ -1290,6 +1290,8 @@
"@kbn/search-connectors/*": ["packages/kbn-search-connectors/*"],
"@kbn/search-examples-plugin": ["examples/search_examples"],
"@kbn/search-examples-plugin/*": ["examples/search_examples/*"],
"@kbn/search-index-documents": ["packages/kbn-search-index-documents"],
"@kbn/search-index-documents/*": ["packages/kbn-search-index-documents/*"],
"@kbn/search-response-warnings": ["packages/kbn-search-response-warnings"],
"@kbn/search-response-warnings/*": ["packages/kbn-search-response-warnings/*"],
"@kbn/searchprofiler-plugin": ["x-pack/plugins/searchprofiler"],

View file

@ -13,12 +13,25 @@ import { searchDocuments } from './search_documents_api_logic';
describe('SearchDocumentsApiLogic', () => {
const { http } = mockHttpValues;
const results = {
_meta: {
page: {
from: 0,
has_more_hits_than_total: false,
size: 10,
total: 0,
},
},
data: [],
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('searchDocuments', () => {
it('calls correct api', async () => {
const promise = Promise.resolve('result');
const promise = Promise.resolve({
results,
});
http.post.mockReturnValue(promise);
const result = searchDocuments({
indexName: 'indexName',
@ -35,10 +48,17 @@ describe('SearchDocumentsApiLogic', () => {
query: { page: 0, size: 10 },
}
);
await expect(result).resolves.toEqual('result');
await expect(result).resolves.toEqual({
meta: {
pageIndex: 0,
pageSize: 10,
totalItemCount: 0,
},
results: [],
});
});
it('calls correct api with query set', async () => {
const promise = Promise.resolve('result');
const promise = Promise.resolve({ results });
http.post.mockReturnValue(promise);
const result = searchDocuments({
indexName: 'düsseldorf',
@ -58,10 +78,17 @@ describe('SearchDocumentsApiLogic', () => {
query: { page: 0, size: 10 },
}
);
await expect(result).resolves.toEqual('result');
await expect(result).resolves.toEqual({
meta: {
pageIndex: 0,
pageSize: 10,
totalItemCount: 0,
},
results: [],
});
});
it('calls with correct pageSize with docsPerPage set', async () => {
const promise = Promise.resolve('result');
const promise = Promise.resolve({ results });
http.post.mockReturnValue(promise);
const result = searchDocuments({
docsPerPage: 25,
@ -79,7 +106,14 @@ describe('SearchDocumentsApiLogic', () => {
query: { page: 0, size: 25 },
}
);
await expect(result).resolves.toEqual('result');
await expect(result).resolves.toEqual({
meta: {
pageIndex: 0,
pageSize: 10,
totalItemCount: 0,
},
results: [],
});
});
});
});

View file

@ -4,10 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SearchHit } from '@elastic/elasticsearch/lib/api/types';
import { Pagination } from '@elastic/eui';
import { pageToPagination } from '@kbn/search-index-documents';
import { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
import { Meta } from '../../../../../common/types';
import { Paginate } from '../../../../../common/types/pagination';
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';
@ -20,8 +21,8 @@ export interface SearchDocumentsApiLogicArgs {
}
export interface SearchDocumentsApiLogicValues {
meta: Meta;
results: SearchResponseBody;
meta: Pagination;
results: Paginate<SearchHit>;
}
export type SearchDocumentsApiLogicActions = Actions<
@ -42,12 +43,17 @@ export const searchDocuments = async ({
size: docsPerPage || pagination.pageSize,
};
return await HttpLogic.values.http.post<SearchDocumentsApiLogicValues>(route, {
const response = await HttpLogic.values.http.post<SearchDocumentsApiLogicValues>(route, {
body: JSON.stringify({
searchQuery,
}),
query,
});
return {
meta: pageToPagination(response.results?._meta?.page),
results: response.results?.data,
};
};
export const searchDocumentsApiLogic = (indexName: string) =>

View file

@ -1,94 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { setMockValues, setMockActions } from '../../../../../__mocks__/kea_logic';
import React from 'react';
import { shallow } from 'enzyme';
import { EuiCallOut, EuiPagination } from '@elastic/eui';
import { Result } from '../../../../../shared/result/result';
import { INDEX_DOCUMENTS_META_DEFAULT } from '../../documents';
import { DocumentList } from './document_list';
const mockActions = {};
export const DEFAULT_VALUES = {
docs: [],
docsPerPage: 25,
isLoading: true,
mappings: undefined,
meta: INDEX_DOCUMENTS_META_DEFAULT,
onPaginate: () => {},
setDocsPerPage: () => {},
};
const mockValues = { ...DEFAULT_VALUES };
describe('DocumentList', () => {
beforeEach(() => {
jest.clearAllMocks();
setMockValues(mockValues);
setMockActions(mockActions);
});
it('renders empty', () => {
const wrapper = shallow(<DocumentList {...DEFAULT_VALUES} />);
expect(wrapper.find(Result)).toHaveLength(0);
expect(wrapper.find(EuiPagination)).toHaveLength(2);
});
it('renders documents when results when there is data and mappings', () => {
const values = {
...DEFAULT_VALUES,
docs: [
{
_id: 'M9ntXoIBTq5dF-1Xnc8A',
_index: 'kibana_sample_data_flights',
_score: 1,
_source: {
AvgTicketPrice: 268.24159591388866,
},
},
{
_id: 'NNntXoIBTq5dF-1Xnc8A',
_index: 'kibana_sample_data_flights',
_score: 1,
_source: {
AvgTicketPrice: 68.91388866,
},
},
],
mappings: {
AvgTicketPrice: {
type: 'float' as const,
},
},
};
const wrapper = shallow(<DocumentList {...values} />);
expect(wrapper.find(Result)).toHaveLength(2);
});
it('renders callout when total results are 10.000', () => {
const values = {
...DEFAULT_VALUES,
...mockValues,
meta: {
page: {
...INDEX_DOCUMENTS_META_DEFAULT.page,
total_results: 10000,
},
},
};
const wrapper = shallow(<DocumentList {...values} />);
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
});
});

View file

@ -5,64 +5,47 @@
* 2.0.
*/
import React, { useEffect, useState, ChangeEvent } from 'react';
import { useEffect, useState } from 'react';
import React from 'react';
import { useActions, useValues } from 'kea';
import {
EuiCallOut,
EuiFieldSearch,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX } from '@kbn/search-connectors';
import {
CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX,
ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
} from '../../../../../common/constants';
DocumentList,
DocumentsOverview,
INDEX_DOCUMENTS_META_DEFAULT,
} from '@kbn/search-index-documents';
import { Status } from '../../../../../common/types/api';
import { DEFAULT_META } from '../../../shared/constants';
import { KibanaLogic } from '../../../shared/kibana';
import { mappingsWithPropsApiLogic } from '../../api/mappings/mappings_logic';
import { searchDocumentsApiLogic } from '../../api/search_documents/search_documents_api_logic';
import {
AccessControlIndexSelector,
AccessControlSelectorOption,
} from './components/access_control_index_selector/access_control_index_selector';
import { DocumentList } from './components/document_list/document_list';
import { IndexNameLogic } from './index_name_logic';
import { IndexViewLogic } from './index_view_logic';
import './documents.scss';
export const INDEX_DOCUMENTS_META_DEFAULT = {
page: {
current: 0,
size: ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
total_pages: 0,
total_results: 0,
},
};
export const DEFAULT_PAGINATION = {
pageIndex: INDEX_DOCUMENTS_META_DEFAULT.page.current,
pageSize: INDEX_DOCUMENTS_META_DEFAULT.page.size,
totalItemCount: INDEX_DOCUMENTS_META_DEFAULT.page.total_results,
const DEFAULT_PAGINATION = {
pageIndex: INDEX_DOCUMENTS_META_DEFAULT.pageIndex,
pageSize: INDEX_DOCUMENTS_META_DEFAULT.pageSize,
totalItemCount: INDEX_DOCUMENTS_META_DEFAULT.totalItemCount,
};
export const SearchIndexDocuments: React.FC = () => {
const { indexName } = useValues(IndexNameLogic);
const { ingestionMethod, hasDocumentLevelSecurityFeature } = useValues(IndexViewLogic);
const { productFeatures } = useValues(KibanaLogic);
const [selectedIndexType, setSelectedIndexType] =
useState<AccessControlSelectorOption['value']>('content-index');
const indexToShow =
@ -71,26 +54,31 @@ export const SearchIndexDocuments: React.FC = () => {
: `${CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX}${indexName}`;
const mappingLogic = mappingsWithPropsApiLogic(indexToShow);
const documentLogic = searchDocumentsApiLogic(indexToShow);
const { makeRequest: getDocuments } = useActions(documentLogic);
const { makeRequest: getMappings } = useActions(mappingLogic);
const { data, status, error } = useValues(documentLogic);
const { data: mappingData, status: mappingStatus } = useValues(mappingLogic);
const docs = data?.results?.hits.hits ?? [];
const docs = data?.results ?? [];
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
const [searchQuery, setSearchQuery] = useState('');
const searchQueryCallback = (searchQ: string) => {
setSearchQuery(searchQ);
};
const shouldShowAccessControlSwitcher =
hasDocumentLevelSecurityFeature && productFeatures.hasDocumentLevelSecurityEnabled;
const isAccessControlIndexNotFound =
shouldShowAccessControlSwitcher && error?.body?.statusCode === 404;
useEffect(() => {
getDocuments({
indexName: indexToShow,
pagination,
pagination: {
...pagination,
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize ?? 10,
},
query: searchQuery,
});
}, [indexToShow, pagination, searchQuery]);
@ -100,49 +88,12 @@ export const SearchIndexDocuments: React.FC = () => {
setPagination(DEFAULT_PAGINATION);
getMappings({ indexName: indexToShow });
}, [indexToShow]);
return (
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
<EuiSpacer />
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFlexGroup direction="row" alignItems="center">
<EuiFlexItem className="enterpriseSearchDocumentsHeader" grow={false}>
<EuiTitle>
<h2>
{i18n.translate('xpack.enterpriseSearch.content.searchIndex.documents.title', {
defaultMessage: 'Browse documents',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
{shouldShowAccessControlSwitcher && (
<EuiFlexItem grow={false}>
<AccessControlIndexSelector
onChange={setSelectedIndexType}
valueOfSelected={selectedIndexType}
/>
</EuiFlexItem>
)}
<EuiFlexItem>
<EuiFieldSearch
data-telemetry-id={`entSearchContent-${ingestionMethod}-documents-searchDocuments`}
placeholder={i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.documents.searchField.placeholder',
{
defaultMessage: 'Search documents in this index',
}
)}
isClearable
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setSearchQuery(event.target.value)
}
fullWidth
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<DocumentsOverview
dataTelemetryIdPrefix={`entSearchContent-${ingestionMethod}`}
searchQueryCallback={searchQueryCallback}
documentComponent={
<>
{isAccessControlIndexNotFound && (
<EuiCallOut
size="m"
@ -167,17 +118,26 @@ export const SearchIndexDocuments: React.FC = () => {
})}
{!isAccessControlIndexNotFound && docs.length > 0 && (
<DocumentList
dataTelemetryIdPrefix={`entSearchContent-${ingestionMethod}`}
docs={docs}
docsPerPage={pagination.pageSize}
docsPerPage={pagination.pageSize ?? 10}
isLoading={status !== Status.SUCCESS && mappingStatus !== Status.SUCCESS}
mappings={mappingData?.mappings?.properties ?? {}}
meta={data?.meta ?? DEFAULT_META}
meta={data?.meta ?? DEFAULT_PAGINATION}
onPaginate={(pageIndex) => setPagination({ ...pagination, pageIndex })}
setDocsPerPage={(pageSize) => setPagination({ ...pagination, pageSize })}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</>
}
accessControlSwitch={
shouldShowAccessControlSwitcher ? (
<AccessControlIndexSelector
onChange={setSelectedIndexType}
valueOfSelected={selectedIndexType}
/>
) : undefined
}
/>
);
};

View file

@ -1,28 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
import { IScopedClusterClient } from '@kbn/core/server';
import { ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } from '../../common/constants';
import { escapeLuceneChars } from '../utils/escape_lucene_chars';
export const fetchSearchResults = async (
client: IScopedClusterClient,
indexName: string,
query?: string,
from: number = 0,
size: number = ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT
): Promise<SearchResponseBody> => {
const results = await client.asCurrentUser.search({
from,
index: indexName,
size,
...(!!query ? { q: escapeLuceneChars(query) } : {}),
});
return results;
};

View file

@ -9,13 +9,14 @@ import { MockRouter, mockDependencies } from '../../__mocks__';
import { RequestHandlerContext } from '@kbn/core/server';
jest.mock('../../lib/fetch_search_results', () => ({
fetchSearchResults: jest.fn(),
}));
import { fetchSearchResults } from '../../lib/fetch_search_results';
import { fetchSearchResults } from '@kbn/search-index-documents/lib';
import { registerSearchRoute } from './search';
jest.mock('@kbn/search-index-documents/lib', () => ({
fetchSearchResults: jest.fn(),
}));
describe('Elasticsearch Search', () => {
let mockRouter: MockRouter;
const mockClient = {};
@ -45,22 +46,22 @@ describe('Elasticsearch Search', () => {
it('returns search results for a query', async () => {
const mockData = {
_shards: { failed: 0, skipped: 0, successful: 2, total: 2 },
hits: {
hits: [
{
_id: '5a12292a0f5ae10021650d7e',
_index: 'search-regular-index',
_score: 4.437291,
_source: { id: '5a12292a0f5ae10021650d7e', name: 'banana' },
},
],
max_score: null,
total: { relation: 'eq', value: 1 },
_meta: {
page: {
from: 0,
has_more_hits_than_total: false,
size: 25,
total: 26,
},
},
timed_out: false,
took: 4,
data: [
{
_id: '5a12292a0f5ae10021650d7e',
_index: 'search-regular-index',
_score: 4.437291,
_source: { id: '5a12292a0f5ae10021650d7e', name: 'banana' },
},
],
};
(fetchSearchResults as jest.Mock).mockImplementationOnce(() => {
@ -74,24 +75,8 @@ describe('Elasticsearch Search', () => {
params: { index_name: 'search-index-name' },
});
expect(fetchSearchResults).toHaveBeenCalledWith(
mockClient,
'search-index-name',
'banana',
0,
25
);
expect(mockRouter.response.ok).toHaveBeenCalledWith({
body: {
meta: {
page: {
current: 0,
size: 1,
total_pages: 1,
total_results: 1,
},
},
results: mockData,
},
@ -125,22 +110,22 @@ describe('Elasticsearch Search', () => {
it('searches returns first 25 search results by default', async () => {
const mockData = {
_shards: { failed: 0, skipped: 0, successful: 2, total: 2 },
hits: {
hits: [
{
_id: '5a12292a0f5ae10021650d7e',
_index: 'search-regular-index',
_score: 4.437291,
_source: { id: '5a12292a0f5ae10021650d7e', name: 'banana' },
},
],
max_score: null,
total: { relation: 'eq', value: 1 },
_meta: {
page: {
from: 0,
has_more_hits_than_total: false,
size: 25,
total: 26,
},
},
timed_out: false,
took: 4,
data: [
{
_id: '5a12292a0f5ae10021650d7e',
_index: 'search-regular-index',
_score: 4.437291,
_source: { id: '5a12292a0f5ae10021650d7e', name: 'banana' },
},
],
};
(fetchSearchResults as jest.Mock).mockImplementationOnce(() => {
@ -161,14 +146,6 @@ describe('Elasticsearch Search', () => {
expect(mockRouterNoQuery.response.ok).toHaveBeenCalledWith({
body: {
meta: {
page: {
current: 0,
size: 1,
total_pages: 1,
total_results: 1,
},
},
results: mockData,
},

View file

@ -5,40 +5,19 @@
* 2.0.
*/
import { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
import { schema } from '@kbn/config-schema';
import { fetchSearchResults } from '@kbn/search-index-documents/lib';
import { ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } from '../../../common/constants';
import { ErrorCode } from '../../../common/types/error_codes';
import { fetchSearchResults } from '../../lib/fetch_search_results';
import { RouteDependencies } from '../../plugin';
import { createError } from '../../utils/create_error';
import { elasticsearchErrorHandler } from '../../utils/elasticsearch_error_handler';
import { isIndexNotFoundException } from '../../utils/identify_exceptions';
const calculateMeta = (searchResults: SearchResponseBody, page: number, size: number) => {
let totalResults = 0;
if (searchResults.hits.total === null || searchResults.hits.total === undefined) {
totalResults = 0;
} else if (typeof searchResults.hits.total === 'number') {
totalResults = searchResults.hits.total;
} else {
totalResults = searchResults.hits.total.value;
}
const totalPages = Math.ceil(totalResults / size) || 1;
return {
page: {
current: page,
size: searchResults.hits.hits.length,
total_pages: (Number.isFinite(totalPages) && totalPages) || 1,
total_results: totalResults,
},
};
};
export function registerSearchRoute({ router, log }: RouteDependencies) {
router.post(
{
@ -68,17 +47,10 @@ export function registerSearchRoute({ router, log }: RouteDependencies) {
const { page = 0, size = ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } = request.query;
const from = page * size;
try {
const searchResults: SearchResponseBody = await fetchSearchResults(
client,
indexName,
searchQuery,
from,
size
);
const searchResults = await fetchSearchResults(client, indexName, searchQuery, from, size);
return response.ok({
body: {
meta: calculateMeta(searchResults, page, size),
results: searchResults,
},
headers: { 'content-type': 'application/json' },

View file

@ -59,10 +59,12 @@
"@kbn/global-search-plugin",
"@kbn/share-plugin",
"@kbn/search-api-panels",
"@kbn/search-index-documents",
"@kbn/search-connectors",
"@kbn/logs-shared-plugin",
"@kbn/core-http-browser-mocks",
"@kbn/core-application-browser",
"@kbn/core-capabilities-common"
"@kbn/core-capabilities-common",
]
}

View file

@ -13140,11 +13140,6 @@
"xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.nameInputHelpText.lineOne": "Votre index sera nommé : {indexName}",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.description": "Un index supprimé appelé {indexName} était, à l'origine, lié à une configuration de connecteur. Voulez-vous remplacer cette configuration de connecteur par la nouvelle ?",
"xpack.enterpriseSearch.content.searchIndex.cannotConnect.body": "Le robot d'indexation Elastic requiert Enterprise Search. {link}",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.description": "Affichage de {results} sur {total}. Nombre maximal de résultats de recherche de {maximum} documents.",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.pagination.itemsPerPage": "Documents par page : {docPerPage}",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationOptions.option": "{docCount} documents",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.resultLimit": "Seuls les {number} premiers résultats sont disponibles pour la pagination. Veuillez utiliser la barre de recherche pour filtrer vos résultats.",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.resultLimitTitle": "Les résultats sont limités à {number} documents",
"xpack.enterpriseSearch.content.searchIndex.mappings.description": "Vos documents sont constitués d'un ensemble de champs. Les mappings d'index donnent à chaque champ un type (tel que {keyword}, {number} ou {date}) et des champs secondaires supplémentaires. Ces mappings d'index déterminent les fonctions disponibles dans votre réglage de pertinence et votre expérience de recherche.",
"xpack.enterpriseSearch.content.searchIndex.nativeCloudCallout.content": "Convertissez-les en {link} afin quils soient autogérés sur votre propre infrastructure. Les connecteurs natifs sont disponibles uniquement dans votre déploiement Elastic Cloud.",
"xpack.enterpriseSearch.content.searchIndices.actions.deleteIndex.caption": "Supprimer l'index {indexName}",
@ -13154,7 +13149,6 @@
"xpack.enterpriseSearch.content.searchIndices.deleteModal.title": "Voulez-vous vraiment supprimer {indexName} ?",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.onlySearchOptimized.tooltipContent": "Les index optimisés pour la recherche sont précédés du préfixe {code}. Ils sont gérés par des mécanismes d'ingestion tels que des robots d'indexation, des connecteurs ou des API d'ingestion.",
"xpack.enterpriseSearch.content.settings.description": "Ces paramètres s'appliquent à tous les nouveaux index Elasticsearch créés par des mécanismes d'ingestion Search. Pour les index basés sur l'ingestion d'API, n'oubliez pas d'inclure le pipeline lorsque vous ingérez des documents. Ces fonctionnalités sont alimentées par {link}.",
"xpack.enterpriseSearch.content.shared.result.header.metadata.icon.ariaLabel": "Métadonnées pour le document : {id}",
"xpack.enterpriseSearch.crawler.action.deleteDomain.confirmationPopupMessage": "Voulez-vous vraiment supprimer le domaine \"{domainUrl}\" et tous ses paramètres ?",
"xpack.enterpriseSearch.crawler.addDomainForm.entryPointLabel": "Le point d'entrée du robot d'indexation a été défini sur {entryPointValue}",
"xpack.enterpriseSearch.crawler.authenticationPanel.emptyPrompt.description": "Cliquer sur {addAuthenticationButtonLabel} afin de fournir les informations d'identification nécessaires pour indexer le contenu protégé",
@ -13210,9 +13204,6 @@
"xpack.enterpriseSearch.setupGuide.cloud.step6.instruction1": "Pour obtenir de l'aide sur les problèmes courants de configuration, lisez notre guide {link}.",
"xpack.enterpriseSearch.setupGuide.step1.instruction1": "Dans votre fichier {configFile}, définissez {configSetting} sur l'URL de votre instance {productName}. Par exemple :",
"xpack.enterpriseSearch.setupGuide.step1.title": "Ajouter votre URL hôte {productName} à votre configuration Kibana",
"xpack.enterpriseSearch.shared.result.expandTooltip.showFewer": "Afficher {amount} champs en moins",
"xpack.enterpriseSearch.shared.result.expandTooltip.showMore": "Afficher {amount} champs en plus",
"xpack.enterpriseSearch.shared.result.title.id": "ID de document : {id}",
"xpack.enterpriseSearch.trialCalloutTitle": "Votre licence d'essai de la Suite Elastic, qui vous permet d'utiliser les fonctionnalités Platinum, expire dans {days, plural, one {# jour} many {# jours} other {# jours}}.",
"xpack.enterpriseSearch.troubleshooting.differentEsClusters.description": "Ce plug-in ne prend actuellement pas en charge l'exécution de {productName} et de Kibana sur des clusters différents.",
"xpack.enterpriseSearch.troubleshooting.differentEsClusters.title": "{productName} et Kibana se trouvent sur des clusters Elasticsearch différents",
@ -14805,17 +14796,13 @@
"xpack.enterpriseSearch.content.searchIndex.configurationTabLabel": "Configuration",
"xpack.enterpriseSearch.content.searchIndex.connectorErrorCallOut.title": "Votre connecteur a rapporté une erreur",
"xpack.enterpriseSearch.content.searchIndex.crawlerConfigurationTabLabel": "Configuration",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.docsPerPage": "Nombre de documents par page déroulée",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationAriaLabel": "Pagination pour la liste de documents",
"xpack.enterpriseSearch.content.searchIndex.documents.noIndex": "Aucun index de contrôle d'accès ne sera créé tant que vous n'activez pas la sécurité au niveau du document et que vous n'effectuez pas la première synchronisation de contrôle d'accès.",
"xpack.enterpriseSearch.content.searchIndex.documents.noIndex.title": "Index de contrôle d'accès introuvable",
"xpack.enterpriseSearch.content.searchIndex.documents.noMappings": "Aucun document trouvé pour l'index",
"xpack.enterpriseSearch.content.searchIndex.documents.searchField.placeholder": "Rechercher des documents dans cet index",
"xpack.enterpriseSearch.content.searchIndex.documents.selector.accessControl.description": "Parcourir les champs de sécurité au niveau du document",
"xpack.enterpriseSearch.content.searchIndex.documents.selector.accessControl.title": "Index de contrôle d'accès",
"xpack.enterpriseSearch.content.searchIndex.documents.selector.contentIndex.description": "Parcourir les champs de contenu",
"xpack.enterpriseSearch.content.searchIndex.documents.selector.contentIndex.title": "Index de contenu",
"xpack.enterpriseSearch.content.searchIndex.documents.title": "Parcourir des documents",
"xpack.enterpriseSearch.content.searchIndex.documentsTabLabel": "Documents",
"xpack.enterpriseSearch.content.searchIndex.domainManagementTabLabel": "Gérer les domaines",
"xpack.enterpriseSearch.content.searchIndex.index.accessControlSyncSuccess.message": "Une synchronisation de contrôle daccès a été programmée avec succès, en attente de son activation par un connecteur",
@ -14908,8 +14895,6 @@
"xpack.enterpriseSearch.content.settings.whitespaceReduction.deploymentHeaderTitle": "Réduction d'espaces sur l'ensemble du déploiement",
"xpack.enterpriseSearch.content.settings.whiteSpaceReduction.description": "La réduction d'espaces supprimera le contenu de texte intégral des espaces par défaut.",
"xpack.enterpriseSearch.content.settings.whitespaceReduction.label": "Réduction d'espaces",
"xpack.enterpriseSearch.content.shared.result.header.metadata.deleteDocument": "Supprimer le document",
"xpack.enterpriseSearch.content.shared.result.header.metadata.title": "Métadonnées du document",
"xpack.enterpriseSearch.content.sources.basicRulesTable.includeEverythingMessage": "Inclure tout le reste à partir de cette source",
"xpack.enterpriseSearch.content.supportedLanguages.chineseLabel": "Chinois",
"xpack.enterpriseSearch.content.supportedLanguages.danishLabel": "Danois",
@ -15704,7 +15689,6 @@
"xpack.enterpriseSearch.setupGuide.step3.title": "Résolution des problèmes",
"xpack.enterpriseSearch.setupGuide.title": "Guide de configuration",
"xpack.enterpriseSearch.shared.flashMessages.defaultErrorMessage": "Une erreur inattendue s'est produite",
"xpack.enterpriseSearch.shared.result.expandTooltip.allVisible": "Tous les champs sont visibles",
"xpack.enterpriseSearch.shared.unsavedChangesMessage": "Vos modifications n'ont pas été enregistrées. Voulez-vous vraiment quitter ?",
"xpack.enterpriseSearch.trialCalloutLink": "Découvrez plus d'informations sur les licences de la Suite Elastic.",
"xpack.enterpriseSearch.troubleshooting.setup.documentationLinkLabel": "Dépannage de la configuration d'Enterprise Search",

View file

@ -13153,11 +13153,6 @@
"xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.nameInputHelpText.lineOne": "インデックスは次の名前になります:{indexName}",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.description": "削除されたインデックス{indexName}は、既存のコネクター構成に関連付けられていました。既存のコネクター構成を新しいコネクター構成で置き換えますか?",
"xpack.enterpriseSearch.content.searchIndex.cannotConnect.body": "Elastic Webクローラーにはエンタープライズ サーチが必要です。{link}",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.description": "{total}件中{results}件を表示中。{maximum}ドキュメントが検索結果の最大数です。",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.pagination.itemsPerPage": "毎秒あたりのドキュメント:{docPerPage}",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationOptions.option": "{docCount}ドキュメント",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.resultLimit": "最初の{number}件の結果のみがページ制御できます。結果を絞り込むには、検索バーを使用してください。",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.resultLimitTitle": "結果は{number}ドキュメントに制限されています",
"xpack.enterpriseSearch.content.searchIndex.mappings.description": "ドキュメントには、複数のフィールドのセットがあります。インデックスマッピングでは、各フィールドに型({keyword}、{number}、{date})と、追加のサブフィールドが指定されます。これらのインデックスマッピングでは、関連性の調整と検索エクスペリエンスで使用可能な機能が決まります。",
"xpack.enterpriseSearch.content.searchIndex.nativeCloudCallout.content": "独自のインフラでセルフマネージドされる{link}に変換します。ネイティブコネクターはElastic Cloudデプロイでのみ使用できます。",
"xpack.enterpriseSearch.content.searchIndices.actions.deleteIndex.caption": "インデックス{indexName}の削除",
@ -13167,7 +13162,6 @@
"xpack.enterpriseSearch.content.searchIndices.deleteModal.title": "{indexName}を削除しますか?",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.onlySearchOptimized.tooltipContent": "検索用に最適化されたインデックスには、{code}がプレフィックスとして付けられます。これらは、クローラー、コネクター、インジェストAPIなどのインジェストメカニズムによって管理されます。",
"xpack.enterpriseSearch.content.settings.description": "これらの設定は、Searchインジェストメカニズムで作成されたすべての新しいElasticsearchインデックスに適用されます。APIインジェストベースのインデックスの場合は、ドキュメントをインジェストするときに、必ずパイプラインを含めてください。これらの機能は{link}によって実現されます。",
"xpack.enterpriseSearch.content.shared.result.header.metadata.icon.ariaLabel": "ドキュメント{id}のメタデータ",
"xpack.enterpriseSearch.crawler.action.deleteDomain.confirmationPopupMessage": "ドメイン\"{domainUrl}\"とすべての設定を削除しますか?",
"xpack.enterpriseSearch.crawler.addDomainForm.entryPointLabel": "Webクローラーエントリポイントが{entryPointValue}として設定されました",
"xpack.enterpriseSearch.crawler.authenticationPanel.emptyPrompt.description": "{addAuthenticationButtonLabel}をクリックすると、保護されたコンテンツのクローリングに必要な資格情報を提供します",
@ -13223,9 +13217,6 @@
"xpack.enterpriseSearch.setupGuide.cloud.step6.instruction1": "一般的なセットアップの問題のヘルプについては、{link}ガイドをお読みください。",
"xpack.enterpriseSearch.setupGuide.step1.instruction1": "{configFile}ファイルで、{configSetting}に{productName}インスタンスのURLを設定します。例",
"xpack.enterpriseSearch.setupGuide.step1.title": "{productName}ホストURLをKibana構成に追加",
"xpack.enterpriseSearch.shared.result.expandTooltip.showFewer": "表示する行数を{amount}減らす",
"xpack.enterpriseSearch.shared.result.expandTooltip.showMore": "表示する行数を{amount}増やす",
"xpack.enterpriseSearch.shared.result.title.id": "ドキュメントID{id}",
"xpack.enterpriseSearch.trialCalloutTitle": "Platinum機能を有効にするElastic Stack試用版ライセンスは{days, plural, other {#日}}後に期限切れになります。",
"xpack.enterpriseSearch.troubleshooting.differentEsClusters.description": "このプラグインは現在、異なるクラスターで実行されている{productName}とKibanaをサポートしていません。",
"xpack.enterpriseSearch.troubleshooting.differentEsClusters.title": "{productName}とKibanaは別のElasticsearchクラスターにあります",
@ -14818,17 +14809,13 @@
"xpack.enterpriseSearch.content.searchIndex.configurationTabLabel": "構成",
"xpack.enterpriseSearch.content.searchIndex.connectorErrorCallOut.title": "コネクターでエラーが発生しました",
"xpack.enterpriseSearch.content.searchIndex.crawlerConfigurationTabLabel": "構成",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.docsPerPage": "ページドロップダウンごとのドキュメント数",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationAriaLabel": "ドキュメントリストのページ制御",
"xpack.enterpriseSearch.content.searchIndex.documents.noIndex": "ドキュメントレベルのセキュリティを有効にし、最初のアクセス制御同期を実行するまで、アクセス制御インデックスは作成されません。",
"xpack.enterpriseSearch.content.searchIndex.documents.noIndex.title": "アクセス制御インデックスが見つかりません",
"xpack.enterpriseSearch.content.searchIndex.documents.noMappings": "インデックスドキュメントが見つかりません",
"xpack.enterpriseSearch.content.searchIndex.documents.searchField.placeholder": "このインデックスでドキュメントを検索",
"xpack.enterpriseSearch.content.searchIndex.documents.selector.accessControl.description": "ドキュメントレベルのセキュリティフィールドを参照",
"xpack.enterpriseSearch.content.searchIndex.documents.selector.accessControl.title": "アクセス制御インデックス",
"xpack.enterpriseSearch.content.searchIndex.documents.selector.contentIndex.description": "コンテンツフィールドを参照",
"xpack.enterpriseSearch.content.searchIndex.documents.selector.contentIndex.title": "コンテンツインデックス",
"xpack.enterpriseSearch.content.searchIndex.documents.title": "ドキュメントを参照",
"xpack.enterpriseSearch.content.searchIndex.documentsTabLabel": "ドキュメント",
"xpack.enterpriseSearch.content.searchIndex.domainManagementTabLabel": "ドメインを管理",
"xpack.enterpriseSearch.content.searchIndex.index.accessControlSyncSuccess.message": "アクセス制御同期が正常にスケジュールされました。コネクターによって取得されるのを待機しています",
@ -14921,8 +14908,6 @@
"xpack.enterpriseSearch.content.settings.whitespaceReduction.deploymentHeaderTitle": "デプロイレベルの空白削除",
"xpack.enterpriseSearch.content.settings.whiteSpaceReduction.description": "空白削除では、デフォルトで全文コンテンツから空白を削除します。",
"xpack.enterpriseSearch.content.settings.whitespaceReduction.label": "空白削除",
"xpack.enterpriseSearch.content.shared.result.header.metadata.deleteDocument": "ドキュメントを削除",
"xpack.enterpriseSearch.content.shared.result.header.metadata.title": "ドキュメントメタデータ",
"xpack.enterpriseSearch.content.sources.basicRulesTable.includeEverythingMessage": "このソースの他のすべての項目を含める",
"xpack.enterpriseSearch.content.supportedLanguages.chineseLabel": "中国語",
"xpack.enterpriseSearch.content.supportedLanguages.danishLabel": "デンマーク語",
@ -15717,7 +15702,6 @@
"xpack.enterpriseSearch.setupGuide.step3.title": "トラブルシューティングのヒント",
"xpack.enterpriseSearch.setupGuide.title": "セットアップガイド",
"xpack.enterpriseSearch.shared.flashMessages.defaultErrorMessage": "予期しないエラーが発生しました",
"xpack.enterpriseSearch.shared.result.expandTooltip.allVisible": "すべてのフィールドが表示されます",
"xpack.enterpriseSearch.shared.unsavedChangesMessage": "変更は保存されていません。終了してよろしいですか?",
"xpack.enterpriseSearch.trialCalloutLink": "Elastic Stackライセンスの詳細を参照してください。",
"xpack.enterpriseSearch.troubleshooting.setup.documentationLinkLabel": "エンタープライズ サーチ設定のトラブルシューティング",

View file

@ -13153,11 +13153,6 @@
"xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.nameInputHelpText.lineOne": "您的索引将命名为:{indexName}",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.description": "名为 {indexName} 的已删除索引最初绑定到现有连接器配置。是否要将现有连接器配置替换成新的?",
"xpack.enterpriseSearch.content.searchIndex.cannotConnect.body": "Elastic 网络爬虫需要 Enterprise Search。{link}",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.description": "显示 {results} 个,共 {total} 个。搜索结果最多包含 {maximum} 个文档。",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.pagination.itemsPerPage": "每页文档数:{docPerPage}",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationOptions.option": "{docCount} 个文档",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.resultLimit": "仅前 {number} 个结果可用于分页。请使用搜索栏筛选结果。",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.resultLimitTitle": "结果被限定为 {number} 个文档",
"xpack.enterpriseSearch.content.searchIndex.mappings.description": "您的文档由一组字段构成。索引映射为每个字段提供类型(例如,{keyword}、{number}或{date})和其他子字段。这些索引映射确定您的相关性调整和搜索体验中可用的功能。",
"xpack.enterpriseSearch.content.searchIndex.nativeCloudCallout.content": "将其转换为将在您自己的基础设施上进行自我管理的 {link}。本机连接器只可用于您的 Elastic Cloud 部署。",
"xpack.enterpriseSearch.content.searchIndices.actions.deleteIndex.caption": "删除索引 {indexName}",
@ -13167,7 +13162,6 @@
"xpack.enterpriseSearch.content.searchIndices.deleteModal.title": "是否确定要删除 {indexName}",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.onlySearchOptimized.tooltipContent": "搜索优化索引以 {code} 为前缀。它们由网络爬虫、连接器或采集 API 等采集机制进行管理。",
"xpack.enterpriseSearch.content.settings.description": "这些设置适用于由 Search 采集机制创建的所有新 Elasticsearch 索引。对于基于 API 采集的索引,在采集文档时请记得包括管道。这些功能由 {link} 提供支持。",
"xpack.enterpriseSearch.content.shared.result.header.metadata.icon.ariaLabel": "以下文档的元数据:{id}",
"xpack.enterpriseSearch.crawler.action.deleteDomain.confirmationPopupMessage": "确定要移除域“{domainUrl}”及其所有设置?",
"xpack.enterpriseSearch.crawler.addDomainForm.entryPointLabel": "网络爬虫入口点已设置为 {entryPointValue}",
"xpack.enterpriseSearch.crawler.authenticationPanel.emptyPrompt.description": "单击 {addAuthenticationButtonLabel} 以提供爬网受保护内容所需的凭据",
@ -13223,9 +13217,6 @@
"xpack.enterpriseSearch.setupGuide.cloud.step6.instruction1": "如需帮助解决常见的设置问题,请阅读我们的 {link}指南。",
"xpack.enterpriseSearch.setupGuide.step1.instruction1": "在 {configFile} 文件中,将 {configSetting} 设置为 {productName} 实例的 URL。例如",
"xpack.enterpriseSearch.setupGuide.step1.title": "将 {productName} 主机 URL 添加到 Kibana 配置",
"xpack.enterpriseSearch.shared.result.expandTooltip.showFewer": "显示少于 {amount} 个字段",
"xpack.enterpriseSearch.shared.result.expandTooltip.showMore": "显示多于 {amount} 个字段",
"xpack.enterpriseSearch.shared.result.title.id": "文档 ID{id}",
"xpack.enterpriseSearch.trialCalloutTitle": "您的可启用白金级功能的 Elastic Stack 试用版许可证将于 {days, plural, other {# 天}}后过期。",
"xpack.enterpriseSearch.troubleshooting.differentEsClusters.description": "此插件当前不支持在不同集群中运行的 {productName} 和 Kibana。",
"xpack.enterpriseSearch.troubleshooting.differentEsClusters.title": "{productName} 和 Kibana 在不同的 Elasticsearch 集群中",
@ -14818,17 +14809,13 @@
"xpack.enterpriseSearch.content.searchIndex.configurationTabLabel": "配置",
"xpack.enterpriseSearch.content.searchIndex.connectorErrorCallOut.title": "您的连接器报告了错误",
"xpack.enterpriseSearch.content.searchIndex.crawlerConfigurationTabLabel": "配置",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.docsPerPage": "每页文档计数下拉列表",
"xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationAriaLabel": "文档列表分页",
"xpack.enterpriseSearch.content.searchIndex.documents.noIndex": "在您启用文档级别安全性并运行首次访问控制同步之前,不会创建访问控制索引。",
"xpack.enterpriseSearch.content.searchIndex.documents.noIndex.title": "找不到访问控制索引",
"xpack.enterpriseSearch.content.searchIndex.documents.noMappings": "找不到索引的文档",
"xpack.enterpriseSearch.content.searchIndex.documents.searchField.placeholder": "在此索引中搜索文档",
"xpack.enterpriseSearch.content.searchIndex.documents.selector.accessControl.description": "浏览文档级别安全性字段",
"xpack.enterpriseSearch.content.searchIndex.documents.selector.accessControl.title": "访问控制索引",
"xpack.enterpriseSearch.content.searchIndex.documents.selector.contentIndex.description": "浏览内容字段",
"xpack.enterpriseSearch.content.searchIndex.documents.selector.contentIndex.title": "内容索引",
"xpack.enterpriseSearch.content.searchIndex.documents.title": "浏览文档",
"xpack.enterpriseSearch.content.searchIndex.documentsTabLabel": "文档",
"xpack.enterpriseSearch.content.searchIndex.domainManagementTabLabel": "管理域",
"xpack.enterpriseSearch.content.searchIndex.index.accessControlSyncSuccess.message": "已成功计划访问控制同步,等待连接器提取",
@ -14921,8 +14908,6 @@
"xpack.enterpriseSearch.content.settings.whitespaceReduction.deploymentHeaderTitle": "部署广泛的空白缩减",
"xpack.enterpriseSearch.content.settings.whiteSpaceReduction.description": "默认情况下,空白缩减将清除空白的全文本内容。",
"xpack.enterpriseSearch.content.settings.whitespaceReduction.label": "空白缩减",
"xpack.enterpriseSearch.content.shared.result.header.metadata.deleteDocument": "删除文档",
"xpack.enterpriseSearch.content.shared.result.header.metadata.title": "文档元数据",
"xpack.enterpriseSearch.content.sources.basicRulesTable.includeEverythingMessage": "包括来自此源的所有其他内容",
"xpack.enterpriseSearch.content.supportedLanguages.chineseLabel": "中文",
"xpack.enterpriseSearch.content.supportedLanguages.danishLabel": "丹麦语",
@ -15717,7 +15702,6 @@
"xpack.enterpriseSearch.setupGuide.step3.title": "解决问题",
"xpack.enterpriseSearch.setupGuide.title": "设置指南",
"xpack.enterpriseSearch.shared.flashMessages.defaultErrorMessage": "发生意外错误",
"xpack.enterpriseSearch.shared.result.expandTooltip.allVisible": "所有字段均可见",
"xpack.enterpriseSearch.shared.unsavedChangesMessage": "您的更改尚未更改。是否确定要离开?",
"xpack.enterpriseSearch.trialCalloutLink": "详细了解 Elastic Stack 许可证。",
"xpack.enterpriseSearch.troubleshooting.setup.documentationLinkLabel": "Enterprise Search 设置故障排除",

View file

@ -5515,6 +5515,10 @@
version "0.0.0"
uid ""
"@kbn/search-index-documents@link:packages/kbn-search-index-documents":
version "0.0.0"
uid ""
"@kbn/search-response-warnings@link:packages/kbn-search-response-warnings":
version "0.0.0"
uid ""