Move Rollup out of legacy (#62891)

This commit is contained in:
CJ Cenizal 2020-04-24 14:27:35 -07:00 committed by GitHub
parent c0bf910a70
commit 18d1af24d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 1192 additions and 1043 deletions

View file

@ -4,7 +4,6 @@ files:
- 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss'
- 'src/legacy/core_plugins/vis_type_vislib/**/*.s+(a|c)ss'
- 'src/legacy/core_plugins/vis_type_xy/**/*.s+(a|c)ss'
- 'x-pack/legacy/plugins/rollup/**/*.s+(a|c)ss'
- 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss'
- 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss'
- 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss'

View file

@ -17,7 +17,6 @@ import { spaces } from './legacy/plugins/spaces';
import { canvas } from './legacy/plugins/canvas';
import { infra } from './legacy/plugins/infra';
import { taskManager } from './legacy/plugins/task_manager';
import { rollup } from './legacy/plugins/rollup';
import { siem } from './legacy/plugins/siem';
import { remoteClusters } from './legacy/plugins/remote_clusters';
import { upgradeAssistant } from './legacy/plugins/upgrade_assistant';
@ -43,7 +42,6 @@ module.exports = function(kibana) {
indexManagement(kibana),
infra(kibana),
taskManager(kibana),
rollup(kibana),
siem(kibana),
remoteClusters(kibana),
upgradeAssistant(kibana),

View file

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LICENSE_TYPE_BASIC, LicenseType } from '../../../common/constants';
export const PLUGIN = {
ID: 'rollup',
MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType,
getI18nName: (i18n: any): string => {
return i18n.translate('xpack.rollupJobs.appName', {
defaultMessage: 'Rollup jobs',
});
},
};
export * from '../../../../plugins/rollup/common';

View file

@ -1,43 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext } from 'src/core/server';
import { RollupSetup } from '../../../plugins/rollup/server';
import { PLUGIN } from './common';
import { plugin } from './server';
export function rollup(kibana: any) {
return new kibana.Plugin({
id: PLUGIN.ID,
configPrefix: 'xpack.rollup',
require: ['kibana', 'elasticsearch', 'xpack_main'],
init(server: any) {
const { core: coreSetup, plugins } = server.newPlatform.setup;
const { usageCollection, visTypeTimeseries, indexManagement } = plugins;
const rollupSetup = (plugins.rollup as unknown) as RollupSetup;
const initContext = ({
config: rollupSetup.__legacy.config,
logger: rollupSetup.__legacy.logger,
} as unknown) as PluginInitializerContext;
const rollupPluginInstance = plugin(initContext);
rollupPluginInstance.setup(coreSetup, {
usageCollection,
visTypeTimeseries,
indexManagement,
__LEGACY: {
plugins: {
xpack_main: server.plugins.xpack_main,
rollup: server.plugins[PLUGIN.ID],
},
},
});
},
});
}

View file

@ -1,15 +0,0 @@
{
"id": "rollup",
"version": "kibana",
"requiredPlugins": [
"home",
"index_management",
"visTypeTimeseries",
"indexPatternManagement"
],
"optionalPlugins": [
"usageCollection"
],
"server": true,
"ui": false
}

View file

@ -1,98 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const jobs = [
{
"job_id" : "foo1",
"rollup_index" : "foo_rollup",
"index_pattern" : "foo-*",
"fields" : {
"node" : [
{
"agg" : "terms"
}
],
"temperature" : [
{
"agg" : "min"
},
{
"agg" : "max"
},
{
"agg" : "sum"
}
],
"timestamp" : [
{
"agg" : "date_histogram",
"time_zone" : "UTC",
"interval" : "1h",
"delay": "7d"
}
],
"voltage" : [
{
"agg" : "histogram",
"interval": 5
},
{
"agg" : "sum"
}
]
}
},
{
"job_id" : "foo2",
"rollup_index" : "foo_rollup",
"index_pattern" : "foo-*",
"fields" : {
"host" : [
{
"agg" : "terms"
}
],
"timestamp" : [
{
"agg" : "date_histogram",
"time_zone" : "UTC",
"interval" : "1h",
"delay": "7d"
}
],
"voltage" : [
{
"agg" : "histogram",
"interval": 20
}
]
}
},
{
"job_id" : "foo3",
"rollup_index" : "foo_rollup",
"index_pattern" : "foo-*",
"fields" : {
"timestamp" : [
{
"agg" : "date_histogram",
"time_zone" : "PST",
"interval" : "1h",
"delay": "7d"
}
],
"voltage" : [
{
"agg" : "histogram",
"interval": 5
},
{
"agg" : "sum"
}
]
}
}
];

View file

@ -1,28 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ElasticsearchServiceSetup } from 'kibana/server';
import { once } from 'lodash';
import { elasticsearchJsPlugin } from '../../client/elasticsearch_rollup';
const callWithRequest = once((elasticsearchService: ElasticsearchServiceSetup) => {
const config = { plugins: [elasticsearchJsPlugin] };
return elasticsearchService.createClient('rollup', config);
});
export const callWithRequestFactory = (
elasticsearchService: ElasticsearchServiceSetup,
request: any
) => {
return (...args: any[]) => {
return (
callWithRequest(elasticsearchService)
.asScoped(request)
// @ts-ignore
.callAsCurrentUser(...args)
);
};
};

View file

@ -1,62 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { licensePreRoutingFactory } from '.';
import {
LICENSE_STATUS_VALID,
LICENSE_STATUS_INVALID,
} from '../../../../../common/constants/license_status';
import { kibanaResponseFactory } from '../../../../../../../src/core/server';
describe('licensePreRoutingFactory()', () => {
let mockServer;
let mockLicenseCheckResults;
beforeEach(() => {
mockServer = {
plugins: {
xpack_main: {
info: {
feature: () => ({
getLicenseCheckResults: () => mockLicenseCheckResults,
}),
},
},
},
};
});
describe('status is invalid', () => {
beforeEach(() => {
mockLicenseCheckResults = {
status: LICENSE_STATUS_INVALID,
};
});
it('replies with 403', () => {
const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => {});
const stubRequest = {};
const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory);
expect(response.status).to.be(403);
});
});
describe('status is valid', () => {
beforeEach(() => {
mockLicenseCheckResults = {
status: LICENSE_STATUS_VALID,
};
});
it('replies with nothing', () => {
const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => null);
const stubRequest = {};
const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory);
expect(response).to.be(null);
});
});
});

View file

@ -1,43 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
KibanaRequest,
KibanaResponseFactory,
RequestHandler,
RequestHandlerContext,
} from 'src/core/server';
import { PLUGIN } from '../../../common';
import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status';
import { ServerShim } from '../../types';
export const licensePreRoutingFactory = (
server: ServerShim,
handler: RequestHandler<any, any, any>
): RequestHandler<any, any, any> => {
const xpackMainPlugin = server.plugins.xpack_main;
// License checking and enable/disable logic
return function licensePreRouting(
ctx: RequestHandlerContext,
request: KibanaRequest,
response: KibanaResponseFactory
) {
const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults();
const { status } = licenseCheckResults;
if (status !== LICENSE_STATUS_VALID) {
return response.customError({
body: {
message: licenseCheckResults.messsage,
},
statusCode: 403,
});
}
return handler(ctx, request, response);
};
};

View file

@ -1,95 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup, Plugin, PluginInitializerContext, Logger } from 'src/core/server';
import { first } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server';
import { IndexManagementPluginSetup } from '../../../../plugins/index_management/server';
import { registerLicenseChecker } from '../../../server/lib/register_license_checker';
import { PLUGIN } from '../common';
import { ServerShim, RouteDependencies } from './types';
import {
registerIndicesRoute,
registerFieldsForWildcardRoute,
registerSearchRoute,
registerJobsRoute,
} from './routes/api';
import { registerRollupUsageCollector } from './collectors';
import { rollupDataEnricher } from './rollup_data_enricher';
import { registerRollupSearchStrategy } from './lib/search_strategies';
export class RollupsServerPlugin implements Plugin<void, void, any, any> {
log: Logger;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.log = initializerContext.logger.get();
}
async setup(
{ http, elasticsearch: elasticsearchService }: CoreSetup,
{
__LEGACY: serverShim,
usageCollection,
visTypeTimeseries,
indexManagement,
}: {
__LEGACY: ServerShim;
usageCollection?: UsageCollectionSetup;
visTypeTimeseries?: VisTypeTimeseriesSetup;
indexManagement?: IndexManagementPluginSetup;
}
) {
const elasticsearch = await elasticsearchService.adminClient;
const router = http.createRouter();
const routeDependencies: RouteDependencies = {
elasticsearch,
elasticsearchService,
router,
};
registerLicenseChecker(
serverShim as any,
PLUGIN.ID,
PLUGIN.getI18nName(i18n),
PLUGIN.MINIMUM_LICENSE_REQUIRED
);
registerIndicesRoute(routeDependencies, serverShim);
registerFieldsForWildcardRoute(routeDependencies, serverShim);
registerSearchRoute(routeDependencies, serverShim);
registerJobsRoute(routeDependencies, serverShim);
if (usageCollection) {
this.initializerContext.config.legacy.globalConfig$
.pipe(first())
.toPromise()
.then(config => {
registerRollupUsageCollector(usageCollection, config.kibana.index);
})
.catch(e => {
this.log.warn(`Registering Rollup collector failed: ${e}`);
});
}
if (indexManagement && indexManagement.indexDataEnricher) {
indexManagement.indexDataEnricher.add(rollupDataEnricher);
}
if (visTypeTimeseries) {
const { addSearchStrategy } = visTypeTimeseries;
registerRollupSearchStrategy(routeDependencies, addSearchStrategy);
}
}
start() {}
stop() {}
}

View file

@ -1,10 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { registerIndicesRoute } from './indices';
export { registerFieldsForWildcardRoute } from './index_patterns';
export { registerSearchRoute } from './search';
export { registerJobsRoute } from './jobs';

View file

