From cd7f26dd81f6b2899e911fb6e24ab95f02f886f9 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 20 Aug 2021 17:58:34 +0200 Subject: [PATCH] [IndexPatterns] No data experience to handle default Fleet assets (#108887) Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com> --- ...ndexpatternsservice.hasuserindexpattern.md | 17 ++ ...lugins-data-public.indexpatternsservice.md | 1 + ...ndexpatternsservice.hasuserindexpattern.md | 17 ++ ...lugins-data-server.indexpatternsservice.md | 1 + .../data/common/index_patterns/constants.ts | 14 ++ .../ensure_default_index_pattern.ts | 5 +- .../index_patterns/index_patterns.ts | 7 + .../data/common/index_patterns/types.ts | 1 + .../index_patterns_api_client.ts | 7 +- src/plugins/data/public/public.api.md | 1 + .../has_user_index_pattern.test.ts | 174 ++++++++++++++++++ .../index_patterns/has_user_index_pattern.ts | 62 +++++++ .../index_patterns_api_client.ts | 15 +- .../index_patterns/index_patterns_service.ts | 2 +- .../data/server/index_patterns/routes.ts | 2 + .../routes/has_user_index_pattern.ts | 40 ++++ src/plugins/data/server/server.api.md | 1 + .../empty_prompts/empty_prompts.test.tsx | 98 ++++++++++ .../empty_prompts/empty_prompts.tsx | 34 ++-- .../index_pattern_editor_flyout_content.tsx | 7 +- .../index_pattern_table.tsx | 5 +- .../public/components/overview/overview.tsx | 4 +- .../has_user_index_pattern.ts | 139 ++++++++++++++ .../has_user_index_pattern/index.ts | 15 ++ .../apis/index_patterns/index.js | 1 + 25 files changed, 644 insertions(+), 26 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.hasuserindexpattern.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.hasuserindexpattern.md create mode 100644 src/plugins/data/server/index_patterns/has_user_index_pattern.test.ts create mode 100644 src/plugins/data/server/index_patterns/has_user_index_pattern.ts create mode 100644 src/plugins/data/server/index_patterns/routes/has_user_index_pattern.ts create mode 100644 src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.test.tsx create mode 100644 test/api_integration/apis/index_patterns/has_user_index_pattern/has_user_index_pattern.ts create mode 100644 test/api_integration/apis/index_patterns/has_user_index_pattern/index.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.hasuserindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.hasuserindexpattern.md new file mode 100644 index 000000000000..31d1b9b9c16a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.hasuserindexpattern.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [hasUserIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.hasuserindexpattern.md) + +## IndexPatternsService.hasUserIndexPattern() method + +Checks if current user has a user created index pattern ignoring fleet's server default index patterns + +Signature: + +```typescript +hasUserIndexPattern(): Promise; +``` +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md index 572a12206686..7b3ad2a379c8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -45,5 +45,6 @@ export declare class IndexPatternsService | [createAndSave(spec, override, skipFetchFields)](./kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md) | | Create a new index pattern and save it right away | | [createSavedObject(indexPattern, override)](./kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md) | | Save a new index pattern | | [delete(indexPatternId)](./kibana-plugin-plugins-data-public.indexpatternsservice.delete.md) | | Deletes an index pattern from .kibana index | +| [hasUserIndexPattern()](./kibana-plugin-plugins-data-public.indexpatternsservice.hasuserindexpattern.md) | | Checks if current user has a user created index pattern ignoring fleet's server default index patterns | | [updateSavedObject(indexPattern, saveAttempts, ignoreErrors)](./kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md) | | Save existing index pattern. Will attempt to merge differences if there are conflicts | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.hasuserindexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.hasuserindexpattern.md new file mode 100644 index 000000000000..49f365c10604 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.hasuserindexpattern.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [hasUserIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.hasuserindexpattern.md) + +## IndexPatternsService.hasUserIndexPattern() method + +Checks if current user has a user created index pattern ignoring fleet's server default index patterns + +Signature: + +```typescript +hasUserIndexPattern(): Promise; +``` +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md index 64c46fe4abbd..65997e0688b7 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md @@ -45,5 +45,6 @@ export declare class IndexPatternsService | [createAndSave(spec, override, skipFetchFields)](./kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md) | | Create a new index pattern and save it right away | | [createSavedObject(indexPattern, override)](./kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md) | | Save a new index pattern | | [delete(indexPatternId)](./kibana-plugin-plugins-data-server.indexpatternsservice.delete.md) | | Deletes an index pattern from .kibana index | +| [hasUserIndexPattern()](./kibana-plugin-plugins-data-server.indexpatternsservice.hasuserindexpattern.md) | | Checks if current user has a user created index pattern ignoring fleet's server default index patterns | | [updateSavedObject(indexPattern, saveAttempts, ignoreErrors)](./kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md) | | Save existing index pattern. Will attempt to merge differences if there are conflicts | diff --git a/src/plugins/data/common/index_patterns/constants.ts b/src/plugins/data/common/index_patterns/constants.ts index d508a62422fc..67e266dbd84a 100644 --- a/src/plugins/data/common/index_patterns/constants.ts +++ b/src/plugins/data/common/index_patterns/constants.ts @@ -15,3 +15,17 @@ export const RUNTIME_FIELD_TYPES = [ 'boolean', 'geo_point', ] as const; + +/** + * Used to determine if the instance has any user created index patterns by filtering index patterns + * that are created and backed only by Fleet server data + * Should be revised after https://github.com/elastic/kibana/issues/82851 is fixed + * For more background see: https://github.com/elastic/kibana/issues/107020 + */ +export const FLEET_ASSETS_TO_IGNORE = { + LOGS_INDEX_PATTERN: 'logs-*', + METRICS_INDEX_PATTERN: 'metrics-*', + LOGS_DATA_STREAM_TO_IGNORE: 'logs-elastic_agent', // ignore ds created by Fleet server itself + METRICS_DATA_STREAM_TO_IGNORE: 'metrics-elastic_agent', // ignore ds created by Fleet server itself + METRICS_ENDPOINT_INDEX_TO_IGNORE: 'metrics-endpoint.metadata_current_default', // ignore index created by Fleet endpoint package installed by default in Cloud +}; diff --git a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts index 492c82a053c0..61ec1c5a4c09 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts @@ -35,8 +35,9 @@ export const createEnsureDefaultIndexPattern = ( return; } - // If there is any index pattern created, set the first as default - if (patterns.length >= 1) { + // If there is any user index pattern created, set the first as default + // if there is 0 patterns, then don't even call `hasUserIndexPattern()` + if (patterns.length >= 1 && (await this.hasUserIndexPattern().catch(() => true))) { defaultId = patterns[0]; await uiSettings.set('defaultIndex', defaultId); } else { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index d20cfc98ba05..74f11badbb41 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -232,6 +232,13 @@ export class IndexPatternsService { } }; + /** + * Checks if current user has a user created index pattern ignoring fleet's server default index patterns + */ + async hasUserIndexPattern(): Promise { + return this.apiClient.hasUserIndexPattern(); + } + /** * Get field list by providing { pattern } * @param options diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 0e088d7aa8a8..c79dc17e9fe8 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -136,6 +136,7 @@ export interface GetFieldsOptionsTimePattern { export interface IIndexPatternsApiClient { getFieldsForTimePattern: (options: GetFieldsOptionsTimePattern) => Promise; getFieldsForWildcard: (options: GetFieldsOptions) => Promise; + hasUserIndexPattern: () => Promise; } export type { SavedObject }; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts index 485f0079a018..d4e8e0624511 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts @@ -23,7 +23,7 @@ export class IndexPatternsApiClient implements IIndexPatternsApiClient { this.http = http; } - private _request(url: string, query: any) { + private _request(url: string, query?: any) { return this.http .fetch(url, { query, @@ -62,4 +62,9 @@ export class IndexPatternsApiClient implements IIndexPatternsApiClient { allow_no_index: allowNoIndex, }).then((resp: any) => resp.fields || []); } + + async hasUserIndexPattern(): Promise { + const response = await this._request(this._getUrl(['has_user_index_pattern'])); + return response.result; + } } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 9dd7dff9e5b6..c0f43ff95c29 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1479,6 +1479,7 @@ export class IndexPatternsService { getIds: (refresh?: boolean) => Promise; getIdsWithTitle: (refresh?: boolean) => Promise; getTitles: (refresh?: boolean) => Promise; + hasUserIndexPattern(): Promise; refreshFields: (indexPattern: IndexPattern) => Promise; savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; setDefault: (id: string | null, force?: boolean) => Promise; diff --git a/src/plugins/data/server/index_patterns/has_user_index_pattern.test.ts b/src/plugins/data/server/index_patterns/has_user_index_pattern.test.ts new file mode 100644 index 000000000000..efc149b40937 --- /dev/null +++ b/src/plugins/data/server/index_patterns/has_user_index_pattern.test.ts @@ -0,0 +1,174 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { hasUserIndexPattern } from './has_user_index_pattern'; +import { elasticsearchServiceMock, savedObjectsClientMock } from '../../../../core/server/mocks'; + +describe('hasUserIndexPattern', () => { + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; + const soClient = savedObjectsClientMock.create(); + + beforeEach(() => jest.resetAllMocks()); + + it('returns false when there are no index patterns', async () => { + soClient.find.mockResolvedValue({ + page: 1, + per_page: 100, + total: 0, + saved_objects: [], + }); + expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(false); + }); + + it('returns true when there are any index patterns other than metrics-* or logs-*', async () => { + soClient.find.mockResolvedValue({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + id: '1', + references: [], + type: 'index-pattern', + score: 99, + attributes: { title: 'my-pattern-*' }, + }, + ], + }); + expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(true); + }); + + describe('when only metrics-* and logs-* index patterns exist', () => { + beforeEach(() => { + soClient.find.mockResolvedValue({ + page: 1, + per_page: 100, + total: 2, + saved_objects: [ + { + id: '1', + references: [], + type: 'index-pattern', + score: 99, + attributes: { title: 'metrics-*' }, + }, + { + id: '2', + references: [], + type: 'index-pattern', + score: 99, + attributes: { title: 'logs-*' }, + }, + ], + }); + }); + + it('calls indices.resolveIndex for the index patterns', async () => { + esClient.indices.resolveIndex.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + indices: [], + data_streams: [], + aliases: [], + }) + ); + await hasUserIndexPattern({ esClient, soClient }); + expect(esClient.indices.resolveIndex).toHaveBeenCalledWith({ + name: 'logs-*,metrics-*', + }); + }); + + it('returns false if no logs or metrics data_streams exist', async () => { + esClient.indices.resolveIndex.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + indices: [], + data_streams: [], + aliases: [], + }) + ); + expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(false); + }); + + it('returns true if any index exists', async () => { + esClient.indices.resolveIndex.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + indices: [{ name: 'logs', attributes: [] }], + data_streams: [], + aliases: [], + }) + ); + expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(true); + }); + + it('returns false if only metrics-elastic_agent data stream exists', async () => { + esClient.indices.resolveIndex.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + indices: [], + data_streams: [ + { + name: 'metrics-elastic_agent', + timestamp_field: '@timestamp', + backing_indices: ['.ds-metrics-elastic_agent'], + }, + ], + aliases: [], + }) + ); + expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(false); + }); + + it('returns false if only logs-elastic_agent data stream exists', async () => { + esClient.indices.resolveIndex.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + indices: [], + data_streams: [ + { + name: 'logs-elastic_agent', + timestamp_field: '@timestamp', + backing_indices: ['.ds-logs-elastic_agent'], + }, + ], + aliases: [], + }) + ); + expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(false); + }); + + it('returns false if only metrics-endpoint.metadata_current_default index exists', async () => { + esClient.indices.resolveIndex.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + indices: [ + { + name: 'metrics-endpoint.metadata_current_default', + attributes: ['open'], + }, + ], + aliases: [], + data_streams: [], + }) + ); + expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(false); + }); + + it('returns true if any other data stream exists', async () => { + esClient.indices.resolveIndex.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + indices: [], + data_streams: [ + { + name: 'other', + timestamp_field: '@timestamp', + backing_indices: ['.ds-other'], + }, + ], + aliases: [], + }) + ); + expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(true); + }); + }); +}); diff --git a/src/plugins/data/server/index_patterns/has_user_index_pattern.ts b/src/plugins/data/server/index_patterns/has_user_index_pattern.ts new file mode 100644 index 000000000000..b65983a7fd5d --- /dev/null +++ b/src/plugins/data/server/index_patterns/has_user_index_pattern.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient, SavedObjectsClientContract } from '../../../../core/server'; +import { IndexPatternSavedObjectAttrs } from '../../common/index_patterns/index_patterns'; +import { FLEET_ASSETS_TO_IGNORE } from '../../common/index_patterns/constants'; + +interface Deps { + esClient: ElasticsearchClient; + soClient: SavedObjectsClientContract; +} + +export const hasUserIndexPattern = async ({ esClient, soClient }: Deps): Promise => { + const indexPatterns = await soClient.find({ + type: 'index-pattern', + fields: ['title'], + search: `*`, + searchFields: ['title'], + perPage: 100, + }); + + if (indexPatterns.total === 0) { + return false; + } + + // If there are any index patterns that are not the default metrics-* and logs-* ones created by Fleet, + // assume there are user created index patterns + if ( + indexPatterns.saved_objects.some( + (ip) => + ip.attributes.title !== FLEET_ASSETS_TO_IGNORE.METRICS_INDEX_PATTERN && + ip.attributes.title !== FLEET_ASSETS_TO_IGNORE.LOGS_INDEX_PATTERN + ) + ) { + return true; + } + + const resolveResponse = await esClient.indices.resolveIndex({ + name: `${FLEET_ASSETS_TO_IGNORE.LOGS_INDEX_PATTERN},${FLEET_ASSETS_TO_IGNORE.METRICS_INDEX_PATTERN}`, + }); + + const hasAnyNonDefaultFleetIndices = resolveResponse.body.indices.some( + (ds) => ds.name !== FLEET_ASSETS_TO_IGNORE.METRICS_ENDPOINT_INDEX_TO_IGNORE + ); + + if (hasAnyNonDefaultFleetIndices) return true; + + const hasAnyNonDefaultFleetDataStreams = resolveResponse.body.data_streams.some( + (ds) => + ds.name !== FLEET_ASSETS_TO_IGNORE.METRICS_DATA_STREAM_TO_IGNORE && + ds.name !== FLEET_ASSETS_TO_IGNORE.LOGS_DATA_STREAM_TO_IGNORE + ); + + if (hasAnyNonDefaultFleetDataStreams) return true; + + return false; +}; diff --git a/src/plugins/data/server/index_patterns/index_patterns_api_client.ts b/src/plugins/data/server/index_patterns/index_patterns_api_client.ts index 0ed84d4eee3b..fb76647a945b 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_api_client.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { GetFieldsOptions, IIndexPatternsApiClient, @@ -14,10 +14,14 @@ import { } from '../../common/index_patterns/types'; import { IndexPatternMissingIndices } from '../../common/index_patterns/lib'; import { IndexPatternsFetcher } from './fetcher'; +import { hasUserIndexPattern } from './has_user_index_pattern'; export class IndexPatternsApiServer implements IIndexPatternsApiClient { esClient: ElasticsearchClient; - constructor(elasticsearchClient: ElasticsearchClient) { + constructor( + elasticsearchClient: ElasticsearchClient, + private readonly savedObjectsClient: SavedObjectsClientContract + ) { this.esClient = elasticsearchClient; } async getFieldsForWildcard({ @@ -50,4 +54,11 @@ export class IndexPatternsApiServer implements IIndexPatternsApiClient { const indexPatterns = new IndexPatternsFetcher(this.esClient); return await indexPatterns.getFieldsForTimePattern(options); } + + async hasUserIndexPattern() { + return hasUserIndexPattern({ + esClient: this.esClient, + soClient: this.savedObjectsClient, + }); + } } diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index c3cdc65d3fa0..6f0491d91a64 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -66,7 +66,7 @@ export const indexPatternsServiceFactory = ({ return new IndexPatternsCommonService({ uiSettings: new UiSettingsServerToCommon(uiSettingsClient), savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient), - apiClient: new IndexPatternsApiServer(elasticsearchClient), + apiClient: new IndexPatternsApiServer(elasticsearchClient, savedObjectsClient), fieldFormats: formats, onError: (error) => { logger.error(error); diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts index d2d8cb82cf64..32fa50940bca 100644 --- a/src/plugins/data/server/index_patterns/routes.ts +++ b/src/plugins/data/server/index_patterns/routes.ts @@ -26,6 +26,7 @@ import { registerGetRuntimeFieldRoute } from './routes/runtime_fields/get_runtim import { registerDeleteRuntimeFieldRoute } from './routes/runtime_fields/delete_runtime_field'; import { registerPutRuntimeFieldRoute } from './routes/runtime_fields/put_runtime_field'; import { registerUpdateRuntimeFieldRoute } from './routes/runtime_fields/update_runtime_field'; +import { registerHasUserIndexPatternRoute } from './routes/has_user_index_pattern'; export function registerRoutes( http: HttpServiceSetup, @@ -49,6 +50,7 @@ export function registerRoutes( registerDeleteIndexPatternRoute(router, getStartServices); registerUpdateIndexPatternRoute(router, getStartServices); registerManageDefaultIndexPatternRoutes(router, getStartServices); + registerHasUserIndexPatternRoute(router, getStartServices); // Fields API registerUpdateFieldsRoute(router, getStartServices); diff --git a/src/plugins/data/server/index_patterns/routes/has_user_index_pattern.ts b/src/plugins/data/server/index_patterns/routes/has_user_index_pattern.ts new file mode 100644 index 000000000000..6447f50f88f2 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/has_user_index_pattern.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { handleErrors } from './util/handle_errors'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; + +export const registerHasUserIndexPatternRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.get( + { + path: '/api/index_patterns/has_user_index_pattern', + validate: {}, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + + return res.ok({ + body: { + result: await indexPatternsService.hasUserIndexPattern(), + }, + }); + }) + ) + ); +}; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 7eafad71f4f9..5931c00a1305 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -528,6 +528,7 @@ class IndexPatternsService { // Warning: (ae-forgotten-export) The symbol "IndexPatternListItem" needs to be exported by the entry point index.d.ts getIdsWithTitle: (refresh?: boolean) => Promise; getTitles: (refresh?: boolean) => Promise; + hasUserIndexPattern(): Promise; refreshFields: (indexPattern: IndexPattern) => Promise; savedObjectToSpec: (savedObject: SavedObject_2) => IndexPatternSpec; setDefault: (id: string | null, force?: boolean) => Promise; diff --git a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.test.tsx b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.test.tsx new file mode 100644 index 000000000000..03902792371e --- /dev/null +++ b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.test.tsx @@ -0,0 +1,98 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isUserDataIndex } from './empty_prompts'; +import { MatchedItem, ResolveIndexResponseItemIndexAttrs } from '../../types'; + +describe('isUserDataIndex', () => { + test('system index is not data index', () => { + const systemIndexes: MatchedItem[] = [ + { + name: '.apm-agent-configuration', + tags: [ + { + key: 'index', + name: 'Index', + color: 'default', + }, + ], + item: { + name: '.apm-agent-configuration', + attributes: [ResolveIndexResponseItemIndexAttrs.OPEN], + }, + }, + { + name: '.kibana', + tags: [ + { + key: 'alias', + name: 'Alias', + color: 'default', + }, + ], + item: { + name: '.kibana', + indices: ['.kibana_8.0.0_001'], + }, + }, + ]; + + expect(systemIndexes.some(isUserDataIndex)).toBe(false); + }); + + test('data index is data index', () => { + const dataIndex: MatchedItem = { + name: 'kibana_sample_data_ecommerce', + tags: [ + { + key: 'index', + name: 'Index', + color: 'default', + }, + ], + item: { + name: 'kibana_sample_data_ecommerce', + attributes: [ResolveIndexResponseItemIndexAttrs.OPEN], + }, + }; + + expect(isUserDataIndex(dataIndex)).toBe(true); + }); + + test('fleet asset is not data index', () => { + const fleetAssetIndex: MatchedItem = { + name: 'logs-elastic_agent', + tags: [ + { + key: 'data_stream', + name: 'Data stream', + color: 'primary', + }, + ], + item: { + name: 'logs-elastic_agent', + backing_indices: ['.ds-logs-elastic_agent-2021.08.18-000001'], + timestamp_field: '@timestamp', + }, + }; + + expect(isUserDataIndex(fleetAssetIndex)).toBe(false); + }); + + test('metrics-endpoint.metadata_current_default is not data index', () => { + const fleetAssetIndex: MatchedItem = { + name: 'metrics-endpoint.metadata_current_default', + tags: [{ key: 'index', name: 'Index', color: 'default' }], + item: { + name: 'metrics-endpoint.metadata_current_default', + attributes: [ResolveIndexResponseItemIndexAttrs.OPEN], + }, + }; + expect(isUserDataIndex(fleetAssetIndex)).toBe(false); + }); +}); diff --git a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx index 80224dbfb673..2f1631694e95 100644 --- a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx +++ b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx @@ -7,6 +7,7 @@ */ import React, { useState, useCallback, FC } from 'react'; +import useAsync from 'react-use/lib/useAsync'; import { useKibana } from '../../shared_imports'; @@ -17,6 +18,7 @@ import { getIndices } from '../../lib'; import { EmptyIndexListPrompt } from './empty_index_list_prompt'; import { EmptyIndexPatternPrompt } from './empty_index_pattern_prompt'; import { PromptFooter } from './prompt_footer'; +import { FLEET_ASSETS_TO_IGNORE } from '../../../../data/common'; const removeAliases = (item: MatchedItem) => !((item as unknown) as ResolveIndexResponseItemAlias).indices; @@ -24,25 +26,33 @@ const removeAliases = (item: MatchedItem) => interface Props { onCancel: () => void; allSources: MatchedItem[]; - hasExistingIndexPatterns: boolean; loadSources: () => void; } -export const EmptyPrompts: FC = ({ - hasExistingIndexPatterns, - allSources, - onCancel, - children, - loadSources, -}) => { +export function isUserDataIndex(source: MatchedItem) { + // filter out indices that start with `.` + if (source.name.startsWith('.')) return false; + + // filter out sources from FLEET_ASSETS_TO_IGNORE + if (source.name === FLEET_ASSETS_TO_IGNORE.LOGS_DATA_STREAM_TO_IGNORE) return false; + if (source.name === FLEET_ASSETS_TO_IGNORE.METRICS_DATA_STREAM_TO_IGNORE) return false; + if (source.name === FLEET_ASSETS_TO_IGNORE.METRICS_ENDPOINT_INDEX_TO_IGNORE) return false; + + return true; +} + +export const EmptyPrompts: FC = ({ allSources, onCancel, children, loadSources }) => { const { - services: { docLinks, application, http, searchClient }, + services: { docLinks, application, http, searchClient, indexPatternService }, } = useKibana(); const [remoteClustersExist, setRemoteClustersExist] = useState(false); const [goToForm, setGoToForm] = useState(false); - const hasDataIndices = allSources.some(({ name }: MatchedItem) => !name.startsWith('.')); + const hasDataIndices = allSources.some(isUserDataIndex); + const hasUserIndexPattern = useAsync(() => + indexPatternService.hasUserIndexPattern().catch(() => true) + ); useCallback(() => { let isMounted = true; @@ -63,7 +73,9 @@ export const EmptyPrompts: FC = ({ }; }, [http, hasDataIndices, searchClient]); - if (!hasExistingIndexPatterns && !goToForm) { + if (hasUserIndexPattern.loading) return null; // return null to prevent UI flickering while loading + + if (!hasUserIndexPattern.value && !goToForm) { if (!hasDataIndices && !remoteClustersExist) { // load data return ( diff --git a/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx b/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx index 4f6f7708d90c..14e1da44c7a3 100644 --- a/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx +++ b/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx @@ -338,12 +338,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ ); return ( - + diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index ef99be4df7cb..0a436f154161 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -81,7 +81,10 @@ export const IndexPatternTable = ({ ); setIndexPatterns(gettedIndexPatterns); setIsLoadingIndexPatterns(false); - if (gettedIndexPatterns.length === 0) { + if ( + gettedIndexPatterns.length === 0 || + !(await data.indexPatterns.hasUserIndexPattern().catch(() => false)) + ) { setShowCreateDialog(true); } })(); diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index b6d486a65686..68a469c753ce 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -93,9 +93,9 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => useEffect(() => { const fetchIsNewKibanaInstance = async () => { - const resp = await indexPatternService.getTitles(); + const hasUserIndexPattern = await indexPatternService.hasUserIndexPattern().catch(() => true); - setNewKibanaInstance(resp.length === 0); + setNewKibanaInstance(!hasUserIndexPattern); }; fetchIsNewKibanaInstance(); diff --git a/test/api_integration/apis/index_patterns/has_user_index_pattern/has_user_index_pattern.ts b/test/api_integration/apis/index_patterns/has_user_index_pattern/has_user_index_pattern.ts new file mode 100644 index 000000000000..8dfb892acfd9 --- /dev/null +++ b/test/api_integration/apis/index_patterns/has_user_index_pattern/has_user_index_pattern.ts @@ -0,0 +1,139 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('has user index pattern API', () => { + beforeEach(async () => { + await esArchiver.emptyKibanaIndex(); + if ((await es.indices.exists({ index: 'metrics-test' })).body) { + await es.indices.delete({ index: 'metrics-test' }); + } + + if ((await es.indices.exists({ index: 'logs-test' })).body) { + await es.indices.delete({ index: 'logs-test' }); + } + }); + + it('should return false if no index patterns', async () => { + const response = await supertest.get('/api/index_patterns/has_user_index_pattern'); + expect(response.status).to.be(200); + expect(response.body.result).to.be(false); + }); + + it('should return true if has index pattern with user data', async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title: 'basic_index', + }, + }); + + const response = await supertest.get('/api/index_patterns/has_user_index_pattern'); + expect(response.status).to.be(200); + expect(response.body.result).to.be(true); + + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('should return true if has user index pattern without data', async () => { + await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title: 'basic_index', + allowNoIndex: true, + }, + }); + + const response = await supertest.get('/api/index_patterns/has_user_index_pattern'); + expect(response.status).to.be(200); + expect(response.body.result).to.be(true); + }); + + it('should return false if only metric-* index pattern without data', async () => { + await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title: 'metrics-*', + allowNoIndex: true, + }, + }); + + const response = await supertest.get('/api/index_patterns/has_user_index_pattern'); + expect(response.status).to.be(200); + expect(response.body.result).to.be(false); + }); + + it('should return true if metric-* index pattern with user data', async () => { + await es.index({ + index: 'metrics-test', + body: { + foo: 'bar', + }, + }); + + await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title: 'metrics-*', + }, + }); + + const response = await supertest.get('/api/index_patterns/has_user_index_pattern'); + expect(response.status).to.be(200); + expect(response.body.result).to.be(true); + }); + + it('should return false if only logs-* index pattern without data', async () => { + await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title: 'logs-*', + }, + }); + + const response = await supertest.get('/api/index_patterns/has_user_index_pattern'); + expect(response.status).to.be(200); + expect(response.body.result).to.be(false); + }); + + it('should return true if logs-* index pattern with user data', async () => { + await es.index({ + index: 'logs-test', + body: { + foo: 'bar', + }, + }); + + await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title: 'logs-*', + }, + }); + + const response = await supertest.get('/api/index_patterns/has_user_index_pattern'); + expect(response.status).to.be(200); + expect(response.body.result).to.be(true); + }); + + // TODO: should setup fleet first similar to x-pack/test/fleet_functional/apps/home/welcome.ts + // but it is skipped due to flakiness https://github.com/elastic/kibana/issues/109017 + it('should return false if logs-* with .ds-logs-elastic_agent only'); + it('should return false if metrics-* with .ds-metrics-elastic_agent only'); + }); +} diff --git a/test/api_integration/apis/index_patterns/has_user_index_pattern/index.ts b/test/api_integration/apis/index_patterns/has_user_index_pattern/index.ts new file mode 100644 index 000000000000..5c05467d15c3 --- /dev/null +++ b/test/api_integration/apis/index_patterns/has_user_index_pattern/index.ts @@ -0,0 +1,15 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('has user index pattern', () => { + loadTestFile(require.resolve('./has_user_index_pattern')); + }); +} diff --git a/test/api_integration/apis/index_patterns/index.js b/test/api_integration/apis/index_patterns/index.js index 3dbe01206afa..b34012e362cb 100644 --- a/test/api_integration/apis/index_patterns/index.js +++ b/test/api_integration/apis/index_patterns/index.js @@ -18,5 +18,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./runtime_fields_crud')); loadTestFile(require.resolve('./integration')); loadTestFile(require.resolve('./deprecations')); + loadTestFile(require.resolve('./has_user_index_pattern')); }); }