mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[data / data views] Disable rollup functionality when rollup plugin is disabled (#162674)
## Summary Rollup functionality will be disabled on serverless instances. The `rollup` plugin will be disabled. While it won't be possible to create rollups on serverless, it will be possible to import data view saved objects which may be configured for use with rollups. We will make a 'best effort' to improve functionality as to help users transition away from rollups. - In classic environments, the `rollup` plugin will enable `dataViews` and `data` plugin rollup functionality. - The data plugin will run an async search instead of a rollup search. - The data views plugin will fetch normal fields, omitting a query for rollup fields - which would fail anyway. Closes https://github.com/elastic/kibana/issues/152708 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
874801bf7e
commit
a69870b950
25 changed files with 252 additions and 115 deletions
|
@ -28,7 +28,6 @@
|
||||||
],
|
],
|
||||||
"optionalPlugins": [
|
"optionalPlugins": [
|
||||||
"usageCollection",
|
"usageCollection",
|
||||||
"taskManager",
|
|
||||||
"security"
|
"security"
|
||||||
],
|
],
|
||||||
"requiredBundles": [
|
"requiredBundles": [
|
||||||
|
|
|
@ -12,10 +12,6 @@ import { BfetchServerSetup } from '@kbn/bfetch-plugin/server';
|
||||||
import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
|
import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
|
||||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||||
import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/server';
|
import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/server';
|
||||||
import type {
|
|
||||||
TaskManagerSetupContract,
|
|
||||||
TaskManagerStartContract,
|
|
||||||
} from '@kbn/task-manager-plugin/server';
|
|
||||||
import type { SecurityPluginSetup } from '@kbn/security-plugin/server';
|
import type { SecurityPluginSetup } from '@kbn/security-plugin/server';
|
||||||
import { ConfigSchema } from '../config';
|
import { ConfigSchema } from '../config';
|
||||||
import type { ISearchSetup, ISearchStart } from './search';
|
import type { ISearchSetup, ISearchStart } from './search';
|
||||||
|
@ -55,7 +51,6 @@ export interface DataPluginSetupDependencies {
|
||||||
expressions: ExpressionsServerSetup;
|
expressions: ExpressionsServerSetup;
|
||||||
usageCollection?: UsageCollectionSetup;
|
usageCollection?: UsageCollectionSetup;
|
||||||
fieldFormats: FieldFormatsSetup;
|
fieldFormats: FieldFormatsSetup;
|
||||||
taskManager?: TaskManagerSetupContract;
|
|
||||||
security?: SecurityPluginSetup;
|
security?: SecurityPluginSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +58,6 @@ export interface DataPluginStartDependencies {
|
||||||
fieldFormats: FieldFormatsStart;
|
fieldFormats: FieldFormatsStart;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
dataViews: DataViewsServerPluginStart;
|
dataViews: DataViewsServerPluginStart;
|
||||||
taskManager?: TaskManagerStartContract;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataServerPlugin
|
export class DataServerPlugin
|
||||||
|
@ -90,14 +84,7 @@ export class DataServerPlugin
|
||||||
|
|
||||||
public setup(
|
public setup(
|
||||||
core: CoreSetup<DataPluginStartDependencies, DataPluginStart>,
|
core: CoreSetup<DataPluginStartDependencies, DataPluginStart>,
|
||||||
{
|
{ bfetch, expressions, usageCollection, fieldFormats, security }: DataPluginSetupDependencies
|
||||||
bfetch,
|
|
||||||
expressions,
|
|
||||||
usageCollection,
|
|
||||||
fieldFormats,
|
|
||||||
taskManager,
|
|
||||||
security,
|
|
||||||
}: DataPluginSetupDependencies
|
|
||||||
) {
|
) {
|
||||||
this.scriptsService.setup(core);
|
this.scriptsService.setup(core);
|
||||||
const querySetup = this.queryService.setup(core);
|
const querySetup = this.queryService.setup(core);
|
||||||
|
@ -110,7 +97,6 @@ export class DataServerPlugin
|
||||||
expressions,
|
expressions,
|
||||||
usageCollection,
|
usageCollection,
|
||||||
security,
|
security,
|
||||||
taskManager,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -120,14 +106,10 @@ export class DataServerPlugin
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public start(
|
public start(core: CoreStart, { fieldFormats, dataViews }: DataPluginStartDependencies) {
|
||||||
core: CoreStart,
|
|
||||||
{ fieldFormats, dataViews, taskManager }: DataPluginStartDependencies
|
|
||||||
) {
|
|
||||||
const search = this.searchService.start(core, {
|
const search = this.searchService.start(core, {
|
||||||
fieldFormats,
|
fieldFormats,
|
||||||
indexPatterns: dataViews,
|
indexPatterns: dataViews,
|
||||||
taskManager,
|
|
||||||
});
|
});
|
||||||
const datatableUtilities = new DatatableUtilitiesService(
|
const datatableUtilities = new DatatableUtilitiesService(
|
||||||
search.aggs,
|
search.aggs,
|
||||||
|
|
|
@ -17,6 +17,7 @@ export function createSearchSetupMock(): jest.Mocked<ISearchSetup> {
|
||||||
aggs: searchAggsSetupMock(),
|
aggs: searchAggsSetupMock(),
|
||||||
registerSearchStrategy: jest.fn(),
|
registerSearchStrategy: jest.fn(),
|
||||||
searchSource: searchSourceMock.createSetupContract(),
|
searchSource: searchSourceMock.createSetupContract(),
|
||||||
|
enableRollups: jest.fn(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,10 +25,6 @@ import { ExpressionsServerSetup } from '@kbn/expressions-plugin/server';
|
||||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/server';
|
import { FieldFormatsStart } from '@kbn/field-formats-plugin/server';
|
||||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||||
import { KbnServerError } from '@kbn/kibana-utils-plugin/server';
|
import { KbnServerError } from '@kbn/kibana-utils-plugin/server';
|
||||||
import type {
|
|
||||||
TaskManagerSetupContract,
|
|
||||||
TaskManagerStartContract,
|
|
||||||
} from '@kbn/task-manager-plugin/server';
|
|
||||||
import type { SecurityPluginSetup } from '@kbn/security-plugin/server';
|
import type { SecurityPluginSetup } from '@kbn/security-plugin/server';
|
||||||
import type { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
|
import type { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
|
||||||
import type {
|
import type {
|
||||||
|
@ -107,7 +103,6 @@ export interface SearchServiceSetupDependencies {
|
||||||
bfetch: BfetchServerSetup;
|
bfetch: BfetchServerSetup;
|
||||||
expressions: ExpressionsServerSetup;
|
expressions: ExpressionsServerSetup;
|
||||||
usageCollection?: UsageCollectionSetup;
|
usageCollection?: UsageCollectionSetup;
|
||||||
taskManager?: TaskManagerSetupContract;
|
|
||||||
security?: SecurityPluginSetup;
|
security?: SecurityPluginSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +110,6 @@ export interface SearchServiceSetupDependencies {
|
||||||
export interface SearchServiceStartDependencies {
|
export interface SearchServiceStartDependencies {
|
||||||
fieldFormats: FieldFormatsStart;
|
fieldFormats: FieldFormatsStart;
|
||||||
indexPatterns: DataViewsServerPluginStart;
|
indexPatterns: DataViewsServerPluginStart;
|
||||||
taskManager?: TaskManagerStartContract;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -131,6 +125,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
||||||
private sessionService: SearchSessionService;
|
private sessionService: SearchSessionService;
|
||||||
private asScoped!: ISearchStart['asScoped'];
|
private asScoped!: ISearchStart['asScoped'];
|
||||||
private searchAsInternalUser!: ISearchStrategy;
|
private searchAsInternalUser!: ISearchStrategy;
|
||||||
|
private rollupsEnabled: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private initializerContext: PluginInitializerContext<ConfigSchema>,
|
private initializerContext: PluginInitializerContext<ConfigSchema>,
|
||||||
|
@ -145,7 +140,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
||||||
|
|
||||||
public setup(
|
public setup(
|
||||||
core: CoreSetup<DataPluginStartDependencies, DataPluginStart>,
|
core: CoreSetup<DataPluginStartDependencies, DataPluginStart>,
|
||||||
{ bfetch, expressions, usageCollection, taskManager, security }: SearchServiceSetupDependencies
|
{ bfetch, expressions, usageCollection, security }: SearchServiceSetupDependencies
|
||||||
): ISearchSetup {
|
): ISearchSetup {
|
||||||
core.savedObjects.registerType(searchSessionSavedObjectType);
|
core.savedObjects.registerType(searchSessionSavedObjectType);
|
||||||
const usage = usageCollection ? usageProvider(core) : undefined;
|
const usage = usageCollection ? usageProvider(core) : undefined;
|
||||||
|
@ -261,12 +256,13 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
||||||
registerSearchStrategy: this.registerSearchStrategy,
|
registerSearchStrategy: this.registerSearchStrategy,
|
||||||
usage,
|
usage,
|
||||||
searchSource: this.searchSourceService.setup(),
|
searchSource: this.searchSourceService.setup(),
|
||||||
|
enableRollups: () => (this.rollupsEnabled = true),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public start(
|
public start(
|
||||||
core: CoreStart,
|
core: CoreStart,
|
||||||
{ fieldFormats, indexPatterns, taskManager }: SearchServiceStartDependencies
|
{ fieldFormats, indexPatterns }: SearchServiceStartDependencies
|
||||||
): ISearchStart {
|
): ISearchStart {
|
||||||
const { elasticsearch, savedObjects, uiSettings } = core;
|
const { elasticsearch, savedObjects, uiSettings } = core;
|
||||||
|
|
||||||
|
@ -278,7 +274,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
||||||
indexPatterns,
|
indexPatterns,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.asScoped = this.asScopedProvider(core);
|
this.asScoped = this.asScopedProvider(core, this.rollupsEnabled);
|
||||||
return {
|
return {
|
||||||
aggs,
|
aggs,
|
||||||
searchAsInternalUser: this.searchAsInternalUser,
|
searchAsInternalUser: this.searchAsInternalUser,
|
||||||
|
@ -516,7 +512,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
||||||
return deps.searchSessionsClient.extend(sessionId, expires);
|
return deps.searchSessionsClient.extend(sessionId, expires);
|
||||||
};
|
};
|
||||||
|
|
||||||
private asScopedProvider = (core: CoreStart) => {
|
private asScopedProvider = (core: CoreStart, rollupsEnabled: boolean = false) => {
|
||||||
const { elasticsearch, savedObjects, uiSettings } = core;
|
const { elasticsearch, savedObjects, uiSettings } = core;
|
||||||
const getSessionAsScoped = this.sessionService.asScopedProvider(core);
|
const getSessionAsScoped = this.sessionService.asScopedProvider(core);
|
||||||
return (request: KibanaRequest): IScopedSearchClient => {
|
return (request: KibanaRequest): IScopedSearchClient => {
|
||||||
|
@ -530,6 +526,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
||||||
uiSettings.asScopedToClient(savedObjectsClient)
|
uiSettings.asScopedToClient(savedObjectsClient)
|
||||||
),
|
),
|
||||||
request,
|
request,
|
||||||
|
rollupsEnabled,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
search: <
|
search: <
|
||||||
|
|
|
@ -64,6 +64,7 @@ describe('ES search strategy', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
searchSessionsClient: createSearchSessionsClientMock(),
|
searchSessionsClient: createSearchSessionsClientMock(),
|
||||||
|
rollupsEnabled: true,
|
||||||
} as unknown as SearchStrategyDependencies;
|
} as unknown as SearchStrategyDependencies;
|
||||||
const mockLegacyConfig$ = new BehaviorSubject<any>({
|
const mockLegacyConfig$ = new BehaviorSubject<any>({
|
||||||
elasticsearch: {
|
elasticsearch: {
|
||||||
|
@ -233,6 +234,31 @@ describe('ES search strategy', () => {
|
||||||
expect(method).toBe('POST');
|
expect(method).toBe('POST');
|
||||||
expect(path).toBe('/foo-%E7%A8%8B/_rollup_search');
|
expect(path).toBe('/foo-%E7%A8%8B/_rollup_search');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("doesn't call the rollup API if the index is a rollup type BUT rollups are disabled", async () => {
|
||||||
|
mockApiCaller.mockResolvedValueOnce(mockRollupResponse);
|
||||||
|
mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse);
|
||||||
|
|
||||||
|
const params = { index: 'foo-程', body: { query: {} } };
|
||||||
|
const esSearch = await enhancedEsSearchStrategyProvider(
|
||||||
|
mockLegacyConfig$,
|
||||||
|
mockSearchConfig,
|
||||||
|
mockLogger
|
||||||
|
);
|
||||||
|
|
||||||
|
await esSearch
|
||||||
|
.search(
|
||||||
|
{
|
||||||
|
indexType: 'rollup',
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{ ...mockDeps, rollupsEnabled: false }
|
||||||
|
)
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
expect(mockApiCaller).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with sessionId', () => {
|
describe('with sessionId', () => {
|
||||||
|
|
|
@ -154,7 +154,7 @@ export const enhancedEsSearchStrategyProvider = (
|
||||||
throw new KbnServerError('Unknown indexType', 400);
|
throw new KbnServerError('Unknown indexType', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.indexType === undefined) {
|
if (request.indexType === undefined || !deps.rollupsEnabled) {
|
||||||
return asyncSearch(request, options, deps);
|
return asyncSearch(request, options, deps);
|
||||||
} else {
|
} else {
|
||||||
return from(rollupSearch(request, options, deps));
|
return from(rollupSearch(request, options, deps));
|
||||||
|
|
|
@ -35,6 +35,7 @@ export interface SearchStrategyDependencies {
|
||||||
uiSettingsClient: Pick<IUiSettingsClient, 'get'>;
|
uiSettingsClient: Pick<IUiSettingsClient, 'get'>;
|
||||||
searchSessionsClient: IScopedSearchSessionsClient;
|
searchSessionsClient: IScopedSearchSessionsClient;
|
||||||
request: KibanaRequest;
|
request: KibanaRequest;
|
||||||
|
rollupsEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISearchSetup {
|
export interface ISearchSetup {
|
||||||
|
@ -55,7 +56,7 @@ export interface ISearchSetup {
|
||||||
* Used internally for telemetry
|
* Used internally for telemetry
|
||||||
*/
|
*/
|
||||||
usage?: SearchUsage;
|
usage?: SearchUsage;
|
||||||
|
enableRollups: () => void;
|
||||||
searchSource: ReturnType<SearchSourceService['setup']>;
|
searchSource: ReturnType<SearchSourceService['setup']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
"@kbn/field-formats-plugin",
|
"@kbn/field-formats-plugin",
|
||||||
"@kbn/data-views-plugin",
|
"@kbn/data-views-plugin",
|
||||||
"@kbn/screenshot-mode-plugin",
|
"@kbn/screenshot-mode-plugin",
|
||||||
"@kbn/task-manager-plugin",
|
|
||||||
"@kbn/security-plugin",
|
"@kbn/security-plugin",
|
||||||
"@kbn/expressions-plugin",
|
"@kbn/expressions-plugin",
|
||||||
"@kbn/field-types",
|
"@kbn/field-types",
|
||||||
|
|
|
@ -25,6 +25,7 @@ interface DataViewsServiceFactoryDeps {
|
||||||
uiSettings: UiSettingsServiceStart;
|
uiSettings: UiSettingsServiceStart;
|
||||||
fieldFormats: FieldFormatsStart;
|
fieldFormats: FieldFormatsStart;
|
||||||
capabilities: CoreStart['capabilities'];
|
capabilities: CoreStart['capabilities'];
|
||||||
|
rollupsEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,14 +39,18 @@ export const dataViewsServiceFactory = (deps: DataViewsServiceFactoryDeps) =>
|
||||||
request?: KibanaRequest,
|
request?: KibanaRequest,
|
||||||
byPassCapabilities?: boolean
|
byPassCapabilities?: boolean
|
||||||
) {
|
) {
|
||||||
const { logger, uiSettings, fieldFormats, capabilities } = deps;
|
const { logger, uiSettings, fieldFormats, capabilities, rollupsEnabled } = deps;
|
||||||
const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
|
const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
|
||||||
const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient);
|
const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient);
|
||||||
|
|
||||||
return new DataViewsService({
|
return new DataViewsService({
|
||||||
uiSettings: new UiSettingsServerToCommon(uiSettingsClient),
|
uiSettings: new UiSettingsServerToCommon(uiSettingsClient),
|
||||||
savedObjectsClient: new SavedObjectsClientWrapper(savedObjectsClient),
|
savedObjectsClient: new SavedObjectsClientWrapper(savedObjectsClient),
|
||||||
apiClient: new IndexPatternsApiServer(elasticsearchClient, savedObjectsClient),
|
apiClient: new IndexPatternsApiServer(
|
||||||
|
elasticsearchClient,
|
||||||
|
savedObjectsClient,
|
||||||
|
rollupsEnabled
|
||||||
|
),
|
||||||
fieldFormats: formats,
|
fieldFormats: formats,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
|
|
@ -10,6 +10,19 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||||
import { IndexPatternsFetcher } from '.';
|
import { IndexPatternsFetcher } from '.';
|
||||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||||
|
|
||||||
|
const rollupResponse = {
|
||||||
|
foo: {
|
||||||
|
rollup_jobs: [
|
||||||
|
{
|
||||||
|
index_pattern: 'foo',
|
||||||
|
job_id: '123',
|
||||||
|
rollup_index: 'foo',
|
||||||
|
fields: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe('Index Pattern Fetcher - server', () => {
|
describe('Index Pattern Fetcher - server', () => {
|
||||||
let indexPatterns: IndexPatternsFetcher;
|
let indexPatterns: IndexPatternsFetcher;
|
||||||
let esClient: ReturnType<typeof elasticsearchServiceMock.createElasticsearchClient>;
|
let esClient: ReturnType<typeof elasticsearchServiceMock.createElasticsearchClient>;
|
||||||
|
@ -21,12 +34,40 @@ describe('Index Pattern Fetcher - server', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
esClient = elasticsearchServiceMock.createElasticsearchClient();
|
esClient = elasticsearchServiceMock.createElasticsearchClient();
|
||||||
indexPatterns = new IndexPatternsFetcher(esClient);
|
indexPatterns = new IndexPatternsFetcher(esClient, false, true);
|
||||||
});
|
});
|
||||||
it('calls fieldcaps once', async () => {
|
it('calls fieldcaps once', async () => {
|
||||||
esClient.fieldCaps.mockResponse(response as unknown as estypes.FieldCapsResponse);
|
esClient.fieldCaps.mockResponse(response as unknown as estypes.FieldCapsResponse);
|
||||||
indexPatterns = new IndexPatternsFetcher(esClient, true);
|
indexPatterns = new IndexPatternsFetcher(esClient, true, true);
|
||||||
await indexPatterns.getFieldsForWildcard({ pattern: patternList });
|
await indexPatterns.getFieldsForWildcard({ pattern: patternList });
|
||||||
expect(esClient.fieldCaps).toHaveBeenCalledTimes(1);
|
expect(esClient.fieldCaps).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calls rollup api when given rollup data view', async () => {
|
||||||
|
esClient.fieldCaps.mockResponse(response as unknown as estypes.FieldCapsResponse);
|
||||||
|
esClient.rollup.getRollupIndexCaps.mockResponse(
|
||||||
|
rollupResponse as unknown as estypes.RollupGetRollupIndexCapsResponse
|
||||||
|
);
|
||||||
|
indexPatterns = new IndexPatternsFetcher(esClient, true, true);
|
||||||
|
await indexPatterns.getFieldsForWildcard({
|
||||||
|
pattern: patternList,
|
||||||
|
type: 'rollup',
|
||||||
|
rollupIndex: 'foo',
|
||||||
|
});
|
||||||
|
expect(esClient.rollup.getRollupIndexCaps).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't call rollup api when given rollup data view and rollups are disabled", async () => {
|
||||||
|
esClient.fieldCaps.mockResponse(response as unknown as estypes.FieldCapsResponse);
|
||||||
|
esClient.rollup.getRollupIndexCaps.mockResponse(
|
||||||
|
rollupResponse as unknown as estypes.RollupGetRollupIndexCapsResponse
|
||||||
|
);
|
||||||
|
indexPatterns = new IndexPatternsFetcher(esClient, true, false);
|
||||||
|
await indexPatterns.getFieldsForWildcard({
|
||||||
|
pattern: patternList,
|
||||||
|
type: 'rollup',
|
||||||
|
rollupIndex: 'foo',
|
||||||
|
});
|
||||||
|
expect(esClient.rollup.getRollupIndexCaps).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,10 +40,16 @@ interface FieldSubType {
|
||||||
export class IndexPatternsFetcher {
|
export class IndexPatternsFetcher {
|
||||||
private elasticsearchClient: ElasticsearchClient;
|
private elasticsearchClient: ElasticsearchClient;
|
||||||
private allowNoIndices: boolean;
|
private allowNoIndices: boolean;
|
||||||
|
private rollupsEnabled: boolean;
|
||||||
|
|
||||||
constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices: boolean = false) {
|
constructor(
|
||||||
|
elasticsearchClient: ElasticsearchClient,
|
||||||
|
allowNoIndices: boolean = false,
|
||||||
|
rollupsEnabled: boolean = false
|
||||||
|
) {
|
||||||
this.elasticsearchClient = elasticsearchClient;
|
this.elasticsearchClient = elasticsearchClient;
|
||||||
this.allowNoIndices = allowNoIndices;
|
this.allowNoIndices = allowNoIndices;
|
||||||
|
this.rollupsEnabled = rollupsEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,7 +87,7 @@ export class IndexPatternsFetcher {
|
||||||
fields: options.fields || ['*'],
|
fields: options.fields || ['*'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (type === 'rollup' && rollupIndex) {
|
if (this.rollupsEnabled && type === 'rollup' && rollupIndex) {
|
||||||
const rollupFields: FieldDescriptor[] = [];
|
const rollupFields: FieldDescriptor[] = [];
|
||||||
const capabilityCheck = getCapabilitiesForRollupIndices(
|
const capabilityCheck = getCapabilitiesForRollupIndices(
|
||||||
await this.elasticsearchClient.rollup.getRollupIndexCaps({
|
await this.elasticsearchClient.rollup.getRollupIndexCaps({
|
||||||
|
|
|
@ -10,6 +10,7 @@ export { getFieldByName, findIndexPatternById } from './utils';
|
||||||
export type { FieldDescriptor, RollupIndexCapability } from './fetcher';
|
export type { FieldDescriptor, RollupIndexCapability } from './fetcher';
|
||||||
export { IndexPatternsFetcher, getCapabilitiesForRollupIndices } from './fetcher';
|
export { IndexPatternsFetcher, getCapabilitiesForRollupIndices } from './fetcher';
|
||||||
export type {
|
export type {
|
||||||
|
DataViewsServerPluginSetup,
|
||||||
DataViewsServerPluginStart,
|
DataViewsServerPluginStart,
|
||||||
DataViewsServerPluginSetupDependencies,
|
DataViewsServerPluginSetupDependencies,
|
||||||
DataViewsServerPluginStartDependencies,
|
DataViewsServerPluginStartDependencies,
|
||||||
|
|
|
@ -16,7 +16,8 @@ export class IndexPatternsApiServer implements IDataViewsApiClient {
|
||||||
esClient: ElasticsearchClient;
|
esClient: ElasticsearchClient;
|
||||||
constructor(
|
constructor(
|
||||||
elasticsearchClient: ElasticsearchClient,
|
elasticsearchClient: ElasticsearchClient,
|
||||||
private readonly savedObjectsClient: SavedObjectsClientContract
|
private readonly savedObjectsClient: SavedObjectsClientContract,
|
||||||
|
private readonly rollupsEnabled: boolean
|
||||||
) {
|
) {
|
||||||
this.esClient = elasticsearchClient;
|
this.esClient = elasticsearchClient;
|
||||||
}
|
}
|
||||||
|
@ -29,7 +30,11 @@ export class IndexPatternsApiServer implements IDataViewsApiClient {
|
||||||
indexFilter,
|
indexFilter,
|
||||||
fields,
|
fields,
|
||||||
}: GetFieldsOptions) {
|
}: GetFieldsOptions) {
|
||||||
const indexPatterns = new IndexPatternsFetcher(this.esClient, allowNoIndex);
|
const indexPatterns = new IndexPatternsFetcher(
|
||||||
|
this.esClient,
|
||||||
|
allowNoIndex,
|
||||||
|
this.rollupsEnabled
|
||||||
|
);
|
||||||
return await indexPatterns
|
return await indexPatterns
|
||||||
.getFieldsForWildcard({
|
.getFieldsForWildcard({
|
||||||
pattern,
|
pattern,
|
||||||
|
|
|
@ -33,6 +33,7 @@ export class DataViewsServerPlugin
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
private readonly logger: Logger;
|
private readonly logger: Logger;
|
||||||
|
private rollupsEnabled: boolean = false;
|
||||||
|
|
||||||
constructor(initializerContext: PluginInitializerContext) {
|
constructor(initializerContext: PluginInitializerContext) {
|
||||||
this.logger = initializerContext.logger.get('dataView');
|
this.logger = initializerContext.logger.get('dataView');
|
||||||
|
@ -46,7 +47,12 @@ export class DataViewsServerPlugin
|
||||||
core.capabilities.registerProvider(capabilitiesProvider);
|
core.capabilities.registerProvider(capabilitiesProvider);
|
||||||
const dataViewRestCounter = usageCollection?.createUsageCounter('dataViewsRestApi');
|
const dataViewRestCounter = usageCollection?.createUsageCounter('dataViewsRestApi');
|
||||||
|
|
||||||
registerRoutes(core.http, core.getStartServices, dataViewRestCounter);
|
registerRoutes(
|
||||||
|
core.http,
|
||||||
|
core.getStartServices,
|
||||||
|
() => this.rollupsEnabled,
|
||||||
|
dataViewRestCounter
|
||||||
|
);
|
||||||
|
|
||||||
expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices }));
|
expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices }));
|
||||||
registerIndexPatternsUsageCollector(core.getStartServices, usageCollection);
|
registerIndexPatternsUsageCollector(core.getStartServices, usageCollection);
|
||||||
|
@ -60,7 +66,9 @@ export class DataViewsServerPlugin
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {};
|
return {
|
||||||
|
enableRollups: () => (this.rollupsEnabled = true),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public start(
|
public start(
|
||||||
|
@ -72,6 +80,7 @@ export class DataViewsServerPlugin
|
||||||
uiSettings,
|
uiSettings,
|
||||||
fieldFormats,
|
fieldFormats,
|
||||||
capabilities,
|
capabilities,
|
||||||
|
rollupsEnabled: this.rollupsEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -111,86 +111,93 @@ const validate: FullValidationConfig<any, any, any> = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler: RequestHandler<{}, IQuery, IBody> = async (context, request, response) => {
|
const handler: (isRollupsEnabled: () => boolean) => RequestHandler<{}, IQuery, IBody> =
|
||||||
const { asCurrentUser } = (await context.core).elasticsearch.client;
|
(isRollupsEnabled) => async (context, request, response) => {
|
||||||
const indexPatterns = new IndexPatternsFetcher(asCurrentUser);
|
const { asCurrentUser } = (await context.core).elasticsearch.client;
|
||||||
const {
|
const indexPatterns = new IndexPatternsFetcher(asCurrentUser, undefined, isRollupsEnabled());
|
||||||
pattern,
|
const {
|
||||||
meta_fields: metaFields,
|
|
||||||
type,
|
|
||||||
rollup_index: rollupIndex,
|
|
||||||
allow_no_index: allowNoIndex,
|
|
||||||
include_unmapped: includeUnmapped,
|
|
||||||
} = request.query;
|
|
||||||
|
|
||||||
// not available to get request
|
|
||||||
const indexFilter = request.body?.index_filter;
|
|
||||||
|
|
||||||
let parsedFields: string[] = [];
|
|
||||||
let parsedMetaFields: string[] = [];
|
|
||||||
try {
|
|
||||||
parsedMetaFields = parseFields(metaFields);
|
|
||||||
parsedFields = parseFields(request.query.fields ?? []);
|
|
||||||
} catch (error) {
|
|
||||||
return response.badRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { fields, indices } = await indexPatterns.getFieldsForWildcard({
|
|
||||||
pattern,
|
pattern,
|
||||||
metaFields: parsedMetaFields,
|
meta_fields: metaFields,
|
||||||
type,
|
type,
|
||||||
rollupIndex,
|
rollup_index: rollupIndex,
|
||||||
fieldCapsOptions: {
|
allow_no_index: allowNoIndex,
|
||||||
allow_no_indices: allowNoIndex || false,
|
include_unmapped: includeUnmapped,
|
||||||
includeUnmapped,
|
} = request.query;
|
||||||
},
|
|
||||||
indexFilter,
|
|
||||||
...(parsedFields.length > 0 ? { fields: parsedFields } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const body: { fields: FieldDescriptorRestResponse[]; indices: string[] } = { fields, indices };
|
// not available to get request
|
||||||
|
const indexFilter = request.body?.index_filter;
|
||||||
|
|
||||||
return response.ok({
|
let parsedFields: string[] = [];
|
||||||
body,
|
let parsedMetaFields: string[] = [];
|
||||||
headers: {
|
try {
|
||||||
'content-type': 'application/json',
|
parsedMetaFields = parseFields(metaFields);
|
||||||
},
|
parsedFields = parseFields(request.query.fields ?? []);
|
||||||
});
|
} catch (error) {
|
||||||
} catch (error) {
|
return response.badRequest();
|
||||||
if (
|
}
|
||||||
typeof error === 'object' &&
|
|
||||||
!!error?.isBoom &&
|
try {
|
||||||
!!error?.output?.payload &&
|
const { fields, indices } = await indexPatterns.getFieldsForWildcard({
|
||||||
typeof error?.output?.payload === 'object'
|
pattern,
|
||||||
) {
|
metaFields: parsedMetaFields,
|
||||||
const payload = error?.output?.payload;
|
type,
|
||||||
return response.notFound({
|
rollupIndex,
|
||||||
body: {
|
fieldCapsOptions: {
|
||||||
message: payload.message,
|
allow_no_indices: allowNoIndex || false,
|
||||||
attributes: payload,
|
includeUnmapped,
|
||||||
|
},
|
||||||
|
indexFilter,
|
||||||
|
...(parsedFields.length > 0 ? { fields: parsedFields } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const body: { fields: FieldDescriptorRestResponse[]; indices: string[] } = {
|
||||||
|
fields,
|
||||||
|
indices,
|
||||||
|
};
|
||||||
|
|
||||||
|
return response.ok({
|
||||||
|
body,
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} catch (error) {
|
||||||
return response.notFound();
|
if (
|
||||||
|
typeof error === 'object' &&
|
||||||
|
!!error?.isBoom &&
|
||||||
|
!!error?.output?.payload &&
|
||||||
|
typeof error?.output?.payload === 'object'
|
||||||
|
) {
|
||||||
|
const payload = error?.output?.payload;
|
||||||
|
return response.notFound({
|
||||||
|
body: {
|
||||||
|
message: payload.message,
|
||||||
|
attributes: payload,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return response.notFound();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
export const registerFieldForWildcard = (
|
export const registerFieldForWildcard = async (
|
||||||
router: IRouter,
|
router: IRouter,
|
||||||
getStartServices: StartServicesAccessor<
|
getStartServices: StartServicesAccessor<
|
||||||
DataViewsServerPluginStartDependencies,
|
DataViewsServerPluginStartDependencies,
|
||||||
DataViewsServerPluginStart
|
DataViewsServerPluginStart
|
||||||
>
|
>,
|
||||||
|
isRollupsEnabled: () => boolean
|
||||||
) => {
|
) => {
|
||||||
|
const configuredHandler = handler(isRollupsEnabled);
|
||||||
|
|
||||||
// handler
|
// handler
|
||||||
router.versioned.put({ path, access }).addVersion({ version, validate }, handler);
|
router.versioned.put({ path, access }).addVersion({ version, validate }, configuredHandler);
|
||||||
router.versioned.post({ path, access }).addVersion({ version, validate }, handler);
|
router.versioned.post({ path, access }).addVersion({ version, validate }, configuredHandler);
|
||||||
router.versioned
|
router.versioned
|
||||||
.get({ path, access })
|
.get({ path, access })
|
||||||
.addVersion(
|
.addVersion(
|
||||||
{ version, validate: { request: { query: querySchema }, response: validate.response } },
|
{ version, validate: { request: { query: querySchema }, response: validate.response } },
|
||||||
handler
|
configuredHandler
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,12 +20,13 @@ export function registerRoutes(
|
||||||
DataViewsServerPluginStartDependencies,
|
DataViewsServerPluginStartDependencies,
|
||||||
DataViewsServerPluginStart
|
DataViewsServerPluginStart
|
||||||
>,
|
>,
|
||||||
|
isRollupsEnabled: () => boolean,
|
||||||
dataViewRestCounter?: UsageCounter
|
dataViewRestCounter?: UsageCounter
|
||||||
) {
|
) {
|
||||||
const router = http.createRouter();
|
const router = http.createRouter();
|
||||||
|
|
||||||
routes.forEach((route) => route(router, getStartServices, dataViewRestCounter));
|
routes.forEach((route) => route(router, getStartServices, dataViewRestCounter));
|
||||||
|
|
||||||
registerFieldForWildcard(router, getStartServices);
|
registerFieldForWildcard(router, getStartServices, isRollupsEnabled);
|
||||||
registerHasDataViewsRoute(router);
|
registerHasDataViewsRoute(router);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,8 +53,9 @@ export interface DataViewsServerPluginStart {
|
||||||
/**
|
/**
|
||||||
* DataViews server plugin setup api
|
* DataViews server plugin setup api
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
export interface DataViewsServerPluginSetup {
|
||||||
export interface DataViewsServerPluginSetup {}
|
enableRollups: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data Views server setup dependencies
|
* Data Views server setup dependencies
|
||||||
|
|
|
@ -298,6 +298,7 @@ const createSearchStrategyDependenciesMock = (): SearchStrategyDependencies => (
|
||||||
savedObjectsClient: savedObjectsClientMock.create(),
|
savedObjectsClient: savedObjectsClientMock.create(),
|
||||||
searchSessionsClient: createSearchSessionsClientMock(),
|
searchSessionsClient: createSearchSessionsClientMock(),
|
||||||
request: httpServerMock.createKibanaRequest(),
|
request: httpServerMock.createKibanaRequest(),
|
||||||
|
rollupsEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// using the official data mock from within x-pack doesn't type-check successfully,
|
// using the official data mock from within x-pack doesn't type-check successfully,
|
||||||
|
|
|
@ -13,13 +13,14 @@
|
||||||
"requiredPlugins": [
|
"requiredPlugins": [
|
||||||
"management",
|
"management",
|
||||||
"licensing",
|
"licensing",
|
||||||
"features"
|
"features",
|
||||||
|
"dataViews",
|
||||||
|
"data"
|
||||||
],
|
],
|
||||||
"optionalPlugins": [
|
"optionalPlugins": [
|
||||||
"home",
|
"home",
|
||||||
"indexManagement",
|
"indexManagement",
|
||||||
"usageCollection",
|
"usageCollection"
|
||||||
"visTypeTimeseries"
|
|
||||||
],
|
],
|
||||||
"requiredBundles": [
|
"requiredBundles": [
|
||||||
"kibanaUtils",
|
"kibanaUtils",
|
||||||
|
|
|
@ -30,8 +30,8 @@ export class RollupPlugin implements Plugin<void, void, any, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public setup(
|
public setup(
|
||||||
{ http, uiSettings, savedObjects, getStartServices }: CoreSetup,
|
{ http, uiSettings, getStartServices }: CoreSetup,
|
||||||
{ features, licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies
|
{ features, licensing, indexManagement, usageCollection, dataViews, data }: Dependencies
|
||||||
) {
|
) {
|
||||||
this.license.setup(
|
this.license.setup(
|
||||||
{
|
{
|
||||||
|
@ -103,6 +103,8 @@ export class RollupPlugin implements Plugin<void, void, any, any> {
|
||||||
if (indexManagement && indexManagement.indexDataEnricher) {
|
if (indexManagement && indexManagement.indexDataEnricher) {
|
||||||
indexManagement.indexDataEnricher.add(rollupDataEnricher);
|
indexManagement.indexDataEnricher.add(rollupDataEnricher);
|
||||||
}
|
}
|
||||||
|
dataViews.enableRollups();
|
||||||
|
data.search.enableRollups();
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {}
|
start() {}
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { VisTypeTimeseriesSetup } from '@kbn/vis-type-timeseries-plugin/server';
|
||||||
import { getCapabilitiesForRollupIndices } from '@kbn/data-plugin/server';
|
import { getCapabilitiesForRollupIndices } from '@kbn/data-plugin/server';
|
||||||
import { IndexManagementPluginSetup } from '@kbn/index-management-plugin/server';
|
import { IndexManagementPluginSetup } from '@kbn/index-management-plugin/server';
|
||||||
import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server';
|
import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server';
|
||||||
|
import { DataViewsServerPluginSetup } from '@kbn/data-views-plugin/server';
|
||||||
|
import { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server';
|
||||||
import { LicensingPluginSetup } from '@kbn/licensing-plugin/server';
|
import { LicensingPluginSetup } from '@kbn/licensing-plugin/server';
|
||||||
import { License } from './services';
|
import { License } from './services';
|
||||||
import { IndexPatternsFetcher } from './shared_imports';
|
import { IndexPatternsFetcher } from './shared_imports';
|
||||||
|
@ -24,6 +26,8 @@ export interface Dependencies {
|
||||||
usageCollection?: UsageCollectionSetup;
|
usageCollection?: UsageCollectionSetup;
|
||||||
licensing: LicensingPluginSetup;
|
licensing: LicensingPluginSetup;
|
||||||
features: FeaturesPluginSetup;
|
features: FeaturesPluginSetup;
|
||||||
|
dataViews: DataViewsServerPluginSetup;
|
||||||
|
data: DataPluginSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RouteDependencies {
|
export interface RouteDependencies {
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"@kbn/i18n-react",
|
"@kbn/i18n-react",
|
||||||
"@kbn/config-schema",
|
"@kbn/config-schema",
|
||||||
"@kbn/shared-ux-router",
|
"@kbn/shared-ux-router",
|
||||||
|
"@kbn/data-views-plugin",
|
||||||
|
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|
|
@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
||||||
loadTestFile(require.resolve('./security_users'));
|
loadTestFile(require.resolve('./security_users'));
|
||||||
loadTestFile(require.resolve('./spaces'));
|
loadTestFile(require.resolve('./spaces'));
|
||||||
loadTestFile(require.resolve('./security_response_headers'));
|
loadTestFile(require.resolve('./security_response_headers'));
|
||||||
|
loadTestFile(require.resolve('./rollups'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from 'expect';
|
||||||
|
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
|
||||||
|
import { INITIAL_REST_VERSION_INTERNAL } from '@kbn/data-views-plugin/server/constants';
|
||||||
|
import { X_ELASTIC_INTERNAL_ORIGIN_REQUEST } from '@kbn/core-http-common/src/constants';
|
||||||
|
import { FIELDS_FOR_WILDCARD_PATH as BASE_URI } from '@kbn/data-views-plugin/common/constants';
|
||||||
|
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||||
|
|
||||||
|
export default function ({ getService }: FtrProviderContext) {
|
||||||
|
const supertest = getService('supertest');
|
||||||
|
const esArchiver = getService('esArchiver');
|
||||||
|
|
||||||
|
describe('rollup data views - fields for wildcard', function () {
|
||||||
|
before(async () => {
|
||||||
|
await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index');
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await esArchiver.unload(
|
||||||
|
'test/api_integration/fixtures/es_archiver/index_patterns/basic_index'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('returns 200 and best effort response despite lack of rollup support', async () => {
|
||||||
|
const response = await supertest
|
||||||
|
.get(BASE_URI)
|
||||||
|
.query({
|
||||||
|
pattern: 'basic_index',
|
||||||
|
type: 'rollup',
|
||||||
|
rollup_index: 'bar',
|
||||||
|
})
|
||||||
|
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL)
|
||||||
|
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'true');
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.fields.length).toEqual(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -44,5 +44,7 @@
|
||||||
"@kbn/fleet-plugin",
|
"@kbn/fleet-plugin",
|
||||||
"@kbn/cases-plugin",
|
"@kbn/cases-plugin",
|
||||||
"@kbn/test-subj-selector",
|
"@kbn/test-subj-selector",
|
||||||
|
"@kbn/core-http-common",
|
||||||
|
"@kbn/data-views-plugin",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue