diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 4df5d02e010db..396ffd4599284 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -100,7 +100,7 @@ pageLoadAssetSize: bfetch: 22837 kibanaUtils: 79713 data: 491273 - dataViews: 42532 + dataViews: 43532 expressions: 140958 fieldFormats: 65209 kibanaReact: 74422 @@ -123,4 +123,4 @@ pageLoadAssetSize: ux: 20784 sessionView: 77750 cloudSecurityPosture: 19109 - visTypeGauge: 24113 \ No newline at end of file + visTypeGauge: 24113 diff --git a/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx b/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx index f608040573abf..0560c66ab1f9b 100644 --- a/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx +++ b/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx @@ -11,7 +11,7 @@ import useAsync from 'react-use/lib/useAsync'; import { useKibana } from '../../shared_imports'; -import { MatchedItem, ResolveIndexResponseItemAlias, DataViewEditorContext } from '../../types'; +import { MatchedItem, DataViewEditorContext } from '../../types'; import { getIndices } from '../../lib'; @@ -20,8 +20,7 @@ import { EmptyIndexPatternPrompt } from './empty_index_pattern_prompt'; import { PromptFooter } from './prompt_footer'; import { DEFAULT_ASSETS_TO_IGNORE } from '../../../../data/common'; -const removeAliases = (item: MatchedItem) => - !(item as unknown as ResolveIndexResponseItemAlias).indices; +const removeAliases = (mItem: MatchedItem) => !mItem.item.indices; interface Props { onCancel: () => void; diff --git a/src/plugins/data_view_editor/public/types.ts b/src/plugins/data_view_editor/public/types.ts index cfeee1df979d8..ed0b19f6e529a 100644 --- a/src/plugins/data_view_editor/public/types.ts +++ b/src/plugins/data_view_editor/public/types.ts @@ -127,21 +127,6 @@ export interface IndexPatternTableItem { sort: string; } -// copied from index pattern management, needs review -export interface MatchedItem { - name: string; - tags: Tag[]; - item: { - name: string; - backing_indices?: string[]; - timestamp_field?: string; - indices?: string[]; - aliases?: string[]; - attributes?: ResolveIndexResponseItemIndexAttrs[]; - data_stream?: string; - }; -} - export enum ResolveIndexResponseItemIndexAttrs { OPEN = 'open', CLOSED = 'closed', diff --git a/src/plugins/data_views/README.mdx b/src/plugins/data_views/README.mdx index 90efdc18d8fdb..f4aa08da8b8ab 100644 --- a/src/plugins/data_views/README.mdx +++ b/src/plugins/data_views/README.mdx @@ -16,4 +16,9 @@ and field lists across the various Kibana apps. Its typically used in conjunctio *Note: Kibana index patterns are currently being renamed to data views. There will be some naming inconsistencies until the transition is complete.* +### Services +**hasData:** A standardized way to check the empty state for indices and data views. +- `hasESData: () => Promise; // Check to see if ES data exists` +- `hasDataView: () => Promise; // Check to see if any data view exists (primitive or user created)` +- `hasUserDataView: () => Promise; // Check to see if user created data views exists` diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index 3cc6d0811d8bc..9a59179ef4a35 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -69,6 +69,44 @@ export interface DataViewsServiceDeps { getCanSave: () => Promise; } +export interface DataViewsServicePublicMethods { + clearCache: (id?: string | undefined) => void; + create: (spec: DataViewSpec, skipFetchFields?: boolean) => Promise; + createAndSave: ( + spec: DataViewSpec, + override?: boolean, + skipFetchFields?: boolean + ) => Promise; + createSavedObject: (indexPattern: DataView, override?: boolean) => Promise; + delete: (indexPatternId: string) => Promise<{}>; + ensureDefaultDataView: EnsureDefaultDataView; + fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => DataViewFieldMap; + find: (search: string, size?: number) => Promise; + get: (id: string) => Promise; + getCache: () => Promise> | null | undefined>; + getCanSave: () => Promise; + getDefault: () => Promise; + getDefaultId: () => Promise; + getDefaultDataView: () => Promise; + getFieldsForIndexPattern: ( + indexPattern: DataView | DataViewSpec, + options?: GetFieldsOptions | undefined + ) => Promise; + getFieldsForWildcard: (options: GetFieldsOptions) => Promise; + getIds: (refresh?: boolean) => Promise; + getIdsWithTitle: (refresh?: boolean) => Promise; + getTitles: (refresh?: boolean) => Promise; + hasUserDataView: () => Promise; + refreshFields: (indexPattern: DataView) => Promise; + savedObjectToSpec: (savedObject: SavedObject) => DataViewSpec; + setDefault: (id: string | null, force?: boolean) => Promise; + updateSavedObject: ( + indexPattern: DataView, + saveAttempts?: number, + ignoreErrors?: boolean + ) => Promise; +} + export class DataViewsService { private config: UiSettingsCommon; private savedObjectsClient: SavedObjectsClientCommon; diff --git a/src/plugins/data_views/common/index.ts b/src/plugins/data_views/common/index.ts index 745adab3a6733..954d3ed7e3590 100644 --- a/src/plugins/data_views/common/index.ts +++ b/src/plugins/data_views/common/index.ts @@ -52,11 +52,16 @@ export type { DataViewFieldMap, DataViewSpec, SourceFilter, + HasDataService, } from './types'; export { DataViewType } from './types'; export type { IndexPatternsContract, DataViewsContract } from './data_views'; export { IndexPatternsService, DataViewsService } from './data_views'; -export type { DataViewListItem, TimeBasedDataView } from './data_views'; +export type { + DataViewListItem, + DataViewsServicePublicMethods, + TimeBasedDataView, +} from './data_views'; export { IndexPattern, DataView } from './data_views'; export { DuplicateDataViewError, diff --git a/src/plugins/data_views/common/types.ts b/src/plugins/data_views/common/types.ts index 8a1f7265296ff..606128d484ddb 100644 --- a/src/plugins/data_views/common/types.ts +++ b/src/plugins/data_views/common/types.ts @@ -289,3 +289,9 @@ export interface DataViewSpec { export interface SourceFilter { value: string; } + +export interface HasDataService { + hasESData: () => Promise; + hasUserDataView: () => Promise; + hasDataView: () => Promise; +} diff --git a/src/plugins/data_views/kibana.json b/src/plugins/data_views/kibana.json index 27bf536ef8040..04d5e93100e40 100644 --- a/src/plugins/data_views/kibana.json +++ b/src/plugins/data_views/kibana.json @@ -3,10 +3,10 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["fieldFormats","expressions"], + "requiredPlugins": ["fieldFormats", "expressions"], "optionalPlugins": ["usageCollection"], "extraPublicDirs": ["common"], - "requiredBundles": ["kibanaUtils","kibanaReact"], + "requiredBundles": ["kibanaUtils", "kibanaReact"], "owner": { "name": "App Services", "githubTeam": "kibana-app-services" diff --git a/src/plugins/data_views/public/data_views_service_public.ts b/src/plugins/data_views/public/data_views_service_public.ts index d89c4e37e872d..ceedffb553b66 100644 --- a/src/plugins/data_views/public/data_views_service_public.ts +++ b/src/plugins/data_views/public/data_views_service_public.ts @@ -9,16 +9,20 @@ import { DataViewsService } from '.'; import { DataViewsServiceDeps } from '../common/data_views/data_views'; +import { HasDataService } from '../common'; interface DataViewsServicePublicDeps extends DataViewsServiceDeps { getCanSaveSync: () => boolean; + hasData: HasDataService; } export class DataViewsServicePublic extends DataViewsService { public getCanSaveSync: () => boolean; + public hasData: HasDataService; constructor(deps: DataViewsServicePublicDeps) { super(deps); this.getCanSaveSync = deps.getCanSaveSync; + this.hasData = deps.hasData; } } diff --git a/src/plugins/data_views/public/index.ts b/src/plugins/data_views/public/index.ts index 47bbbe0406a90..f6a0843babed6 100644 --- a/src/plugins/data_views/public/index.ts +++ b/src/plugins/data_views/public/index.ts @@ -54,6 +54,9 @@ export type { DataViewsPublicPluginSetup, DataViewsPublicPluginStart, DataViewsContract, + HasDataViewsResponse, + IndicesResponse, + IndicesResponseModified, } from './types'; // Export plugin after all other imports diff --git a/src/plugins/data_views/public/plugin.ts b/src/plugins/data_views/public/plugin.ts index c2d0a70502b22..eb05e2ab209fc 100644 --- a/src/plugins/data_views/public/plugin.ts +++ b/src/plugins/data_views/public/plugin.ts @@ -23,6 +23,7 @@ import { } from '.'; import { DataViewsServicePublic } from './data_views_service_public'; +import { HasData } from './services'; export class DataViewsPublicPlugin implements @@ -33,6 +34,8 @@ export class DataViewsPublicPlugin DataViewsPublicStartDependencies > { + private readonly hasData = new HasData(); + public setup( core: CoreSetup, { expressions }: DataViewsPublicSetupDependencies @@ -47,8 +50,8 @@ export class DataViewsPublicPlugin { fieldFormats }: DataViewsPublicStartDependencies ): DataViewsPublicPluginStart { const { uiSettings, http, notifications, savedObjects, theme, overlays, application } = core; - return new DataViewsServicePublic({ + hasData: this.hasData.start(core), uiSettings: new UiSettingsPublicToCommon(uiSettings), savedObjectsClient: new SavedObjectsClientPublicToCommon(savedObjects.client), apiClient: new DataViewsApiClient(http), diff --git a/src/plugins/data_views/public/services/has_data.test.ts b/src/plugins/data_views/public/services/has_data.test.ts new file mode 100644 index 0000000000000..3d1fcef1100bd --- /dev/null +++ b/src/plugins/data_views/public/services/has_data.test.ts @@ -0,0 +1,147 @@ +/* + * 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 { coreMock } from '../../../../core/public/mocks'; + +import { HasData } from './has_data'; + +describe('when calling hasData service', () => { + it('should return true for hasESData when indices exist', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementation(() => + Promise.resolve({ + aliases: [], + data_streams: [], + indices: [ + { + aliases: [], + attributes: ['open'], + name: 'sample_data_logs', + }, + ], + }) + ); + + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart); + const reponse = hasDataService.hasESData(); + + expect(spy).toHaveBeenCalledTimes(1); + + expect(await reponse).toBe(true); + }); + + it('should return false for hasESData when no indices exist', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementation(() => + Promise.resolve({ + aliases: [], + data_streams: [], + indices: [], + }) + ); + + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart); + const reponse = hasDataService.hasESData(); + + expect(spy).toHaveBeenCalledTimes(1); + + expect(await reponse).toBe(false); + }); + + it('should return true for hasDataView when server returns true', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementation(() => + Promise.resolve({ + hasDataView: true, + hasUserDataView: true, + }) + ); + + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart); + const reponse = hasDataService.hasDataView(); + + expect(spy).toHaveBeenCalledTimes(1); + + expect(await reponse).toBe(true); + }); + + it('should return false for hasDataView when server returns false', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementation(() => + Promise.resolve({ + hasDataView: false, + hasUserDataView: true, + }) + ); + + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart); + const reponse = hasDataService.hasDataView(); + + expect(spy).toHaveBeenCalledTimes(1); + + expect(await reponse).toBe(false); + }); + + it('should return false for hasUserDataView when server returns false', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementation(() => + Promise.resolve({ + hasDataView: true, + hasUserDataView: false, + }) + ); + + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart); + const reponse = hasDataService.hasUserDataView(); + + expect(spy).toHaveBeenCalledTimes(1); + + expect(await reponse).toBe(false); + }); + + it('should return true for hasUserDataView when server returns true', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementation(() => + Promise.resolve({ + hasDataView: true, + hasUserDataView: true, + }) + ); + + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart); + const reponse = hasDataService.hasUserDataView(); + + expect(spy).toHaveBeenCalledTimes(1); + + expect(await reponse).toBe(true); + }); +}); diff --git a/src/plugins/data_views/public/services/has_data.ts b/src/plugins/data_views/public/services/has_data.ts new file mode 100644 index 0000000000000..628f68140603a --- /dev/null +++ b/src/plugins/data_views/public/services/has_data.ts @@ -0,0 +1,144 @@ +/* + * 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 { CoreStart, HttpStart } from 'kibana/public'; +import { DEFAULT_ASSETS_TO_IGNORE } from '../../common'; +import { HasDataViewsResponse, IndicesResponse, IndicesResponseModified } from '../'; + +export class HasData { + private removeAliases = (source: IndicesResponseModified): boolean => !source.item.indices; + + private isUserDataIndex = (source: IndicesResponseModified): boolean => { + // filter out indices that start with `.` + if (source.name.startsWith('.')) return false; + + // filter out empty sources created by apm server + if (source.name.startsWith('apm-')) return false; + + // filter out sources from DEFAULT_ASSETS_TO_IGNORE + if (source.name === DEFAULT_ASSETS_TO_IGNORE.LOGS_DATA_STREAM_TO_IGNORE) return false; + if (source.name === DEFAULT_ASSETS_TO_IGNORE.METRICS_DATA_STREAM_TO_IGNORE) return false; + if (source.name === DEFAULT_ASSETS_TO_IGNORE.METRICS_ENDPOINT_INDEX_TO_IGNORE) return false; + if (source.name === DEFAULT_ASSETS_TO_IGNORE.ENT_SEARCH_LOGS_DATA_STREAM_TO_IGNORE) + return false; + + return true; + }; + + start(core: CoreStart) { + const { http } = core; + return { + /** + * Check to see if ES data exists + */ + hasESData: async (): Promise => { + const hasLocalESData = await this.checkLocalESData(http); + if (!hasLocalESData) { + const hasRemoteESData = await this.checkRemoteESData(http); + return hasRemoteESData; + } + return hasLocalESData; + }, + /** + * Check to see if any data view exists + */ + hasDataView: async (): Promise => { + const dataViewsCheck = await this.findDataViews(http); + return dataViewsCheck; + }, + /** + * Check to see if user created data views exist + */ + hasUserDataView: async (): Promise => { + const userDataViewsCheck = await this.findUserDataViews(http); + return userDataViewsCheck; + }, + }; + } + + // ES Data + + private responseToItemArray = (response: IndicesResponse): IndicesResponseModified[] => { + const { indices = [], aliases = [] } = response; + const source: IndicesResponseModified[] = []; + + [...indices, ...aliases, ...(response.data_streams || [])].forEach((item) => { + source.push({ + name: item.name, + item, + }); + }); + + return source; + }; + + private getIndices = async ({ + http, + pattern, + showAllIndices, + }: { + http: HttpStart; + pattern: string; + showAllIndices: boolean; + }): Promise => + http + .get(`/internal/index-pattern-management/resolve_index/${pattern}`, { + query: showAllIndices ? { expand_wildcards: 'all' } : undefined, + }) + .then((response) => { + if (!response) { + return []; + } else { + return this.responseToItemArray(response); + } + }) + .catch(() => []); + + private checkLocalESData = (http: HttpStart): Promise => + this.getIndices({ + http, + pattern: '*', + showAllIndices: false, + }).then((dataSources: IndicesResponseModified[]) => { + return dataSources.some(this.isUserDataIndex); + }); + + private checkRemoteESData = (http: HttpStart): Promise => + this.getIndices({ + http, + pattern: '*:*', + showAllIndices: false, + }).then((dataSources: IndicesResponseModified[]) => { + return !!dataSources.filter(this.removeAliases).length; + }); + + // Data Views + + private getHasDataViews = async ({ http }: { http: HttpStart }): Promise => + http.get(`/internal/data_views/has_data_views`); + + private findDataViews = (http: HttpStart): Promise => { + return this.getHasDataViews({ http }) + .then((response: HasDataViewsResponse) => { + const { hasDataView } = response; + return hasDataView; + }) + .catch(() => false); + }; + + private findUserDataViews = (http: HttpStart): Promise => { + return this.getHasDataViews({ http }) + .then((response: HasDataViewsResponse) => { + const { hasUserDataView } = response; + return hasUserDataView; + }) + .catch(() => false); + }; +} + +export type HasDataStart = ReturnType; diff --git a/src/plugins/data_views/public/services/index.ts b/src/plugins/data_views/public/services/index.ts new file mode 100644 index 0000000000000..36d35d69bcb54 --- /dev/null +++ b/src/plugins/data_views/public/services/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export * from './has_data'; diff --git a/src/plugins/data_views/public/types.ts b/src/plugins/data_views/public/types.ts index e012695dfc6b5..5e9f769ba6cb4 100644 --- a/src/plugins/data_views/public/types.ts +++ b/src/plugins/data_views/public/types.ts @@ -8,8 +8,58 @@ import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { FieldFormatsSetup, FieldFormatsStart } from 'src/plugins/field_formats/public'; -import { PublicMethodsOf } from '@kbn/utility-types'; -import { DataViewsService } from './data_views'; +import { DataViewsServicePublicMethods } from './data_views'; +import { HasDataService } from '../common'; + +export enum IndicesResponseItemIndexAttrs { + OPEN = 'open', + CLOSED = 'closed', + HIDDEN = 'hidden', + FROZEN = 'frozen', +} + +export interface IndicesResponseModified { + name: string; + item: { + name: string; + backing_indices?: string[]; + timestamp_field?: string; + indices?: string[]; + aliases?: string[]; + attributes?: IndicesResponseItemIndexAttrs[]; + data_stream?: string; + }; +} + +export interface IndicesResponseItem { + name: string; +} + +export interface IndicesResponseItemAlias extends IndicesResponseItem { + indices: string[]; +} + +export interface IndicesResponseItemDataStream extends IndicesResponseItem { + backing_indices: string[]; + timestamp_field: string; +} + +export interface IndicesResponseItemIndex extends IndicesResponseItem { + aliases?: string[]; + attributes?: IndicesResponseItemIndexAttrs[]; + data_stream?: string; +} + +export interface IndicesResponse { + indices?: IndicesResponseItemIndex[]; + aliases?: IndicesResponseItemAlias[]; + data_streams?: IndicesResponseItemDataStream[]; +} + +export interface HasDataViewsResponse { + hasDataView: boolean; + hasUserDataView: boolean; +} export interface DataViewsPublicSetupDependencies { expressions: ExpressionsSetup; @@ -26,13 +76,14 @@ export interface DataViewsPublicStartDependencies { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DataViewsPublicPluginSetup {} -export interface DataViewsServicePublic extends DataViewsService { +export interface DataViewsServicePublic extends DataViewsServicePublicMethods { getCanSaveSync: () => boolean; + hasData: HasDataService; } -export type DataViewsContract = PublicMethodsOf; +export type DataViewsContract = DataViewsServicePublic; /** * Data views plugin public Start contract */ -export type DataViewsPublicPluginStart = PublicMethodsOf; +export type DataViewsPublicPluginStart = DataViewsServicePublic; diff --git a/src/plugins/data_views/server/has_user_index_pattern.ts b/src/plugins/data_views/server/has_user_index_pattern.ts index 087432a719857..b844c2f741258 100644 --- a/src/plugins/data_views/server/has_user_index_pattern.ts +++ b/src/plugins/data_views/server/has_user_index_pattern.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -import { ElasticsearchClient, SavedObjectsClientContract } from '../../../core/server'; +import { + ElasticsearchClient, + SavedObjectsClientContract, + SavedObjectsFindResponse, +} from '../../../core/server'; import { IndexPatternSavedObjectAttrs } from '../common/data_views'; import { DEFAULT_ASSETS_TO_IGNORE } from '../common/constants'; @@ -15,8 +19,11 @@ interface Deps { soClient: SavedObjectsClientContract; } -export const hasUserIndexPattern = async ({ esClient, soClient }: Deps): Promise => { - const indexPatterns = await soClient.find({ +export const getIndexPattern = async ({ + esClient, + soClient, +}: Deps): Promise> => + soClient.find({ type: 'index-pattern', fields: ['title'], search: `*`, @@ -24,10 +31,17 @@ export const hasUserIndexPattern = async ({ esClient, soClient }: Deps): Promise perPage: 100, }); +export const hasUserIndexPattern = async ( + { esClient, soClient }: Deps, + indexPatterns?: SavedObjectsFindResponse +): Promise => { + if (!indexPatterns) { + indexPatterns = await getIndexPattern({ esClient, soClient }); + } + 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 ( @@ -49,7 +63,6 @@ export const hasUserIndexPattern = async ({ esClient, soClient }: Deps): Promise ); if (hasAnyNonDefaultFleetIndices) return true; - const hasAnyNonDefaultFleetDataStreams = resolveResponse.data_streams.some( (ds) => ds.name !== DEFAULT_ASSETS_TO_IGNORE.METRICS_DATA_STREAM_TO_IGNORE && @@ -58,6 +71,5 @@ export const hasUserIndexPattern = async ({ esClient, soClient }: Deps): Promise ); if (hasAnyNonDefaultFleetDataStreams) return true; - return false; }; diff --git a/src/plugins/data_views/server/routes.ts b/src/plugins/data_views/server/routes.ts index 1e50e36316ff0..f21d951cab007 100644 --- a/src/plugins/data_views/server/routes.ts +++ b/src/plugins/data_views/server/routes.ts @@ -13,7 +13,8 @@ import { IndexPatternsFetcher } from './fetcher'; import { routes } from './rest_api_routes'; import type { DataViewsServerPluginStart, DataViewsServerPluginStartDependencies } from './types'; -import { registerFieldForWildcard } from './fields_for'; +import { registerFieldForWildcard } from './routes/fields_for'; +import { registerHasDataViewsRoute } from './routes/has_data_views'; export function registerRoutes( http: HttpServiceSetup, @@ -38,6 +39,7 @@ export function registerRoutes( routes.forEach((route) => route(router, getStartServices, dataViewRestCounter)); registerFieldForWildcard(router, getStartServices); + registerHasDataViewsRoute(router); router.get( { diff --git a/src/plugins/data_views/server/fields_for.ts b/src/plugins/data_views/server/routes/fields_for.ts similarity index 96% rename from src/plugins/data_views/server/fields_for.ts rename to src/plugins/data_views/server/routes/fields_for.ts index 6bd3f682249a5..77d39a6ea5d8b 100644 --- a/src/plugins/data_views/server/fields_for.ts +++ b/src/plugins/data_views/server/routes/fields_for.ts @@ -12,9 +12,9 @@ import { StartServicesAccessor, RequestHandler, RouteValidatorFullConfig, -} from '../../../core/server'; -import type { DataViewsServerPluginStart, DataViewsServerPluginStartDependencies } from './types'; -import { IndexPatternsFetcher } from './fetcher'; +} from '../../../../core/server'; +import type { DataViewsServerPluginStart, DataViewsServerPluginStartDependencies } from '../types'; +import { IndexPatternsFetcher } from '../fetcher'; const parseMetaFields = (metaFields: string | string[]) => { let parsedFields: string[] = []; diff --git a/src/plugins/data_views/server/routes/has_data_views.test.ts b/src/plugins/data_views/server/routes/has_data_views.test.ts new file mode 100644 index 0000000000000..1468026ad66e2 --- /dev/null +++ b/src/plugins/data_views/server/routes/has_data_views.test.ts @@ -0,0 +1,140 @@ +/* + * 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 type { MockedKeys } from '@kbn/utility-types/jest'; +import { CoreSetup, RequestHandlerContext } from 'src/core/server'; +import { coreMock, httpServerMock } from '../../../../../src/core/server/mocks'; +import { registerHasDataViewsRoute } from './has_data_views'; + +describe('preview has_data_views route', () => { + let mockCoreSetup: MockedKeys; + + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + }); + + it('should send hasDataView: true, hasUserDataView: true when data view exists', async () => { + const mockESClientResolveIndexResponse = { indices: [], aliases: [], data_streams: [] }; + const mockSOClientFindResponse = { + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + type: 'index-pattern', + id: '12345', + namespaces: ['default'], + attributes: { title: 'sample_data_logs' }, + }, + ], + }; + const mockESClient = { + indices: { + resolveIndex: jest.fn().mockResolvedValue(mockESClientResolveIndexResponse), + }, + }; + const mockSOClient = { find: jest.fn().mockResolvedValue(mockSOClientFindResponse) }; + const mockContext = { + core: { + elasticsearch: { client: { asCurrentUser: mockESClient } }, + savedObjects: { client: mockSOClient }, + }, + }; + + const mockRequest = httpServerMock.createKibanaRequest({ + body: {}, + query: {}, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerHasDataViewsRoute(mockCoreSetup.http.createRouter()); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.get.mock.calls[0][1]; + await handler(mockContext as unknown as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockSOClient.find.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "fields": Array [ + "title", + ], + "perPage": 100, + "search": "*", + "searchFields": Array [ + "title", + ], + "type": "index-pattern", + } + `); + + expect(mockResponse.ok).toBeCalled(); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ + body: { hasDataView: true, hasUserDataView: true }, + }); + }); + + it('should send hasDataView: true, hasUserDataView: false when default data view exists', async () => { + const mockESClientResolveIndexResponse = { indices: [], aliases: [], data_streams: [] }; + const mockSOClientFindResponse = { + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + type: 'index-pattern', + id: '12345', + namespaces: ['default'], + attributes: { title: 'logs-*' }, + }, + ], + }; + const mockESClient = { + indices: { + resolveIndex: jest.fn().mockResolvedValue(mockESClientResolveIndexResponse), + }, + }; + const mockSOClient = { find: jest.fn().mockResolvedValue(mockSOClientFindResponse) }; + const mockContext = { + core: { + elasticsearch: { client: { asCurrentUser: mockESClient } }, + savedObjects: { client: mockSOClient }, + }, + }; + + const mockRequest = httpServerMock.createKibanaRequest({ + body: {}, + query: {}, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerHasDataViewsRoute(mockCoreSetup.http.createRouter()); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.get.mock.calls[0][1]; + await handler(mockContext as unknown as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockSOClient.find.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "fields": Array [ + "title", + ], + "perPage": 100, + "search": "*", + "searchFields": Array [ + "title", + ], + "type": "index-pattern", + } + `); + + expect(mockResponse.ok).toBeCalled(); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ + body: { hasDataView: true, hasUserDataView: false }, + }); + }); +}); diff --git a/src/plugins/data_views/server/routes/has_data_views.ts b/src/plugins/data_views/server/routes/has_data_views.ts new file mode 100644 index 0000000000000..80204221b0aa4 --- /dev/null +++ b/src/plugins/data_views/server/routes/has_data_views.ts @@ -0,0 +1,39 @@ +/* + * 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 { IRouter } from '../../../../core/server'; +import { getIndexPattern, hasUserIndexPattern } from '../has_user_index_pattern'; + +export const registerHasDataViewsRoute = (router: IRouter): void => { + router.get( + { + path: '/internal/data_views/has_data_views', + validate: {}, + }, + async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const dataViews = await getIndexPattern({ + esClient: elasticsearchClient, + soClient: savedObjectsClient, + }); + const checkDataPattern = await hasUserIndexPattern( + { + esClient: elasticsearchClient, + soClient: savedObjectsClient, + }, + dataViews + ); + const response = { + hasDataView: !!dataViews.total, + hasUserDataView: !!checkDataPattern, + }; + return res.ok({ body: response }); + } + ); +};