Skip to content

Commit

Permalink
[Onboarding] Connection details + Quick Stats (#192636)
Browse files Browse the repository at this point in the history
## Summary

Adding in the connection details and quickstats for the search_details
page.

![Screenshot 2024-09-11 at 20 36
31](https://github.com/user-attachments/assets/5f030c06-4a98-4d9d-a465-c6719998ca56)
![Screenshot 2024-09-11 at 20 36
27](https://github.com/user-attachments/assets/d96be2f1-bcaa-42e5-9d32-1612e090b916)
![Screenshot 2024-09-11 at 20 36
09](https://github.com/user-attachments/assets/1f7995ae-5a0d-4810-acfb-3fafe33be451)

### Checklist

Delete any items that are not applicable to this PR.

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Liam Thompson <[email protected]>
(cherry picked from commit 4d48881)
  • Loading branch information
joemcelroy committed Sep 17, 2024
1 parent 259bfca commit abb63db
Show file tree
Hide file tree
Showing 15 changed files with 603 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/get_doc_links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
searchApplicationsSearch: `${ELASTICSEARCH_DOCS}search-application-client.html`,
searchLabs: `${SEARCH_LABS_URL}`,
searchLabsRepo: `${SEARCH_LABS_REPO}`,
semanticSearch: `${ELASTICSEARCH_DOCS}semantic-search.html`,
searchTemplates: `${ELASTICSEARCH_DOCS}search-template.html`,
semanticTextField: `${ELASTICSEARCH_DOCS}semantic-text.html`,
start: `${ENTERPRISE_SEARCH_DOCS}start.html`,
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export interface DocLinks {
readonly searchApplicationsSearch: string;
readonly searchLabs: string;
readonly searchLabsRepo: string;
readonly semanticSearch: string;
readonly searchTemplates: string;
readonly semanticTextField: string;
readonly start: string;
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/search_indices/common/doc_links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import { DocLinks } from '@kbn/doc-links';

class SearchIndicesDocLinks {
public apiReference: string = '';
public setupSemanticSearch: string = '';

constructor() {}

setDocLinks(newDocLinks: DocLinks) {
this.apiReference = newDocLinks.apiReference;
this.setupSemanticSearch = newDocLinks.enterpriseSearch.semanticSearch;
}
}
export const docLinks = new SearchIndicesDocLinks();
Original file line number Diff line number Diff line change
@@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';

import {
EuiButtonIcon,
EuiCopy,
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { FormattedMessage } from '@kbn/i18n-react';
import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url';

export const ConnectionDetails: React.FC = () => {
const { euiTheme } = useEuiTheme();
const elasticsearchUrl = useElasticsearchUrl();

return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xxxs">
<h1>
<FormattedMessage
id="xpack.searchIndices.connectionDetails.endpointTitle"
defaultMessage="Elasticsearch URL"
/>
</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem css={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
<p
data-test-subj="connectionDetailsEndpoint"
css={{
color: euiTheme.colors.successText,
padding: `${euiTheme.size.s} ${euiTheme.size.m}`,
backgroundColor: euiTheme.colors.lightestShade,
textOverflow: 'ellipsis',
overflow: 'hidden',
}}
>
{elasticsearchUrl}
</p>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCopy
textToCopy={elasticsearchUrl}
afterMessage={i18n.translate('xpack.searchIndices.connectionDetails.copyMessage', {
defaultMessage: 'Copied',
})}
>
{(copy) => (
<EuiButtonIcon
onClick={copy}
iconType="copy"
aria-label={i18n.translate('xpack.searchIndices.connectionDetails.copyMessage', {
defaultMessage: 'Copy Elasticsearch URL to clipboard',
})}
/>
)}
</EuiCopy>
</EuiFlexItem>
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,22 @@ import { i18n } from '@kbn/i18n';
import { SectionLoading } from '@kbn/es-ui-shared-plugin/public';
import { useIndex } from '../../hooks/api/use_index';
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 { DeleteIndexModal } from './delete_index_modal';
import { IndexloadingError } from './details_page_loading_error';

export const SearchIndexDetailsPage = () => {
const indexName = decodeURIComponent(useParams<{ indexName: string }>().indexName);
const { console: consolePlugin, docLinks, application } = useKibana().services;
const { data: index, refetch, isSuccess, isInitialLoading } = useIndex(indexName);

const { data: index, refetch, isError: isIndexError, isInitialLoading } = useIndex(indexName);
const {
data: mappings,
isError: isMappingsError,
isInitialLoading: isMappingsInitialLoading,
} = useIndexMapping(indexName);

const embeddableConsole = useMemo(
() => (consolePlugin?.EmbeddableConsole ? <consolePlugin.EmbeddableConsole /> : null),
Expand Down Expand Up @@ -87,7 +96,7 @@ export const SearchIndexDetailsPage = () => {
/>
</EuiPopover>
);
if (isInitialLoading) {
if (isInitialLoading || isMappingsInitialLoading) {
return (
<SectionLoading>
{i18n.translate('xpack.searchIndices.loadingDescription', {
Expand All @@ -103,9 +112,10 @@ export const SearchIndexDetailsPage = () => {
restrictWidth={false}
data-test-subj="searchIndicesDetailsPage"
grow={false}
bottomBorder={false}
panelled
bottomBorder
>
{!isSuccess || !index ? (
{isIndexError || isMappingsError || !index || !mappings ? (
<IndexloadingError
indexName={indexName}
navigateToIndexListPage={navigateToIndexListPage}
Expand Down Expand Up @@ -156,8 +166,20 @@ export const SearchIndexDetailsPage = () => {
navigateToIndexListPage={navigateToIndexListPage}
/>
)}
<EuiPageTemplate.Section grow={false}>
<EuiFlexGroup>
<EuiFlexItem>
<ConnectionDetails />
</EuiFlexItem>
<EuiFlexItem>{/* TODO: API KEY */}</EuiFlexItem>
</EuiFlexGroup>

<EuiSpacer size="l" />

<div data-test-subj="searchIndexDetailsContent" />
<EuiFlexGroup>
<QuickStats index={index} mappings={mappings} />
</EuiFlexGroup>
</EuiPageTemplate.Section>
</>
)}
{embeddableConsole}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { Mappings } from '../../types';
import { countVectorBasedTypesFromMappings } from './mappings_convertor';

describe('mappings convertor', () => {
it('should count vector based types from mappings', () => {
const mappings = {
mappings: {
properties: {
field1: {
type: 'dense_vector',
},
field2: {
type: 'dense_vector',
},
field3: {
type: 'sparse_vector',
},
field4: {
type: 'dense_vector',
},
field5: {
type: 'semantic_text',
},
},
},
};
const result = countVectorBasedTypesFromMappings(mappings as unknown as Mappings);
expect(result).toEqual({
dense_vector: 3,
sparse_vector: 1,
semantic_text: 1,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 type {
MappingProperty,
MappingPropertyBase,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { Mappings } from '../../types';

interface VectorFieldTypes {
semantic_text: number;
dense_vector: number;
sparse_vector: number;
}

export function countVectorBasedTypesFromMappings(mappings: Mappings): VectorFieldTypes {
const typeCounts: VectorFieldTypes = {
semantic_text: 0,
dense_vector: 0,
sparse_vector: 0,
};

const typeCountKeys = Object.keys(typeCounts);

function recursiveCount(fields: MappingProperty | Mappings | MappingPropertyBase['fields']) {
if (!fields) {
return;
}
if ('mappings' in fields) {
recursiveCount(fields.mappings);
}
if ('properties' in fields && fields.properties) {
Object.keys(fields.properties).forEach((key) => {
const value = (fields.properties as Record<string, MappingProperty>)?.[key];

if (value && value.type) {
if (typeCountKeys.includes(value.type)) {
const type = value.type as keyof VectorFieldTypes;
typeCounts[type] = typeCounts[type] + 1;
}

if ('fields' in value) {
recursiveCount(value.fields);
}

if ('properties' in value) {
recursiveCount(value.properties);
}
} else if (value.properties || value.fields) {
recursiveCount(value);
}
});
}
}

recursiveCount(mappings);
return typeCounts;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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 {
EuiAccordion,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPanel,
EuiText,
useEuiTheme,
useGeneratedHtmlId,
} from '@elastic/eui';

interface BaseQuickStatProps {
icon: string;
iconColor: string;
title: string;
secondaryTitle: React.ReactNode;
open: boolean;
content?: React.ReactNode;
stats: Array<{
title: string;
description: NonNullable<React.ReactNode>;
}>;
setOpen: (open: boolean) => void;
first?: boolean;
}

export const QuickStat: React.FC<BaseQuickStatProps> = ({
icon,
title,
stats,
open,
setOpen,
first,
secondaryTitle,
iconColor,
content,
...rest
}) => {
const { euiTheme } = useEuiTheme();

const id = useGeneratedHtmlId({
prefix: 'formAccordion',
suffix: title,
});

return (
<EuiAccordion
forceState={open ? 'open' : 'closed'}
onToggle={() => setOpen(!open)}
paddingSize="none"
id={id}
buttonElement="div"
arrowDisplay="right"
{...rest}
css={{
borderLeft: euiTheme.border.thin,
...(first ? { borderLeftWidth: 0 } : {}),
'.euiAccordion__arrow': {
marginRight: euiTheme.size.s,
},
'.euiAccordion__triggerWrapper': {
background: euiTheme.colors.ghost,
},
'.euiAccordion__children': {
borderTop: euiTheme.border.thin,
padding: euiTheme.size.m,
},
}}
buttonContent={
<EuiPanel hasShadow={false} hasBorder={false} paddingSize="s">
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiIcon type={icon} color={iconColor} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<h4>{title}</h4>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued">{secondaryTitle}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
}
>
{content ? (
content
) : (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiDescriptionList
type="column"
listItems={stats}
columnWidths={[3, 1]}
compressed
descriptionProps={{
color: 'subdued',
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiAccordion>
);
};
Loading

0 comments on commit abb63db

Please sign in to comment.