mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Search] [Onboarding] Hosted Quick Stats (#207925)
## Summary This PR updates the `search_indices` Index Details page to support quicks stats specific to stateful indices. ### Demo https://github.com/user-attachments/assets/5584f0b4-a7cb-4802-8aef-6708642a4629 ### Checklist - [ ] 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/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [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 --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
5be4d61e9f
commit
95d863bc8b
18 changed files with 726 additions and 168 deletions
|
@ -47,6 +47,7 @@ export const SearchIndexDetailsPage = () => {
|
|||
const tabId = decodeURIComponent(useParams<{ tabId: string }>().tabId);
|
||||
|
||||
const {
|
||||
cloud,
|
||||
console: consolePlugin,
|
||||
docLinks,
|
||||
application,
|
||||
|
@ -290,7 +291,12 @@ export const SearchIndexDetailsPage = () => {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<QuickStats indexDocuments={indexDocuments} index={index} mappings={mappings} />
|
||||
<QuickStats
|
||||
isStateless={cloud?.isServerlessEnabled ?? false}
|
||||
indexDocuments={indexDocuments}
|
||||
index={index}
|
||||
mappings={mappings}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 { useEuiTheme } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SetupAISearchButton } from './setup_ai_search_button';
|
||||
import { VectorFieldTypes } from './mappings_convertor';
|
||||
import { QuickStat } from './quick_stat';
|
||||
|
||||
export interface AISearchQuickStatProps {
|
||||
mappingStats: VectorFieldTypes;
|
||||
vectorFieldCount: number;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const AISearchQuickStat = ({
|
||||
mappingStats,
|
||||
vectorFieldCount,
|
||||
open,
|
||||
setOpen,
|
||||
}: AISearchQuickStatProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<QuickStat
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
icon="sparkles"
|
||||
iconColor={euiTheme.colors.fullShade}
|
||||
title={i18n.translate('xpack.searchIndices.quickStats.ai_search_heading', {
|
||||
defaultMessage: 'AI Search',
|
||||
})}
|
||||
data-test-subj="QuickStatsAIMappings"
|
||||
secondaryTitle={
|
||||
vectorFieldCount > 0
|
||||
? i18n.translate('xpack.searchIndices.quickStats.total_count', {
|
||||
defaultMessage: '{value, plural, one {# Field} other {# Fields}}',
|
||||
values: {
|
||||
value: vectorFieldCount,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.searchIndices.quickStats.no_vector_fields', {
|
||||
defaultMessage: 'Not configured',
|
||||
})
|
||||
}
|
||||
content={vectorFieldCount === 0 ? <SetupAISearchButton /> : undefined}
|
||||
stats={[
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.quickStats.sparse_vector', {
|
||||
defaultMessage: 'Sparse Vector',
|
||||
}),
|
||||
description: i18n.translate('xpack.searchIndices.quickStats.sparse_vector_count', {
|
||||
defaultMessage: '{value, plural, one {# Field} other {# Fields}}',
|
||||
values: { value: mappingStats.sparse_vector },
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.quickStats.dense_vector', {
|
||||
defaultMessage: 'Dense Vector',
|
||||
}),
|
||||
description: i18n.translate('xpack.searchIndices.quickStats.dense_vector_count', {
|
||||
defaultMessage: '{value, plural, one {# Field} other {# Fields}}',
|
||||
values: { value: mappingStats.dense_vector },
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.quickStats.semantic_text', {
|
||||
defaultMessage: 'Semantic Text',
|
||||
}),
|
||||
description: i18n.translate('xpack.searchIndices.quickStats.semantic_text_count', {
|
||||
defaultMessage: '{value, plural, one {# Field} other {# Fields}}',
|
||||
values: { value: mappingStats.semantic_text },
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiI18nNumber, useEuiTheme } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { QuickStat } from './quick_stat';
|
||||
import { AliasesContentStyle } from './styles';
|
||||
|
||||
export interface AliasesStatProps {
|
||||
aliases: string[];
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const AliasesStat = ({ aliases, open, setOpen }: AliasesStatProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<QuickStat
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
icon="symlink"
|
||||
iconColor={euiTheme.colors.fullShade}
|
||||
title={i18n.translate('xpack.searchIndices.quickStats.aliases_heading', {
|
||||
defaultMessage: 'Aliases',
|
||||
})}
|
||||
data-test-subj="QuickStatsAliases"
|
||||
secondaryTitle={<EuiI18nNumber value={aliases.length} />}
|
||||
content={
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
css={AliasesContentStyle}
|
||||
className="eui-yScroll"
|
||||
>
|
||||
{aliases.map((alias, i) => (
|
||||
<EuiFlexItem key={`alias.${i}`}>
|
||||
<EuiText size="s" color="subdued">
|
||||
{alias}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
stats={[]}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const DOCUMENT_COUNT_LABEL = i18n.translate(
|
||||
'xpack.searchIndices.quickStats.document_count_heading',
|
||||
{
|
||||
defaultMessage: 'Document count',
|
||||
}
|
||||
);
|
||||
export const TOTAL_COUNT_LABEL = i18n.translate(
|
||||
'xpack.searchIndices.quickStats.documents.totalTitle',
|
||||
{
|
||||
defaultMessage: 'Total',
|
||||
}
|
||||
);
|
||||
export const DELETED_COUNT_LABEL = i18n.translate(
|
||||
'xpack.searchIndices.quickStats.documents.deletedTitle',
|
||||
{
|
||||
defaultMessage: 'Deleted',
|
||||
}
|
||||
);
|
||||
export const INDEX_SIZE_LABEL = i18n.translate(
|
||||
'xpack.searchIndices.quickStats.documents.indexSize',
|
||||
{
|
||||
defaultMessage: 'Index Size',
|
||||
}
|
||||
);
|
||||
export const DOCUMENT_COUNT_TOOLTIP = i18n.translate(
|
||||
'xpack.searchIndices.quickStats.documentCountTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'This excludes nested documents, which Elasticsearch uses internally to store chunks of vectors.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { type IndicesStatsIndexMetadataState } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { Index } from '@kbn/index-management-shared-types';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { QuickStat, type QuickStatDefinition } from './quick_stat';
|
||||
import {
|
||||
indexHealthToHealthColor,
|
||||
HealthStatusStrings,
|
||||
normalizeHealth,
|
||||
} from '../../utils/indices';
|
||||
|
||||
export const healthTitleMap: Record<HealthStatusStrings, string> = {
|
||||
red: i18n.translate('xpack.searchIndices.quickStats.indexHealth.red', { defaultMessage: 'Red' }),
|
||||
green: i18n.translate('xpack.searchIndices.quickStats.indexHealth.green', {
|
||||
defaultMessage: 'Green',
|
||||
}),
|
||||
yellow: i18n.translate('xpack.searchIndices.quickStats.indexHealth.yellow', {
|
||||
defaultMessage: 'Yellow',
|
||||
}),
|
||||
unavailable: i18n.translate('xpack.searchIndices.quickStats.indexHealth.unavailable', {
|
||||
defaultMessage: 'Unavailable',
|
||||
}),
|
||||
};
|
||||
export const statusDescriptionMap: Record<IndicesStatsIndexMetadataState | 'undefined', string> = {
|
||||
open: i18n.translate('xpack.searchIndices.quickStats.indexStatus.open', {
|
||||
defaultMessage: 'Index available',
|
||||
}),
|
||||
close: i18n.translate('xpack.searchIndices.quickStats.indexStatus.close', {
|
||||
defaultMessage: 'Index unavailable',
|
||||
}),
|
||||
undefined: i18n.translate('xpack.searchIndices.quickStats.indexStatus.undefined', {
|
||||
defaultMessage: 'Unknown',
|
||||
}),
|
||||
};
|
||||
|
||||
export interface IndexStatusStatProps {
|
||||
index: Index;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
function safelyParseShardCount(count: string | number) {
|
||||
if (typeof count === 'number') return count;
|
||||
const parsedValue = parseInt(count, 10);
|
||||
if (!isNaN(parsedValue)) return parsedValue;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const IndexStatusStat = ({ index, open, setOpen }: IndexStatusStatProps) => {
|
||||
const { replicaShards, stats: indexStats } = useMemo(() => {
|
||||
let primaryShardCount: number | undefined;
|
||||
let replicaShardCount: number | undefined;
|
||||
const stats: QuickStatDefinition[] = [
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.quickStats.indexStatus.title', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
description: statusDescriptionMap[index.status ?? 'undefined'],
|
||||
},
|
||||
];
|
||||
if (index.primary) {
|
||||
primaryShardCount = safelyParseShardCount(index.primary);
|
||||
stats.push({
|
||||
title: i18n.translate('xpack.searchIndices.quickStats.indexStatus.primary', {
|
||||
defaultMessage: 'Primary shards',
|
||||
}),
|
||||
description: index.primary,
|
||||
});
|
||||
}
|
||||
if (index.replica) {
|
||||
replicaShardCount = safelyParseShardCount(index.replica);
|
||||
stats.push({
|
||||
title: i18n.translate('xpack.searchIndices.quickStats.indexStatus.replica', {
|
||||
defaultMessage: 'Replica shards',
|
||||
}),
|
||||
description: index.replica,
|
||||
});
|
||||
}
|
||||
|
||||
return { stats, primaryShards: primaryShardCount, replicaShards: replicaShardCount };
|
||||
}, [index]);
|
||||
return (
|
||||
<QuickStat
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
icon="dot"
|
||||
iconColor={indexHealthToHealthColor(index.health)}
|
||||
title={healthTitleMap[normalizeHealth(index.health ?? 'unavailable')]}
|
||||
secondaryTitle={
|
||||
index.replica &&
|
||||
i18n.translate('xpack.searchIndices.quickStats.indexStatus.replicaTitle', {
|
||||
defaultMessage: '{replicaShards, plural, one {# Replica} other {# Replicas}}',
|
||||
values: {
|
||||
replicaShards,
|
||||
},
|
||||
})
|
||||
}
|
||||
data-test-subj="QuickStatsIndexStatus"
|
||||
stats={indexStats}
|
||||
statsColumnWidths={[2, 2]}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
import type { MappingProperty, MappingPropertyBase } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { Mappings } from '../../types';
|
||||
|
||||
interface VectorFieldTypes {
|
||||
export interface VectorFieldTypes {
|
||||
semantic_text: number;
|
||||
dense_vector: number;
|
||||
sparse_vector: number;
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiDescriptionList,
|
||||
|
@ -21,20 +20,22 @@ import {
|
|||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface BaseQuickStatProps {
|
||||
export interface BaseQuickStatProps {
|
||||
icon: string;
|
||||
iconColor: string;
|
||||
title: string;
|
||||
secondaryTitle: React.ReactNode;
|
||||
secondaryTitle?: React.ReactNode;
|
||||
open: boolean;
|
||||
content?: React.ReactNode;
|
||||
stats: Array<{
|
||||
title: string;
|
||||
description: NonNullable<React.ReactNode>;
|
||||
}>;
|
||||
stats: QuickStatDefinition[];
|
||||
setOpen: (open: boolean) => void;
|
||||
first?: boolean;
|
||||
tooltipContent?: string;
|
||||
statsColumnWidths?: [string | number, string | number] | undefined;
|
||||
}
|
||||
|
||||
export interface QuickStatDefinition {
|
||||
title: string;
|
||||
description: NonNullable<React.ReactNode>;
|
||||
}
|
||||
|
||||
export const QuickStat: React.FC<BaseQuickStatProps> = ({
|
||||
|
@ -43,11 +44,11 @@ export const QuickStat: React.FC<BaseQuickStatProps> = ({
|
|||
stats,
|
||||
open,
|
||||
setOpen,
|
||||
first,
|
||||
secondaryTitle,
|
||||
iconColor,
|
||||
content,
|
||||
tooltipContent,
|
||||
statsColumnWidths,
|
||||
...rest
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
@ -60,6 +61,7 @@ export const QuickStat: React.FC<BaseQuickStatProps> = ({
|
|||
return (
|
||||
<EuiAccordion
|
||||
forceState={open ? 'open' : 'closed'}
|
||||
data-test-subj={id}
|
||||
onToggle={() => setOpen(!open)}
|
||||
paddingSize="none"
|
||||
id={id}
|
||||
|
@ -67,8 +69,6 @@ export const QuickStat: React.FC<BaseQuickStatProps> = ({
|
|||
arrowDisplay="right"
|
||||
{...rest}
|
||||
css={{
|
||||
borderLeft: euiTheme.border.thin,
|
||||
...(first ? { borderLeftWidth: 0 } : {}),
|
||||
'.euiAccordion__arrow': {
|
||||
marginRight: euiTheme.size.s,
|
||||
},
|
||||
|
@ -76,7 +76,6 @@ export const QuickStat: React.FC<BaseQuickStatProps> = ({
|
|||
background: euiTheme.colors.emptyShade,
|
||||
},
|
||||
'.euiAccordion__children': {
|
||||
borderTop: euiTheme.border.thin,
|
||||
padding: euiTheme.size.m,
|
||||
},
|
||||
}}
|
||||
|
@ -84,21 +83,25 @@ export const QuickStat: React.FC<BaseQuickStatProps> = ({
|
|||
<EuiPanel hasShadow={false} hasBorder={false} paddingSize="s">
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={icon} color={iconColor} />
|
||||
<span>
|
||||
<EuiIcon type={icon} color={iconColor} />
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxs">
|
||||
<h4>{title}</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" color="subdued">
|
||||
{secondaryTitle}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{secondaryTitle && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" color="subdued">
|
||||
{secondaryTitle}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{tooltipContent && (
|
||||
<EuiFlexItem>
|
||||
<EuiIconTip content={tooltipContent} />
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip content={tooltipContent} display="block" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
@ -113,7 +116,7 @@ export const QuickStat: React.FC<BaseQuickStatProps> = ({
|
|||
<EuiDescriptionList
|
||||
type="column"
|
||||
listItems={stats}
|
||||
columnWidths={[3, 1]}
|
||||
columnWidths={statsColumnWidths ?? [3, 1]}
|
||||
compressed
|
||||
descriptionProps={{
|
||||
color: 'subdued',
|
||||
|
|
|
@ -8,168 +8,93 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import type { Index } from '@kbn/index-management-shared-types';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiI18nNumber,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPanel, useEuiTheme } from '@elastic/eui';
|
||||
import { Mappings } from '../../types';
|
||||
import { countVectorBasedTypesFromMappings } from './mappings_convertor';
|
||||
import { QuickStat } from './quick_stat';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { IndexDocuments } from '../../hooks/api/use_document_search';
|
||||
|
||||
import { AISearchQuickStat } from './ai_search_stat';
|
||||
import { AliasesStat } from './aliases_quick_stat';
|
||||
import { StatefulDocumentCountStat } from './stateful_document_count_stat';
|
||||
import { StatefulIndexStorageStat } from './stateful_storage_stat';
|
||||
import { IndexStatusStat } from './index_status_stat';
|
||||
import { QuickStatsContainer } from './quick_stats_container';
|
||||
import { countVectorBasedTypesFromMappings } from './mappings_convertor';
|
||||
import { StatelessDocumentCountStat } from './stateless_document_cout_stat';
|
||||
import { StatelessQuickStats } from './stateless_quick_stats';
|
||||
import { QuickStatsPanelStyle } from './styles';
|
||||
|
||||
export interface QuickStatsProps {
|
||||
index: Index;
|
||||
mappings: Mappings;
|
||||
indexDocuments: IndexDocuments;
|
||||
tooltipContent?: string;
|
||||
isStateless: boolean;
|
||||
}
|
||||
|
||||
export const SetupAISearchButton: React.FC = () => {
|
||||
const {
|
||||
services: { docLinks },
|
||||
} = useKibana();
|
||||
return (
|
||||
<EuiPanel hasBorder={false} hasShadow={false} color="transparent">
|
||||
<EuiFlexGroup gutterSize="s" direction="column" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<h6>
|
||||
{i18n.translate('xpack.searchIndices.quickStats.setup_ai_search_description', {
|
||||
defaultMessage: 'Build AI-powered search experiences with Elastic',
|
||||
})}
|
||||
</h6>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
href={docLinks.links.enterpriseSearch.semanticSearch}
|
||||
target="_blank"
|
||||
data-test-subj="setupAISearchButton"
|
||||
>
|
||||
{i18n.translate('xpack.searchIndices.quickStats.setup_ai_search_button', {
|
||||
defaultMessage: 'Set up now',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const QuickStats: React.FC<QuickStatsProps> = ({ index, mappings, indexDocuments }) => {
|
||||
export const QuickStats: React.FC<QuickStatsProps> = ({
|
||||
index,
|
||||
mappings,
|
||||
indexDocuments,
|
||||
isStateless,
|
||||
}) => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const mappingStats = useMemo(() => countVectorBasedTypesFromMappings(mappings), [mappings]);
|
||||
const vectorFieldCount =
|
||||
mappingStats.sparse_vector + mappingStats.dense_vector + mappingStats.semantic_text;
|
||||
const docCount = indexDocuments?.results._meta.page.total ?? 0;
|
||||
const { mappingStats, vectorFieldCount } = useMemo(() => {
|
||||
const stats = countVectorBasedTypesFromMappings(mappings);
|
||||
const vectorFields = stats.sparse_vector + stats.dense_vector + stats.semantic_text;
|
||||
return { mappingStats: stats, vectorFieldCount: vectorFields };
|
||||
}, [mappings]);
|
||||
|
||||
const stats = isStateless
|
||||
? [
|
||||
<StatelessDocumentCountStat
|
||||
index={index}
|
||||
documentCount={indexDocuments?.results._meta.page.total ?? 0}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
/>,
|
||||
...(Array.isArray(index.aliases) && index.aliases.length > 0
|
||||
? [<AliasesStat aliases={index.aliases} open={open} setOpen={setOpen} />]
|
||||
: []),
|
||||
<AISearchQuickStat
|
||||
mappingStats={mappingStats}
|
||||
vectorFieldCount={vectorFieldCount}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
/>,
|
||||
]
|
||||
: [
|
||||
<IndexStatusStat index={index} open={open} setOpen={setOpen} />,
|
||||
<StatefulIndexStorageStat index={index} open={open} setOpen={setOpen} />,
|
||||
<StatefulDocumentCountStat
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
index={index}
|
||||
mappingStats={mappingStats}
|
||||
/>,
|
||||
...(Array.isArray(index.aliases) && index.aliases.length > 0
|
||||
? [<AliasesStat aliases={index.aliases} open={open} setOpen={setOpen} />]
|
||||
: []),
|
||||
<AISearchQuickStat
|
||||
mappingStats={mappingStats}
|
||||
vectorFieldCount={vectorFieldCount}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
/>,
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
paddingSize="none"
|
||||
data-test-subj="quickStats"
|
||||
hasShadow={false}
|
||||
css={() => ({
|
||||
border: euiTheme.border.thin,
|
||||
background: euiTheme.colors.lightestShade,
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
css={QuickStatsPanelStyle(euiTheme)}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<QuickStat
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
icon="documents"
|
||||
iconColor={euiTheme.colors.fullShade}
|
||||
title={i18n.translate('xpack.searchIndices.quickStats.document_count_heading', {
|
||||
defaultMessage: 'Document count',
|
||||
})}
|
||||
data-test-subj="QuickStatsDocumentCount"
|
||||
secondaryTitle={<EuiI18nNumber value={docCount ?? 0} />}
|
||||
stats={[
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.quickStats.documents.totalTitle', {
|
||||
defaultMessage: 'Total',
|
||||
}),
|
||||
description: <EuiI18nNumber value={docCount ?? 0} />,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.quickStats.documents.indexSize', {
|
||||
defaultMessage: 'Index Size',
|
||||
}),
|
||||
description: index.size ?? '0b',
|
||||
},
|
||||
]}
|
||||
tooltipContent={i18n.translate('xpack.searchIndices.quickStats.documentCountTooltip', {
|
||||
defaultMessage:
|
||||
'This excludes nested documents, which Elasticsearch uses internally to store chunks of vectors.',
|
||||
})}
|
||||
first
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<QuickStat
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
icon="sparkles"
|
||||
iconColor={euiTheme.colors.fullShade}
|
||||
title={i18n.translate('xpack.searchIndices.quickStats.ai_search_heading', {
|
||||
defaultMessage: 'AI Search',
|
||||
})}
|
||||
data-test-subj="QuickStatsAIMappings"
|
||||
secondaryTitle={
|
||||
vectorFieldCount > 0
|
||||
? i18n.translate('xpack.searchIndices.quickStats.total_count', {
|
||||
defaultMessage: '{value, plural, one {# Field} other {# Fields}}',
|
||||
values: {
|
||||
value: vectorFieldCount,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.searchIndices.quickStats.no_vector_fields', {
|
||||
defaultMessage: 'Not configured',
|
||||
})
|
||||
}
|
||||
content={vectorFieldCount === 0 && <SetupAISearchButton />}
|
||||
stats={[
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.quickStats.sparse_vector', {
|
||||
defaultMessage: 'Sparse Vector',
|
||||
}),
|
||||
description: i18n.translate('xpack.searchIndices.quickStats.sparse_vector_count', {
|
||||
defaultMessage: '{value, plural, one {# Field} other {# Fields}}',
|
||||
values: { value: mappingStats.sparse_vector },
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.quickStats.dense_vector', {
|
||||
defaultMessage: 'Dense Vector',
|
||||
}),
|
||||
description: i18n.translate('xpack.searchIndices.quickStats.dense_vector_count', {
|
||||
defaultMessage: '{value, plural, one {# Field} other {# Fields}}',
|
||||
values: { value: mappingStats.dense_vector },
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.quickStats.semantic_text', {
|
||||
defaultMessage: 'Semantic Text',
|
||||
}),
|
||||
description: i18n.translate('xpack.searchIndices.quickStats.semantic_text_count', {
|
||||
defaultMessage: '{value, plural, one {# Field} other {# Fields}}',
|
||||
values: { value: mappingStats.semantic_text },
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{isStateless ? (
|
||||
<StatelessQuickStats>{stats}</StatelessQuickStats>
|
||||
) : (
|
||||
<QuickStatsContainer>{stats}</QuickStatsContainer>
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiFlexItem, useEuiTheme } from '@elastic/eui';
|
||||
import { StatsGridContainerStyle, StatsItemStyle } from './styles';
|
||||
|
||||
export const QuickStatsContainer = ({ children }: { children: React.ReactNode[] }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<div css={StatsGridContainerStyle}>
|
||||
{children.map((item, i) =>
|
||||
item ? (
|
||||
<EuiFlexItem key={`quickstat.${i}`} css={StatsItemStyle(euiTheme)}>
|
||||
{item}
|
||||
</EuiFlexItem>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
|
||||
export const SetupAISearchButton: React.FC = () => {
|
||||
const {
|
||||
services: { docLinks },
|
||||
} = useKibana();
|
||||
return (
|
||||
<EuiPanel hasBorder={false} hasShadow={false} color="transparent">
|
||||
<EuiFlexGroup gutterSize="s" direction="column" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<h6>
|
||||
{i18n.translate('xpack.searchIndices.quickStats.setup_ai_search_description', {
|
||||
defaultMessage: 'Build AI-powered search experiences with Elastic',
|
||||
})}
|
||||
</h6>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
href={docLinks.links.enterpriseSearch.semanticSearch}
|
||||
target="_blank"
|
||||
data-test-subj="setupAISearchButton"
|
||||
>
|
||||
{i18n.translate('xpack.searchIndices.quickStats.setup_ai_search_button', {
|
||||
defaultMessage: 'Set up now',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { Index } from '@kbn/index-management-shared-types';
|
||||
|
||||
import { EuiI18nNumber, useEuiTheme } from '@elastic/eui';
|
||||
|
||||
import { QuickStat } from './quick_stat';
|
||||
import {
|
||||
DELETED_COUNT_LABEL,
|
||||
DOCUMENT_COUNT_LABEL,
|
||||
DOCUMENT_COUNT_TOOLTIP,
|
||||
TOTAL_COUNT_LABEL,
|
||||
} from './constants';
|
||||
import { VectorFieldTypes } from './mappings_convertor';
|
||||
|
||||
export interface StatefulDocumentCountStatProps {
|
||||
index: Index;
|
||||
mappingStats: VectorFieldTypes;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const StatefulDocumentCountStat = ({
|
||||
index,
|
||||
open,
|
||||
setOpen,
|
||||
mappingStats,
|
||||
}: StatefulDocumentCountStatProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<QuickStat
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
icon="documents"
|
||||
iconColor={euiTheme.colors.fullShade}
|
||||
title={DOCUMENT_COUNT_LABEL}
|
||||
data-test-subj="QuickStatsDocumentCount"
|
||||
secondaryTitle={<EuiI18nNumber value={index.documents ?? 0} />}
|
||||
stats={[
|
||||
{
|
||||
title: TOTAL_COUNT_LABEL,
|
||||
description: <EuiI18nNumber value={index.documents ?? 0} />,
|
||||
},
|
||||
{
|
||||
title: DELETED_COUNT_LABEL,
|
||||
description: <EuiI18nNumber value={index.documents_deleted ?? 0} />,
|
||||
},
|
||||
]}
|
||||
tooltipContent={mappingStats.semantic_text > 0 ? DOCUMENT_COUNT_TOOLTIP : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 type { Index } from '@kbn/index-management-shared-types';
|
||||
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { QuickStat } from './quick_stat';
|
||||
import { INDEX_SIZE_LABEL } from './constants';
|
||||
|
||||
export interface StatefulIndexStorageStatProps {
|
||||
index: Index;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const StatefulIndexStorageStat = ({
|
||||
index,
|
||||
open,
|
||||
setOpen,
|
||||
}: StatefulIndexStorageStatProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<QuickStat
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
icon="storage"
|
||||
iconColor={euiTheme.colors.fullShade}
|
||||
title={i18n.translate('xpack.searchIndices.quickStats.storage_heading', {
|
||||
defaultMessage: 'Storage',
|
||||
})}
|
||||
data-test-subj="QuickStatsStorage"
|
||||
secondaryTitle={index.size ?? '0b'}
|
||||
stats={[
|
||||
{
|
||||
title: INDEX_SIZE_LABEL,
|
||||
description: index.size ?? '0b',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.quickStats.primarySize_title', {
|
||||
defaultMessage: 'Primary Size',
|
||||
}),
|
||||
description: index.primary_size ?? '0b',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { Index } from '@kbn/index-management-shared-types';
|
||||
|
||||
import { EuiI18nNumber, useEuiTheme } from '@elastic/eui';
|
||||
|
||||
import { QuickStat } from './quick_stat';
|
||||
import {
|
||||
DOCUMENT_COUNT_LABEL,
|
||||
DOCUMENT_COUNT_TOOLTIP,
|
||||
INDEX_SIZE_LABEL,
|
||||
TOTAL_COUNT_LABEL,
|
||||
} from './constants';
|
||||
|
||||
export interface StatelessDocumentCountStatProps {
|
||||
index: Index;
|
||||
documentCount: number;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const StatelessDocumentCountStat = ({
|
||||
index,
|
||||
documentCount,
|
||||
open,
|
||||
setOpen,
|
||||
}: StatelessDocumentCountStatProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<QuickStat
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
icon="documents"
|
||||
iconColor={euiTheme.colors.fullShade}
|
||||
title={DOCUMENT_COUNT_LABEL}
|
||||
data-test-subj="QuickStatsDocumentCount"
|
||||
secondaryTitle={<EuiI18nNumber value={documentCount} />}
|
||||
stats={[
|
||||
{
|
||||
title: TOTAL_COUNT_LABEL,
|
||||
description: <EuiI18nNumber value={documentCount} />,
|
||||
},
|
||||
{
|
||||
title: INDEX_SIZE_LABEL,
|
||||
description: index.size ?? '0b',
|
||||
},
|
||||
]}
|
||||
tooltipContent={DOCUMENT_COUNT_TOOLTIP}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
|
||||
|
||||
import { StatsItemStyle } from './styles';
|
||||
|
||||
export interface StatelessQuickStatsProps {
|
||||
children: React.ReactNode[];
|
||||
}
|
||||
|
||||
export const StatelessQuickStats = ({ children }: StatelessQuickStatsProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" wrap>
|
||||
{children.map((stat, i) => (
|
||||
<EuiFlexItem key={`stat.${i}`} css={StatsItemStyle(euiTheme)}>
|
||||
{stat}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { type UseEuiTheme } from '@elastic/eui';
|
||||
|
||||
export const QuickStatsPanelStyle = (euiTheme: UseEuiTheme['euiTheme']) => css`
|
||||
background: ${euiTheme.colors.lightestShade};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const StatsGridContainerStyle = css`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
`;
|
||||
|
||||
export const StatsItemStyle = (euiTheme: UseEuiTheme['euiTheme']) => css`
|
||||
border: ${euiTheme.border.thin};
|
||||
min-width: 250px;
|
||||
`;
|
||||
|
||||
export const AliasesContentStyle = css`
|
||||
max-height: 100px;
|
||||
`;
|
|
@ -5,6 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { HealthStatus } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import type { IconColor } from '@elastic/eui';
|
||||
|
||||
// see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html for the current rules
|
||||
|
||||
export function isValidIndexName(name: string) {
|
||||
|
@ -44,3 +48,19 @@ export function getFirstNewIndexName(startingIndexNames: string[], currentIndexN
|
|||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export type HealthStatusStrings = 'red' | 'green' | 'yellow' | 'unavailable';
|
||||
export const healthColorsMap: Record<HealthStatusStrings, IconColor> = {
|
||||
red: 'danger',
|
||||
green: 'success',
|
||||
yellow: 'warning',
|
||||
unavailable: '',
|
||||
};
|
||||
|
||||
export const normalizeHealth = (health: HealthStatusStrings | HealthStatus): HealthStatusStrings =>
|
||||
health.toLowerCase() as HealthStatusStrings;
|
||||
export const indexHealthToHealthColor = (
|
||||
health: HealthStatus | 'unavailable' = 'unavailable'
|
||||
): IconColor => {
|
||||
return healthColorsMap[normalizeHealth(health)] ?? healthColorsMap.unavailable;
|
||||
};
|
||||
|
|
|
@ -50,12 +50,26 @@ export function SearchIndexDetailPageProvider({ getService }: FtrProviderContext
|
|||
'QuickStatsDocumentCount'
|
||||
);
|
||||
expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Document count\n0');
|
||||
expect(await quickStatsDocumentElem.getVisibleText()).not.to.contain('Index Size\n0b');
|
||||
expect(await quickStatsDocumentElem.getVisibleText()).not.to.contain('Total\n0');
|
||||
await quickStatsDocumentElem.click();
|
||||
expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Index Size\n227b');
|
||||
expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Total\n0\nDeleted\n0');
|
||||
},
|
||||
|
||||
async expectQuickStatsToHaveIndexStatus() {
|
||||
await testSubjects.existOrFail('QuickStatsIndexStatus');
|
||||
},
|
||||
|
||||
async expectQuickStatsToHaveIndexStorage(size?: string) {
|
||||
await testSubjects.existOrFail('QuickStatsStorage');
|
||||
if (!size) return;
|
||||
|
||||
const quickStatsElem = await testSubjects.find('quickStats');
|
||||
const quickStatsStorageElem = await quickStatsElem.findByTestSubject('QuickStatsStorage');
|
||||
expect(await quickStatsStorageElem.getVisibleText()).to.contain(`Storage\n${size}`);
|
||||
},
|
||||
|
||||
async expectQuickStatsToHaveDocumentCount(count: number) {
|
||||
await testSubjects.existOrFail('QuickStatsDocumentCount');
|
||||
const quickStatsElem = await testSubjects.find('quickStats');
|
||||
const quickStatsDocumentElem = await quickStatsElem.findByTestSubject(
|
||||
'QuickStatsDocumentCount'
|
||||
|
@ -65,6 +79,7 @@ export function SearchIndexDetailPageProvider({ getService }: FtrProviderContext
|
|||
|
||||
async expectQuickStatsAIMappings() {
|
||||
await testSubjects.existOrFail('quickStats', { timeout: 2000 });
|
||||
await testSubjects.existOrFail('QuickStatsAIMappings');
|
||||
const quickStatsElem = await testSubjects.find('quickStats');
|
||||
const quickStatsAIMappingsElem = await quickStatsElem.findByTestSubject(
|
||||
'QuickStatsAIMappings'
|
||||
|
|
|
@ -112,6 +112,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
|
||||
it('should have quick stats', async () => {
|
||||
await pageObjects.searchIndexDetailsPage.expectQuickStats();
|
||||
await pageObjects.searchIndexDetailsPage.expectQuickStatsToHaveIndexStatus();
|
||||
await pageObjects.searchIndexDetailsPage.expectQuickStatsToHaveIndexStorage('227b');
|
||||
await pageObjects.searchIndexDetailsPage.expectQuickStatsAIMappings();
|
||||
await es.indices.putMapping({
|
||||
index: indexName,
|
||||
|
@ -187,6 +189,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
it('should be able to delete document', async () => {
|
||||
await pageObjects.searchIndexDetailsPage.changeTab('dataTab');
|
||||
await pageObjects.searchIndexDetailsPage.clickFirstDocumentDeleteAction();
|
||||
|
||||
// re-open page to refresh queries for test (these will auto-refresh,
|
||||
// but waiting for that will make this test flakey)
|
||||
await pageObjects.searchNavigation.navigateToIndexDetailPage(indexName);
|
||||
await pageObjects.searchIndexDetailsPage.expectAddDocumentCodeExamples();
|
||||
await pageObjects.searchIndexDetailsPage.expectQuickStatsToHaveDocumentCount(0);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue