mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Search apps: Use the alias for the indices list (#162621)
## Summary This changes the search apps UI to use the alias instead of the list of indices that is stored in search apps. Tested with https://github.com/elastic/elasticsearch/pull/98036 Some of the aspects of the UI can be improved, I will follow up with @julianrosado with some ideas, but for now we are at least handling the case where the alias is missing without having the UIs crashing, by showing readable error messages. When the alias is missing: <img width="1265" alt="Screenshot 2023-07-31 at 17 02 46" src="2aa5d3da
-4dae-4eff-aff9-a38c221de673"> clicking on the list of indices when the alias is missing: <img width="1562" alt="Screenshot 2023-07-31 at 17 07 27" src="bb552833
-f64e-4fe7-84f3-e237d40fa79b"> search preview: <img width="1550" alt="Screenshot 2023-07-31 at 17 07 17" src="402c8667
-884d-4894-a7db-8cc1d9cf98b1"> content: <img width="1553" alt="Screenshot 2023-07-31 at 17 03 53" src="aa0a9261
-5cab-46f3-a87d-6fa2d87d4b8f"> field capabilities (unchanged): <img width="1557" alt="Screenshot 2023-07-31 at 17 04 00" src="9812561f
-53ec-497d-9b10-91c65271a503"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3efb83851f
commit
4d00959533
8 changed files with 120 additions and 36 deletions
|
@ -22,6 +22,7 @@ export enum ErrorCode {
|
|||
SEARCH_APPLICATION_ALREADY_EXISTS = 'search_application_already_exists',
|
||||
SEARCH_APPLICATION_NAME_INVALID = 'search_application_name_invalid',
|
||||
SEARCH_APPLICATION_NOT_FOUND = 'search_application_not_found',
|
||||
SEARCH_APPLICATION_ALIAS_NOT_FOUND = 'search_application_alias_not_found',
|
||||
UNAUTHORIZED = 'unauthorized',
|
||||
UNCAUGHT_EXCEPTION = 'uncaught_exception',
|
||||
}
|
||||
|
|
|
@ -247,8 +247,7 @@ export const SearchApplicationsList: React.FC<ListProps> = ({
|
|||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.searchApplications.list.searchBar.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Locate a search application via name or by its included indices.',
|
||||
defaultMessage: 'Locate a search application via name.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
|
||||
|
||||
export const fetchAliasIndices = async (client: IScopedClusterClient, aliasName: string) => {
|
||||
const aliasIndices = await client.asCurrentUser.indices.getAlias({
|
||||
name: aliasName,
|
||||
});
|
||||
|
||||
return Object.keys(aliasIndices).sort();
|
||||
};
|
|
@ -22,7 +22,7 @@ describe('search applications field_capabilities', () => {
|
|||
const mockClient = {
|
||||
asCurrentUser: {
|
||||
fieldCaps: jest.fn(),
|
||||
indices: { get: jest.fn() },
|
||||
indices: { get: jest.fn(), getAlias: jest.fn() },
|
||||
},
|
||||
asInternalUser: {},
|
||||
};
|
||||
|
@ -56,6 +56,11 @@ describe('search applications field_capabilities', () => {
|
|||
'index-001': { aliases: { unit_test_search_application: {} } },
|
||||
};
|
||||
|
||||
const getAliasIndicesResponse = {
|
||||
'index-001': { aliases: { unit_test_search_application: {} } },
|
||||
};
|
||||
|
||||
mockClient.asCurrentUser.indices.getAlias.mockResolvedValueOnce(getAliasIndicesResponse);
|
||||
mockClient.asCurrentUser.indices.get.mockResolvedValueOnce(getAllAvailableIndexResponse);
|
||||
mockClient.asCurrentUser.fieldCaps.mockResolvedValueOnce(fieldCapsResponse);
|
||||
|
||||
|
|
|
@ -15,12 +15,15 @@ import {
|
|||
} from '../../../common/types/search_applications';
|
||||
|
||||
import { availableIndices } from './available_indices';
|
||||
import { fetchAliasIndices } from './fetch_alias_indices';
|
||||
|
||||
export const fetchSearchApplicationFieldCapabilities = async (
|
||||
client: IScopedClusterClient,
|
||||
searchApplication: EnterpriseSearchApplication
|
||||
): Promise<EnterpriseSearchApplicationFieldCapabilities> => {
|
||||
const { name, updated_at_millis, indices } = searchApplication;
|
||||
const { name, updated_at_millis } = searchApplication;
|
||||
|
||||
const indices = await fetchAliasIndices(client, name);
|
||||
|
||||
const availableIndicesList = await availableIndices(client, indices);
|
||||
|
||||
|
|
|
@ -13,8 +13,13 @@ jest.mock('../../lib/search_applications/field_capabilities', () => ({
|
|||
jest.mock('../../lib/search_applications/fetch_indices_stats', () => ({
|
||||
fetchIndicesStats: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../lib/search_applications/fetch_alias_indices', () => ({
|
||||
fetchAliasIndices: jest.fn(),
|
||||
}));
|
||||
|
||||
import { RequestHandlerContext } from '@kbn/core/server';
|
||||
|
||||
import { fetchAliasIndices } from '../../lib/search_applications/fetch_alias_indices';
|
||||
import { fetchIndicesStats } from '../../lib/search_applications/fetch_indices_stats';
|
||||
import { fetchSearchApplicationFieldCapabilities } from '../../lib/search_applications/field_capabilities';
|
||||
|
||||
|
@ -49,13 +54,11 @@ describe('engines routes', () => {
|
|||
});
|
||||
|
||||
it('GET search applications API creates request', async () => {
|
||||
mockClient.asCurrentUser.searchApplication.list.mockImplementation(() => ({}));
|
||||
mockClient.asCurrentUser.searchApplication.list.mockImplementation(() => ({ results: [] }));
|
||||
const request = { query: {} };
|
||||
await mockRouter.callRoute({});
|
||||
await mockRouter.callRoute(request);
|
||||
expect(mockClient.asCurrentUser.searchApplication.list).toHaveBeenCalledWith(request.query);
|
||||
expect(mockRouter.response.ok).toHaveBeenCalledWith({
|
||||
body: {},
|
||||
});
|
||||
expect(mockRouter.response.ok).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('validates query parameters', () => {
|
||||
|
@ -105,32 +108,40 @@ describe('engines routes', () => {
|
|||
});
|
||||
|
||||
it('GET search application API creates request', async () => {
|
||||
mockClient.asCurrentUser.searchApplication.get.mockImplementation(() => ({}));
|
||||
await mockRouter.callRoute({
|
||||
params: { engine_name: 'engine-name' },
|
||||
});
|
||||
|
||||
expect(mockClient.asCurrentUser.searchApplication.get).toHaveBeenCalledWith({
|
||||
name: 'engine-name',
|
||||
});
|
||||
const mock = jest.fn();
|
||||
|
||||
const fetchIndicesStatsResponse = [
|
||||
{ count: 5, health: 'green', name: 'test-index-name-1' },
|
||||
{ count: 10, health: 'yellow', name: 'test-index-name-2' },
|
||||
{ count: 0, health: 'red', name: 'test-index-name-3' },
|
||||
];
|
||||
const mock = jest.fn();
|
||||
const fetchAliasIndicesResponse = mock([
|
||||
'test-index-name-1',
|
||||
'test-index-name-2',
|
||||
'test-index-name-3',
|
||||
]);
|
||||
|
||||
const engineResult = {
|
||||
indices: mock(['test-index-name-1', 'test-index-name-2', 'test-index-name-3']),
|
||||
name: 'test-engine-1',
|
||||
indices: fetchAliasIndicesResponse,
|
||||
name: 'engine-name',
|
||||
updated_at_millis: 1679847286355,
|
||||
};
|
||||
|
||||
(mockClient.asCurrentUser.searchApplication.get as jest.Mock).mockResolvedValueOnce(
|
||||
engineResult
|
||||
);
|
||||
|
||||
await mockRouter.callRoute({
|
||||
params: { engine_name: engineResult.name },
|
||||
});
|
||||
|
||||
(fetchAliasIndices as jest.Mock).mockResolvedValueOnce(fetchAliasIndicesResponse);
|
||||
expect(fetchAliasIndices).toHaveBeenCalledWith(mockClient, engineResult.name);
|
||||
|
||||
(fetchIndicesStats as jest.Mock).mockResolvedValueOnce(fetchIndicesStatsResponse);
|
||||
expect(fetchIndicesStats).toHaveBeenCalledWith(mockClient, engineResult.indices);
|
||||
expect(fetchIndicesStats).toHaveBeenCalledWith(mockClient, fetchAliasIndicesResponse);
|
||||
|
||||
expect(mockRouter.response.ok).toHaveBeenCalledWith({
|
||||
body: {},
|
||||
body: engineResult,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
EnterpriseSearchApplicationUpsertResponse,
|
||||
} from '../../../common/types/search_applications';
|
||||
import { createApiKey } from '../../lib/search_applications/create_api_key';
|
||||
import { fetchAliasIndices } from '../../lib/search_applications/fetch_alias_indices';
|
||||
import { fetchIndicesStats } from '../../lib/search_applications/fetch_indices_stats';
|
||||
|
||||
import { fetchSearchApplicationFieldCapabilities } from '../../lib/search_applications/field_capabilities';
|
||||
|
@ -24,6 +25,7 @@ import { createError } from '../../utils/create_error';
|
|||
import { elasticsearchErrorHandler } from '../../utils/elasticsearch_error_handler';
|
||||
import {
|
||||
isInvalidSearchApplicationNameException,
|
||||
isMissingAliasException,
|
||||
isNotFoundException,
|
||||
isVersionConflictEngineException,
|
||||
} from '../../utils/identify_exceptions';
|
||||
|
@ -46,6 +48,19 @@ export function registerSearchApplicationsRoutes({ log, router }: RouteDependenc
|
|||
request.query
|
||||
)) as EnterpriseSearchApplicationsResponse;
|
||||
|
||||
await Promise.all(
|
||||
engines.results.map(async (searchApp) => {
|
||||
try {
|
||||
searchApp.indices = await fetchAliasIndices(client, searchApp.name);
|
||||
} catch (error) {
|
||||
if (isMissingAliasException(error)) {
|
||||
searchApp.indices = [];
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
return response.ok({ body: engines });
|
||||
})
|
||||
);
|
||||
|
@ -64,7 +79,18 @@ export function registerSearchApplicationsRoutes({ log, router }: RouteDependenc
|
|||
const engine = (await client.asCurrentUser.searchApplication.get({
|
||||
name: request.params.engine_name,
|
||||
})) as EnterpriseSearchApplication;
|
||||
const indicesStats = await fetchIndicesStats(client, engine.indices);
|
||||
|
||||
let indices: string[];
|
||||
try {
|
||||
indices = await fetchAliasIndices(client, engine.name);
|
||||
} catch (error) {
|
||||
if (isMissingAliasException(error)) {
|
||||
indices = [];
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const indicesStats = await fetchIndicesStats(client, indices);
|
||||
|
||||
return response.ok({ body: { ...engine, indices: indicesStats } });
|
||||
})
|
||||
|
@ -215,20 +241,15 @@ export function registerSearchApplicationsRoutes({ log, router }: RouteDependenc
|
|||
validate: { params: schema.object({ engine_name: schema.string() }) },
|
||||
},
|
||||
elasticsearchErrorHandler(log, async (context, request, response) => {
|
||||
try {
|
||||
const { client } = (await context.core).elasticsearch;
|
||||
const { client } = (await context.core).elasticsearch;
|
||||
|
||||
const engine = (await client.asCurrentUser.searchApplication.get({
|
||||
let engine;
|
||||
try {
|
||||
engine = (await client.asCurrentUser.searchApplication.get({
|
||||
name: request.params.engine_name,
|
||||
})) as EnterpriseSearchApplication;
|
||||
|
||||
const data = await fetchSearchApplicationFieldCapabilities(client, engine);
|
||||
return response.ok({
|
||||
body: data,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
} catch (e) {
|
||||
if (isNotFoundException(e)) {
|
||||
} catch (error) {
|
||||
if (isNotFoundException(error)) {
|
||||
return createError({
|
||||
errorCode: ErrorCode.SEARCH_APPLICATION_NOT_FOUND,
|
||||
message: i18n.translate(
|
||||
|
@ -239,7 +260,28 @@ export function registerSearchApplicationsRoutes({ log, router }: RouteDependenc
|
|||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetchSearchApplicationFieldCapabilities(client, engine);
|
||||
return response.ok({
|
||||
body: data,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
if (isMissingAliasException(error)) {
|
||||
return createError({
|
||||
errorCode: ErrorCode.SEARCH_APPLICATION_ALIAS_NOT_FOUND,
|
||||
message: i18n.translate(
|
||||
'xpack.enterpriseSearch.server.routes.fetchSearchApplicationFieldCapabilities.missingAliasError',
|
||||
{ defaultMessage: 'Search application alias is missing.' }
|
||||
),
|
||||
response,
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -19,6 +19,8 @@ export interface ElasticsearchResponseError {
|
|||
name: 'ResponseError';
|
||||
}
|
||||
|
||||
const MISSING_ALIAS_ERROR = new RegExp(/^alias \[.+\] missing/);
|
||||
|
||||
export const isIndexNotFoundException = (error: ElasticsearchResponseError) =>
|
||||
error?.meta?.body?.error?.type === 'index_not_found_exception';
|
||||
|
||||
|
@ -45,3 +47,8 @@ export const isVersionConflictEngineException = (error: ElasticsearchResponseErr
|
|||
|
||||
export const isInvalidSearchApplicationNameException = (error: ElasticsearchResponseError) =>
|
||||
error.meta?.body?.error?.type === 'invalid_alias_name_exception';
|
||||
|
||||
export const isMissingAliasException = (error: ElasticsearchResponseError) =>
|
||||
error.meta?.statusCode === 404 &&
|
||||
typeof error.meta?.body?.error === 'string' &&
|
||||
MISSING_ALIAS_ERROR.test(error.meta?.body?.error);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue