does not play well with the angular router. It will cause this controller
- // to re-execute without the $destroy handler being called. This means that the app will be mounted twice
- // creating a memory leak when leaving (only 1 app will be unmounted).
- // To avoid this, we unmount the React app each time we enter the controller.
- unmountReactApp();
-
- $scope.$$postDigest(() => {
- elem = document.getElementById(CCR_REACT_ROOT);
- renderReact(elem, npStart.core.i18n.Context);
-
- // Angular Lifecycle
- const appRoute = $route.current;
- const stopListeningForLocationChange = $scope.$on('$locationChangeSuccess', () => {
- const currentRoute = $route.current;
- const isNavigationInApp = currentRoute.$$route.template === appRoute.$$route.template;
-
- // When we navigate within CCR, prevent Angular from re-matching the route and rebuild the app
- if (isNavigationInApp) {
- $route.current = appRoute;
- } else {
- // Any clean up when User leaves the CCR
- }
-
- $scope.$on('$destroy', () => {
- stopListeningForLocationChange && stopListeningForLocationChange();
- unmountReactApp();
- });
- });
- });
- }
- },
- });
-}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts
deleted file mode 100644
index ae15073b979e1..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts
+++ /dev/null
@@ -1,36 +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 { APICaller } from 'src/core/server';
-import { Index } from '../../../../../plugins/index_management/server';
-
-export const ccrDataEnricher = async (indicesList: Index[], callWithRequest: APICaller) => {
- if (!indicesList?.length) {
- return indicesList;
- }
- const params = {
- path: '/_all/_ccr/info',
- method: 'GET',
- };
- try {
- const { follower_indices: followerIndices } = await callWithRequest(
- 'transport.request',
- params
- );
- return indicesList.map(index => {
- const isFollowerIndex = !!followerIndices.find(
- (followerIndex: { follower_index: string }) => {
- return followerIndex.follower_index === index.name;
- }
- );
- return {
- ...index,
- isFollowerIndex,
- };
- });
- } catch (e) {
- return indicesList;
- }
-};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts
deleted file mode 100644
index 7a38d024d99a2..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts
+++ /dev/null
@@ -1,11 +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 { CrossClusterReplicationServerPlugin } from './plugin';
-
-export const plugin = (ctx: PluginInitializerContext) =>
- new CrossClusterReplicationServerPlugin(ctx);
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js
deleted file mode 100644
index 99d72ce1a0e6e..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js
+++ /dev/null
@@ -1,20 +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 { once } from 'lodash';
-import { elasticsearchJsPlugin } from '../../client/elasticsearch_ccr';
-
-const callWithRequest = once(server => {
- const config = { plugins: [elasticsearchJsPlugin] };
- const cluster = server.plugins.elasticsearch.createCluster('ccr', config);
- return cluster.callWithRequest;
-});
-
-export const callWithRequestFactory = (server, request) => {
- return (...args) => {
- return callWithRequest(server)(request, ...args);
- };
-};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js
deleted file mode 100644
index 787814d87dff9..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js
+++ /dev/null
@@ -1,7 +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 { callWithRequestFactory } from './call_with_request_factory';
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js
deleted file mode 100644
index 6cf12896fa472..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js
+++ /dev/null
@@ -1,70 +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 { i18n } from '@kbn/i18n';
-
-export function checkLicense(xpackLicenseInfo) {
- const pluginName = 'Cross-Cluster Replication';
-
- // If, for some reason, we cannot get the license information
- // from Elasticsearch, assume worst case and disable
- if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) {
- return {
- isAvailable: false,
- showLinks: true,
- enableLinks: false,
- message: i18n.translate(
- 'xpack.crossClusterReplication.checkLicense.errorUnavailableMessage',
- {
- defaultMessage:
- 'You cannot use {pluginName} because license information is not available at this time.',
- values: { pluginName },
- }
- ),
- };
- }
-
- const VALID_LICENSE_MODES = ['trial', 'platinum', 'enterprise'];
-
- const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_LICENSE_MODES);
- const isLicenseActive = xpackLicenseInfo.license.isActive();
- const licenseType = xpackLicenseInfo.license.getType();
-
- // License is not valid
- if (!isLicenseModeValid) {
- return {
- isAvailable: false,
- isActive: false,
- message: i18n.translate(
- 'xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage',
- {
- defaultMessage:
- 'Your {licenseType} license does not support {pluginName}. Please upgrade your license.',
- values: { licenseType, pluginName },
- }
- ),
- };
- }
-
- // License is valid but not active
- if (!isLicenseActive) {
- return {
- isAvailable: true,
- isActive: false,
- message: i18n.translate('xpack.crossClusterReplication.checkLicense.errorExpiredMessage', {
- defaultMessage:
- 'You cannot use {pluginName} because your {licenseType} license has expired',
- values: { licenseType, pluginName },
- }),
- };
- }
-
- // License is valid and active
- return {
- isAvailable: true,
- isActive: true,
- };
-}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js
deleted file mode 100644
index 11a6fd4e1d816..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js
+++ /dev/null
@@ -1,33 +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 { wrapEsError } from '../wrap_es_error';
-
-describe('wrap_es_error', () => {
- describe('#wrapEsError', () => {
- let originalError;
- beforeEach(() => {
- originalError = new Error('I am an error');
- originalError.statusCode = 404;
- originalError.response = '{}';
- });
-
- it('should return the correct object', () => {
- const wrappedError = wrapEsError(originalError);
-
- expect(wrappedError.statusCode).to.be(originalError.statusCode);
- expect(wrappedError.message).to.be(originalError.message);
- });
-
- it('should return the correct object with custom message', () => {
- const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' });
-
- expect(wrappedError.statusCode).to.be(originalError.statusCode);
- expect(wrappedError.message).to.be('No encontrado!');
- });
- });
-});
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js
deleted file mode 100644
index 5f2141cce9395..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js
+++ /dev/null
@@ -1,44 +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 { isEsErrorFactory } from '../is_es_error_factory';
-import { set } from 'lodash';
-
-class MockAbstractEsError {}
-
-describe('is_es_error_factory', () => {
- let mockServer;
- let isEsError;
-
- beforeEach(() => {
- const mockEsErrors = {
- _Abstract: MockAbstractEsError,
- };
- mockServer = {};
- set(mockServer, 'plugins.elasticsearch.getCluster', () => ({ errors: mockEsErrors }));
-
- isEsError = isEsErrorFactory(mockServer);
- });
-
- describe('#isEsErrorFactory', () => {
- it('should return a function', () => {
- expect(isEsError).to.be.a(Function);
- });
-
- describe('returned function', () => {
- it('should return true if passed-in err is a known esError', () => {
- const knownEsError = new MockAbstractEsError();
- expect(isEsError(knownEsError)).to.be(true);
- });
-
- it('should return false if passed-in err is not a known esError', () => {
- const unknownEsError = {};
- expect(isEsError(unknownEsError)).to.be(false);
- });
- });
- });
-});
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts
deleted file mode 100644
index fc6405b8e7513..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts
+++ /dev/null
@@ -1,18 +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 { memoize } from 'lodash';
-
-const esErrorsFactory = memoize((server: any) => {
- return server.plugins.elasticsearch.getCluster('admin').errors;
-});
-
-export function isEsErrorFactory(server: any) {
- const esErrors = esErrorsFactory(server);
- return function isEsError(err: any) {
- return err instanceof esErrors._Abstract;
- };
-}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts
deleted file mode 100644
index d22505f0e315a..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts
+++ /dev/null
@@ -1,64 +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 { kibanaResponseFactory } from '../../../../../../../../../src/core/server';
-import { licensePreRoutingFactory } from '../license_pre_routing_factory';
-
-describe('license_pre_routing_factory', () => {
- describe('#reportingFeaturePreRoutingFactory', () => {
- let mockDeps: any;
- let mockLicenseCheckResults: any;
-
- const anyContext: any = {};
- const anyRequest: any = {};
-
- beforeEach(() => {
- mockDeps = {
- __LEGACY: {
- server: {
- plugins: {
- xpack_main: {
- info: {
- feature: () => ({
- getLicenseCheckResults: () => mockLicenseCheckResults,
- }),
- },
- },
- },
- },
- },
- requestHandler: jest.fn(),
- };
- });
-
- describe('isAvailable is false', () => {
- beforeEach(() => {
- mockLicenseCheckResults = {
- isAvailable: false,
- };
- });
-
- it('replies with 403', async () => {
- const licensePreRouting = licensePreRoutingFactory(mockDeps);
- const response = await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory);
- expect(response.status).toBe(403);
- });
- });
-
- describe('isAvailable is true', () => {
- beforeEach(() => {
- mockLicenseCheckResults = {
- isAvailable: true,
- };
- });
-
- it('it calls the wrapped handler', async () => {
- const licensePreRouting = licensePreRoutingFactory(mockDeps);
- await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory);
- expect(mockDeps.requestHandler).toHaveBeenCalledTimes(1);
- });
- });
- });
-});
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts
deleted file mode 100644
index c47faa940a650..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts
+++ /dev/null
@@ -1,32 +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 { RequestHandler } from 'src/core/server';
-import { PLUGIN } from '../../../../common/constants';
-
-export const licensePreRoutingFactory = ({
- __LEGACY,
- requestHandler,
-}: {
- __LEGACY: { server: any };
- requestHandler: RequestHandler
;
-}) => {
- const xpackMainPlugin = __LEGACY.server.plugins.xpack_main;
-
- // License checking and enable/disable logic
- const licensePreRouting: RequestHandler
= (ctx, request, response) => {
- const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults();
- if (!licenseCheckResults.isAvailable) {
- return response.forbidden({
- body: licenseCheckResults.message,
- });
- } else {
- return requestHandler(ctx, request, response);
- }
- };
-
- return licensePreRouting;
-};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js
deleted file mode 100644
index 7b0f97c38d129..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js
+++ /dev/null
@@ -1,7 +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 { registerLicenseChecker } from './register_license_checker';
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js
deleted file mode 100644
index b9bb34a80ce79..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js
+++ /dev/null
@@ -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 { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status';
-import { PLUGIN } from '../../../../common/constants';
-import { checkLicense } from '../check_license';
-
-export function registerLicenseChecker(__LEGACY) {
- const xpackMainPlugin = __LEGACY.server.plugins.xpack_main;
- const ccrPluggin = __LEGACY.server.plugins[PLUGIN.ID];
-
- mirrorPluginStatus(xpackMainPlugin, ccrPluggin);
- xpackMainPlugin.status.once('green', () => {
- // Register a function that is called whenever the xpack info changes,
- // to re-compute the license check results for this plugin
- xpackMainPlugin.info.feature(PLUGIN.ID).registerLicenseCheckResultsGenerator(checkLicense);
- });
-}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts
deleted file mode 100644
index 829de10ad0177..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts
+++ /dev/null
@@ -1,38 +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 { Plugin, PluginInitializerContext, CoreSetup } from 'src/core/server';
-
-import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/server';
-
-// @ts-ignore
-import { registerLicenseChecker } from './lib/register_license_checker';
-// @ts-ignore
-import { registerRoutes } from './routes/register_routes';
-import { ccrDataEnricher } from './cross_cluster_replication_data';
-
-interface PluginDependencies {
- indexManagement: IndexManagementPluginSetup;
- __LEGACY: {
- server: any;
- ccrUIEnabled: boolean;
- };
-}
-
-export class CrossClusterReplicationServerPlugin implements Plugin {
- // @ts-ignore
- constructor(private readonly ctx: PluginInitializerContext) {}
- setup({ http }: CoreSetup, { indexManagement, __LEGACY }: PluginDependencies) {
- registerLicenseChecker(__LEGACY);
-
- const router = http.createRouter();
- registerRoutes({ router, __LEGACY });
- if (__LEGACY.ccrUIEnabled && indexManagement && indexManagement.indexDataEnricher) {
- indexManagement.indexDataEnricher.add(ccrDataEnricher);
- }
- }
- start() {}
-}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js
deleted file mode 100644
index f3024515c7213..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js
+++ /dev/null
@@ -1,330 +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 { deserializeAutoFollowPattern } from '../../../../../common/services/auto_follow_pattern_serialization';
-import { callWithRequestFactory } from '../../../lib/call_with_request_factory';
-import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
-import { getAutoFollowPatternMock, getAutoFollowPatternListMock } from '../../../../../fixtures';
-import { registerAutoFollowPatternRoutes } from '../auto_follow_pattern';
-
-import { createRouter, callRoute } from './helpers';
-
-jest.mock('../../../lib/call_with_request_factory');
-jest.mock('../../../lib/is_es_error_factory');
-jest.mock('../../../lib/license_pre_routing_factory', () => ({
- licensePreRoutingFactory: ({ requestHandler }) => requestHandler,
-}));
-
-const DESERIALIZED_KEYS = Object.keys(deserializeAutoFollowPattern(getAutoFollowPatternMock()));
-
-let routeRegistry;
-
-/**
- * Helper to extract all the different server route handler so we can easily call them in our tests.
- *
- * Important: This method registers the handlers in the order that they appear in the file, so
- * if a "server.route()" call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here.
- */
-const registerHandlers = () => {
- const HANDLER_INDEX_TO_ACTION = {
- 0: 'list',
- 1: 'create',
- 2: 'update',
- 3: 'get',
- 4: 'delete',
- 5: 'pause',
- 6: 'resume',
- };
-
- routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION);
-
- registerAutoFollowPatternRoutes({
- __LEGACY: {},
- router: routeRegistry.router,
- });
-};
-
-/**
- * Queue to save request response and errors
- * It allows us to fake multiple responses from the
- * callWithRequestFactory() when the request handler call it
- * multiple times.
- */
-let requestResponseQueue = [];
-
-/**
- * Helper to mock the response from the call to Elasticsearch
- *
- * @param {*} err The mock error to throw
- * @param {*} response The response to return
- */
-const setHttpRequestResponse = (error, response) => {
- requestResponseQueue.push({ error, response });
-};
-
-const resetHttpRequestResponses = () => (requestResponseQueue = []);
-
-const getNextResponseFromQueue = () => {
- if (!requestResponseQueue.length) {
- return null;
- }
-
- const next = requestResponseQueue.shift();
- if (next.error) {
- return Promise.reject(next.error);
- }
- return Promise.resolve(next.response);
-};
-
-describe('[CCR API Routes] Auto Follow Pattern', () => {
- let routeHandler;
-
- beforeAll(() => {
- isEsErrorFactory.mockReturnValue(() => false);
- callWithRequestFactory.mockReturnValue(getNextResponseFromQueue);
- registerHandlers();
- });
-
- describe('list()', () => {
- beforeEach(() => {
- routeHandler = routeRegistry.getRoutes().list;
- });
-
- it('should deserialize the response from Elasticsearch', async () => {
- const totalResult = 2;
- setHttpRequestResponse(null, getAutoFollowPatternListMock(totalResult));
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler);
- const autoFollowPattern = response.patterns[0];
-
- expect(response.patterns.length).toEqual(totalResult);
- expect(Object.keys(autoFollowPattern)).toEqual(DESERIALIZED_KEYS);
- });
- });
-
- describe('create()', () => {
- beforeEach(() => {
- resetHttpRequestResponses();
- routeHandler = routeRegistry.getRoutes().create;
- });
-
- it('should throw a 409 conflict error if id already exists', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
-
- const response = await callRoute(
- routeHandler,
- {},
- {
- body: {
- id: 'some-id',
- foo: 'bar',
- },
- }
- );
-
- expect(response.status).toEqual(409);
- });
-
- it('should return 200 status when the id does not exist', async () => {
- const error = new Error('Resource not found.');
- error.statusCode = 404;
- setHttpRequestResponse(error);
- setHttpRequestResponse(null, { acknowledge: true });
-
- const {
- options: { body: response },
- } = await callRoute(
- routeHandler,
- {},
- {
- body: {
- id: 'some-id',
- foo: 'bar',
- },
- }
- );
-
- expect(response).toEqual({ acknowledge: true });
- });
- });
-
- describe('update()', () => {
- beforeEach(() => {
- routeHandler = routeRegistry.getRoutes().update;
- });
-
- it('should serialize the payload before sending it to Elasticsearch', async () => {
- callWithRequestFactory.mockReturnValueOnce((_, payload) => payload);
-
- const request = {
- params: { id: 'foo' },
- body: {
- remoteCluster: 'bar1',
- leaderIndexPatterns: ['bar2'],
- followIndexPattern: 'bar3',
- },
- };
-
- const response = await callRoute(routeHandler, {}, request);
-
- expect(response.options.body).toEqual({
- id: 'foo',
- body: {
- remote_cluster: 'bar1',
- leader_index_patterns: ['bar2'],
- follow_index_pattern: 'bar3',
- },
- });
- });
- });
-
- describe('get()', () => {
- beforeEach(() => {
- routeHandler = routeRegistry.getRoutes().get;
- });
-
- it('should return a single resource even though ES return an array with 1 item', async () => {
- const autoFollowPattern = getAutoFollowPatternMock();
- const esResponse = { patterns: [autoFollowPattern] };
-
- setHttpRequestResponse(null, esResponse);
-
- const response = await callRoute(routeHandler, {}, { params: { id: 1 } });
- expect(Object.keys(response.options.body)).toEqual(DESERIALIZED_KEYS);
- });
- });
-
- describe('delete()', () => {
- beforeEach(() => {
- resetHttpRequestResponses();
- routeHandler = routeRegistry.getRoutes().delete;
- });
-
- it('should delete a single item', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: 'a' } });
-
- expect(response.itemsDeleted).toEqual(['a']);
- expect(response.errors).toEqual([]);
- });
-
- it('should accept a list of ids to delete', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
-
- const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } });
-
- expect(response.options.body.itemsDeleted).toEqual(['a', 'b', 'c']);
- });
-
- it('should catch error and return them in array', async () => {
- const error = new Error('something went wrong');
- error.response = '{ "error": {} }';
-
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(error);
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } });
-
- expect(response.itemsDeleted).toEqual(['a']);
- expect(response.errors[0].id).toEqual('b');
- });
- });
-
- describe('pause()', () => {
- beforeEach(() => {
- resetHttpRequestResponses();
- routeHandler = routeRegistry.getRoutes().pause;
- });
-
- it('accept a single item', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: 'a' } });
-
- expect(response.itemsPaused).toEqual(['a']);
- expect(response.errors).toEqual([]);
- });
-
- it('should accept a list of items to pause', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
-
- const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } });
-
- expect(response.options.body.itemsPaused).toEqual(['a', 'b', 'c']);
- });
-
- it('should catch error and return them in array', async () => {
- const error = new Error('something went wrong');
- error.response = '{ "error": {} }';
-
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(error);
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } });
-
- expect(response.itemsPaused).toEqual(['a']);
- expect(response.errors[0].id).toEqual('b');
- });
- });
-
- describe('resume()', () => {
- beforeEach(() => {
- resetHttpRequestResponses();
- routeHandler = routeRegistry.getRoutes().resume;
- });
-
- it('accept a single item', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: 'a' } });
-
- expect(response.itemsResumed).toEqual(['a']);
- expect(response.errors).toEqual([]);
- });
-
- it('should accept a list of items to pause', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
-
- const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } });
-
- expect(response.options.body.itemsResumed).toEqual(['a', 'b', 'c']);
- });
-
- it('should catch error and return them in array', async () => {
- const error = new Error('something went wrong');
- error.response = '{ "error": {} }';
-
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(error);
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } });
-
- expect(response.itemsResumed).toEqual(['a']);
- expect(response.errors[0].id).toEqual('b');
- });
- });
-});
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js
deleted file mode 100644
index f0139e5bd7011..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js
+++ /dev/null
@@ -1,312 +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 { deserializeFollowerIndex } from '../../../../../common/services/follower_index_serialization';
-import {
- getFollowerIndexStatsMock,
- getFollowerIndexListStatsMock,
- getFollowerIndexInfoMock,
- getFollowerIndexListInfoMock,
-} from '../../../../../fixtures';
-import { callWithRequestFactory } from '../../../lib/call_with_request_factory';
-import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
-import { registerFollowerIndexRoutes } from '../follower_index';
-import { createRouter, callRoute } from './helpers';
-
-jest.mock('../../../lib/call_with_request_factory');
-jest.mock('../../../lib/is_es_error_factory');
-jest.mock('../../../lib/license_pre_routing_factory', () => ({
- licensePreRoutingFactory: ({ requestHandler }) => requestHandler,
-}));
-
-const DESERIALIZED_KEYS = Object.keys(
- deserializeFollowerIndex({
- ...getFollowerIndexInfoMock(),
- ...getFollowerIndexStatsMock(),
- })
-);
-
-let routeRegistry;
-
-/**
- * Helper to extract all the different server route handler so we can easily call them in our tests.
- *
- * Important: This method registers the handlers in the order that they appear in the file, so
- * if a 'server.route()' call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here.
- */
-const registerHandlers = () => {
- const HANDLER_INDEX_TO_ACTION = {
- 0: 'list',
- 1: 'get',
- 2: 'create',
- 3: 'edit',
- 4: 'pause',
- 5: 'resume',
- 6: 'unfollow',
- };
-
- routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION);
- registerFollowerIndexRoutes({
- __LEGACY: {},
- router: routeRegistry.router,
- });
-};
-
-/**
- * Queue to save request response and errors
- * It allows us to fake multiple responses from the
- * callWithRequestFactory() when the request handler call it
- * multiple times.
- */
-let requestResponseQueue = [];
-
-/**
- * Helper to mock the response from the call to Elasticsearch
- *
- * @param {*} err The mock error to throw
- * @param {*} response The response to return
- */
-const setHttpRequestResponse = (error, response) => {
- requestResponseQueue.push({ error, response });
-};
-
-const resetHttpRequestResponses = () => (requestResponseQueue = []);
-
-const getNextResponseFromQueue = () => {
- if (!requestResponseQueue.length) {
- return null;
- }
-
- const next = requestResponseQueue.shift();
- if (next.error) {
- return Promise.reject(next.error);
- }
- return Promise.resolve(next.response);
-};
-
-describe('[CCR API Routes] Follower Index', () => {
- let routeHandler;
-
- beforeAll(() => {
- isEsErrorFactory.mockReturnValue(() => false);
- callWithRequestFactory.mockReturnValue(getNextResponseFromQueue);
- registerHandlers();
- });
-
- describe('list()', () => {
- beforeEach(() => {
- routeHandler = routeRegistry.getRoutes().list;
- });
-
- it('deserializes the response from Elasticsearch', async () => {
- const totalResult = 2;
- const infoResult = getFollowerIndexListInfoMock(totalResult);
- const statsResult = getFollowerIndexListStatsMock(
- totalResult,
- infoResult.follower_indices.map(index => index.follower_index)
- );
- setHttpRequestResponse(null, infoResult);
- setHttpRequestResponse(null, statsResult);
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler);
- const followerIndex = response.indices[0];
-
- expect(response.indices.length).toEqual(totalResult);
- expect(Object.keys(followerIndex)).toEqual(DESERIALIZED_KEYS);
- });
- });
-
- describe('get()', () => {
- beforeEach(() => {
- routeHandler = routeRegistry.getRoutes().get;
- });
-
- it('should return a single resource even though ES return an array with 1 item', async () => {
- const mockId = 'test1';
- const followerIndexInfo = getFollowerIndexInfoMock(mockId);
- const followerIndexStats = getFollowerIndexStatsMock(mockId);
-
- setHttpRequestResponse(null, { follower_indices: [followerIndexInfo] });
- setHttpRequestResponse(null, { indices: [followerIndexStats] });
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: mockId } });
- expect(Object.keys(response)).toEqual(DESERIALIZED_KEYS);
- });
- });
-
- describe('create()', () => {
- beforeEach(() => {
- resetHttpRequestResponses();
- routeHandler = routeRegistry.getRoutes().create;
- });
-
- it('should return 200 status when follower index is created', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
-
- const response = await callRoute(
- routeHandler,
- {},
- {
- body: {
- name: 'follower_index',
- remoteCluster: 'remote_cluster',
- leaderIndex: 'leader_index',
- },
- }
- );
-
- expect(response.options.body).toEqual({ acknowledge: true });
- });
- });
-
- describe('pause()', () => {
- beforeEach(() => {
- resetHttpRequestResponses();
- routeHandler = routeRegistry.getRoutes().pause;
- });
-
- it('should pause a single item', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: '1' } });
-
- expect(response.itemsPaused).toEqual(['1']);
- expect(response.errors).toEqual([]);
- });
-
- it('should accept a list of ids to pause', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
-
- const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } });
-
- expect(response.options.body.itemsPaused).toEqual(['1', '2', '3']);
- });
-
- it('should catch error and return them in array', async () => {
- const error = new Error('something went wrong');
- error.response = '{ "error": {} }';
-
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(error);
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: '1,2' } });
-
- expect(response.itemsPaused).toEqual(['1']);
- expect(response.errors[0].id).toEqual('2');
- });
- });
-
- describe('resume()', () => {
- beforeEach(() => {
- resetHttpRequestResponses();
- routeHandler = routeRegistry.getRoutes().resume;
- });
-
- it('should resume a single item', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: '1' } });
-
- expect(response.itemsResumed).toEqual(['1']);
- expect(response.errors).toEqual([]);
- });
-
- it('should accept a list of ids to resume', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
-
- const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } });
-
- expect(response.options.body.itemsResumed).toEqual(['1', '2', '3']);
- });
-
- it('should catch error and return them in array', async () => {
- const error = new Error('something went wrong');
- error.response = '{ "error": {} }';
-
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(error);
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: '1,2' } });
-
- expect(response.itemsResumed).toEqual(['1']);
- expect(response.errors[0].id).toEqual('2');
- });
- });
-
- describe('unfollow()', () => {
- beforeEach(() => {
- resetHttpRequestResponses();
- routeHandler = routeRegistry.getRoutes().unfollow;
- });
-
- it('should unfollow await single item', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: '1' } });
-
- expect(response.itemsUnfollowed).toEqual(['1']);
- expect(response.errors).toEqual([]);
- });
-
- it('should accept a list of ids to unfollow', async () => {
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
-
- const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } });
-
- expect(response.options.body.itemsUnfollowed).toEqual(['1', '2', '3']);
- });
-
- it('should catch error and return them in array', async () => {
- const error = new Error('something went wrong');
- error.response = '{ "error": {} }';
-
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(null, { acknowledge: true });
- setHttpRequestResponse(error);
-
- const {
- options: { body: response },
- } = await callRoute(routeHandler, {}, { params: { id: '1,2' } });
-
- expect(response.itemsUnfollowed).toEqual(['1']);
- expect(response.errors[0].id).toEqual('2');
- });
- });
-});
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts
deleted file mode 100644
index 555fc0937c0ad..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts
+++ /dev/null
@@ -1,37 +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 { RequestHandler } from 'src/core/server';
-import { kibanaResponseFactory } from '../../../../../../../../../src/core/server';
-
-export const callRoute = (
- route: RequestHandler,
- ctx = {},
- request = {},
- response = kibanaResponseFactory
-) => {
- return route(ctx as any, request as any, response);
-};
-
-export const createRouter = (indexToActionMap: Record) => {
- let index = 0;
- const routeHandlers: Record> = {};
- const addHandler = (ignoreCtxForNow: any, handler: RequestHandler) => {
- // Save handler and increment index
- routeHandlers[indexToActionMap[index]] = handler;
- index++;
- };
-
- return {
- getRoutes: () => routeHandlers,
- router: {
- get: addHandler,
- post: addHandler,
- put: addHandler,
- delete: addHandler,
- },
- };
-};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts
deleted file mode 100644
index d458f1ccb354b..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts
+++ /dev/null
@@ -1,301 +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';
-// @ts-ignore
-import { callWithRequestFactory } from '../../lib/call_with_request_factory';
-import { isEsError } from '../../lib/is_es_error';
-// @ts-ignore
-import {
- deserializeAutoFollowPattern,
- deserializeListAutoFollowPatterns,
- serializeAutoFollowPattern,
- // @ts-ignore
-} from '../../../../common/services/auto_follow_pattern_serialization';
-
-import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
-import { API_BASE_PATH } from '../../../../common/constants';
-
-import { RouteDependencies } from '../types';
-import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error';
-
-export const registerAutoFollowPatternRoutes = ({ router, __LEGACY }: RouteDependencies) => {
- /**
- * Returns a list of all auto-follow patterns
- */
- router.get(
- {
- path: `${API_BASE_PATH}/auto_follow_patterns`,
- validate: false,
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
-
- try {
- const result = await callWithRequest('ccr.autoFollowPatterns');
- return response.ok({
- body: {
- patterns: deserializeListAutoFollowPatterns(result.patterns),
- },
- });
- } catch (err) {
- return mapErrorToKibanaHttpResponse(err);
- }
- },
- })
- );
-
- /**
- * Create an auto-follow pattern
- */
- router.post(
- {
- path: `${API_BASE_PATH}/auto_follow_patterns`,
- validate: {
- body: schema.object(
- {
- id: schema.string(),
- },
- { unknowns: 'allow' }
- ),
- },
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
- const { id, ...rest } = request.body;
- const body = serializeAutoFollowPattern(rest);
-
- /**
- * First let's make sur that an auto-follow pattern with
- * the same id does not exist.
- */
- try {
- await callWithRequest('ccr.autoFollowPattern', { id });
- // If we get here it means that an auto-follow pattern with the same id exists
- return response.conflict({
- body: `An auto-follow pattern with the name "${id}" already exists.`,
- });
- } catch (err) {
- if (err.statusCode !== 404) {
- return mapErrorToKibanaHttpResponse(err);
- }
- }
-
- try {
- return response.ok({
- body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }),
- });
- } catch (err) {
- return mapErrorToKibanaHttpResponse(err);
- }
- },
- })
- );
-
- /**
- * Update an auto-follow pattern
- */
- router.put(
- {
- path: `${API_BASE_PATH}/auto_follow_patterns/{id}`,
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- body: schema.object({}, { unknowns: 'allow' }),
- },
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
- const { id } = request.params;
- const body = serializeAutoFollowPattern(request.body);
-
- try {
- return response.ok({
- body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }),
- });
- } catch (err) {
- return mapErrorToKibanaHttpResponse(err);
- }
- },
- })
- );
-
- /**
- * Returns a single auto-follow pattern
- */
- router.get(
- {
- path: `${API_BASE_PATH}/auto_follow_patterns/{id}`,
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- },
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
- const { id } = request.params;
-
- try {
- const result = await callWithRequest('ccr.autoFollowPattern', { id });
- const autoFollowPattern = result.patterns[0];
-
- return response.ok({
- body: deserializeAutoFollowPattern(autoFollowPattern),
- });
- } catch (err) {
- return mapErrorToKibanaHttpResponse(err);
- }
- },
- })
- );
-
- /**
- * Delete an auto-follow pattern
- */
- router.delete(
- {
- path: `${API_BASE_PATH}/auto_follow_patterns/{id}`,
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- },
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
- const { id } = request.params;
- const ids = id.split(',');
-
- const itemsDeleted: string[] = [];
- const errors: Array<{ id: string; error: any }> = [];
-
- await Promise.all(
- ids.map(_id =>
- callWithRequest('ccr.deleteAutoFollowPattern', { id: _id })
- .then(() => itemsDeleted.push(_id))
- .catch((err: Error) => {
- if (isEsError(err)) {
- errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
- } else {
- errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
- }
- })
- )
- );
-
- return response.ok({
- body: {
- itemsDeleted,
- errors,
- },
- });
- },
- })
- );
-
- /**
- * Pause auto-follow pattern(s)
- */
- router.post(
- {
- path: `${API_BASE_PATH}/auto_follow_patterns/{id}/pause`,
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- },
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
- const { id } = request.params;
- const ids = id.split(',');
-
- const itemsPaused: string[] = [];
- const errors: Array<{ id: string; error: any }> = [];
-
- await Promise.all(
- ids.map(_id =>
- callWithRequest('ccr.pauseAutoFollowPattern', { id: _id })
- .then(() => itemsPaused.push(_id))
- .catch((err: Error) => {
- if (isEsError(err)) {
- errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
- } else {
- errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
- }
- })
- )
- );
-
- return response.ok({
- body: {
- itemsPaused,
- errors,
- },
- });
- },
- })
- );
-
- /**
- * Resume auto-follow pattern(s)
- */
- router.post(
- {
- path: `${API_BASE_PATH}/auto_follow_patterns/{id}/resume`,
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- },
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
- const { id } = request.params;
- const ids = id.split(',');
-
- const itemsResumed: string[] = [];
- const errors: Array<{ id: string; error: any }> = [];
-
- await Promise.all(
- ids.map(_id =>
- callWithRequest('ccr.resumeAutoFollowPattern', { id: _id })
- .then(() => itemsResumed.push(_id))
- .catch((err: Error) => {
- if (isEsError(err)) {
- errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
- } else {
- errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
- }
- })
- )
- );
-
- return response.ok({
- body: {
- itemsResumed,
- errors,
- },
- });
- },
- })
- );
-};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts
deleted file mode 100644
index b08b056ad2c8a..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts
+++ /dev/null
@@ -1,112 +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 { API_BASE_PATH } from '../../../../common/constants';
-// @ts-ignore
-import { callWithRequestFactory } from '../../lib/call_with_request_factory';
-// @ts-ignore
-import { deserializeAutoFollowStats } from '../../lib/ccr_stats_serialization';
-import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
-
-import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error';
-import { RouteDependencies } from '../types';
-
-export const registerCcrRoutes = ({ router, __LEGACY }: RouteDependencies) => {
- /**
- * Returns Auto-follow stats
- */
- router.get(
- {
- path: `${API_BASE_PATH}/stats/auto_follow`,
- validate: false,
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
-
- try {
- const { auto_follow_stats: autoFollowStats } = await callWithRequest('ccr.stats');
-
- return response.ok({
- body: deserializeAutoFollowStats(autoFollowStats),
- });
- } catch (err) {
- return mapErrorToKibanaHttpResponse(err);
- }
- },
- })
- );
-
- /**
- * Returns whether the user has CCR permissions
- */
- router.get(
- {
- path: `${API_BASE_PATH}/permissions`,
- validate: false,
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const xpackMainPlugin = __LEGACY.server.plugins.xpack_main;
- const xpackInfo = xpackMainPlugin && xpackMainPlugin.info;
-
- if (!xpackInfo) {
- // xpackInfo is updated via poll, so it may not be available until polling has begun.
- // In this rare situation, tell the client the service is temporarily unavailable.
- return response.customError({
- statusCode: 503,
- body: 'Security info unavailable',
- });
- }
-
- const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security');
- if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) {
- // If security isn't enabled or available (in the case where security is enabled but license reverted to Basic) let the user use CCR.
- return response.ok({
- body: {
- hasPermission: true,
- missingClusterPrivileges: [],
- },
- });
- }
-
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
-
- try {
- const { has_all_requested: hasPermission, cluster } = await callWithRequest(
- 'ccr.permissions',
- {
- body: {
- cluster: ['manage', 'manage_ccr'],
- },
- }
- );
-
- const missingClusterPrivileges = Object.keys(cluster).reduce(
- (permissions: any, permissionName: any) => {
- if (!cluster[permissionName]) {
- permissions.push(permissionName);
- return permissions;
- }
- },
- [] as any[]
- );
-
- return response.ok({
- body: {
- hasPermission,
- missingClusterPrivileges,
- },
- });
- } catch (err) {
- return mapErrorToKibanaHttpResponse(err);
- }
- },
- })
- );
-};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts
deleted file mode 100644
index 1d7dacf4a8688..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts
+++ /dev/null
@@ -1,357 +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 {
- deserializeFollowerIndex,
- deserializeListFollowerIndices,
- serializeFollowerIndex,
- serializeAdvancedSettings,
- // @ts-ignore
-} from '../../../../common/services/follower_index_serialization';
-import { API_BASE_PATH } from '../../../../common/constants';
-// @ts-ignore
-import { removeEmptyFields } from '../../../../common/services/utils';
-// @ts-ignore
-import { callWithRequestFactory } from '../../lib/call_with_request_factory';
-import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
-
-import { RouteDependencies } from '../types';
-import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error';
-
-export const registerFollowerIndexRoutes = ({ router, __LEGACY }: RouteDependencies) => {
- /**
- * Returns a list of all follower indices
- */
- router.get(
- {
- path: `${API_BASE_PATH}/follower_indices`,
- validate: false,
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
-
- try {
- const { follower_indices: followerIndices } = await callWithRequest('ccr.info', {
- id: '_all',
- });
-
- const {
- follow_stats: { indices: followerIndicesStats },
- } = await callWithRequest('ccr.stats');
-
- const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => {
- map[stats.index] = stats;
- return map;
- }, {});
-
- const collatedFollowerIndices = followerIndices.map((followerIndex: any) => {
- return {
- ...followerIndex,
- ...followerIndicesStatsMap[followerIndex.follower_index],
- };
- });
-
- return response.ok({
- body: {
- indices: deserializeListFollowerIndices(collatedFollowerIndices),
- },
- });
- } catch (err) {
- return mapErrorToKibanaHttpResponse(err);
- }
- },
- })
- );
-
- /**
- * Returns a single follower index pattern
- */
- router.get(
- {
- path: `${API_BASE_PATH}/follower_indices/{id}`,
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- },
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
- const { id } = request.params;
-
- try {
- const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id });
-
- const followerIndexInfo = followerIndices && followerIndices[0];
-
- if (!followerIndexInfo) {
- return response.notFound({
- body: `The follower index "${id}" does not exist.`,
- });
- }
-
- // If this follower is paused, skip call to ES stats api since it will return 404
- if (followerIndexInfo.status === 'paused') {
- return response.ok({
- body: deserializeFollowerIndex({
- ...followerIndexInfo,
- }),
- });
- } else {
- const {
- indices: followerIndicesStats,
- } = await callWithRequest('ccr.followerIndexStats', { id });
-
- return response.ok({
- body: deserializeFollowerIndex({
- ...followerIndexInfo,
- ...(followerIndicesStats ? followerIndicesStats[0] : {}),
- }),
- });
- }
- } catch (err) {
- return mapErrorToKibanaHttpResponse(err);
- }
- },
- })
- );
-
- /**
- * Create a follower index
- */
- router.post(
- {
- path: `${API_BASE_PATH}/follower_indices`,
- validate: {
- body: schema.object(
- {
- name: schema.string(),
- },
- { unknowns: 'allow' }
- ),
- },
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
- const { name, ...rest } = request.body;
- const body = removeEmptyFields(serializeFollowerIndex(rest));
-
- try {
- return response.ok({
- body: await callWithRequest('ccr.saveFollowerIndex', { name, body }),
- });
- } catch (err) {
- return mapErrorToKibanaHttpResponse(err);
- }
- },
- })
- );
-
- /**
- * Edit a follower index
- */
- router.put(
- {
- path: `${API_BASE_PATH}/follower_indices/{id}`,
- validate: {
- params: schema.object({ id: schema.string() }),
- body: schema.object({
- maxReadRequestOperationCount: schema.maybe(schema.number()),
- maxOutstandingReadRequests: schema.maybe(schema.number()),
- maxReadRequestSize: schema.maybe(schema.string()), // byte value
- maxWriteRequestOperationCount: schema.maybe(schema.number()),
- maxWriteRequestSize: schema.maybe(schema.string()), // byte value
- maxOutstandingWriteRequests: schema.maybe(schema.number()),
- maxWriteBufferCount: schema.maybe(schema.number()),
- maxWriteBufferSize: schema.maybe(schema.string()), // byte value
- maxRetryDelay: schema.maybe(schema.string()), // time value
- readPollTimeout: schema.maybe(schema.string()), // time value
- }),
- },
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
- const { id } = request.params;
-
- // We need to first pause the follower and then resume it passing the advanced settings
- try {
- const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id });
- const followerIndexInfo = followerIndices && followerIndices[0];
- if (!followerIndexInfo) {
- return response.notFound({ body: `The follower index "${id}" does not exist.` });
- }
-
- // Retrieve paused state instead of pulling it from the payload to ensure it's not stale.
- const isPaused = followerIndexInfo.status === 'paused';
- // Pause follower if not already paused
- if (!isPaused) {
- await callWithRequest('ccr.pauseFollowerIndex', { id });
- }
-
- // Resume follower
- const body = removeEmptyFields(serializeAdvancedSettings(request.body));
- return response.ok({
- body: await callWithRequest('ccr.resumeFollowerIndex', { id, body }),
- });
- } catch (err) {
- return mapErrorToKibanaHttpResponse(err);
- }
- },
- })
- );
-
- /**
- * Pauses a follower index
- */
- router.put(
- {
- path: `${API_BASE_PATH}/follower_indices/{id}/pause`,
- validate: {
- params: schema.object({ id: schema.string() }),
- },
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
- const { id } = request.params;
- const ids = id.split(',');
-
- const itemsPaused: string[] = [];
- const errors: Array<{ id: string; error: any }> = [];
-
- await Promise.all(
- ids.map(_id =>
- callWithRequest('ccr.pauseFollowerIndex', { id: _id })
- .then(() => itemsPaused.push(_id))
- .catch((err: Error) => {
- errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
- })
- )
- );
-
- return response.ok({
- body: {
- itemsPaused,
- errors,
- },
- });
- },
- })
- );
-
- /**
- * Resumes a follower index
- */
- router.put(
- {
- path: `${API_BASE_PATH}/follower_indices/{id}/resume`,
- validate: {
- params: schema.object({ id: schema.string() }),
- },
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
- const { id } = request.params;
- const ids = id.split(',');
-
- const itemsResumed: string[] = [];
- const errors: Array<{ id: string; error: any }> = [];
-
- await Promise.all(
- ids.map(_id =>
- callWithRequest('ccr.resumeFollowerIndex', { id: _id })
- .then(() => itemsResumed.push(_id))
- .catch((err: Error) => {
- errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
- })
- )
- );
-
- return response.ok({
- body: {
- itemsResumed,
- errors,
- },
- });
- },
- })
- );
-
- /**
- * Unfollow follower index's leader index
- */
- router.put(
- {
- path: `${API_BASE_PATH}/follower_indices/{id}/unfollow`,
- validate: {
- params: schema.object({ id: schema.string() }),
- },
- },
- licensePreRoutingFactory({
- __LEGACY,
- requestHandler: async (ctx, request, response) => {
- const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
- const { id } = request.params;
- const ids = id.split(',');
-
- const itemsUnfollowed: string[] = [];
- const itemsNotOpen: string[] = [];
- const errors: Array<{ id: string; error: any }> = [];
-
- await Promise.all(
- ids.map(async _id => {
- try {
- // Try to pause follower, let it fail silently since it may already be paused
- try {
- await callWithRequest('ccr.pauseFollowerIndex', { id: _id });
- } catch (e) {
- // Swallow errors
- }
-
- // Close index
- await callWithRequest('indices.close', { index: _id });
-
- // Unfollow leader
- await callWithRequest('ccr.unfollowLeaderIndex', { id: _id });
-
- // Try to re-open the index, store failures in a separate array to surface warnings in the UI
- // This will allow users to query their index normally after unfollowing
- try {
- await callWithRequest('indices.open', { index: _id });
- } catch (e) {
- itemsNotOpen.push(_id);
- }
-
- // Push success
- itemsUnfollowed.push(_id);
- } catch (err) {
- errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
- }
- })
- );
-
- return response.ok({
- body: {
- itemsUnfollowed,
- itemsNotOpen,
- errors,
- },
- });
- },
- })
- );
-};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts
deleted file mode 100644
index 6a81bd26dc47d..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts
+++ /dev/null
@@ -1,26 +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 { kibanaResponseFactory } from '../../../../../../../src/core/server';
-// @ts-ignore
-import { wrapEsError } from '../lib/error_wrappers';
-import { isEsError } from '../lib/is_es_error';
-
-export const mapErrorToKibanaHttpResponse = (err: any) => {
- if (isEsError(err)) {
- const { statusCode, message, body } = wrapEsError(err);
- return kibanaResponseFactory.customError({
- statusCode,
- body: {
- message,
- attributes: {
- cause: body?.cause,
- },
- },
- });
- }
- return kibanaResponseFactory.internalError(err);
-};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts
deleted file mode 100644
index 7f57c20c536e0..0000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts
+++ /dev/null
@@ -1,13 +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 } from 'src/core/server';
-
-export interface RouteDependencies {
- router: IRouter;
- __LEGACY: {
- server: any;
- };
-}
diff --git a/x-pack/plugins/cross_cluster_replication/common/constants/index.ts b/x-pack/plugins/cross_cluster_replication/common/constants/index.ts
new file mode 100644
index 0000000000000..797141b0996af
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/common/constants/index.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+import { LicenseType } from '../../../licensing/common/types';
+
+const platinumLicense: LicenseType = 'platinum';
+
+export const PLUGIN = {
+ ID: 'crossClusterReplication',
+ TITLE: i18n.translate('xpack.crossClusterReplication.appTitle', {
+ defaultMessage: 'Cross-Cluster Replication',
+ }),
+ minimumLicenseType: platinumLicense,
+};
+
+export const APPS = {
+ CCR_APP: 'ccr',
+ REMOTE_CLUSTER_APP: 'remote_cluster',
+};
+
+export const MANAGEMENT_ID = 'cross_cluster_replication';
+export const BASE_PATH = `/management/elasticsearch/${MANAGEMENT_ID}`;
+export const BASE_PATH_REMOTE_CLUSTERS = '/management/elasticsearch/remote_clusters';
+export const API_BASE_PATH = '/api/cross_cluster_replication';
+export const API_REMOTE_CLUSTERS_BASE_PATH = '/api/remote_clusters';
+export const API_INDEX_MANAGEMENT_BASE_PATH = '/api/index_management';
+
+export const FOLLOWER_INDEX_ADVANCED_SETTINGS = {
+ maxReadRequestOperationCount: 5120,
+ maxOutstandingReadRequests: 12,
+ maxReadRequestSize: '32mb',
+ maxWriteRequestOperationCount: 5120,
+ maxWriteRequestSize: '9223372036854775807b',
+ maxOutstandingWriteRequests: 9,
+ maxWriteBufferCount: 2147483647,
+ maxWriteBufferSize: '512mb',
+ maxRetryDelay: '500ms',
+ readPollTimeout: '1m',
+};
diff --git a/x-pack/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.ts.snap b/x-pack/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.ts.snap
new file mode 100644
index 0000000000000..c20556fe1434d
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.ts.snap
@@ -0,0 +1,128 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`[CCR] follower index serialization deserializeFollowerIndex() deserializes Elasticsearch follower index object 1`] = `
+Object {
+ "leaderIndex": "leader 1",
+ "maxOutstandingReadRequests": 1,
+ "maxOutstandingWriteRequests": 1,
+ "maxReadRequestOperationCount": 1,
+ "maxReadRequestSize": "1b",
+ "maxRetryDelay": "1s",
+ "maxWriteBufferCount": 1,
+ "maxWriteBufferSize": "1b",
+ "maxWriteRequestOperationCount": 1,
+ "maxWriteRequestSize": "1b",
+ "name": "follower index 1",
+ "readPollTimeout": "1s",
+ "remoteCluster": "cluster 1",
+ "shards": Array [
+ Object {
+ "bytesReadCount": 1,
+ "failedReadRequestsCount": 1,
+ "failedWriteRequestsCount": 1,
+ "followerGlobalCheckpoint": 1,
+ "followerMappingVersion": 1,
+ "followerMaxSequenceNum": 1,
+ "followerSettingsVersion": 1,
+ "id": 1,
+ "lastRequestedSequenceNum": 1,
+ "leaderGlobalCheckpoint": 1,
+ "leaderIndex": "leader 1",
+ "leaderMaxSequenceNum": 1,
+ "operationsReadCount": 1,
+ "operationsWrittenCount": 1,
+ "outstandingReadRequestsCount": 1,
+ "outstandingWriteRequestsCount": 1,
+ "readExceptions": Array [],
+ "remoteCluster": "cluster 1",
+ "successfulReadRequestCount": 1,
+ "successfulWriteRequestsCount": 1,
+ "timeSinceLastReadMs": 1,
+ "totalReadRemoteExecTimeMs": 1,
+ "totalReadTimeMs": 1,
+ "totalWriteTimeMs": 1,
+ "writeBufferOperationsCount": 1,
+ "writeBufferSizeBytes": 1,
+ },
+ Object {
+ "bytesReadCount": undefined,
+ "failedReadRequestsCount": undefined,
+ "failedWriteRequestsCount": undefined,
+ "followerGlobalCheckpoint": undefined,
+ "followerMappingVersion": undefined,
+ "followerMaxSequenceNum": undefined,
+ "followerSettingsVersion": undefined,
+ "id": "shard 2",
+ "lastRequestedSequenceNum": undefined,
+ "leaderGlobalCheckpoint": undefined,
+ "leaderIndex": "leader_index 2",
+ "leaderMaxSequenceNum": undefined,
+ "operationsReadCount": undefined,
+ "operationsWrittenCount": undefined,
+ "outstandingReadRequestsCount": undefined,
+ "outstandingWriteRequestsCount": undefined,
+ "readExceptions": undefined,
+ "remoteCluster": "remote_cluster 2",
+ "successfulReadRequestCount": undefined,
+ "successfulWriteRequestsCount": undefined,
+ "timeSinceLastReadMs": undefined,
+ "totalReadRemoteExecTimeMs": undefined,
+ "totalReadTimeMs": undefined,
+ "totalWriteTimeMs": undefined,
+ "writeBufferOperationsCount": undefined,
+ "writeBufferSizeBytes": undefined,
+ },
+ ],
+ "status": "active",
+}
+`;
+
+exports[`[CCR] follower index serialization deserializeShard() deserializes shard 1`] = `
+Object {
+ "bytesReadCount": 1,
+ "failedReadRequestsCount": 1,
+ "failedWriteRequestsCount": 1,
+ "followerGlobalCheckpoint": 1,
+ "followerMappingVersion": 1,
+ "followerMaxSequenceNum": 1,
+ "followerSettingsVersion": 1,
+ "id": 1,
+ "lastRequestedSequenceNum": 1,
+ "leaderGlobalCheckpoint": 1,
+ "leaderIndex": "leader index",
+ "leaderMaxSequenceNum": 1,
+ "operationsReadCount": 1,
+ "operationsWrittenCount": 1,
+ "outstandingReadRequestsCount": 1,
+ "outstandingWriteRequestsCount": 1,
+ "readExceptions": Array [
+ "read exception",
+ ],
+ "remoteCluster": "remote cluster",
+ "successfulReadRequestCount": 1,
+ "successfulWriteRequestsCount": 1,
+ "timeSinceLastReadMs": 1,
+ "totalReadRemoteExecTimeMs": 1,
+ "totalReadTimeMs": 1,
+ "totalWriteTimeMs": 1,
+ "writeBufferOperationsCount": 1,
+ "writeBufferSizeBytes": 1,
+}
+`;
+
+exports[`[CCR] follower index serialization serializeFollowerIndex() serializes object to Elasticsearch follower index object 1`] = `
+Object {
+ "leader_index": "leader index",
+ "max_outstanding_read_requests": 1,
+ "max_outstanding_write_requests": 1,
+ "max_read_request_operation_count": 1,
+ "max_read_request_size": "1b",
+ "max_retry_delay": "1s",
+ "max_write_buffer_count": 1,
+ "max_write_buffer_size": "1b",
+ "max_write_request_operation_count": 1,
+ "max_write_request_size": "1b",
+ "read_poll_timeout": "1s",
+ "remote_cluster": "remote cluster",
+}
+`;
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.ts
similarity index 85%
rename from x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js
rename to x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.ts
index eef87a6cc4c89..fe3e59f21ee23 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js
+++ b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.ts
@@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { AutoFollowPattern, AutoFollowPatternFromEs } from '../types';
+
import {
deserializeAutoFollowPattern,
deserializeListAutoFollowPatterns,
@@ -12,13 +14,10 @@ import {
describe('[CCR] auto-follow_serialization', () => {
describe('deserializeAutoFollowPattern()', () => {
- it('should return empty object if name or esObject are not provided', () => {
- expect(deserializeAutoFollowPattern()).toEqual({});
- });
-
it('should deserialize Elasticsearch object', () => {
const expected = {
name: 'some-name',
+ active: true,
remoteCluster: 'foo',
leaderIndexPatterns: ['foo-*'],
followIndexPattern: 'bar',
@@ -27,13 +26,14 @@ describe('[CCR] auto-follow_serialization', () => {
const esObject = {
name: 'some-name',
pattern: {
+ active: true,
remote_cluster: expected.remoteCluster,
leader_index_patterns: expected.leaderIndexPatterns,
follow_index_pattern: expected.followIndexPattern,
},
};
- expect(deserializeAutoFollowPattern(esObject)).toEqual(expected);
+ expect(deserializeAutoFollowPattern(esObject as AutoFollowPatternFromEs)).toEqual(expected);
});
});
@@ -78,7 +78,9 @@ describe('[CCR] auto-follow_serialization', () => {
],
};
- expect(deserializeListAutoFollowPatterns(esObjects.patterns)).toEqual(expected);
+ expect(
+ deserializeListAutoFollowPatterns(esObjects.patterns as AutoFollowPatternFromEs[])
+ ).toEqual(expected);
});
});
@@ -96,7 +98,7 @@ describe('[CCR] auto-follow_serialization', () => {
followIndexPattern: expected.follow_index_pattern,
};
- expect(serializeAutoFollowPattern(object)).toEqual(expected);
+ expect(serializeAutoFollowPattern(object as AutoFollowPattern)).toEqual(expected);
});
});
});
diff --git a/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts
new file mode 100644
index 0000000000000..265af0ede1462
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts
@@ -0,0 +1,38 @@
+/*
+ * 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 { AutoFollowPattern, AutoFollowPatternFromEs, AutoFollowPatternToEs } from '../types';
+
+export const deserializeAutoFollowPattern = (
+ autoFollowPattern: AutoFollowPatternFromEs
+): AutoFollowPattern => {
+ const {
+ name,
+ pattern: { active, remote_cluster, leader_index_patterns, follow_index_pattern },
+ } = autoFollowPattern;
+
+ return {
+ name,
+ active,
+ remoteCluster: remote_cluster,
+ leaderIndexPatterns: leader_index_patterns,
+ followIndexPattern: follow_index_pattern,
+ };
+};
+
+export const deserializeListAutoFollowPatterns = (
+ autoFollowPatterns: AutoFollowPatternFromEs[]
+): AutoFollowPattern[] => autoFollowPatterns.map(deserializeAutoFollowPattern);
+
+export const serializeAutoFollowPattern = ({
+ remoteCluster,
+ leaderIndexPatterns,
+ followIndexPattern,
+}: AutoFollowPattern): AutoFollowPatternToEs => ({
+ remote_cluster: remoteCluster,
+ leader_index_patterns: leaderIndexPatterns,
+ follow_index_pattern: followIndexPattern,
+});
diff --git a/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.ts b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.ts
new file mode 100644
index 0000000000000..bfe3e1b3443e6
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.ts
@@ -0,0 +1,224 @@
+/*
+ * 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 { ShardFromEs, FollowerIndexFromEs, FollowerIndex } from '../types';
+
+import {
+ deserializeShard,
+ deserializeFollowerIndex,
+ deserializeListFollowerIndices,
+ serializeFollowerIndex,
+} from './follower_index_serialization';
+
+describe('[CCR] follower index serialization', () => {
+ describe('deserializeShard()', () => {
+ it('deserializes shard', () => {
+ const serializedShard = {
+ remote_cluster: 'remote cluster',
+ leader_index: 'leader index',
+ shard_id: 1,
+ leader_global_checkpoint: 1,
+ leader_max_seq_no: 1,
+ follower_global_checkpoint: 1,
+ follower_max_seq_no: 1,
+ last_requested_seq_no: 1,
+ outstanding_read_requests: 1,
+ outstanding_write_requests: 1,
+ write_buffer_operation_count: 1,
+ write_buffer_size_in_bytes: 1,
+ follower_mapping_version: 1,
+ follower_settings_version: 1,
+ total_read_time_millis: 1,
+ total_read_remote_exec_time_millis: 1,
+ successful_read_requests: 1,
+ failed_read_requests: 1,
+ operations_read: 1,
+ bytes_read: 1,
+ total_write_time_millis: 1,
+ successful_write_requests: 1,
+ failed_write_requests: 1,
+ operations_written: 1,
+ read_exceptions: ['read exception'],
+ time_since_last_read_millis: 1,
+ };
+
+ expect(deserializeShard(serializedShard as ShardFromEs)).toMatchSnapshot();
+ });
+ });
+
+ describe('deserializeFollowerIndex()', () => {
+ it('deserializes Elasticsearch follower index object', () => {
+ const serializedFollowerIndex = {
+ follower_index: 'follower index 1',
+ remote_cluster: 'cluster 1',
+ leader_index: 'leader 1',
+ status: 'active',
+ parameters: {
+ max_read_request_operation_count: 1,
+ max_outstanding_read_requests: 1,
+ max_read_request_size: '1b',
+ max_write_request_operation_count: 1,
+ max_write_request_size: '1b',
+ max_outstanding_write_requests: 1,
+ max_write_buffer_count: 1,
+ max_write_buffer_size: '1b',
+ max_retry_delay: '1s',
+ read_poll_timeout: '1s',
+ },
+ shards: [
+ {
+ remote_cluster: 'cluster 1',
+ leader_index: 'leader 1',
+ shard_id: 1,
+ leader_global_checkpoint: 1,
+ leader_max_seq_no: 1,
+ follower_global_checkpoint: 1,
+ follower_max_seq_no: 1,
+ last_requested_seq_no: 1,
+ outstanding_read_requests: 1,
+ outstanding_write_requests: 1,
+ write_buffer_operation_count: 1,
+ write_buffer_size_in_bytes: 1,
+ follower_mapping_version: 1,
+ follower_settings_version: 1,
+ total_read_time_millis: 1,
+ total_read_remote_exec_time_millis: 1,
+ successful_read_requests: 1,
+ failed_read_requests: 1,
+ operations_read: 1,
+ bytes_read: 1,
+ total_write_time_millis: 1,
+ successful_write_requests: 1,
+ failed_write_requests: 1,
+ operations_written: 1,
+ // This is an array of exception objects
+ read_exceptions: [],
+ time_since_last_read_millis: 1,
+ },
+ {
+ remote_cluster: 'remote_cluster 2',
+ leader_index: 'leader_index 2',
+ shard_id: 'shard 2',
+ },
+ ],
+ };
+
+ expect(
+ deserializeFollowerIndex(serializedFollowerIndex as FollowerIndexFromEs)
+ ).toMatchSnapshot();
+ });
+ });
+
+ describe('deserializeListFollowerIndices()', () => {
+ it('deserializes list of Elasticsearch follower index objects', () => {
+ const serializedFollowerIndexList = [
+ {
+ follower_index: 'follower index 1',
+ remote_cluster: 'cluster 1',
+ leader_index: 'leader 1',
+ status: 'active',
+ parameters: {
+ max_read_request_operation_count: 1,
+ max_outstanding_read_requests: 1,
+ max_read_request_size: '1b',
+ max_write_request_operation_count: 1,
+ max_write_request_size: '1b',
+ max_outstanding_write_requests: 1,
+ max_write_buffer_count: 1,
+ max_write_buffer_size: '1b',
+ max_retry_delay: '1s',
+ read_poll_timeout: '1s',
+ },
+ shards: [],
+ },
+ {
+ follower_index: 'follower index 2',
+ remote_cluster: 'cluster 2',
+ leader_index: 'leader 2',
+ status: 'paused',
+ parameters: {
+ max_read_request_operation_count: 2,
+ max_outstanding_read_requests: 2,
+ max_read_request_size: '2b',
+ max_write_request_operation_count: 2,
+ max_write_request_size: '2b',
+ max_outstanding_write_requests: 2,
+ max_write_buffer_count: 2,
+ max_write_buffer_size: '2b',
+ max_retry_delay: '2s',
+ read_poll_timeout: '2s',
+ },
+ shards: [],
+ },
+ ];
+
+ const deserializedFollowerIndexList = [
+ {
+ name: 'follower index 1',
+ remoteCluster: 'cluster 1',
+ leaderIndex: 'leader 1',
+ status: 'active',
+ maxReadRequestOperationCount: 1,
+ maxOutstandingReadRequests: 1,
+ maxReadRequestSize: '1b',
+ maxWriteRequestOperationCount: 1,
+ maxWriteRequestSize: '1b',
+ maxOutstandingWriteRequests: 1,
+ maxWriteBufferCount: 1,
+ maxWriteBufferSize: '1b',
+ maxRetryDelay: '1s',
+ readPollTimeout: '1s',
+ shards: [],
+ },
+ {
+ name: 'follower index 2',
+ remoteCluster: 'cluster 2',
+ leaderIndex: 'leader 2',
+ status: 'paused',
+ maxReadRequestOperationCount: 2,
+ maxOutstandingReadRequests: 2,
+ maxReadRequestSize: '2b',
+ maxWriteRequestOperationCount: 2,
+ maxWriteRequestSize: '2b',
+ maxOutstandingWriteRequests: 2,
+ maxWriteBufferCount: 2,
+ maxWriteBufferSize: '2b',
+ maxRetryDelay: '2s',
+ readPollTimeout: '2s',
+ shards: [],
+ },
+ ];
+
+ expect(deserializeListFollowerIndices(serializedFollowerIndexList)).toEqual(
+ deserializedFollowerIndexList
+ );
+ });
+ });
+
+ describe('serializeFollowerIndex()', () => {
+ it('serializes object to Elasticsearch follower index object', () => {
+ const deserializedFollowerIndex = {
+ name: 'test',
+ status: 'active',
+ shards: [],
+ remoteCluster: 'remote cluster',
+ leaderIndex: 'leader index',
+ maxReadRequestOperationCount: 1,
+ maxOutstandingReadRequests: 1,
+ maxReadRequestSize: '1b',
+ maxWriteRequestOperationCount: 1,
+ maxWriteRequestSize: '1b',
+ maxOutstandingWriteRequests: 1,
+ maxWriteBufferCount: 1,
+ maxWriteBufferSize: '1b',
+ maxRetryDelay: '1s',
+ readPollTimeout: '1s',
+ };
+
+ expect(serializeFollowerIndex(deserializedFollowerIndex as FollowerIndex)).toMatchSnapshot();
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.ts
similarity index 87%
rename from x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js
rename to x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.ts
index c41fde8f7818d..df476a0b2db89 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js
+++ b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.ts
@@ -4,7 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
-/* eslint-disable camelcase */
+import {
+ ShardFromEs,
+ Shard,
+ FollowerIndexFromEs,
+ FollowerIndex,
+ FollowerIndexToEs,
+ FollowerIndexAdvancedSettings,
+ FollowerIndexAdvancedSettingsToEs,
+} from '../types';
+
export const deserializeShard = ({
remote_cluster,
leader_index,
@@ -32,7 +41,7 @@ export const deserializeShard = ({
operations_written,
read_exceptions,
time_since_last_read_millis,
-}) => ({
+}: ShardFromEs): Shard => ({
id: shard_id,
remoteCluster: remote_cluster,
leaderIndex: leader_index,
@@ -61,9 +70,7 @@ export const deserializeShard = ({
readExceptions: read_exceptions,
timeSinceLastReadMs: time_since_last_read_millis,
});
-/* eslint-enable camelcase */
-/* eslint-disable camelcase */
export const deserializeFollowerIndex = ({
follower_index,
remote_cluster,
@@ -82,7 +89,7 @@ export const deserializeFollowerIndex = ({
read_poll_timeout,
} = {},
shards,
-}) => ({
+}: FollowerIndexFromEs): FollowerIndex => ({
name: follower_index,
remoteCluster: remote_cluster,
leaderIndex: leader_index,
@@ -99,10 +106,10 @@ export const deserializeFollowerIndex = ({
readPollTimeout: read_poll_timeout,
shards: shards && shards.map(deserializeShard),
});
-/* eslint-enable camelcase */
-export const deserializeListFollowerIndices = followerIndices =>
- followerIndices.map(deserializeFollowerIndex);
+export const deserializeListFollowerIndices = (
+ followerIndices: FollowerIndexFromEs[]
+): FollowerIndex[] => followerIndices.map(deserializeFollowerIndex);
export const serializeAdvancedSettings = ({
maxReadRequestOperationCount,
@@ -115,7 +122,7 @@ export const serializeAdvancedSettings = ({
maxWriteBufferSize,
maxRetryDelay,
readPollTimeout,
-}) => ({
+}: FollowerIndexAdvancedSettings): FollowerIndexAdvancedSettingsToEs => ({
max_read_request_operation_count: maxReadRequestOperationCount,
max_outstanding_read_requests: maxOutstandingReadRequests,
max_read_request_size: maxReadRequestSize,
@@ -128,7 +135,7 @@ export const serializeAdvancedSettings = ({
read_poll_timeout: readPollTimeout,
});
-export const serializeFollowerIndex = followerIndex => ({
+export const serializeFollowerIndex = (followerIndex: FollowerIndex): FollowerIndexToEs => ({
remote_cluster: followerIndex.remoteCluster,
leader_index: followerIndex.leaderIndex,
...serializeAdvancedSettings(followerIndex),
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.test.js b/x-pack/plugins/cross_cluster_replication/common/services/utils.test.ts
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.test.js
rename to x-pack/plugins/cross_cluster_replication/common/services/utils.test.ts
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.js b/x-pack/plugins/cross_cluster_replication/common/services/utils.ts
similarity index 62%
rename from x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.js
rename to x-pack/plugins/cross_cluster_replication/common/services/utils.ts
index 3d8c97f45327c..dda6732254cc3 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.js
+++ b/x-pack/plugins/cross_cluster_replication/common/services/utils.ts
@@ -3,14 +3,14 @@
* 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 arrify = val => (Array.isArray(val) ? val : [val]);
+export const arrify = (val: any): any[] => (Array.isArray(val) ? val : [val]);
/**
* Utilty to add some latency in a Promise chain
*
* @param {number} time Time in millisecond to wait
*/
-export const wait = (time = 1000) => data => {
+export const wait = (time = 1000) => (data: any): Promise => {
return new Promise(resolve => {
setTimeout(() => resolve(data), time);
});
@@ -19,8 +19,11 @@ export const wait = (time = 1000) => data => {
/**
* Utility to remove empty fields ("") from a request body
*/
-export const removeEmptyFields = body =>
- Object.entries(body).reduce((acc, [key, value]) => {
+export const removeEmptyFields = (body: Record): Record =>
+ Object.entries(body).reduce((acc: Record, [key, value]: [string, any]): Record<
+ string,
+ any
+ > => {
if (value !== '') {
acc[key] = value;
}
diff --git a/x-pack/plugins/cross_cluster_replication/common/types.ts b/x-pack/plugins/cross_cluster_replication/common/types.ts
new file mode 100644
index 0000000000000..4932d6c570297
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/common/types.ts
@@ -0,0 +1,186 @@
+/*
+ * 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 interface AutoFollowPattern {
+ name: string;
+ active: boolean;
+ remoteCluster: string;
+ leaderIndexPatterns: string[];
+ followIndexPattern: string;
+}
+
+export interface AutoFollowPatternFromEs {
+ name: string;
+ pattern: {
+ active: boolean;
+ remote_cluster: string;
+ leader_index_patterns: string[];
+ follow_index_pattern: string;
+ };
+}
+
+export interface AutoFollowPatternToEs {
+ remote_cluster: string;
+ leader_index_patterns: string[];
+ follow_index_pattern: string;
+}
+
+export interface ShardFromEs {
+ remote_cluster: string;
+ leader_index: string;
+ shard_id: number;
+ leader_global_checkpoint: number;
+ leader_max_seq_no: number;
+ follower_global_checkpoint: number;
+ follower_max_seq_no: number;
+ last_requested_seq_no: number;
+ outstanding_read_requests: number;
+ outstanding_write_requests: number;
+ write_buffer_operation_count: number;
+ write_buffer_size_in_bytes: number;
+ follower_mapping_version: number;
+ follower_settings_version: number;
+ total_read_time_millis: number;
+ total_read_remote_exec_time_millis: number;
+ successful_read_requests: number;
+ failed_read_requests: number;
+ operations_read: number;
+ bytes_read: number;
+ total_write_time_millis: number;
+ successful_write_requests: number;
+ failed_write_requests: number;
+ operations_written: number;
+ // This is an array of exception objects
+ read_exceptions: any[];
+ time_since_last_read_millis: number;
+}
+
+export interface Shard {
+ remoteCluster: string;
+ leaderIndex: string;
+ id: number;
+ leaderGlobalCheckpoint: number;
+ leaderMaxSequenceNum: number;
+ followerGlobalCheckpoint: number;
+ followerMaxSequenceNum: number;
+ lastRequestedSequenceNum: number;
+ outstandingReadRequestsCount: number;
+ outstandingWriteRequestsCount: number;
+ writeBufferOperationsCount: number;
+ writeBufferSizeBytes: number;
+ followerMappingVersion: number;
+ followerSettingsVersion: number;
+ totalReadTimeMs: number;
+ totalReadRemoteExecTimeMs: number;
+ successfulReadRequestCount: number;
+ failedReadRequestsCount: number;
+ operationsReadCount: number;
+ bytesReadCount: number;
+ totalWriteTimeMs: number;
+ successfulWriteRequestsCount: number;
+ failedWriteRequestsCount: number;
+ operationsWrittenCount: number;
+ // This is an array of exception objects
+ readExceptions: any[];
+ timeSinceLastReadMs: number;
+}
+
+export interface FollowerIndexFromEs {
+ follower_index: string;
+ remote_cluster: string;
+ leader_index: string;
+ status: string;
+ // Once https://github.com/elastic/elasticsearch/issues/54996 is resolved so that paused follower
+ // indices contain this information, we can removed this optional typing as well as the optional
+ // typing in FollowerIndexAdvancedSettings and FollowerIndexAdvancedSettingsToEs.
+ parameters?: FollowerIndexAdvancedSettingsToEs;
+ shards: ShardFromEs[];
+}
+
+export interface FollowerIndex extends FollowerIndexAdvancedSettings {
+ name: string;
+ remoteCluster: string;
+ leaderIndex: string;
+ status: string;
+ shards: Shard[];
+}
+
+export interface FollowerIndexToEs extends FollowerIndexAdvancedSettingsToEs {
+ remote_cluster: string;
+ leader_index: string;
+}
+
+export interface FollowerIndexAdvancedSettings {
+ maxReadRequestOperationCount?: number;
+ maxOutstandingReadRequests?: number;
+ maxReadRequestSize?: string; // byte value
+ maxWriteRequestOperationCount?: number;
+ maxWriteRequestSize?: string; // byte value
+ maxOutstandingWriteRequests?: number;
+ maxWriteBufferCount?: number;
+ maxWriteBufferSize?: string; // byte value
+ maxRetryDelay?: string; // time value
+ readPollTimeout?: string; // time value
+}
+
+export interface FollowerIndexAdvancedSettingsToEs {
+ max_read_request_operation_count?: number;
+ max_outstanding_read_requests?: number;
+ max_read_request_size?: string; // byte value
+ max_write_request_operation_count?: number;
+ max_write_request_size?: string; // byte value
+ max_outstanding_write_requests?: number;
+ max_write_buffer_count?: number;
+ max_write_buffer_size?: string; // byte value
+ max_retry_delay?: string; // time value
+ read_poll_timeout?: string; // time value
+}
+
+export interface RecentAutoFollowError {
+ timestamp: number;
+ leaderIndex: string;
+ autoFollowException: {
+ type: string;
+ reason: string;
+ };
+}
+
+export interface RecentAutoFollowErrorFromEs {
+ timestamp: number;
+ leader_index: string;
+ auto_follow_exception: {
+ type: string;
+ reason: string;
+ };
+}
+
+export interface AutoFollowedCluster {
+ clusterName: string;
+ timeSinceLastCheckMillis: number;
+ lastSeenMetadataVersion: number;
+}
+
+export interface AutoFollowedClusterFromEs {
+ cluster_name: string;
+ time_since_last_check_millis: number;
+ last_seen_metadata_version: number;
+}
+
+export interface AutoFollowStats {
+ numberOfFailedFollowIndices: number;
+ numberOfFailedRemoteClusterStateRequests: number;
+ numberOfSuccessfulFollowIndices: number;
+ recentAutoFollowErrors: RecentAutoFollowError[];
+ autoFollowedClusters: AutoFollowedCluster[];
+}
+
+export interface AutoFollowStatsFromEs {
+ number_of_failed_follow_indices: number;
+ number_of_failed_remote_cluster_state_requests: number;
+ number_of_successful_follow_indices: number;
+ recent_auto_follow_errors: RecentAutoFollowErrorFromEs[];
+ auto_followed_clusters: AutoFollowedClusterFromEs[];
+}
diff --git a/x-pack/plugins/cross_cluster_replication/kibana.json b/x-pack/plugins/cross_cluster_replication/kibana.json
new file mode 100644
index 0000000000000..ccf98f41def47
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/kibana.json
@@ -0,0 +1,17 @@
+{
+ "id": "crossClusterReplication",
+ "version": "kibana",
+ "server": true,
+ "ui": true,
+ "requiredPlugins": [
+ "home",
+ "licensing",
+ "management",
+ "remoteClusters",
+ "indexManagement"
+ ],
+ "optionalPlugins": [
+ "usageCollection"
+ ],
+ "configPath": ["xpack", "ccr"]
+}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js
similarity index 98%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js
index 2be00e70f6f84..db1430d157183 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js
@@ -3,11 +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 '../../public/np_ready/app/services/breadcrumbs.mock';
-import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers';
-import { indexPatterns } from '../../../../../../src/plugins/data/public';
-jest.mock('ui/new_platform');
+import { indexPatterns } from '../../../../../../src/plugins/data/public';
+import './mocks';
+import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers';
const { setup } = pageHelpers.autoFollowPatternAdd;
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js
similarity index 95%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js
index abc3e5dc9def2..170bce7b82085 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js
@@ -4,13 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import '../../public/np_ready/app/services/breadcrumbs.mock';
-import { AutoFollowPatternForm } from '../../public/np_ready/app/components/auto_follow_pattern_form';
+import { AutoFollowPatternForm } from '../../app/components/auto_follow_pattern_form';
+import './mocks';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { AUTO_FOLLOW_PATTERN_EDIT } from './helpers/constants';
-jest.mock('ui/new_platform');
-
const { setup } = pageHelpers.autoFollowPatternEdit;
const { setup: setupAutoFollowPatternAdd } = pageHelpers.autoFollowPatternAdd;
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js
similarity index 97%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js
index 20e982856dc19..190400e988634 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js
@@ -4,13 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import '../../public/np_ready/app/services/breadcrumbs.mock';
+import { getAutoFollowPatternMock } from './fixtures/auto_follow_pattern';
+import './mocks';
import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers';
-import { getAutoFollowPatternClientMock } from '../../fixtures/auto_follow_pattern';
-
-jest.mock('ui/new_platform');
-
const { setup } = pageHelpers.autoFollowPatternList;
describe('', () => {
@@ -79,11 +76,11 @@ describe('', () => {
const testPrefix = 'prefix_';
const testSuffix = '_suffix';
- const autoFollowPattern1 = getAutoFollowPatternClientMock({
+ const autoFollowPattern1 = getAutoFollowPatternMock({
name: `a${getRandomString()}`,
followIndexPattern: `${testPrefix}{{leader_index}}${testSuffix}`,
});
- const autoFollowPattern2 = getAutoFollowPatternClientMock({
+ const autoFollowPattern2 = getAutoFollowPatternMock({
name: `b${getRandomString()}`,
followIndexPattern: '{{leader_index}}', // no prefix nor suffix
});
@@ -305,10 +302,12 @@ describe('', () => {
const message = 'bar';
const recentAutoFollowErrors = [
{
+ timestamp: 1587081600021,
leaderIndex: `${autoFollowPattern1.name}:my-leader-test`,
autoFollowException: { type: 'exception', reason: message },
},
{
+ timestamp: 1587081600021,
leaderIndex: `${autoFollowPattern2.name}:my-leader-test`,
autoFollowException: { type: 'exception', reason: message },
},
@@ -327,7 +326,7 @@ describe('', () => {
expect(exists('autoFollowPatternDetail.errors')).toBe(true);
expect(exists('autoFollowPatternDetail.titleErrors')).toBe(true);
expect(find('autoFollowPatternDetail.recentError').map(error => error.text())).toEqual([
- message,
+ 'April 16th, 2020 8:00:00 PM: bar',
]);
});
});
diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts
new file mode 100644
index 0000000000000..e6444c37e8590
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { getRandomString } from '../../../../../../test_utils';
+import { AutoFollowPattern } from '../../../../common/types';
+
+export const getAutoFollowPatternMock = ({
+ name = getRandomString(),
+ active = false,
+ remoteCluster = getRandomString(),
+ leaderIndexPatterns = [`${getRandomString()}-*`],
+ followIndexPattern = getRandomString(),
+}: {
+ name: string;
+ active: boolean;
+ remoteCluster: string;
+ leaderIndexPatterns: string[];
+ followIndexPattern: string;
+}): AutoFollowPattern => ({
+ name,
+ active,
+ remoteCluster,
+ leaderIndexPatterns,
+ followIndexPattern,
+});
diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts
new file mode 100644
index 0000000000000..ff051d470531b
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { getRandomString } from '../../../../../../test_utils';
+import { FollowerIndex } from '../../../../common/types';
+
+const Chance = require('chance'); // eslint-disable-line import/no-extraneous-dependencies, @typescript-eslint/no-var-requires
+const chance = new Chance();
+
+interface FollowerIndexMock {
+ name: string;
+ remoteCluster: string;
+ leaderIndex: string;
+ status: string;
+}
+
+export const getFollowerIndexMock = ({
+ name = getRandomString(),
+ remoteCluster = getRandomString(),
+ leaderIndex = getRandomString(),
+ status = 'Active',
+}: FollowerIndexMock): FollowerIndex => ({
+ name,
+ remoteCluster,
+ leaderIndex,
+ status,
+ maxReadRequestOperationCount: chance.integer(),
+ maxOutstandingReadRequests: chance.integer(),
+ maxReadRequestSize: getRandomString({ length: 5 }),
+ maxWriteRequestOperationCount: chance.integer(),
+ maxWriteRequestSize: '9223372036854775807b',
+ maxOutstandingWriteRequests: chance.integer(),
+ maxWriteBufferCount: chance.integer(),
+ maxWriteBufferSize: getRandomString({ length: 5 }),
+ maxRetryDelay: getRandomString({ length: 5 }),
+ readPollTimeout: getRandomString({ length: 5 }),
+ shards: [
+ {
+ id: 0,
+ remoteCluster,
+ leaderIndex,
+ leaderGlobalCheckpoint: chance.integer(),
+ leaderMaxSequenceNum: chance.integer(),
+ followerGlobalCheckpoint: chance.integer(),
+ followerMaxSequenceNum: chance.integer(),
+ lastRequestedSequenceNum: chance.integer(),
+ outstandingReadRequestsCount: chance.integer(),
+ outstandingWriteRequestsCount: chance.integer(),
+ writeBufferOperationsCount: chance.integer(),
+ writeBufferSizeBytes: chance.integer(),
+ followerMappingVersion: chance.integer(),
+ followerSettingsVersion: chance.integer(),
+ totalReadTimeMs: chance.integer(),
+ totalReadRemoteExecTimeMs: chance.integer(),
+ successfulReadRequestCount: chance.integer(),
+ failedReadRequestsCount: chance.integer(),
+ operationsReadCount: chance.integer(),
+ bytesReadCount: chance.integer(),
+ totalWriteTimeMs: chance.integer(),
+ successfulWriteRequestsCount: chance.integer(),
+ failedWriteRequestsCount: chance.integer(),
+ operationsWrittenCount: chance.integer(),
+ readExceptions: [],
+ timeSinceLastReadMs: chance.integer(),
+ },
+ ],
+});
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js
similarity index 98%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js
index 7680be9d858a4..4c99339e16952 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js
@@ -4,13 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import '../../public/np_ready/app/services/breadcrumbs.mock';
-import { setupEnvironment, pageHelpers, nextTick } from './helpers';
-import { RemoteClustersFormField } from '../../public/np_ready/app/components';
-
import { indexPatterns } from '../../../../../../src/plugins/data/public';
-
-jest.mock('ui/new_platform');
+import './mocks';
+import { setupEnvironment, pageHelpers, nextTick } from './helpers';
+import { RemoteClustersFormField } from '../../app/components';
const { setup } = pageHelpers.followerIndexAdd;
const { setup: setupAutoFollowPatternAdd } = pageHelpers.autoFollowPatternAdd;
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js
similarity index 95%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js
index cfa37ff2e0358..f4bda2af653aa 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js
@@ -4,12 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import '../../public/np_ready/app/services/breadcrumbs.mock';
-import { setupEnvironment, pageHelpers, nextTick } from './helpers';
-import { FollowerIndexForm } from '../../public/np_ready/app/components/follower_index_form/follower_index_form';
+import { FollowerIndexForm } from '../../app/components/follower_index_form/follower_index_form';
+import './mocks';
import { FOLLOWER_INDEX_EDIT } from './helpers/constants';
-
-jest.mock('ui/new_platform');
+import { setupEnvironment, pageHelpers, nextTick } from './helpers';
const { setup } = pageHelpers.followerIndexEdit;
const { setup: setupFollowerIndexAdd } = pageHelpers.followerIndexAdd;
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js
similarity index 99%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js
index dde31d1d166f9..f98a1dafbbcbf 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js
@@ -4,12 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { getFollowerIndexMock } from './fixtures/follower_index';
+import './mocks';
import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers';
-import { getFollowerIndexMock } from '../../fixtures/follower_index';
-
-jest.mock('ui/new_platform');
-
const { setup } = pageHelpers.followerIndexList;
describe('', () => {
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js
similarity index 76%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js
index 1f64e589bc4c1..1cb4e7c7725df 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js
@@ -5,9 +5,9 @@
*/
import { registerTestBed } from '../../../../../../test_utils';
-import { AutoFollowPatternAdd } from '../../../public/np_ready/app/sections/auto_follow_pattern_add';
-import { ccrStore } from '../../../public/np_ready/app/store';
-import routing from '../../../public/np_ready/app/services/routing';
+import { AutoFollowPatternAdd } from '../../../app/sections/auto_follow_pattern_add';
+import { ccrStore } from '../../../app/store';
+import { routing } from '../../../app/services/routing';
const testBedConfig = {
store: ccrStore,
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js
similarity index 82%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js
index 2b110c6552072..9cad61893c409 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js
@@ -5,9 +5,9 @@
*/
import { registerTestBed } from '../../../../../../test_utils';
-import { AutoFollowPatternEdit } from '../../../public/np_ready/app/sections/auto_follow_pattern_edit';
-import { ccrStore } from '../../../public/np_ready/app/store';
-import routing from '../../../public/np_ready/app/services/routing';
+import { AutoFollowPatternEdit } from '../../../app/sections/auto_follow_pattern_edit';
+import { ccrStore } from '../../../app/store';
+import { routing } from '../../../app/services/routing';
import { AUTO_FOLLOW_PATTERN_EDIT_NAME } from './constants';
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js
similarity index 92%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js
index 1d3e8ad6dff83..450feed49f9f2 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js
@@ -5,9 +5,9 @@
*/
import { registerTestBed, findTestSubject } from '../../../../../../test_utils';
-import { AutoFollowPatternList } from '../../../public/np_ready/app/sections/home/auto_follow_pattern_list';
-import { ccrStore } from '../../../public/np_ready/app/store';
-import routing from '../../../public/np_ready/app/services/routing';
+import { AutoFollowPatternList } from '../../../app/sections/home/auto_follow_pattern_list';
+import { ccrStore } from '../../../app/store';
+import { routing } from '../../../app/services/routing';
const testBedConfig = {
store: ccrStore,
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/constants.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/constants.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js
similarity index 79%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js
index f74baa1b2ad0a..856b09f3f3cba 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js
@@ -5,9 +5,9 @@
*/
import { registerTestBed } from '../../../../../../test_utils';
-import { FollowerIndexAdd } from '../../../public/np_ready/app/sections/follower_index_add';
-import { ccrStore } from '../../../public/np_ready/app/store';
-import routing from '../../../public/np_ready/app/services/routing';
+import { FollowerIndexAdd } from '../../../app/sections/follower_index_add';
+import { ccrStore } from '../../../app/store';
+import { routing } from '../../../app/services/routing';
const testBedConfig = {
store: ccrStore,
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js
similarity index 84%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js
index 47f8539bb593b..893d01f151bc2 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js
@@ -5,9 +5,9 @@
*/
import { registerTestBed } from '../../../../../../test_utils';
-import { FollowerIndexEdit } from '../../../public/np_ready/app/sections/follower_index_edit';
-import { ccrStore } from '../../../public/np_ready/app/store';
-import routing from '../../../public/np_ready/app/services/routing';
+import { FollowerIndexEdit } from '../../../app/sections/follower_index_edit';
+import { ccrStore } from '../../../app/store';
+import { routing } from '../../../app/services/routing';
import { FOLLOWER_INDEX_EDIT_NAME } from './constants';
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js
similarity index 90%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js
index 2154e11e17b1f..52f4267594cc1 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js
@@ -5,9 +5,9 @@
*/
import { registerTestBed, findTestSubject } from '../../../../../../test_utils';
-import { FollowerIndicesList } from '../../../public/np_ready/app/sections/home/follower_indices_list';
-import { ccrStore } from '../../../public/np_ready/app/store';
-import routing from '../../../public/np_ready/app/services/routing';
+import { FollowerIndicesList } from '../../../app/sections/home/follower_indices_list';
+import { ccrStore } from '../../../app/store';
+import { routing } from '../../../app/services/routing';
const testBedConfig = {
store: ccrStore,
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js
similarity index 68%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js
index 664ad909ba8e7..56dfa765bfa4f 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js
@@ -5,10 +5,10 @@
*/
import { registerTestBed } from '../../../../../../test_utils';
-import { CrossClusterReplicationHome } from '../../../public/np_ready/app/sections/home/home';
-import { ccrStore } from '../../../public/np_ready/app/store';
-import routing from '../../../public/np_ready/app/services/routing';
-import { BASE_PATH } from '../../../common/constants';
+import { BASE_PATH } from '../../../../common/constants';
+import { CrossClusterReplicationHome } from '../../../app/sections/home/home';
+import { ccrStore } from '../../../app/store';
+import { routing } from '../../../app/services/routing';
const testBedConfig = {
store: ccrStore,
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/http_requests.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/http_requests.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/index.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/index.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js
similarity index 91%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js
index 3562ad0df5b51..6dedbbfa79b19 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js
@@ -7,7 +7,7 @@
import axios from 'axios';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
-import { setHttpClient } from '../../../public/np_ready/app/services/api';
+import { setHttpClient } from '../../../app/services/api';
import { init as initHttpRequests } from './http_requests';
export const setupEnvironment = () => {
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/home.test.js
similarity index 93%
rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/home.test.js
index 2c536d069ef53..18d8b4eb9dbe0 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/home.test.js
@@ -4,11 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import '../../public/np_ready/app/services/breadcrumbs.mock';
+import './mocks';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
-jest.mock('ui/new_platform');
-
const { setup } = pageHelpers.home;
describe('', () => {
@@ -36,7 +34,7 @@ describe('', () => {
({ exists, find, component } = setup());
});
- test('should set the correct an app title', () => {
+ test('should set the correct app title', () => {
expect(exists('appTitle')).toBe(true);
expect(find('appTitle').text()).toEqual('Cross-Cluster Replication');
});
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/breadcrumbs.mock.ts
similarity index 70%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/breadcrumbs.mock.ts
index b7c75108d4ef0..60a196254d408 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/breadcrumbs.mock.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-jest.mock('./breadcrumbs', () => ({
- ...jest.requireActual('./breadcrumbs'),
+jest.mock('../../../app/services/breadcrumbs', () => ({
+ ...jest.requireActual('../../../app/services/breadcrumbs'),
setBreadcrumbs: jest.fn(),
}));
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts
similarity index 79%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts
rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts
index 441648a8701e0..cff9c003f3e80 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts
@@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { isEsErrorFactory } from './is_es_error_factory';
+import './breadcrumbs.mock';
+import './track_ui_metric.mock';
diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts
new file mode 100644
index 0000000000000..016e259343285
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts
@@ -0,0 +1,13 @@
+/*
+ * 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.
+ */
+
+jest.mock('../../../app/services/track_ui_metric', () => ({
+ ...jest.requireActual('../../../app/services/track_ui_metric'),
+ trackUiMetric: jest.fn(),
+ trackUserRequest: (request: Promise) => {
+ return request.then(response => response);
+ },
+}));
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx
similarity index 89%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js
rename to x-pack/plugins/cross_cluster_replication/public/app/app.tsx
index 968646a4bd1b0..ec349ccd6f2c7 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx
@@ -5,8 +5,8 @@
*/
import React, { Component, Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { Route, Switch, Redirect, withRouter } from 'react-router-dom';
+import { Route, Switch, Redirect, withRouter, RouteComponentProps } from 'react-router-dom';
+import { History } from 'history';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -20,12 +20,14 @@ import {
EuiTitle,
} from '@elastic/eui';
-import { BASE_PATH } from '../../../common/constants';
+import { BASE_PATH } from '../../common/constants';
import { getFatalErrors } from './services/notifications';
import { SectionError } from './components';
-import routing from './services/routing';
+import { routing } from './services/routing';
+// @ts-ignore
import { loadPermissions } from './services/api';
+// @ts-ignore
import {
CrossClusterReplicationHome,
AutoFollowPatternAdd,
@@ -34,16 +36,21 @@ import {
FollowerIndexEdit,
} from './sections';
-class AppComponent extends Component {
- static propTypes = {
- history: PropTypes.shape({
- push: PropTypes.func.isRequired,
- createHref: PropTypes.func.isRequired,
- }).isRequired,
- };
+interface AppProps {
+ history: History;
+ location: any;
+}
+
+interface AppState {
+ isFetchingPermissions: boolean;
+ fetchPermissionError: any;
+ hasPermission: boolean;
+ missingClusterPrivileges: any[];
+}
- constructor(...args) {
- super(...args);
+class AppComponent extends Component {
+ constructor(props: any) {
+ super(props);
this.registerRouter();
this.state = {
@@ -54,18 +61,10 @@ class AppComponent extends Component {
};
}
- UNSAFE_componentWillMount() {
- routing.userHasLeftApp = false;
- }
-
componentDidMount() {
this.checkPermissions();
}
- componentWillUnmount() {
- routing.userHasLeftApp = true;
- }
-
async checkPermissions() {
this.setState({
isFetchingPermissions: true,
@@ -163,7 +162,6 @@ class AppComponent extends Component {
-
+
(
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_provider.js
rename to x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_error.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_error.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_error.js
rename to x-pack/plugins/cross_cluster_replication/public/app/components/section_error.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_loading.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_loading.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_loading.js
rename to x-pack/plugins/cross_cluster_replication/public/app/components/section_loading.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_unauthorized.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_unauthorized.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_unauthorized.js
rename to x-pack/plugins/cross_cluster_replication/public/app/components/section_unauthorized.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/api.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/api.ts
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/api.js
rename to x-pack/plugins/cross_cluster_replication/public/app/constants/api.ts
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/index.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/index.ts
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/constants/index.ts
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/sections.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/sections.ts
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/sections.js
rename to x-pack/plugins/cross_cluster_replication/public/app/constants/sections.ts
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/ui_metric.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/ui_metric.ts
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/ui_metric.js
rename to x-pack/plugins/cross_cluster_replication/public/app/constants/ui_metric.ts
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/index.tsx b/x-pack/plugins/cross_cluster_replication/public/app/index.tsx
new file mode 100644
index 0000000000000..79569b587f97f
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/app/index.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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 React from 'react';
+import { render, unmountComponentAtNode } from 'react-dom';
+import { Provider } from 'react-redux';
+import { HashRouter } from 'react-router-dom';
+import { I18nStart } from 'kibana/public';
+import { UnmountCallback } from 'src/core/public';
+
+import { init as initBreadcrumbs, SetBreadcrumbs } from './services/breadcrumbs';
+import { init as initDocumentation } from './services/documentation_links';
+import { App } from './app';
+import { ccrStore } from './store';
+
+const renderApp = (element: Element, I18nContext: I18nStart['Context']): UnmountCallback => {
+ render(
+
+
+
+
+
+
+ ,
+ element
+ );
+
+ return () => unmountComponentAtNode(element);
+};
+
+export async function mountApp({
+ element,
+ setBreadcrumbs,
+ I18nContext,
+ ELASTIC_WEBSITE_URL,
+ DOC_LINK_VERSION,
+}: {
+ element: Element;
+ setBreadcrumbs: SetBreadcrumbs;
+ I18nContext: I18nStart['Context'];
+ ELASTIC_WEBSITE_URL: string;
+ DOC_LINK_VERSION: string;
+}): Promise {
+ // Import and initialize additional services here instead of in plugin.ts to reduce the size of the
+ // initial bundle as much as possible.
+ initBreadcrumbs(setBreadcrumbs);
+ initDocumentation(`${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`);
+
+ return renderApp(element, I18nContext);
+}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js
similarity index 80%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js
index 2c90456076f85..be470edc07537 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js
@@ -39,8 +39,23 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
getAutoFollowPattern: id => dispatch(getAutoFollowPattern(id)),
selectAutoFollowPattern: id => dispatch(selectEditAutoFollowPattern(id)),
- saveAutoFollowPattern: (id, autoFollowPattern) =>
- dispatch(saveAutoFollowPattern(id, autoFollowPattern, true)),
+ saveAutoFollowPattern: (id, autoFollowPattern) => {
+ // Strip out errors.
+ const { active, remoteCluster, leaderIndexPatterns, followIndexPattern } = autoFollowPattern;
+
+ dispatch(
+ saveAutoFollowPattern(
+ id,
+ {
+ active,
+ remoteCluster,
+ leaderIndexPatterns,
+ followIndexPattern,
+ },
+ true
+ )
+ );
+ },
clearApiError: () => {
dispatch(clearApiError(`${scope}-get`));
dispatch(clearApiError(`${scope}-save`));
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js
similarity index 99%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js
index 4cd3617abd989..387d7817a0357 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js
@@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPageContent, EuiSpacer } from '@elastic/eui';
import { listBreadcrumb, editBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs';
-import routing from '../../services/routing';
+import { routing } from '../../services/routing';
import {
AutoFollowPatternForm,
AutoFollowPatternPageTitle,
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.container.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.container.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.container.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.container.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js
similarity index 99%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js
index 21493602c12a7..22f9a7338384b 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js
@@ -20,7 +20,7 @@ import {
} from '@elastic/eui';
import { setBreadcrumbs, listBreadcrumb, editBreadcrumb } from '../../services/breadcrumbs';
-import routing from '../../services/routing';
+import { routing } from '../../services/routing';
import {
FollowerIndexForm,
FollowerIndexPageTitle,
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
similarity index 99%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
index e9e14f57e814f..c8cf94842aa68 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
@@ -17,7 +17,7 @@ import {
EuiSpacer,
} from '@elastic/eui';
-import routing from '../../../services/routing';
+import { routing } from '../../../services/routing';
import { extractQueryParams } from '../../../services/query_params';
import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric';
import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD } from '../../../constants';
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js
similarity index 97%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js
index 956a9f10d810b..eb90e59e99fee 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js
@@ -20,8 +20,8 @@ import {
AutoFollowPatternDeleteProvider,
AutoFollowPatternActionMenu,
} from '../../../../../components';
-import routing from '../../../../../services/routing';
-import { trackUiMetric, METRIC_TYPE } from '../../../../../services/track_ui_metric';
+import { routing } from '../../../../../services/routing';
+import { trackUiMetric } from '../../../../../services/track_ui_metric';
export class AutoFollowPatternTable extends PureComponent {
static propTypes = {
@@ -86,7 +86,7 @@ export class AutoFollowPatternTable extends PureComponent {
return (
{
- trackUiMetric(METRIC_TYPE.CLICK, UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK);
+ trackUiMetric('click', UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK);
selectAutoFollowPattern(name);
}}
data-test-subj="autoFollowPatternLink"
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js
similarity index 98%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js
index 1a6d5e6efe35a..3f2ed82420ff1 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js
@@ -7,7 +7,9 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
-import { getIndexListUri } from '../../../../../../../../../../../plugins/index_management/public';
+import moment from 'moment';
+
+import { getIndexListUri } from '../../../../../../../../../plugins/index_management/public';
import {
EuiButtonEmpty,
@@ -247,6 +249,7 @@ export class DetailPanel extends Component {
{autoFollowPattern.errors.map((error, i) => (
-
+ {moment(error.timestamp).format('MMMM Do, YYYY h:mm:ss A')}:{' '}
{error.autoFollowException.reason}
))}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js
similarity index 98%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js
index 0f6ef75522ff7..4a66f7b717bac 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js
@@ -15,7 +15,7 @@ import {
EuiPopoverTitle,
} from '@elastic/eui';
-import routing from '../../../../../services/routing';
+import { routing } from '../../../../../services/routing';
import {
FollowerIndexPauseProvider,
FollowerIndexResumeProvider,
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js
similarity index 99%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js
index 3e8cf6d3e2f78..4436d76643e6c 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js
@@ -31,7 +31,7 @@ import {
} from '@elastic/eui';
import 'brace/theme/textmate';
-import { getIndexListUri } from '../../../../../../../../../../../plugins/index_management/public';
+import { getIndexListUri } from '../../../../../../../../../plugins/index_management/public';
import { API_STATUS } from '../../../../../constants';
import { ContextMenu } from '../context_menu';
@@ -489,7 +489,6 @@ export class DetailPanel extends Component {
return (
{
- trackUiMetric(METRIC_TYPE.CLICK, UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK);
+ trackUiMetric('click', UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK);
selectFollowerIndex(name);
}}
data-test-subj="followerIndexLink"
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.container.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js
similarity index 99%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js
index b7e04721f4748..7b843d08cefd3 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js
@@ -17,7 +17,7 @@ import {
EuiSpacer,
} from '@elastic/eui';
-import routing from '../../../services/routing';
+import { routing } from '../../../services/routing';
import { extractQueryParams } from '../../../services/query_params';
import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric';
import { API_STATUS, UIM_FOLLOWER_INDEX_LIST_LOAD } from '../../../constants';
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.container.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.container.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.container.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js
similarity index 96%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js
index 88db909612245..bcd9dad114862 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js
@@ -10,9 +10,9 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui';
-import { BASE_PATH } from '../../../../../common/constants';
+import { BASE_PATH } from '../../../../common/constants';
import { setBreadcrumbs, listBreadcrumb } from '../../services/breadcrumbs';
-import routing from '../../services/routing';
+import { routing } from '../../services/routing';
import { AutoFollowPatternList } from './auto_follow_pattern_list';
import { FollowerIndicesList } from './follower_indices_list';
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts b/x-pack/plugins/cross_cluster_replication/public/app/sections/index.d.ts
similarity index 50%
rename from x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/index.d.ts
index 4ce0a2f5644f3..b7c1f495604be 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/index.d.ts
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const APPS = {
- CCR_APP: 'ccr',
- REMOTE_CLUSTER_APP: 'remote_cluster',
-};
+export declare const CrossClusterReplicationHome: any;
+export declare const AutoFollowPatternAdd: any;
+export declare const AutoFollowPatternEdit: any;
+export declare const FollowerIndexAdd: any;
+export declare const FollowerIndexEdit: any;
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/sections/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap b/x-pack/plugins/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap
rename to x-pack/plugins/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js b/x-pack/plugins/cross_cluster_replication/public/app/services/api.js
similarity index 98%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/api.js
index 24bc7e17356e2..adff40ef29be6 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/api.js
@@ -7,8 +7,8 @@ import {
API_BASE_PATH,
API_REMOTE_CLUSTERS_BASE_PATH,
API_INDEX_MANAGEMENT_BASE_PATH,
-} from '../../../../common/constants';
-import { arrify } from '../../../../common/services/utils';
+} from '../../../common/constants';
+import { arrify } from '../../../common/services/utils';
import {
UIM_FOLLOWER_INDEX_CREATE,
UIM_FOLLOWER_INDEX_UPDATE,
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js
similarity index 92%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js
index 95aa3f0ebc3e4..70311d5ba1e4d 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js
@@ -9,11 +9,12 @@ export const parseAutoFollowError = error => {
return null;
}
- const { leaderIndex, autoFollowException } = error;
+ const { leaderIndex, autoFollowException, timestamp } = error;
const id = leaderIndex.substring(0, leaderIndex.lastIndexOf(':'));
return {
id,
+ timestamp,
leaderIndex,
autoFollowException,
};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.test.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.test.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js
similarity index 97%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js
index 1b5a39658ee46..cf394d4b3c7d8 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js
@@ -8,8 +8,8 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public';
-import { indexPatterns } from '../../../../../../../../src/plugins/data/public';
+import { indices } from '../../../../../../src/plugins/es_ui_shared/public';
+import { indexPatterns } from '../../../../../../src/plugins/data/public';
const {
indexNameBeginsWithPeriod,
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.test.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts
similarity index 62%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts
rename to x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts
index dc64cdee07f7d..84ac9356462ad 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts
@@ -3,26 +3,20 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+
import { i18n } from '@kbn/i18n';
import { ChromeBreadcrumb } from 'src/core/public';
-import { ManagementAppMountParams } from '../../../../../../../../src/plugins/management/public';
+import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public';
+
+import { BASE_PATH } from '../../../common/constants';
-import { BASE_PATH } from '../../../../common/constants';
+export type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs'];
-let setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
+let setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
-export const setBreadcrumbSetter = ({
- __LEGACY,
-}: {
- __LEGACY: {
- chrome: any;
- MANAGEMENT_BREADCRUMB: ChromeBreadcrumb;
- };
-}): void => {
- setBreadcrumbs = (crumbs: ChromeBreadcrumb[]) => {
- __LEGACY.chrome.breadcrumbs.set([__LEGACY.MANAGEMENT_BREADCRUMB, ...crumbs]);
- };
+export const init = (_setBreadcrumbs: SetBreadcrumbs): void => {
+ setBreadcrumbs = _setBreadcrumbs;
};
export const listBreadcrumb = {
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts
new file mode 100644
index 0000000000000..c8b00f6e246b5
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
+
+let _esBase: string;
+
+export const init = (esBase: string) => {
+ _esBase = esBase;
+};
+
+export const getAutoFollowPatternUrl = (): string => `${_esBase}/ccr-put-auto-follow-pattern.html`;
+export const getFollowerIndexUrl = (): string => `${_esBase}/ccr-put-follow.html`;
+export const getByteUnitsUrl = (): string => `${_esBase}/common-options.html#byte-units`;
+export const getTimeUnitsUrl = (): string => `${_esBase}/common-options.html#time-units`;
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js b/x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js
similarity index 89%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js
index d20fa76ef5451..118a54887d404 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../common/constants';
+import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../common/constants';
export const getSettingDefault = name => {
if (!FOLLOWER_INDEX_ADVANCED_SETTINGS[name]) {
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/get_remote_cluster_name.js b/x-pack/plugins/cross_cluster_replication/public/app/services/get_remote_cluster_name.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/get_remote_cluster_name.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/get_remote_cluster_name.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js b/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js
similarity index 97%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js
index 64c3e8412437e..7e2b45b625c1f 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js
@@ -6,7 +6,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public';
+import { indices } from '../../../../../../src/plugins/es_ui_shared/public';
const isEmpty = value => {
return !value || !value.trim().length;
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/notifications.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/notifications.ts
new file mode 100644
index 0000000000000..66fc9de00995c
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/notifications.ts
@@ -0,0 +1,18 @@
+/*
+ * 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 { IToasts, FatalErrorsSetup } from 'src/core/public';
+
+let _toasts: IToasts;
+let _fatalErrors: FatalErrorsSetup;
+
+export const init = (toasts: IToasts, fatalErrors: FatalErrorsSetup) => {
+ _toasts = toasts;
+ _fatalErrors = fatalErrors;
+};
+
+export const getToasts = () => _toasts;
+export const getFatalErrors = () => _fatalErrors;
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/query_params.js b/x-pack/plugins/cross_cluster_replication/public/app/services/query_params.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/query_params.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/query_params.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/index.js b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.d.ts
similarity index 87%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/routing.d.ts
index e92c44da34474..9e96ea12856f6 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/index.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.d.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import './register_routes';
+export declare const routing: any;
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.js
similarity index 93%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/routing.js
index 965aeaaad22ad..124c61e1ba19e 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.js
@@ -10,7 +10,7 @@
import { createLocation } from 'history';
import { stringify } from 'query-string';
-import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../../common/constants';
+import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants';
const isModifiedEvent = event =>
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
@@ -32,7 +32,6 @@ const appToBasePathMap = {
};
class Routing {
- _userHasLeftApp = false;
_reactRouter = null;
/**
@@ -97,14 +96,6 @@ class Routing {
set reactRouter(router) {
this._reactRouter = router;
}
-
- get userHasLeftApp() {
- return this._userHasLeftApp;
- }
-
- set userHasLeftApp(hasLeft) {
- this._userHasLeftApp = hasLeft;
- }
}
-export default new Routing();
+export const routing = new Routing();
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/track_ui_metric.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/track_ui_metric.ts
new file mode 100644
index 0000000000000..aecc4eb83893f
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/track_ui_metric.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
+import { UiStatsMetricType, METRIC_TYPE } from '@kbn/analytics';
+
+import { UIM_APP_NAME } from '../constants';
+
+export { METRIC_TYPE };
+
+// usageCollection is an optional dependency, so we default to a no-op.
+export let trackUiMetric = (metricType: UiStatsMetricType, eventName: string) => {};
+
+export function init(usageCollection: UsageCollectionSetup): void {
+ trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, UIM_APP_NAME);
+}
+
+/**
+ * Transparently return provided request Promise, while allowing us to track
+ * a successful completion of the request.
+ */
+export function trackUserRequest(request: Promise, actionType: string) {
+ // Only track successful actions.
+ return request.then(response => {
+ // It looks like we're using the wrong type here, added via
+ // https://github.com/elastic/kibana/pull/41113/files#diff-e65a0a6696a9d723969afd871cbd60cdR19
+ // but we'll keep it for now to avoid discontinuity in our telemetry data.
+ trackUiMetric(METRIC_TYPE.LOADED, actionType);
+
+ // We return the response immediately without waiting for the tracking request to resolve,
+ // to avoid adding additional latency.
+ return response;
+ });
+}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.js b/x-pack/plugins/cross_cluster_replication/public/app/services/utils.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/utils.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/utils.test.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.test.js
rename to x-pack/plugins/cross_cluster_replication/public/app/services/utils.test.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/action_types.js b/x-pack/plugins/cross_cluster_replication/public/app/store/action_types.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/action_types.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/action_types.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/api.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/api.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/api.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/api.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js
similarity index 95%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js
index b81cd30f3977a..52a22cb17d0a9 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
-import { getNotifications } from '../../services/notifications';
+import { getToasts } from '../../services/notifications';
import { SECTIONS, API_STATUS } from '../../constants';
import {
loadAutoFollowPatterns as loadAutoFollowPatternsRequest,
@@ -15,7 +15,7 @@ import {
pauseAutoFollowPattern as pauseAutoFollowPatternRequest,
resumeAutoFollowPattern as resumeAutoFollowPatternRequest,
} from '../../services/api';
-import routing from '../../services/routing';
+import { routing } from '../../services/routing';
import * as t from '../action_types';
import { sendApiRequest } from './api';
import { getSelectedAutoFollowPatternId } from '../selectors';
@@ -75,7 +75,7 @@ export const saveAutoFollowPattern = (id, autoFollowPattern, isUpdating = false)
}
);
- getNotifications().addSuccess(successMessage);
+ getToasts().addSuccess(successMessage);
routing.navigate(`/auto_follow_patterns`, undefined, {
pattern: encodeURIComponent(id),
});
@@ -111,7 +111,7 @@ export const deleteAutoFollowPattern = id =>
}
);
- getNotifications().addDanger(errorMessage);
+ getToasts().addDanger(errorMessage);
}
if (response.itemsDeleted.length) {
@@ -133,7 +133,7 @@ export const deleteAutoFollowPattern = id =>
}
);
- getNotifications().addSuccess(successMessage);
+ getToasts().addSuccess(successMessage);
// If we've just deleted a pattern we were looking at, we need to close the panel.
const autoFollowPatternId = getSelectedAutoFollowPatternId('detail')(getState());
@@ -173,7 +173,7 @@ export const pauseAutoFollowPattern = id =>
}
);
- getNotifications().addDanger(errorMessage);
+ getToasts().addDanger(errorMessage);
}
if (response.itemsPaused.length) {
@@ -195,7 +195,7 @@ export const pauseAutoFollowPattern = id =>
}
);
- getNotifications().addSuccess(successMessage);
+ getToasts().addSuccess(successMessage);
}
},
});
@@ -229,7 +229,7 @@ export const resumeAutoFollowPattern = id =>
}
);
- getNotifications().addDanger(errorMessage);
+ getToasts().addDanger(errorMessage);
}
if (response.itemsResumed.length) {
@@ -251,7 +251,7 @@ export const resumeAutoFollowPattern = id =>
}
);
- getNotifications().addSuccess(successMessage);
+ getToasts().addSuccess(successMessage);
}
},
});
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/ccr.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/ccr.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/ccr.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/ccr.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js
similarity index 94%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js
index ebdee067ced75..d081e0444eb58 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js
@@ -5,8 +5,8 @@
*/
import { i18n } from '@kbn/i18n';
-import routing from '../../services/routing';
-import { getNotifications } from '../../services/notifications';
+import { routing } from '../../services/routing';
+import { getToasts } from '../../services/notifications';
import { SECTIONS, API_STATUS } from '../../constants';
import {
loadFollowerIndices as loadFollowerIndicesRequest,
@@ -76,7 +76,7 @@ export const saveFollowerIndex = (name, followerIndex, isUpdating = false) =>
}
);
- getNotifications().addSuccess(successMessage);
+ getToasts().addSuccess(successMessage);
routing.navigate(`/follower_indices`, undefined, {
name: encodeURIComponent(name),
});
@@ -112,7 +112,7 @@ export const pauseFollowerIndex = id =>
}
);
- getNotifications().addDanger(errorMessage);
+ getToasts().addDanger(errorMessage);
}
if (response.itemsPaused.length) {
@@ -134,7 +134,7 @@ export const pauseFollowerIndex = id =>
}
);
- getNotifications().addSuccess(successMessage);
+ getToasts().addSuccess(successMessage);
// Refresh list
dispatch(loadFollowerIndices(true));
@@ -149,6 +149,7 @@ export const resumeFollowerIndex = id =>
scope,
handler: async () => resumeFollowerIndexRequest(id),
onSuccess(response, dispatch) {
+ console.log('response', response);
/**
* We can have 1 or more follower index resume operation
* that can fail or succeed. We will show 1 toast notification for each.
@@ -171,7 +172,7 @@ export const resumeFollowerIndex = id =>
}
);
- getNotifications().addDanger(errorMessage);
+ getToasts().addDanger(errorMessage);
}
if (response.itemsResumed.length) {
@@ -193,7 +194,7 @@ export const resumeFollowerIndex = id =>
}
);
- getNotifications().addSuccess(successMessage);
+ getToasts().addSuccess(successMessage);
}
// Refresh list
@@ -230,7 +231,7 @@ export const unfollowLeaderIndex = id =>
}
);
- getNotifications().addDanger(errorMessage);
+ getToasts().addDanger(errorMessage);
}
if (response.itemsUnfollowed.length) {
@@ -252,7 +253,7 @@ export const unfollowLeaderIndex = id =>
}
);
- getNotifications().addSuccess(successMessage);
+ getToasts().addSuccess(successMessage);
}
if (response.itemsNotOpen.length) {
@@ -274,7 +275,7 @@ export const unfollowLeaderIndex = id =>
}
);
- getNotifications().addWarning(warningMessage);
+ getToasts().addWarning(warningMessage);
}
// If we've just unfollowed a follower index we were looking at, we need to close the panel.
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/index.d.ts
similarity index 83%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/index.d.ts
index f2c070fd44b6e..6d35dfeddfd46 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/store/index.d.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { checkLicense } from './check_license';
+export declare const ccrStore: any;
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.test.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.test.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/auto_follow_pattern.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/follower_index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/follower_index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/stats.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/stats.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/stats.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/stats.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/selectors/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/selectors/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/selectors/index.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/selectors/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/store.js b/x-pack/plugins/cross_cluster_replication/public/app/store/store.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/store.js
rename to x-pack/plugins/cross_cluster_replication/public/app/store/store.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts b/x-pack/plugins/cross_cluster_replication/public/index.ts
similarity index 61%
rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts
rename to x-pack/plugins/cross_cluster_replication/public/index.ts
index 11aea6b7b5de4..e3e2d860e526d 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts
+++ b/x-pack/plugins/cross_cluster_replication/public/index.ts
@@ -6,6 +6,7 @@
import { PluginInitializerContext } from 'src/core/public';
-import { CrossClusterReplicationUIPlugin } from './plugin';
+import { CrossClusterReplicationPlugin } from './plugin';
-export const plugin = (ctx: PluginInitializerContext) => new CrossClusterReplicationUIPlugin(ctx);
+export const plugin = (initializerContext: PluginInitializerContext) =>
+ new CrossClusterReplicationPlugin(initializerContext);
diff --git a/x-pack/plugins/cross_cluster_replication/public/plugin.ts b/x-pack/plugins/cross_cluster_replication/public/plugin.ts
new file mode 100644
index 0000000000000..bdaa04e9d53ee
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/plugin.ts
@@ -0,0 +1,102 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { get } from 'lodash';
+import { first } from 'rxjs/operators';
+import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public';
+
+import { PLUGIN, MANAGEMENT_ID } from '../common/constants';
+import { init as initUiMetric } from './app/services/track_ui_metric';
+import { init as initNotification } from './app/services/notifications';
+import { PluginDependencies, ClientConfigType } from './types';
+
+// @ts-ignore;
+import { setHttpClient } from './app/services/api';
+
+export class CrossClusterReplicationPlugin implements Plugin {
+ constructor(private readonly initializerContext: PluginInitializerContext) {}
+
+ public setup(coreSetup: CoreSetup, plugins: PluginDependencies) {
+ const { licensing, remoteClusters, usageCollection, management, indexManagement } = plugins;
+ const esSection = management.sections.getSection('elasticsearch');
+
+ const {
+ http,
+ notifications: { toasts },
+ fatalErrors,
+ getStartServices,
+ } = coreSetup;
+
+ // Initialize services even if the app isn't mounted, because they're used by index management extensions.
+ setHttpClient(http);
+ initUiMetric(usageCollection);
+ initNotification(toasts, fatalErrors);
+
+ const ccrApp = esSection!.registerApp({
+ id: MANAGEMENT_ID,
+ title: PLUGIN.TITLE,
+ order: 4,
+ mount: async ({ element, setBreadcrumbs }) => {
+ const { mountApp } = await import('./app');
+
+ const [coreStart] = await getStartServices();
+ const {
+ i18n: { Context: I18nContext },
+ docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION },
+ } = coreStart;
+
+ return mountApp({
+ element,
+ setBreadcrumbs,
+ I18nContext,
+ ELASTIC_WEBSITE_URL,
+ DOC_LINK_VERSION,
+ });
+ },
+ });
+
+ ccrApp.disable();
+
+ licensing.license$
+ .pipe(first())
+ .toPromise()
+ .then(license => {
+ const licenseStatus = license.check(PLUGIN.ID, PLUGIN.minimumLicenseType);
+ const isLicenseOk = licenseStatus.state === 'valid';
+ const config = this.initializerContext.config.get();
+
+ // remoteClusters.isUiEnabled is driven by the xpack.remote_clusters.ui.enabled setting.
+ // The CCR UI depends upon the Remote Clusters UI (e.g. by cross-linking to it), so if
+ // the Remote Clusters UI is disabled we can't show the CCR UI.
+ const isCcrUiEnabled = config.ui.enabled && remoteClusters.isUiEnabled;
+
+ if (isLicenseOk && isCcrUiEnabled) {
+ ccrApp.enable();
+
+ if (indexManagement) {
+ const propertyPath = 'isFollowerIndex';
+
+ const followerBadgeExtension = {
+ matchIndex: (index: any) => {
+ return get(index, propertyPath);
+ },
+ label: i18n.translate('xpack.crossClusterReplication.indexMgmtBadge.followerLabel', {
+ defaultMessage: 'Follower',
+ }),
+ color: 'default',
+ filterExpression: 'isFollowerIndex:true',
+ };
+
+ indexManagement.extensionsService.addBadge(followerBadgeExtension);
+ }
+ }
+ });
+ }
+
+ public start() {}
+ public stop() {}
+}
diff --git a/x-pack/plugins/cross_cluster_replication/public/types.ts b/x-pack/plugins/cross_cluster_replication/public/types.ts
new file mode 100644
index 0000000000000..aac174b7524d3
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/types.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
+import { ManagementSetup } from '../../../../src/plugins/management/public';
+import { IndexManagementPluginSetup } from '../../index_management/public';
+import { RemoteClustersPluginSetup } from '../../remote_clusters/public';
+import { LicensingPluginSetup } from '../../licensing/public';
+
+export interface PluginDependencies {
+ usageCollection: UsageCollectionSetup;
+ management: ManagementSetup;
+ indexManagement: IndexManagementPluginSetup;
+ remoteClusters: RemoteClustersPluginSetup;
+ licensing: LicensingPluginSetup;
+}
+
+export interface ClientConfigType {
+ ui: {
+ enabled: boolean;
+ };
+}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts
similarity index 97%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js
rename to x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts
index 91527b8eb7cc5..d4de54391286b 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js
+++ b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const elasticsearchJsPlugin = (Client, config, components) => {
+export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => {
const ca = components.clientAction.factory;
Client.prototype.ccr = components.clientAction.namespaceFactory();
diff --git a/x-pack/plugins/cross_cluster_replication/server/config.ts b/x-pack/plugins/cross_cluster_replication/server/config.ts
new file mode 100644
index 0000000000000..17999d37c76b7
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/config.ts
@@ -0,0 +1,16 @@
+/*
+ * 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, TypeOf } from '@kbn/config-schema';
+
+export const configSchema = schema.object({
+ enabled: schema.boolean({ defaultValue: true }),
+ ui: schema.object({
+ enabled: schema.boolean({ defaultValue: true }),
+ }),
+});
+
+export type CrossClusterReplicationConfig = TypeOf;
diff --git a/x-pack/plugins/cross_cluster_replication/server/index.ts b/x-pack/plugins/cross_cluster_replication/server/index.ts
new file mode 100644
index 0000000000000..597c039ad202e
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/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 { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server';
+import { CrossClusterReplicationServerPlugin } from './plugin';
+import { configSchema, CrossClusterReplicationConfig } from './config';
+
+export const plugin = (pluginInitializerContext: PluginInitializerContext) =>
+ new CrossClusterReplicationServerPlugin(pluginInitializerContext);
+
+export const config: PluginConfigDescriptor = {
+ schema: configSchema,
+ exposeToBrowser: {
+ ui: true,
+ },
+};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.ts.snap
similarity index 93%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap
rename to x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.ts.snap
index 92ac6070904b5..3eced37112a35 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap
+++ b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.ts.snap
@@ -19,6 +19,7 @@ Object {
"type": "exception",
},
"leaderIndex": "pattern-1:kibana_sample_1",
+ "timestamp": 1587081600021,
},
Object {
"autoFollowException": Object {
@@ -26,6 +27,7 @@ Object {
"type": "exception",
},
"leaderIndex": "pattern-2:kibana_sample_1",
+ "timestamp": 1587081600021,
},
],
}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.ts
similarity index 95%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js
rename to x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.ts
index 5120c56701e5b..5141aa56c1d7e 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js
+++ b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.ts
@@ -15,6 +15,7 @@ describe('[CCR] auto-follow stats serialization', () => {
recent_auto_follow_errors: [
{
leader_index: 'pattern-1:kibana_sample_1',
+ timestamp: 1587081600021,
auto_follow_exception: {
type: 'exception',
reason:
@@ -23,6 +24,7 @@ describe('[CCR] auto-follow stats serialization', () => {
},
{
leader_index: 'pattern-2:kibana_sample_1',
+ timestamp: 1587081600021,
auto_follow_exception: {
type: 'exception',
reason:
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.ts
similarity index 77%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js
rename to x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.ts
index e4d2f8d64d1bb..7e2b088919842 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js
+++ b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.ts
@@ -4,11 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
-/* eslint-disable camelcase */
+import {
+ RecentAutoFollowError,
+ RecentAutoFollowErrorFromEs,
+ AutoFollowedCluster,
+ AutoFollowedClusterFromEs,
+ AutoFollowStats,
+ AutoFollowStatsFromEs,
+} from '../../common/types';
+
export const deserializeRecentAutoFollowErrors = ({
+ timestamp,
leader_index,
auto_follow_exception: { type, reason },
-}) => ({
+}: RecentAutoFollowErrorFromEs): RecentAutoFollowError => ({
+ timestamp,
leaderIndex: leader_index,
autoFollowException: {
type,
@@ -20,7 +30,7 @@ export const deserializeAutoFollowedClusters = ({
cluster_name,
time_since_last_check_millis,
last_seen_metadata_version,
-}) => ({
+}: AutoFollowedClusterFromEs): AutoFollowedCluster => ({
clusterName: cluster_name,
timeSinceLastCheckMillis: time_since_last_check_millis,
lastSeenMetadataVersion: last_seen_metadata_version,
@@ -32,11 +42,10 @@ export const deserializeAutoFollowStats = ({
number_of_successful_follow_indices,
recent_auto_follow_errors,
auto_followed_clusters,
-}) => ({
+}: AutoFollowStatsFromEs): AutoFollowStats => ({
numberOfFailedFollowIndices: number_of_failed_follow_indices,
numberOfFailedRemoteClusterStateRequests: number_of_failed_remote_cluster_state_requests,
numberOfSuccessfulFollowIndices: number_of_successful_follow_indices,
recentAutoFollowErrors: recent_auto_follow_errors.map(deserializeRecentAutoFollowErrors),
autoFollowedClusters: auto_followed_clusters.map(deserializeAutoFollowedClusters),
});
-/* eslint-enable camelcase */
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts b/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts
similarity index 90%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts
rename to x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts
index 8afd5f1a018eb..9dde027cd6949 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts
@@ -63,3 +63,16 @@ export function wrapEsError(
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,
+ },
+ },
+ };
+}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts b/x-pack/plugins/cross_cluster_replication/server/lib/is_es_error.ts
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts
rename to x-pack/plugins/cross_cluster_replication/server/lib/is_es_error.ts
diff --git a/x-pack/plugins/cross_cluster_replication/server/plugin.ts b/x-pack/plugins/cross_cluster_replication/server/plugin.ts
new file mode 100644
index 0000000000000..25c99803480f3
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/plugin.ts
@@ -0,0 +1,139 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+declare module 'src/core/server' {
+ interface RequestHandlerContext {
+ crossClusterReplication?: CrossClusterReplicationContext;
+ }
+}
+
+import { Observable } from 'rxjs';
+import { first } from 'rxjs/operators';
+import { i18n } from '@kbn/i18n';
+import {
+ CoreSetup,
+ Plugin,
+ Logger,
+ PluginInitializerContext,
+ APICaller,
+ IScopedClusterClient,
+} from 'src/core/server';
+
+import { Index } from '../../index_management/server';
+import { PLUGIN } from '../common/constants';
+import { Dependencies } from './types';
+import { registerApiRoutes } from './routes';
+import { License } from './services';
+import { elasticsearchJsPlugin } from './client/elasticsearch_ccr';
+import { CrossClusterReplicationConfig } from './config';
+import { isEsError } from './lib/is_es_error';
+import { formatEsError } from './lib/format_es_error';
+
+interface CrossClusterReplicationContext {
+ client: IScopedClusterClient;
+}
+
+const ccrDataEnricher = async (indicesList: Index[], callWithRequest: APICaller) => {
+ if (!indicesList?.length) {
+ return indicesList;
+ }
+ const params = {
+ path: '/_all/_ccr/info',
+ method: 'GET',
+ };
+ try {
+ const { follower_indices: followerIndices } = await callWithRequest(
+ 'transport.request',
+ params
+ );
+ return indicesList.map(index => {
+ const isFollowerIndex = !!followerIndices.find(
+ (followerIndex: { follower_index: string }) => {
+ return followerIndex.follower_index === index.name;
+ }
+ );
+ return {
+ ...index,
+ isFollowerIndex,
+ };
+ });
+ } catch (e) {
+ return indicesList;
+ }
+};
+
+export class CrossClusterReplicationServerPlugin implements Plugin {
+ private readonly config$: Observable;
+ private readonly license: License;
+ private readonly logger: Logger;
+
+ constructor(initializerContext: PluginInitializerContext) {
+ this.logger = initializerContext.logger.get();
+ this.config$ = initializerContext.config.create();
+ this.license = new License();
+ }
+
+ setup(
+ { http, elasticsearch }: CoreSetup,
+ { licensing, indexManagement, remoteClusters }: Dependencies
+ ) {
+ this.config$
+ .pipe(first())
+ .toPromise()
+ .then(config => {
+ // remoteClusters.isUiEnabled is driven by the xpack.remote_clusters.ui.enabled setting.
+ // The CCR UI depends upon the Remote Clusters UI (e.g. by cross-linking to it), so if
+ // the Remote Clusters UI is disabled we can't show the CCR UI.
+ const isCcrUiEnabled = config.ui.enabled && remoteClusters.isUiEnabled;
+
+ // If the UI isn't enabled, then we don't want to expose any CCR concepts in the UI, including
+ // "follower" badges for follower indices.
+ if (isCcrUiEnabled) {
+ if (indexManagement.indexDataEnricher) {
+ indexManagement.indexDataEnricher.add(ccrDataEnricher);
+ }
+ }
+ });
+
+ this.license.setup(
+ {
+ pluginId: PLUGIN.ID,
+ minimumLicenseType: PLUGIN.minimumLicenseType,
+ defaultErrorMessage: i18n.translate(
+ 'xpack.crossClusterReplication.licenseCheckErrorMessage',
+ {
+ defaultMessage: 'License check failed',
+ }
+ ),
+ },
+ {
+ licensing,
+ logger: this.logger,
+ }
+ );
+
+ // Extend the elasticsearchJs client with additional endpoints.
+ const esClientConfig = { plugins: [elasticsearchJsPlugin] };
+ const ccrEsClient = elasticsearch.createClient('crossClusterReplication', esClientConfig);
+ http.registerRouteHandlerContext('crossClusterReplication', (ctx, request) => {
+ return {
+ client: ccrEsClient.asScoped(request),
+ };
+ });
+
+ registerApiRoutes({
+ router: http.createRouter(),
+ license: this.license,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+ }
+
+ start() {}
+ stop() {}
+}
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/index.ts
new file mode 100644
index 0000000000000..4cbdc7703a694
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/index.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { registerFetchRoute } from './register_fetch_route';
+import { registerGetRoute } from './register_get_route';
+import { registerPauseRoute } from './register_pause_route';
+import { registerResumeRoute } from './register_resume_route';
+import { registerUpdateRoute } from './register_update_route';
+
+export function registerAutoFollowPatternRoutes(dependencies: RouteDependencies) {
+ registerCreateRoute(dependencies);
+ registerDeleteRoute(dependencies);
+ registerFetchRoute(dependencies);
+ registerGetRoute(dependencies);
+ registerPauseRoute(dependencies);
+ registerResumeRoute(dependencies);
+ registerUpdateRoute(dependencies);
+}
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts
new file mode 100644
index 0000000000000..b41b52e1764c8
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts
@@ -0,0 +1,74 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerCreateRoute } from './register_create_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Create auto-follow pattern', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerCreateRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.post.mock.calls[0][1];
+ });
+
+ it('should throw a 409 conflict error if id already exists', async () => {
+ const routeContextMock = mockRouteContext({
+ // Fail the uniqueness check.
+ callAsCurrentUser: jest.fn().mockResolvedValueOnce(true),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ body: {
+ id: 'some-id',
+ foo: 'bar',
+ },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.status).toEqual(409);
+ });
+
+ it('should return 200 status when the id does not exist', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ // Pass the uniqueness check.
+ .mockRejectedValueOnce({ statusCode: 404 })
+ .mockResolvedValueOnce(true),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ body: {
+ id: 'some-id',
+ foo: 'bar',
+ },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.status).toEqual(200);
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts
new file mode 100644
index 0000000000000..12503e3532a47
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts
@@ -0,0 +1,77 @@
+/*
+ * 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 { serializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization';
+import { AutoFollowPattern } from '../../../../common/types';
+import { addBasePath } from '../../../services';
+import { RouteDependencies } from '../../../types';
+
+/**
+ * Create an auto-follow pattern
+ */
+export const registerCreateRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const bodySchema = schema.object({
+ id: schema.string(),
+ remoteCluster: schema.string(),
+ leaderIndexPatterns: schema.arrayOf(schema.string()),
+ followIndexPattern: schema.string(),
+ });
+
+ router.post(
+ {
+ path: addBasePath('/auto_follow_patterns'),
+ validate: {
+ body: bodySchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id, ...rest } = request.body;
+ const body = serializeAutoFollowPattern(rest as AutoFollowPattern);
+
+ /**
+ * First let's make sure that an auto-follow pattern with
+ * the same id does not exist.
+ */
+ try {
+ await context.crossClusterReplication!.client.callAsCurrentUser('ccr.autoFollowPattern', {
+ id,
+ });
+ // If we get here it means that an auto-follow pattern with the same id exists
+ return response.conflict({
+ body: `An auto-follow pattern with the name "${id}" already exists.`,
+ });
+ } catch (err) {
+ if (err.statusCode !== 404) {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ }
+ }
+
+ try {
+ return response.ok({
+ body: await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.saveAutoFollowPattern',
+ { id, body }
+ ),
+ });
+ } catch (err) {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts
new file mode 100644
index 0000000000000..e610d09b44275
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts
@@ -0,0 +1,87 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerDeleteRoute } from './register_delete_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Delete auto-follow pattern(s)', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerDeleteRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.delete.mock.calls[0][1];
+ });
+
+ it('deletes a single item', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsDeleted).toEqual(['a']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('deletes multiple items', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b,c' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsDeleted).toEqual(['a', 'b', 'c']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('returns partial errors', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockRejectedValueOnce({ response: { error: {} } }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+
+ expect(response.payload.itemsDeleted).toEqual(['a']);
+ expect(response.payload.errors[0].id).toEqual('b');
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts
new file mode 100644
index 0000000000000..ed2633a4a469e
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts
@@ -0,0 +1,67 @@
+/*
+ * 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';
+
+/**
+ * Delete an auto-follow pattern
+ */
+export const registerDeleteRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const paramsSchema = schema.object({
+ id: schema.string(),
+ });
+
+ router.delete(
+ {
+ path: addBasePath('/auto_follow_patterns/{id}'),
+ validate: {
+ params: paramsSchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsDeleted: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ const formatError = (err: any) => {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ };
+
+ await Promise.all(
+ ids.map(_id =>
+ context
+ .crossClusterReplication!.client.callAsCurrentUser('ccr.deleteAutoFollowPattern', {
+ id: _id,
+ })
+ .then(() => itemsDeleted.push(_id))
+ .catch((err: any) => {
+ errors.push({ id: _id, error: formatError(err) });
+ })
+ )
+ );
+
+ return response.ok({
+ body: {
+ itemsDeleted,
+ errors,
+ },
+ });
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts
new file mode 100644
index 0000000000000..dd102c45665cb
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerFetchRoute } from './register_fetch_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Fetch all auto-follow patterns', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerFetchRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.get.mock.calls[0][1];
+ });
+
+ it('deserializes the response from Elasticsearch', async () => {
+ const ccrAutoFollowPatternResponseMock = {
+ patterns: [
+ {
+ name: 'autoFollowPattern',
+ pattern: {
+ active: true,
+ remote_cluster: 'remoteCluster',
+ leader_index_patterns: ['leader*'],
+ follow_index_pattern: 'follow',
+ },
+ },
+ ],
+ };
+
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock),
+ });
+
+ const request = httpServerMock.createKibanaRequest();
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.patterns).toEqual([
+ {
+ active: true,
+ followIndexPattern: 'follow',
+ leaderIndexPatterns: ['leader*'],
+ name: 'autoFollowPattern',
+ remoteCluster: 'remoteCluster',
+ },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts
new file mode 100644
index 0000000000000..70d8ae4d51e3b
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 { deserializeListAutoFollowPatterns } from '../../../../common/services/auto_follow_pattern_serialization';
+import { addBasePath } from '../../../services';
+import { RouteDependencies } from '../../../types';
+
+/**
+ * Get a list of all auto-follow patterns
+ */
+export const registerFetchRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ router.get(
+ {
+ path: addBasePath('/auto_follow_patterns'),
+ validate: false,
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ try {
+ const result = await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.autoFollowPatterns'
+ );
+ return response.ok({
+ body: {
+ patterns: deserializeListAutoFollowPatterns(result.patterns),
+ },
+ });
+ } catch (err) {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts
new file mode 100644
index 0000000000000..d5889074651f5
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerGetRoute } from './register_get_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Get one auto-follow pattern', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerGetRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.get.mock.calls[0][1];
+ });
+
+ it('should return a single resource even though ES returns an array with 1 item', async () => {
+ const ccrAutoFollowPatternResponseMock = {
+ patterns: [
+ {
+ name: 'autoFollowPattern',
+ pattern: {
+ active: true,
+ remote_cluster: 'remoteCluster',
+ leader_index_patterns: ['leader*'],
+ follow_index_pattern: 'follow',
+ },
+ },
+ ],
+ };
+
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock),
+ });
+
+ const request = httpServerMock.createKibanaRequest();
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload).toEqual({
+ active: true,
+ followIndexPattern: 'follow',
+ leaderIndexPatterns: ['leader*'],
+ name: 'autoFollowPattern',
+ remoteCluster: 'remoteCluster',
+ });
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts
new file mode 100644
index 0000000000000..1edbf7e8806c7
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { deserializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization';
+import { addBasePath } from '../../../services';
+import { RouteDependencies } from '../../../types';
+
+/**
+ * Get a single auto-follow pattern
+ */
+export const registerGetRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const paramsSchema = schema.object({
+ id: schema.string(),
+ });
+
+ router.get(
+ {
+ path: addBasePath('/auto_follow_patterns/{id}'),
+ validate: {
+ params: paramsSchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id } = request.params;
+
+ try {
+ const result = await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.autoFollowPattern',
+ { id }
+ );
+ const autoFollowPattern = result.patterns[0];
+
+ return response.ok({
+ body: deserializeAutoFollowPattern(autoFollowPattern),
+ });
+ } catch (err) {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts
new file mode 100644
index 0000000000000..1eaac02918b88
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerPauseRoute } from './register_pause_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Pause auto-follow pattern(s)', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerPauseRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.post.mock.calls[0][1];
+ });
+
+ it('pauses a single item', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsPaused).toEqual(['a']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('pauses multiple items', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b,c' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsPaused).toEqual(['a', 'b', 'c']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('returns partial errors', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockRejectedValueOnce({ response: { error: {} } }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsPaused).toEqual(['a']);
+ expect(response.payload.errors[0].id).toEqual('b');
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts
new file mode 100644
index 0000000000000..325939709e751
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts
@@ -0,0 +1,66 @@
+/*
+ * 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';
+
+/**
+ * Pause auto-follow pattern(s)
+ */
+export const registerPauseRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const paramsSchema = schema.object({
+ id: schema.string(),
+ });
+
+ router.post(
+ {
+ path: addBasePath('/auto_follow_patterns/{id}/pause'),
+ validate: {
+ params: paramsSchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsPaused: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ const formatError = (err: any) => {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ };
+
+ await Promise.all(
+ ids.map(_id =>
+ context
+ .crossClusterReplication!.client.callAsCurrentUser('ccr.pauseAutoFollowPattern', {
+ id: _id,
+ })
+ .then(() => itemsPaused.push(_id))
+ .catch(err => {
+ errors.push({ id: _id, error: formatError(err) });
+ })
+ )
+ );
+
+ return response.ok({
+ body: {
+ itemsPaused,
+ errors,
+ },
+ });
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts
new file mode 100644
index 0000000000000..9839761e701fc
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerResumeRoute } from './register_resume_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Resume auto-follow pattern(s)', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerResumeRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.post.mock.calls[0][1];
+ });
+
+ it('resumes a single item', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsResumed).toEqual(['a']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('resumes multiple items', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b,c' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsResumed).toEqual(['a', 'b', 'c']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('returns partial errors', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockRejectedValueOnce({ response: { error: {} } }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsResumed).toEqual(['a']);
+ expect(response.payload.errors[0].id).toEqual('b');
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts
new file mode 100644
index 0000000000000..f5e917773704c
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts
@@ -0,0 +1,66 @@
+/*
+ * 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';
+
+/**
+ * Resume auto-follow pattern(s)
+ */
+export const registerResumeRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const paramsSchema = schema.object({
+ id: schema.string(),
+ });
+
+ router.post(
+ {
+ path: addBasePath('/auto_follow_patterns/{id}/resume'),
+ validate: {
+ params: paramsSchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsResumed: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ const formatError = (err: any) => {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ };
+
+ await Promise.all(
+ ids.map((_id: string) =>
+ context
+ .crossClusterReplication!.client.callAsCurrentUser('ccr.resumeAutoFollowPattern', {
+ id: _id,
+ })
+ .then(() => itemsResumed.push(_id))
+ .catch(err => {
+ errors.push({ id: _id, error: formatError(err) });
+ })
+ )
+ );
+
+ return response.ok({
+ body: {
+ itemsResumed,
+ errors,
+ },
+ });
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts
new file mode 100644
index 0000000000000..85f2270ec3aee
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerUpdateRoute } from './register_update_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Update auto-follow pattern', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerUpdateRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.put.mock.calls[0][1];
+ });
+
+ it('should serialize the payload before sending it to Elasticsearch', async () => {
+ const routeContextMock = mockRouteContext({
+ // Just echo back what we send so we can inspect it.
+ callAsCurrentUser: jest.fn().mockImplementation((endpoint, payload) => payload),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'foo' },
+ body: {
+ remoteCluster: 'bar1',
+ leaderIndexPatterns: ['bar2'],
+ followIndexPattern: 'bar3',
+ },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+
+ expect(response.payload).toEqual({
+ id: 'foo',
+ body: {
+ remote_cluster: 'bar1',
+ leader_index_patterns: ['bar2'],
+ follow_index_pattern: 'bar3',
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts
new file mode 100644
index 0000000000000..836e5f55c5a48
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts
@@ -0,0 +1,60 @@
+/*
+ * 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 { serializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization';
+import { AutoFollowPattern } from '../../../../common/types';
+import { addBasePath } from '../../../services';
+import { RouteDependencies } from '../../../types';
+
+/**
+ * Update an auto-follow pattern
+ */
+export const registerUpdateRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const paramsSchema = schema.object({
+ id: schema.string(),
+ });
+
+ const bodySchema = schema.object({
+ active: schema.boolean(),
+ remoteCluster: schema.string(),
+ leaderIndexPatterns: schema.arrayOf(schema.string()),
+ followIndexPattern: schema.string(),
+ });
+
+ router.put(
+ {
+ path: addBasePath('/auto_follow_patterns/{id}'),
+ validate: {
+ params: paramsSchema,
+ body: bodySchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id } = request.params;
+ const body = serializeAutoFollowPattern(request.body as AutoFollowPattern);
+
+ try {
+ return response.ok({
+ body: await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.saveAutoFollowPattern',
+ { id, body }
+ ),
+ });
+ } catch (err) {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/index.ts
new file mode 100644
index 0000000000000..45c5729535e58
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/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 { registerPermissionsRoute } from './register_permissions_route';
+import { registerStatsRoute } from './register_stats_route';
+
+export function registerCrossClusterReplicationRoutes(dependencies: RouteDependencies) {
+ registerPermissionsRoute(dependencies);
+ registerStatsRoute(dependencies);
+}
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts
new file mode 100644
index 0000000000000..b8eb5ae14750e
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 whether the user has CCR permissions
+ */
+export const registerPermissionsRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ router.get(
+ {
+ path: addBasePath('/permissions'),
+ validate: false,
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ if (!license.isEsSecurityEnabled) {
+ // If security has been disabled in elasticsearch.yml. we'll just let the user use CCR
+ // because permissions are irrelevant.
+ return response.ok({
+ body: {
+ hasPermission: true,
+ missingClusterPrivileges: [],
+ },
+ });
+ }
+
+ try {
+ const {
+ has_all_requested: hasPermission,
+ cluster,
+ } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.permissions', {
+ body: {
+ cluster: ['manage', 'manage_ccr'],
+ },
+ });
+
+ const missingClusterPrivileges = Object.keys(cluster).reduce(
+ (permissions: any, permissionName: any) => {
+ if (!cluster[permissionName]) {
+ permissions.push(permissionName);
+ return permissions;
+ }
+ },
+ [] as any[]
+ );
+
+ return response.ok({
+ body: {
+ hasPermission,
+ missingClusterPrivileges,
+ },
+ });
+ } catch (err) {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts
new file mode 100644
index 0000000000000..d4288cf7303e2
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { deserializeAutoFollowStats } from '../../../lib/ccr_stats_serialization';
+import { RouteDependencies } from '../../../types';
+
+/**
+ * Returns Auto-follow stats
+ */
+export const registerStatsRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ router.get(
+ {
+ path: addBasePath('/stats/auto_follow'),
+ validate: false,
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ try {
+ const {
+ auto_follow_stats: autoFollowStats,
+ } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats');
+
+ return response.ok({
+ body: deserializeAutoFollowStats(autoFollowStats),
+ });
+ } catch (err) {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/index.ts
new file mode 100644
index 0000000000000..f5d8c7a4f5bda
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/index.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { registerFetchRoute } from './register_fetch_route';
+import { registerGetRoute } from './register_get_route';
+import { registerPauseRoute } from './register_pause_route';
+import { registerResumeRoute } from './register_resume_route';
+import { registerUnfollowRoute } from './register_unfollow_route';
+import { registerUpdateRoute } from './register_update_route';
+
+export function registerFollowerIndexRoutes(dependencies: RouteDependencies) {
+ registerCreateRoute(dependencies);
+ registerFetchRoute(dependencies);
+ registerGetRoute(dependencies);
+ registerPauseRoute(dependencies);
+ registerResumeRoute(dependencies);
+ registerUnfollowRoute(dependencies);
+ registerUpdateRoute(dependencies);
+}
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts
new file mode 100644
index 0000000000000..bba82b04ce9a0
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerCreateRoute } from './register_create_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Create follower index', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerCreateRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.post.mock.calls[0][1];
+ });
+
+ it('should return 200 status when follower index is created', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ body: {
+ name: 'follower_index',
+ remoteCluster: 'remote_cluster',
+ leaderIndex: 'leader_index',
+ },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.status).toEqual(200);
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts
new file mode 100644
index 0000000000000..acaeedacfdb2a
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 { serializeFollowerIndex } from '../../../../common/services/follower_index_serialization';
+import { FollowerIndex } from '../../../../common/types';
+import { addBasePath } from '../../../services';
+import { removeEmptyFields } from '../../../../common/services/utils';
+import { RouteDependencies } from '../../../types';
+
+/**
+ * Create a follower index
+ */
+export const registerCreateRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const bodySchema = schema.object({
+ name: schema.string(),
+ remoteCluster: schema.string(),
+ leaderIndex: schema.string(),
+ maxReadRequestOperationCount: schema.maybe(schema.number()),
+ maxOutstandingReadRequests: schema.maybe(schema.number()),
+ maxReadRequestSize: schema.maybe(schema.string()), // byte value
+ maxWriteRequestOperationCount: schema.maybe(schema.number()),
+ maxWriteRequestSize: schema.maybe(schema.string()), // byte value
+ maxOutstandingWriteRequests: schema.maybe(schema.number()),
+ maxWriteBufferCount: schema.maybe(schema.number()),
+ maxWriteBufferSize: schema.maybe(schema.string()), // byte value
+ maxRetryDelay: schema.maybe(schema.string()), // time value
+ readPollTimeout: schema.maybe(schema.string()), // time value
+ });
+
+ router.post(
+ {
+ path: addBasePath('/follower_indices'),
+ validate: {
+ body: bodySchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { name, ...rest } = request.body;
+ const body = removeEmptyFields(serializeFollowerIndex(rest as FollowerIndex));
+
+ try {
+ return response.ok({
+ body: await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.saveFollowerIndex',
+ { name, body }
+ ),
+ });
+ } catch (err) {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts
new file mode 100644
index 0000000000000..151ab84fabf4c
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts
@@ -0,0 +1,160 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerFetchRoute } from './register_fetch_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Fetch all follower indices', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerFetchRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.get.mock.calls[0][1];
+ });
+
+ it('deserializes the response from Elasticsearch', async () => {
+ const ccrInfoMockResponse = {
+ follower_indices: [
+ {
+ follower_index: 'followerIndexName',
+ remote_cluster: 'remoteCluster',
+ leader_index: 'leaderIndex',
+ status: 'active',
+ parameters: {
+ max_read_request_operation_count: 1,
+ max_outstanding_read_requests: 1,
+ max_read_request_size: '1b',
+ max_write_request_operation_count: 1,
+ max_write_request_size: '1b',
+ max_outstanding_write_requests: 1,
+ max_write_buffer_count: 1,
+ max_write_buffer_size: '1b',
+ max_retry_delay: '1s',
+ read_poll_timeout: '1s',
+ },
+ },
+ ],
+ };
+
+ // These stats correlate to the above follower indices.
+ const ccrStatsMockResponse = {
+ follow_stats: {
+ indices: [
+ {
+ index: 'followerIndexName',
+ shards: [
+ {
+ shard_id: 1,
+ leader_index: 'leaderIndex',
+ leader_global_checkpoint: 1,
+ leader_max_seq_no: 1,
+ follower_global_checkpoint: 1,
+ follower_max_seq_no: 1,
+ last_requested_seq_no: 1,
+ outstanding_read_requests: 1,
+ outstanding_write_requests: 1,
+ write_buffer_operation_count: 1,
+ write_buffer_size_in_bytes: 1,
+ follower_mapping_version: 1,
+ follower_settings_version: 1,
+ total_read_time_millis: 1,
+ total_read_remote_exec_time_millis: 1,
+ successful_read_requests: 1,
+ failed_read_requests: 1,
+ operations_read: 1,
+ bytes_read: 1,
+ total_write_time_millis: 1,
+ successful_write_requests: 1,
+ failed_write_requests: 1,
+ operations_written: 1,
+ read_exceptions: 1,
+ time_since_last_read_millis: 1,
+ },
+ ],
+ },
+ ],
+ },
+ };
+
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce(ccrInfoMockResponse)
+ .mockResolvedValueOnce(ccrStatsMockResponse),
+ });
+
+ const request = httpServerMock.createKibanaRequest();
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+
+ expect(response.payload.indices).toEqual([
+ {
+ name: 'followerIndexName',
+ remoteCluster: 'remoteCluster',
+ leaderIndex: 'leaderIndex',
+ status: 'active',
+ maxReadRequestOperationCount: 1,
+ maxOutstandingReadRequests: 1,
+ maxReadRequestSize: '1b',
+ maxWriteRequestOperationCount: 1,
+ maxWriteRequestSize: '1b',
+ maxOutstandingWriteRequests: 1,
+ maxWriteBufferCount: 1,
+ maxWriteBufferSize: '1b',
+ maxRetryDelay: '1s',
+ readPollTimeout: '1s',
+ shards: [
+ {
+ id: 1,
+ leaderIndex: 'leaderIndex',
+ leaderGlobalCheckpoint: 1,
+ leaderMaxSequenceNum: 1,
+ followerGlobalCheckpoint: 1,
+ followerMaxSequenceNum: 1,
+ lastRequestedSequenceNum: 1,
+ outstandingReadRequestsCount: 1,
+ outstandingWriteRequestsCount: 1,
+ writeBufferOperationsCount: 1,
+ writeBufferSizeBytes: 1,
+ followerMappingVersion: 1,
+ followerSettingsVersion: 1,
+ totalReadTimeMs: 1,
+ totalReadRemoteExecTimeMs: 1,
+ successfulReadRequestCount: 1,
+ failedReadRequestsCount: 1,
+ operationsReadCount: 1,
+ bytesReadCount: 1,
+ totalWriteTimeMs: 1,
+ successfulWriteRequestsCount: 1,
+ failedWriteRequestsCount: 1,
+ operationsWrittenCount: 1,
+ readExceptions: 1,
+ timeSinceLastReadMs: 1,
+ },
+ ],
+ },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts
new file mode 100644
index 0000000000000..a78901ce174e4
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { deserializeListFollowerIndices } from '../../../../common/services/follower_index_serialization';
+import { addBasePath } from '../../../services';
+import { RouteDependencies } from '../../../types';
+
+/**
+ * Returns a list of all follower indices
+ */
+export const registerFetchRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ router.get(
+ {
+ path: addBasePath('/follower_indices'),
+ validate: false,
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ try {
+ const {
+ follower_indices: followerIndices,
+ } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', {
+ id: '_all',
+ });
+
+ const {
+ follow_stats: { indices: followerIndicesStats },
+ } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats');
+
+ const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => {
+ map[stats.index] = stats;
+ return map;
+ }, {});
+
+ const collatedFollowerIndices = followerIndices.map((followerIndex: any) => {
+ return {
+ ...followerIndex,
+ ...followerIndicesStatsMap[followerIndex.follower_index],
+ };
+ });
+
+ return response.ok({
+ body: {
+ indices: deserializeListFollowerIndices(collatedFollowerIndices),
+ },
+ });
+ } catch (err) {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts
new file mode 100644
index 0000000000000..42d04ca65b1cb
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts
@@ -0,0 +1,159 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerGetRoute } from './register_get_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Get one follower index', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerGetRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.get.mock.calls[0][1];
+ });
+
+ it('should return a single resource even though ES returns an array with 1 item', async () => {
+ const ccrInfoMockResponse = {
+ follower_indices: [
+ {
+ follower_index: 'followerIndexName',
+ remote_cluster: 'remoteCluster',
+ leader_index: 'leaderIndex',
+ status: 'active',
+ parameters: {
+ max_read_request_operation_count: 1,
+ max_outstanding_read_requests: 1,
+ max_read_request_size: '1b',
+ max_write_request_operation_count: 1,
+ max_write_request_size: '1b',
+ max_outstanding_write_requests: 1,
+ max_write_buffer_count: 1,
+ max_write_buffer_size: '1b',
+ max_retry_delay: '1s',
+ read_poll_timeout: '1s',
+ },
+ },
+ ],
+ };
+
+ // These stats correlate to the above follower indices.
+ const ccrFollowerIndexStatsMockResponse = {
+ indices: [
+ {
+ index: 'followerIndexName',
+ shards: [
+ {
+ shard_id: 1,
+ leader_index: 'leaderIndex',
+ leader_global_checkpoint: 1,
+ leader_max_seq_no: 1,
+ follower_global_checkpoint: 1,
+ follower_max_seq_no: 1,
+ last_requested_seq_no: 1,
+ outstanding_read_requests: 1,
+ outstanding_write_requests: 1,
+ write_buffer_operation_count: 1,
+ write_buffer_size_in_bytes: 1,
+ follower_mapping_version: 1,
+ follower_settings_version: 1,
+ total_read_time_millis: 1,
+ total_read_remote_exec_time_millis: 1,
+ successful_read_requests: 1,
+ failed_read_requests: 1,
+ operations_read: 1,
+ bytes_read: 1,
+ total_write_time_millis: 1,
+ successful_write_requests: 1,
+ failed_write_requests: 1,
+ operations_written: 1,
+ read_exceptions: 1,
+ time_since_last_read_millis: 1,
+ },
+ ],
+ },
+ ],
+ };
+
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce(ccrInfoMockResponse)
+ .mockResolvedValueOnce(ccrFollowerIndexStatsMockResponse),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'doesnt_matter' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+
+ expect(response.payload).toEqual({
+ name: 'followerIndexName',
+ remoteCluster: 'remoteCluster',
+ leaderIndex: 'leaderIndex',
+ status: 'active',
+ maxReadRequestOperationCount: 1,
+ maxOutstandingReadRequests: 1,
+ maxReadRequestSize: '1b',
+ maxWriteRequestOperationCount: 1,
+ maxWriteRequestSize: '1b',
+ maxOutstandingWriteRequests: 1,
+ maxWriteBufferCount: 1,
+ maxWriteBufferSize: '1b',
+ maxRetryDelay: '1s',
+ readPollTimeout: '1s',
+ shards: [
+ {
+ id: 1,
+ leaderIndex: 'leaderIndex',
+ leaderGlobalCheckpoint: 1,
+ leaderMaxSequenceNum: 1,
+ followerGlobalCheckpoint: 1,
+ followerMaxSequenceNum: 1,
+ lastRequestedSequenceNum: 1,
+ outstandingReadRequestsCount: 1,
+ outstandingWriteRequestsCount: 1,
+ writeBufferOperationsCount: 1,
+ writeBufferSizeBytes: 1,
+ followerMappingVersion: 1,
+ followerSettingsVersion: 1,
+ totalReadTimeMs: 1,
+ totalReadRemoteExecTimeMs: 1,
+ successfulReadRequestCount: 1,
+ failedReadRequestsCount: 1,
+ operationsReadCount: 1,
+ bytesReadCount: 1,
+ totalWriteTimeMs: 1,
+ successfulWriteRequestsCount: 1,
+ failedWriteRequestsCount: 1,
+ operationsWrittenCount: 1,
+ readExceptions: 1,
+ timeSinceLastReadMs: 1,
+ },
+ ],
+ });
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts
new file mode 100644
index 0000000000000..98a182fc15681
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts
@@ -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.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { deserializeFollowerIndex } from '../../../../common/services/follower_index_serialization';
+import { addBasePath } from '../../../services';
+import { RouteDependencies } from '../../../types';
+
+/**
+ * Returns a single follower index pattern
+ */
+export const registerGetRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const paramsSchema = schema.object({
+ id: schema.string(),
+ });
+
+ router.get(
+ {
+ path: addBasePath('/follower_indices/{id}'),
+ validate: {
+ params: paramsSchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id } = request.params;
+
+ try {
+ const {
+ follower_indices: followerIndices,
+ } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { id });
+
+ const followerIndexInfo = followerIndices && followerIndices[0];
+
+ if (!followerIndexInfo) {
+ return response.notFound({
+ body: `The follower index "${id}" does not exist.`,
+ });
+ }
+
+ // If this follower is paused, skip call to ES stats api since it will return 404
+ if (followerIndexInfo.status === 'paused') {
+ return response.ok({
+ body: deserializeFollowerIndex({
+ ...followerIndexInfo,
+ }),
+ });
+ } else {
+ const {
+ indices: followerIndicesStats,
+ } = await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.followerIndexStats',
+ { id }
+ );
+
+ return response.ok({
+ body: deserializeFollowerIndex({
+ ...followerIndexInfo,
+ ...(followerIndicesStats ? followerIndicesStats[0] : {}),
+ }),
+ });
+ }
+ } catch (err) {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts
new file mode 100644
index 0000000000000..82cb88cbacea7
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerPauseRoute } from './register_pause_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Pause follower index/indices', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerPauseRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.put.mock.calls[0][1];
+ });
+
+ it('pauses a single item', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsPaused).toEqual(['a']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('pauses multiple items', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b,c' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsPaused).toEqual(['a', 'b', 'c']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('returns partial errors', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockRejectedValueOnce({ response: { error: {} } }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsPaused).toEqual(['a']);
+ expect(response.payload.errors[0].id).toEqual('b');
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts
new file mode 100644
index 0000000000000..7432ea7ca5c82
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts
@@ -0,0 +1,64 @@
+/*
+ * 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';
+
+/**
+ * Pauses a follower index
+ */
+export const registerPauseRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const paramsSchema = schema.object({ id: schema.string() });
+
+ router.put(
+ {
+ path: addBasePath('/follower_indices/{id}/pause'),
+ validate: {
+ params: paramsSchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsPaused: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ const formatError = (err: any) => {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ };
+
+ await Promise.all(
+ ids.map((_id: string) =>
+ context
+ .crossClusterReplication!.client.callAsCurrentUser('ccr.pauseFollowerIndex', {
+ id: _id,
+ })
+ .then(() => itemsPaused.push(_id))
+ .catch(err => {
+ errors.push({ id: _id, error: formatError(err) });
+ })
+ )
+ );
+
+ return response.ok({
+ body: {
+ itemsPaused,
+ errors,
+ },
+ });
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts
new file mode 100644
index 0000000000000..04167c5db3162
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerResumeRoute } from './register_resume_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Resume follower index/indices', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerResumeRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.put.mock.calls[0][1];
+ });
+
+ it('resumes a single item', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsResumed).toEqual(['a']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('resumes multiple items', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b,c' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsResumed).toEqual(['a', 'b', 'c']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('returns partial errors', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockRejectedValueOnce({ response: { error: {} } }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsResumed).toEqual(['a']);
+ expect(response.payload.errors[0].id).toEqual('b');
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts
new file mode 100644
index 0000000000000..ca8f3a9f5fe9d
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts
@@ -0,0 +1,64 @@
+/*
+ * 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';
+
+/**
+ * Resumes a follower index
+ */
+export const registerResumeRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const paramsSchema = schema.object({ id: schema.string() });
+
+ router.put(
+ {
+ path: addBasePath('/follower_indices/{id}/resume'),
+ validate: {
+ params: paramsSchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsResumed: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ const formatError = (err: any) => {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ };
+
+ await Promise.all(
+ ids.map((_id: string) =>
+ context
+ .crossClusterReplication!.client.callAsCurrentUser('ccr.resumeFollowerIndex', {
+ id: _id,
+ })
+ .then(() => itemsResumed.push(_id))
+ .catch(err => {
+ errors.push({ id: _id, error: formatError(err) });
+ })
+ )
+ );
+
+ return response.ok({
+ body: {
+ itemsResumed,
+ errors,
+ },
+ });
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts
new file mode 100644
index 0000000000000..6302d5868b0db
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
+
+import { isEsError } from '../../../lib/is_es_error';
+import { formatEsError } from '../../../lib/format_es_error';
+import { License } from '../../../services';
+import { mockRouteContext } from '../test_lib';
+import { registerUnfollowRoute } from './register_unfollow_route';
+
+const httpService = httpServiceMock.createSetupContract();
+
+describe('[CCR API] Unfollow follower index/indices', () => {
+ let routeHandler: RequestHandler;
+
+ beforeEach(() => {
+ const router = httpService.createRouter('') as jest.Mocked;
+
+ registerUnfollowRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.put.mock.calls[0][1];
+ });
+
+ it('unfollows a single item', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsUnfollowed).toEqual(['a']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('unfollows multiple items', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ // a
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ // b
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ // c
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b,c' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsUnfollowed).toEqual(['a', 'b', 'c']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('returns partial errors', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ // a
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ // b
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockRejectedValueOnce({ response: { error: {} } }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsUnfollowed).toEqual(['a']);
+ expect(response.payload.errors[0].id).toEqual('b');
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts
new file mode 100644
index 0000000000000..282fead02bbe0
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts
@@ -0,0 +1,95 @@
+/*
+ * 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';
+
+/**
+ * Unfollow follower index's leader index
+ */
+export const registerUnfollowRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const paramsSchema = schema.object({ id: schema.string() });
+
+ router.put(
+ {
+ path: addBasePath('/follower_indices/{id}/unfollow'),
+ validate: {
+ params: paramsSchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsUnfollowed: string[] = [];
+ const itemsNotOpen: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ const formatError = (err: any) => {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ };
+
+ await Promise.all(
+ ids.map(async (_id: string) => {
+ try {
+ // Try to pause follower, let it fail silently since it may already be paused
+ try {
+ await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.pauseFollowerIndex',
+ { id: _id }
+ );
+ } catch (e) {
+ // Swallow errors
+ }
+
+ // Close index
+ await context.crossClusterReplication!.client.callAsCurrentUser('indices.close', {
+ index: _id,
+ });
+
+ // Unfollow leader
+ await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.unfollowLeaderIndex',
+ { id: _id }
+ );
+
+ // Try to re-open the index, store failures in a separate array to surface warnings in the UI
+ // This will allow users to query their index normally after unfollowing
+ try {
+ await context.crossClusterReplication!.client.callAsCurrentUser('indices.open', {
+ index: _id,
+ });
+ } catch (e) {
+ itemsNotOpen.push(_id);
+ }
+
+ // Push success
+ itemsUnfollowed.push(_id);
+ } catch (err) {
+ errors.push({ id: _id, error: formatError(err) });
+ }
+ })
+ );
+
+ return response.ok({
+ body: {
+ itemsUnfollowed,
+ itemsNotOpen,
+ errors,
+ },
+ });
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts
new file mode 100644
index 0000000000000..521de77180974
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.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 { schema } from '@kbn/config-schema';
+import { serializeAdvancedSettings } from '../../../../common/services/follower_index_serialization';
+import { FollowerIndexAdvancedSettings } from '../../../../common/types';
+import { removeEmptyFields } from '../../../../common/services/utils';
+import { addBasePath } from '../../../services';
+import { RouteDependencies } from '../../../types';
+
+/**
+ * Update a follower index
+ */
+export const registerUpdateRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const paramsSchema = schema.object({ id: schema.string() });
+
+ const bodySchema = schema.object({
+ maxReadRequestOperationCount: schema.maybe(schema.number()),
+ maxOutstandingReadRequests: schema.maybe(schema.number()),
+ maxReadRequestSize: schema.maybe(schema.string()), // byte value
+ maxWriteRequestOperationCount: schema.maybe(schema.number()),
+ maxWriteRequestSize: schema.maybe(schema.string()), // byte value
+ maxOutstandingWriteRequests: schema.maybe(schema.number()),
+ maxWriteBufferCount: schema.maybe(schema.number()),
+ maxWriteBufferSize: schema.maybe(schema.string()), // byte value
+ maxRetryDelay: schema.maybe(schema.string()), // time value
+ readPollTimeout: schema.maybe(schema.string()), // time value
+ });
+
+ router.put(
+ {
+ path: addBasePath('/follower_indices/{id}'),
+ validate: {
+ params: paramsSchema,
+ body: bodySchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id } = request.params;
+
+ // We need to first pause the follower and then resume it by passing the advanced settings
+ try {
+ const {
+ follower_indices: followerIndices,
+ } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { id });
+
+ const followerIndexInfo = followerIndices && followerIndices[0];
+
+ if (!followerIndexInfo) {
+ return response.notFound({ body: `The follower index "${id}" does not exist.` });
+ }
+
+ // Retrieve paused state instead of pulling it from the payload to ensure it's not stale.
+ const isPaused = followerIndexInfo.status === 'paused';
+
+ // Pause follower if not already paused
+ if (!isPaused) {
+ await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.pauseFollowerIndex',
+ {
+ id,
+ }
+ );
+ }
+
+ // Resume follower
+ const body = removeEmptyFields(
+ serializeAdvancedSettings(request.body as FollowerIndexAdvancedSettings)
+ );
+
+ return response.ok({
+ body: await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.resumeFollowerIndex',
+ { id, body }
+ ),
+ });
+ } catch (err) {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts
new file mode 100644
index 0000000000000..9b4fb134ed230
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts
@@ -0,0 +1,23 @@
+/*
+ * 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 { RequestHandlerContext } from 'src/core/server';
+
+export function mockRouteContext({
+ callAsCurrentUser,
+}: {
+ callAsCurrentUser: any;
+}): RequestHandlerContext {
+ const routeContextMock = ({
+ crossClusterReplication: {
+ client: {
+ callAsCurrentUser,
+ },
+ },
+ } as unknown) as RequestHandlerContext;
+
+ return routeContextMock;
+}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts b/x-pack/plugins/cross_cluster_replication/server/routes/index.ts
similarity index 52%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts
rename to x-pack/plugins/cross_cluster_replication/server/routes/index.ts
index 7e59417550691..84abfb369e002 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/index.ts
@@ -4,13 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { RouteDependencies } from '../types';
+
import { registerAutoFollowPatternRoutes } from './api/auto_follow_pattern';
import { registerFollowerIndexRoutes } from './api/follower_index';
-import { registerCcrRoutes } from './api/ccr';
-import { RouteDependencies } from './types';
+import { registerCrossClusterReplicationRoutes } from './api/cross_cluster_replication';
-export function registerRoutes(deps: RouteDependencies) {
- registerAutoFollowPatternRoutes(deps);
- registerFollowerIndexRoutes(deps);
- registerCcrRoutes(deps);
+export function registerApiRoutes(dependencies: RouteDependencies) {
+ registerAutoFollowPatternRoutes(dependencies);
+ registerFollowerIndexRoutes(dependencies);
+ registerCrossClusterReplicationRoutes(dependencies);
}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts b/x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts
similarity index 64%
rename from x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts
rename to x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts
index bd5bb50514c01..3f3dd131df7c7 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts
@@ -4,6 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const PLUGIN = {
- ID: 'cross_cluster_replication',
-};
+import { API_BASE_PATH } from '../../common/constants';
+
+export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`;
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts b/x-pack/plugins/cross_cluster_replication/server/services/index.ts
similarity index 74%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts
rename to x-pack/plugins/cross_cluster_replication/server/services/index.ts
index 0743e443955f4..d7b544b290c39 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/services/index.ts
@@ -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 { License } from './license';
+export { addBasePath } from './add_base_path';
diff --git a/x-pack/plugins/cross_cluster_replication/server/services/license.ts b/x-pack/plugins/cross_cluster_replication/server/services/license.ts
new file mode 100644
index 0000000000000..bfd357867c3e2
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/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/plugins/cross_cluster_replication/server/types.ts b/x-pack/plugins/cross_cluster_replication/server/types.ts
new file mode 100644
index 0000000000000..049d440e3d85d
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/types.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 } from 'src/core/server';
+import { LicensingPluginSetup } from '../../licensing/server';
+import { IndexManagementPluginSetup } from '../../index_management/server';
+import { RemoteClustersPluginSetup } from '../../remote_clusters/server';
+import { License } from './services';
+import { isEsError } from './lib/is_es_error';
+import { formatEsError } from './lib/format_es_error';
+
+export interface Dependencies {
+ licensing: LicensingPluginSetup;
+ indexManagement: IndexManagementPluginSetup;
+ remoteClusters: RemoteClustersPluginSetup;
+}
+
+export interface RouteDependencies {
+ router: IRouter;
+ license: License;
+ lib: {
+ isEsError: typeof isEsError;
+ formatEsError: typeof formatEsError;
+ };
+}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts
index 7d24bc31006b4..aa3ac9ea75c22 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts
@@ -4,10 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export let toasts: any;
-export let fatalErrors: any;
+import { IToasts, FatalErrorsSetup } from 'src/core/public';
-export function init(_toasts: any, _fatalErrors: any): void {
+export let toasts: IToasts;
+export let fatalErrors: FatalErrorsSetup;
+
+export function init(_toasts: IToasts, _fatalErrors: FatalErrorsSetup): void {
toasts = _toasts;
fatalErrors = _fatalErrors;
}
diff --git a/x-pack/plugins/remote_clusters/public/index.ts b/x-pack/plugins/remote_clusters/public/index.ts
index 6ba021b157c3e..127ec2a670645 100644
--- a/x-pack/plugins/remote_clusters/public/index.ts
+++ b/x-pack/plugins/remote_clusters/public/index.ts
@@ -3,8 +3,11 @@
* 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 'kibana/public';
import { RemoteClustersUIPlugin } from './plugin';
+export { RemoteClustersPluginSetup } from './plugin';
+
export const plugin = (initializerContext: PluginInitializerContext) =>
new RemoteClustersUIPlugin(initializerContext);
diff --git a/x-pack/plugins/remote_clusters/public/plugin.ts b/x-pack/plugins/remote_clusters/public/plugin.ts
index d110c461c1e3f..22f98e94748d8 100644
--- a/x-pack/plugins/remote_clusters/public/plugin.ts
+++ b/x-pack/plugins/remote_clusters/public/plugin.ts
@@ -14,7 +14,12 @@ import { init as initNotification } from './application/services/notification';
import { init as initRedirect } from './application/services/redirect';
import { Dependencies, ClientConfigType } from './types';
-export class RemoteClustersUIPlugin implements Plugin {
+export interface RemoteClustersPluginSetup {
+ isUiEnabled: boolean;
+}
+
+export class RemoteClustersUIPlugin
+ implements Plugin {
constructor(private readonly initializerContext: PluginInitializerContext) {}
setup(
@@ -55,6 +60,10 @@ export class RemoteClustersUIPlugin implements Plugin new RemoteClustersServerPlugin(ctx);
diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts
index b86f16228878a..4b22be3c43b86 100644
--- a/x-pack/plugins/remote_clusters/server/plugin.ts
+++ b/x-pack/plugins/remote_clusters/server/plugin.ts
@@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n';
import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server';
import { Observable } from 'rxjs';
+import { first } from 'rxjs/operators';
import { PLUGIN } from '../common/constants';
import { Dependencies, LicenseStatus, RouteDependencies } from './types';
@@ -18,14 +19,19 @@ import {
registerDeleteRoute,
} from './routes/api';
-export class RemoteClustersServerPlugin implements Plugin {
+export interface RemoteClustersPluginSetup {
+ isUiEnabled: boolean;
+}
+
+export class RemoteClustersServerPlugin
+ implements Plugin {
licenseStatus: LicenseStatus;
log: Logger;
- config: Observable;
+ config$: Observable;
constructor({ logger, config }: PluginInitializerContext) {
this.log = logger.get();
- this.config = config.create();
+ this.config$ = config.create();
this.licenseStatus = { valid: false };
}
@@ -35,6 +41,8 @@ export class RemoteClustersServerPlugin implements Plugin
) {
const elasticsearch = await elasticsearchService.adminClient;
const router = http.createRouter();
+ const config = await this.config$.pipe(first()).toPromise();
+
const routeDependencies: RouteDependencies = {
elasticsearch,
elasticsearchService,
@@ -70,6 +78,10 @@ export class RemoteClustersServerPlugin implements Plugin
}
}
});
+
+ return {
+ isUiEnabled: config.ui.enabled,
+ };
}
start() {}
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 877f8163c44b9..e3acaa58bd988 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -6106,9 +6106,6 @@
"xpack.crossClusterReplication.autoFollowPatternList.table.statusTextPaused": "一時停止中",
"xpack.crossClusterReplication.autoFollowPatternList.table.statusTitle": "ステータス",
"xpack.crossClusterReplication.autoFollowPatternList.table.suffixColumnTitle": "フォロワーインデックスの接尾辞",
- "xpack.crossClusterReplication.checkLicense.errorExpiredMessage": "{licenseType} ライセンスが期限切れのため {pluginName} を使用できません",
- "xpack.crossClusterReplication.checkLicense.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。",
- "xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。",
"xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.cancelButtonText": "キャンセル",
"xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.confirmButtonText": "削除",
"xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.deleteMultipleTitle": "{count} 個の自動フォローパターンを削除しますか?",
@@ -16765,4 +16762,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。",
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 2aac5dd24a1b3..42d17b3b6eade 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -6108,9 +6108,6 @@
"xpack.crossClusterReplication.autoFollowPatternList.table.statusTextPaused": "已暂停",
"xpack.crossClusterReplication.autoFollowPatternList.table.statusTitle": "状态",
"xpack.crossClusterReplication.autoFollowPatternList.table.suffixColumnTitle": "Follower 索引后缀",
- "xpack.crossClusterReplication.checkLicense.errorExpiredMessage": "您不能使用 {pluginName},因为您的 {licenseType} 许可证已过期",
- "xpack.crossClusterReplication.checkLicense.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。",
- "xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage": "您的 {licenseType} 许可证不支持 {pluginName}。请升级您的许可。",
"xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.cancelButtonText": "取消",
"xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.confirmButtonText": "删除",
"xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.deleteMultipleTitle": "是否删除 {count} 个自动跟随模式?",
@@ -16770,4 +16767,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。",
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js
index 22f0bde50b073..b9a0bfd40a8d6 100644
--- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js
+++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js
@@ -5,33 +5,27 @@
*/
import { API_BASE_PATH } from './constants';
-import { getRandomString } from './lib';
-import { getAutoFollowIndexPayload } from './fixtures';
export const registerHelpers = supertest => {
let autoFollowPatternsCreated = [];
const loadAutoFollowPatterns = () => supertest.get(`${API_BASE_PATH}/auto_follow_patterns`);
- const getAutoFollowPattern = name =>
- supertest.get(`${API_BASE_PATH}/auto_follow_patterns/${name}`);
+ const getAutoFollowPattern = id => supertest.get(`${API_BASE_PATH}/auto_follow_patterns/${id}`);
- const createAutoFollowPattern = (
- name = getRandomString(),
- payload = getAutoFollowIndexPayload()
- ) => {
- autoFollowPatternsCreated.push(name);
+ const createAutoFollowPattern = payload => {
+ autoFollowPatternsCreated.push(payload.id);
return supertest
.post(`${API_BASE_PATH}/auto_follow_patterns`)
.set('kbn-xsrf', 'xxx')
- .send({ ...payload, id: name });
+ .send(payload);
};
- const deleteAutoFollowPattern = name => {
- autoFollowPatternsCreated = autoFollowPatternsCreated.filter(c => c !== name);
+ const deleteAutoFollowPattern = id => {
+ autoFollowPatternsCreated = autoFollowPatternsCreated.filter(c => c !== id);
- return supertest.delete(`${API_BASE_PATH}/auto_follow_patterns/${name}`).set('kbn-xsrf', 'xxx');
+ return supertest.delete(`${API_BASE_PATH}/auto_follow_patterns/${id}`).set('kbn-xsrf', 'xxx');
};
const deleteAllAutoFollowPatterns = () =>
diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js
index 3efb4d6600f7f..7a95ba7fcd981 100644
--- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js
+++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js
@@ -6,8 +6,7 @@
import expect from '@kbn/expect';
-import { getRandomString } from './lib';
-import { getAutoFollowIndexPayload } from './fixtures';
+import { REMOTE_CLUSTER_NAME } from './constants';
import { registerHelpers as registerRemoteClustersHelpers } from './remote_clusters.helpers';
import { registerHelpers as registerAutoFollowPatternHelpers } from './auto_follow_pattern.helpers';
@@ -37,44 +36,60 @@ export default function({ getService }) {
describe('when remote cluster does not exist', () => {
it('should throw a 404 error when cluster is unknown', async () => {
- const payload = getAutoFollowIndexPayload();
- payload.remoteCluster = 'unknown-cluster';
+ const { body } = await createAutoFollowPattern({
+ id: 'pattern0',
+ remoteCluster: 'unknown-cluster',
+ leaderIndexPatterns: ['leader-*'],
+ followIndexPattern: '{{leader_index}}_follower',
+ });
- const { body } = await createAutoFollowPattern(undefined, payload).expect(404);
+ expect(body.statusCode).to.be(404);
expect(body.attributes.cause[0]).to.contain('no such remote cluster');
});
});
describe('when remote cluster exists', () => {
- before(() => addCluster());
+ before(async () => addCluster());
describe('create()', () => {
it('should create an auto-follow pattern when cluster is known', async () => {
- const name = getRandomString();
- const { body } = await createAutoFollowPattern(name).expect(200);
- console.log(body);
-
+ const { body, statusCode } = await createAutoFollowPattern({
+ id: 'pattern1',
+ remoteCluster: REMOTE_CLUSTER_NAME,
+ leaderIndexPatterns: ['leader-*'],
+ followIndexPattern: '{{leader_index}}_follower',
+ });
+
+ expect(statusCode).to.be(200);
expect(body.acknowledged).to.eql(true);
});
});
describe('get()', () => {
it('should return a 404 when the auto-follow pattern is not found', async () => {
- const name = getRandomString();
- const { body } = await getAutoFollowPattern(name).expect(404);
-
+ const { body } = await getAutoFollowPattern('missing-pattern');
+ expect(body.statusCode).to.be(404);
expect(body.attributes.cause).not.to.be(undefined);
});
it('should return an auto-follow pattern that was created', async () => {
- const name = getRandomString();
- const autoFollowPattern = getAutoFollowIndexPayload();
-
- await createAutoFollowPattern(name, autoFollowPattern);
-
- const { body } = await getAutoFollowPattern(name).expect(200);
-
- expect(body).to.eql({ ...autoFollowPattern, name });
+ await createAutoFollowPattern({
+ id: 'pattern2',
+ remoteCluster: REMOTE_CLUSTER_NAME,
+ leaderIndexPatterns: ['leader-*'],
+ followIndexPattern: '{{leader_index}}_follower',
+ });
+
+ const { body, statusCode } = await getAutoFollowPattern('pattern2');
+
+ expect(statusCode).to.be(200);
+ expect(body).to.eql({
+ name: 'pattern2',
+ remoteCluster: REMOTE_CLUSTER_NAME,
+ active: true,
+ leaderIndexPatterns: ['leader-*'],
+ followIndexPattern: '{{leader_index}}_follower',
+ });
});
});
});
diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js
index de47f5d9ea85e..6e254b27356f2 100644
--- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js
+++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js
@@ -7,13 +7,6 @@
import { REMOTE_CLUSTER_NAME } from './constants';
import { getRandomString } from './lib';
-export const getAutoFollowIndexPayload = (remoteCluster = REMOTE_CLUSTER_NAME, active = true) => ({
- active,
- remoteCluster,
- leaderIndexPatterns: ['leader-*'],
- followIndexPattern: '{{leader_index}}_follower',
-});
-
export const getFollowerIndexPayload = (
leaderIndexName = getRandomString(),
remoteCluster = REMOTE_CLUSTER_NAME,
diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js
index eabf474120f2b..d03b1f83fb404 100644
--- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js
+++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js
@@ -6,7 +6,7 @@
import expect from '@kbn/expect';
-import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../../legacy/plugins/cross_cluster_replication/common/constants';
+import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../../plugins/cross_cluster_replication/common/constants';
import { getFollowerIndexPayload } from './fixtures';
import { registerHelpers as registerElasticSearchHelpers, getRandomString } from './lib';
import { registerHelpers as registerRemoteClustersHelpers } from './remote_clusters.helpers';
@@ -57,7 +57,8 @@ export default function({ getService }) {
expect(body.attributes.cause[0]).to.contain('no such index');
});
- it('should create a follower index that follows an existing remote index', async () => {
+ // NOTE: If this test fails locally it's probably because you have another cluster running.
+ it('should create a follower index that follows an existing leader index', async () => {
// First let's create an index to follow
const leaderIndex = await createIndex();
@@ -65,7 +66,7 @@ export default function({ getService }) {
const { body } = await createFollowerIndex(undefined, payload).expect(200);
// There is a race condition in which Elasticsearch can respond without acknowledging,
- // i.e. `body .follow_index_shards_acked` is sometimes true and sometimes false.
+ // i.e. `body.follow_index_shards_acked` is sometimes true and sometimes false.
// By only asserting that `follow_index_created` is true, we eliminate this flakiness.
expect(body.follow_index_created).to.eql(true);
});
@@ -79,6 +80,7 @@ export default function({ getService }) {
expect(body.attributes.cause[0]).to.contain('no such index');
});
+ // NOTE: If this test fails locally it's probably because you have another cluster running.
it('should return a follower index that was created', async () => {
const leaderIndex = await createIndex();
diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js
index 7195b8680a286..d2d07eca475e7 100644
--- a/x-pack/test/api_integration/apis/management/index_management/indices.js
+++ b/x-pack/test/api_integration/apis/management/index_management/indices.js
@@ -193,10 +193,10 @@ export default function({ getService }) {
'size',
'isFrozen',
'aliases',
- 'ilm', // data enricher
- 'isRollupIndex', // data enricher
// Cloud disables CCR, so wouldn't expect follower indices.
'isFollowerIndex', // data enricher
+ 'ilm', // data enricher
+ 'isRollupIndex', // data enricher
];
expect(Object.keys(body[0])).to.eql(expectedKeys);
});
@@ -219,10 +219,10 @@ export default function({ getService }) {
'size',
'isFrozen',
'aliases',
- 'ilm', // data enricher
- 'isRollupIndex', // data enricher
// Cloud disables CCR, so wouldn't expect follower indices.
'isFollowerIndex', // data enricher
+ 'ilm', // data enricher
+ 'isRollupIndex', // data enricher
];
expect(Object.keys(body[0])).to.eql(expectedKeys);
expect(body.length > 1).to.be(true); // to contrast it with the next test