{
},
});
- 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;
- public start() {}
- public stop() {}
-}
+ 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);
+ }
+ }
-export interface RollupSetup {
- /** @deprecated */
- __legacy: {
- config: PluginInitializerContext['config'];
- logger: PluginInitializerContext['logger'];
- };
+ start() {}
+ stop() {}
}
diff --git a/x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts b/x-pack/plugins/rollup/server/rollup_data_enricher.ts
similarity index 92%
rename from x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts
rename to x-pack/plugins/rollup/server/rollup_data_enricher.ts
index ad621f2d9ba80..b06cf971a6460 100644
--- a/x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts
+++ b/x-pack/plugins/rollup/server/rollup_data_enricher.ts
@@ -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) {
diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts
new file mode 100644
index 0000000000000..7bf525ca4aa98
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts
@@ -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);
+}
diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts
new file mode 100644
index 0000000000000..32f23314c5259
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts
@@ -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 });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/rollup/server/routes/api/indices/index.ts b/x-pack/plugins/rollup/server/routes/api/indices/index.ts
new file mode 100644
index 0000000000000..0aa5772b56991
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/indices/index.ts
@@ -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);
+}
diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts
new file mode 100644
index 0000000000000..3521650c1dc3e
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * 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 });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts
new file mode 100644
index 0000000000000..9e22060b9beb7
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts
@@ -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 });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/index.ts b/x-pack/plugins/rollup/server/routes/api/jobs/index.ts
new file mode 100644
index 0000000000000..fe1d1c6109a88
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/jobs/index.ts
@@ -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);
+}
diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts
new file mode 100644
index 0000000000000..adf8c1da0af0e
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts
@@ -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 });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts
new file mode 100644
index 0000000000000..32f7b3f35e163
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts
@@ -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 });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts
new file mode 100644
index 0000000000000..a8d51f4639fc6
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts
@@ -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 });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts
new file mode 100644
index 0000000000000..fb6f2b12ba52e
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts
@@ -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 });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts
new file mode 100644
index 0000000000000..118d98e36e03c
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts
@@ -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 });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/rollup/server/routes/api/search/index.ts b/x-pack/plugins/rollup/server/routes/api/search/index.ts
new file mode 100644
index 0000000000000..2a2d823e79bc6
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/search/index.ts
@@ -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 { registerSearchRoute } from './register_search_route';
+
+export function registerSearchRoutes(dependencies: RouteDependencies) {
+ registerSearchRoute(dependencies);
+}
diff --git a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts
new file mode 100644
index 0000000000000..c5c56336def1a
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts
@@ -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 });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/rollup/server/routes/index.ts b/x-pack/plugins/rollup/server/routes/index.ts
new file mode 100644
index 0000000000000..b25480855b4a2
--- /dev/null
+++ b/x-pack/plugins/rollup/server/routes/index.ts
@@ -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);
+}
diff --git a/x-pack/plugins/rollup/server/services/add_base_path.ts b/x-pack/plugins/rollup/server/services/add_base_path.ts
new file mode 100644
index 0000000000000..7d7cce3aab334
--- /dev/null
+++ b/x-pack/plugins/rollup/server/services/add_base_path.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { API_BASE_PATH } from '../../common';
+
+export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`;
diff --git a/x-pack/legacy/plugins/rollup/server/index.ts b/x-pack/plugins/rollup/server/services/index.ts
similarity index 55%
rename from x-pack/legacy/plugins/rollup/server/index.ts
rename to x-pack/plugins/rollup/server/services/index.ts
index 6bbd00ac6576e..7f79c4f446546 100644
--- a/x-pack/legacy/plugins/rollup/server/index.ts
+++ b/x-pack/plugins/rollup/server/services/index.ts
@@ -3,7 +3,6 @@
* 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);
+export { addBasePath } from './add_base_path';
+export { License } from './license';
diff --git a/x-pack/plugins/rollup/server/services/license.ts b/x-pack/plugins/rollup/server/services/license.ts
new file mode 100644
index 0000000000000..bfd357867c3e2
--- /dev/null
+++ b/x-pack/plugins/rollup/server/services/license.ts
@@ -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(handler: RequestHandler
) {
+ const license = this;
+
+ return function licenseCheck(
+ ctx: RequestHandlerContext,
+ request: KibanaRequest
,
+ 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;
+ }
+}
diff --git a/x-pack/legacy/plugins/rollup/server/shared_imports.ts b/x-pack/plugins/rollup/server/shared_imports.ts
similarity index 75%
rename from x-pack/legacy/plugins/rollup/server/shared_imports.ts
rename to x-pack/plugins/rollup/server/shared_imports.ts
index 941610b97707f..09842f529abed 100644
--- a/x-pack/legacy/plugins/rollup/server/shared_imports.ts
+++ b/x-pack/plugins/rollup/server/shared_imports.ts
@@ -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';
diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts
new file mode 100644
index 0000000000000..c21d76400164e
--- /dev/null
+++ b/x-pack/plugins/rollup/server/types.ts
@@ -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;
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 5b55c7a15f1b3..591de8ace1869 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -138,17 +138,6 @@
"charts.controls.rangeErrorMessage": "値は {min} と {max} の間でなければなりません",
"charts.controls.vislibBasicOptions.legendPositionLabel": "凡例位置",
"charts.controls.vislibBasicOptions.showTooltipLabel": "ツールヒントを表示",
- "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "エラー",
- "common.ui.errorAutoCreateIndex.errorDescription": "Elasticsearch クラスターの {autoCreateIndexActionConfig} 設定が原因で、Kibana が保存されたオブジェクトを格納するインデックスを自動的に作成できないようです。Kibana は、保存されたオブジェクトインデックスが適切なマッピング/スキーマを使用し Kibana から Elasticsearch へのポーリングの回数を減らすための最適な手段であるため、この Elasticsearch の機能を使用します。",
- "common.ui.errorAutoCreateIndex.errorDisclaimer": "申し訳ございませんが、この問題が解決されるまで Kibana で何も保存することができません。",
- "common.ui.errorAutoCreateIndex.errorTitle": "おっと!",
- "common.ui.errorAutoCreateIndex.howToFixError.goBackText": "ブラウザの戻るボタンで前の画面に戻ります。",
- "common.ui.errorAutoCreateIndex.howToFixError.removeConfigText": "Elasticsearch 構成ファイルから {autoCreateIndexActionConfig} を削除します。",
- "common.ui.errorAutoCreateIndex.howToFixError.restartText": "Elasticsearch を再起動します。",
- "common.ui.errorAutoCreateIndex.howToFixErrorTitle": "どうすれば良いのでしょう?",
- "common.ui.errorAutoCreateIndex.noteImageAriaLabel": "情報",
- "common.ui.errorAutoCreateIndex.noteMessage": "{autoCreateIndexActionConfig} は、機能を有効にするパターンのホワイトリストを定義することもできます。Kibana と同じ理由でこの機能を使用する他のプラグイン/操作をすべて把握する必要があるため、この設定のこのような使い方はここでは説明しません。",
- "common.ui.errorAutoCreateIndex.noteTitle": "注:",
"common.ui.errorUrlOverflow.breadcrumbs.errorText": "エラー",
"common.ui.errorUrlOverflow.errorDescription": "とても長い URL ですね。残念なお知らせがあります。ご使用のブラウザは Kibana の超巨大 URL に対応していません。問題を避けるため、Kibana はご使用のブラウザでの URL を {urlCharacterLimit} 文字に制限します。",
"common.ui.errorUrlOverflow.errorTitle": "おっと!",
@@ -8830,10 +8819,8 @@
"xpack.logstash.workersTooltip": "パイプラインのフィルターとアウトプットステージを同時に実行するワーカーの数です。イベントが詰まってしまう場合や CPU が飽和状態ではない場合は、マシンの処理能力をより有効に活用するため、この数字を上げてみてください。\n\nデフォルト値:ホストの CPU コア数です",
"xpack.maps.addLayerPanel.addLayer": "レイヤーを追加",
"xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "データソースを変更",
- "xpack.maps.addLayerPanel.chooseDataSourceTitle": "データソースの選択",
"xpack.maps.addLayerPanel.footer.cancelButtonLabel": "キャンセル",
"xpack.maps.addLayerPanel.importFile": "ファイルのインポート",
- "xpack.maps.addLayerPanel.selectSource": "ソースを選択",
"xpack.maps.aggs.defaultCountLabel": "カウント",
"xpack.maps.appDescription": "マップアプリケーション",
"xpack.maps.appTitle": "マップ",
@@ -12366,7 +12353,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": "戻る",
@@ -15944,7 +15930,6 @@
"xpack.triggersActionsUI.sections.alertForm.loadingActionTypesDescription": "アクションタイプを読み込み中...",
"xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知間隔",
"xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "アラートがアクティブな間にアクションを繰り返す頻度を定義します。",
- "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle": "{actionConnectorName}",
"xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle": "アクション:アクションタイプを選択してください",
"xpack.triggersActionsUI.sections.alertForm.selectAlertTypeTitle": "トリガータイプを選択してください",
"xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle": "{alertType}",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 1758588d01ba8..c46f395a8c64e 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -138,17 +138,6 @@
"charts.controls.rangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内",
"charts.controls.vislibBasicOptions.legendPositionLabel": "图例位置",
"charts.controls.vislibBasicOptions.showTooltipLabel": "显示工具提示",
- "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "错误",
- "common.ui.errorAutoCreateIndex.errorDescription": "似乎 Elasticsearch 集群的 {autoCreateIndexActionConfig} 设置使 Kibana 无法自动创建用于存储已保存对象的索引。Kibana 将使用此 Elasticsearch 功能,因为这是确保已保存对象索引使用正确映射/架构的最好方式,而且其允许 Kibana 较少地轮询 Elasticsearch。",
- "common.ui.errorAutoCreateIndex.errorDisclaimer": "但是,只有解决了此问题后,您才能在 Kibana 保存内容。",
- "common.ui.errorAutoCreateIndex.errorTitle": "糟糕!",
- "common.ui.errorAutoCreateIndex.howToFixError.goBackText": "使用浏览器的后退按钮返回您之前正做的工作。",
- "common.ui.errorAutoCreateIndex.howToFixError.removeConfigText": "从 Elasticsearch 配置文件中删除 {autoCreateIndexActionConfig}",
- "common.ui.errorAutoCreateIndex.howToFixError.restartText": "重新启动 Elasticsearch。",
- "common.ui.errorAutoCreateIndex.howToFixErrorTitle": "那么,我如何解决此问题?",
- "common.ui.errorAutoCreateIndex.noteImageAriaLabel": "信息",
- "common.ui.errorAutoCreateIndex.noteMessage": "{autoCreateIndexActionConfig} 还可以定义应启用此功能的模式白名单。我们在这里不讨论如何以那种方式使用该设置,因为这和 Kibana 一样需要您了解依赖该功能的所有其他插件/交互。",
- "common.ui.errorAutoCreateIndex.noteTitle": "注意:",
"common.ui.errorUrlOverflow.breadcrumbs.errorText": "错误",
"common.ui.errorUrlOverflow.errorDescription": "您的 URL 真不小。我有一些不幸的消息:您的浏览器与 Kibana 的超长 URL 不太兼容。为了避免您遇到问题,Kibana 在您的浏览器中将 URL 长度限制在 {urlCharacterLimit} 个字符。",
"common.ui.errorUrlOverflow.errorTitle": "喔哦!",
@@ -8833,10 +8822,8 @@
"xpack.logstash.workersTooltip": "并行执行管道的筛选和输出阶段的工作线程数目。如果您发现事件出现积压或 CPU 未饱和,请考虑增大此数值,以更好地利用机器处理能力。\n\n默认值:主机的 CPU 核心数",
"xpack.maps.addLayerPanel.addLayer": "添加图层",
"xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "更改数据源",
- "xpack.maps.addLayerPanel.chooseDataSourceTitle": "选择数据源",
"xpack.maps.addLayerPanel.footer.cancelButtonLabel": "鍙栨秷",
"xpack.maps.addLayerPanel.importFile": "导入文件",
- "xpack.maps.addLayerPanel.selectSource": "选择源",
"xpack.maps.aggs.defaultCountLabel": "计数",
"xpack.maps.appDescription": "地图应用程序",
"xpack.maps.appTitle": "Maps",
@@ -12370,7 +12357,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": "上一步",
@@ -15949,7 +15935,6 @@
"xpack.triggersActionsUI.sections.alertForm.loadingActionTypesDescription": "正在加载操作类型……",
"xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知频率",
"xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "定义告警处于活动状态时重复操作的频率。",
- "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle": "{actionConnectorName}",
"xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle": "操作:选择操作类型",
"xpack.triggersActionsUI.sections.alertForm.selectAlertTypeTitle": "选择触发器类型",
"xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle": "{alertType}",
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts
index 9ce50cf47560a..0a2ec3f203a9a 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts
@@ -33,11 +33,20 @@ test('should sort enabled action types first', async () => {
enabledInConfig: true,
enabledInLicense: true,
},
+ {
+ id: '4',
+ minimumLicenseRequired: 'basic',
+ name: 'x-fourth',
+ enabled: true,
+ enabledInConfig: false,
+ enabledInLicense: true,
+ },
];
const result = [...actionTypes].sort(actionTypeCompare);
expect(result[0]).toEqual(actionTypes[0]);
expect(result[1]).toEqual(actionTypes[2]);
- expect(result[2]).toEqual(actionTypes[1]);
+ expect(result[2]).toEqual(actionTypes[3]);
+ expect(result[3]).toEqual(actionTypes[1]);
});
test('should sort by name when all enabled', async () => {
@@ -66,9 +75,18 @@ test('should sort by name when all enabled', async () => {
enabledInConfig: true,
enabledInLicense: true,
},
+ {
+ id: '4',
+ minimumLicenseRequired: 'basic',
+ name: 'x-fourth',
+ enabled: true,
+ enabledInConfig: false,
+ enabledInLicense: true,
+ },
];
const result = [...actionTypes].sort(actionTypeCompare);
expect(result[0]).toEqual(actionTypes[1]);
expect(result[1]).toEqual(actionTypes[2]);
expect(result[2]).toEqual(actionTypes[0]);
+ expect(result[3]).toEqual(actionTypes[3]);
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts
index d18cb21b3a0fe..8078ef4938e50 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts
@@ -4,14 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ActionType } from '../../types';
+import { ActionType, ActionConnector } from '../../types';
-export function actionTypeCompare(a: ActionType, b: ActionType) {
- if (a.enabled === true && b.enabled === false) {
+export function actionTypeCompare(
+ a: ActionType,
+ b: ActionType,
+ preconfiguredConnectors?: ActionConnector[]
+) {
+ const aEnabled = getIsEnabledValue(a, preconfiguredConnectors);
+ const bEnabled = getIsEnabledValue(b, preconfiguredConnectors);
+
+ if (aEnabled === true && bEnabled === false) {
return -1;
}
- if (a.enabled === false && b.enabled === true) {
+ if (aEnabled === false && bEnabled === true) {
return 1;
}
return a.name.localeCompare(b.name);
}
+
+const getIsEnabledValue = (actionType: ActionType, preconfiguredConnectors?: ActionConnector[]) => {
+ let isEnabled = actionType.enabled;
+ if (
+ !actionType.enabledInConfig &&
+ preconfiguredConnectors &&
+ preconfiguredConnectors.length > 0
+ ) {
+ isEnabled =
+ preconfiguredConnectors.find(connector => connector.actionTypeId === actionType.id) !==
+ undefined;
+ }
+ return isEnabled;
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx
index 566ed7935e013..9c017aa6fd31f 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx
@@ -4,43 +4,47 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ActionType } from '../../types';
-import { checkActionTypeEnabled } from './check_action_type_enabled';
+import { ActionType, ActionConnector } from '../../types';
+import {
+ checkActionTypeEnabled,
+ checkActionFormActionTypeEnabled,
+} from './check_action_type_enabled';
-test(`returns isEnabled:true when action type isn't provided`, async () => {
- expect(checkActionTypeEnabled()).toMatchInlineSnapshot(`
+describe('checkActionTypeEnabled', () => {
+ test(`returns isEnabled:true when action type isn't provided`, async () => {
+ expect(checkActionTypeEnabled()).toMatchInlineSnapshot(`
Object {
"isEnabled": true,
}
`);
-});
+ });
-test('returns isEnabled:true when action type is enabled', async () => {
- const actionType: ActionType = {
- id: '1',
- minimumLicenseRequired: 'basic',
- name: 'my action',
- enabled: true,
- enabledInConfig: true,
- enabledInLicense: true,
- };
- expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(`
+ test('returns isEnabled:true when action type is enabled', async () => {
+ const actionType: ActionType = {
+ id: '1',
+ minimumLicenseRequired: 'basic',
+ name: 'my action',
+ enabled: true,
+ enabledInConfig: true,
+ enabledInLicense: true,
+ };
+ expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(`
Object {
"isEnabled": true,
}
`);
-});
+ });
-test('returns isEnabled:false when action type is disabled by license', async () => {
- const actionType: ActionType = {
- id: '1',
- minimumLicenseRequired: 'basic',
- name: 'my action',
- enabled: false,
- enabledInConfig: true,
- enabledInLicense: false,
- };
- expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(`
+ test('returns isEnabled:false when action type is disabled by license', async () => {
+ const actionType: ActionType = {
+ id: '1',
+ minimumLicenseRequired: 'basic',
+ name: 'my action',
+ enabled: false,
+ enabledInConfig: true,
+ enabledInLicense: false,
+ };
+ expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(`
Object {
"isEnabled": false,
"message": "This connector requires a Basic license.",
@@ -63,18 +67,82 @@ test('returns isEnabled:false when action type is disabled by license', async ()
,
}
`);
+ });
+
+ test('returns isEnabled:false when action type is disabled by config', async () => {
+ const actionType: ActionType = {
+ id: '1',
+ minimumLicenseRequired: 'basic',
+ name: 'my action',
+ enabled: false,
+ enabledInConfig: false,
+ enabledInLicense: true,
+ };
+ expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(`
+ Object {
+ "isEnabled": false,
+ "message": "This connector is disabled by the Kibana configuration.",
+ "messageCard": ,
+ }
+ `);
+ });
});
-test('returns isEnabled:false when action type is disabled by config', async () => {
- const actionType: ActionType = {
- id: '1',
- minimumLicenseRequired: 'basic',
- name: 'my action',
- enabled: false,
- enabledInConfig: false,
- enabledInLicense: true,
- };
- expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(`
+describe('checkActionFormActionTypeEnabled', () => {
+ const preconfiguredConnectors: ActionConnector[] = [
+ {
+ actionTypeId: '1',
+ config: {},
+ id: 'test1',
+ isPreconfigured: true,
+ name: 'test',
+ secrets: {},
+ referencedByCount: 0,
+ },
+ {
+ actionTypeId: '2',
+ config: {},
+ id: 'test2',
+ isPreconfigured: true,
+ name: 'test',
+ secrets: {},
+ referencedByCount: 0,
+ },
+ ];
+
+ test('returns isEnabled:true when action type is preconfigured', async () => {
+ const actionType: ActionType = {
+ id: '1',
+ minimumLicenseRequired: 'basic',
+ name: 'my action',
+ enabled: true,
+ enabledInConfig: false,
+ enabledInLicense: true,
+ };
+
+ expect(checkActionFormActionTypeEnabled(actionType, preconfiguredConnectors))
+ .toMatchInlineSnapshot(`
+ Object {
+ "isEnabled": true,
+ }
+ `);
+ });
+
+ test('returns isEnabled:false when action type is disabled by config and not preconfigured', async () => {
+ const actionType: ActionType = {
+ id: 'disabled-by-config',
+ minimumLicenseRequired: 'basic',
+ name: 'my action',
+ enabled: true,
+ enabledInConfig: false,
+ enabledInLicense: true,
+ };
+ expect(checkActionFormActionTypeEnabled(actionType, preconfiguredConnectors))
+ .toMatchInlineSnapshot(`
Object {
"isEnabled": false,
"message": "This connector is disabled by the Kibana configuration.",
@@ -85,4 +153,5 @@ test('returns isEnabled:false when action type is disabled by config', async ()
/>,
}
`);
+ });
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx
index 263502a82ec79..971d6dbbb57bf 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx
@@ -9,7 +9,7 @@ import { capitalize } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCard, EuiLink } from '@elastic/eui';
-import { ActionType } from '../../types';
+import { ActionType, ActionConnector } from '../../types';
import { VIEW_LICENSE_OPTIONS_LINK } from '../../common/constants';
import './check_action_type_enabled.scss';
@@ -22,71 +22,98 @@ export interface IsDisabledResult {
messageCard: JSX.Element;
}
+const getLicenseCheckResult = (actionType: ActionType) => {
+ return {
+ isEnabled: false,
+ message: i18n.translate(
+ 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage',
+ {
+ defaultMessage: 'This connector requires a {minimumLicenseRequired} license.',
+ values: {
+ minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired),
+ },
+ }
+ ),
+ messageCard: (
+
+
+
+ }
+ />
+ ),
+ };
+};
+
+const configurationCheckResult = {
+ isEnabled: false,
+ message: i18n.translate(
+ 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage',
+ { defaultMessage: 'This connector is disabled by the Kibana configuration.' }
+ ),
+ messageCard: (
+
+ ),
+};
+
export function checkActionTypeEnabled(
actionType?: ActionType
): IsEnabledResult | IsDisabledResult {
if (actionType?.enabledInLicense === false) {
- return {
- isEnabled: false,
- message: i18n.translate(
- 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage',
- {
- defaultMessage: 'This connector requires a {minimumLicenseRequired} license.',
- values: {
- minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired),
- },
- }
- ),
- messageCard: (
-
-
-
- }
- />
- ),
- };
+ return getLicenseCheckResult(actionType);
}
if (actionType?.enabledInConfig === false) {
- return {
- isEnabled: false,
- message: i18n.translate(
- 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage',
- { defaultMessage: 'This connector is disabled by the Kibana configuration.' }
- ),
- messageCard: (
-
- ),
- };
+ return configurationCheckResult;
+ }
+
+ return { isEnabled: true };
+}
+
+export function checkActionFormActionTypeEnabled(
+ actionType: ActionType,
+ preconfiguredConnectors: ActionConnector[]
+): IsEnabledResult | IsDisabledResult {
+ if (actionType?.enabledInLicense === false) {
+ return getLicenseCheckResult(actionType);
+ }
+
+ if (
+ actionType?.enabledInConfig === false &&
+ // do not disable action type if it contains preconfigured connectors (is preconfigured)
+ !preconfiguredConnectors.find(
+ preconfiguredConnector => preconfiguredConnector.actionTypeId === actionType.id
+ )
+ ) {
+ return configurationCheckResult;
}
return { isEnabled: true };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx
index d4def86b07b1f..aed7d18bd9f3d 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx
@@ -73,6 +73,21 @@ describe('action_form', () => {
actionParamsFields: null,
};
+ const preconfiguredOnly = {
+ id: 'preconfigured',
+ iconClass: 'test',
+ selectMessage: 'test',
+ validateConnector: (): ValidationResult => {
+ return { errors: {} };
+ },
+ validateParams: (): ValidationResult => {
+ const validationResult = { errors: {} };
+ return validationResult;
+ },
+ actionConnectorFields: null,
+ actionParamsFields: null,
+ };
+
describe('action_form in alert', () => {
let wrapper: ReactWrapper;
@@ -95,6 +110,22 @@ describe('action_form', () => {
config: {},
isPreconfigured: true,
},
+ {
+ secrets: {},
+ id: 'test3',
+ actionTypeId: preconfiguredOnly.id,
+ name: 'Preconfigured Only',
+ config: {},
+ isPreconfigured: true,
+ },
+ {
+ secrets: {},
+ id: 'test4',
+ actionTypeId: preconfiguredOnly.id,
+ name: 'Regular connector',
+ config: {},
+ isPreconfigured: false,
+ },
]);
const mockes = coreMock.createSetup();
deps = {
@@ -106,6 +137,7 @@ describe('action_form', () => {
actionType,
disabledByConfigActionType,
disabledByLicenseActionType,
+ preconfiguredOnly,
]);
actionTypeRegistry.has.mockReturnValue(true);
actionTypeRegistry.get.mockReturnValue(actionType);
@@ -166,6 +198,14 @@ describe('action_form', () => {
enabledInLicense: true,
minimumLicenseRequired: 'basic',
},
+ {
+ id: 'preconfigured',
+ name: 'Preconfigured only',
+ enabled: true,
+ enabledInConfig: false,
+ enabledInLicense: true,
+ minimumLicenseRequired: 'basic',
+ },
{
id: 'disabled-by-config',
name: 'Disabled by config',
@@ -207,21 +247,27 @@ describe('action_form', () => {
).toBeFalsy();
});
- it(`doesn't render action types disabled by config`, async () => {
+ it('does not render action types disabled by config', async () => {
await setup();
const actionOption = wrapper.find(
- `[data-test-subj="disabled-by-config-ActionTypeSelectOption"]`
+ '[data-test-subj="disabled-by-config-ActionTypeSelectOption"]'
);
expect(actionOption.exists()).toBeFalsy();
});
- it(`renders available connectors for the selected action type`, async () => {
+ it('render action types which is preconfigured only (disabled by config and with preconfigured connectors)', async () => {
+ await setup();
+ const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
+ expect(actionOption.exists()).toBeTruthy();
+ });
+
+ it('renders available connectors for the selected action type', async () => {
await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
);
actionOption.first().simulate('click');
- const combobox = wrapper.find(`[data-test-subj="selectActionConnector"]`);
+ const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}"]`);
expect((combobox.first().props() as any).options).toMatchInlineSnapshot(`
Array [
Object {
@@ -238,10 +284,37 @@ describe('action_form', () => {
`);
});
+ it('renders only preconfigured connectors for the selected preconfigured action type', async () => {
+ await setup();
+ const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
+ actionOption.first().simulate('click');
+ const combobox = wrapper.find('[data-test-subj="selectActionConnector-preconfigured"]');
+ expect((combobox.first().props() as any).options).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "id": "test3",
+ "key": "test3",
+ "label": "Preconfigured Only (preconfigured)",
+ },
+ ]
+ `);
+ });
+
+ it('does not render "Add new" button for preconfigured only action type', async () => {
+ await setup();
+ const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
+ actionOption.first().simulate('click');
+ const preconfigPannel = wrapper.find('[data-test-subj="alertActionAccordion-default"]');
+ const addNewConnectorButton = preconfigPannel.find(
+ '[data-test-subj="addNewActionConnectorButton-preconfigured"]'
+ );
+ expect(addNewConnectorButton.exists()).toBeFalsy();
+ });
+
it('renders action types disabled by license', async () => {
await setup();
const actionOption = wrapper.find(
- `[data-test-subj="disabled-by-license-ActionTypeSelectOption"]`
+ '[data-test-subj="disabled-by-license-ActionTypeSelectOption"]'
);
expect(actionOption.exists()).toBeTruthy();
expect(
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
index 4199cfb7b4b7f..0027837c913d1 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
@@ -29,7 +29,7 @@ import {
EuiText,
} from '@elastic/eui';
import { HttpSetup, ToastsApi } from 'kibana/public';
-import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api';
+import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
import {
IErrorObject,
ActionTypeModel,
@@ -42,7 +42,7 @@ import { SectionLoading } from '../../components/section_loading';
import { ConnectorAddModal } from './connector_add_modal';
import { TypeRegistry } from '../../type_registry';
import { actionTypeCompare } from '../../lib/action_type_compare';
-import { checkActionTypeEnabled } from '../../lib/check_action_type_enabled';
+import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants';
interface ActionAccordionFormProps {
@@ -111,14 +111,12 @@ export const ActionForm = ({
setHasActionsDisabled(hasActionsDisabled);
}
} catch (e) {
- if (toastNotifications) {
- toastNotifications.addDanger({
- title: i18n.translate(
- 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage',
- { defaultMessage: 'Unable to load action types' }
- ),
- });
- }
+ toastNotifications.addDanger({
+ title: i18n.translate(
+ 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage',
+ { defaultMessage: 'Unable to load action types' }
+ ),
+ });
} finally {
setIsLoadingActionTypes(false);
}
@@ -126,41 +124,50 @@ export const ActionForm = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+ // load connectors
useEffect(() => {
- loadConnectors();
+ (async () => {
+ try {
+ setIsLoadingConnectors(true);
+ setConnectors(await loadConnectors({ http }));
+ } catch (e) {
+ toastNotifications.addDanger({
+ title: i18n.translate(
+ 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage',
+ {
+ defaultMessage: 'Unable to load connectors',
+ }
+ ),
+ });
+ } finally {
+ setIsLoadingConnectors(false);
+ }
+ })();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- async function loadConnectors() {
- try {
- setIsLoadingConnectors(true);
- const actionsResponse = await loadAllActions({ http });
- setConnectors(actionsResponse);
- } catch (e) {
- toastNotifications.addDanger({
- title: i18n.translate(
- 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage',
- {
- defaultMessage: 'Unable to load connectors',
- }
- ),
- });
- } finally {
- setIsLoadingConnectors(false);
- }
- }
const preconfiguredMessage = i18n.translate(
'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage',
{
defaultMessage: '(preconfigured)',
}
);
+
const getSelectedOptions = (actionItemId: string) => {
- const val = connectors.find(connector => connector.id === actionItemId);
- if (!val) {
+ const selectedConnector = connectors.find(connector => connector.id === actionItemId);
+ if (
+ !selectedConnector ||
+ // if selected connector is not preconfigured and action type is for preconfiguration only,
+ // do not show regular connectors of this type
+ (actionTypesIndex &&
+ !actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig &&
+ !selectedConnector.isPreconfigured)
+ ) {
return [];
}
- const optionTitle = `${val.name} ${val.isPreconfigured ? preconfiguredMessage : ''}`;
+ const optionTitle = `${selectedConnector.name} ${
+ selectedConnector.isPreconfigured ? preconfiguredMessage : ''
+ }`;
return [
{
label: optionTitle,
@@ -179,8 +186,15 @@ export const ActionForm = ({
},
index: number
) => {
+ const actionType = actionTypesIndex![actionItem.actionTypeId];
+
const optionsList = connectors
- .filter(connectorItem => connectorItem.actionTypeId === actionItem.actionTypeId)
+ .filter(
+ connectorItem =>
+ connectorItem.actionTypeId === actionItem.actionTypeId &&
+ // include only enabled by config connectors or preconfigured
+ (actionType.enabledInConfig || connectorItem.isPreconfigured)
+ )
.map(({ name, id, isPreconfigured }) => ({
label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`,
key: id,
@@ -189,8 +203,9 @@ export const ActionForm = ({
const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId);
if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields;
- const checkEnabledResult = checkActionTypeEnabled(
- actionTypesIndex && actionTypesIndex[actionConnector.actionTypeId]
+ const checkEnabledResult = checkActionFormActionTypeEnabled(
+ actionTypesIndex![actionConnector.actionTypeId],
+ connectors.filter(connector => connector.isPreconfigured)
);
const accordionContent = checkEnabledResult.isEnabled ? (
@@ -211,19 +226,21 @@ export const ActionForm = ({
/>
}
labelAppend={
- {
- setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
- setAddModalVisibility(true);
- }}
- >
-
-
+ actionTypesIndex![actionConnector.actionTypeId].enabledInConfig ? (
+ {
+ setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
+ setAddModalVisibility(true);
+ }}
+ >
+
+
+ ) : null
}
>
{
setActionIdByIndex(selectedOptions[0].id ?? '', index);
@@ -258,10 +275,9 @@ export const ActionForm = ({
);
return (
-
+
-
+
-
+