mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Search] [Onboarding] With Data View (#193121)
## Summary Adds the document view tab when the index has documents  ### Checklist Delete any items that are not applicable to this PR. - [ ] 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) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a90d1f64fa
commit
2d78f23dde
10 changed files with 274 additions and 38 deletions
|
@ -30,7 +30,7 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react';
|
||||
|
||||
import { resultMetaData } from './result/result_metadata';
|
||||
import { resultMetaData, resultToField } from './result/result_metadata';
|
||||
|
||||
import { Result } from '..';
|
||||
interface DocumentListProps {
|
||||
|
@ -55,20 +55,6 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||
setDocsPerPage,
|
||||
}) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const resultToField = (result: SearchHit) => {
|
||||
if (mappings && result._source && !Array.isArray(result._source)) {
|
||||
if (typeof result._source === 'object') {
|
||||
return Object.entries(result._source).map(([key, value]) => {
|
||||
return {
|
||||
fieldName: key,
|
||||
fieldType: mappings[key]?.type ?? 'object',
|
||||
fieldValue: JSON.stringify(value, null, 2),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const getIconType = (size: number) => {
|
||||
return size === docsPerPage ? 'check' : 'empty';
|
||||
|
@ -113,7 +99,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||
{docs.map((doc) => {
|
||||
return (
|
||||
<React.Fragment key={doc._id}>
|
||||
<Result fields={resultToField(doc)} metaData={resultMetaData(doc)} />
|
||||
<Result fields={resultToField(doc, mappings)} metaData={resultMetaData(doc)} />
|
||||
<EuiSpacer size="s" />
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
@ -8,3 +8,4 @@
|
|||
*/
|
||||
|
||||
export { Result } from './result';
|
||||
export { resultMetaData, resultToField } from './result_metadata';
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { SearchHit } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { MetaDataProps } from './result_types';
|
||||
import type { MappingProperty, SearchHit } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { MetaDataProps } from './result_types';
|
||||
|
||||
const TITLE_KEYS = ['title', 'name'];
|
||||
|
||||
|
@ -38,3 +38,18 @@ export const resultMetaData = (result: SearchHit): MetaDataProps => ({
|
|||
id: result._id!,
|
||||
title: resultTitle(result),
|
||||
});
|
||||
|
||||
export const resultToField = (result: SearchHit, mappings?: Record<string, MappingProperty>) => {
|
||||
if (mappings && result._source && !Array.isArray(result._source)) {
|
||||
if (typeof result._source === 'object') {
|
||||
return Object.entries(result._source).map(([key, value]) => {
|
||||
return {
|
||||
fieldName: key,
|
||||
fieldType: mappings[key]?.type ?? 'object',
|
||||
fieldValue: JSON.stringify(value, null, 2),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
|
||||
export const AddDocumentsCodeExample: React.FC = () => {
|
||||
return (
|
||||
<EuiPanel
|
||||
hasBorder={false}
|
||||
hasShadow={false}
|
||||
paddingSize="none"
|
||||
data-test-subj="SearchIndicesAddDocumentsCode"
|
||||
>
|
||||
TODO: WITHOUT DATA TICKET
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Result, resultToField, resultMetaData } from '@kbn/search-index-documents';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { useIndexDocumentSearch } from '../../hooks/api/use_document_search';
|
||||
import { useIndexMapping } from '../../hooks/api/use_index_mappings';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { AddDocumentsCodeExample } from './add_documents_code_example';
|
||||
|
||||
interface IndexDocumentsProps {
|
||||
indexName: string;
|
||||
}
|
||||
|
||||
interface RecentDocsActionMessageProps {
|
||||
indexName: string;
|
||||
}
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 50;
|
||||
|
||||
const RecentDocsActionMessage: React.FC<RecentDocsActionMessageProps> = ({ indexName }) => {
|
||||
const {
|
||||
services: { share },
|
||||
} = useKibana();
|
||||
|
||||
const discoverLocator = share.url.locators.get('DISCOVER_APP_LOCATOR');
|
||||
|
||||
const onClick = async () => {
|
||||
await discoverLocator?.navigate({ dataViewSpec: { title: indexName } });
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={false} hasShadow={false} color="subdued" borderRadius="none">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="calendar" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<p>
|
||||
{i18n.translate('xpack.searchIndices.indexDocuments.recentDocsActionMessage', {
|
||||
defaultMessage:
|
||||
'You are viewing the {pageSize} most recently ingested documents in this index. To see all documents, view in',
|
||||
values: {
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
},
|
||||
})}{' '}
|
||||
<EuiLink onClick={onClick}>
|
||||
{i18n.translate('xpack.searchIndices.indexDocuments.recentDocsActionMessageLink', {
|
||||
defaultMessage: 'Discover.',
|
||||
})}
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const IndexDocuments: React.FC<IndexDocumentsProps> = ({ indexName }) => {
|
||||
const { data: indexDocuments, isInitialLoading } = useIndexDocumentSearch(indexName, {
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
pageIndex: 0,
|
||||
});
|
||||
|
||||
const { data: mappingData } = useIndexMapping(indexName);
|
||||
|
||||
const docs = indexDocuments?.results?.data ?? [];
|
||||
const mappingProperties = mappingData?.mappings?.properties ?? {};
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
{isInitialLoading && <EuiProgress size="xs" color="primary" />}
|
||||
{docs.length === 0 && <AddDocumentsCodeExample />}
|
||||
{docs.length > 0 && (
|
||||
<>
|
||||
<RecentDocsActionMessage indexName={indexName} />
|
||||
<EuiSpacer size="m" />
|
||||
{docs.map((doc) => {
|
||||
return (
|
||||
<React.Fragment key={doc._id}>
|
||||
<Result
|
||||
fields={resultToField(doc, mappingProperties)}
|
||||
metaData={resultMetaData(doc)}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import {
|
||||
EuiPageSection,
|
||||
EuiSpacer,
|
||||
EuiButton,
|
||||
EuiPageTemplate,
|
||||
EuiFlexItem,
|
||||
EuiTabbedContent,
|
||||
EuiFlexGroup,
|
||||
EuiPopover,
|
||||
EuiButtonIcon,
|
||||
|
@ -30,6 +30,7 @@ import { useKibana } from '../../hooks/use_kibana';
|
|||
import { ConnectionDetails } from '../connection_details/connection_details';
|
||||
import { QuickStats } from '../quick_stats/quick_stats';
|
||||
import { useIndexMapping } from '../../hooks/api/use_index_mappings';
|
||||
import { IndexDocuments } from '../index_documents/index_documents';
|
||||
import { DeleteIndexModal } from './delete_index_modal';
|
||||
import { IndexloadingError } from './details_page_loading_error';
|
||||
|
||||
|
@ -157,31 +158,47 @@ export const SearchIndexDetailsPage = () => {
|
|||
</EuiFlexGroup>,
|
||||
]}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
{isShowingDeleteModal && (
|
||||
<DeleteIndexModal
|
||||
onCancel={() => setShowDeleteIndexModal(!isShowingDeleteModal)}
|
||||
indexName={indexName}
|
||||
navigateToIndexListPage={navigateToIndexListPage}
|
||||
/>
|
||||
)}
|
||||
<EuiPageTemplate.Section grow={false}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<ConnectionDetails />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<ConnectionDetails />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{/* TODO: API KEY */}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<QuickStats index={index} mappings={mappings} />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTabbedContent
|
||||
tabs={[
|
||||
{
|
||||
id: 'data',
|
||||
name: i18n.translate('xpack.searchIndices.documentsTabLabel', {
|
||||
defaultMessage: 'Data',
|
||||
}),
|
||||
content: <IndexDocuments indexName={indexName} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{/* TODO: API KEY */}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<QuickStats index={index} mappings={mappings} />
|
||||
</EuiFlexGroup>
|
||||
</EuiPageTemplate.Section>
|
||||
</>
|
||||
)}
|
||||
{isShowingDeleteModal && (
|
||||
<DeleteIndexModal
|
||||
onCancel={() => setShowDeleteIndexModal(!isShowingDeleteModal)}
|
||||
indexName={indexName}
|
||||
navigateToIndexListPage={navigateToIndexListPage}
|
||||
/>
|
||||
)}
|
||||
{embeddableConsole}
|
||||
</EuiPageTemplate>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Pagination } from '@elastic/eui';
|
||||
import { SearchHit } from '@kbn/es-types';
|
||||
import { pageToPagination, Paginate } from '@kbn/search-index-documents';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useKibana } from '../use_kibana';
|
||||
|
||||
interface IndexDocuments {
|
||||
meta: Pagination;
|
||||
results: Paginate<SearchHit>;
|
||||
}
|
||||
const DEFAULT_PAGINATION = {
|
||||
from: 0,
|
||||
has_more_hits_than_total: false,
|
||||
size: 10,
|
||||
total: 0,
|
||||
};
|
||||
const pollingInterval = 5 * 1000;
|
||||
export const useIndexDocumentSearch = (
|
||||
indexName: string,
|
||||
pagination: Omit<Pagination, 'totalItemCount'>,
|
||||
searchQuery?: string
|
||||
) => {
|
||||
const {
|
||||
services: { http },
|
||||
} = useKibana();
|
||||
const response = useQuery({
|
||||
queryKey: ['fetchIndexDocuments', pagination, searchQuery],
|
||||
refetchInterval: pollingInterval,
|
||||
refetchIntervalInBackground: true,
|
||||
refetchOnWindowFocus: 'always',
|
||||
queryFn: async () =>
|
||||
http.post<IndexDocuments>(`/internal/serverless_search/indices/${indexName}/search`, {
|
||||
body: JSON.stringify({
|
||||
searchQuery,
|
||||
}),
|
||||
query: {
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
},
|
||||
}),
|
||||
});
|
||||
return {
|
||||
...response,
|
||||
meta: pageToPagination(response?.data?.results?._meta?.page ?? DEFAULT_PAGINATION),
|
||||
};
|
||||
};
|
|
@ -32,6 +32,8 @@
|
|||
"@kbn/index-management-shared-types",
|
||||
"@kbn/try-in-console",
|
||||
"@kbn/cloud-plugin",
|
||||
"@kbn/search-index-documents",
|
||||
"@kbn/es-types",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -66,6 +66,16 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont
|
|||
await testSubjects.missingOrFail('setupAISearchButton', { timeout: 2000 });
|
||||
},
|
||||
|
||||
async expectAddDocumentCodeExamples() {
|
||||
await testSubjects.existOrFail('SearchIndicesAddDocumentsCode', { timeout: 2000 });
|
||||
},
|
||||
|
||||
async expectHasIndexDocuments() {
|
||||
await retry.try(async () => {
|
||||
await testSubjects.existOrFail('search-index-documents-result', { timeout: 2000 });
|
||||
});
|
||||
},
|
||||
|
||||
async expectMoreOptionsActionButtonExists() {
|
||||
await testSubjects.existOrFail('moreOptionsActionButton');
|
||||
},
|
||||
|
|
|
@ -61,10 +61,25 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
},
|
||||
});
|
||||
await svlSearchNavigation.navigateToIndexDetailPage(indexName);
|
||||
|
||||
await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappingsToHaveVectorFields();
|
||||
});
|
||||
|
||||
it('should show code examples for adding documents', async () => {
|
||||
await pageObjects.svlSearchIndexDetailPage.expectAddDocumentCodeExamples();
|
||||
});
|
||||
|
||||
it('should have index documents', async () => {
|
||||
await es.index({
|
||||
index: indexName,
|
||||
body: {
|
||||
my_field: [1, 0, 1],
|
||||
},
|
||||
});
|
||||
|
||||
await svlSearchNavigation.navigateToIndexDetailPage(indexName);
|
||||
await pageObjects.svlSearchIndexDetailPage.expectHasIndexDocuments();
|
||||
});
|
||||
|
||||
it('back to indices button should redirect to list page', async () => {
|
||||
await pageObjects.svlSearchIndexDetailPage.expectBackToIndicesButtonExists();
|
||||
await pageObjects.svlSearchIndexDetailPage.clickBackToIndicesButton();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue