[Enterprise Search] Show warnings when an index in an engine has unknown health (#150627)

## Summary

- Alert icon and warning colors for indices count on overview page
- Warning callout on indices page
- Unknown index row rendering changes
    - `N/A` document count
    - No view index action
    - Disabled link for view index

Open question: Is there a good way to render a row as "disabled?"

<img width="1194" alt="Screenshot 2023-02-08 at 2 30 57 PM"
src="https://user-images.githubusercontent.com/1699281/217632438-249d77b3-5cbe-492c-af2c-d7f9a66b298b.png">


<img width="1000" alt="image"
src="https://user-images.githubusercontent.com/1699281/217635524-4465a062-3fee-491c-b201-d87ab5ed5160.png">


### 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] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] 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))
- [x] 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))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Sloane Perrault 2023-02-10 15:21:14 -05:00 committed by GitHub
parent 7d3b0c369b
commit 6ad4a44917
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 492 additions and 41 deletions

View file

@ -10,13 +10,16 @@ import React, { useState } from 'react';
import { useActions, useValues } from 'kea';
import {
EuiTableActionsColumnType,
EuiBasicTableColumn,
EuiButton,
EuiCallOut,
EuiConfirmModal,
EuiIcon,
EuiInMemoryTable,
EuiSpacer,
EuiTableActionsColumnType,
EuiText,
useEuiBackgroundColor,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -39,6 +42,7 @@ import { AddIndicesFlyout } from './add_indices_flyout';
import { EngineIndicesLogic } from './engine_indices_logic';
export const EngineIndices: React.FC = () => {
const subduedBackground = useEuiBackgroundColor('subdued');
const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic);
const { engineData, engineName, isLoadingEngine, addIndicesFlyoutOpen } =
useValues(EngineIndicesLogic);
@ -50,6 +54,8 @@ export const EngineIndices: React.FC = () => {
if (!engineData) return null;
const { indices } = engineData;
const hasUnknownIndices = indices.some(({ health }) => health === 'unknown');
const removeIndexAction: EuiTableActionsColumnType<EnterpriseSearchEngineIndex>['actions'][0] = {
color: 'danger',
'data-test-subj': 'engine-remove-index-btn',
@ -77,21 +83,24 @@ export const EngineIndices: React.FC = () => {
},
type: 'icon',
};
const columns: Array<EuiBasicTableColumn<EnterpriseSearchEngineIndex>> = [
{
field: 'name',
name: i18n.translate('xpack.enterpriseSearch.content.engine.indices.name.columnTitle', {
defaultMessage: 'Index name',
}),
render: (name: string) => (
<EuiLinkTo
data-test-subj="engine-index-link"
to={generateEncodedPath(SEARCH_INDEX_PATH, { indexName: name })}
>
{name}
</EuiLinkTo>
),
sortable: true,
render: ({ health, name }: EnterpriseSearchEngineIndex) =>
health === 'unknown' ? (
name
) : (
<EuiLinkTo
data-test-subj="engine-index-link"
to={generateEncodedPath(SEARCH_INDEX_PATH, { indexName: name })}
>
{name}
</EuiLinkTo>
),
sortable: ({ name }: EnterpriseSearchEngineIndex) => name,
truncateText: true,
width: '40%',
},
@ -115,6 +124,13 @@ export const EngineIndices: React.FC = () => {
name: i18n.translate('xpack.enterpriseSearch.content.engine.indices.docsCount.columnTitle', {
defaultMessage: 'Docs count',
}),
render: (count: number | null) =>
count === null
? i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.docsCount.notAvailableLabel',
{ defaultMessage: 'N/A' }
)
: count,
sortable: true,
truncateText: true,
width: '15%',
@ -136,6 +152,7 @@ export const EngineIndices: React.FC = () => {
{
actions: [
{
available: (index) => index.health !== 'unknown',
'data-test-subj': 'engine-view-index-btn',
description: i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.actions.viewIndex.title',
@ -197,9 +214,39 @@ export const EngineIndices: React.FC = () => {
engineName={engineName}
>
<>
{hasUnknownIndices && (
<>
<EuiCallOut
color="warning"
iconType="alert"
title={i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.unknownIndicesCallout.title',
{ defaultMessage: 'Some of your indices are unavailable.' }
)}
>
<p>
{i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.unknownIndicesCallout.description',
{
defaultMessage:
'Some data might be unreachable from this engine. Check for any pending operations or errors on affected indices, or remove those that should no longer be used by this engine.',
}
)}
</p>
</EuiCallOut>
<EuiSpacer />
</>
)}
<EuiInMemoryTable
items={indices}
columns={columns}
rowProps={(index: EnterpriseSearchEngineIndex) => {
if (index.health === 'unknown') {
return { style: { backgroundColor: subduedBackground } };
}
return {};
}}
search={{
box: {
incremental: true,

View file

@ -9,7 +9,7 @@ import React from 'react';
import { useValues } from 'kea';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiStat } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiStat, useEuiTheme } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { generateEncodedPath } from '../../../shared/encode_path_params';
@ -21,8 +21,17 @@ import { EngineOverviewLogic } from './engine_overview_logic';
import { EngineViewHeaderActions } from './engine_view_header_actions';
export const EngineOverview: React.FC = () => {
const { engineName, indicesCount, documentsCount, fieldsCount, isLoadingEngine } =
useValues(EngineOverviewLogic);
const {
euiTheme: { colors: colors },
} = useEuiTheme();
const {
documentsCount,
engineName,
fieldsCount,
hasUnknownIndices,
indicesCount,
isLoadingEngine,
} = useValues(EngineOverviewLogic);
return (
<EnterpriseSearchEnginesPageTemplate
@ -49,7 +58,11 @@ export const EngineOverview: React.FC = () => {
color="text"
>
<EuiFlexGroup alignItems="center">
<EuiIcon size="xxl" type="visTable" color="#98A2B3" />
{hasUnknownIndices ? (
<EuiIcon size="xxl" type="alert" color={colors.warning} />
) : (
<EuiIcon size="xxl" type="visTable" color={colors.mediumShade} />
)}
<EuiStat
titleSize="l"
isLoading={isLoadingEngine}
@ -58,14 +71,14 @@ export const EngineOverview: React.FC = () => {
'xpack.enterpriseSearch.content.engine.overview.indicesDescription',
{ defaultMessage: 'Indices' }
)}
titleColor="primary"
titleColor={hasUnknownIndices ? colors.warningText : 'primary'}
/>
</EuiFlexGroup>
</EuiLinkTo>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup alignItems="center">
<EuiIcon size="xxl" type="documents" color="#98A2B3" />
<EuiIcon size="xxl" type="documents" color={colors.mediumShade} />
<EuiStat
titleSize="l"
isLoading={isLoadingEngine}
@ -87,7 +100,7 @@ export const EngineOverview: React.FC = () => {
color="text"
>
<EuiFlexGroup alignItems="center">
<EuiIcon size="xxl" type="documents" color="#98A2B3" />
<EuiIcon size="xxl" type="documents" color={colors.mediumShade} />
<EuiStat
titleSize="l"
isLoading={false}

View file

@ -8,8 +8,17 @@
import { LogicMounter } from '../../../__mocks__/kea_logic';
import { Status } from '../../../../../common/types/api';
import { EnterpriseSearchEngineIndex } from '../../../../../common/types/engines';
import { EngineOverviewLogic, EngineOverviewValues } from './engine_overview_logic';
import {
EngineOverviewLogic,
EngineOverviewValues,
selectDocumentsCount,
selectFieldsCount,
selectHasUnknownIndices,
selectIndices,
selectIndicesCount,
} from './engine_overview_logic';
const DEFAULT_VALUES: EngineOverviewValues = {
documentsCount: 0,
@ -18,6 +27,7 @@ const DEFAULT_VALUES: EngineOverviewValues = {
engineFieldCapabilitiesData: undefined,
engineName: '',
fieldsCount: 0,
hasUnknownIndices: false,
indices: [],
indicesCount: 0,
isLoadingEngine: true,
@ -36,4 +46,379 @@ describe('EngineOverviewLogic', () => {
it('has expected default values', () => {
expect(EngineOverviewLogic.values).toEqual(DEFAULT_VALUES);
});
describe('listeners', () => {
describe('setEngineName', () => {
it('refetches the engine field capabilities', () => {
jest.spyOn(EngineOverviewLogic.actions, 'fetchEngineFieldCapabilities');
EngineOverviewLogic.actions.setEngineName('foobar');
expect(EngineOverviewLogic.actions.fetchEngineFieldCapabilities).toHaveBeenCalledTimes(1);
expect(EngineOverviewLogic.actions.fetchEngineFieldCapabilities).toHaveBeenCalledWith({
engineName: 'foobar',
});
});
});
});
describe('selectors', () => {
describe('indices', () => {
it('is defined', () => {
expect(selectIndices).toBeDefined();
});
it('returns an empty array before engineData is loaded', () => {
expect(selectIndices(undefined)).toEqual([]);
});
it('returns the array of indices', () => {
const indices = [
{
count: 10,
health: 'green',
name: 'index-001',
source: 'api',
},
{
count: 10,
health: 'green',
name: 'index-002',
source: 'api',
},
];
const engineData = {
created: '2023-02-07T19:16:43Z',
indices,
name: 'foo-engine',
updated: '2023-02-07T19:16:43Z',
} as EngineOverviewValues['engineData'];
expect(selectIndices(engineData)).toBe(indices);
});
});
describe('indicesCount', () => {
it('is defined', () => {
expect(selectIndicesCount).toBeDefined();
});
it('returns the number of indices', () => {
const noIndices: EnterpriseSearchEngineIndex[] = [];
const oneIndex = [
{ count: 23, health: 'unknown', name: 'index-001', source: 'api' },
] as EnterpriseSearchEngineIndex[];
const twoIndices = [
{ count: 23, health: 'unknown', name: 'index-001', source: 'api' },
{ count: 92, health: 'unknown', name: 'index-002', source: 'api' },
] as EnterpriseSearchEngineIndex[];
expect(selectIndicesCount(noIndices)).toBe(0);
expect(selectIndicesCount(oneIndex)).toBe(1);
expect(selectIndicesCount(twoIndices)).toBe(2);
});
});
describe('hasUnknownIndices', () => {
it('is defined', () => {
expect(selectHasUnknownIndices).toBeDefined();
});
describe('no indices', () => {
const indices: EnterpriseSearchEngineIndex[] = [];
it('returns false', () => {
expect(selectHasUnknownIndices(indices)).toBe(false);
});
});
describe('all indices unknown', () => {
const indices = [
{
count: 12,
health: 'unknown',
name: 'index-001',
source: 'api',
},
{
count: 34,
health: 'unknown',
name: 'index-002',
source: 'crawler',
},
{
count: 56,
health: 'unknown',
name: 'index-003',
source: 'api',
},
] as EnterpriseSearchEngineIndex[];
it('returns true', () => {
expect(selectHasUnknownIndices(indices)).toBe(true);
});
});
describe('one index unknown', () => {
const indices = [
{
count: 12,
health: 'unknown',
name: 'index-001',
source: 'api',
},
{
count: 34,
health: 'yellow',
name: 'index-002',
source: 'crawler',
},
{
count: 56,
health: 'green',
name: 'index-003',
source: 'api',
},
] as EnterpriseSearchEngineIndex[];
it('returns true', () => {
expect(selectHasUnknownIndices(indices)).toBe(true);
});
});
describe('multiple but not all indices unknown', () => {
const indices = [
{
count: 12,
health: 'unknown',
name: 'index-001',
source: 'api',
},
{
count: 34,
health: 'yellow',
name: 'index-002',
source: 'crawler',
},
{
count: 56,
health: 'unknown',
name: 'index-003',
source: 'api',
},
] as EnterpriseSearchEngineIndex[];
it('returns true', () => {
expect(selectHasUnknownIndices(indices)).toBe(true);
});
});
describe('no indices unknown', () => {
const indices = [
{
count: 12,
health: 'green',
name: 'index-001',
source: 'api',
},
{
count: 34,
health: 'yellow',
name: 'index-002',
source: 'crawler',
},
{
count: 56,
health: 'green',
name: 'index-003',
source: 'api',
},
] as EnterpriseSearchEngineIndex[];
it('returns false', () => {
expect(selectHasUnknownIndices(indices)).toBe(false);
});
});
});
describe('documentsCount', () => {
it('is defined', () => {
expect(selectDocumentsCount).toBeDefined();
});
it('returns 0 for no indices', () => {
expect(selectDocumentsCount([])).toBe(0);
});
it('returns the `count` for a single index', () => {
expect(
selectDocumentsCount([
{
count: 23,
health: 'green',
name: 'index-001',
source: 'crawler',
},
] as EnterpriseSearchEngineIndex[])
).toBe(23);
});
it('returns the sum of all `count`', () => {
expect(
selectDocumentsCount([
{
count: 23,
health: 'green',
name: 'index-001',
source: 'crawler',
},
{
count: 45,
health: 'green',
name: 'index-002',
source: 'crawler',
},
] as EnterpriseSearchEngineIndex[])
).toBe(68);
});
it('does not count indices without a `count`', () => {
expect(
selectDocumentsCount([
{
count: 23,
health: 'green',
name: 'index-001',
source: 'crawler',
},
{
count: null,
health: 'unknown',
name: 'index-002',
source: 'crawler',
},
{
count: 45,
health: 'green',
name: 'index-002',
source: 'crawler',
},
] as EnterpriseSearchEngineIndex[])
).toBe(68);
});
});
describe('fieldsCount', () => {
it('is defined', () => {
expect(selectFieldsCount).toBeDefined();
});
it('counts the fields from the field capabilities', () => {
const fieldCapabilities = {
created: '2023-02-07T19:16:43Z',
field_capabilities: {
fields: {
age: {
integer: {
aggregatable: true,
metadata_field: false,
searchable: true,
type: 'integer',
},
},
color: {
keyword: {
aggregatable: true,
metadata_field: false,
searchable: true,
type: 'keyword',
},
},
name: {
text: {
aggregatable: false,
metadata_field: false,
searchable: true,
type: 'text',
},
},
},
indices: ['index-001', 'index-002'],
},
name: 'engine-001',
updated: '2023-02-07T19:16:43Z',
};
expect(selectFieldsCount(fieldCapabilities)).toBe(3);
});
it('excludes metadata fields from the count', () => {
const fieldCapabilities = {
created: '2023-02-07T19:16:43Z',
field_capabilities: {
fields: {
_doc_count: {
integer: {
aggregatable: false,
metadata_field: true,
searchable: false,
type: 'integer',
},
},
_id: {
_id: {
aggregatable: false,
metadata_field: true,
searchable: true,
type: '_id',
},
},
_index: {
_index: {
aggregatable: true,
metadata_field: true,
searchable: true,
type: '_index',
},
},
_source: {
_source: {
aggregatable: false,
metadata_field: true,
searchable: false,
type: '_source',
},
},
_version: {
_version: {
aggregatable: true,
metadata_field: true,
searchable: false,
type: '_version',
},
},
age: {
integer: {
aggregatable: true,
metadata_field: false,
searchable: true,
type: 'integer',
},
},
color: {
keyword: {
aggregatable: true,
metadata_field: false,
searchable: true,
type: 'keyword',
},
},
name: {
text: {
aggregatable: false,
metadata_field: false,
searchable: true,
type: 'text',
},
},
},
indices: ['index-001', 'index-002'],
},
name: 'foo-engine',
updated: '2023-02-07T19:16:43Z',
};
expect(selectFieldsCount(fieldCapabilities)).toBe(3);
});
it('returns 0 when field capability data is not available', () => {
expect(selectFieldsCount(undefined)).toBe(0);
});
});
});
});

View file

@ -17,6 +17,7 @@ import { EngineViewLogic } from './engine_view_logic';
export interface EngineOverviewActions {
fetchEngineFieldCapabilities: typeof FetchEngineFieldCapabilitiesApiLogic.actions.makeRequest;
setEngineName: typeof EngineNameLogic.actions.setEngineName;
}
export interface EngineOverviewValues {
documentsCount: number;
@ -25,11 +26,30 @@ export interface EngineOverviewValues {
engineFieldCapabilitiesData: typeof FetchEngineFieldCapabilitiesApiLogic.values.data;
engineName: typeof EngineNameLogic.values.engineName;
fieldsCount: number;
hasUnknownIndices: boolean;
indices: EnterpriseSearchEngineIndex[];
indicesCount: number;
isLoadingEngine: typeof EngineViewLogic.values.isLoadingEngine;
}
export const selectIndices = (engineData: EngineOverviewValues['engineData']) =>
engineData?.indices ?? [];
export const selectIndicesCount = (indices: EngineOverviewValues['indices']) => indices.length;
export const selectHasUnknownIndices = (indices: EngineOverviewValues['indices']) =>
indices.some(({ health }) => health === 'unknown');
export const selectDocumentsCount = (indices: EngineOverviewValues['indices']) =>
indices.reduce((sum, { count }) => sum + count, 0);
export const selectFieldsCount = (
engineFieldCapabilitiesData: EngineOverviewValues['engineFieldCapabilitiesData']
) =>
Object.values(engineFieldCapabilitiesData?.field_capabilities?.fields ?? {}).filter(
(value) => !Object.values(value).some((field) => !!field.metadata_field)
).length;
export const EngineOverviewLogic = kea<MakeLogicType<EngineOverviewValues, EngineOverviewActions>>({
actions: {},
connect: {
@ -57,33 +77,19 @@ export const EngineOverviewLogic = kea<MakeLogicType<EngineOverviewValues, Engin
}
},
}),
listeners: ({ actions }) => ({
setEngineName: ({ engineName }) => {
listeners: ({ actions, values }) => ({
setEngineName: () => {
const { engineName } = values;
actions.fetchEngineFieldCapabilities({ engineName });
},
}),
path: ['enterprise_search', 'content', 'engine_overview_logic'],
reducers: {},
selectors: ({ selectors }) => ({
documentsCount: [
() => [selectors.indices],
(indices: EngineOverviewValues['indices']) =>
indices.reduce((sum, { count }) => sum + count, 0),
],
fieldsCount: [
() => [selectors.engineFieldCapabilitiesData],
(engineFieldCapabilitiesData: EngineOverviewValues['engineFieldCapabilitiesData']) =>
Object.values(engineFieldCapabilitiesData?.field_capabilities?.fields ?? {}).filter(
(value) => !Object.values(value).some((field) => !!field.metadata_field)
).length,
],
indices: [
() => [selectors.engineData],
(engineData: EngineOverviewValues['engineData']) => engineData?.indices ?? [],
],
indicesCount: [
() => [selectors.indices],
(indices: EngineOverviewValues['indices']) => indices.length,
],
documentsCount: [() => [selectors.indices], selectDocumentsCount],
fieldsCount: [() => [selectors.engineFieldCapabilitiesData], selectFieldsCount],
hasUnknownIndices: [() => [selectors.indices], selectHasUnknownIndices],
indices: [() => [selectors.engineData], selectIndices],
indicesCount: [() => [selectors.indices], selectIndicesCount],
}),
});