@ -1,131 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { RequestHandler } from 'src/core/server';
import { indexBy } from 'lodash';
import { IndexPatternsFetcher } from '../../shared_imports';
import { RouteDependencies, ServerShim } from '../../types';
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsError } from '../../lib/is_es_error';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities';
import { mergeCapabilitiesWithFields, Field } from '../../lib/merge_capabilities_with_fields';
const parseMetaFields = (metaFields: string | string[]) => {
let parsedFields: string[] = [];
if (typeof metaFields === 'string') {
parsedFields = JSON.parse(metaFields);
} else {
parsedFields = metaFields;
}
return parsedFields;
};
const getFieldsForWildcardRequest = async (context: any, request: any, response: any) => {
const { callAsCurrentUser } = context.core.elasticsearch.dataClient;
const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser);
const { pattern, meta_fields: metaFields } = request.query;
let parsedFields: string[] = [];
try {
parsedFields = parseMetaFields(metaFields);
} catch (error) {
return response.badRequest({
body: error,
});
}
try {
const fields = await indexPatterns.getFieldsForWildcard({
pattern,
metaFields: parsedFields,
});
return response.ok({
body: { fields },
headers: {
'content-type': 'application/json',
},
});
} catch (error) {
return response.notFound();
}
};
/**
* Get list of fields for rollup index pattern, in the format of regular index pattern fields
*/
export function registerFieldsForWildcardRoute(deps: RouteDependencies, legacy: ServerShim) {
const handler: RequestHandler<any, any, any> = async (ctx, request, response) => {
const { params, meta_fields: metaFields } = request.query;
try {
// Make call and use field information from response
const { payload } = await getFieldsForWildcardRequest(ctx, request, response);
const fields = payload.fields;
const parsedParams = JSON.parse(params);
const rollupIndex = parsedParams.rollup_index;
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
const rollupFields: Field[] = [];
const fieldsFromFieldCapsApi: { [key: string]: any } = indexBy(fields, 'name');
const rollupIndexCapabilities = getCapabilitiesForRollupIndices(
await callWithRequest('rollup.rollupIndexCapabilities', {
indexPattern: rollupIndex,
})
)[rollupIndex].aggs;
// Keep meta fields
metaFields.forEach(
(field: string) =>
fieldsFromFieldCapsApi[field] && rollupFields.push(fieldsFromFieldCapsApi[field])
);
const mergedRollupFields = mergeCapabilitiesWithFields(
rollupIndexCapabilities,
fieldsFromFieldCapsApi,
rollupFields
);
return response.ok({ body: { fields: mergedRollupFields } });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
deps.router.get(
{
path: '/api/index_patterns/rollup/_fields_for_wildcard',
validate: {
query: schema.object({
pattern: schema.string(),
meta_fields: schema.arrayOf(schema.string(), {
defaultValue: [],
}),
params: schema.string({
validate(value) {
try {
const params = JSON.parse(value);
const keys = Object.keys(params);
const { rollup_index: rollupIndex } = params;
if (!rollupIndex) {
return '[request query.params]: "rollup_index" is required';
} else if (keys.length > 1) {
const invalidParams = keys.filter(key => key !== 'rollup_index');
return `[request query.params]: ${invalidParams.join(', ')} is not allowed`;
}
} catch (err) {
return '[request query.params]: expected JSON string';
}
},
}),
}),
},
},
licensePreRoutingFactory(legacy, handler)
);
}

View file

@ -1,175 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { RequestHandler } from 'src/core/server';
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsError } from '../../lib/is_es_error';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities';
import { API_BASE_PATH } from '../../../common';
import { RouteDependencies, ServerShim } from '../../types';
type NumericField =
| 'long'
| 'integer'
| 'short'
| 'byte'
| 'scaled_float'
| 'double'
| 'float'
| 'half_float';
interface FieldCapability {
date?: any;
keyword?: any;
long?: any;
integer?: any;
short?: any;
byte?: any;
double?: any;
float?: any;
half_float?: any;
scaled_float?: any;
}
interface FieldCapabilities {
fields: FieldCapability[];
}
function isNumericField(fieldCapability: FieldCapability) {
const numericTypes = [
'long',
'integer',
'short',
'byte',
'double',
'float',
'half_float',
'scaled_float',
];
return numericTypes.some(numericType => fieldCapability[numericType as NumericField] != null);
}
export function registerIndicesRoute(deps: RouteDependencies, legacy: ServerShim) {
const getIndicesHandler: RequestHandler<any, any, any> = async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
try {
const data = await callWithRequest('rollup.rollupIndexCapabilities', {
indexPattern: '_all',
});
return response.ok({ body: getCapabilitiesForRollupIndices(data) });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
const validateIndexPatternHandler: RequestHandler<any, any, any> = async (
ctx,
request,
response
) => {
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
try {
const { indexPattern } = request.params;
const [fieldCapabilities, rollupIndexCapabilities]: [
FieldCapabilities,
{ [key: string]: any }
] = await Promise.all([
callWithRequest('rollup.fieldCapabilities', { indexPattern }),
callWithRequest('rollup.rollupIndexCapabilities', { indexPattern }),
]);
const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0;
const doesMatchRollupIndices = Object.entries(rollupIndexCapabilities).length !== 0;
const dateFields: string[] = [];
const numericFields: string[] = [];
const keywordFields: string[] = [];
const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields);
fieldCapabilitiesEntries.forEach(
([fieldName, fieldCapability]: [string, FieldCapability]) => {
if (fieldCapability.date) {
dateFields.push(fieldName);
return;
}
if (isNumericField(fieldCapability)) {
numericFields.push(fieldName);
return;
}
if (fieldCapability.keyword) {
keywordFields.push(fieldName);
}
}
);
const body = {
doesMatchIndices,
doesMatchRollupIndices,
dateFields,
numericFields,
keywordFields,
};
return response.ok({ body });
} catch (err) {
// 404s are still valid results.
if (err.statusCode === 404) {
const notFoundBody = {
doesMatchIndices: false,
doesMatchRollupIndices: false,
dateFields: [],
numericFields: [],
keywordFields: [],
};
return response.ok({ body: notFoundBody });
}
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
/**
* Returns a list of all rollup index names
*/
deps.router.get(
{
path: `${API_BASE_PATH}/indices`,
validate: false,
},
licensePreRoutingFactory(legacy, getIndicesHandler)
);
/**
* Returns information on validity of an index pattern for creating a rollup job:
* - Does the index pattern match any indices?
* - Does the index pattern match rollup indices?
* - Which date fields, numeric fields, and keyword fields are available in the matching indices?
*/
deps.router.get(
{
path: `${API_BASE_PATH}/index_pattern_validity/{indexPattern}`,
validate: {
params: schema.object({
indexPattern: schema.string(),
}),
},
},
licensePreRoutingFactory(legacy, validateIndexPatternHandler)
);
}

View file

@ -1,178 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { RequestHandler } from 'src/core/server';
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsError } from '../../lib/is_es_error';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { API_BASE_PATH } from '../../../common';
import { RouteDependencies, ServerShim } from '../../types';
export function registerJobsRoute(deps: RouteDependencies, legacy: ServerShim) {
const getJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
try {
const data = await callWithRequest('rollup.jobs');
return response.ok({ body: data });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
const createJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => {
try {
const { id, ...rest } = request.body.job;
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
// Create job.
await callWithRequest('rollup.createJob', {
id,
body: rest,
});
// Then request the newly created job.
const results = await callWithRequest('rollup.job', { id });
return response.ok({ body: results.jobs[0] });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
const startJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => {
try {
const { jobIds } = request.body;
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
const data = await Promise.all(
jobIds.map((id: string) => callWithRequest('rollup.startJob', { id }))
).then(() => ({ success: true }));
return response.ok({ body: data });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
const stopJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => {
try {
const { jobIds } = request.body;
// For our API integration tests we need to wait for the jobs to be stopped
// in order to be able to delete them sequencially.
const { waitForCompletion } = request.query;
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
const stopRollupJob = (id: string) =>
callWithRequest('rollup.stopJob', {
id,
waitForCompletion: waitForCompletion === 'true',
});
const data = await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true }));
return response.ok({ body: data });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
const deleteJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => {
try {
const { jobIds } = request.body;
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
const data = await Promise.all(
jobIds.map((id: string) => callWithRequest('rollup.deleteJob', { id }))
).then(() => ({ success: true }));
return response.ok({ body: data });
} catch (err) {
// There is an issue opened on ES to handle the following error correctly
// https://github.com/elastic/elasticsearch/issues/42908
// Until then we'll modify the response here.
if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) {
err.status = 400;
err.statusCode = 400;
err.displayName = 'Bad request';
err.message = JSON.parse(err.response).task_failures[0].reason.reason;
}
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
deps.router.get(
{
path: `${API_BASE_PATH}/jobs`,
validate: false,
},
licensePreRoutingFactory(legacy, getJobsHandler)
);
deps.router.put(
{
path: `${API_BASE_PATH}/create`,
validate: {
body: schema.object({
job: schema.object(
{
id: schema.string(),
},
{ unknowns: 'allow' }
),
}),
},
},
licensePreRoutingFactory(legacy, createJobsHandler)
);
deps.router.post(
{
path: `${API_BASE_PATH}/start`,
validate: {
body: schema.object({
jobIds: schema.arrayOf(schema.string()),
}),
query: schema.maybe(
schema.object({
waitForCompletion: schema.maybe(schema.string()),
})
),
},
},
licensePreRoutingFactory(legacy, startJobsHandler)
);
deps.router.post(
{
path: `${API_BASE_PATH}/stop`,
validate: {
body: schema.object({
jobIds: schema.arrayOf(schema.string()),
}),
},
},
licensePreRoutingFactory(legacy, stopJobsHandler)
);
deps.router.post(
{
path: `${API_BASE_PATH}/delete`,
validate: {
body: schema.object({
jobIds: schema.arrayOf(schema.string()),
}),
},
},
licensePreRoutingFactory(legacy, deleteJobsHandler)
);
}

View file

@ -1,50 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { RequestHandler } from 'src/core/server';
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsError } from '../../lib/is_es_error';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { API_BASE_PATH } from '../../../common';
import { RouteDependencies, ServerShim } from '../../types';
export function registerSearchRoute(deps: RouteDependencies, legacy: ServerShim) {
const handler: RequestHandler<any, any, any> = async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request);
try {
const requests = request.body.map(({ index, query }: { index: string; query: any }) =>
callWithRequest('rollup.search', {
index,
rest_total_hits_as_int: true,
body: query,
})
);
const data = await Promise.all(requests);
return response.ok({ body: data });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
};
deps.router.post(
{
path: `${API_BASE_PATH}/search`,
validate: {
body: schema.arrayOf(
schema.object({
index: schema.string(),
query: schema.any(),
})
),
},
},
licensePreRoutingFactory(legacy, handler)
);
}

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'src/core/server';
import { XPackMainPlugin } from '../../xpack_main/server/xpack_main';
export interface ServerShim {
plugins: {
xpack_main: XPackMainPlugin;
rollup: any;
};
}
export interface RouteDependencies {
router: IRouter;
elasticsearchService: ElasticsearchServiceSetup;
elasticsearch: IClusterClient;
}

View file

@ -1,3 +0,0 @@
{
"extends": "../../../tsconfig.json"
}

View file

@ -14,7 +14,7 @@ The rest of this doc dives into the implementation details of each of the above
## Create and manage rollup jobs
The most straight forward part of this plugin! A new app called Rollup Jobs is registered in the Management section and follows a typical CRUD UI pattern. This app allows users to create, start, stop, clone, and delete rollup jobs. There is no way to edit an existing rollup job; instead, the UI offers a cloning ability. The client-side portion of this app lives [here](../../../plugins/rollup/public/crud_app) and uses endpoints registered [here](server/routes/api/jobs.js).
The most straight forward part of this plugin! A new app called Rollup Jobs is registered in the Management section and follows a typical CRUD UI pattern. This app allows users to create, start, stop, clone, and delete rollup jobs. There is no way to edit an existing rollup job; instead, the UI offers a cloning ability. The client-side portion of this app lives in [public/crud_app](public/crud_app) and uses endpoints registered in [(server/routes/api/jobs](server/routes/api/jobs).
Refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-getting-started.html) to understand rollup indices and how to create rollup jobs.
@ -22,22 +22,22 @@ Refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elast
Kibana uses index patterns to consume and visualize rollup indices. Typically, Kibana can inspect the indices captured by an index pattern, identify its aggregations and fields, and determine how to consume the data. Rollup indices don't contain this type of information, so we predefine how to consume a rollup index pattern with the type and typeMeta fields on the index pattern saved object. All rollup index patterns have `type` defined as "rollup" and `typeMeta` defined as an object of the index pattern's capabilities.
In the Index Pattern app, the "Create index pattern" button includes a context menu when a rollup index is detected. This menu offers items for creating a standard index pattern and a rollup index pattern. A [rollup config is registered to index pattern creation extension point](../../../plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js). The context menu behavior in particular uses the `getIndexPatternCreationOption()` method. When the user chooses to create a rollup index pattern, this config changes the behavior of the index pattern creation wizard:
In the Index Pattern app, the "Create index pattern" button includes a context menu when a rollup index is detected. This menu offers items for creating a standard index pattern and a rollup index pattern. A [rollup config is registered to index pattern creation extension point](public/index_pattern_creation/rollup_index_pattern_creation_config.js). The context menu behavior in particular uses the `getIndexPatternCreationOption()` method. When the user chooses to create a rollup index pattern, this config changes the behavior of the index pattern creation wizard:
1. Adds a `Rollup` badge to rollup indices using `getIndexTags()`.
2. Enforces index pattern rules using `checkIndicesForErrors()`. Rollup index patterns must match **one** rollup index, and optionally, any number of regular indices. A rollup index pattern configured with one or more regular indices is known as a "hybrid" index pattern. This allows the user to visualize historical (rollup) data and live (regular) data in the same visualization.
3. Routes to this plugin's [rollup `_fields_for_wildcard` endpoint](server/routes/api/index_patterns.js), instead of the standard one, using `getFetchForWildcardOptions()`, so that the internal rollup data field names are mapped to the original field names.
3. Routes to this plugin's [rollup `_fields_for_wildcard` endpoint](server/routes/api/index_patterns/register_fields_for_wildcard_route.ts), instead of the standard one, using `getFetchForWildcardOptions()`, so that the internal rollup data field names are mapped to the original field names.
4. Writes additional information about aggregations, fields, histogram interval, and date histogram interval and timezone to the rollup index pattern saved object using `getIndexPatternMappings()`. This collection of information is referred to as its "capabilities".
Once a rollup index pattern is created, it is tagged with `Rollup` in the list of index patterns, and its details page displays capabilities information. This is done by registering [yet another config for the index pattern list](../../../plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js) extension points.
Once a rollup index pattern is created, it is tagged with `Rollup` in the list of index patterns, and its details page displays capabilities information. This is done by registering [yet another config for the index pattern list](public/index_pattern_list/rollup_index_pattern_list_config.js) extension points.
## Create visualizations from rollup index patterns
This plugin enables the user to create visualizations from rollup data using the Visualize app, excluding TSVB, Vega, and Timelion. When Visualize sends search requests, this plugin routes the requests to the [Elasticsearch rollup search endpoint](https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-search.html), which searches the special document structure within rollup indices. The visualization options available to users are based on the capabilities of the rollup index pattern they're visualizing.
Routing to the Elasticsearch rollup search endpoint is done by creating an extension point in Courier, effectively allowing multiple "search strategies" to be registered. A [rollup search strategy](../../../plugins/rollup/public/search/register.js) is registered by this plugin that queries [this plugin's rollup search endpoint](server/routes/api/search.js).
Routing to the Elasticsearch rollup search endpoint is done by creating an extension point in Courier, effectively allowing multiple "search strategies" to be registered. A [rollup search strategy](public/search/register.js) is registered by this plugin that queries [this plugin's rollup search endpoint](server/routes/api/search.js).
Limiting visualization editor options is done by [registering configs](../../../plugins/rollup/public/visualize/index.js) to various vis extension points. These configs use information stored on the rollup index pattern to limit:
Limiting visualization editor options is done by [registering configs](public/visualize/index.js) to various vis extension points. These configs use information stored on the rollup index pattern to limit:
* Available aggregation types
* Available fields for a particular aggregation
* Default and base interval for histogram aggregation
@ -47,6 +47,6 @@ Limiting visualization editor options is done by [registering configs](../../../
In Index Management, similar to system indices, rollup indices are hidden by default. A toggle is provided to show rollup indices and add a badge to the table rows. This is done by using Index Management's extension points.
The toggle and badge are registered on client-side [here](../../../plugins/rollup/public/extend_index_management/index.js).
The toggle and badge are registered on the client-side in [public/extend_index_management](public/extend_index_management).
Additional data needed to filter rollup indices in Index Management is provided with a [data enricher](rollup_data_enricher.js).
Additional data needed to filter rollup indices in Index Management is provided with a [data enricher](rollup_data_enricher.ts).

View file

@ -4,6 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { LicenseType } from '../../licensing/common/types';
const basicLicense: LicenseType = 'basic';
export const PLUGIN = {
ID: 'rollup',
minimumLicenseType: basicLicense,
};
export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns';
export const API_BASE_PATH = '/api/rollup';

View file

@ -4,6 +4,17 @@
"kibanaVersion": "kibana",
"server": true,
"ui": true,
"optionalPlugins": ["home", "indexManagement", "indexPatternManagement", "usageCollection"],
"requiredPlugins": ["management", "data"]
"requiredPlugins": [
"indexPatternManagement",
"management",
"licensing",
"data"
],
"optionalPlugins": [
"home",
"indexManagement",
"usageCollection",
"visTypeTimeseries"
],
"configPath": ["xpack", "rollup"]
}

View file

@ -42,7 +42,7 @@ export const stepIds = [
* 1. getDefaultFields: (overrides) => object
* 2. fieldValidations
*
* See x-pack/plugins/rollup/public/crud_app/services/jobs.js for more information on override's shape
* See rollup/public/crud_app/services/jobs.js for more information on override's shape
*/
export const stepIdToStepConfigMap = {
[STEP_LOGISTICS]: {

View file

@ -16,6 +16,9 @@ export { METRIC_TYPE };
export function trackUserRequest<TResponse>(request: Promise<TResponse>, actionType: string) {
// Only track successful actions.
return request.then(response => {
// NOTE: METRIC_TYPE.LOADED is probably the wrong metric type here. The correct metric type
// is more likely METRIC_TYPE.APPLICATION_USAGE. This change was introduced in
// https://github.com/elastic/kibana/pull/41113/files#diff-58ac12bdd1a3a05a24e69ff20633c482R20
trackUiMetric(METRIC_TYPE.LOADED, actionType);
// We return the response immediately without waiting for the tracking request to resolve,
// to avoid adding additional latency.

View file

@ -5,21 +5,34 @@
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCallOut } from '@elastic/eui';
export const RollupPrompt = () => (
<EuiCallOut color="warning" iconType="help" title="Beta feature">
<p>
Kibana&apos;s support for rollup index patterns is in beta. You might encounter issues using
these patterns in saved searches, visualizations, and dashboards. They are not supported in
some advanced features, such as Timelion, and Machine Learning.
{i18n.translate(
'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text',
{
defaultMessage:
"Kibana's support for rollup index patterns is in beta. You might encounter issues using " +
'these patterns in saved searches, visualizations, and dashboards. They are not supported in ' +
'some advanced features, such as Timelion, and Machine Learning.',
}
)}
</p>
<p>
You can match a rollup index pattern against one rollup index and zero or more regular
indices. A rollup index pattern has limited metrics, fields, intervals, and aggregations. A
rollup index is limited to indices that have one job configuration, or multiple jobs with
compatible configurations.
{i18n.translate(
'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text',
{
defaultMessage:
'You can match a rollup index pattern against one rollup index and zero or more regular ' +
'indices. A rollup index pattern has limited metrics, fields, intervals, and aggregations. A ' +
'rollup index is limited to indices that have one job configuration, or multiple jobs with ' +
'compatible configurations.',
}
)}
</p>
</EuiCallOut>
);

View file

@ -4,4 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { isEsError } from './is_es_error';
import { schema, TypeOf } from '@kbn/config-schema';
export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
});
export type RollupConfig = TypeOf<typeof configSchema>;

View file

@ -4,9 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext } from 'src/core/server';
import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server';
import { RollupPlugin } from './plugin';
import { configSchema, RollupConfig } from './config';
export const plugin = (initContext: PluginInitializerContext) => new RollupPlugin(initContext);
export const plugin = (pluginInitializerContext: PluginInitializerContext) =>
new RollupPlugin(pluginInitializerContext);
export { RollupSetup } from './plugin';
export const config: PluginConfigDescriptor<RollupConfig> = {
schema: configSchema,
};

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
export const jobs = [
{
job_id: 'foo1',
rollup_index: 'foo_rollup',
index_pattern: 'foo-*',
fields: {
node: [
{
agg: 'terms',
},
],
temperature: [
{
agg: 'min',
},
{
agg: 'max',
},
{
agg: 'sum',
},
],
timestamp: [
{
agg: 'date_histogram',
time_zone: 'UTC',
interval: '1h',
delay: '7d',
},
],
voltage: [
{
agg: 'histogram',
interval: 5,
},
{
agg: 'sum',
},
],
},
},
{
job_id: 'foo2',
rollup_index: 'foo_rollup',
index_pattern: 'foo-*',
fields: {
host: [
{
agg: 'terms',
},
],
timestamp: [
{
agg: 'date_histogram',
time_zone: 'UTC',
interval: '1h',
delay: '7d',
},
],
voltage: [
{
agg: 'histogram',
interval: 20,
},
],
},
},
{
job_id: 'foo3',
rollup_index: 'foo_rollup',
index_pattern: 'foo-*',
fields: {
timestamp: [
{
agg: 'date_histogram',
time_zone: 'PST',
interval: '1h',
delay: '7d',
},
],
voltage: [
{
agg: 'histogram',
interval: 5,
},
{
agg: 'sum',
},
],
},
},
];

View file

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
function extractCausedByChain(
causedBy: Record<string, any> = {},
accumulator: string[] = []
): string[] {
const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/camelcase
if (reason) {
accumulator.push(reason);
}
// eslint-disable-next-line @typescript-eslint/camelcase
if (caused_by) {
return extractCausedByChain(caused_by, accumulator);
}
return accumulator;
}
/**
* Wraps an error thrown by the ES JS client into a Boom error response and returns it
*
* @param err Object Error thrown by ES JS client
* @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages
*/
export function wrapEsError(
err: any,
statusCodeToMessageMap: Record<string, string> = {}
): { message: string; body?: { cause?: string[] }; statusCode: number } {
const { statusCode, response } = err;
const {
error: {
root_cause = [], // eslint-disable-line @typescript-eslint/camelcase
caused_by = undefined, // eslint-disable-line @typescript-eslint/camelcase
} = {},
} = JSON.parse(response);
// If no custom message if specified for the error's status code, just
// wrap the error as a Boom error response and return it
if (!statusCodeToMessageMap[statusCode]) {
// The caused_by chain has the most information so use that if it's available. If not then
// settle for the root_cause.
const causedByChain = extractCausedByChain(caused_by);
const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined;
return {
message: err.message,
statusCode,
body: {
cause: causedByChain.length ? causedByChain : defaultCause,
},
};
}
// Otherwise, use the custom message to create a Boom error response and
// return it
const message = statusCodeToMessageMap[statusCode];
return { message, statusCode };
}
export function formatEsError(err: any): any {
const { statusCode, message, body } = wrapEsError(err);
return {
statusCode,
body: {
message,
attributes: {
cause: body?.cause,
},
},
};
}

View file

@ -3,18 +3,19 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getRollupSearchStrategy } from './rollup_search_strategy';
import { getRollupSearchRequest } from './rollup_search_request';
import { getRollupSearchCapabilities } from './rollup_search_capabilities';
import {
AbstractSearchRequest,
DefaultSearchCapabilities,
AbstractSearchStrategy,
} from '../../../../../../../src/plugins/vis_type_timeseries/server';
import { RouteDependencies } from '../../types';
} from '../../../../../../src/plugins/vis_type_timeseries/server';
import { CallWithRequestFactoryShim } from '../../types';
import { getRollupSearchStrategy } from './rollup_search_strategy';
import { getRollupSearchRequest } from './rollup_search_request';
import { getRollupSearchCapabilities } from './rollup_search_capabilities';
export const registerRollupSearchStrategy = (
{ elasticsearchService }: RouteDependencies,
callWithRequestFactory: CallWithRequestFactoryShim,
addSearchStrategy: (searchStrategy: any) => void
) => {
const RollupSearchRequest = getRollupSearchRequest(AbstractSearchRequest);
@ -22,8 +23,9 @@ export const registerRollupSearchStrategy = (
const RollupSearchStrategy = getRollupSearchStrategy(
AbstractSearchStrategy,
RollupSearchRequest,
RollupSearchCapabilities
RollupSearchCapabilities,
callWithRequestFactory
);
addSearchStrategy(new RollupSearchStrategy(elasticsearchService));
addSearchStrategy(new RollupSearchStrategy());
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { get, has } from 'lodash';
import { KibanaRequest } from 'kibana/server';
import { KibanaRequest } from 'src/core/server';
import { leastCommonInterval, isCalendarInterval } from './lib/interval_helper';
export const getRollupSearchCapabilities = (DefaultSearchCapabilities: any) =>

View file

@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { indexBy, isString } from 'lodash';
import { ElasticsearchServiceSetup, KibanaRequest } from 'kibana/server';
import { callWithRequestFactory } from '../call_with_request_factory';
import { KibanaRequest } from 'src/core/server';
import { CallWithRequestFactoryShim } from '../../types';
import { mergeCapabilitiesWithFields } from '../merge_capabilities_with_fields';
import { getCapabilitiesForRollupIndices } from '../map_capabilities';
@ -20,13 +21,16 @@ const isIndexPatternValid = (indexPattern: string) =>
export const getRollupSearchStrategy = (
AbstractSearchStrategy: any,
RollupSearchRequest: any,
RollupSearchCapabilities: any
RollupSearchCapabilities: any,
callWithRequestFactory: CallWithRequestFactoryShim
) =>
class RollupSearchStrategy extends AbstractSearchStrategy {
name = 'rollup';
constructor(elasticsearchService: ElasticsearchServiceSetup) {
super(elasticsearchService, callWithRequestFactory, RollupSearchRequest);
constructor() {
// TODO: When vis_type_timeseries and AbstractSearchStrategy are migrated to the NP, it
// shouldn't require elasticsearchService to be injected, and we can remove this null argument.
super(null, callWithRequestFactory, RollupSearchRequest);
}
getRollupData(req: KibanaRequest, indexPattern: string) {

View file

@ -4,20 +4,98 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server';
declare module 'src/core/server' {
interface RequestHandlerContext {
rollup?: RollupContext;
}
}
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import {
CoreSetup,
Plugin,
Logger,
KibanaRequest,
PluginInitializerContext,
IScopedClusterClient,
APICaller,
SharedGlobalConfig,
} from 'src/core/server';
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { CONFIG_ROLLUPS } from '../common';
export class RollupPlugin implements Plugin<RollupSetup> {
private readonly initContext: PluginInitializerContext;
import { PLUGIN, CONFIG_ROLLUPS } from '../common';
import { Dependencies, CallWithRequestFactoryShim } from './types';
import { registerApiRoutes } from './routes';
import { License } from './services';
import { registerRollupUsageCollector } from './collectors';
import { rollupDataEnricher } from './rollup_data_enricher';
import { IndexPatternsFetcher } from './shared_imports';
import { registerRollupSearchStrategy } from './lib/search_strategies';
import { elasticsearchJsPlugin } from './client/elasticsearch_rollup';
import { isEsError } from './lib/is_es_error';
import { formatEsError } from './lib/format_es_error';
import { getCapabilitiesForRollupIndices } from './lib/map_capabilities';
import { mergeCapabilitiesWithFields } from './lib/merge_capabilities_with_fields';
constructor(initContext: PluginInitializerContext) {
this.initContext = initContext;
interface RollupContext {
client: IScopedClusterClient;
}
export class RollupPlugin implements Plugin<void, void, any, any> {
private readonly logger: Logger;
private readonly globalConfig$: Observable<SharedGlobalConfig>;
private readonly license: License;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
this.globalConfig$ = initializerContext.config.legacy.globalConfig$;
this.license = new License();
}
public setup(core: CoreSetup) {
core.uiSettings.register({
public setup(
{ http, uiSettings, elasticsearch }: CoreSetup,
{ licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies
) {
this.license.setup(
{
pluginId: PLUGIN.ID,
minimumLicenseType: PLUGIN.minimumLicenseType,
defaultErrorMessage: i18n.translate('xpack.rollupJobs.licenseCheckErrorMessage', {
defaultMessage: 'License check failed',
}),
},
{
licensing,
logger: this.logger,
}
);
// Extend the elasticsearchJs client with additional endpoints.
const esClientConfig = { plugins: [elasticsearchJsPlugin] };
const rollupEsClient = elasticsearch.createClient('rollup', esClientConfig);
http.registerRouteHandlerContext('rollup', (context, request) => {
return {
client: rollupEsClient.asScoped(request),
};
});
registerApiRoutes({
router: http.createRouter(),
license: this.license,
lib: {
isEsError,
formatEsError,
getCapabilitiesForRollupIndices,
mergeCapabilitiesWithFields,
},
sharedImports: {
IndexPatternsFetcher,
},
});
uiSettings.register({
[CONFIG_ROLLUPS]: {
name: i18n.translate('xpack.rollupJobs.rollupIndexPatternsTitle', {
defaultMessage: 'Enable rollup index patterns',
@ -33,22 +111,34 @@ export class RollupPlugin implements Plugin<RollupSetup> {
},
});
return {
__legacy: {
config: this.initContext.config,
logger: this.initContext.logger,
},
};
if (visTypeTimeseries) {
// TODO: When vis_type_timeseries is fully migrated to the NP, it shouldn't require this shim.
const callWithRequestFactoryShim = (
elasticsearchServiceShim: CallWithRequestFactoryShim,
request: KibanaRequest
): APICaller => rollupEsClient.asScoped(request).callAsCurrentUser;
const { addSearchStrategy } = visTypeTimeseries;
registerRollupSearchStrategy(callWithRequestFactoryShim, addSearchStrategy);
}
if (usageCollection) {
this.globalConfig$
.pipe(first())
.toPromise()
.then(globalConfig => {
registerRollupUsageCollector(usageCollection, globalConfig.kibana.index);
})
.catch((e: any) => {
this.logger.warn(`Registering Rollup collector failed: ${e}`);
});
}
if (indexManagement && indexManagement.indexDataEnricher) {
indexManagement.indexDataEnricher.add(rollupDataEnricher);
}
}
public start() {}
public stop() {}
}
export interface RollupSetup {
/** @deprecated */
__legacy: {
config: PluginInitializerContext['config'];
logger: PluginInitializerContext['logger'];
};
start() {}
stop() {}
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Index } from '../../../../plugins/index_management/server';
import { Index } from '../../../plugins/index_management/server';
export const rollupDataEnricher = async (indicesList: Index[], callWithRequest: any) => {
if (!indicesList || !indicesList.length) {

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { RouteDependencies } from '../../../types';
import { registerFieldsForWildcardRoute } from './register_fields_for_wildcard_route';
export function registerIndexPatternsRoutes(dependencies: RouteDependencies) {
registerFieldsForWildcardRoute(dependencies);
}

View file

@ -0,0 +1,141 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { indexBy } from 'lodash';
import { schema } from '@kbn/config-schema';
import { Field } from '../../../lib/merge_capabilities_with_fields';
import { RouteDependencies } from '../../../types';
const parseMetaFields = (metaFields: string | string[]) => {
let parsedFields: string[] = [];
if (typeof metaFields === 'string') {
parsedFields = JSON.parse(metaFields);
} else {
parsedFields = metaFields;
}
return parsedFields;
};
const getFieldsForWildcardRequest = async (
context: any,
request: any,
response: any,
IndexPatternsFetcher: any
) => {
const { callAsCurrentUser } = context.core.elasticsearch.dataClient;
const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser);
const { pattern, meta_fields: metaFields } = request.query;
let parsedFields: string[] = [];
try {
parsedFields = parseMetaFields(metaFields);
} catch (error) {
return response.badRequest({
body: error,
});
}
try {
const fields = await indexPatterns.getFieldsForWildcard({
pattern,
metaFields: parsedFields,
});
return response.ok({
body: { fields },
headers: {
'content-type': 'application/json',
},
});
} catch (error) {
return response.notFound();
}
};
/**
* Get list of fields for rollup index pattern, in the format of regular index pattern fields
*/
export const registerFieldsForWildcardRoute = ({
router,
license,
lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices, mergeCapabilitiesWithFields },
sharedImports: { IndexPatternsFetcher },
}: RouteDependencies) => {
const querySchema = schema.object({
pattern: schema.string(),
meta_fields: schema.arrayOf(schema.string(), {
defaultValue: [],
}),
params: schema.string({
validate(value) {
try {
const params = JSON.parse(value);
const keys = Object.keys(params);
const { rollup_index: rollupIndex } = params;
if (!rollupIndex) {
return '[request query.params]: "rollup_index" is required';
} else if (keys.length > 1) {
const invalidParams = keys.filter(key => key !== 'rollup_index');
return `[request query.params]: ${invalidParams.join(', ')} is not allowed`;
}
} catch (err) {
return '[request query.params]: expected JSON string';
}
},
}),
});
router.get(
{
path: '/api/index_patterns/rollup/_fields_for_wildcard',
validate: {
query: querySchema,
},
},
license.guardApiRoute(async (context, request, response) => {
const { params, meta_fields: metaFields } = request.query;
try {
// Make call and use field information from response
const { payload } = await getFieldsForWildcardRequest(
context,
request,
response,
IndexPatternsFetcher
);
const fields = payload.fields;
const parsedParams = JSON.parse(params);
const rollupIndex = parsedParams.rollup_index;
const rollupFields: Field[] = [];
const fieldsFromFieldCapsApi: { [key: string]: any } = indexBy(fields, 'name');
const rollupIndexCapabilities = getCapabilitiesForRollupIndices(
await context.rollup!.client.callAsCurrentUser('rollup.rollupIndexCapabilities', {
indexPattern: rollupIndex,
})
)[rollupIndex].aggs;
// Keep meta fields
metaFields.forEach(
(field: string) =>
fieldsFromFieldCapsApi[field] && rollupFields.push(fieldsFromFieldCapsApi[field])
);
const mergedRollupFields = mergeCapabilitiesWithFields(
rollupIndexCapabilities,
fieldsFromFieldCapsApi,
rollupFields
);
return response.ok({ body: { fields: mergedRollupFields } });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
})
);
};

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { RouteDependencies } from '../../../types';
import { registerGetRoute } from './register_get_route';
import { registerValidateIndexPatternRoute } from './register_validate_index_pattern_route';
export function registerIndicesRoutes(dependencies: RouteDependencies) {
registerGetRoute(dependencies);
registerValidateIndexPatternRoute(dependencies);
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { addBasePath } from '../../../services';
import { RouteDependencies } from '../../../types';
/**
* Returns a list of all rollup index names
*/
export const registerGetRoute = ({
router,
license,
lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices },
}: RouteDependencies) => {
router.get(
{
path: addBasePath('/indices'),
validate: false,
},
license.guardApiRoute(async (context, request, response) => {
try {
const data = await context.rollup!.client.callAsCurrentUser(
'rollup.rollupIndexCapabilities',
{
indexPattern: '_all',
}
);
return response.ok({ body: getCapabilitiesForRollupIndices(data) });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
})
);
};

View file

@ -0,0 +1,142 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { addBasePath } from '../../../services';
import { RouteDependencies } from '../../../types';
type NumericField =
| 'long'
| 'integer'
| 'short'
| 'byte'
| 'scaled_float'
| 'double'
| 'float'
| 'half_float';
interface FieldCapability {
date?: any;
keyword?: any;
long?: any;
integer?: any;
short?: any;
byte?: any;
double?: any;
float?: any;
half_float?: any;
scaled_float?: any;
}
interface FieldCapabilities {
fields: FieldCapability[];
}
function isNumericField(fieldCapability: FieldCapability) {
const numericTypes = [
'long',
'integer',
'short',
'byte',
'double',
'float',
'half_float',
'scaled_float',
];
return numericTypes.some(numericType => fieldCapability[numericType as NumericField] != null);
}
/**
* Returns information on validity of an index pattern for creating a rollup job:
* - Does the index pattern match any indices?
* - Does the index pattern match rollup indices?
* - Which date fields, numeric fields, and keyword fields are available in the matching indices?
*/
export const registerValidateIndexPatternRoute = ({
router,
license,
lib: { isEsError, formatEsError },
}: RouteDependencies) => {
router.get(
{
path: addBasePath('/index_pattern_validity/{indexPattern}'),
validate: {
params: schema.object({
indexPattern: schema.string(),
}),
},
},
license.guardApiRoute(async (context, request, response) => {
try {
const { indexPattern } = request.params;
const [fieldCapabilities, rollupIndexCapabilities]: [
FieldCapabilities,
{ [key: string]: any }
] = await Promise.all([
context.rollup!.client.callAsCurrentUser('rollup.fieldCapabilities', { indexPattern }),
context.rollup!.client.callAsCurrentUser('rollup.rollupIndexCapabilities', {
indexPattern,
}),
]);
const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0;
const doesMatchRollupIndices = Object.entries(rollupIndexCapabilities).length !== 0;
const dateFields: string[] = [];
const numericFields: string[] = [];
const keywordFields: string[] = [];
const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields);
fieldCapabilitiesEntries.forEach(
([fieldName, fieldCapability]: [string, FieldCapability]) => {
if (fieldCapability.date) {
dateFields.push(fieldName);
return;
}
if (isNumericField(fieldCapability)) {
numericFields.push(fieldName);
return;
}
if (fieldCapability.keyword) {
keywordFields.push(fieldName);
}
}
);
const body = {
doesMatchIndices,
doesMatchRollupIndices,
dateFields,
numericFields,
keywordFields,
};
return response.ok({ body });
} catch (err) {
// 404s are still valid results.
if (err.statusCode === 404) {
const notFoundBody = {
doesMatchIndices: false,
doesMatchRollupIndices: false,
dateFields: [],
numericFields: [],
keywordFields: [],
};
return response.ok({ body: notFoundBody });
}
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
})
);
};

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { RouteDependencies } from '../../../types';
import { registerCreateRoute } from './register_create_route';
import { registerDeleteRoute } from './register_delete_route';
import { registerGetRoute } from './register_get_route';
import { registerStartRoute } from './register_start_route';
import { registerStopRoute } from './register_stop_route';
export function registerJobsRoutes(dependencies: RouteDependencies) {
registerCreateRoute(dependencies);
registerDeleteRoute(dependencies);
registerGetRoute(dependencies);
registerStartRoute(dependencies);
registerStopRoute(dependencies);
}

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { addBasePath } from '../../../services';
import { RouteDependencies } from '../../../types';
export const registerCreateRoute = ({
router,
license,
lib: { isEsError, formatEsError },
}: RouteDependencies) => {
router.put(
{
path: addBasePath('/create'),
validate: {
body: schema.object({
job: schema.object(
{
id: schema.string(),
},
{ unknowns: 'allow' }
),
}),
},
},
license.guardApiRoute(async (context, request, response) => {
try {
const { id, ...rest } = request.body.job;
// Create job.
await context.rollup!.client.callAsCurrentUser('rollup.createJob', {
id,
body: rest,
});
// Then request the newly created job.
const results = await context.rollup!.client.callAsCurrentUser('rollup.job', { id });
return response.ok({ body: results.jobs[0] });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
})
);
};

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { addBasePath } from '../../../services';
import { RouteDependencies } from '../../../types';
export const registerDeleteRoute = ({
router,
license,
lib: { isEsError, formatEsError },
}: RouteDependencies) => {
router.post(
{
path: addBasePath('/delete'),
validate: {
body: schema.object({
jobIds: schema.arrayOf(schema.string()),
}),
},
},
license.guardApiRoute(async (context, request, response) => {
try {
const { jobIds } = request.body;
const data = await Promise.all(
jobIds.map((id: string) =>
context.rollup!.client.callAsCurrentUser('rollup.deleteJob', { id })
)
).then(() => ({ success: true }));
return response.ok({ body: data });
} catch (err) {
// There is an issue opened on ES to handle the following error correctly
// https://github.com/elastic/elasticsearch/issues/42908
// Until then we'll modify the response here.
if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) {
err.status = 400;
err.statusCode = 400;
err.displayName = 'Bad request';
err.message = JSON.parse(err.response).task_failures[0].reason.reason;
}
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
})
);
};

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { addBasePath } from '../../../services';
import { RouteDependencies } from '../../../types';
export const registerGetRoute = ({
router,
license,
lib: { isEsError, formatEsError },
}: RouteDependencies) => {
router.get(
{
path: addBasePath('/jobs'),
validate: false,
},
license.guardApiRoute(async (context, request, response) => {
try {
const data = await context.rollup!.client.callAsCurrentUser('rollup.jobs');
return response.ok({ body: data });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
})
);
};

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { addBasePath } from '../../../services';
import { RouteDependencies } from '../../../types';
export const registerStartRoute = ({
router,
license,
lib: { isEsError, formatEsError },
}: RouteDependencies) => {
router.post(
{
path: addBasePath('/start'),
validate: {
body: schema.object({
jobIds: schema.arrayOf(schema.string()),
}),
query: schema.maybe(
schema.object({
waitForCompletion: schema.maybe(schema.string()),
})
),
},
},
license.guardApiRoute(async (context, request, response) => {
try {
const { jobIds } = request.body;
const data = await Promise.all(
jobIds.map((id: string) =>
context.rollup!.client.callAsCurrentUser('rollup.startJob', { id })
)
).then(() => ({ success: true }));
return response.ok({ body: data });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
})
);
};

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { addBasePath } from '../../../services';
import { RouteDependencies } from '../../../types';
export const registerStopRoute = ({
router,
license,
lib: { isEsError, formatEsError },
}: RouteDependencies) => {
router.post(
{
path: addBasePath('/stop'),
validate: {
body: schema.object({
jobIds: schema.arrayOf(schema.string()),
}),
query: schema.object({
waitForCompletion: schema.maybe(schema.string()),
}),
},
},
license.guardApiRoute(async (context, request, response) => {
try {
const { jobIds } = request.body;
// For our API integration tests we need to wait for the jobs to be stopped
// in order to be able to delete them sequentially.
const { waitForCompletion } = request.query;
const stopRollupJob = (id: string) =>
context.rollup!.client.callAsCurrentUser('rollup.stopJob', {
id,
waitForCompletion: waitForCompletion === 'true',
});
const data = await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true }));
return response.ok({ body: data });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
})
);
};

View file

@ -3,7 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext } from 'src/core/server';
import { RollupsServerPlugin } from './plugin';
export const plugin = (ctx: PluginInitializerContext) => new RollupsServerPlugin(ctx);
import { RouteDependencies } from '../../../types';
import { registerSearchRoute } from './register_search_route';
export function registerSearchRoutes(dependencies: RouteDependencies) {
registerSearchRoute(dependencies);
}

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { addBasePath } from '../../../services';
import { RouteDependencies } from '../../../types';
export const registerSearchRoute = ({
router,
license,
lib: { isEsError, formatEsError },
}: RouteDependencies) => {
router.post(
{
path: addBasePath('/search'),
validate: {
body: schema.arrayOf(
schema.object({
index: schema.string(),
query: schema.any(),
})
),
},
},
license.guardApiRoute(async (context, request, response) => {
try {
const requests = request.body.map(({ index, query }: { index: string; query?: any }) =>
context.rollup!.client.callAsCurrentUser('rollup.search', {
index,
rest_total_hits_as_int: true,
body: query,
})
);
const data = await Promise.all(requests);
return response.ok({ body: data });
} catch (err) {
if (isEsError(err)) {
return response.customError({ statusCode: err.statusCode, body: err });
}
return response.internalError({ body: err });
}
})
);
};

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { RouteDependencies } from '../types';
import { registerIndexPatternsRoutes } from './api/index_patterns';
import { registerIndicesRoutes } from './api/indices';
import { registerJobsRoutes } from './api/jobs';
import { registerSearchRoutes } from './api/search';
export function registerApiRoutes(dependencies: RouteDependencies) {
registerIndexPatternsRoutes(dependencies);
registerIndicesRoutes(dependencies);
registerJobsRoutes(dependencies);
registerSearchRoutes(dependencies);
}

View file

@ -4,4 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { callWithRequestFactory } from './call_with_request_factory';
import { API_BASE_PATH } from '../../common';
export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`;

View file

@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { licensePreRoutingFactory } from './license_pre_routing_factory';
export { addBasePath } from './add_base_path';
export { License } from './license';

View file

@ -0,0 +1,93 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Logger } from 'src/core/server';
import {
KibanaRequest,
KibanaResponseFactory,
RequestHandler,
RequestHandlerContext,
} from 'src/core/server';
import { LicensingPluginSetup } from '../../../licensing/server';
import { LicenseType } from '../../../licensing/common/types';
export interface LicenseStatus {
isValid: boolean;
message?: string;
}
interface SetupSettings {
pluginId: string;
minimumLicenseType: LicenseType;
defaultErrorMessage: string;
}
export class License {
private licenseStatus: LicenseStatus = {
isValid: false,
message: 'Invalid License',
};
private _isEsSecurityEnabled: boolean = false;
setup(
{ pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings,
{ licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger }
) {
licensing.license$.subscribe(license => {
const { state, message } = license.check(pluginId, minimumLicenseType);
const hasRequiredLicense = state === 'valid';
// Retrieving security checks the results of GET /_xpack as well as license state,
// so we're also checking whether the security is disabled in elasticsearch.yml.
this._isEsSecurityEnabled = license.getFeature('security').isEnabled;
if (hasRequiredLicense) {
this.licenseStatus = { isValid: true };
} else {
this.licenseStatus = {
isValid: false,
message: message || defaultErrorMessage,
};
if (message) {
logger.info(message);
}
}
});
}
guardApiRoute<P, Q, B>(handler: RequestHandler<P, Q, B>) {
const license = this;
return function licenseCheck(
ctx: RequestHandlerContext,
request: KibanaRequest<P, Q, B>,
response: KibanaResponseFactory
) {
const licenseStatus = license.getStatus();
if (!licenseStatus.isValid) {
return response.customError({
body: {
message: licenseStatus.message || '',
},
statusCode: 403,
});
}
return handler(ctx, request, response);
};
}
getStatus() {
return this.licenseStatus;
}
// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
get isEsSecurityEnabled() {
return this._isEsSecurityEnabled;
}
}

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { IndexPatternsFetcher } from '../../../../../src/plugins/data/server';
export { IndexPatternsFetcher } from '../../../../src/plugins/data/server';

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IRouter, APICaller, KibanaRequest } from 'src/core/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server';
import { IndexManagementPluginSetup } from '../../index_management/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { License } from './services';
import { IndexPatternsFetcher } from './shared_imports';
import { isEsError } from './lib/is_es_error';
import { formatEsError } from './lib/format_es_error';
import { getCapabilitiesForRollupIndices } from './lib/map_capabilities';
import { mergeCapabilitiesWithFields } from './lib/merge_capabilities_with_fields';
export interface Dependencies {
indexManagement?: IndexManagementPluginSetup;
visTypeTimeseries?: VisTypeTimeseriesSetup;
usageCollection?: UsageCollectionSetup;
licensing: LicensingPluginSetup;
}
export interface RouteDependencies {
router: IRouter;
license: License;
lib: {
isEsError: typeof isEsError;
formatEsError: typeof formatEsError;
getCapabilitiesForRollupIndices: typeof getCapabilitiesForRollupIndices;
mergeCapabilitiesWithFields: typeof mergeCapabilitiesWithFields;
};
sharedImports: {
IndexPatternsFetcher: typeof IndexPatternsFetcher;
};
}
// TODO: When vis_type_timeseries is fully migrated to the NP, it shouldn't require this shim.
export type CallWithRequestFactoryShim = (
elasticsearchServiceShim: CallWithRequestFactoryShim,
request: KibanaRequest
) => APICaller;

View file

@ -12366,7 +12366,6 @@
"xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV レポート",
"xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF レポート",
"xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG レポート",
"xpack.rollupJobs.appName": "ロールアップジョブ",
"xpack.rollupJobs.appTitle": "ロールアップジョブ",
"xpack.rollupJobs.breadcrumbsTitle": "ロールアップジョブ",
"xpack.rollupJobs.create.backButton.label": "戻る",

View file

@ -12370,7 +12370,6 @@
"xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 报告",
"xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF 报告",
"xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG 报告",
"xpack.rollupJobs.appName": "汇总/打包作业",
"xpack.rollupJobs.appTitle": "汇总/打包作业",
"xpack.rollupJobs.breadcrumbsTitle": "汇总/打包作业",
"xpack.rollupJobs.create.backButton.label": "上一步",