mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[CCR] Remote Clusters and Cross-cluster Replication apps (#26777)
This commit is contained in:
parent
9df85816c0
commit
2371e58590
222 changed files with 11540 additions and 6 deletions
|
@ -18,6 +18,7 @@
|
|||
"tsvb": "src/legacy/core_plugins/metrics",
|
||||
"xpack.apm": "x-pack/plugins/apm",
|
||||
"xpack.beatsManagement": "x-pack/plugins/beats_management",
|
||||
"xpack.crossClusterReplication": "x-pack/plugins/cross_cluster_replication",
|
||||
"xpack.graph": "x-pack/plugins/graph",
|
||||
"xpack.grokDebugger": "x-pack/plugins/grokdebugger",
|
||||
"xpack.idxMgmt": "x-pack/plugins/index_management",
|
||||
|
@ -26,6 +27,7 @@
|
|||
"xpack.ml": "x-pack/plugins/ml",
|
||||
"xpack.logstash": "x-pack/plugins/logstash",
|
||||
"xpack.monitoring": "x-pack/plugins/monitoring",
|
||||
"xpack.remoteClusters": "x-pack/plugins/remote_clusters",
|
||||
"xpack.reporting": "x-pack/plugins/reporting",
|
||||
"xpack.rollupJobs": "x-pack/plugins/rollup",
|
||||
"xpack.searchProfiler": "x-pack/plugins/searchprofiler",
|
||||
|
|
|
@ -29,3 +29,9 @@ export {
|
|||
INDEX_PATTERN_ILLEGAL_CHARACTERS,
|
||||
INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE,
|
||||
} from './constants';
|
||||
|
||||
export {
|
||||
ILLEGAL_CHARACTERS,
|
||||
CONTAINS_SPACES,
|
||||
validateIndexPattern,
|
||||
} from './validate';
|
||||
|
|
24
src/ui/public/index_patterns/validate/index.js
Normal file
24
src/ui/public/index_patterns/validate/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
ILLEGAL_CHARACTERS,
|
||||
CONTAINS_SPACES,
|
||||
validateIndexPattern,
|
||||
} from './validate_index_pattern';
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from '../constants';
|
||||
|
||||
export const ILLEGAL_CHARACTERS = 'ILLEGAL_CHARACTERS';
|
||||
export const CONTAINS_SPACES = 'CONTAINS_SPACES';
|
||||
|
||||
function findIllegalCharacters(indexPattern) {
|
||||
const illegalCharacters = INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.reduce((chars, char) => {
|
||||
if (indexPattern.includes(char)) {
|
||||
chars.push(char);
|
||||
}
|
||||
|
||||
return chars;
|
||||
}, []);
|
||||
|
||||
return illegalCharacters;
|
||||
}
|
||||
|
||||
function indexPatternContainsSpaces(indexPattern) {
|
||||
return indexPattern.includes(' ');
|
||||
}
|
||||
|
||||
export function validateIndexPattern(indexPattern) {
|
||||
const errors = {};
|
||||
|
||||
const illegalCharacters = findIllegalCharacters(indexPattern);
|
||||
|
||||
if (illegalCharacters.length) {
|
||||
errors[ILLEGAL_CHARACTERS] = illegalCharacters;
|
||||
}
|
||||
|
||||
if (indexPatternContainsSpaces(indexPattern)) {
|
||||
errors[CONTAINS_SPACES] = true;
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from '../constants';
|
||||
|
||||
import {
|
||||
ILLEGAL_CHARACTERS,
|
||||
CONTAINS_SPACES,
|
||||
validateIndexPattern,
|
||||
} from './validate_index_pattern';
|
||||
|
||||
describe('Index Pattern Validation', () => {
|
||||
it('should not allow space in the pattern', () => {
|
||||
const errors = validateIndexPattern('my pattern');
|
||||
expect(errors[CONTAINS_SPACES]).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow illegal characters', () => {
|
||||
INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.forEach((char) => {
|
||||
const errors = validateIndexPattern(`pattern${char}`);
|
||||
expect(errors[ILLEGAL_CHARACTERS]).toEqual([ char ]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty object when there are no errors', () => {
|
||||
expect(validateIndexPattern('my-pattern-*')).toEqual({});
|
||||
});
|
||||
});
|
|
@ -20,3 +20,9 @@
|
|||
export {
|
||||
INDEX_ILLEGAL_CHARACTERS_VISIBLE,
|
||||
} from './constants';
|
||||
|
||||
export {
|
||||
indexNameBeginsWithPeriod,
|
||||
findIllegalCharactersInIndexName,
|
||||
indexNameContainsSpaces,
|
||||
} from './validate';
|
||||
|
|
24
src/ui/public/indices/validate/index.js
Normal file
24
src/ui/public/indices/validate/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
indexNameBeginsWithPeriod,
|
||||
findIllegalCharactersInIndexName,
|
||||
indexNameContainsSpaces,
|
||||
} from './validate_index';
|
41
src/ui/public/indices/validate/validate_index.js
Normal file
41
src/ui/public/indices/validate/validate_index.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from '../constants';
|
||||
|
||||
// Names beginning with periods are reserved for system indices.
|
||||
export function indexNameBeginsWithPeriod(indexName = '') {
|
||||
return indexName[0] === '.';
|
||||
}
|
||||
|
||||
export function findIllegalCharactersInIndexName(indexName) {
|
||||
const illegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.reduce((chars, char) => {
|
||||
if (indexName.includes(char)) {
|
||||
chars.push(char);
|
||||
}
|
||||
|
||||
return chars;
|
||||
}, []);
|
||||
|
||||
return illegalCharacters;
|
||||
}
|
||||
|
||||
export function indexNameContainsSpaces(indexName) {
|
||||
return indexName.includes(' ');
|
||||
}
|
53
src/ui/public/indices/validate/validate_index.test.js
Normal file
53
src/ui/public/indices/validate/validate_index.test.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from '../constants';
|
||||
|
||||
import {
|
||||
indexNameBeginsWithPeriod,
|
||||
findIllegalCharactersInIndexName,
|
||||
indexNameContainsSpaces,
|
||||
} from './validate_index';
|
||||
|
||||
jest.mock('ui/index_patterns/index_patterns.js', () => ({
|
||||
IndexPatternsProvider: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('ui/index_patterns/index_patterns_api_client_provider.js', () => ({
|
||||
IndexPatternsApiClientProvider: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Index name validation', () => {
|
||||
it('should not allow name to begin with a period', () => {
|
||||
const beginsWithPeriod = indexNameBeginsWithPeriod('.system_index');
|
||||
expect(beginsWithPeriod).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow space in the name', () => {
|
||||
const containsSpaces = indexNameContainsSpaces('my name');
|
||||
expect(containsSpaces).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow illegal characters', () => {
|
||||
INDEX_ILLEGAL_CHARACTERS_VISIBLE.forEach((char) => {
|
||||
const illegalCharacters = findIllegalCharactersInIndexName(`name${char}`);
|
||||
expect(illegalCharacters).toEqual([ char ]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -29,6 +29,8 @@ import { kueryAutocomplete } from './plugins/kuery_autocomplete';
|
|||
import { canvas } from './plugins/canvas';
|
||||
import { infra } from './plugins/infra';
|
||||
import { rollup } from './plugins/rollup';
|
||||
import { remoteClusters } from './plugins/remote_clusters';
|
||||
import { crossClusterReplication } from './plugins/cross_cluster_replication';
|
||||
import { upgradeAssistant } from './plugins/upgrade_assistant';
|
||||
|
||||
module.exports = function (kibana) {
|
||||
|
@ -58,6 +60,8 @@ module.exports = function (kibana) {
|
|||
kueryAutocomplete(kibana),
|
||||
infra(kibana),
|
||||
rollup(kibana),
|
||||
remoteClusters(kibana),
|
||||
crossClusterReplication(kibana),
|
||||
upgradeAssistant(kibana),
|
||||
];
|
||||
};
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const APPS = {
|
||||
CCR_APP: 'ccr',
|
||||
REMOTE_CLUSTER_APP: 'remote_cluster',
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const BASE_PATH = '/management/elasticsearch/cross_cluster_replication';
|
||||
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';
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './plugin';
|
||||
export * from './base_path';
|
||||
export * from './app';
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const PLUGIN = {
|
||||
ID: 'cross_cluster_replication',
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
export const arrify = val => 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) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(data), time);
|
||||
});
|
||||
};
|
|
@ -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 { arrify } from './utils';
|
||||
describe('utils', () => {
|
||||
describe('arrify()', () => {
|
||||
it('should convert value to array', () => {
|
||||
const value = 'foo';
|
||||
expect(arrify(value)).toEqual([value]);
|
||||
});
|
||||
it('should not change a value that is already an array', () => {
|
||||
const value = ['foo'];
|
||||
expect(arrify(value)).toBe(value);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const Chance = require('chance'); // eslint-disable-line import/no-extraneous-dependencies
|
||||
const chance = new Chance();
|
||||
|
||||
export const getAutoFollowPatternMock = (
|
||||
name = chance.string(),
|
||||
remoteCluster = chance.string(),
|
||||
leaderIndexPatterns = [chance.string()],
|
||||
followIndexPattern = chance.string()
|
||||
) => ({
|
||||
name,
|
||||
pattern: {
|
||||
remote_cluster: remoteCluster,
|
||||
leader_index_patterns: leaderIndexPatterns,
|
||||
follow_index_pattern: followIndexPattern
|
||||
}
|
||||
});
|
||||
|
||||
export const getAutoFollowPatternListMock = (total = 3) => {
|
||||
const list = {
|
||||
patterns: []
|
||||
};
|
||||
|
||||
let i = total;
|
||||
while(i--) {
|
||||
list.patterns.push(getAutoFollowPatternMock());
|
||||
}
|
||||
|
||||
return list;
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Errors mocks to throw during development to help visualizing
|
||||
* the different flows in the UI
|
||||
*
|
||||
* TODO: Consult the ES team and make sure the error shapes are correct
|
||||
* for each statusCode.
|
||||
*/
|
||||
|
||||
const error400 = new Error('Something went wrong');
|
||||
error400.statusCode = 400;
|
||||
error400.response = `
|
||||
{
|
||||
"error": {
|
||||
"root_cause": [
|
||||
{
|
||||
"type": "x_content_parse_exception",
|
||||
"reason": "[2:3] [put_auto_follow_pattern_request] unknown field [remote_clusterxxxxx], parser not found"
|
||||
}
|
||||
],
|
||||
"type": "x_content_parse_exception",
|
||||
"reason": "[2:3] [put_auto_follow_pattern_request] unknown field [remote_clusterxxxxx], parser not found"
|
||||
},
|
||||
"status": 400
|
||||
}`;
|
||||
|
||||
const error403 = new Error('Unauthorized');
|
||||
error403.statusCode = 403;
|
||||
error403.response = `
|
||||
{
|
||||
"acknowledged": true,
|
||||
"trial_was_started": false,
|
||||
"error_message": "Operation failed: Trial was already activated."
|
||||
}
|
||||
`;
|
||||
|
||||
export const esErrors = {
|
||||
400: error400,
|
||||
403: error403
|
||||
};
|
12
x-pack/plugins/cross_cluster_replication/fixtures/index.js
Normal file
12
x-pack/plugins/cross_cluster_replication/fixtures/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export {
|
||||
getAutoFollowPatternMock,
|
||||
getAutoFollowPatternListMock,
|
||||
} from './auto_follow_pattern';
|
||||
|
||||
export { esErrors } from './es_errors';
|
46
x-pack/plugins/cross_cluster_replication/index.js
Normal file
46
x-pack/plugins/cross_cluster_replication/index.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { resolve } from 'path';
|
||||
import { PLUGIN } from './common/constants';
|
||||
import { registerLicenseChecker } from './server/lib/register_license_checker';
|
||||
import { registerRoutes } from './server/routes/register_routes';
|
||||
|
||||
export function crossClusterReplication(kibana) {
|
||||
return new kibana.Plugin({
|
||||
id: PLUGIN.ID,
|
||||
configPrefix: 'xpack.ccr',
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
require: ['kibana', 'elasticsearch', 'xpack_main', 'remote_clusters', 'index_management'],
|
||||
uiExports: {
|
||||
styleSheetPaths: `${__dirname}/public/index.scss`,
|
||||
managementSections: ['plugins/cross_cluster_replication'],
|
||||
injectDefaultVars(server) {
|
||||
const config = server.config();
|
||||
return {
|
||||
ccrUiEnabled: config.get('xpack.ccr.ui.enabled'),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
config(Joi) {
|
||||
return Joi.object({
|
||||
// display menu item
|
||||
ui: Joi.object({
|
||||
enabled: Joi.boolean().default(true)
|
||||
}).default(),
|
||||
|
||||
// enable plugin
|
||||
enabled: Joi.boolean().default(true),
|
||||
}).default();
|
||||
},
|
||||
|
||||
init: function initCcrPlugin(server) {
|
||||
registerLicenseChecker(server);
|
||||
registerRoutes(server);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* 1. Encapsulate this fix to avoid modifying global styling.
|
||||
*/
|
||||
.ccrPageContent {
|
||||
max-width: 1000px !important;
|
||||
width: 100% !important;
|
||||
|
||||
.euiText .euiButton--warning {
|
||||
color: $euiColorWarning; // [1]
|
||||
}
|
||||
}
|
||||
|
||||
.ccrFollowerIndicesFormRow {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.ccrFollowerIndicesHelpText {
|
||||
transform: translateY(-3px);
|
||||
}
|
61
x-pack/plugins/cross_cluster_replication/public/app/app.js
Normal file
61
x-pack/plugins/cross_cluster_replication/public/app/app.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||
|
||||
import routing from './services/routing';
|
||||
import { BASE_PATH } from '../../common/constants';
|
||||
|
||||
import {
|
||||
CrossClusterReplicationHome,
|
||||
AutoFollowPatternAdd,
|
||||
AutoFollowPatternEdit
|
||||
} from './sections';
|
||||
|
||||
export class App extends Component {
|
||||
static contextTypes = {
|
||||
router: PropTypes.shape({
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func.isRequired,
|
||||
createHref: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.registerRouter();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
routing.userHasLeftApp = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
routing.userHasLeftApp = true;
|
||||
}
|
||||
|
||||
registerRouter() {
|
||||
const { router } = this.context;
|
||||
routing.reactRouter = router;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Switch>
|
||||
<Redirect exact from={`${BASE_PATH}`} to={`${BASE_PATH}/auto_follow_patterns`} />
|
||||
<Route exact path={`${BASE_PATH}/auto_follow_patterns/add`} component={AutoFollowPatternAdd} />
|
||||
<Route exact path={`${BASE_PATH}/auto_follow_patterns/edit/:id`} component={AutoFollowPatternEdit} />
|
||||
<Route exact path={`${BASE_PATH}/:section`} component={CrossClusterReplicationHome} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<AutoFollowPatternForm state update updateFormErrors() should merge errors with existing fieldsErrors 1`] = `
|
||||
Object {
|
||||
"fieldsErrors": Object {
|
||||
"leaderIndexPatterns": null,
|
||||
"name": "Some error",
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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, { PureComponent, Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiConfirmModal,
|
||||
EuiOverlayMask,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { deleteAutoFollowPattern } from '../store/actions';
|
||||
import { arrify } from '../../../common/services/utils';
|
||||
|
||||
class Provider extends PureComponent {
|
||||
state = {
|
||||
isModalOpen: false,
|
||||
ids: null
|
||||
}
|
||||
|
||||
onMouseOverModal = (event) => {
|
||||
// This component can sometimes be used inside of an EuiToolTip, in which case mousing over
|
||||
// the modal can trigger the tooltip. Stopping propagation prevents this.
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
deleteAutoFollowPattern = (id) => {
|
||||
this.setState({ isModalOpen: true, ids: arrify(id) });
|
||||
};
|
||||
|
||||
onConfirm = () => {
|
||||
this.props.deleteAutoFollowPattern(this.state.ids);
|
||||
this.setState({ isModalOpen: false, ids: null });
|
||||
}
|
||||
|
||||
closeConfirmModal = () => {
|
||||
this.setState({
|
||||
isModalOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
renderModal = () => {
|
||||
const { intl } = this.props;
|
||||
const { ids } = this.state;
|
||||
const isSingle = ids.length === 1;
|
||||
const title = isSingle
|
||||
? intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.deleteSingleTitle',
|
||||
defaultMessage: 'Remove auto-follow pattern \'{name}\'?',
|
||||
}, { name: ids[0] })
|
||||
: intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.deleteMultipleTitle',
|
||||
defaultMessage: 'Remove {count} auto-follow patterns?',
|
||||
}, { count: ids.length });
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
{ /* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */ }
|
||||
<EuiConfirmModal
|
||||
title={title}
|
||||
onCancel={this.closeConfirmModal}
|
||||
onConfirm={this.onConfirm}
|
||||
cancelButtonText={
|
||||
intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.cancelButtonText',
|
||||
defaultMessage: 'Cancel',
|
||||
})
|
||||
}
|
||||
buttonColor="danger"
|
||||
confirmButtonText={
|
||||
intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.confirmButtonText',
|
||||
defaultMessage: 'Remove',
|
||||
})
|
||||
}
|
||||
onMouseOver={this.onMouseOverModal}
|
||||
>
|
||||
{!isSingle && (
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.multipleDeletionDescription"
|
||||
defaultMessage="You are about to remove these auto-follow patterns:"
|
||||
/>
|
||||
</p>
|
||||
<ul>{ids.map(id => <li key={id}>{id}</li>)}</ul>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
const { isModalOpen } = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{children(this.deleteAutoFollowPattern)}
|
||||
{isModalOpen && this.renderModal()}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
deleteAutoFollowPattern: (id) => dispatch(deleteAutoFollowPattern(id)),
|
||||
});
|
||||
|
||||
export const AutoFollowPatternDeleteProvider = connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(injectI18n(Provider));
|
||||
|
|
@ -0,0 +1,661 @@
|
|||
/*
|
||||
* 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, { PureComponent, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiComboBox,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormHelpText,
|
||||
EuiFormRow,
|
||||
EuiLoadingKibana,
|
||||
EuiLoadingSpinner,
|
||||
EuiOverlayMask,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiSuperSelect,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/index_patterns';
|
||||
import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices';
|
||||
|
||||
import routing from '../services/routing';
|
||||
import { API_STATUS } from '../constants';
|
||||
import { SectionError, AutoFollowPatternIndicesPreview } from './';
|
||||
import { validateAutoFollowPattern, validateLeaderIndexPattern } from '../services/auto_follow_pattern_validators';
|
||||
|
||||
const indexPatternIllegalCharacters = INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.join(' ');
|
||||
const indexNameIllegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' ');
|
||||
|
||||
const getFirstConnectedCluster = (clusters) => {
|
||||
for (let i = 0; i < clusters.length; i++) {
|
||||
if (clusters[i].isConnected) {
|
||||
return clusters[i];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const getEmptyAutoFollowPattern = (remoteClusters) => ({
|
||||
name: '',
|
||||
remoteCluster: getFirstConnectedCluster(remoteClusters).name,
|
||||
leaderIndexPatterns: [],
|
||||
followIndexPatternPrefix: '',
|
||||
followIndexPatternSuffix: '',
|
||||
});
|
||||
|
||||
export const updateFormErrors = (errors, existingErrors) => ({
|
||||
fieldsErrors: {
|
||||
...existingErrors,
|
||||
...errors,
|
||||
}
|
||||
});
|
||||
|
||||
export class AutoFollowPatternFormUI extends PureComponent {
|
||||
static propTypes = {
|
||||
saveAutoFollowPattern: PropTypes.func.isRequired,
|
||||
autoFollowPattern: PropTypes.object,
|
||||
apiError: PropTypes.object,
|
||||
apiStatus: PropTypes.string.isRequired,
|
||||
remoteClusters: PropTypes.array.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const isNew = this.props.autoFollowPattern === undefined;
|
||||
|
||||
const autoFollowPattern = isNew
|
||||
? getEmptyAutoFollowPattern(this.props.remoteClusters)
|
||||
: {
|
||||
...this.props.autoFollowPattern,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
autoFollowPattern,
|
||||
fieldsErrors: validateAutoFollowPattern(autoFollowPattern),
|
||||
areErrorsVisible: false,
|
||||
isNew,
|
||||
};
|
||||
}
|
||||
|
||||
onFieldsChange = (fields) => {
|
||||
this.setState(({ autoFollowPattern }) => ({
|
||||
autoFollowPattern: {
|
||||
...autoFollowPattern,
|
||||
...fields,
|
||||
},
|
||||
}));
|
||||
|
||||
const errors = validateAutoFollowPattern(fields);
|
||||
this.setState(({ fieldsErrors }) => updateFormErrors(errors, fieldsErrors));
|
||||
};
|
||||
|
||||
onClusterChange = (remoteCluster) => {
|
||||
this.onFieldsChange({ remoteCluster });
|
||||
};
|
||||
|
||||
onCreateLeaderIndexPattern = (indexPattern) => {
|
||||
const error = validateLeaderIndexPattern(indexPattern);
|
||||
|
||||
if (error) {
|
||||
const errors = {
|
||||
leaderIndexPatterns:
|
||||
{
|
||||
...error,
|
||||
alwaysVisible: true,
|
||||
},
|
||||
};
|
||||
|
||||
this.setState(({ fieldsErrors }) => updateFormErrors(errors, fieldsErrors));
|
||||
|
||||
// Return false to explicitly reject the user's input.
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
autoFollowPattern: {
|
||||
leaderIndexPatterns,
|
||||
},
|
||||
} = this.state;
|
||||
|
||||
const newLeaderIndexPatterns = [
|
||||
...leaderIndexPatterns,
|
||||
indexPattern,
|
||||
];
|
||||
|
||||
this.onFieldsChange({ leaderIndexPatterns: newLeaderIndexPatterns });
|
||||
};
|
||||
|
||||
onLeaderIndexPatternChange = (indexPatterns) => {
|
||||
this.onFieldsChange({
|
||||
leaderIndexPatterns: indexPatterns.map(({ label }) => label)
|
||||
});
|
||||
};
|
||||
|
||||
onLeaderIndexPatternInputChange = (leaderIndexPattern) => {
|
||||
if (!leaderIndexPattern || !leaderIndexPattern.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { autoFollowPattern: { leaderIndexPatterns } } = this.state;
|
||||
|
||||
if (leaderIndexPatterns.includes(leaderIndexPattern)) {
|
||||
const { intl } = this.props;
|
||||
const errorMsg = intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternForm.leaderIndexPatternError.duplicateMessage',
|
||||
defaultMessage: `Duplicate leader index pattern aren't allowed.`,
|
||||
});
|
||||
|
||||
const errors = {
|
||||
leaderIndexPatterns: {
|
||||
message: errorMsg,
|
||||
alwaysVisible: true,
|
||||
},
|
||||
};
|
||||
|
||||
this.setState(({ fieldsErrors }) => updateFormErrors(errors, fieldsErrors));
|
||||
} else {
|
||||
this.setState(({ fieldsErrors, autoFollowPattern }) => {
|
||||
const errors = validateAutoFollowPattern(autoFollowPattern);
|
||||
return updateFormErrors(errors, fieldsErrors);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getFields = () => {
|
||||
const { autoFollowPattern: stateFields } = this.state;
|
||||
const { followIndexPatternPrefix, followIndexPatternSuffix, ...rest } = stateFields;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
followIndexPattern: `${followIndexPatternPrefix}{{leader_index}}${followIndexPatternSuffix}`
|
||||
};
|
||||
};
|
||||
|
||||
isFormValid() {
|
||||
return Object.values(this.state.fieldsErrors).every(error => error === null);
|
||||
}
|
||||
|
||||
sendForm = () => {
|
||||
const isFormValid = this.isFormValid();
|
||||
|
||||
if (!isFormValid) {
|
||||
this.setState({ areErrorsVisible: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ areErrorsVisible: false });
|
||||
|
||||
const { name, ...autoFollowPattern } = this.getFields();
|
||||
this.props.saveAutoFollowPattern(name, autoFollowPattern);
|
||||
};
|
||||
|
||||
cancelForm = () => {
|
||||
routing.navigate('/auto_follow_patterns');
|
||||
};
|
||||
|
||||
/**
|
||||
* Secctions Renders
|
||||
*/
|
||||
renderApiErrors() {
|
||||
const { apiError, intl } = this.props;
|
||||
|
||||
if (apiError) {
|
||||
const title = intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternForm.savingErrorTitle',
|
||||
defaultMessage: 'Error creating auto-follow pattern',
|
||||
});
|
||||
return <SectionError title={title} error={apiError} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderForm = () => {
|
||||
const { intl } = this.props;
|
||||
const {
|
||||
autoFollowPattern: {
|
||||
name,
|
||||
remoteCluster,
|
||||
leaderIndexPatterns,
|
||||
followIndexPatternPrefix,
|
||||
followIndexPatternSuffix,
|
||||
},
|
||||
isNew,
|
||||
areErrorsVisible,
|
||||
fieldsErrors,
|
||||
} = this.state;
|
||||
|
||||
/**
|
||||
* Auto-follow pattern Name
|
||||
*/
|
||||
const renderAutoFollowPatternName = () => {
|
||||
const isInvalid = areErrorsVisible && !!fieldsErrors.name;
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={(
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.sectionAutoFollowPatternNameTitle"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
)}
|
||||
description={(
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.sectionAutoFollowPatternNameDescription"
|
||||
defaultMessage="A unique name for the auto-follow pattern."
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.autoFollowPatternName.fieldNameLabel"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
)}
|
||||
error={fieldsErrors.name}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
isInvalid={isInvalid}
|
||||
value={name}
|
||||
onChange={e => this.onFieldsChange({ name: e.target.value })}
|
||||
fullWidth
|
||||
disabled={!isNew}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remote Cluster
|
||||
*/
|
||||
const renderRemoteClusterField = () => {
|
||||
const remoteClustersOptions = this.props.remoteClusters.map(({ name, isConnected }) => ({
|
||||
value: name,
|
||||
inputDisplay: isConnected ? name : `${name} (not connected)`,
|
||||
disabled: !isConnected,
|
||||
'data-test-subj': `option-${name}`
|
||||
}));
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={(
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.sectionRemoteClusterTitle"
|
||||
defaultMessage="Remote cluster"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
)}
|
||||
description={(
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.sectionRemoteClusterDescription"
|
||||
defaultMessage="The remote cluster to replicate leader indices from."
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.remoteCluster.fieldClusterLabel"
|
||||
defaultMessage="Remote cluster"
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
>
|
||||
<Fragment>
|
||||
{ isNew && (
|
||||
<EuiSuperSelect
|
||||
options={remoteClustersOptions}
|
||||
valueOfSelected={remoteCluster}
|
||||
onChange={this.onClusterChange}
|
||||
/>
|
||||
)}
|
||||
{ !isNew && (
|
||||
<EuiFieldText
|
||||
value={remoteCluster}
|
||||
fullWidth
|
||||
disabled={true}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Leader index pattern(s)
|
||||
*/
|
||||
const renderLeaderIndexPatterns = () => {
|
||||
const hasError = !!fieldsErrors.leaderIndexPatterns;
|
||||
const isInvalid = hasError && (fieldsErrors.leaderIndexPatterns.alwaysVisible || areErrorsVisible);
|
||||
const formattedLeaderIndexPatterns = leaderIndexPatterns.map(pattern => ({ label: pattern }));
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={(
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.sectionLeaderIndexPatternsTitle"
|
||||
defaultMessage="Leader indices"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
)}
|
||||
description={(
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.sectionLeaderIndexPatternsDescription1"
|
||||
defaultMessage="One or more index patterns that identify the indices you want to
|
||||
replicate from the remote cluster. As new indices matching these patterns are
|
||||
created, they are replicated to follower indices on the local cluster."
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.sectionLeaderIndexPatternsDescription2"
|
||||
defaultMessage="{note} indices that already exist are not replicated."
|
||||
values={{ note: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.sectionLeaderIndexPatternsDescription2.noteLabel"
|
||||
defaultMessage="Note:"
|
||||
/>
|
||||
</strong>
|
||||
) }}
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.fieldLeaderIndexPatternsLabel"
|
||||
defaultMessage="Index patterns"
|
||||
/>
|
||||
)}
|
||||
helpText={(
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.fieldLeaderIndexPatternsHelpLabel"
|
||||
defaultMessage="Spaces and the characters {characterList} are not allowed."
|
||||
values={{ characterList: <strong>{indexPatternIllegalCharacters}</strong> }}
|
||||
/>
|
||||
)}
|
||||
isInvalid={isInvalid}
|
||||
error={fieldsErrors.leaderIndexPatterns && fieldsErrors.leaderIndexPatterns.message}
|
||||
fullWidth
|
||||
>
|
||||
<EuiComboBox
|
||||
noSuggestions
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternForm.fieldLeaderIndexPatternsPlaceholder',
|
||||
defaultMessage: 'Type and then hit ENTER',
|
||||
})}
|
||||
selectedOptions={formattedLeaderIndexPatterns}
|
||||
onCreateOption={this.onCreateLeaderIndexPattern}
|
||||
onChange={this.onLeaderIndexPatternChange}
|
||||
onSearchChange={this.onLeaderIndexPatternInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Auto-follow pattern
|
||||
*/
|
||||
const renderAutoFollowPattern = () => {
|
||||
const isPrefixInvalid = areErrorsVisible && !!fieldsErrors.followIndexPatternPrefix;
|
||||
const isSuffixInvalid = areErrorsVisible && !!fieldsErrors.followIndexPatternSuffix;
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={(
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.sectionAutoFollowPatternTitle"
|
||||
defaultMessage="Follower indices (optional)"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
)}
|
||||
description={(
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.sectionAutoFollowPatternDescription"
|
||||
defaultMessage="A custom prefix or suffix to apply to the names of the follower
|
||||
indices so you can more easily identify replicated indices. By default, a follower
|
||||
index has the same name as the leader index."
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
className="ccrFollowerIndicesFormRow"
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.autoFollowPattern.fieldPrefixLabel"
|
||||
defaultMessage="Prefix"
|
||||
/>
|
||||
)}
|
||||
error={fieldsErrors.followIndexPatternPrefix}
|
||||
isInvalid={isPrefixInvalid}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
isInvalid={isPrefixInvalid}
|
||||
value={followIndexPatternPrefix}
|
||||
onChange={e => this.onFieldsChange({ followIndexPatternPrefix: e.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
className="ccrFollowerIndicesFormRow"
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.autoFollowPattern.fieldSuffixLabel"
|
||||
defaultMessage="Suffix"
|
||||
/>
|
||||
)}
|
||||
error={fieldsErrors.followIndexPatternSuffix}
|
||||
isInvalid={isSuffixInvalid}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
isInvalid={isSuffixInvalid}
|
||||
value={followIndexPatternSuffix}
|
||||
onChange={e => this.onFieldsChange({ followIndexPatternSuffix: e.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFormHelpText className={isPrefixInvalid || isSuffixInvalid ? null : 'ccrFollowerIndicesHelpText'}>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.fieldFollowerIndicesHelpLabel"
|
||||
defaultMessage="Spaces and the characters {characterList} are not allowed."
|
||||
values={{ characterList: <strong>{indexNameIllegalCharacters}</strong> }}
|
||||
/>
|
||||
</EuiFormHelpText>
|
||||
|
||||
{!!leaderIndexPatterns.length && (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<AutoFollowPatternIndicesPreview
|
||||
prefix={followIndexPatternPrefix}
|
||||
suffix={followIndexPatternSuffix}
|
||||
leaderIndexPatterns={leaderIndexPatterns}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Form Error warning message
|
||||
*/
|
||||
const renderFormErrorWarning = () => {
|
||||
const { areErrorsVisible } = this.state;
|
||||
const isFormValid = this.isFormValid();
|
||||
|
||||
if (!areErrorsVisible || isFormValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.validationErrorTitle"
|
||||
defaultMessage="Fix errors before continuing."
|
||||
/>
|
||||
)}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Form Actions
|
||||
*/
|
||||
const renderActions = () => {
|
||||
const { apiStatus } = this.props;
|
||||
const { areErrorsVisible } = this.state;
|
||||
|
||||
if (apiStatus === API_STATUS.SAVING) {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexStart" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="l"/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.actions.savingText"
|
||||
defaultMessage="Saving"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const isSaveDisabled = areErrorsVisible && !this.isFormValid();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="secondary"
|
||||
iconType="check"
|
||||
onClick={this.sendForm}
|
||||
fill
|
||||
disabled={isSaveDisabled}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
onClick={this.cancelForm}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiForm>
|
||||
{renderAutoFollowPatternName()}
|
||||
{renderRemoteClusterField()}
|
||||
{renderLeaderIndexPatterns()}
|
||||
{renderAutoFollowPattern()}
|
||||
</EuiForm>
|
||||
{renderFormErrorWarning()}
|
||||
<EuiSpacer size="l" />
|
||||
{renderActions()}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoading = () => {
|
||||
const { apiStatus } = this.props;
|
||||
|
||||
if (apiStatus === API_STATUS.SAVING) {
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiLoadingKibana size="xl"/>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
{this.renderApiErrors()}
|
||||
{this.renderForm()}
|
||||
{this.renderLoading()}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const AutoFollowPatternForm = injectI18n(AutoFollowPatternFormUI);
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { updateFormErrors } from './auto_follow_pattern_form';
|
||||
|
||||
jest.mock('../services/auto_follow_pattern_validators', () => ({
|
||||
validateAutoFollowPattern: jest.fn(),
|
||||
validateLeaderIndexPattern: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('ui/index_patterns/index_patterns.js', () => ({
|
||||
IndexPatternsProvider: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('ui/index_patterns/index_patterns_api_client_provider.js', () => ({
|
||||
IndexPatternsApiClientProvider: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('<AutoFollowPatternForm state update', () => {
|
||||
describe('updateFormErrors()', () => {
|
||||
it('should merge errors with existing fieldsErrors', () => {
|
||||
const errors = { name: 'Some error' };
|
||||
const existingErrors = { leaderIndexPatterns: null };
|
||||
const output = updateFormErrors(errors, existingErrors);
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 React from 'react';
|
||||
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import { getPreviewIndicesFromAutoFollowPattern } from '../services/auto_follow_pattern';
|
||||
|
||||
export const AutoFollowPatternIndicesPreview = injectI18n(({ prefix, suffix, leaderIndexPatterns, intl }) => {
|
||||
const { indicesPreview } = getPreviewIndicesFromAutoFollowPattern({
|
||||
prefix,
|
||||
suffix,
|
||||
leaderIndexPatterns
|
||||
});
|
||||
|
||||
const title = intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternForm.indicesPreviewTitle',
|
||||
defaultMessage: 'Index name examples',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={title}
|
||||
iconType="indexMapping"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternForm.indicesPreviewDescription"
|
||||
defaultMessage="The above settings will generate index names that look like this:"
|
||||
/>
|
||||
<ul>
|
||||
{indicesPreview.map(({ followPattern: { prefix, suffix, template } }, i) => (
|
||||
<li key={i}>
|
||||
{prefix}<strong>{template}</strong>{suffix}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</EuiCallOut>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPageContentHeader,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { autoFollowPatternUrl } from '../services/documentation_links';
|
||||
|
||||
export const AutoFollowPatternPageTitle = ({ title }) => (
|
||||
<Fragment>
|
||||
<EuiSpacer size="xs" />
|
||||
|
||||
<EuiPageContentHeader>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="l">
|
||||
<h1>{title}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
flush="right"
|
||||
href={autoFollowPatternUrl}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.readDocsButtonLabel"
|
||||
defaultMessage="Auto-follow pattern docs"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentHeader>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
AutoFollowPatternPageTitle.propTypes = {
|
||||
title: PropTypes.node.isRequired,
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export { SectionLoading } from './section_loading';
|
||||
export { SectionError } from './section_error';
|
||||
export { SectionUnauthorized } from './section_unauthorized';
|
||||
export { RemoteClustersProvider } from './remote_clusters_provider';
|
||||
export { AutoFollowPatternForm } from './auto_follow_pattern_form';
|
||||
export { AutoFollowPatternDeleteProvider } from './auto_follow_pattern_delete_provider';
|
||||
export { AutoFollowPatternPageTitle } from './auto_follow_pattern_page_title';
|
||||
export { AutoFollowPatternIndicesPreview } from './auto_follow_pattern_indices_preview';
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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, { PureComponent } from 'react'; // eslint-disable-line no-unused-vars
|
||||
import { loadRemoteClusters } from '../services/api';
|
||||
|
||||
export class RemoteClustersProvider extends PureComponent {
|
||||
state = {
|
||||
isLoading: true,
|
||||
error: null,
|
||||
remoteClusters: []
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadRemoteClusters();
|
||||
}
|
||||
|
||||
loadRemoteClusters() {
|
||||
const sortClusterByName = (remoteClusters) => (
|
||||
remoteClusters.sort((a, b) => {
|
||||
if(a.name < b.name) { return -1; }
|
||||
if(a.name > b.name) { return 1; }
|
||||
return 0;
|
||||
})
|
||||
);
|
||||
loadRemoteClusters()
|
||||
.then((remoteClusters) => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
remoteClusters: sortClusterByName(remoteClusters)
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
error
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
const { isLoading, error, remoteClusters } = this.state;
|
||||
|
||||
return children({ isLoading, error, remoteClusters });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export function SectionError({ title, error }) {
|
||||
const {
|
||||
error: errorString,
|
||||
cause, // wrapEsError() on the server add a "cause" array
|
||||
message,
|
||||
} = error.data;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={title}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
<div>{message || errorString}</div>
|
||||
{ cause && (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<ul>
|
||||
{ cause.map((message, i) => <li key={i}>{message}</li>) }
|
||||
</ul>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -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 React from 'react';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export function SectionLoading({ children }) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiTextColor color="subdued">
|
||||
{children}
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import { injectI18n } from '@kbn/i18n/react';
|
||||
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
|
||||
export function SectionUnauthorizedUI({ intl, children }) {
|
||||
const title = intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.remoteClusterList.noPermissionTitle',
|
||||
defaultMessage: 'Permission error',
|
||||
});
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={title}
|
||||
color="warning"
|
||||
iconType="help"
|
||||
>
|
||||
{children}
|
||||
</EuiCallOut>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export const SectionUnauthorized = injectI18n(SectionUnauthorizedUI);
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export const API_STATUS = {
|
||||
IDLE: 'idle',
|
||||
LOADING: 'loading',
|
||||
UPDATING: 'updating',
|
||||
SAVING: 'saving',
|
||||
DELETING: 'deleting'
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { API_STATUS } from './api';
|
||||
export { SECTIONS } from './sections';
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const SECTIONS = {
|
||||
AUTO_FOLLOW_PATTERN: 'autoFollowPattern',
|
||||
INDEX_FOLLOWER: 'indexFollower',
|
||||
REMOTE_CLUSTER: 'remoteCluster'
|
||||
};
|
26
x-pack/plugins/cross_cluster_replication/public/app/index.js
Normal file
26
x-pack/plugins/cross_cluster_replication/public/app/index.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { I18nProvider } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
import { App } from './app';
|
||||
import { ccrStore } from './store';
|
||||
|
||||
export const renderReact = async (elem) => {
|
||||
render(
|
||||
<I18nProvider>
|
||||
<Provider store={ccrStore}>
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
</I18nProvider>,
|
||||
elem
|
||||
);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { connect } from 'react-redux';
|
||||
|
||||
import { SECTIONS } from '../../constants';
|
||||
import { getApiStatus, getApiError } from '../../store/selectors';
|
||||
import { saveAutoFollowPattern, clearApiError } from '../../store/actions';
|
||||
import { AutoFollowPatternAdd as AutoFollowPatternAddView } from './auto_follow_pattern_add';
|
||||
|
||||
const scope = SECTIONS.AUTO_FOLLOW_PATTERN;
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
apiStatus: getApiStatus(scope)(state),
|
||||
apiError: getApiError(scope)(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
saveAutoFollowPattern: (id, autoFollowPattern) => dispatch(saveAutoFollowPattern(id, autoFollowPattern)),
|
||||
clearApiError: () => dispatch(clearApiError(scope)),
|
||||
});
|
||||
|
||||
export const AutoFollowPatternAdd = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AutoFollowPatternAddView);
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* 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, { PureComponent, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import chrome from 'ui/chrome';
|
||||
import { MANAGEMENT_BREADCRUMB } from 'ui/management';
|
||||
|
||||
import {
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { listBreadcrumb, addBreadcrumb } from '../../services/breadcrumbs';
|
||||
import routing from '../../services/routing';
|
||||
import { BASE_PATH_REMOTE_CLUSTERS } from '../../../../common/constants';
|
||||
import {
|
||||
AutoFollowPatternForm,
|
||||
AutoFollowPatternPageTitle,
|
||||
RemoteClustersProvider,
|
||||
SectionLoading,
|
||||
SectionError,
|
||||
} from '../../components';
|
||||
|
||||
export const AutoFollowPatternAdd = injectI18n(
|
||||
class extends PureComponent {
|
||||
static propTypes = {
|
||||
saveAutoFollowPattern: PropTypes.func.isRequired,
|
||||
clearApiError: PropTypes.func.isRequired,
|
||||
apiError: PropTypes.object,
|
||||
apiStatus: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
chrome.breadcrumbs.set([ MANAGEMENT_BREADCRUMB, listBreadcrumb, addBreadcrumb ]);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearApiError();
|
||||
}
|
||||
|
||||
renderEmptyClusters() {
|
||||
const { intl, match: { url: currentUrl } } = this.props;
|
||||
const title = intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternCreateForm.emptyRemoteClustersCallOutTitle',
|
||||
defaultMessage: 'No remote cluster found'
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={title}
|
||||
color="warning"
|
||||
iconType="cross"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternCreateForm.emptyRemoteClustersCallOutDescription"
|
||||
defaultMessage="Auto-follow patterns capture indices on remote clusters. You must add
|
||||
a remote cluster."
|
||||
/>
|
||||
</p>
|
||||
|
||||
<EuiButton
|
||||
{...routing.getRouterLinkProps('/add', BASE_PATH_REMOTE_CLUSTERS, { redirect: currentUrl })}
|
||||
iconType="plusInCircle"
|
||||
color="warning"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternCreateForm.addRemoteClusterButtonLabel"
|
||||
defaultMessage="Add remote cluster"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderNoConnectedCluster() {
|
||||
const { intl } = this.props;
|
||||
const title = intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternCreateForm.noRemoteClustersConnectedCallOutTitle',
|
||||
defaultMessage: 'Remote cluster connection error'
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={title}
|
||||
color="warning"
|
||||
iconType="cross"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternCreateForm.noRemoteClustersConnectedCallOutDescription"
|
||||
defaultMessage="None of your clusters are connected. Verify your clusters settings
|
||||
and make sure at least one cluster is connected before creating an auto-follow pattern." //eslint-disable-line max-len
|
||||
/>
|
||||
</p>
|
||||
<EuiButton
|
||||
{...routing.getRouterLinkProps('/', BASE_PATH_REMOTE_CLUSTERS)}
|
||||
color="warning"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternCreateForm.viewRemoteClusterButtonLabel"
|
||||
defaultMessage="View remote clusters"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { saveAutoFollowPattern, apiStatus, apiError, intl } = this.props;
|
||||
|
||||
return (
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageContent
|
||||
horizontalPosition="center"
|
||||
className="ccrPageContent"
|
||||
>
|
||||
<AutoFollowPatternPageTitle
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPattern.addTitle"
|
||||
defaultMessage="Add auto-follow pattern"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<RemoteClustersProvider>
|
||||
{({ isLoading, error, remoteClusters }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternCreateForm.loadingRemoteClusters"
|
||||
defaultMessage="Loading remote clusters..."
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const title = intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternCreateForm.loadingRemoteClustersErrorTitle',
|
||||
defaultMessage: 'Error loading remote clusters',
|
||||
});
|
||||
return <SectionError title={title} error={error} />;
|
||||
}
|
||||
|
||||
if (!remoteClusters.length) {
|
||||
return this.renderEmptyClusters();
|
||||
}
|
||||
|
||||
if (remoteClusters.every(cluster => cluster.isConnected === false)) {
|
||||
return this.renderNoConnectedCluster();
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoFollowPatternForm
|
||||
apiStatus={apiStatus}
|
||||
apiError={apiError}
|
||||
remoteClusters={remoteClusters}
|
||||
saveAutoFollowPattern={saveAutoFollowPattern}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</RemoteClustersProvider>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { AutoFollowPatternAdd } from './auto_follow_pattern_add.container';
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { connect } from 'react-redux';
|
||||
|
||||
import { SECTIONS } from '../../constants';
|
||||
import { getApiStatus, getApiError, getSelectedAutoFollowPattern } from '../../store/selectors';
|
||||
import { getAutoFollowPattern, saveAutoFollowPattern, clearApiError } from '../../store/actions';
|
||||
import { AutoFollowPatternEdit as AutoFollowPatternEditView } from './auto_follow_pattern_edit';
|
||||
|
||||
const scope = SECTIONS.AUTO_FOLLOW_PATTERN;
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
apiStatus: getApiStatus(scope)(state),
|
||||
apiError: getApiError(scope)(state),
|
||||
autoFollowPattern: getSelectedAutoFollowPattern(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
getAutoFollowPattern: (id) => dispatch(getAutoFollowPattern(id)),
|
||||
saveAutoFollowPattern: (id, autoFollowPattern) => dispatch(saveAutoFollowPattern(id, autoFollowPattern, true)),
|
||||
clearApiError: () => dispatch(clearApiError(scope)),
|
||||
});
|
||||
|
||||
export const AutoFollowPatternEdit = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AutoFollowPatternEditView);
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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, { PureComponent, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import chrome from 'ui/chrome';
|
||||
import { MANAGEMENT_BREADCRUMB } from 'ui/management';
|
||||
|
||||
import {
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiSpacer,
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { listBreadcrumb, editBreadcrumb } from '../../services/breadcrumbs';
|
||||
import routing from '../../services/routing';
|
||||
import { BASE_PATH_REMOTE_CLUSTERS } from '../../../../common/constants';
|
||||
import {
|
||||
AutoFollowPatternForm,
|
||||
AutoFollowPatternPageTitle,
|
||||
RemoteClustersProvider,
|
||||
SectionLoading,
|
||||
SectionError,
|
||||
} from '../../components';
|
||||
import { API_STATUS } from '../../constants';
|
||||
|
||||
export const AutoFollowPatternEdit = injectI18n(
|
||||
class extends PureComponent {
|
||||
static propTypes = {
|
||||
getAutoFollowPattern: PropTypes.func.isRequired,
|
||||
saveAutoFollowPattern: PropTypes.func.isRequired,
|
||||
clearApiError: PropTypes.func.isRequired,
|
||||
apiError: PropTypes.object,
|
||||
apiStatus: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { autoFollowPattern, match: { params: { id } } } = this.props;
|
||||
if (!autoFollowPattern) {
|
||||
const decodedId = decodeURIComponent(id);
|
||||
this.props.getAutoFollowPattern(decodedId);
|
||||
}
|
||||
|
||||
chrome.breadcrumbs.set([ MANAGEMENT_BREADCRUMB, listBreadcrumb, editBreadcrumb ]);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearApiError();
|
||||
}
|
||||
|
||||
renderApiError(error) {
|
||||
const { intl } = this.props;
|
||||
const title = intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorTitle',
|
||||
defaultMessage: 'Error loading auto-follow pattern',
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SectionError title={title} error={error} />
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
{...routing.getRouterLinkProps('/auto_follow_patterns')}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternEditForm.viewAutoFollowPatternsButtonLabel"
|
||||
defaultMessage="View auto-follow patterns"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoadingAutoFollowPattern() {
|
||||
return (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternEditForm.loadingTitle"
|
||||
defaultMessage="Loading auto-follow pattern..."
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
}
|
||||
|
||||
renderMissingCluster({ name, remoteCluster }) {
|
||||
const { intl } = this.props;
|
||||
|
||||
const title = intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternEditForm.emptyRemoteClustersTitle',
|
||||
defaultMessage: 'Remote cluster missing'
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={title}
|
||||
color="warning"
|
||||
iconType="help"
|
||||
>
|
||||
<p>
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternEditForm.emptyRemoteClustersDescription"
|
||||
defaultMessage="The remote cluster '{remoteCluster}' does not exist or is not
|
||||
connected. Make sure it is connected before editing the '{name}' auto-follow pattern."
|
||||
values={{ remoteCluster, name }}
|
||||
/>
|
||||
</p>
|
||||
<EuiButton
|
||||
{...routing.getRouterLinkProps('/list', BASE_PATH_REMOTE_CLUSTERS)}
|
||||
color="warning"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternEditForm.viewRemoteClustersButtonLabel"
|
||||
defaultMessage="View remote clusters"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { saveAutoFollowPattern, apiStatus, apiError, autoFollowPattern, intl } = this.props;
|
||||
|
||||
return (
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageContent
|
||||
horizontalPosition="center"
|
||||
className="ccrPageContent"
|
||||
>
|
||||
<AutoFollowPatternPageTitle
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPattern.editTitle"
|
||||
defaultMessage="Edit auto-follow pattern"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{apiStatus === API_STATUS.LOADING && this.renderLoadingAutoFollowPattern()}
|
||||
|
||||
{apiError && this.renderApiError(apiError)}
|
||||
|
||||
{autoFollowPattern && (
|
||||
<RemoteClustersProvider>
|
||||
{({ isLoading, error, remoteClusters }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternEditForm.loadingRemoteClusters"
|
||||
defaultMessage="Loading remote clusters..."
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const title = intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingRemoteClustersErrorTitle',
|
||||
defaultMessage: 'Error loading remote clusters',
|
||||
});
|
||||
return <SectionError title={title} error={error} />;
|
||||
}
|
||||
|
||||
const autoFollowPatternCluster = remoteClusters.find(cluster => cluster.name === autoFollowPattern.remoteCluster);
|
||||
|
||||
if (!autoFollowPatternCluster || !autoFollowPatternCluster.isConnected) {
|
||||
return this.renderMissingCluster(autoFollowPattern);
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoFollowPatternForm
|
||||
apiStatus={apiStatus}
|
||||
apiError={apiError}
|
||||
remoteClusters={remoteClusters}
|
||||
autoFollowPattern={autoFollowPattern}
|
||||
saveAutoFollowPattern={saveAutoFollowPattern}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</RemoteClustersProvider>
|
||||
)}
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { AutoFollowPatternEdit } from './auto_follow_pattern_edit.container';
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { SECTIONS } from '../../../constants';
|
||||
import {
|
||||
getListAutoFollowPatterns,
|
||||
getApiStatus,
|
||||
getApiError,
|
||||
isApiAuthorized,
|
||||
isAutoFollowPatternDetailPanelOpen as isDetailPanelOpen,
|
||||
} from '../../../store/selectors';
|
||||
import {
|
||||
loadAutoFollowPatterns,
|
||||
openAutoFollowPatternDetailPanel as openDetailPanel,
|
||||
closeAutoFollowPatternDetailPanel as closeDetailPanel,
|
||||
} from '../../../store/actions';
|
||||
import { AutoFollowPatternList as AutoFollowPatternListView } from './auto_follow_pattern_list';
|
||||
|
||||
const scope = SECTIONS.AUTO_FOLLOW_PATTERN;
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
autoFollowPatterns: getListAutoFollowPatterns(state),
|
||||
apiStatus: getApiStatus(scope)(state),
|
||||
apiError: getApiError(scope)(state),
|
||||
isAuthorized: isApiAuthorized(scope)(state),
|
||||
isDetailPanelOpen: isDetailPanelOpen(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
loadAutoFollowPatterns: (inBackground) => dispatch(loadAutoFollowPatterns(inBackground)),
|
||||
openDetailPanel: (name) => {
|
||||
dispatch(openDetailPanel(name));
|
||||
},
|
||||
closeDetailPanel: () => {
|
||||
dispatch(closeDetailPanel());
|
||||
},
|
||||
});
|
||||
|
||||
export const AutoFollowPatternList = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AutoFollowPatternListView);
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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, { PureComponent, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import routing from '../../../services/routing';
|
||||
import { extractQueryParams } from '../../../services/query_params';
|
||||
import { API_STATUS } from '../../../constants';
|
||||
import { SectionLoading, SectionError } from '../../../components';
|
||||
import { AutoFollowPatternTable, DetailPanel } from './components';
|
||||
|
||||
const REFRESH_RATE_MS = 30000;
|
||||
|
||||
export const AutoFollowPatternList = injectI18n(
|
||||
class extends PureComponent {
|
||||
static propTypes = {
|
||||
loadAutoFollowPatterns: PropTypes.func,
|
||||
autoFollowPatterns: PropTypes.array,
|
||||
apiStatus: PropTypes.string,
|
||||
apiError: PropTypes.object,
|
||||
openDetailPanel: PropTypes.func.isRequired,
|
||||
closeDetailPanel: PropTypes.func.isRequired,
|
||||
isDetailPanelOpen: PropTypes.bool,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadAutoFollowPatterns();
|
||||
|
||||
// Interval to load auto-follow patterns in the background passing "true" to the fetch method
|
||||
this.interval = setInterval(() => this.props.loadAutoFollowPatterns(true), REFRESH_RATE_MS);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {
|
||||
openDetailPanel,
|
||||
closeDetailPanel,
|
||||
isDetailPanelOpen,
|
||||
history: {
|
||||
location: {
|
||||
search,
|
||||
},
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const { pattern: patternName } = extractQueryParams(search);
|
||||
|
||||
// Show deeplinked auto follow pattern whenever patterns get loaded or the URL changes.
|
||||
if (patternName != null) {
|
||||
openDetailPanel(patternName);
|
||||
} else if (isDetailPanelOpen) {
|
||||
closeDetailPanel();
|
||||
}
|
||||
}
|
||||
|
||||
renderEmpty() {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
title={(
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternList.emptyPromptTitle"
|
||||
defaultMessage="Create your first auto-follow pattern"
|
||||
/>
|
||||
</h1>
|
||||
)}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternList.emptyPromptDescription"
|
||||
defaultMessage="Use an auto-follow pattern to automatically replicate indices from
|
||||
a remote cluster."
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
{...routing.getRouterLinkProps('/auto_follow_patterns/add')}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.addAutoFollowPatternButtonLabel"
|
||||
defaultMessage="Create auto-follow pattern"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderList() {
|
||||
const { autoFollowPatterns, apiStatus } = this.props;
|
||||
|
||||
if (apiStatus === API_STATUS.LOADING) {
|
||||
return (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternList.loadingTitle"
|
||||
defaultMessage="Loading auto-follow patterns..."
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<AutoFollowPatternTable autoFollowPatterns={autoFollowPatterns} />
|
||||
<DetailPanel />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { autoFollowPatterns, apiStatus, apiError, isAuthorized, intl } = this.props;
|
||||
|
||||
if (!isAuthorized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (apiStatus === API_STATUS.IDLE && !autoFollowPatterns.length) {
|
||||
return this.renderEmpty();
|
||||
}
|
||||
|
||||
if (apiError) {
|
||||
const title = intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternList.loadingErrorTitle',
|
||||
defaultMessage: 'Error loading auto-follow patterns',
|
||||
});
|
||||
return <SectionError title={title} error={apiError} />;
|
||||
}
|
||||
|
||||
return this.renderList();
|
||||
}
|
||||
}
|
||||
);
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { connect } from 'react-redux';
|
||||
|
||||
import { SECTIONS } from '../../../../../constants';
|
||||
import {
|
||||
editAutoFollowPattern,
|
||||
openAutoFollowPatternDetailPanel as openDetailPanel,
|
||||
} from '../../../../../store/actions';
|
||||
import { getApiStatus } from '../../../../../store/selectors';
|
||||
import { AutoFollowPatternTable as AutoFollowPatternTableComponent } from './auto_follow_pattern_table';
|
||||
|
||||
const scope = SECTIONS.AUTO_FOLLOW_PATTERN;
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
apiStatusDelete: getApiStatus(`${scope}-delete`)(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
editAutoFollowPattern: (name) => dispatch(editAutoFollowPattern(name)),
|
||||
openDetailPanel: (name) => {
|
||||
dispatch(openDetailPanel(name));
|
||||
},
|
||||
});
|
||||
|
||||
export const AutoFollowPatternTable = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(AutoFollowPatternTableComponent);
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* 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, { PureComponent, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiInMemoryTable,
|
||||
EuiLink,
|
||||
EuiLoadingKibana,
|
||||
EuiToolTip,
|
||||
EuiOverlayMask,
|
||||
} from '@elastic/eui';
|
||||
import { API_STATUS } from '../../../../../constants';
|
||||
import { AutoFollowPatternDeleteProvider } from '../../../../../components';
|
||||
import routing from '../../../../../services/routing';
|
||||
|
||||
export const AutoFollowPatternTable = injectI18n(
|
||||
class extends PureComponent {
|
||||
static propTypes = {
|
||||
autoFollowPatterns: PropTypes.array,
|
||||
openDetailPanel: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
selectedItems: [],
|
||||
}
|
||||
|
||||
onSearch = ({ query }) => {
|
||||
const { text } = query;
|
||||
const normalizedSearchText = text.toLowerCase();
|
||||
this.setState({
|
||||
queryText: normalizedSearchText,
|
||||
});
|
||||
};
|
||||
|
||||
getFilteredPatterns = () => {
|
||||
const { autoFollowPatterns } = this.props;
|
||||
const { queryText } = this.state;
|
||||
|
||||
if(queryText) {
|
||||
return autoFollowPatterns.filter(autoFollowPattern => {
|
||||
const { name, remoteCluster, followIndexPatternPrefix, followIndexPatternSuffix } = autoFollowPattern;
|
||||
|
||||
const inName = name.toLowerCase().includes(queryText);
|
||||
const inRemoteCluster = remoteCluster.toLowerCase().includes(queryText);
|
||||
const inPrefix = followIndexPatternPrefix.toLowerCase().includes(queryText);
|
||||
const inSuffix = followIndexPatternSuffix.toLowerCase().includes(queryText);
|
||||
|
||||
return inName || inRemoteCluster || inPrefix || inSuffix;
|
||||
});
|
||||
}
|
||||
|
||||
return autoFollowPatterns.slice(0);
|
||||
};
|
||||
|
||||
getTableColumns() {
|
||||
const { intl, editAutoFollowPattern, openDetailPanel } = this.props;
|
||||
|
||||
return [{
|
||||
field: 'name',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternList.table.nameColumnTitle',
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
render: (name) => {
|
||||
return (
|
||||
<EuiLink onClick={() => openDetailPanel(name)}>
|
||||
{name}
|
||||
</EuiLink>
|
||||
);
|
||||
}
|
||||
}, {
|
||||
field: 'remoteCluster',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternList.table.clusterColumnTitle',
|
||||
defaultMessage: 'Cluster',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
}, {
|
||||
field: 'leaderIndexPatterns',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternList.table.leaderPatternsColumnTitle',
|
||||
defaultMessage: 'Leader patterns',
|
||||
}),
|
||||
render: (leaderPatterns) => leaderPatterns.join(', '),
|
||||
}, {
|
||||
field: 'followIndexPatternPrefix',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternList.table.prefixColumnTitle',
|
||||
defaultMessage: 'Follower pattern prefix',
|
||||
}),
|
||||
sortable: true,
|
||||
}, {
|
||||
field: 'followIndexPatternSuffix',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternList.table.suffixColumnTitle',
|
||||
defaultMessage: 'Follower pattern suffix',
|
||||
}),
|
||||
sortable: true,
|
||||
}, {
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.autoFollowPatternList.table.actionsColumnTitle',
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
actions: [
|
||||
{
|
||||
render: ({ name }) => {
|
||||
const label = i18n.translate(
|
||||
'xpack.crossClusterReplication.autofollowPatternList.table.actionDeleteDescription',
|
||||
{
|
||||
defaultMessage: 'Delete auto-follow pattern',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={label}
|
||||
delay="long"
|
||||
>
|
||||
<AutoFollowPatternDeleteProvider>
|
||||
{(deleteAutoFollowPattern) => (
|
||||
<EuiButtonIcon
|
||||
aria-label={label}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={() => deleteAutoFollowPattern(name)}
|
||||
/>
|
||||
)}
|
||||
</AutoFollowPatternDeleteProvider>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.editIndexPattern.fields.table.actionEditLabel',
|
||||
defaultMessage: 'Edit',
|
||||
}),
|
||||
description: intl.formatMessage({
|
||||
id: 'xpack.crossClusterReplication.editIndexPattern.fields.table.actionEditDescription',
|
||||
defaultMessage: 'Edit',
|
||||
}),
|
||||
icon: 'pencil',
|
||||
onClick: ({ name }) => {
|
||||
editAutoFollowPattern(name);
|
||||
routing.navigate(encodeURI(`/auto_follow_patterns/edit/${encodeURIComponent(name)}`));
|
||||
},
|
||||
type: 'icon',
|
||||
},
|
||||
],
|
||||
width: '100px',
|
||||
}];
|
||||
}
|
||||
|
||||
renderLoading = () => {
|
||||
const { apiStatusDelete } = this.props;
|
||||
|
||||
if (apiStatusDelete === API_STATUS.DELETING) {
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiLoadingKibana size="xl"/>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectedItems,
|
||||
} = this.state;
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: 'name',
|
||||
direction: 'asc',
|
||||
}
|
||||
};
|
||||
|
||||
const pagination = {
|
||||
initialPageSize: 20,
|
||||
pageSizeOptions: [10, 20, 50]
|
||||
};
|
||||
|
||||
const selection = {
|
||||
onSelectionChange: (selectedItems) => this.setState({ selectedItems })
|
||||
};
|
||||
|
||||
const search = {
|
||||
toolsLeft: selectedItems.length ? (
|
||||
<AutoFollowPatternDeleteProvider>
|
||||
{(deleteAutoFollowPattern) => (
|
||||
<EuiButton
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={() => deleteAutoFollowPattern(selectedItems.map(({ name }) => name))}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.deleteAutoFollowPatternButtonLabel"
|
||||
defaultMessage="Delete auto-follow {total, plural, one {pattern} other {patterns}}"
|
||||
values={{
|
||||
total: selectedItems.length
|
||||
}}
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</AutoFollowPatternDeleteProvider>
|
||||
) : undefined,
|
||||
onChange: this.onSearch,
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiInMemoryTable
|
||||
items={this.getFilteredPatterns()}
|
||||
itemId="name"
|
||||
columns={this.getTableColumns()}
|
||||
search={search}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
selection={selection}
|
||||
isSelectable={true}
|
||||
/>
|
||||
{this.renderLoading()}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { AutoFollowPatternTable } from './auto_follow_pattern_table.container';
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { DetailPanel as DetailPanelView } from './detail_panel';
|
||||
|
||||
import {
|
||||
getDetailPanelAutoFollowPattern,
|
||||
getDetailPanelAutoFollowPatternName,
|
||||
getApiStatus,
|
||||
isAutoFollowPatternDetailPanelOpen as isDetailPanelOpen,
|
||||
} from '../../../../../store/selectors';
|
||||
|
||||
import {
|
||||
closeAutoFollowPatternDetailPanel as closeDetailPanel,
|
||||
editAutoFollowPattern,
|
||||
} from '../../../../../store/actions';
|
||||
|
||||
import {
|
||||
SECTIONS
|
||||
} from '../../../../../constants';
|
||||
|
||||
const scope = SECTIONS.AUTO_FOLLOW_PATTERN;
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
isDetailPanelOpen: isDetailPanelOpen(state),
|
||||
autoFollowPattern: getDetailPanelAutoFollowPattern(state),
|
||||
autoFollowPatternName: getDetailPanelAutoFollowPatternName(state),
|
||||
apiStatus: getApiStatus(scope)(state),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
closeDetailPanel: () => {
|
||||
dispatch(closeDetailPanel());
|
||||
},
|
||||
editAutoFollowPattern: (name) => {
|
||||
dispatch(editAutoFollowPattern(name));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const DetailPanel = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DetailPanelView);
|
|
@ -0,0 +1,353 @@
|
|||
/*
|
||||
* 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, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import { getIndexListUri } from '../../../../../../../../index_management/public/services/navigation';
|
||||
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListDescription,
|
||||
EuiDescriptionListTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiLoadingSpinner,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import {
|
||||
AutoFollowPatternIndicesPreview,
|
||||
AutoFollowPatternDeleteProvider,
|
||||
} from '../../../../../components';
|
||||
|
||||
import { API_STATUS } from '../../../../../constants';
|
||||
import routing from '../../../../../services/routing';
|
||||
|
||||
export class DetailPanelUi extends Component {
|
||||
static propTypes = {
|
||||
isDetailPanelOpen: PropTypes.bool.isRequired,
|
||||
apiStatus: PropTypes.string,
|
||||
autoFollowPattern: PropTypes.object,
|
||||
autoFollowPatternName: PropTypes.string,
|
||||
closeDetailPanel: PropTypes.func.isRequired,
|
||||
editAutoFollowPattern: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
renderAutoFollowPattern() {
|
||||
const {
|
||||
autoFollowPattern: {
|
||||
followIndexPatternPrefix,
|
||||
followIndexPatternSuffix,
|
||||
remoteCluster,
|
||||
leaderIndexPatterns,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
let indexManagementFilter;
|
||||
if(followIndexPatternPrefix) {
|
||||
indexManagementFilter = `name:${followIndexPatternPrefix}`;
|
||||
} else if(followIndexPatternSuffix) {
|
||||
indexManagementFilter = `name:${followIndexPatternSuffix}`;
|
||||
}
|
||||
const indexManagementUri = getIndexListUri(indexManagementFilter);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFlyoutBody>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.statusTitle"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.remoteClusterLabel"
|
||||
defaultMessage="Remote cluster"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription>
|
||||
{remoteCluster}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.leaderPatternsLabel"
|
||||
defaultMessage="Leader patterns"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription>
|
||||
{leaderIndexPatterns.join(', ')}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.prefixLabel"
|
||||
defaultMessage="Prefix"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription>
|
||||
{followIndexPatternPrefix || (
|
||||
<em>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.prefixEmptyValue"
|
||||
defaultMessage="No prefix"
|
||||
/>
|
||||
</em>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.suffixLabel"
|
||||
defaultMessage="Suffix"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription>
|
||||
{followIndexPatternSuffix || (
|
||||
<em>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.suffixEmptyValue"
|
||||
defaultMessage="No suffix"
|
||||
/>
|
||||
</em>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<AutoFollowPatternIndicesPreview
|
||||
prefix={followIndexPatternPrefix}
|
||||
suffix={followIndexPatternSuffix}
|
||||
leaderIndexPatterns={leaderIndexPatterns}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiLink
|
||||
href={indexManagementUri}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.viewIndicesLink"
|
||||
defaultMessage="View your follower indices in Index Management"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlyoutBody>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const {
|
||||
apiStatus,
|
||||
autoFollowPattern,
|
||||
} = this.props;
|
||||
|
||||
if(apiStatus === API_STATUS.LOADING) {
|
||||
return (
|
||||
<EuiFlyoutBody>
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.loadingLabel"
|
||||
defaultMessage="Loading auto-follow pattern..."
|
||||
/>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
);
|
||||
}
|
||||
|
||||
if (!autoFollowPattern) {
|
||||
return (
|
||||
<EuiFlyoutBody>
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon size="m" type="alert" color="danger" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.notFoundLabel"
|
||||
defaultMessage="Auto-follow pattern not found"
|
||||
/>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
);
|
||||
}
|
||||
|
||||
return this.renderAutoFollowPattern();
|
||||
}
|
||||
|
||||
renderFooter() {
|
||||
const {
|
||||
editAutoFollowPattern,
|
||||
autoFollowPattern,
|
||||
autoFollowPatternName,
|
||||
closeDetailPanel,
|
||||
} = this.props;
|
||||
|
||||
if (!autoFollowPattern) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
flush="left"
|
||||
onClick={closeDetailPanel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.closeButtonLabel"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<AutoFollowPatternDeleteProvider>
|
||||
{(deleteAutoFollowPattern) => (
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
onClick={() => deleteAutoFollowPattern(autoFollowPatternName)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.deleteButtonLabel"
|
||||
defaultMessage="Delete"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</AutoFollowPatternDeleteProvider>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
editAutoFollowPattern(autoFollowPatternName);
|
||||
routing.navigate(encodeURI(`/auto_follow_patterns/edit/${encodeURIComponent(autoFollowPatternName)}`));
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.editButtonLabel"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isDetailPanelOpen,
|
||||
closeDetailPanel,
|
||||
autoFollowPatternName,
|
||||
} = this.props;
|
||||
|
||||
if (!isDetailPanelOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
data-test-subj="autoFollowPatternDetailsFlyout"
|
||||
onClose={closeDetailPanel}
|
||||
aria-labelledby="autoFollowPatternDetailsFlyoutTitle"
|
||||
size="m"
|
||||
maxWidth={400}
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m" id="autoFollowPatternDetailsFlyoutTitle">
|
||||
<h2>{autoFollowPatternName}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
{this.renderContent()}
|
||||
|
||||
{this.renderFooter()}
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DetailPanel = injectI18n(DetailPanelUi);
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { DetailPanel } from './detail_panel.container';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { AutoFollowPatternTable } from './auto_follow_pattern_table';
|
||||
export { DetailPanel } from './detail_panel';
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { AutoFollowPatternList } from './auto_follow_pattern_list.container';
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { connect } from 'react-redux';
|
||||
|
||||
import { SECTIONS } from '../../constants';
|
||||
import { getListAutoFollowPatterns, isApiAuthorized } from '../../store/selectors';
|
||||
import { CrossClusterReplicationHome as CrossClusterReplicationHomeView } from './home';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
autoFollowPatterns: getListAutoFollowPatterns(state),
|
||||
isAutoFollowApiAuthorized: isApiAuthorized(SECTIONS.AUTO_FOLLOW_PATTERN)(state)
|
||||
});
|
||||
|
||||
export const CrossClusterReplicationHome = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(CrossClusterReplicationHomeView);
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React, { PureComponent, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import chrome from 'ui/chrome';
|
||||
import { MANAGEMENT_BREADCRUMB } from 'ui/management';
|
||||
import { BASE_PATH } from '../../../../common/constants';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { listBreadcrumb } from '../../services/breadcrumbs';
|
||||
import routing from '../../services/routing';
|
||||
import { AutoFollowPatternList } from './auto_follow_pattern_list';
|
||||
import { SectionUnauthorized } from '../../components';
|
||||
|
||||
export const CrossClusterReplicationHome = injectI18n(
|
||||
class extends PureComponent {
|
||||
static propTypes = {
|
||||
autoFollowPatterns: PropTypes.array,
|
||||
}
|
||||
|
||||
state = {
|
||||
sectionActive: 'auto-follow'
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
chrome.breadcrumbs.set([ MANAGEMENT_BREADCRUMB, listBreadcrumb ]);
|
||||
}
|
||||
|
||||
getHeaderSection() {
|
||||
const { isAutoFollowApiAuthorized, autoFollowPatterns } = this.props;
|
||||
|
||||
// We want to show the title when the user isn't authorized.
|
||||
if (isAutoFollowApiAuthorized && !autoFollowPatterns.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternList.crossClusterReplicationTitle"
|
||||
defaultMessage="Cross Cluster Replication"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternList.autoFollowPatternsTitle"
|
||||
defaultMessage="Auto-follow patterns"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternList.autoFollowPatternsDescription"
|
||||
defaultMessage="Auto-follow patterns replicate leader indices from a remote
|
||||
cluster to follower indices on the local cluster."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
{isAutoFollowApiAuthorized && (
|
||||
<EuiButton
|
||||
{...routing.getRouterLinkProps('/auto_follow_patterns/add')}
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternList.addAutofollowPatternButtonLabel"
|
||||
defaultMessage="Create an auto-follow pattern"
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
getUnauthorizedSection() {
|
||||
const { isAutoFollowApiAuthorized } = this.props;
|
||||
if (!isAutoFollowApiAuthorized) {
|
||||
return (
|
||||
<SectionUnauthorized>
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPatternList.noPermissionText"
|
||||
defaultMessage="You do not have permission to view or add auto-follow patterns."
|
||||
/>
|
||||
</SectionUnauthorized>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
{this.getHeaderSection()}
|
||||
{this.getUnauthorizedSection()}
|
||||
<Switch>
|
||||
<Route exact path={`${BASE_PATH}/auto_follow_patterns`} component={AutoFollowPatternList} />
|
||||
</Switch>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { CrossClusterReplicationHome } from './home.container';
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { CrossClusterReplicationHome } from './home';
|
||||
export { AutoFollowPatternAdd } from './auto_follow_pattern_add';
|
||||
export { AutoFollowPatternEdit } from './auto_follow_pattern_edit';
|
|
@ -0,0 +1,35 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Auto-follow pattern validators validateAutoFollowPattern() returns empty object when autoFollowPattern is undefined 1`] = `Object {}`;
|
||||
|
||||
exports[`Auto-follow pattern validators validateAutoFollowPattern() should validate all props from auto-follow patten 1`] = `
|
||||
Object {
|
||||
"followIndexPatternPrefix": <FormattedMessage
|
||||
defaultMessage="Remove the {characterListLength, plural, one {character} other {characters}} {characterList} from the prefix."
|
||||
id="xpack.crossClusterReplication.autoFollowPattern.prefixValidation.illegalCharacters"
|
||||
values={
|
||||
Object {
|
||||
"characterList": <strong>
|
||||
?
|
||||
</strong>,
|
||||
"characterListLength": 1,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
"followIndexPatternSuffix": <FormattedMessage
|
||||
defaultMessage="Remove the {characterListLength, plural, one {character} other {characters}} {characterList} from the suffix."
|
||||
id="xpack.crossClusterReplication.autoFollowPattern.suffixValidation.illegalCharacters"
|
||||
values={
|
||||
Object {
|
||||
"characterList": <strong>
|
||||
?
|
||||
</strong>,
|
||||
"characterListLength": 1,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
"leaderIndexPatterns": null,
|
||||
"name": "Name can't begin with an underscore.",
|
||||
"otherProp": null,
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 chrome from 'ui/chrome';
|
||||
import { API_BASE_PATH, API_REMOTE_CLUSTERS_BASE_PATH } from '../../../common/constants';
|
||||
import { arrify } from '../../../common/services/utils';
|
||||
|
||||
const apiPrefix = chrome.addBasePath(API_BASE_PATH);
|
||||
const apiPrefixRemoteClusters = chrome.addBasePath(API_REMOTE_CLUSTERS_BASE_PATH);
|
||||
|
||||
// This is an Angular service, which is why we use this provider pattern
|
||||
// to access it within our React app.
|
||||
let httpClient;
|
||||
|
||||
export function setHttpClient(client) {
|
||||
httpClient = client;
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
const extractData = (response) => response.data;
|
||||
|
||||
export const loadAutoFollowPatterns = () => (
|
||||
httpClient.get(`${apiPrefix}/auto_follow_patterns`).then(extractData)
|
||||
);
|
||||
|
||||
export const getAutoFollowPattern = (id) => (
|
||||
httpClient.get(`${apiPrefix}/auto_follow_patterns/${encodeURIComponent(id)}`).then(extractData)
|
||||
);
|
||||
|
||||
export const loadRemoteClusters = () => (
|
||||
httpClient.get(`${apiPrefixRemoteClusters}`).then(extractData)
|
||||
);
|
||||
|
||||
export const createAutoFollowPattern = (autoFollowPattern) => (
|
||||
httpClient.post(`${apiPrefix}/auto_follow_patterns`, autoFollowPattern).then(extractData)
|
||||
);
|
||||
|
||||
export const updateAutoFollowPattern = (id, autoFollowPattern) => (
|
||||
httpClient.put(`${apiPrefix}/auto_follow_patterns/${encodeURIComponent(id)}`, autoFollowPattern).then(extractData)
|
||||
);
|
||||
|
||||
export const deleteAutoFollowPattern = (id) => {
|
||||
const ids = arrify(id).map(_id => encodeURIComponent(_id)).join(',');
|
||||
|
||||
return httpClient.delete(`${apiPrefix}/auto_follow_patterns/${ids}`).then(extractData);
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
|
||||
const getFollowPattern = (prefix = '', suffix = '', template = '{{leader_index}}') => (
|
||||
{
|
||||
followPattern: {
|
||||
prefix,
|
||||
suffix,
|
||||
template,
|
||||
},
|
||||
toString: prefix + template + suffix
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Generate an array of indices preview that would be generated for an auto-follow pattern.
|
||||
* It concatenates the prefix + the leader index pattern populated with values + the suffix
|
||||
*
|
||||
* Example of the array returned:
|
||||
* ["prefix_leader-index-0_suffix", "prefix_leader-index-1_suffix", "prefix_leader-index-2_suffix"]
|
||||
*/
|
||||
export const getPreviewIndicesFromAutoFollowPattern = ({
|
||||
prefix,
|
||||
suffix,
|
||||
leaderIndexPatterns,
|
||||
limit = 5,
|
||||
wildcardPlaceHolders = [
|
||||
moment().format('YYYY-MM-DD'),
|
||||
moment().add(1, 'days').format('YYYY-MM-DD'),
|
||||
moment().add(2, 'days').format('YYYY-MM-DD'),
|
||||
]
|
||||
}) => {
|
||||
const indicesPreview = [];
|
||||
let indexPreview;
|
||||
let leaderIndexTemplate;
|
||||
|
||||
leaderIndexPatterns.forEach((leaderIndexPattern) => {
|
||||
wildcardPlaceHolders.forEach((placeHolder) => {
|
||||
leaderIndexTemplate = leaderIndexPattern.replace(/\*/g, placeHolder);
|
||||
indexPreview = getFollowPattern(prefix, suffix, leaderIndexTemplate);
|
||||
|
||||
if (!indicesPreview.some((_indexPreview) => indexPreview.toString === _indexPreview.toString)) {
|
||||
indicesPreview.push(indexPreview);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
indicesPreview: indicesPreview.slice(0, limit),
|
||||
hasMore: indicesPreview.length > limit,
|
||||
};
|
||||
};
|
||||
|
||||
export const getPrefixSuffixFromFollowPattern = (followPattern) => {
|
||||
let followIndexPatternPrefix;
|
||||
let followIndexPatternSuffix;
|
||||
|
||||
const template = '{{leader_index}}';
|
||||
const index = followPattern.indexOf(template);
|
||||
|
||||
if (index >= 0) {
|
||||
followIndexPatternPrefix = followPattern.slice(0, index);
|
||||
followIndexPatternSuffix = followPattern.slice(index + template.length);
|
||||
}
|
||||
|
||||
return { followIndexPatternPrefix, followIndexPatternSuffix };
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import { getPreviewIndicesFromAutoFollowPattern, getPrefixSuffixFromFollowPattern } from './auto_follow_pattern';
|
||||
|
||||
describe('Auto-follo pattern service', () => {
|
||||
describe('getPreviewIndicesFromAutoFollowPattern()', () => {
|
||||
let prefix;
|
||||
let suffix;
|
||||
let leaderIndexPatterns;
|
||||
|
||||
beforeEach(() => {
|
||||
prefix = 'prefix_';
|
||||
suffix = '_suffix';
|
||||
leaderIndexPatterns = ['logstash-*'];
|
||||
});
|
||||
|
||||
it('should render a list of indices preview', () => {
|
||||
const { indicesPreview, hasMore } = getPreviewIndicesFromAutoFollowPattern({ prefix, suffix, leaderIndexPatterns });
|
||||
|
||||
expect(hasMore).toBe(false);
|
||||
expect(indicesPreview.map(preview => preview.toString)).toEqual([
|
||||
`prefix_logstash-${moment().format('YYYY-MM-DD')}_suffix`,
|
||||
`prefix_logstash-${moment().add(1, 'days').format('YYYY-MM-DD')}_suffix`,
|
||||
`prefix_logstash-${moment().add(2, 'days').format('YYYY-MM-DD')}_suffix`,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have a default limit of 5', () => {
|
||||
leaderIndexPatterns.push('other-*');
|
||||
|
||||
const { indicesPreview, hasMore } = getPreviewIndicesFromAutoFollowPattern({ prefix, suffix, leaderIndexPatterns });
|
||||
|
||||
expect(hasMore).toBe(true);
|
||||
expect(indicesPreview.map(preview => preview.toString)).toEqual([
|
||||
`prefix_logstash-${moment().format('YYYY-MM-DD')}_suffix`,
|
||||
`prefix_logstash-${moment().add(1, 'days').format('YYYY-MM-DD')}_suffix`,
|
||||
`prefix_logstash-${moment().add(2, 'days').format('YYYY-MM-DD')}_suffix`,
|
||||
`prefix_other-${moment().format('YYYY-MM-DD')}_suffix`,
|
||||
`prefix_other-${moment().add(1, 'days').format('YYYY-MM-DD')}_suffix`,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow custom limit and wildcard placeholder', () => {
|
||||
const limit = 2;
|
||||
const wildcardPlaceHolders = ['A', 'B', 'C'];
|
||||
const { indicesPreview } = getPreviewIndicesFromAutoFollowPattern({
|
||||
prefix,
|
||||
suffix,
|
||||
leaderIndexPatterns,
|
||||
limit,
|
||||
wildcardPlaceHolders
|
||||
});
|
||||
|
||||
expect(indicesPreview.map(preview => preview.toString)).toEqual([
|
||||
'prefix_logstash-A_suffix',
|
||||
'prefix_logstash-B_suffix',
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getPrefixSuffixFromFollowPattern()', () => {
|
||||
it('should extract prefix and suffix from a {{leader_index}} template', () => {
|
||||
const result = getPrefixSuffixFromFollowPattern('prefix_{{leader_index}}_suffix');
|
||||
expect(result.followIndexPatternPrefix).toEqual('prefix_');
|
||||
expect(result.followIndexPatternSuffix).toEqual('_suffix');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
ILLEGAL_CHARACTERS,
|
||||
CONTAINS_SPACES,
|
||||
validateIndexPattern as getIndexPatternErrors,
|
||||
} from 'ui/index_patterns';
|
||||
|
||||
import {
|
||||
indexNameBeginsWithPeriod,
|
||||
findIllegalCharactersInIndexName,
|
||||
indexNameContainsSpaces,
|
||||
} from 'ui/indices';
|
||||
|
||||
export const validateName = (name = '') => {
|
||||
let errorMsg = null;
|
||||
|
||||
if (!name || !name.trim()) {
|
||||
errorMsg = i18n.translate(
|
||||
'xpack.crossClusterReplication.autoFollowPattern.nameValidation.errorEmptyName',
|
||||
{ defaultMessage: 'Name is required.' }
|
||||
);
|
||||
} else {
|
||||
if (name.includes(' ')) {
|
||||
errorMsg = i18n.translate('xpack.crossClusterReplication.autoFollowPattern.nameValidation.errorSpace', {
|
||||
defaultMessage: 'Spaces are not allowed in the name.'
|
||||
});
|
||||
}
|
||||
|
||||
if (name[0] === '_') {
|
||||
errorMsg = i18n.translate(
|
||||
'xpack.crossClusterReplication.autoFollowPattern.nameValidation.errorUnderscore',
|
||||
{ defaultMessage: "Name can't begin with an underscore." }
|
||||
);
|
||||
}
|
||||
|
||||
if (name.includes(',')) {
|
||||
errorMsg = i18n.translate(
|
||||
'xpack.crossClusterReplication.autoFollowPattern.nameValidation.errorComma',
|
||||
{ defaultMessage: "Commas are not allowed in the name." }
|
||||
);
|
||||
}
|
||||
}
|
||||
return errorMsg;
|
||||
};
|
||||
|
||||
export const validateLeaderIndexPattern = (indexPattern) => {
|
||||
const errors = getIndexPatternErrors(indexPattern);
|
||||
|
||||
if (errors[ILLEGAL_CHARACTERS]) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPattern.leaderIndexPatternValidation.illegalCharacters"
|
||||
defaultMessage="Remove the {characterListLength, plural, one {character} other {characters}}
|
||||
{characterList} from the index pattern."
|
||||
values={{
|
||||
characterList: <strong>{errors[ILLEGAL_CHARACTERS].join(' ')}</strong>,
|
||||
characterListLength: errors[ILLEGAL_CHARACTERS].length,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (errors[CONTAINS_SPACES]) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPattern.leaderIndexPatternValidation.noEmptySpace"
|
||||
defaultMessage="Spaces are not allowed in the index pattern."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const validatePrefix = (prefix) => {
|
||||
// If it's empty, it is valid
|
||||
if (!prefix || !prefix.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prefix can't begin with a period, because that's reserved for system indices.
|
||||
if (indexNameBeginsWithPeriod(prefix)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPattern.prefixValidation.beginsWithPeriod"
|
||||
defaultMessage="The prefix can't begin with a period."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const illegalCharacters = findIllegalCharactersInIndexName(prefix);
|
||||
|
||||
if (illegalCharacters.length) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPattern.prefixValidation.illegalCharacters"
|
||||
defaultMessage="Remove the {characterListLength, plural, one {character} other {characters}}
|
||||
{characterList} from the prefix."
|
||||
values={{
|
||||
characterList: <strong>{illegalCharacters.join(' ')}</strong>,
|
||||
characterListLength: illegalCharacters.length,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (indexNameContainsSpaces(prefix)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPattern.prefixValidation.noEmptySpace"
|
||||
defaultMessage="Spaces are not allowed in the prefix."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const validateSuffix = (suffix) => {
|
||||
// If it's empty, it is valid
|
||||
if (!suffix || !suffix.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const illegalCharacters = findIllegalCharactersInIndexName(suffix);
|
||||
|
||||
if (illegalCharacters.length) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPattern.suffixValidation.illegalCharacters"
|
||||
defaultMessage="Remove the {characterListLength, plural, one {character} other {characters}}
|
||||
{characterList} from the suffix."
|
||||
values={{
|
||||
characterList: <strong>{illegalCharacters.join(' ')}</strong>,
|
||||
characterListLength: illegalCharacters.length,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (indexNameContainsSpaces(suffix)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.crossClusterReplication.autoFollowPattern.suffixValidation.noEmptySpace"
|
||||
defaultMessage="Spaces are not allowed in the suffix."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const validateAutoFollowPattern = (autoFollowPattern = {}) => {
|
||||
const errors = {};
|
||||
let error = null;
|
||||
let fieldValue;
|
||||
|
||||
Object.keys(autoFollowPattern).forEach((fieldName) => {
|
||||
fieldValue = autoFollowPattern[fieldName];
|
||||
error = null;
|
||||
|
||||
switch(fieldName) {
|
||||
case 'name':
|
||||
error = validateName(fieldValue);
|
||||
break;
|
||||
|
||||
case 'leaderIndexPatterns':
|
||||
if (!fieldValue.length) {
|
||||
error = {
|
||||
message: i18n.translate('xpack.crossClusterReplication.autoFollowPattern.leaderIndexPatternValidation.isEmpty', {
|
||||
defaultMessage: 'At least one leader index pattern is required.',
|
||||
})
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case 'followIndexPatternPrefix':
|
||||
error = validatePrefix(fieldValue);
|
||||
break;
|
||||
|
||||
case 'followIndexPatternSuffix':
|
||||
error = validateSuffix(fieldValue);
|
||||
break;
|
||||
}
|
||||
|
||||
errors[fieldName] = error;
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
|
@ -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 { validateAutoFollowPattern } from './auto_follow_pattern_validators';
|
||||
|
||||
jest.mock('ui/index_patterns/index_patterns.js', () => ({
|
||||
IndexPatternsProvider: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('ui/index_patterns/index_patterns_api_client_provider.js', () => ({
|
||||
IndexPatternsApiClientProvider: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Auto-follow pattern validators', () => {
|
||||
describe('validateAutoFollowPattern()', () => {
|
||||
it('returns empty object when autoFollowPattern is undefined', () => {
|
||||
const errors = validateAutoFollowPattern();
|
||||
expect(errors).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should validate all props from auto-follow patten', () => {
|
||||
const autoFollowPattern = {
|
||||
name: '_wrong-name',
|
||||
leaderIndexPatterns: ['wrong\pattern'],
|
||||
followIndexPatternPrefix: 'pre?fix_',
|
||||
followIndexPatternSuffix: '_suf?fix',
|
||||
otherProp: 'foo'
|
||||
};
|
||||
const errors = validateAutoFollowPattern(autoFollowPattern);
|
||||
expect(errors).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { BASE_PATH } from '../../../common/constants';
|
||||
|
||||
export const listBreadcrumb = {
|
||||
text: i18n.translate('xpack.crossClusterReplication.homeBreadcrumbTitle', {
|
||||
defaultMessage: 'Cross Cluster Replication',
|
||||
}),
|
||||
href: `#${BASE_PATH}`,
|
||||
};
|
||||
|
||||
export const addBreadcrumb = {
|
||||
text: i18n.translate('xpack.crossClusterReplication.addBreadcrumbTitle', {
|
||||
defaultMessage: 'Add',
|
||||
}),
|
||||
};
|
||||
|
||||
export const editBreadcrumb = {
|
||||
text: i18n.translate('xpack.crossClusterReplication.editBreadcrumbTitle', {
|
||||
defaultMessage: 'Edit',
|
||||
}),
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
|
||||
|
||||
const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`;
|
||||
|
||||
export const autoFollowPatternUrl = `${esBase}/ccr-put-auto-follow-pattern.html`;
|
|
@ -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 { parse } from 'querystring';
|
||||
|
||||
export function extractQueryParams(queryString) {
|
||||
const hrefSplit = queryString.split('?');
|
||||
if (!hrefSplit.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return parse(hrefSplit[1]);
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file based on guidance from https://github.com/elastic/eui/blob/master/wiki/react-router.md
|
||||
*/
|
||||
|
||||
import { createLocation } from 'history';
|
||||
import { stringify } from 'querystring';
|
||||
import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants';
|
||||
|
||||
const isModifiedEvent = event => !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
|
||||
|
||||
const isLeftClickEvent = event => event.button === 0;
|
||||
|
||||
const queryParamsFromObject = params => {
|
||||
if (!params) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paramsStr = stringify(params, '&', '=', {
|
||||
encodeURIComponent: (val) => val, // Don't encode special chars
|
||||
});
|
||||
return `?${paramsStr}`;
|
||||
};
|
||||
|
||||
const appToBasePathMap = {
|
||||
[APPS.CCR_APP]: BASE_PATH,
|
||||
[APPS.REMOTE_CLUSTER_APP]: BASE_PATH_REMOTE_CLUSTERS
|
||||
};
|
||||
|
||||
class Routing {
|
||||
_userHasLeftApp = false;
|
||||
_reactRouter = null;
|
||||
|
||||
/**
|
||||
* The logic for generating hrefs and onClick handlers from the `to` prop is largely borrowed from
|
||||
* https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/Link.js.
|
||||
*
|
||||
* @param {*} to URL to navigate to
|
||||
*/
|
||||
getRouterLinkProps(to, base = BASE_PATH, params = {}) {
|
||||
const search = queryParamsFromObject(params) || '';
|
||||
const location = typeof to === "string"
|
||||
? createLocation(base + to + search, null, null, this._reactRouter.history.location)
|
||||
: to;
|
||||
const href = this._reactRouter.history.createHref(location);
|
||||
|
||||
const onClick = event => {
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If target prop is set (e.g. to "_blank"), let browser handle link.
|
||||
if (event.target.getAttribute('target')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isModifiedEvent(event) || !isLeftClickEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent regular link behavior, which causes a browser refresh.
|
||||
event.preventDefault();
|
||||
this._reactRouter.history.push(location);
|
||||
};
|
||||
|
||||
return { href, onClick };
|
||||
}
|
||||
|
||||
navigate(route = '/home', app = APPS.CCR_APP, params) {
|
||||
const search = queryParamsFromObject(params);
|
||||
|
||||
this._reactRouter.history.push({
|
||||
pathname: encodeURI(appToBasePathMap[app] + route),
|
||||
search,
|
||||
});
|
||||
}
|
||||
|
||||
get reactRouter() {
|
||||
return this._reactRouter;
|
||||
}
|
||||
|
||||
set reactRouter(router) {
|
||||
this._reactRouter = router;
|
||||
}
|
||||
|
||||
get userHasLeftApp() {
|
||||
return this._userHasLeftApp;
|
||||
}
|
||||
|
||||
set userHasLeftApp(hasLeft) {
|
||||
this._userHasLeftApp = hasLeft;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Routing();
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export const objectToArray = (obj) => (
|
||||
Object.keys(obj).map(k => ({ ...obj[k], __id__: k }))
|
||||
);
|
||||
|
||||
export const arrayToObject = (array, keyProp = 'id') => (
|
||||
array.reduce((acc, item) => {
|
||||
acc[item[keyProp]] = item;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { objectToArray, arrayToObject } from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('objectToArray()', () => {
|
||||
it('should convert object to an array', () => {
|
||||
const item1 = { name: 'foo' };
|
||||
const item2 = { name: 'bar' };
|
||||
const expected = [{ ...item1, __id__: 'item1' }, { ...item2, __id__: 'item2' }];
|
||||
const output = objectToArray({ item1, item2 });
|
||||
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arrayToObject()', () => {
|
||||
it('should convert an array to array', () => {
|
||||
const item1 = { name: 'foo', customKey: 'key1' };
|
||||
const item2 = { name: 'bar', customKey: 'key2' };
|
||||
const expected = { key1: item1, key2: item2 };
|
||||
const output = arrayToObject([item1, item2], 'customKey');
|
||||
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// Api
|
||||
export const API = 'API';
|
||||
export const API_REQUEST_START = 'API_REQUEST_START';
|
||||
export const API_REQUEST_END = 'API_REQUEST_END';
|
||||
export const API_ERROR_SET = 'API_ERROR_SET';
|
||||
|
||||
// Auto Follow Pattern
|
||||
export const AUTO_FOLLOW_PATTERN_EDIT = 'AUTO_FOLLOW_PATTERN_EDIT';
|
||||
export const AUTO_FOLLOW_PATTERN_LOAD = 'AUTO_FOLLOW_PATTERN_LOAD';
|
||||
export const AUTO_FOLLOW_PATTERN_GET = 'AUTO_FOLLOW_PATTERN_GET';
|
||||
export const AUTO_FOLLOW_PATTERN_CREATE = 'AUTO_FOLLOW_PATTERN_CREATE';
|
||||
export const AUTO_FOLLOW_PATTERN_UPDATE = 'AUTO_FOLLOW_PATTERN_UPDATE';
|
||||
export const AUTO_FOLLOW_PATTERN_DELETE = 'AUTO_FOLLOW_PATTERN_DELETE';
|
||||
export const AUTO_FOLLOW_PATTERN_DETAIL_PANEL = 'AUTO_FOLLOW_PATTERN_DETAIL_PANEL';
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 * as t from '../action_types';
|
||||
import { API_STATUS } from '../../constants';
|
||||
|
||||
export const sendApiRequest = ({
|
||||
label,
|
||||
scope,
|
||||
status,
|
||||
handler,
|
||||
onSuccess = () => undefined,
|
||||
onError = () => undefined,
|
||||
}) => ({ type: t.API, payload: { label, scope, status, handler, onSuccess, onError } });
|
||||
|
||||
export const apiRequestStart = ({ label, scope, status = API_STATUS.LOADING }) => ({
|
||||
type: t.API_REQUEST_START,
|
||||
payload: { label, scope, status },
|
||||
});
|
||||
|
||||
export const apiRequestEnd = ({ label, scope }) => ({ type: t.API_REQUEST_END, payload: { label, scope } });
|
||||
|
||||
export const setApiError = ({ error, scope }) => ({
|
||||
type: t.API_ERROR_SET,
|
||||
payload: { error, scope },
|
||||
});
|
||||
|
||||
export const clearApiError = scope => ({ type: t.API_ERROR_SET, payload: { error: null, scope } });
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { SECTIONS, API_STATUS } from '../../constants';
|
||||
import {
|
||||
loadAutoFollowPatterns as loadAutoFollowPatternsRequest,
|
||||
getAutoFollowPattern as getAutoFollowPatternRequest,
|
||||
createAutoFollowPattern as createAutoFollowPatternRequest,
|
||||
updateAutoFollowPattern as updateAutoFollowPatternRequest,
|
||||
deleteAutoFollowPattern as deleteAutoFollowPatternRequest,
|
||||
} from '../../services/api';
|
||||
import routing from '../../services/routing';
|
||||
import * as t from '../action_types';
|
||||
import { sendApiRequest } from './api';
|
||||
import { getDetailPanelAutoFollowPatternName } from '../selectors';
|
||||
|
||||
const { AUTO_FOLLOW_PATTERN: scope } = SECTIONS;
|
||||
|
||||
export const editAutoFollowPattern = (name) => ({
|
||||
type: t.AUTO_FOLLOW_PATTERN_EDIT,
|
||||
payload: name
|
||||
});
|
||||
|
||||
export const openAutoFollowPatternDetailPanel = (name) => {
|
||||
return {
|
||||
type: t.AUTO_FOLLOW_PATTERN_DETAIL_PANEL,
|
||||
payload: name
|
||||
};
|
||||
};
|
||||
|
||||
export const closeAutoFollowPatternDetailPanel = () => ({
|
||||
type: t.AUTO_FOLLOW_PATTERN_DETAIL_PANEL,
|
||||
payload: null
|
||||
});
|
||||
|
||||
export const loadAutoFollowPatterns = (isUpdating = false) =>
|
||||
sendApiRequest({
|
||||
label: t.AUTO_FOLLOW_PATTERN_LOAD,
|
||||
scope,
|
||||
status: isUpdating ? API_STATUS.UPDATING : API_STATUS.LOADING,
|
||||
handler: async () => {
|
||||
return await loadAutoFollowPatternsRequest();
|
||||
},
|
||||
});
|
||||
|
||||
export const getAutoFollowPattern = (id) =>
|
||||
sendApiRequest({
|
||||
label: t.AUTO_FOLLOW_PATTERN_GET,
|
||||
scope,
|
||||
handler: async (dispatch) => (
|
||||
getAutoFollowPatternRequest(id)
|
||||
.then((response) => {
|
||||
dispatch(editAutoFollowPattern(id));
|
||||
return response;
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
export const saveAutoFollowPattern = (id, autoFollowPattern, isUpdating = false) => (
|
||||
sendApiRequest({
|
||||
label: isUpdating ? t.AUTO_FOLLOW_PATTERN_UPDATE : t.AUTO_FOLLOW_PATTERN_CREATE,
|
||||
status: API_STATUS.SAVING,
|
||||
scope,
|
||||
handler: async () => {
|
||||
if (isUpdating) {
|
||||
return await updateAutoFollowPatternRequest(id, autoFollowPattern);
|
||||
}
|
||||
return await createAutoFollowPatternRequest({ id, ...autoFollowPattern });
|
||||
},
|
||||
onSuccess() {
|
||||
const successMessage = isUpdating
|
||||
? i18n.translate('xpack.crossClusterReplication.autoFollowPattern.addAction.successMultipleNotificationTitle', {
|
||||
defaultMessage: `Auto-follow pattern '{name}' updated successfully`,
|
||||
values: { name: id },
|
||||
})
|
||||
: i18n.translate('xpack.crossClusterReplication.autoFollowPattern.addAction.successSingleNotificationTitle', {
|
||||
defaultMessage: `Added auto-follow pattern '{name}'`,
|
||||
values: { name: id },
|
||||
});
|
||||
|
||||
toastNotifications.addSuccess(successMessage);
|
||||
routing.navigate(`/auto_follow_patterns`, undefined, {
|
||||
pattern: encodeURIComponent(id),
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export const deleteAutoFollowPattern = (id) => (
|
||||
sendApiRequest({
|
||||
label: t.AUTO_FOLLOW_PATTERN_DELETE,
|
||||
scope: `${scope}-delete`,
|
||||
status: API_STATUS.DELETING,
|
||||
handler: async () => (
|
||||
deleteAutoFollowPatternRequest(id)
|
||||
),
|
||||
onSuccess(response, dispatch, getState) {
|
||||
/**
|
||||
* We can have 1 or more auto-follow pattern delete operation
|
||||
* that can fail or succeed. We will show 1 toast notification for each.
|
||||
*/
|
||||
if (response.errors.length) {
|
||||
const hasMultipleErrors = response.errors.length > 1;
|
||||
const errorMessage = hasMultipleErrors
|
||||
? i18n.translate('xpack.crossClusterReplication.autoFollowPattern.removeAction.errorMultipleNotificationTitle', {
|
||||
defaultMessage: `Error removing {count} auto-follow patterns`,
|
||||
values: { count: response.errors.length },
|
||||
})
|
||||
: i18n.translate('xpack.crossClusterReplication.autoFollowPattern.removeAction.errorSingleNotificationTitle', {
|
||||
defaultMessage: `Error removing the '{name}' auto-follow pattern`,
|
||||
values: { name: response.errors[0].id },
|
||||
});
|
||||
|
||||
toastNotifications.addDanger(errorMessage);
|
||||
}
|
||||
|
||||
if (response.itemsDeleted.length) {
|
||||
const hasMultipleDelete = response.itemsDeleted.length > 1;
|
||||
|
||||
const successMessage = hasMultipleDelete
|
||||
? i18n.translate('xpack.crossClusterReplication.autoFollowPattern.removeAction.successMultipleNotificationTitle', {
|
||||
defaultMessage: `{count} auto-follow patterns were removed`,
|
||||
values: { count: response.itemsDeleted.length },
|
||||
})
|
||||
: i18n.translate('xpack.crossClusterReplication.autoFollowPattern.removeAction.successSingleNotificationTitle', {
|
||||
defaultMessage: `Auto-follow pattern '{name}' was removed`,
|
||||
values: { name: response.itemsDeleted[0] },
|
||||
});
|
||||
|
||||
toastNotifications.addSuccess(successMessage);
|
||||
}
|
||||
|
||||
// If we've just deleted a pattern we were looking at, we need to close the panel.
|
||||
const detailPanelAutoFollowPatternName = getDetailPanelAutoFollowPatternName(getState());
|
||||
if (detailPanelAutoFollowPatternName && response.itemsDeleted.includes(detailPanelAutoFollowPatternName)) {
|
||||
dispatch(closeAutoFollowPatternDetailPanel());
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './auto_follow_pattern';
|
||||
|
||||
export * from './api';
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { ccrStore } from './store';
|
|
@ -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 * as t from '../action_types';
|
||||
import { apiRequestStart, apiRequestEnd, setApiError, clearApiError } from '../actions/api';
|
||||
|
||||
export const apiMiddleware = ({ dispatch, getState }) => next => async (action) => {
|
||||
next(action);
|
||||
|
||||
if (action.type !== t.API) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { label, scope, status, handler, onSuccess, onError } = action.payload;
|
||||
|
||||
dispatch(clearApiError(scope));
|
||||
dispatch(apiRequestStart({ label, scope, status }));
|
||||
|
||||
try {
|
||||
const response = await handler(dispatch);
|
||||
|
||||
dispatch(apiRequestEnd({ label, scope }));
|
||||
dispatch({ type: `${label}_SUCCESS`, payload: response });
|
||||
|
||||
onSuccess(response, dispatch, getState);
|
||||
|
||||
} catch (error) {
|
||||
dispatch(apiRequestEnd({ label, scope }));
|
||||
dispatch(setApiError({ error, scope }));
|
||||
dispatch({ type: `${label}_FAILURE`, payload: error });
|
||||
|
||||
onError(error, dispatch, getState);
|
||||
}
|
||||
};
|
|
@ -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 routing from '../../services/routing';
|
||||
import * as t from '../action_types';
|
||||
import { extractQueryParams } from '../../services/query_params';
|
||||
|
||||
export const autoFollowPatternMiddleware = () => next => action => {
|
||||
const { type, payload: name } = action;
|
||||
const { history } = routing.reactRouter;
|
||||
const search = history.location.search;
|
||||
const { pattern: patternName } = extractQueryParams(search);
|
||||
|
||||
switch (type) {
|
||||
case t.AUTO_FOLLOW_PATTERN_DETAIL_PANEL:
|
||||
if (!routing.userHasLeftApp) {
|
||||
// Persist state to query params by removing deep link.
|
||||
if(!name) {
|
||||
history.replace({
|
||||
search: '',
|
||||
});
|
||||
}
|
||||
// Allow the user to share a deep link to this job.
|
||||
else if (patternName !== name) {
|
||||
history.replace({
|
||||
search: `?pattern=${encodeURIComponent(name)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { apiMiddleware } from './api';
|
||||
export { autoFollowPatternMiddleware } from './auto_follow_pattern';
|
|
@ -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 { SECTIONS, API_STATUS } from '../../constants';
|
||||
import * as t from '../action_types';
|
||||
|
||||
export const initialState = {
|
||||
status: {
|
||||
[SECTIONS.AUTO_FOLLOW_PATTERN]: API_STATUS.IDLE,
|
||||
[SECTIONS.INDEX_FOLLOWER]: API_STATUS.IDLE,
|
||||
},
|
||||
error: {
|
||||
[SECTIONS.AUTO_FOLLOW_PATTERN]: null,
|
||||
[SECTIONS.INDEX_FOLLOWER]: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const reducer = (state = initialState, action) => {
|
||||
const payload = action.payload || {};
|
||||
const { scope, status, error } = payload;
|
||||
|
||||
switch (action.type) {
|
||||
case t.API_REQUEST_START: {
|
||||
return { ...state, status: { ...state.status, [scope]: status } };
|
||||
}
|
||||
case t.API_REQUEST_END: {
|
||||
return { ...state, status: { ...state.status, [scope]: API_STATUS.IDLE } };
|
||||
}
|
||||
case t.API_ERROR_SET: {
|
||||
return { ...state, error: { ...state.error, [scope]: error } };
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { reducer, initialState } from './api';
|
||||
import { API_STATUS } from '../../constants';
|
||||
import { apiRequestStart, apiRequestEnd, setApiError } from '../actions';
|
||||
|
||||
describe('CCR Api reducers', () => {
|
||||
const scope = 'testSection';
|
||||
|
||||
it('API_REQUEST_START should set the Api status to "loading" on scope', () => {
|
||||
const result = reducer(initialState, apiRequestStart({ scope }));
|
||||
|
||||
expect(result.status[scope]).toEqual(API_STATUS.LOADING);
|
||||
});
|
||||
|
||||
it('API_END should set the Api status to "idle" on scope', () => {
|
||||
const updatedState = reducer(initialState, apiRequestStart({ scope }));
|
||||
const result = reducer(updatedState, apiRequestEnd({ scope }));
|
||||
|
||||
expect(result.status[scope]).toEqual(API_STATUS.IDLE);
|
||||
});
|
||||
|
||||
it('API_ERROR_SET should set the Api error on scope', () => {
|
||||
const error = { foo: 'bar' };
|
||||
const result = reducer(initialState, setApiError({ error, scope }));
|
||||
|
||||
expect(result.error[scope]).toBe(error);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as t from '../action_types';
|
||||
import { arrayToObject } from '../../services/utils';
|
||||
import { getPrefixSuffixFromFollowPattern } from '../../services/auto_follow_pattern';
|
||||
|
||||
const initialState = {
|
||||
byId: {},
|
||||
selectedId: null,
|
||||
detailPanelId: null,
|
||||
};
|
||||
|
||||
const success = action => `${action}_SUCCESS`;
|
||||
|
||||
const parseAutoFollowPattern = (autoFollowPattern) => {
|
||||
// Extract prefix and suffix from follow index pattern
|
||||
const { followIndexPatternPrefix, followIndexPatternSuffix } = getPrefixSuffixFromFollowPattern(autoFollowPattern.followIndexPattern);
|
||||
|
||||
return { ...autoFollowPattern, followIndexPatternPrefix, followIndexPatternSuffix };
|
||||
};
|
||||
|
||||
export const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case success(t.AUTO_FOLLOW_PATTERN_LOAD): {
|
||||
return { ...state, byId: arrayToObject(action.payload.patterns.map(parseAutoFollowPattern), 'name') };
|
||||
}
|
||||
case success(t.AUTO_FOLLOW_PATTERN_GET): {
|
||||
return { ...state, byId: { ...state.byId, [action.payload.name]: parseAutoFollowPattern(action.payload) } };
|
||||
}
|
||||
case t.AUTO_FOLLOW_PATTERN_EDIT: {
|
||||
return { ...state, selectedId: action.payload };
|
||||
}
|
||||
case t.AUTO_FOLLOW_PATTERN_DETAIL_PANEL: {
|
||||
return { ...state, detailPanelId: action.payload };
|
||||
}
|
||||
case success(t.AUTO_FOLLOW_PATTERN_DELETE): {
|
||||
const byId = { ...state.byId };
|
||||
const { itemsDeleted } = action.payload;
|
||||
itemsDeleted.forEach(id => delete byId[id]);
|
||||
return { ...state, byId };
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -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 { combineReducers } from 'redux';
|
||||
import { reducer as api } from './api';
|
||||
import { reducer as autoFollowPattern } from './auto_follow_pattern';
|
||||
|
||||
export const ccr = combineReducers({
|
||||
autoFollowPattern,
|
||||
api,
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { createSelector } from 'reselect';
|
||||
import { objectToArray } from '../../services/utils';
|
||||
|
||||
// Api
|
||||
export const getApiState = (state) => state.api;
|
||||
export const getApiStatus = (scope) => createSelector(getApiState, (apiState) => apiState.status[scope]);
|
||||
export const getApiError = (scope) => createSelector(getApiState, (apiState) => apiState.error[scope]);
|
||||
export const isApiAuthorized = (scope) => createSelector(getApiError(scope), (error) => {
|
||||
if (!error) {
|
||||
return true;
|
||||
}
|
||||
return error.status !== 403;
|
||||
});
|
||||
|
||||
// Auto-follow pattern
|
||||
export const getAutoFollowPatternState = (state) => state.autoFollowPattern;
|
||||
export const getAutoFollowPatterns = createSelector(getAutoFollowPatternState, (autoFollowPatternsState) => autoFollowPatternsState.byId);
|
||||
export const getDetailPanelAutoFollowPatternName = createSelector(getAutoFollowPatternState,
|
||||
(autoFollowPatternsState) => autoFollowPatternsState.detailPanelId);
|
||||
export const getSelectedAutoFollowPattern = createSelector(getAutoFollowPatternState, (autoFollowPatternsState) => {
|
||||
if(!autoFollowPatternsState.selectedId) {
|
||||
return null;
|
||||
}
|
||||
return autoFollowPatternsState.byId[autoFollowPatternsState.selectedId];
|
||||
});
|
||||
export const isAutoFollowPatternDetailPanelOpen = createSelector(getAutoFollowPatternState, (autoFollowPatternsState) => {
|
||||
return !!autoFollowPatternsState.detailPanelId;
|
||||
});
|
||||
export const getDetailPanelAutoFollowPattern = createSelector(getAutoFollowPatternState, (autoFollowPatternsState) => {
|
||||
if(!autoFollowPatternsState.detailPanelId) {
|
||||
return null;
|
||||
}
|
||||
return autoFollowPatternsState.byId[autoFollowPatternsState.detailPanelId];
|
||||
});
|
||||
export const getListAutoFollowPatterns = createSelector(getAutoFollowPatterns, (autoFollowPatterns) => objectToArray(autoFollowPatterns));
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { applyMiddleware, compose, createStore } from 'redux';
|
||||
|
||||
import { apiMiddleware, autoFollowPatternMiddleware } from './middleware';
|
||||
import { ccr } from './reducers';
|
||||
|
||||
function createCrossClusterReplicationStore(initialState = {}) {
|
||||
const enhancers = [applyMiddleware(apiMiddleware, autoFollowPatternMiddleware)];
|
||||
|
||||
if (window.__REDUX_DEVTOOLS_EXTENSION__) {
|
||||
enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__());
|
||||
}
|
||||
return createStore(ccr, initialState, compose(...enhancers));
|
||||
}
|
||||
|
||||
export const ccrStore = createCrossClusterReplicationStore();
|
8
x-pack/plugins/cross_cluster_replication/public/index.js
Normal file
8
x-pack/plugins/cross_cluster_replication/public/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 './register_ccr_section';
|
||||
import './register_routes';
|
13
x-pack/plugins/cross_cluster_replication/public/index.scss
Normal file
13
x-pack/plugins/cross_cluster_replication/public/index.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
// Import the EUI global scope so we can use EUI constants
|
||||
@import 'ui/public/styles/_styling_constants';
|
||||
|
||||
// Cross Cluster Replication plugin styles
|
||||
|
||||
// Prefix all styles with "ccr" to avoid conflicts.
|
||||
// Examples
|
||||
// ccrChart
|
||||
// ccrChart__legend
|
||||
// ccrChart__legend--small
|
||||
// ccrChart__legend-isLoading
|
||||
|
||||
@import 'app/app';
|
|
@ -0,0 +1,3 @@
|
|||
<kbn-management-app section="elasticsearch/ccr">
|
||||
<div id="ccrReactRoot"></div>
|
||||
</kbn-management-app>
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { management } from 'ui/management';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import chrome from 'ui/chrome';
|
||||
import { BASE_PATH } from '../common/constants';
|
||||
|
||||
if (chrome.getInjected('ccrUiEnabled')) {
|
||||
const esSection = management.getSection('elasticsearch');
|
||||
|
||||
esSection.register('ccr', {
|
||||
visible: true,
|
||||
display: i18n.translate('xpack.crossClusterReplication.appTitle', { defaultMessage: 'Cross Cluster Replication' }),
|
||||
order: 3,
|
||||
url: `#${BASE_PATH}`
|
||||
});
|
||||
}
|
|
@ -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 routes from 'ui/routes';
|
||||
import { unmountComponentAtNode } from 'react-dom';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
import template from './main.html';
|
||||
import { BASE_PATH } from '../common/constants/base_path';
|
||||
import { renderReact } from './app';
|
||||
import { setHttpClient } from './app/services/api';
|
||||
|
||||
if (chrome.getInjected('ccrUiEnabled')) {
|
||||
let elem;
|
||||
|
||||
const CCR_REACT_ROOT = 'ccrReactRoot';
|
||||
|
||||
const unmountReactApp = () => elem && unmountComponentAtNode(elem);
|
||||
|
||||
routes.when(`${BASE_PATH}/:section?/:view?/:id?`, {
|
||||
template: template,
|
||||
controllerAs: 'ccr',
|
||||
controller: class CrossClusterReplicationController {
|
||||
constructor($scope, $route, $http) {
|
||||
/**
|
||||
* React-router's <Redirect> does not play wall 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();
|
||||
|
||||
// NOTE: We depend upon Angular's $http service because it's decorated with interceptors,
|
||||
// e.g. to check license status per request.
|
||||
setHttpClient($http);
|
||||
|
||||
$scope.$$postDigest(() => {
|
||||
elem = document.getElementById(CCR_REACT_ROOT);
|
||||
renderReact(elem);
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export const elasticsearchJsPlugin = (Client, config, components) => {
|
||||
const ca = components.clientAction.factory;
|
||||
|
||||
Client.prototype.ccr = components.clientAction.namespaceFactory();
|
||||
const ccr = Client.prototype.ccr.prototype;
|
||||
|
||||
ccr.autoFollowPatterns = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_ccr/auto_follow',
|
||||
}
|
||||
],
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
ccr.autoFollowPattern = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_ccr/auto_follow/<%=id%>',
|
||||
req: {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
ccr.saveAutoFollowPattern = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_ccr/auto_follow/<%=id%>',
|
||||
req: {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
needBody: true,
|
||||
method: 'PUT'
|
||||
});
|
||||
|
||||
ccr.deleteAutoFollowPattern = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_ccr/auto_follow/<%=id%>',
|
||||
req: {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
needBody: true,
|
||||
method: 'DELETE'
|
||||
});
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const deserializeAutoFollowPattern = (
|
||||
{ name, pattern: { remote_cluster, leader_index_patterns, follow_index_pattern } } = { pattern: {} } // eslint-disable-line camelcase
|
||||
) => ({
|
||||
name,
|
||||
remoteCluster: remote_cluster,
|
||||
leaderIndexPatterns: leader_index_patterns,
|
||||
followIndexPattern: follow_index_pattern,
|
||||
});
|
||||
|
||||
export const deserializeListAutoFollowPatterns = autoFollowPatterns =>
|
||||
autoFollowPatterns.map(deserializeAutoFollowPattern);
|
||||
|
||||
export const serializeAutoFollowPattern = ({
|
||||
remoteCluster,
|
||||
leaderIndexPatterns,
|
||||
followIndexPattern,
|
||||
}) => ({
|
||||
remote_cluster: remoteCluster,
|
||||
leader_index_patterns: leaderIndexPatterns,
|
||||
follow_index_pattern: followIndexPattern,
|
||||
});
|
|
@ -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 {
|
||||
deserializeAutoFollowPattern,
|
||||
deserializeListAutoFollowPatterns,
|
||||
serializeAutoFollowPattern,
|
||||
} from './auto_follow_pattern_serialization';
|
||||
|
||||
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',
|
||||
remoteCluster: 'foo',
|
||||
leaderIndexPatterns: ['foo-*'],
|
||||
followIndexPattern: 'bar'
|
||||
};
|
||||
|
||||
const esObject = {
|
||||
name: 'some-name',
|
||||
pattern: {
|
||||
remote_cluster: expected.remoteCluster,
|
||||
leader_index_patterns: expected.leaderIndexPatterns,
|
||||
follow_index_pattern: expected.followIndexPattern
|
||||
}
|
||||
};
|
||||
|
||||
expect(deserializeAutoFollowPattern(esObject)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deserializeListAutoFollowPatterns()', () => {
|
||||
it('should deserialize list of Elasticsearch objects', () => {
|
||||
const name1 = 'foo1';
|
||||
const name2 = 'foo2';
|
||||
|
||||
const expected = [
|
||||
{
|
||||
name: name1,
|
||||
remoteCluster: 'foo1',
|
||||
leaderIndexPatterns: ['foo1-*'],
|
||||
followIndexPattern: 'bar2'
|
||||
},
|
||||
{
|
||||
name: name2,
|
||||
remoteCluster: 'foo2',
|
||||
leaderIndexPatterns: ['foo2-*'],
|
||||
followIndexPattern: 'bar2'
|
||||
}
|
||||
];
|
||||
|
||||
const esObjects = {
|
||||
patterns: [
|
||||
{
|
||||
name: name1,
|
||||
pattern: {
|
||||
remote_cluster: expected[0].remoteCluster,
|
||||
leader_index_patterns: expected[0].leaderIndexPatterns,
|
||||
follow_index_pattern: expected[0].followIndexPattern
|
||||
}
|
||||
},
|
||||
{
|
||||
name: name2,
|
||||
pattern: {
|
||||
remote_cluster: expected[1].remoteCluster,
|
||||
leader_index_patterns: expected[1].leaderIndexPatterns,
|
||||
follow_index_pattern: expected[1].followIndexPattern
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(deserializeListAutoFollowPatterns(esObjects.patterns)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeAutoFollowPattern()', () => {
|
||||
it('should serialize object to Elasticsearch object', () => {
|
||||
const expected = {
|
||||
remote_cluster: 'foo',
|
||||
leader_index_patterns: ['bar-*'],
|
||||
follow_index_pattern: 'faz'
|
||||
};
|
||||
|
||||
const object = {
|
||||
remoteCluster: expected.remote_cluster,
|
||||
leaderIndexPatterns: expected.leader_index_patterns,
|
||||
followIndexPattern: expected.follow_index_pattern
|
||||
};
|
||||
|
||||
expect(serializeAutoFollowPattern(object)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { once } from 'lodash';
|
||||
import { elasticsearchJsPlugin } from '../../client/elasticsearch_ccr';
|
||||
|
||||
const callWithRequest = once(server => {
|
||||
const config = {
|
||||
plugins: [ elasticsearchJsPlugin ],
|
||||
...server.config().get('elasticsearch')
|
||||
};
|
||||
const cluster = server.plugins.elasticsearch.createCluster('ccr', config);
|
||||
return cluster.callWithRequest;
|
||||
});
|
||||
|
||||
export const callWithRequestFactory = (server, request) => {
|
||||
return (...args) => {
|
||||
return callWithRequest(server)(request, ...args);
|
||||
};
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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';
|
|
@ -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.
|
||||
*/
|
||||
|
||||
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: `You cannot use ${pluginName} because license information is not available at this time.`,
|
||||
};
|
||||
}
|
||||
|
||||
const VALID_LICENSE_MODES = ['trial', 'basic', 'standard', 'gold', 'platinum'];
|
||||
|
||||
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,
|
||||
showLinks: false,
|
||||
message: `Your ${licenseType} license does not support ${pluginName}. Please upgrade your license.`,
|
||||
};
|
||||
}
|
||||
|
||||
// License is valid but not active
|
||||
if (!isLicenseActive) {
|
||||
return {
|
||||
isAvailable: false,
|
||||
showLinks: true,
|
||||
enableLinks: false,
|
||||
message: `You cannot use ${pluginName} because your ${licenseType} license has expired.`,
|
||||
};
|
||||
}
|
||||
|
||||
// License is valid and active
|
||||
return {
|
||||
isAvailable: true,
|
||||
showLinks: true,
|
||||
enableLinks: true,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { checkLicense } from './check_license';
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 'expect.js';
|
||||
import { wrapCustomError } from '../wrap_custom_error';
|
||||
|
||||
describe('wrap_custom_error', () => {
|
||||
describe('#wrapCustomError', () => {
|
||||
it('should return a Boom object', () => {
|
||||
const originalError = new Error('I am an error');
|
||||
const statusCode = 404;
|
||||
const wrappedError = wrapCustomError(originalError, statusCode);
|
||||
|
||||
expect(wrappedError.isBoom).to.be(true);
|
||||
expect(wrappedError.output.statusCode).to.equal(statusCode);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 'expect.js';
|
||||
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 a Boom object', () => {
|
||||
const wrappedError = wrapEsError(originalError);
|
||||
|
||||
expect(wrappedError.isBoom).to.be(true);
|
||||
});
|
||||
|
||||
it('should return the correct Boom object', () => {
|
||||
const wrappedError = wrapEsError(originalError);
|
||||
|
||||
expect(wrappedError.output.statusCode).to.be(originalError.statusCode);
|
||||
expect(wrappedError.output.payload.message).to.be(originalError.message);
|
||||
});
|
||||
|
||||
it('should return the correct Boom object with custom message', () => {
|
||||
const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' });
|
||||
|
||||
expect(wrappedError.output.statusCode).to.be(originalError.statusCode);
|
||||
expect(wrappedError.output.payload.message).to.be('No encontrado!');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 expect from 'expect.js';
|
||||
import { wrapUnknownError } from '../wrap_unknown_error';
|
||||
|
||||
describe('wrap_unknown_error', () => {
|
||||
describe('#wrapUnknownError', () => {
|
||||
it('should return a Boom object', () => {
|
||||
const originalError = new Error('I am an error');
|
||||
const wrappedError = wrapUnknownError(originalError);
|
||||
|
||||
expect(wrappedError.isBoom).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { wrapCustomError } from './wrap_custom_error';
|
||||
export { wrapEsError } from './wrap_es_error';
|
||||
export { wrapUnknownError } from './wrap_unknown_error';
|
|
@ -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 Boom from 'boom';
|
||||
|
||||
/**
|
||||
* Wraps a custom error into a Boom error response and returns it
|
||||
*
|
||||
* @param err Object error
|
||||
* @param statusCode Error status code
|
||||
* @return Object Boom error response
|
||||
*/
|
||||
export function wrapCustomError(err, statusCode) {
|
||||
return Boom.boomify(err, { statusCode });
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue