mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* Extract isEsError from router.
This commit is contained in:
parent
101b4d56cc
commit
32a5c22639
151 changed files with 3256 additions and 3755 deletions
|
@ -5,9 +5,9 @@
|
|||
*/
|
||||
|
||||
import { registerTestBed } from '../../../../../test_utils';
|
||||
import { RemoteClusterAdd } from '../../../public/sections/remote_cluster_add';
|
||||
import { createRemoteClustersStore } from '../../../public/store';
|
||||
import { registerRouter } from '../../../public/services/routing';
|
||||
import { RemoteClusterAdd } from '../../../public/app/sections/remote_cluster_add';
|
||||
import { createRemoteClustersStore } from '../../../public/app/store';
|
||||
import { registerRouter } from '../../../public/app/services/routing';
|
||||
|
||||
const testBedConfig = {
|
||||
store: createRemoteClustersStore,
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
*/
|
||||
|
||||
import { registerTestBed } from '../../../../../test_utils';
|
||||
import { RemoteClusterEdit } from '../../../public/sections/remote_cluster_edit';
|
||||
import { createRemoteClustersStore } from '../../../public/store';
|
||||
import { registerRouter } from '../../../public/services/routing';
|
||||
import { RemoteClusterEdit } from '../../../public/app/sections/remote_cluster_edit';
|
||||
import { createRemoteClustersStore } from '../../../public/app/store';
|
||||
import { registerRouter } from '../../../public/app/services/routing';
|
||||
|
||||
import { REMOTE_CLUSTER_EDIT_NAME } from './constants';
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
*/
|
||||
|
||||
import { registerTestBed, findTestSubject } from '../../../../../test_utils';
|
||||
import { RemoteClusterList } from '../../../public/sections/remote_cluster_list';
|
||||
import { createRemoteClustersStore } from '../../../public/store';
|
||||
import { registerRouter } from '../../../public/services/routing';
|
||||
import { RemoteClusterList } from '../../../public/app/sections/remote_cluster_list';
|
||||
import { createRemoteClustersStore } from '../../../public/app/store';
|
||||
import { registerRouter } from '../../../public/app/services/routing';
|
||||
|
||||
const testBedConfig = {
|
||||
store: createRemoteClustersStore,
|
||||
|
|
|
@ -6,13 +6,23 @@
|
|||
|
||||
import axios from 'axios';
|
||||
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
|
||||
import chrome from 'ui/chrome'; // eslint-disable-line import/no-unresolved
|
||||
import { MANAGEMENT_BREADCRUMB } from 'ui/management'; // eslint-disable-line import/no-unresolved
|
||||
import { fatalError, toastNotifications } from 'ui/notify'; // eslint-disable-line import/no-unresolved
|
||||
|
||||
import { setHttpClient } from '../../../public/services/api';
|
||||
import { init as initBreadcrumb } from '../../../public/app/services/breadcrumb';
|
||||
import { init as initHttp } from '../../../public/app/services/http';
|
||||
import { init as initNotification } from '../../../public/app/services/notification';
|
||||
import { init as initHttpRequests } from './http_requests';
|
||||
|
||||
export const setupEnvironment = () => {
|
||||
chrome.breadcrumbs = {
|
||||
set: () => {},
|
||||
};
|
||||
// axios has a $http like interface so using it to simulate $http
|
||||
setHttpClient(axios.create({ adapter: axiosXhrAdapter }));
|
||||
initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path);
|
||||
initBreadcrumb(() => {}, MANAGEMENT_BREADCRUMB);
|
||||
initNotification(toastNotifications, fatalError);
|
||||
|
||||
const { server, httpRequestsMockHelpers } = initHttpRequests();
|
||||
|
||||
|
|
|
@ -4,14 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { pageHelpers, nextTick } from './helpers';
|
||||
import { pageHelpers, nextTick, setupEnvironment } from './helpers';
|
||||
import { NON_ALPHA_NUMERIC_CHARS, ACCENTED_CHARS } from './helpers/constants';
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
addBasePath: (path) => path || 'api/cross_cluster_replication',
|
||||
breadcrumbs: { set: () => {} },
|
||||
}));
|
||||
|
||||
const { setup } = pageHelpers.remoteClustersAdd;
|
||||
|
||||
describe('Create Remote cluster', () => {
|
||||
|
@ -20,6 +15,15 @@ describe('Create Remote cluster', () => {
|
|||
let exists;
|
||||
let actions;
|
||||
let form;
|
||||
let server;
|
||||
|
||||
beforeAll(() => {
|
||||
({ server } = setupEnvironment());
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
({ form, exists, find, actions } = setup());
|
||||
|
|
|
@ -4,15 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { RemoteClusterForm } from '../../public/sections/components/remote_cluster_form';
|
||||
import { RemoteClusterForm } from '../../public/app/sections/components/remote_cluster_form';
|
||||
import { pageHelpers, setupEnvironment, nextTick } from './helpers';
|
||||
import { REMOTE_CLUSTER_EDIT, REMOTE_CLUSTER_EDIT_NAME } from './helpers/constants';
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
addBasePath: (path) => path || '/api/remote_clusters',
|
||||
breadcrumbs: { set: () => {} },
|
||||
}));
|
||||
|
||||
const { setup } = pageHelpers.remoteClustersEdit;
|
||||
const { setup: setupRemoteClustersAdd } = pageHelpers.remoteClustersAdd;
|
||||
|
||||
|
|
|
@ -6,28 +6,9 @@
|
|||
|
||||
import { pageHelpers, setupEnvironment, nextTick, getRandomString, findTestSubject } from './helpers';
|
||||
|
||||
import { getRouter } from '../../public/services';
|
||||
import { getRouter } from '../../public/app/services';
|
||||
import { getRemoteClusterMock } from '../../fixtures/remote_cluster';
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
addBasePath: (path) => path || '/api/remote_clusters',
|
||||
breadcrumbs: { set: () => {} },
|
||||
getInjected: (key) => {
|
||||
if (key === 'uiCapabilities') {
|
||||
return {
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {}
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected call to chrome.getInjected with key ${key}`);
|
||||
}
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../src/legacy/core_plugins/ui_metric/public', () => ({
|
||||
trackUiMetric: jest.fn(),
|
||||
}));
|
||||
|
||||
const { setup } = pageHelpers.remoteClustersList;
|
||||
|
||||
describe('<RemoteClusterList />', () => {
|
||||
|
|
21
x-pack/plugins/remote_clusters/common/index.ts
Normal file
21
x-pack/plugins/remote_clusters/common/index.ts
Normal file
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
import { LICENSE_TYPE_BASIC, LicenseType } from '../../../common/constants';
|
||||
|
||||
export const PLUGIN = {
|
||||
ID: 'remote_clusters',
|
||||
// Remote Clusters are used in both CCS and CCR, and CCS is available for all licenses.
|
||||
MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType,
|
||||
getI18nName: (): string => {
|
||||
return i18n.translate('xpack.remoteClusters.appName', {
|
||||
defaultMessage: 'Remote Clusters',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const API_BASE_PATH = '/api/remote_clusters';
|
|
@ -4,58 +4,64 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Legacy } from 'kibana';
|
||||
import { resolve } from 'path';
|
||||
import { PLUGIN } from './common';
|
||||
import { registerLicenseChecker } from './server/lib/register_license_checker';
|
||||
import {
|
||||
registerListRoute,
|
||||
registerAddRoute,
|
||||
registerUpdateRoute,
|
||||
registerDeleteRoute,
|
||||
} from './server/routes/api/remote_clusters';
|
||||
import { Plugin as RemoteClustersPlugin } from './plugin';
|
||||
import { createShim } from './shim';
|
||||
|
||||
export function remoteClusters(kibana) {
|
||||
export function remoteClusters(kibana: any) {
|
||||
return new kibana.Plugin({
|
||||
id: PLUGIN.ID,
|
||||
configPrefix: 'xpack.remote_clusters',
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
// xpack_main is required for license checking.
|
||||
require: ['kibana', 'elasticsearch', 'xpack_main', 'index_management'],
|
||||
uiExports: {
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
managementSections: [
|
||||
'plugins/remote_clusters',
|
||||
],
|
||||
injectDefaultVars(server) {
|
||||
managementSections: ['plugins/remote_clusters'],
|
||||
injectDefaultVars(server: Legacy.Server) {
|
||||
const config = server.config();
|
||||
return {
|
||||
remoteClustersUiEnabled: config.get('xpack.remote_clusters.ui.enabled'),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
config(Joi) {
|
||||
config(Joi: any) {
|
||||
return Joi.object({
|
||||
// display menu item
|
||||
ui: Joi.object({
|
||||
enabled: Joi.boolean().default(true)
|
||||
enabled: Joi.boolean().default(true),
|
||||
}).default(),
|
||||
|
||||
// enable plugin
|
||||
enabled: Joi.boolean().default(true),
|
||||
}).default();
|
||||
},
|
||||
isEnabled(config) {
|
||||
isEnabled(config: any) {
|
||||
return (
|
||||
config.get('xpack.remote_clusters.enabled') &&
|
||||
config.get('xpack.index_management.enabled')
|
||||
config.get('xpack.remote_clusters.enabled') && config.get('xpack.index_management.enabled')
|
||||
);
|
||||
},
|
||||
init(server: Legacy.Server) {
|
||||
const {
|
||||
coreSetup,
|
||||
pluginsSetup: {
|
||||
license: { registerLicenseChecker },
|
||||
},
|
||||
} = createShim(server, PLUGIN.ID);
|
||||
|
||||
const remoteClustersPlugin = new RemoteClustersPlugin();
|
||||
|
||||
// Set up plugin.
|
||||
remoteClustersPlugin.setup(coreSetup);
|
||||
|
||||
registerLicenseChecker(
|
||||
server,
|
||||
PLUGIN.ID,
|
||||
PLUGIN.getI18nName(),
|
||||
PLUGIN.MINIMUM_LICENSE_REQUIRED
|
||||
);
|
||||
},
|
||||
init: function (server) {
|
||||
registerLicenseChecker(server);
|
||||
registerListRoute(server);
|
||||
registerAddRoute(server);
|
||||
registerUpdateRoute(server);
|
||||
registerDeleteRoute(server);
|
||||
}
|
||||
});
|
||||
}
|
30
x-pack/plugins/remote_clusters/plugin.ts
Normal file
30
x-pack/plugins/remote_clusters/plugin.ts
Normal file
|
@ -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 { API_BASE_PATH } from './common';
|
||||
import { CoreSetup } from './shim';
|
||||
import {
|
||||
registerGetRoute,
|
||||
registerAddRoute,
|
||||
registerUpdateRoute,
|
||||
registerDeleteRoute,
|
||||
} from './server/routes/api';
|
||||
|
||||
export class Plugin {
|
||||
public setup(core: CoreSetup): void {
|
||||
const {
|
||||
http: { createRouter, isEsError },
|
||||
} = core;
|
||||
|
||||
const router = createRouter(API_BASE_PATH);
|
||||
|
||||
// Register routes.
|
||||
registerGetRoute(router);
|
||||
registerAddRoute(router);
|
||||
registerUpdateRoute(router);
|
||||
registerDeleteRoute(router, isEsError);
|
||||
}
|
||||
}
|
|
@ -4,8 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export {
|
||||
CRUD_APP_BASE_PATH,
|
||||
} from './paths';
|
||||
export { CRUD_APP_BASE_PATH } from './paths';
|
||||
|
||||
export * from './ui_metric';
|
|
@ -4,6 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const PLUGIN = {
|
||||
ID: 'remote_clusters'
|
||||
};
|
||||
export const CRUD_APP_BASE_PATH: string = '/management/elasticsearch/remote_clusters';
|
|
@ -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 const UIM_APP_NAME: string = 'remote_clusters';
|
||||
|
||||
export const UIM_APP_LOAD: string = 'app_load';
|
||||
export const UIM_CLUSTER_ADD: string = 'cluster_add';
|
||||
export const UIM_CLUSTER_UPDATE: string = 'cluster_update';
|
||||
export const UIM_CLUSTER_REMOVE: string = 'cluster_remove';
|
||||
export const UIM_CLUSTER_REMOVE_MANY: string = 'cluster_remove_many';
|
||||
export const UIM_SHOW_DETAILS_CLICK: string = 'show_details_click';
|
25
x-pack/plugins/remote_clusters/public/app/index.js
Normal file
25
x-pack/plugins/remote_clusters/public/app/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { App } from './app';
|
||||
import { remoteClustersStore } from './store';
|
||||
|
||||
export const renderReact = async (elem, I18nContext) => {
|
||||
render(
|
||||
<I18nContext>
|
||||
<Provider store={remoteClustersStore}>
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
</I18nContext>
|
||||
, elem);
|
||||
};
|
|
@ -5,12 +5,13 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
export function ConfiguredByNodeWarning() {
|
||||
return (
|
||||
<EuiCallOut
|
|
@ -173,14 +173,12 @@ Array [
|
|||
id="mockId-help"
|
||||
>
|
||||
An IP address or host name, followed by the
|
||||
<a
|
||||
<button
|
||||
class="euiLink euiLink--primary"
|
||||
href="undefinedguide/en/elasticsearch/reference/undefined/modules-transport.html"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
type="button"
|
||||
>
|
||||
transport port
|
||||
</a>
|
||||
</button>
|
||||
of the remote cluster.
|
||||
</div>
|
||||
</div>
|
||||
|
@ -218,14 +216,12 @@ Array [
|
|||
Skip if unavailable
|
||||
</strong>
|
||||
.
|
||||
<a
|
||||
<button
|
||||
class="euiLink euiLink--primary"
|
||||
href="undefinedguide/en/elasticsearch/reference/undefined/modules-cross-cluster-search.html#_skipping_disconnected_clusters"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
type="button"
|
||||
>
|
||||
Learn more.
|
||||
</a>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -439,14 +435,12 @@ Array [
|
|||
id="mockId-help"
|
||||
>
|
||||
An IP address or host name, followed by the
|
||||
<a
|
||||
<button
|
||||
class="euiLink euiLink--primary"
|
||||
href="undefinedguide/en/elasticsearch/reference/undefined/modules-transport.html"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
type="button"
|
||||
>
|
||||
transport port
|
||||
</a>
|
||||
</button>
|
||||
of the remote cluster.
|
||||
</div>
|
||||
</div>,
|
|
@ -0,0 +1,613 @@
|
|||
/*
|
||||
* 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 { merge } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiComboBox,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiLoadingKibana,
|
||||
EuiLoadingSpinner,
|
||||
EuiOverlayMask,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import {
|
||||
skippingDisconnectedClustersUrl,
|
||||
transportPortUrl,
|
||||
} from '../../../services/documentation';
|
||||
|
||||
import { validateName, validateSeeds, validateSeed } from './validators';
|
||||
|
||||
const defaultFields = {
|
||||
name: '',
|
||||
seeds: [],
|
||||
skipUnavailable: false,
|
||||
};
|
||||
|
||||
export class RemoteClusterForm extends Component {
|
||||
static propTypes = {
|
||||
save: PropTypes.func.isRequired,
|
||||
cancel: PropTypes.func,
|
||||
isSaving: PropTypes.bool,
|
||||
saveError: PropTypes.object,
|
||||
fields: PropTypes.object,
|
||||
disabledFields: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
fields: merge({}, defaultFields),
|
||||
disabledFields: {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { fields, disabledFields } = props;
|
||||
const fieldsState = merge({}, defaultFields, fields);
|
||||
|
||||
this.state = {
|
||||
localSeedErrors: [],
|
||||
seedInput: '',
|
||||
fields: fieldsState,
|
||||
disabledFields,
|
||||
fieldsErrors: this.getFieldsErrors(fieldsState),
|
||||
areErrorsVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
getFieldsErrors(fields, seedInput = '') {
|
||||
const { name, seeds } = fields;
|
||||
return {
|
||||
name: validateName(name),
|
||||
seeds: validateSeeds(seeds, seedInput),
|
||||
};
|
||||
}
|
||||
|
||||
onFieldsChange = (changedFields) => {
|
||||
this.setState(({ fields: prevFields, seedInput }) => {
|
||||
const newFields = {
|
||||
...prevFields,
|
||||
...changedFields,
|
||||
};
|
||||
return ({
|
||||
fields: newFields,
|
||||
fieldsErrors: this.getFieldsErrors(newFields, seedInput),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
getAllFields() {
|
||||
const {
|
||||
fields: {
|
||||
name,
|
||||
seeds,
|
||||
skipUnavailable,
|
||||
},
|
||||
} = this.state;
|
||||
|
||||
return {
|
||||
name,
|
||||
seeds,
|
||||
skipUnavailable,
|
||||
};
|
||||
}
|
||||
|
||||
save = () => {
|
||||
const { save } = this.props;
|
||||
|
||||
if (this.hasErrors()) {
|
||||
this.setState({
|
||||
areErrorsVisible: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const cluster = this.getAllFields();
|
||||
save(cluster);
|
||||
};
|
||||
|
||||
onCreateSeed = (newSeed) => {
|
||||
// If the user just hit enter without typing anything, treat it as a no-op.
|
||||
if (!newSeed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localSeedErrors = validateSeed(newSeed);
|
||||
|
||||
if (localSeedErrors.length !== 0) {
|
||||
this.setState({
|
||||
localSeedErrors,
|
||||
});
|
||||
|
||||
// Return false to explicitly reject the user's input.
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
fields: {
|
||||
seeds,
|
||||
},
|
||||
} = this.state;
|
||||
|
||||
const newSeeds = seeds.slice(0);
|
||||
newSeeds.push(newSeed.toLowerCase());
|
||||
this.onFieldsChange({ seeds: newSeeds });
|
||||
};
|
||||
|
||||
onSeedsInputChange = (seedInput) => {
|
||||
if (!seedInput) {
|
||||
// If empty seedInput ("") don't do anything. This happens
|
||||
// right after a seed is created.
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(({ fields, localSeedErrors }) => {
|
||||
const { seeds } = fields;
|
||||
|
||||
// Allow typing to clear the errors, but not to add new ones.
|
||||
const errors = (!seedInput || validateSeed(seedInput).length === 0) ? [] : localSeedErrors;
|
||||
|
||||
// EuiComboBox internally checks for duplicates and prevents calling onCreateOption if the
|
||||
// input is a duplicate. So we need to surface this error here instead.
|
||||
const isDuplicate = seeds.includes(seedInput);
|
||||
|
||||
if (isDuplicate) {
|
||||
errors.push(i18n.translate('xpack.remoteClusters.remoteClusterForm.localSeedError.duplicateMessage',
|
||||
{
|
||||
defaultMessage: `Duplicate seed nodes aren't allowed.`,
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
return ({
|
||||
localSeedErrors: errors,
|
||||
fieldsErrors: this.getFieldsErrors(fields, seedInput),
|
||||
seedInput,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onSeedsChange = (seeds) => {
|
||||
this.onFieldsChange({ seeds: seeds.map(({ label }) => label) });
|
||||
};
|
||||
|
||||
onSkipUnavailableChange = (e) => {
|
||||
const skipUnavailable = e.target.checked;
|
||||
this.onFieldsChange({ skipUnavailable });
|
||||
};
|
||||
|
||||
resetToDefault = (fieldName) => {
|
||||
this.onFieldsChange({
|
||||
[fieldName]: defaultFields[fieldName],
|
||||
});
|
||||
};
|
||||
|
||||
hasErrors = () => {
|
||||
const { fieldsErrors, localSeedErrors } = this.state;
|
||||
const errorValues = Object.values(fieldsErrors);
|
||||
const hasErrors = errorValues.some(error => error != null) || localSeedErrors.length;
|
||||
return hasErrors;
|
||||
};
|
||||
|
||||
renderSeeds() {
|
||||
const {
|
||||
areErrorsVisible,
|
||||
fields: {
|
||||
seeds,
|
||||
},
|
||||
fieldsErrors: {
|
||||
seeds: errorsSeeds,
|
||||
},
|
||||
localSeedErrors,
|
||||
} = this.state;
|
||||
|
||||
// Show errors if there is a general form error or local errors.
|
||||
const areFormErrorsVisible = Boolean(areErrorsVisible && errorsSeeds);
|
||||
const showErrors = areFormErrorsVisible || localSeedErrors.length !== 0;
|
||||
const errors = areFormErrorsVisible ? localSeedErrors.concat(errorsSeeds) : localSeedErrors;
|
||||
|
||||
const formattedSeeds = seeds.map(seed => ({ label: seed }));
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={(
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsTitle"
|
||||
defaultMessage="Seed nodes for cluster discovery"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
)}
|
||||
description={(
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsDescription1"
|
||||
defaultMessage="A list of remote cluster nodes to query for the cluster state.
|
||||
Specify multiple seed nodes so discovery doesn't fail if a node is unavailable."
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
data-test-subj="remoteClusterFormSeedNodesFormRow"
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.fieldSeedsLabel"
|
||||
defaultMessage="Seed nodes"
|
||||
/>
|
||||
)}
|
||||
helpText={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText"
|
||||
defaultMessage="An IP address or host name, followed by the {transportPort} of the remote cluster."
|
||||
values={{
|
||||
transportPort: (
|
||||
<EuiLink href={transportPortUrl} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText.transportPortLinkText"
|
||||
defaultMessage="transport port"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
isInvalid={showErrors}
|
||||
error={errors}
|
||||
fullWidth
|
||||
>
|
||||
<EuiComboBox
|
||||
noSuggestions
|
||||
placeholder={i18n.translate('xpack.remoteClusters.remoteClusterForm.fieldSeedsPlaceholder',
|
||||
{
|
||||
defaultMessage: 'host:port',
|
||||
}
|
||||
)}
|
||||
selectedOptions={formattedSeeds}
|
||||
onCreateOption={this.onCreateSeed}
|
||||
onChange={this.onSeedsChange}
|
||||
onSearchChange={this.onSeedsInputChange}
|
||||
isInvalid={showErrors}
|
||||
fullWidth
|
||||
data-test-subj="remoteClusterFormSeedsInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderSkipUnavailable() {
|
||||
const {
|
||||
fields: {
|
||||
skipUnavailable,
|
||||
},
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={(
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableTitle"
|
||||
defaultMessage="Make remote cluster optional"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
)}
|
||||
description={(
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription"
|
||||
defaultMessage="By default, a request fails if any of the queried remote clusters
|
||||
are unavailable. To continue sending a request to other remote clusters if this
|
||||
cluster is unavailable, enable {optionName}. {learnMoreLink}"
|
||||
values={{
|
||||
optionName: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.optionNameLabel"
|
||||
defaultMessage="Skip if unavailable"
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
learnMoreLink: (
|
||||
<EuiLink href={skippingDisconnectedClustersUrl} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.learnMoreLinkLabel"
|
||||
defaultMessage="Learn more."
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
data-test-subj="remoteClusterFormSkipUnavailableFormRow"
|
||||
className="remoteClusterSkipIfUnavailableSwitch"
|
||||
hasEmptyLabelSpace
|
||||
fullWidth
|
||||
helpText={
|
||||
skipUnavailable !== defaultFields.skipUnavailable ? (
|
||||
<EuiLink onClick={() => { this.resetToDefault('skipUnavailable'); }}>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableResetLabel"
|
||||
defaultMessage="Reset to default"
|
||||
/>
|
||||
</EuiLink>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableLabel', {
|
||||
defaultMessage: 'Skip if unavailable',
|
||||
})}
|
||||
checked={skipUnavailable}
|
||||
onChange={this.onSkipUnavailableChange}
|
||||
data-test-subj="remoteClusterFormSkipUnavailableFormToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderActions() {
|
||||
const { isSaving, cancel } = this.props;
|
||||
const { areErrorsVisible } = this.state;
|
||||
|
||||
if (isSaving) {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexStart" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="l"/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.actions.savingText"
|
||||
defaultMessage="Saving"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
let cancelButton;
|
||||
|
||||
if (cancel) {
|
||||
cancelButton = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
onClick={cancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
const isSaveDisabled = areErrorsVisible && this.hasErrors();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="remoteClusterFormSaveButton"
|
||||
color="secondary"
|
||||
iconType="check"
|
||||
onClick={this.save}
|
||||
fill
|
||||
disabled={isSaveDisabled}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
||||
{cancelButton}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderSavingFeedback() {
|
||||
if (this.props.isSaving) {
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiLoadingKibana size="xl"/>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderSaveErrorFeedback() {
|
||||
const { saveError } = this.props;
|
||||
|
||||
if (saveError) {
|
||||
const { message, cause } = saveError;
|
||||
|
||||
let errorBody;
|
||||
|
||||
if (cause) {
|
||||
if (cause.length === 1) {
|
||||
errorBody = (
|
||||
<p>{cause[0]}</p>
|
||||
);
|
||||
} else {
|
||||
errorBody = (
|
||||
<ul>
|
||||
{cause.map(causeValue => <li key={causeValue}>{causeValue}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={message}
|
||||
icon="cross"
|
||||
color="danger"
|
||||
>
|
||||
{errorBody}
|
||||
</EuiCallOut>
|
||||
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderErrors = () => {
|
||||
const { areErrorsVisible } = this.state;
|
||||
const hasErrors = this.hasErrors();
|
||||
|
||||
if (!areErrorsVisible || !hasErrors) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
data-test-subj="remoteClusterFormGlobalError"
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.errorTitle"
|
||||
defaultMessage="Fix errors before continuing."
|
||||
/>
|
||||
)}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
disabledFields: {
|
||||
name: disabledName,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
areErrorsVisible,
|
||||
fields: {
|
||||
name,
|
||||
},
|
||||
fieldsErrors: {
|
||||
name: errorClusterName,
|
||||
},
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{this.renderSaveErrorFeedback()}
|
||||
|
||||
<EuiForm>
|
||||
<EuiDescribedFormGroup
|
||||
title={(
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionNameTitle"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
)}
|
||||
description={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionNameDescription"
|
||||
defaultMessage="A unique name for the remote cluster."
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
data-test-subj="remoteClusterFormNameFormRow"
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.fieldNameLabel"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
)}
|
||||
helpText={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.fieldNameLabelHelpText"
|
||||
defaultMessage="Name can only contain letters, numbers, underscores, and dashes."
|
||||
/>
|
||||
)}
|
||||
error={errorClusterName}
|
||||
isInvalid={Boolean(areErrorsVisible && errorClusterName)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
isInvalid={Boolean(areErrorsVisible && errorClusterName)}
|
||||
value={name}
|
||||
onChange={e => this.onFieldsChange({ name: e.target.value })}
|
||||
fullWidth
|
||||
disabled={disabledName}
|
||||
data-test-subj="remoteClusterFormNameInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{this.renderSeeds()}
|
||||
|
||||
{this.renderSkipUnavailable()}
|
||||
</EuiForm>
|
||||
|
||||
{this.renderErrors()}
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
{this.renderActions()}
|
||||
|
||||
{this.renderSavingFeedback()}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ import {
|
|||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { remoteClustersUrl } from '../../../services/documentation_links';
|
||||
import { remoteClustersUrl } from '../../../services/documentation';
|
||||
|
||||
export const RemoteClusterPageTitle = ({ title }) => (
|
||||
<Fragment>
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiPageContent,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CRUD_APP_BASE_PATH } from '../../constants';
|
||||
import { getRouter, redirect, extractQueryParams } from '../../services';
|
||||
import { setBreadcrumbs } from '../../services/breadcrumb';
|
||||
import { RemoteClusterPageTitle, RemoteClusterForm } from '../components';
|
||||
|
||||
export class RemoteClusterAdd extends PureComponent {
|
||||
static propTypes = {
|
||||
addCluster: PropTypes.func,
|
||||
isAddingCluster: PropTypes.bool,
|
||||
addClusterError: PropTypes.object,
|
||||
clearAddClusterErrors: PropTypes.func,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
setBreadcrumbs('add');
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Clean up after ourselves.
|
||||
this.props.clearAddClusterErrors();
|
||||
}
|
||||
|
||||
save = (clusterConfig) => {
|
||||
this.props.addCluster(clusterConfig);
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
const { history, route: { location: { search } } } = getRouter();
|
||||
const { redirect: redirectUrl } = extractQueryParams(search);
|
||||
|
||||
if (redirectUrl) {
|
||||
const decodedRedirect = decodeURIComponent(redirectUrl);
|
||||
redirect(decodedRedirect);
|
||||
} else {
|
||||
history.push(CRUD_APP_BASE_PATH);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isAddingCluster, addClusterError } = this.props;
|
||||
|
||||
return (
|
||||
<EuiPageContent
|
||||
horizontalPosition="center"
|
||||
className="remoteClusterAddPage"
|
||||
>
|
||||
<RemoteClusterPageTitle
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.addTitle"
|
||||
defaultMessage="Add remote cluster"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<RemoteClusterForm
|
||||
isSaving={isAddingCluster}
|
||||
saveError={addClusterError}
|
||||
save={this.save}
|
||||
cancel={this.cancel}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiPageContent,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CRUD_APP_BASE_PATH } from '../../constants';
|
||||
import {
|
||||
extractQueryParams,
|
||||
getRouter,
|
||||
getRouterLinkProps,
|
||||
redirect,
|
||||
} from '../../services';
|
||||
import { setBreadcrumbs } from '../../services/breadcrumb';
|
||||
import { RemoteClusterPageTitle, RemoteClusterForm, ConfiguredByNodeWarning } from '../components';
|
||||
|
||||
const disabledFields = {
|
||||
name: true,
|
||||
};
|
||||
|
||||
export class RemoteClusterEdit extends Component {
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
cluster: PropTypes.object,
|
||||
startEditingCluster: PropTypes.func,
|
||||
stopEditingCluster: PropTypes.func,
|
||||
editCluster: PropTypes.func,
|
||||
isEditingCluster: PropTypes.bool,
|
||||
getEditClusterError: PropTypes.string,
|
||||
clearEditClusterErrors: PropTypes.func,
|
||||
openDetailPanel: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const {
|
||||
match: {
|
||||
params: {
|
||||
name,
|
||||
},
|
||||
},
|
||||
} = props;
|
||||
|
||||
setBreadcrumbs('edit', `?cluster=${name}`);
|
||||
|
||||
this.state = {
|
||||
clusterName: name,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { startEditingCluster } = this.props;
|
||||
const { clusterName } = this.state;
|
||||
startEditingCluster(clusterName);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Clean up after ourselves.
|
||||
this.props.clearEditClusterErrors();
|
||||
this.props.stopEditingCluster();
|
||||
}
|
||||
|
||||
save = (clusterConfig) => {
|
||||
this.props.editCluster(clusterConfig);
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
const { openDetailPanel } = this.props;
|
||||
const { clusterName } = this.state;
|
||||
const { history, route: { location: { search } } } = getRouter();
|
||||
const { redirect: redirectUrl } = extractQueryParams(search);
|
||||
|
||||
if (redirectUrl) {
|
||||
const decodedRedirect = decodeURIComponent(redirectUrl);
|
||||
redirect(decodedRedirect);
|
||||
} else {
|
||||
history.push(CRUD_APP_BASE_PATH);
|
||||
openDetailPanel(clusterName);
|
||||
}
|
||||
};
|
||||
|
||||
renderContent() {
|
||||
const {
|
||||
clusterName,
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
cluster,
|
||||
isEditingCluster,
|
||||
getEditClusterError,
|
||||
} = this.props;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.edit.loadingLabel"
|
||||
defaultMessage="Loading remote cluster..."
|
||||
/>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.edit.loadingErrorTitle"
|
||||
defaultMessage="Error loading remote cluster"
|
||||
/>
|
||||
)}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.edit.loadingErrorMessage"
|
||||
defaultMessage="The remote cluster '{name}' does not exist."
|
||||
values={{ name: clusterName }}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
{...getRouterLinkProps(CRUD_APP_BASE_PATH)}
|
||||
iconType="arrowLeft"
|
||||
flush="left"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.edit.viewRemoteClustersButtonLabel"
|
||||
defaultMessage="View remote clusters"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const { isConfiguredByNode } = cluster;
|
||||
|
||||
if (isConfiguredByNode) {
|
||||
return (
|
||||
<Fragment>
|
||||
<ConfiguredByNodeWarning />
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
onClick={this.cancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.edit.backToRemoteClustersButtonLabel"
|
||||
defaultMessage="Back to remote clusters"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RemoteClusterForm
|
||||
fields={cluster}
|
||||
disabledFields={disabledFields}
|
||||
isSaving={isEditingCluster}
|
||||
saveError={getEditClusterError}
|
||||
save={this.save}
|
||||
cancel={this.cancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiPageContent
|
||||
horizontalPosition="center"
|
||||
className="remoteClusterAddPage"
|
||||
>
|
||||
<RemoteClusterPageTitle
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.editTitle"
|
||||
defaultMessage="Edit remote cluster"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{this.renderContent()}
|
||||
</EuiPageContent>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiConfirmModal,
|
||||
EuiOverlayMask,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export class RemoveClusterButtonProvider extends Component {
|
||||
static propTypes = {
|
||||
removeClusters: PropTypes.func.isRequired,
|
||||
clusterNames: PropTypes.array.isRequired,
|
||||
children: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
isModalOpen: false,
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
showConfirmModal = () => {
|
||||
this.setState({
|
||||
isModalOpen: true,
|
||||
});
|
||||
};
|
||||
|
||||
closeConfirmModal = () => {
|
||||
this.setState({
|
||||
isModalOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
onConfirm = () => {
|
||||
const { removeClusters, clusterNames } = this.props;
|
||||
removeClusters(clusterNames);
|
||||
this.closeConfirmModal();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { clusterNames, children } = this.props;
|
||||
const { isModalOpen } = this.state;
|
||||
const isSingleCluster = clusterNames.length === 1;
|
||||
let modal;
|
||||
|
||||
if (isModalOpen) {
|
||||
const title = isSingleCluster ? i18n.translate(
|
||||
'xpack.remoteClusters.removeButton.confirmModal.deleteSingleClusterTitle',
|
||||
{
|
||||
defaultMessage: 'Remove remote cluster \'{name}\'?',
|
||||
values: { name: clusterNames[0] },
|
||||
}
|
||||
) : i18n.translate(
|
||||
'xpack.remoteClusters.removeButton.confirmModal.multipleDeletionTitle',
|
||||
{
|
||||
defaultMessage: 'Remove {count} remote clusters?',
|
||||
values: { count: clusterNames.length },
|
||||
}
|
||||
);
|
||||
|
||||
const content = (
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.removeButton.confirmModal.multipleDeletionDescription"
|
||||
defaultMessage="You are about to remove these remote clusters:"
|
||||
/>
|
||||
</p>
|
||||
<ul>{clusterNames.map(name => <li key={name}>{name}</li>)}</ul>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
modal = (
|
||||
<EuiOverlayMask>
|
||||
{ /* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */ }
|
||||
<EuiConfirmModal
|
||||
data-test-subj="remoteClustersDeleteConfirmModal"
|
||||
title={title}
|
||||
onCancel={this.closeConfirmModal}
|
||||
onConfirm={this.onConfirm}
|
||||
cancelButtonText={
|
||||
i18n.translate('xpack.remoteClusters.removeButton.confirmModal.cancelButtonText',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)
|
||||
}
|
||||
buttonColor="danger"
|
||||
confirmButtonText={
|
||||
i18n.translate('xpack.remoteClusters.removeButton.confirmModal.confirmButtonText',
|
||||
{
|
||||
defaultMessage: 'Remove',
|
||||
}
|
||||
)
|
||||
}
|
||||
onMouseOver={this.onMouseOverModal}
|
||||
>
|
||||
{!isSingleCluster && content}
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{children(this.showConfirmModal)}
|
||||
{modal}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,352 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListDescription,
|
||||
EuiDescriptionListTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiIcon,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CRUD_APP_BASE_PATH } from '../../../constants';
|
||||
import { getRouterLinkProps } from '../../../services';
|
||||
import { ConfiguredByNodeWarning } from '../../components';
|
||||
import { ConnectionStatus, RemoveClusterButtonProvider } from '../components';
|
||||
|
||||
export class DetailPanel extends Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
cluster: PropTypes.object,
|
||||
closeDetailPanel: PropTypes.func.isRequired,
|
||||
clusterName: PropTypes.string,
|
||||
}
|
||||
|
||||
renderSkipUnavailableValue(skipUnavailable) {
|
||||
if (skipUnavailable === true) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.skipUnavailableTrueValue"
|
||||
defaultMessage="Yes"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (skipUnavailable === false) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.skipUnavailableFalseValue"
|
||||
defaultMessage="No"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.skipUnavailableNullValue"
|
||||
defaultMessage="Default"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderClusterNotFound() {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
data-test-subj="remoteClusterDetailClusterNotFound"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon size="m" type="alert" color="danger" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.notFoundLabel"
|
||||
defaultMessage="Remote cluster not found"
|
||||
/>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderClusterConfiguredByNodeWarning({ isConfiguredByNode }) {
|
||||
if (!isConfiguredByNode) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<ConfiguredByNodeWarning />
|
||||
<EuiSpacer size="l" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderCluster({
|
||||
isConnected,
|
||||
connectedNodesCount,
|
||||
skipUnavailable,
|
||||
seeds,
|
||||
maxConnectionsPerCluster,
|
||||
initialConnectTimeout,
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="xpack.remoteClusters.detailPanel.statusTitle"
|
||||
data-test-subj="remoteClusterDetailPanelStatusSection"
|
||||
>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.statusTitle"
|
||||
defaultMessage="Status"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList data-test-subj="remoteClusterDetailPanelStatusValues">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.connectedLabel"
|
||||
defaultMessage="Connection"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription data-test-subj="remoteClusterDetailIsConnected">
|
||||
<ConnectionStatus isConnected={isConnected} />
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.connectedNodesLabel"
|
||||
defaultMessage="Connected nodes"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription data-test-subj="remoteClusterDetailConnectedNodesCount">
|
||||
{connectedNodesCount}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.seedsLabel"
|
||||
defaultMessage="Seeds"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription data-test-subj="remoteClusterDetailSeeds">
|
||||
{seeds.map(seed => <EuiText key={seed}>{seed}</EuiText>)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.skipUnavailableLabel"
|
||||
defaultMessage="Skip unavailable"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription data-test-subj="remoteClusterDetailSkipUnavailable">
|
||||
{this.renderSkipUnavailableValue(skipUnavailable)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.maxConnectionsPerClusterLabel"
|
||||
defaultMessage="Maximum number of connections"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription data-test-subj="remoteClusterDetailMaxConnections">
|
||||
{maxConnectionsPerCluster}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.initialConnectTimeoutLabel"
|
||||
defaultMessage="Initial connect timeout"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription data-test-subj="remoteClusterDetailInitialConnectTimeout">
|
||||
{initialConnectTimeout}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiDescriptionList>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderFlyoutBody() {
|
||||
const { cluster } = this.props;
|
||||
|
||||
return (
|
||||
<EuiFlyoutBody>
|
||||
{!cluster && (
|
||||
this.renderClusterNotFound()
|
||||
)}
|
||||
{cluster && (
|
||||
<Fragment>
|
||||
{this.renderClusterConfiguredByNodeWarning(cluster)}
|
||||
{this.renderCluster(cluster)}
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
);
|
||||
}
|
||||
|
||||
renderFlyoutFooter() {
|
||||
const {
|
||||
cluster,
|
||||
clusterName,
|
||||
closeDetailPanel,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
flush="left"
|
||||
onClick={closeDetailPanel}
|
||||
data-test-subj="remoteClusterDetailsPanelCloseButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.closeButtonLabel"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
{cluster && !cluster.isConfiguredByNode && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<RemoveClusterButtonProvider clusterNames={[clusterName]}>
|
||||
{(removeCluster) => (
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
onClick={removeCluster}
|
||||
data-test-subj="remoteClusterDetailPanelRemoveButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.removeButtonLabel"
|
||||
defaultMessage="Remove"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</RemoveClusterButtonProvider>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
{...getRouterLinkProps(`${CRUD_APP_BASE_PATH}/edit/${clusterName}`)}
|
||||
fill
|
||||
color="primary"
|
||||
data-test-subj="remoteClusterDetailPanelEditButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.editButtonLabel"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isOpen, closeDetailPanel, clusterName } = this.props;
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
data-test-subj="remoteClusterDetailFlyout"
|
||||
onClose={closeDetailPanel}
|
||||
aria-labelledby="remoteClusterDetailsFlyoutTitle"
|
||||
size="m"
|
||||
maxWidth={400}
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle
|
||||
size="m"
|
||||
id="remoteClusterDetailsFlyoutTitle"
|
||||
data-test-subj="remoteClusterDetailsFlyoutTitle"
|
||||
>
|
||||
<h2>{clusterName}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
{this.renderFlyoutBody()}
|
||||
|
||||
{this.renderFlyoutFooter()}
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingKibana,
|
||||
EuiLoadingSpinner,
|
||||
EuiOverlayMask,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CRUD_APP_BASE_PATH } from '../../constants';
|
||||
import { getRouterLinkProps, extractQueryParams } from '../../services';
|
||||
import { setBreadcrumbs } from '../../services/breadcrumb';
|
||||
|
||||
import {
|
||||
RemoteClusterTable,
|
||||
} from './remote_cluster_table';
|
||||
|
||||
import {
|
||||
DetailPanel,
|
||||
} from './detail_panel';
|
||||
|
||||
const REFRESH_RATE_MS = 30000;
|
||||
|
||||
export class RemoteClusterList extends Component {
|
||||
static propTypes = {
|
||||
loadClusters: PropTypes.func.isRequired,
|
||||
refreshClusters: PropTypes.func.isRequired,
|
||||
openDetailPanel: PropTypes.func.isRequired,
|
||||
closeDetailPanel: PropTypes.func.isRequired,
|
||||
isDetailPanelOpen: PropTypes.bool,
|
||||
clusters: PropTypes.array,
|
||||
isLoading: PropTypes.bool,
|
||||
isCopyingCluster: PropTypes.bool,
|
||||
isRemovingCluster: PropTypes.bool,
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {
|
||||
openDetailPanel,
|
||||
closeDetailPanel,
|
||||
isDetailPanelOpen,
|
||||
history: {
|
||||
location: {
|
||||
search,
|
||||
},
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const { cluster: clusterName } = extractQueryParams(search);
|
||||
|
||||
// Show deeplinked remoteCluster whenever remoteClusters get loaded or the URL changes.
|
||||
if (clusterName != null) {
|
||||
openDetailPanel(clusterName);
|
||||
} else if (isDetailPanelOpen) {
|
||||
closeDetailPanel();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadClusters();
|
||||
this.interval = setInterval(this.props.refreshClusters, REFRESH_RATE_MS);
|
||||
setBreadcrumbs('home');
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
getHeaderSection(isAuthorized) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterListTitle"
|
||||
defaultMessage="Remote Clusters"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeaderSection>
|
||||
|
||||
{ isAuthorized && (
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiButton
|
||||
{...getRouterLinkProps(`${CRUD_APP_BASE_PATH}/add`)}
|
||||
fill
|
||||
data-test-subj="remoteClusterCreateButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.connectButtonLabel"
|
||||
defaultMessage="Add a remote cluster"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiPageContentHeaderSection>
|
||||
)}
|
||||
</EuiPageContentHeader>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderBlockingAction() {
|
||||
const { isCopyingCluster, isRemovingCluster } = this.props;
|
||||
|
||||
if (isCopyingCluster || isRemovingCluster) {
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiLoadingKibana size="xl"/>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderNoPermission() {
|
||||
const title = i18n.translate('xpack.remoteClusters.remoteClusterList.noPermissionTitle', {
|
||||
defaultMessage: 'Permission error',
|
||||
});
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={title}
|
||||
color="warning"
|
||||
iconType="help"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.noPermissionText"
|
||||
defaultMessage="You do not have permission to view or add remote clusters."
|
||||
/>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
renderError(error) {
|
||||
// We can safely depend upon the shape of this error coming from Angular $http, because we
|
||||
// handle unexpected error shapes in the API action.
|
||||
const {
|
||||
statusCode,
|
||||
error: errorString,
|
||||
} = error.data;
|
||||
|
||||
const title = i18n.translate('xpack.remoteClusters.remoteClusterList.loadingErrorTitle', {
|
||||
defaultMessage: 'Error loading remote clusters',
|
||||
});
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={title}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
{statusCode} {errorString}
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmpty() {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="remoteClusterListEmptyPrompt"
|
||||
iconType="managementApp"
|
||||
title={(
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.emptyPromptTitle"
|
||||
defaultMessage="Add your first remote cluster"
|
||||
/>
|
||||
</h1>
|
||||
)}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.emptyPromptDescription"
|
||||
defaultMessage="Remote clusters create a uni-directional connection from your
|
||||
local cluster to other clusters."
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
{...getRouterLinkProps(`${CRUD_APP_BASE_PATH}/add`)}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="remoteClusterEmptyPromptCreateButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.emptyPrompt.connectButtonLabel"
|
||||
defaultMessage="Add a remote cluster"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
data-test-subj="remoteClustersTableLoading"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.loadingTitle"
|
||||
defaultMessage="Loading remote clusters..."
|
||||
/>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderList() {
|
||||
const { clusters } = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<RemoteClusterTable clusters={clusters} />
|
||||
<DetailPanel />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, clusters, clusterLoadError } = this.props;
|
||||
const isEmpty = !isLoading && !clusters.length;
|
||||
const isAuthorized = !clusterLoadError || clusterLoadError.status !== 403;
|
||||
const isHeaderVisible = clusterLoadError || !isEmpty;
|
||||
|
||||
let content;
|
||||
|
||||
if (clusterLoadError) {
|
||||
if (!isAuthorized) {
|
||||
content = this.renderNoPermission();
|
||||
} else {
|
||||
content = this.renderError(clusterLoadError);
|
||||
}
|
||||
} else if (isEmpty) {
|
||||
content = this.renderEmpty();
|
||||
} else if (isLoading) {
|
||||
content = this.renderLoading();
|
||||
} else {
|
||||
content = this.renderList();
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
{(isHeaderVisible) && this.getHeaderSection(isAuthorized)}
|
||||
{content}
|
||||
{this.renderBlockingAction()}
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIconTip,
|
||||
EuiInMemoryTable,
|
||||
EuiLink,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CRUD_APP_BASE_PATH, UIM_SHOW_DETAILS_CLICK } from '../../../constants';
|
||||
import { getRouterLinkProps, trackUiMetric } from '../../../services';
|
||||
import { ConnectionStatus, RemoveClusterButtonProvider } from '../components';
|
||||
|
||||
export class RemoteClusterTable extends Component {
|
||||
static propTypes = {
|
||||
clusters: PropTypes.array,
|
||||
openDetailPanel: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
clusters: [],
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
queryText: undefined,
|
||||
selectedItems: [],
|
||||
};
|
||||
}
|
||||
|
||||
onSearch = ({ query }) => {
|
||||
const { text } = query;
|
||||
const normalizedSearchText = text.toLowerCase();
|
||||
this.setState({
|
||||
queryText: normalizedSearchText,
|
||||
});
|
||||
};
|
||||
|
||||
getFilteredClusters = () => {
|
||||
const { clusters } = this.props;
|
||||
const { queryText } = this.state;
|
||||
|
||||
if (queryText) {
|
||||
return clusters.filter(cluster => {
|
||||
const { name, seeds } = cluster;
|
||||
const normalizedName = name.toLowerCase();
|
||||
if (normalizedName.toLowerCase().includes(queryText)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return seeds.some(seed => seed.includes(queryText));
|
||||
});
|
||||
} else {
|
||||
return clusters.slice(0);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
openDetailPanel,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
selectedItems,
|
||||
} = this.state;
|
||||
|
||||
const columns = [{
|
||||
field: 'name',
|
||||
name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.nameColumnTitle', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
render: (name, { isConfiguredByNode }) => {
|
||||
const link = (
|
||||
<EuiLink
|
||||
data-test-subj="remoteClustersTableListClusterLink"
|
||||
onClick={() => {
|
||||
trackUiMetric(UIM_SHOW_DETAILS_CLICK);
|
||||
openDetailPanel(name);
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</EuiLink>
|
||||
);
|
||||
|
||||
if (isConfiguredByNode) {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{link}
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false} data-test-subj="remoteClustersTableListClusterDefinedByNodeTooltip">
|
||||
<EuiIconTip
|
||||
type="iInCircle"
|
||||
color="subdued"
|
||||
content={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.table.isConfiguredByNodeMessage"
|
||||
defaultMessage="Defined in elasticsearch.yml"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return link;
|
||||
}
|
||||
}, {
|
||||
field: 'seeds',
|
||||
name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.seedsColumnTitle', {
|
||||
defaultMessage: 'Seeds',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: (seeds) => seeds.join(', '),
|
||||
}, {
|
||||
field: 'isConnected',
|
||||
name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.connectedColumnTitle', {
|
||||
defaultMessage: 'Connection',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (isConnected) => <ConnectionStatus isConnected={isConnected} />,
|
||||
width: '240px',
|
||||
}, {
|
||||
field: 'connectedNodesCount',
|
||||
name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.connectedNodesColumnTitle', {
|
||||
defaultMessage: 'Connected nodes',
|
||||
}),
|
||||
sortable: true,
|
||||
width: '160px',
|
||||
}, {
|
||||
name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.actionsColumnTitle', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
width: '100px',
|
||||
actions: [{
|
||||
render: ({ name, isConfiguredByNode }) => {
|
||||
const label = isConfiguredByNode
|
||||
? i18n.translate('xpack.remoteClusters.remoteClusterList.table.actionBlockedDeleteDescription', {
|
||||
defaultMessage: `Remote clusters defined in elasticsearch.yml can't be deleted`,
|
||||
}) : i18n.translate('xpack.remoteClusters.remoteClusterList.table.actionDeleteDescription', {
|
||||
defaultMessage: 'Delete remote cluster',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={label}
|
||||
delay="long"
|
||||
>
|
||||
<RemoveClusterButtonProvider clusterNames={[name]}>
|
||||
{(removeCluster) => (
|
||||
<EuiButtonIcon
|
||||
data-test-subj="remoteClusterTableRowRemoveButton"
|
||||
aria-label={label}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
isDisabled={isConfiguredByNode}
|
||||
onClick={removeCluster}
|
||||
/>
|
||||
)}
|
||||
</RemoveClusterButtonProvider>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
render: ({ name, isConfiguredByNode }) => {
|
||||
const label = isConfiguredByNode
|
||||
? i18n.translate('xpack.remoteClusters.remoteClusterList.table.actionBlockedEditDescription', {
|
||||
defaultMessage: `Remote clusters defined in elasticsearch.yml can't be edited`,
|
||||
}) : i18n.translate('xpack.remoteClusters.remoteClusterList.table.actionEditDescription', {
|
||||
defaultMessage: 'Edit remote cluster',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={label}
|
||||
delay="long"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="remoteClusterTableRowEditButton"
|
||||
aria-label={label}
|
||||
iconType="pencil"
|
||||
color="primary"
|
||||
isDisabled={isConfiguredByNode}
|
||||
{...getRouterLinkProps(`${CRUD_APP_BASE_PATH}/edit/${name}`)}
|
||||
disabled={isConfiguredByNode}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
}],
|
||||
}];
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: 'name',
|
||||
direction: 'asc',
|
||||
}
|
||||
};
|
||||
|
||||
const search = {
|
||||
toolsLeft: selectedItems.length ? (
|
||||
<RemoveClusterButtonProvider clusterNames={selectedItems.map(({ name }) => name)}>
|
||||
{(removeCluster) => (
|
||||
<EuiButton
|
||||
color="danger"
|
||||
onClick={removeCluster}
|
||||
data-test-subj="remoteClusterBulkDeleteButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.table.removeButtonLabel"
|
||||
defaultMessage="Remove {count, plural, one {remote cluster} other {{count} remote clusters}}"
|
||||
values={{
|
||||
count: selectedItems.length
|
||||
}}
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</RemoveClusterButtonProvider>
|
||||
) : undefined,
|
||||
onChange: this.onSearch,
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
};
|
||||
|
||||
const pagination = {
|
||||
initialPageSize: 20,
|
||||
pageSizeOptions: [10, 20, 50]
|
||||
};
|
||||
|
||||
const selection = {
|
||||
onSelectionChange: (selectedItems) => this.setState({ selectedItems }),
|
||||
selectable: ({ isConfiguredByNode }) => !isConfiguredByNode,
|
||||
};
|
||||
|
||||
const filteredClusters = this.getFilteredClusters();
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
items={filteredClusters}
|
||||
itemId="name"
|
||||
columns={columns}
|
||||
search={search}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
selection={selection}
|
||||
isSelectable={true}
|
||||
data-test-subj="remoteClusterListTable"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,29 +4,17 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { UIM_CLUSTER_ADD, UIM_CLUSTER_UPDATE } from '../constants';
|
||||
import { trackUserRequest } from './track_ui_metric';
|
||||
|
||||
let httpClient;
|
||||
|
||||
export const setHttpClient = (client) => {
|
||||
httpClient = client;
|
||||
};
|
||||
|
||||
export const getHttpClient = () => {
|
||||
return httpClient;
|
||||
};
|
||||
|
||||
const apiPrefix = chrome.addBasePath('/api/remote_clusters');
|
||||
import { sendGet, sendPost, sendPut, sendDelete } from './http';
|
||||
|
||||
export async function loadClusters() {
|
||||
const response = await httpClient.get(apiPrefix);
|
||||
const response = await sendGet();
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function addCluster(cluster) {
|
||||
const request = httpClient.post(apiPrefix, cluster);
|
||||
const request = sendPost('', cluster);
|
||||
return await trackUserRequest(request, UIM_CLUSTER_ADD);
|
||||
}
|
||||
|
||||
|
@ -36,10 +24,10 @@ export async function editCluster(cluster) {
|
|||
...rest
|
||||
} = cluster;
|
||||
|
||||
const request = httpClient.put(`${apiPrefix}/${name}`, rest);
|
||||
const request = sendPut(name, rest);
|
||||
return await trackUserRequest(request, UIM_CLUSTER_UPDATE);
|
||||
}
|
||||
|
||||
export function removeClusterRequest(name) {
|
||||
return httpClient.delete(`${apiPrefix}/${name}`);
|
||||
return sendDelete(name);
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { fatalError, toastNotifications } from 'ui/notify';
|
||||
import { fatalError, toasts } from './notification';
|
||||
|
||||
function createToastConfig(error, errorTitle) {
|
||||
// Expect an error in the shape provided by Angular's $http service.
|
||||
|
@ -21,7 +21,7 @@ export function showApiWarning(error, errorTitle) {
|
|||
const toastConfig = createToastConfig(error, errorTitle);
|
||||
|
||||
if (toastConfig) {
|
||||
return toastNotifications.addWarning(toastConfig);
|
||||
return toasts.addWarning(toastConfig);
|
||||
}
|
||||
|
||||
// This error isn't an HTTP error, so let the fatal error screen tell the user something
|
||||
|
@ -33,7 +33,7 @@ export function showApiError(error, errorTitle) {
|
|||
const toastConfig = createToastConfig(error, errorTitle);
|
||||
|
||||
if (toastConfig) {
|
||||
return toastNotifications.addDanger(toastConfig);
|
||||
return toasts.addDanger(toastConfig);
|
||||
}
|
||||
|
||||
// This error isn't an HTTP error, so let the fatal error screen tell the user something
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { CRUD_APP_BASE_PATH } from '../constants';
|
||||
|
||||
let _setBreadcrumbs: any;
|
||||
let _breadcrumbs: any;
|
||||
|
||||
export function init(setGlobalBreadcrumbs: any, managementBreadcrumb: any): void {
|
||||
_setBreadcrumbs = setGlobalBreadcrumbs;
|
||||
_breadcrumbs = {
|
||||
management: managementBreadcrumb,
|
||||
home: {
|
||||
text: i18n.translate('xpack.remoteClusters.listBreadcrumbTitle', {
|
||||
defaultMessage: 'Remote Clusters',
|
||||
}),
|
||||
href: `#${CRUD_APP_BASE_PATH}/list`,
|
||||
},
|
||||
add: {
|
||||
text: i18n.translate('xpack.remoteClusters.addBreadcrumbTitle', {
|
||||
defaultMessage: 'Add',
|
||||
}),
|
||||
},
|
||||
edit: {
|
||||
text: i18n.translate('xpack.remoteClusters.editBreadcrumbTitle', {
|
||||
defaultMessage: 'Edit',
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function setBreadcrumbs(type: string, queryParams?: string): void {
|
||||
if (!_breadcrumbs[type]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'home') {
|
||||
_setBreadcrumbs([_breadcrumbs.management, _breadcrumbs.home]);
|
||||
} else {
|
||||
// Support deep-linking back to a remote cluster in the detail panel.
|
||||
const homeBreadcrumb = {
|
||||
text: _breadcrumbs.home.text,
|
||||
href: `${_breadcrumbs.home.href}${queryParams}`,
|
||||
};
|
||||
|
||||
_setBreadcrumbs([_breadcrumbs.management, homeBreadcrumb, _breadcrumbs[type]]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 let skippingDisconnectedClustersUrl: string;
|
||||
export let remoteClustersUrl: string;
|
||||
export let transportPortUrl: string;
|
||||
|
||||
export function init(esDocBasePath: string): void {
|
||||
skippingDisconnectedClustersUrl = `${esDocBasePath}/modules-cross-cluster-search.html#_skipping_disconnected_clusters`;
|
||||
remoteClustersUrl = `${esDocBasePath}/modules-remote-clusters.html`;
|
||||
transportPortUrl = `${esDocBasePath}/modules-transport.html`;
|
||||
}
|
39
x-pack/plugins/remote_clusters/public/app/services/http.ts
Normal file
39
x-pack/plugins/remote_clusters/public/app/services/http.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
let _httpClient: any;
|
||||
let _prependBasePath: any;
|
||||
|
||||
export function init(httpClient: any, prependBasePath: any): void {
|
||||
_httpClient = httpClient;
|
||||
_prependBasePath = prependBasePath;
|
||||
}
|
||||
|
||||
export function getFullPath(path: string): string {
|
||||
const apiPrefix = _prependBasePath('/api/remote_clusters');
|
||||
|
||||
if (path) {
|
||||
return `${apiPrefix}/${path}`;
|
||||
}
|
||||
|
||||
return apiPrefix;
|
||||
}
|
||||
|
||||
export function sendPost(path: string, payload: any): any {
|
||||
return _httpClient.post(getFullPath(path), payload);
|
||||
}
|
||||
|
||||
export function sendGet(path: string): any {
|
||||
return _httpClient.get(getFullPath(path));
|
||||
}
|
||||
|
||||
export function sendPut(path: string, payload: any): any {
|
||||
return _httpClient.put(getFullPath(path), payload);
|
||||
}
|
||||
|
||||
export function sendDelete(path: string): any {
|
||||
return _httpClient.delete(getFullPath(path));
|
||||
}
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
export {
|
||||
setHttpClient,
|
||||
loadClusters,
|
||||
addCluster,
|
||||
editCluster,
|
||||
|
@ -17,13 +16,6 @@ export {
|
|||
showApiWarning,
|
||||
} from './api_errors';
|
||||
|
||||
export {
|
||||
listBreadcrumb,
|
||||
buildListBreadcrumb,
|
||||
addBreadcrumb,
|
||||
editBreadcrumb,
|
||||
} from './breadcrumbs';
|
||||
|
||||
export {
|
||||
setRedirect,
|
||||
redirect,
|
|
@ -4,4 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { callWithRequestFactory } from './call_with_request_factory';
|
||||
export let toasts: any;
|
||||
export let fatalError: any;
|
||||
|
||||
export function init(_toasts: any, _fatalError: any): void {
|
||||
toasts = _toasts;
|
||||
fatalError = _fatalError;
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { trackUiMetric as track } from '../../../../../src/legacy/core_plugins/ui_metric/public';
|
||||
import { trackUiMetric as track } from '../../../../../../src/legacy/core_plugins/ui_metric/public';
|
||||
import { UIM_APP_NAME } from '../constants';
|
||||
|
||||
export function trackUiMetric(actionType) {
|
|
@ -4,4 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const CRUD_APP_BASE_PATH = '/management/elasticsearch/remote_clusters';
|
||||
import { UIM_APP_NAME } from '../constants';
|
||||
|
||||
export let track: any;
|
||||
|
||||
export function init(_track: any): void {
|
||||
track = _track;
|
||||
}
|
||||
|
||||
export function trackUiMetric(actionType: string): any {
|
||||
return track(UIM_APP_NAME, actionType);
|
||||
}
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { fatalError, toastNotifications } from 'ui/notify';
|
||||
|
||||
import { CRUD_APP_BASE_PATH } from '../../constants';
|
||||
import {
|
||||
|
@ -14,6 +13,7 @@ import {
|
|||
extractQueryParams,
|
||||
redirect,
|
||||
} from '../../services';
|
||||
import { fatalError, toasts } from '../../services/notification';
|
||||
|
||||
import {
|
||||
ADD_CLUSTER_START,
|
||||
|
@ -27,10 +27,8 @@ export const addCluster = (cluster) => async (dispatch) => {
|
|||
type: ADD_CLUSTER_START,
|
||||
});
|
||||
|
||||
let newCluster;
|
||||
|
||||
try {
|
||||
[ newCluster ] = await Promise.all([
|
||||
await Promise.all([
|
||||
sendAddClusterRequest(cluster),
|
||||
// Wait at least half a second to avoid a weird flicker of the saving feedback.
|
||||
new Promise(resolve => setTimeout(resolve, 500)),
|
||||
|
@ -80,7 +78,6 @@ export const addCluster = (cluster) => async (dispatch) => {
|
|||
|
||||
dispatch({
|
||||
type: ADD_CLUSTER_SUCCESS,
|
||||
payload: { cluster: newCluster.data },
|
||||
});
|
||||
|
||||
const { history, route: { location: { search } } } = getRouter();
|
||||
|
@ -88,7 +85,7 @@ export const addCluster = (cluster) => async (dispatch) => {
|
|||
|
||||
if (redirectUrl) {
|
||||
// A toast is only needed if we're leaving the app.
|
||||
toastNotifications.addSuccess(i18n.translate('xpack.remoteClusters.addAction.successTitle', {
|
||||
toasts.addSuccess(i18n.translate('xpack.remoteClusters.addAction.successTitle', {
|
||||
defaultMessage: `Added remote cluster '{name}'`,
|
||||
values: { name: cluster.name },
|
||||
}));
|
|
@ -5,7 +5,8 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { fatalError, toastNotifications } from 'ui/notify';
|
||||
|
||||
import { fatalError, toasts } from '../../services/notification';
|
||||
import { CRUD_APP_BASE_PATH } from '../../constants';
|
||||
import { loadClusters } from './load_clusters';
|
||||
|
||||
|
@ -72,7 +73,7 @@ export const editCluster = (cluster) => async (dispatch) => {
|
|||
|
||||
if (redirectUrl) {
|
||||
// A toast is only needed if we're leaving the app.
|
||||
toastNotifications.addSuccess(i18n.translate('xpack.remoteClusters.editAction.successTitle', {
|
||||
toasts.addSuccess(i18n.translate('xpack.remoteClusters.editAction.successTitle', {
|
||||
defaultMessage: `Edited remote cluster '{name}'`,
|
||||
values: { name: cluster.name },
|
||||
}));
|
|
@ -10,6 +10,7 @@ import {
|
|||
loadClusters as sendLoadClustersRequest,
|
||||
showApiError,
|
||||
} from '../../services';
|
||||
|
||||
import {
|
||||
LOAD_CLUSTERS_START,
|
||||
LOAD_CLUSTERS_SUCCESS,
|
|
@ -10,6 +10,7 @@ import {
|
|||
loadClusters as sendLoadClustersRequest,
|
||||
showApiWarning,
|
||||
} from '../../services';
|
||||
|
||||
import {
|
||||
REFRESH_CLUSTERS_SUCCESS,
|
||||
} from '../action_types';
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
||||
import { toasts } from '../../services/notification';
|
||||
import { UIM_CLUSTER_REMOVE, UIM_CLUSTER_REMOVE_MANY } from '../../constants';
|
||||
|
||||
import {
|
||||
|
@ -56,7 +56,7 @@ export const removeClusters = (names) => async (dispatch, getState) => {
|
|||
new Promise(resolve => setTimeout(resolve, 500)),
|
||||
]).catch(error => {
|
||||
const errorTitle = getErrorTitle(names.length, names[0]);
|
||||
toastNotifications.addDanger({
|
||||
toasts.addDanger({
|
||||
title: errorTitle,
|
||||
text: error.data.message,
|
||||
});
|
||||
|
@ -75,7 +75,7 @@ export const removeClusters = (names) => async (dispatch, getState) => {
|
|||
} = errors[0];
|
||||
|
||||
const title = getErrorTitle(errors.length, name);
|
||||
toastNotifications.addDanger({
|
||||
toasts.addDanger({
|
||||
title,
|
||||
text: message,
|
||||
});
|
||||
|
@ -86,12 +86,12 @@ export const removeClusters = (names) => async (dispatch, getState) => {
|
|||
trackUiMetric(names.length > 1 ? UIM_CLUSTER_REMOVE_MANY : UIM_CLUSTER_REMOVE);
|
||||
|
||||
if (itemsDeleted.length === 1) {
|
||||
toastNotifications.addSuccess(i18n.translate('xpack.remoteClusters.removeAction.successSingleNotificationTitle', {
|
||||
toasts.addSuccess(i18n.translate('xpack.remoteClusters.removeAction.successSingleNotificationTitle', {
|
||||
defaultMessage: `Remote cluster '{name}' was removed`,
|
||||
values: { name: itemsDeleted[0] },
|
||||
}));
|
||||
} else {
|
||||
toastNotifications.addSuccess(i18n.translate('xpack.remoteClusters.removeAction.successMultipleNotificationTitle', {
|
||||
toasts.addSuccess(i18n.translate('xpack.remoteClusters.removeAction.successMultipleNotificationTitle', {
|
||||
defaultMessage: '{count} remote clusters were removed',
|
||||
values: { count: itemsDeleted.length },
|
||||
}));
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const UIM_APP_NAME = 'remote_clusters';
|
||||
|
||||
export const UIM_APP_LOAD = 'app_load';
|
||||
export const UIM_CLUSTER_ADD = 'cluster_add';
|
||||
export const UIM_CLUSTER_UPDATE = 'cluster_update';
|
||||
export const UIM_CLUSTER_REMOVE = 'cluster_remove';
|
||||
export const UIM_CLUSTER_REMOVE_MANY = 'cluster_remove_many';
|
||||
export const UIM_SHOW_DETAILS_CLICK = 'show_details_click';
|
|
@ -4,97 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { management } from 'ui/management';
|
||||
import routes from 'ui/routes';
|
||||
import chrome from 'ui/chrome';
|
||||
import { Plugin as RemoteClustersPlugin } from './plugin';
|
||||
import { createShim } from './shim';
|
||||
|
||||
import { CRUD_APP_BASE_PATH } from './constants';
|
||||
import { setHttpClient, setUserHasLeftApp, setRedirect } from './services';
|
||||
import { App } from './app';
|
||||
import template from './main.html';
|
||||
import { remoteClustersStore } from './store';
|
||||
|
||||
if (chrome.getInjected('remoteClustersUiEnabled')) {
|
||||
const esSection = management.getSection('elasticsearch');
|
||||
|
||||
esSection.register('remote_clusters', {
|
||||
visible: true,
|
||||
display: i18n.translate('xpack.remoteClusters.appTitle', { defaultMessage: 'Remote Clusters' }),
|
||||
order: 5,
|
||||
url: `#${CRUD_APP_BASE_PATH}/list`,
|
||||
});
|
||||
|
||||
let appElement;
|
||||
|
||||
const renderReact = async (elem) => {
|
||||
render(
|
||||
<I18nContext>
|
||||
<Provider store={remoteClustersStore}>
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
</I18nContext>,
|
||||
elem
|
||||
);
|
||||
};
|
||||
|
||||
routes.when(`${CRUD_APP_BASE_PATH}/:view?/:id?`, {
|
||||
template: template,
|
||||
controllerAs: 'remoteClusters',
|
||||
controller: class RemoteClustersController {
|
||||
constructor($scope, $route, $http, kbnUrl) {
|
||||
if (appElement) {
|
||||
// React-router's <Redirect> will cause this controller to re-execute without the $destroy
|
||||
// handler being called. This means the app will re-mount, so we need to unmount it first
|
||||
// here.
|
||||
unmountComponentAtNode(appElement);
|
||||
}
|
||||
|
||||
// NOTE: We depend upon Angular's $http service because it's decorated with interceptors,
|
||||
// e.g. to check license status per request.
|
||||
setHttpClient($http);
|
||||
|
||||
setRedirect((path) => {
|
||||
$scope.$evalAsync(() => {
|
||||
kbnUrl.redirect(path);
|
||||
});
|
||||
});
|
||||
|
||||
// If returning to the app, we'll need to reset this state.
|
||||
setUserHasLeftApp(false);
|
||||
|
||||
$scope.$$postDigest(() => {
|
||||
appElement = document.getElementById('remoteClustersReactRoot');
|
||||
renderReact(appElement);
|
||||
|
||||
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 rollups, prevent Angular from re-matching the route and
|
||||
// rebuilding the app.
|
||||
if (isNavigationInApp) {
|
||||
$route.current = appRoute;
|
||||
} else {
|
||||
// Set internal flag so we can prevent reacting to the route change internally.
|
||||
setUserHasLeftApp(true);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
stopListeningForLocationChange();
|
||||
unmountComponentAtNode(appElement);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const { coreStart, pluginsStart } = createShim();
|
||||
const remoteClustersPlugin = new RemoteClustersPlugin();
|
||||
remoteClustersPlugin.start(coreStart, pluginsStart);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Import the EUI global scope so we can use EUI constants
|
||||
@import 'src/legacy/ui/public/styles/_styling_constants';
|
||||
@import './sections/remote_cluster_list/components/connection_status/index';
|
||||
@import './app/sections/remote_cluster_list/components/connection_status/index';
|
||||
|
||||
// Index management plugin styles
|
||||
|
||||
|
|
118
x-pack/plugins/remote_clusters/public/plugin.js
Normal file
118
x-pack/plugins/remote_clusters/public/plugin.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 { unmountComponentAtNode } from 'react-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import routes from 'ui/routes';
|
||||
|
||||
import template from './index.html';
|
||||
import { renderReact } from './app';
|
||||
import { CRUD_APP_BASE_PATH } from './app/constants';
|
||||
import { setUserHasLeftApp, setRedirect } from './app/services';
|
||||
import { init as initBreadcrumbs } from './app/services/breadcrumb';
|
||||
import { init as initDocumentation } from './app/services/documentation';
|
||||
import { init as initHttp } from './app/services/http';
|
||||
import { init as initUiMetric } from './app/services/ui_metric';
|
||||
import { init as initNotification } from './app/services/notification';
|
||||
|
||||
const REACT_ROOT_ID = 'remoteClustersReactRoot';
|
||||
|
||||
export class Plugin {
|
||||
start(coreStart, pluginsStart) {
|
||||
const {
|
||||
i18n: { Context },
|
||||
chrome: { setBreadcrumbs },
|
||||
notifications: { toasts },
|
||||
fatalError,
|
||||
http: { prependBasePath },
|
||||
injectedMetadata: { getInjectedVar },
|
||||
documentation: { elasticWebsiteUrl, docLinkVersion },
|
||||
} = coreStart;
|
||||
|
||||
if (getInjectedVar('remoteClustersUiEnabled')) {
|
||||
const {
|
||||
management: { getSection, breadcrumb: managementBreadcrumb },
|
||||
uiMetric: { track },
|
||||
} = pluginsStart;
|
||||
|
||||
const esSection = getSection('elasticsearch');
|
||||
esSection.register('remote_clusters', {
|
||||
visible: true,
|
||||
display: i18n.translate('xpack.remoteClusters.appTitle', { defaultMessage: 'Remote Clusters' }),
|
||||
order: 5,
|
||||
url: `#${CRUD_APP_BASE_PATH}/list`,
|
||||
});
|
||||
|
||||
// Initialize services
|
||||
initBreadcrumbs(setBreadcrumbs, managementBreadcrumb);
|
||||
initDocumentation(`${elasticWebsiteUrl}guide/en/elasticsearch/reference/${docLinkVersion}/`);
|
||||
initUiMetric(track);
|
||||
initNotification(toasts, fatalError);
|
||||
|
||||
const unmountReactApp = () => {
|
||||
const appElement = document.getElementById(REACT_ROOT_ID);
|
||||
if (appElement) {
|
||||
unmountComponentAtNode(appElement);
|
||||
}
|
||||
};
|
||||
|
||||
// NOTE: The New Platform will implicitly handle much of this logic by mounting the app at
|
||||
// the base route.
|
||||
routes.when(`${CRUD_APP_BASE_PATH}/:view?/:id?`, {
|
||||
template,
|
||||
controllerAs: 'remoteClusters',
|
||||
controller: class RemoteClustersController {
|
||||
constructor($scope, $route, $http, kbnUrl) {
|
||||
// NOTE: We depend upon Angular's $http service because it's decorated with interceptors,
|
||||
// e.g. to check license status per request.
|
||||
initHttp($http, prependBasePath);
|
||||
|
||||
setRedirect((path) => {
|
||||
$scope.$evalAsync(() => {
|
||||
kbnUrl.redirect(path);
|
||||
});
|
||||
});
|
||||
|
||||
// If returning to the app, we'll need to reset this state.
|
||||
setUserHasLeftApp(false);
|
||||
|
||||
// React-router's <Redirect> will cause this controller to re-execute without the $destroy
|
||||
// handler being called. This means the app will re-mount, so we need to unmount it first
|
||||
// here.
|
||||
unmountReactApp();
|
||||
|
||||
$scope.$$postDigest(() => {
|
||||
const appElement = document.getElementById(REACT_ROOT_ID);
|
||||
if (appElement) {
|
||||
renderReact(appElement, Context);
|
||||
}
|
||||
|
||||
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 the app, prevent Angular from re-matching the route and
|
||||
// rebuilding the app.
|
||||
if (isNavigationInApp) {
|
||||
$route.current = appRoute;
|
||||
} else {
|
||||
// Set internal flag so we can prevent reacting to the route change internally.
|
||||
setUserHasLeftApp(true);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
stopListeningForLocationChange();
|
||||
unmountReactApp();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,616 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { merge } from 'lodash';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiComboBox,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiLoadingKibana,
|
||||
EuiLoadingSpinner,
|
||||
EuiOverlayMask,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import {
|
||||
skippingDisconnectedClustersUrl,
|
||||
transportPortUrl,
|
||||
} from '../../../services/documentation_links';
|
||||
import { validateName, validateSeeds, validateSeed } from './validators';
|
||||
|
||||
const defaultFields = {
|
||||
name: '',
|
||||
seeds: [],
|
||||
skipUnavailable: false,
|
||||
};
|
||||
|
||||
export const RemoteClusterForm = injectI18n(
|
||||
class extends Component {
|
||||
static propTypes = {
|
||||
save: PropTypes.func.isRequired,
|
||||
cancel: PropTypes.func,
|
||||
isSaving: PropTypes.bool,
|
||||
saveError: PropTypes.object,
|
||||
fields: PropTypes.object,
|
||||
disabledFields: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
fields: merge({}, defaultFields),
|
||||
disabledFields: {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { fields, disabledFields } = props;
|
||||
const fieldsState = merge({}, defaultFields, fields);
|
||||
|
||||
this.state = {
|
||||
localSeedErrors: [],
|
||||
seedInput: '',
|
||||
fields: fieldsState,
|
||||
disabledFields,
|
||||
fieldsErrors: this.getFieldsErrors(fieldsState),
|
||||
areErrorsVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
getFieldsErrors(fields, seedInput = '') {
|
||||
const { name, seeds } = fields;
|
||||
return {
|
||||
name: validateName(name),
|
||||
seeds: validateSeeds(seeds, seedInput),
|
||||
};
|
||||
}
|
||||
|
||||
onFieldsChange = (changedFields) => {
|
||||
this.setState(({ fields: prevFields, seedInput }) => {
|
||||
const newFields = {
|
||||
...prevFields,
|
||||
...changedFields,
|
||||
};
|
||||
return ({
|
||||
fields: newFields,
|
||||
fieldsErrors: this.getFieldsErrors(newFields, seedInput),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
getAllFields() {
|
||||
const {
|
||||
fields: {
|
||||
name,
|
||||
seeds,
|
||||
skipUnavailable,
|
||||
},
|
||||
} = this.state;
|
||||
|
||||
return {
|
||||
name,
|
||||
seeds,
|
||||
skipUnavailable,
|
||||
};
|
||||
}
|
||||
|
||||
save = () => {
|
||||
const { save } = this.props;
|
||||
|
||||
if (this.hasErrors()) {
|
||||
this.setState({
|
||||
areErrorsVisible: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const cluster = this.getAllFields();
|
||||
save(cluster);
|
||||
};
|
||||
|
||||
onCreateSeed = (newSeed) => {
|
||||
// If the user just hit enter without typing anything, treat it as a no-op.
|
||||
if (!newSeed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localSeedErrors = validateSeed(newSeed);
|
||||
|
||||
if (localSeedErrors.length !== 0) {
|
||||
this.setState({
|
||||
localSeedErrors,
|
||||
});
|
||||
|
||||
// Return false to explicitly reject the user's input.
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
fields: {
|
||||
seeds,
|
||||
},
|
||||
} = this.state;
|
||||
|
||||
const newSeeds = seeds.slice(0);
|
||||
newSeeds.push(newSeed.toLowerCase());
|
||||
this.onFieldsChange({ seeds: newSeeds });
|
||||
};
|
||||
|
||||
onSeedsInputChange = (seedInput) => {
|
||||
if (!seedInput) {
|
||||
// If empty seedInput ("") don't do anything. This happens
|
||||
// right after a seed is created.
|
||||
return;
|
||||
}
|
||||
|
||||
const { intl } = this.props;
|
||||
|
||||
this.setState(({ fields, localSeedErrors }) => {
|
||||
const { seeds } = fields;
|
||||
|
||||
// Allow typing to clear the errors, but not to add new ones.
|
||||
const errors = (!seedInput || validateSeed(seedInput).length === 0) ? [] : localSeedErrors;
|
||||
|
||||
// EuiComboBox internally checks for duplicates and prevents calling onCreateOption if the
|
||||
// input is a duplicate. So we need to surface this error here instead.
|
||||
const isDuplicate = seeds.includes(seedInput);
|
||||
|
||||
if (isDuplicate) {
|
||||
errors.push(intl.formatMessage({
|
||||
id: 'xpack.remoteClusters.remoteClusterForm.localSeedError.duplicateMessage',
|
||||
defaultMessage: `Duplicate seed nodes aren't allowed.`,
|
||||
}));
|
||||
}
|
||||
|
||||
return ({
|
||||
localSeedErrors: errors,
|
||||
fieldsErrors: this.getFieldsErrors(fields, seedInput),
|
||||
seedInput,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onSeedsChange = (seeds) => {
|
||||
this.onFieldsChange({ seeds: seeds.map(({ label }) => label) });
|
||||
};
|
||||
|
||||
onSkipUnavailableChange = (e) => {
|
||||
const skipUnavailable = e.target.checked;
|
||||
this.onFieldsChange({ skipUnavailable });
|
||||
};
|
||||
|
||||
resetToDefault = (fieldName) => {
|
||||
this.onFieldsChange({
|
||||
[fieldName]: defaultFields[fieldName],
|
||||
});
|
||||
};
|
||||
|
||||
hasErrors = () => {
|
||||
const { fieldsErrors, localSeedErrors } = this.state;
|
||||
const errorValues = Object.values(fieldsErrors);
|
||||
const hasErrors = errorValues.some(error => error != null) || localSeedErrors.length;
|
||||
return hasErrors;
|
||||
};
|
||||
|
||||
renderSeeds() {
|
||||
const {
|
||||
areErrorsVisible,
|
||||
fields: {
|
||||
seeds,
|
||||
},
|
||||
fieldsErrors: {
|
||||
seeds: errorsSeeds,
|
||||
},
|
||||
localSeedErrors,
|
||||
} = this.state;
|
||||
|
||||
const { intl } = this.props;
|
||||
|
||||
// Show errors if there is a general form error or local errors.
|
||||
const areFormErrorsVisible = Boolean(areErrorsVisible && errorsSeeds);
|
||||
const showErrors = areFormErrorsVisible || localSeedErrors.length !== 0;
|
||||
const errors = areFormErrorsVisible ? localSeedErrors.concat(errorsSeeds) : localSeedErrors;
|
||||
|
||||
const formattedSeeds = seeds.map(seed => ({ label: seed }));
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={(
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsTitle"
|
||||
defaultMessage="Seed nodes for cluster discovery"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
)}
|
||||
description={(
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsDescription1"
|
||||
defaultMessage="A list of remote cluster nodes to query for the cluster state.
|
||||
Specify multiple seed nodes so discovery doesn't fail if a node is unavailable."
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
data-test-subj="remoteClusterFormSeedNodesFormRow"
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.fieldSeedsLabel"
|
||||
defaultMessage="Seed nodes"
|
||||
/>
|
||||
)}
|
||||
helpText={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText"
|
||||
defaultMessage="An IP address or host name, followed by the {transportPort} of the remote cluster."
|
||||
values={{
|
||||
transportPort: (
|
||||
<EuiLink href={transportPortUrl} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText.transportPortLinkText"
|
||||
defaultMessage="transport port"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
isInvalid={showErrors}
|
||||
error={errors}
|
||||
fullWidth
|
||||
>
|
||||
<EuiComboBox
|
||||
noSuggestions
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'xpack.remoteClusters.remoteClusterForm.fieldSeedsPlaceholder',
|
||||
defaultMessage: 'host:port',
|
||||
})}
|
||||
selectedOptions={formattedSeeds}
|
||||
onCreateOption={this.onCreateSeed}
|
||||
onChange={this.onSeedsChange}
|
||||
onSearchChange={this.onSeedsInputChange}
|
||||
isInvalid={showErrors}
|
||||
fullWidth
|
||||
data-test-subj="remoteClusterFormSeedsInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderSkipUnavailable() {
|
||||
const {
|
||||
fields: {
|
||||
skipUnavailable,
|
||||
},
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={(
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableTitle"
|
||||
defaultMessage="Make remote cluster optional"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
)}
|
||||
description={(
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription"
|
||||
defaultMessage="By default, a request fails if any of the queried remote clusters
|
||||
are unavailable. To continue sending a request to other remote clusters if this
|
||||
cluster is unavailable, enable {optionName}. {learnMoreLink}"
|
||||
values={{
|
||||
optionName: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.optionNameLabel"
|
||||
defaultMessage="Skip if unavailable"
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
learnMoreLink: (
|
||||
<EuiLink href={skippingDisconnectedClustersUrl} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.learnMoreLinkLabel"
|
||||
defaultMessage="Learn more."
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
data-test-subj="remoteClusterFormSkipUnavailableFormRow"
|
||||
className="remoteClusterSkipIfUnavailableSwitch"
|
||||
hasEmptyLabelSpace
|
||||
fullWidth
|
||||
helpText={
|
||||
skipUnavailable !== defaultFields.skipUnavailable ? (
|
||||
<EuiLink onClick={() => { this.resetToDefault('skipUnavailable'); }}>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableResetLabel"
|
||||
defaultMessage="Reset to default"
|
||||
/>
|
||||
</EuiLink>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableLabel', {
|
||||
defaultMessage: 'Skip if unavailable',
|
||||
})}
|
||||
checked={skipUnavailable}
|
||||
onChange={this.onSkipUnavailableChange}
|
||||
data-test-subj="remoteClusterFormSkipUnavailableFormToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderActions() {
|
||||
const { isSaving, cancel } = this.props;
|
||||
const { areErrorsVisible } = this.state;
|
||||
|
||||
if (isSaving) {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexStart" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="l"/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.actions.savingText"
|
||||
defaultMessage="Saving"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
let cancelButton;
|
||||
|
||||
if (cancel) {
|
||||
cancelButton = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
onClick={cancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
const isSaveDisabled = areErrorsVisible && this.hasErrors();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="remoteClusterFormSaveButton"
|
||||
color="secondary"
|
||||
iconType="check"
|
||||
onClick={this.save}
|
||||
fill
|
||||
disabled={isSaveDisabled}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
||||
{cancelButton}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderSavingFeedback() {
|
||||
if (this.props.isSaving) {
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiLoadingKibana size="xl"/>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderSaveErrorFeedback() {
|
||||
const { saveError } = this.props;
|
||||
|
||||
if (saveError) {
|
||||
const { message, cause } = saveError;
|
||||
|
||||
let errorBody;
|
||||
|
||||
if (cause) {
|
||||
if (cause.length === 1) {
|
||||
errorBody = (
|
||||
<p>{cause[0]}</p>
|
||||
);
|
||||
} else {
|
||||
errorBody = (
|
||||
<ul>
|
||||
{cause.map(causeValue => <li key={causeValue}>{causeValue}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={message}
|
||||
icon="cross"
|
||||
color="danger"
|
||||
>
|
||||
{errorBody}
|
||||
</EuiCallOut>
|
||||
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderErrors = () => {
|
||||
const { areErrorsVisible } = this.state;
|
||||
const hasErrors = this.hasErrors();
|
||||
|
||||
if (!areErrorsVisible || !hasErrors) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
data-test-subj="remoteClusterFormGlobalError"
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.errorTitle"
|
||||
defaultMessage="Fix errors before continuing."
|
||||
/>
|
||||
)}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
disabledFields: {
|
||||
name: disabledName,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
areErrorsVisible,
|
||||
fields: {
|
||||
name,
|
||||
},
|
||||
fieldsErrors: {
|
||||
name: errorClusterName,
|
||||
},
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{this.renderSaveErrorFeedback()}
|
||||
|
||||
<EuiForm>
|
||||
<EuiDescribedFormGroup
|
||||
title={(
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionNameTitle"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
)}
|
||||
description={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.sectionNameDescription"
|
||||
defaultMessage="A unique name for the remote cluster."
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
data-test-subj="remoteClusterFormNameFormRow"
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.fieldNameLabel"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
)}
|
||||
helpText={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterForm.fieldNameLabelHelpText"
|
||||
defaultMessage="Name can only contain letters, numbers, underscores, and dashes."
|
||||
/>
|
||||
)}
|
||||
error={errorClusterName}
|
||||
isInvalid={Boolean(areErrorsVisible && errorClusterName)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
isInvalid={Boolean(areErrorsVisible && errorClusterName)}
|
||||
value={name}
|
||||
onChange={e => this.onFieldsChange({ name: e.target.value })}
|
||||
fullWidth
|
||||
disabled={disabledName}
|
||||
data-test-subj="remoteClusterFormNameInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{this.renderSeeds()}
|
||||
|
||||
{this.renderSkipUnavailable()}
|
||||
</EuiForm>
|
||||
|
||||
{this.renderErrors()}
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
{this.renderActions()}
|
||||
|
||||
{this.renderSavingFeedback()}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { PureComponent } 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 {
|
||||
EuiPageContent,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CRUD_APP_BASE_PATH } from '../../constants';
|
||||
import { listBreadcrumb, addBreadcrumb, getRouter, redirect, extractQueryParams } from '../../services';
|
||||
import { RemoteClusterPageTitle, RemoteClusterForm } from '../components';
|
||||
|
||||
export const RemoteClusterAdd = injectI18n(
|
||||
class extends PureComponent {
|
||||
static propTypes = {
|
||||
addCluster: PropTypes.func,
|
||||
isAddingCluster: PropTypes.bool,
|
||||
addClusterError: PropTypes.object,
|
||||
clearAddClusterErrors: PropTypes.func,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
chrome.breadcrumbs.set([ MANAGEMENT_BREADCRUMB, listBreadcrumb, addBreadcrumb ]);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Clean up after ourselves.
|
||||
this.props.clearAddClusterErrors();
|
||||
}
|
||||
|
||||
save = (clusterConfig) => {
|
||||
this.props.addCluster(clusterConfig);
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
const { history, route: { location: { search } } } = getRouter();
|
||||
const { redirect: redirectUrl } = extractQueryParams(search);
|
||||
|
||||
if (redirectUrl) {
|
||||
const decodedRedirect = decodeURIComponent(redirectUrl);
|
||||
redirect(decodedRedirect);
|
||||
} else {
|
||||
history.push(CRUD_APP_BASE_PATH);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isAddingCluster, addClusterError } = this.props;
|
||||
|
||||
return (
|
||||
<EuiPageContent
|
||||
horizontalPosition="center"
|
||||
className="remoteClusterAddPage"
|
||||
>
|
||||
<RemoteClusterPageTitle
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.addTitle"
|
||||
defaultMessage="Add remote cluster"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<RemoteClusterForm
|
||||
isSaving={isAddingCluster}
|
||||
saveError={addClusterError}
|
||||
save={this.save}
|
||||
cancel={this.cancel}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -1,236 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component, 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 {
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiPageContent,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CRUD_APP_BASE_PATH } from '../../constants';
|
||||
import {
|
||||
buildListBreadcrumb,
|
||||
editBreadcrumb,
|
||||
extractQueryParams,
|
||||
getRouter,
|
||||
getRouterLinkProps,
|
||||
redirect,
|
||||
} from '../../services';
|
||||
import { RemoteClusterPageTitle, RemoteClusterForm, ConfiguredByNodeWarning } from '../components';
|
||||
|
||||
const disabledFields = {
|
||||
name: true,
|
||||
};
|
||||
|
||||
export const RemoteClusterEdit = injectI18n(
|
||||
class extends Component {
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
cluster: PropTypes.object,
|
||||
startEditingCluster: PropTypes.func,
|
||||
stopEditingCluster: PropTypes.func,
|
||||
editCluster: PropTypes.func,
|
||||
isEditingCluster: PropTypes.bool,
|
||||
getEditClusterError: PropTypes.string,
|
||||
clearEditClusterErrors: PropTypes.func,
|
||||
openDetailPanel: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const {
|
||||
match: {
|
||||
params: {
|
||||
name,
|
||||
},
|
||||
},
|
||||
} = props;
|
||||
|
||||
chrome.breadcrumbs.set([
|
||||
MANAGEMENT_BREADCRUMB,
|
||||
buildListBreadcrumb(`?cluster=${name}`),
|
||||
editBreadcrumb,
|
||||
]);
|
||||
|
||||
this.state = {
|
||||
clusterName: name,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { startEditingCluster } = this.props;
|
||||
const { clusterName } = this.state;
|
||||
startEditingCluster(clusterName);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Clean up after ourselves.
|
||||
this.props.clearEditClusterErrors();
|
||||
this.props.stopEditingCluster();
|
||||
}
|
||||
|
||||
save = (clusterConfig) => {
|
||||
this.props.editCluster(clusterConfig);
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
const { openDetailPanel } = this.props;
|
||||
const { clusterName } = this.state;
|
||||
const { history, route: { location: { search } } } = getRouter();
|
||||
const { redirect: redirectUrl } = extractQueryParams(search);
|
||||
|
||||
if (redirectUrl) {
|
||||
const decodedRedirect = decodeURIComponent(redirectUrl);
|
||||
redirect(decodedRedirect);
|
||||
} else {
|
||||
history.push(CRUD_APP_BASE_PATH);
|
||||
openDetailPanel(clusterName);
|
||||
}
|
||||
};
|
||||
|
||||
renderContent() {
|
||||
const {
|
||||
clusterName,
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
cluster,
|
||||
isEditingCluster,
|
||||
getEditClusterError,
|
||||
} = this.props;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.edit.loadingLabel"
|
||||
defaultMessage="Loading remote cluster..."
|
||||
/>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.edit.loadingErrorTitle"
|
||||
defaultMessage="Error loading remote cluster"
|
||||
/>
|
||||
)}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.edit.loadingErrorMessage"
|
||||
defaultMessage="The remote cluster '{name}' does not exist."
|
||||
values={{ name: clusterName }}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
{...getRouterLinkProps(CRUD_APP_BASE_PATH)}
|
||||
iconType="arrowLeft"
|
||||
flush="left"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.edit.viewRemoteClustersButtonLabel"
|
||||
defaultMessage="View remote clusters"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const { isConfiguredByNode } = cluster;
|
||||
|
||||
if (isConfiguredByNode) {
|
||||
return (
|
||||
<Fragment>
|
||||
<ConfiguredByNodeWarning />
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
onClick={this.cancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.edit.backToRemoteClustersButtonLabel"
|
||||
defaultMessage="Back to remote clusters"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RemoteClusterForm
|
||||
fields={cluster}
|
||||
disabledFields={disabledFields}
|
||||
isSaving={isEditingCluster}
|
||||
saveError={getEditClusterError}
|
||||
save={this.save}
|
||||
cancel={this.cancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiPageContent
|
||||
horizontalPosition="center"
|
||||
className="remoteClusterAddPage"
|
||||
>
|
||||
<RemoteClusterPageTitle
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.editTitle"
|
||||
defaultMessage="Edit remote cluster"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{this.renderContent()}
|
||||
</EuiPageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -1,116 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiConfirmModal,
|
||||
EuiOverlayMask,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export const RemoveClusterButtonProvider = injectI18n(
|
||||
class extends Component {
|
||||
static propTypes = {
|
||||
removeClusters: PropTypes.func.isRequired,
|
||||
clusterNames: PropTypes.array.isRequired,
|
||||
children: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
isModalOpen: false,
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
showConfirmModal = () => {
|
||||
this.setState({
|
||||
isModalOpen: true,
|
||||
});
|
||||
};
|
||||
|
||||
closeConfirmModal = () => {
|
||||
this.setState({
|
||||
isModalOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
onConfirm = () => {
|
||||
const { removeClusters, clusterNames } = this.props;
|
||||
removeClusters(clusterNames);
|
||||
this.closeConfirmModal();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, clusterNames, children } = this.props;
|
||||
const { isModalOpen } = this.state;
|
||||
const isSingleCluster = clusterNames.length === 1;
|
||||
let modal;
|
||||
|
||||
if (isModalOpen) {
|
||||
const title = isSingleCluster ? intl.formatMessage({
|
||||
id: 'xpack.remoteClusters.removeButton.confirmModal.deleteSingleClusterTitle',
|
||||
defaultMessage: 'Remove remote cluster \'{name}\'?',
|
||||
}, { name: clusterNames[0] }) : intl.formatMessage({
|
||||
id: 'xpack.remoteClusters.removeButton.confirmModal.multipleDeletionTitle',
|
||||
defaultMessage: 'Remove {count} remote clusters?',
|
||||
}, { count: clusterNames.length });
|
||||
|
||||
const content = (
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.removeButton.confirmModal.multipleDeletionDescription"
|
||||
defaultMessage="You are about to remove these remote clusters:"
|
||||
/>
|
||||
</p>
|
||||
<ul>{clusterNames.map(name => <li key={name}>{name}</li>)}</ul>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
modal = (
|
||||
<EuiOverlayMask>
|
||||
{ /* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */ }
|
||||
<EuiConfirmModal
|
||||
data-test-subj="remoteClustersDeleteConfirmModal"
|
||||
title={title}
|
||||
onCancel={this.closeConfirmModal}
|
||||
onConfirm={this.onConfirm}
|
||||
cancelButtonText={
|
||||
intl.formatMessage({
|
||||
id: 'xpack.remoteClusters.removeButton.confirmModal.cancelButtonText',
|
||||
defaultMessage: 'Cancel',
|
||||
})
|
||||
}
|
||||
buttonColor="danger"
|
||||
confirmButtonText={
|
||||
intl.formatMessage({
|
||||
id: 'xpack.remoteClusters.removeButton.confirmModal.confirmButtonText',
|
||||
defaultMessage: 'Remove',
|
||||
})
|
||||
}
|
||||
onMouseOver={this.onMouseOverModal}
|
||||
>
|
||||
{!isSingleCluster && content}
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{children(this.showConfirmModal)}
|
||||
{modal}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -1,354 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListDescription,
|
||||
EuiDescriptionListTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiIcon,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CRUD_APP_BASE_PATH } from '../../../constants';
|
||||
import { getRouterLinkProps } from '../../../services';
|
||||
import { ConfiguredByNodeWarning } from '../../components';
|
||||
import { ConnectionStatus, RemoveClusterButtonProvider } from '../components';
|
||||
|
||||
export const DetailPanel = injectI18n(
|
||||
class extends Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
cluster: PropTypes.object,
|
||||
closeDetailPanel: PropTypes.func.isRequired,
|
||||
clusterName: PropTypes.string,
|
||||
}
|
||||
|
||||
renderSkipUnavailableValue(skipUnavailable) {
|
||||
if (skipUnavailable === true) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.skipUnavailableTrueValue"
|
||||
defaultMessage="Yes"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (skipUnavailable === false) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.skipUnavailableFalseValue"
|
||||
defaultMessage="No"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.skipUnavailableNullValue"
|
||||
defaultMessage="Default"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderClusterNotFound() {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
data-test-subj="remoteClusterDetailClusterNotFound"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon size="m" type="alert" color="danger" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.notFoundLabel"
|
||||
defaultMessage="Remote cluster not found"
|
||||
/>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderClusterConfiguredByNodeWarning({ isConfiguredByNode }) {
|
||||
if (!isConfiguredByNode) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<ConfiguredByNodeWarning />
|
||||
<EuiSpacer size="l" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderCluster({
|
||||
isConnected,
|
||||
connectedNodesCount,
|
||||
skipUnavailable,
|
||||
seeds,
|
||||
maxConnectionsPerCluster,
|
||||
initialConnectTimeout,
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="xpack.remoteClusters.detailPanel.statusTitle"
|
||||
data-test-subj="remoteClusterDetailPanelStatusSection"
|
||||
>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.statusTitle"
|
||||
defaultMessage="Status"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList data-test-subj="remoteClusterDetailPanelStatusValues">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.connectedLabel"
|
||||
defaultMessage="Connection"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription data-test-subj="remoteClusterDetailIsConnected">
|
||||
<ConnectionStatus isConnected={isConnected} />
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.connectedNodesLabel"
|
||||
defaultMessage="Connected nodes"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription data-test-subj="remoteClusterDetailConnectedNodesCount">
|
||||
{connectedNodesCount}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.seedsLabel"
|
||||
defaultMessage="Seeds"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription data-test-subj="remoteClusterDetailSeeds">
|
||||
{seeds.map(seed => <EuiText key={seed}>{seed}</EuiText>)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.skipUnavailableLabel"
|
||||
defaultMessage="Skip unavailable"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription data-test-subj="remoteClusterDetailSkipUnavailable">
|
||||
{this.renderSkipUnavailableValue(skipUnavailable)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.maxConnectionsPerClusterLabel"
|
||||
defaultMessage="Maximum number of connections"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription data-test-subj="remoteClusterDetailMaxConnections">
|
||||
{maxConnectionsPerCluster}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.initialConnectTimeoutLabel"
|
||||
defaultMessage="Initial connect timeout"
|
||||
/>
|
||||
</EuiTitle>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription data-test-subj="remoteClusterDetailInitialConnectTimeout">
|
||||
{initialConnectTimeout}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiDescriptionList>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderFlyoutBody() {
|
||||
const { cluster } = this.props;
|
||||
|
||||
return (
|
||||
<EuiFlyoutBody>
|
||||
{!cluster && (
|
||||
this.renderClusterNotFound()
|
||||
)}
|
||||
{cluster && (
|
||||
<Fragment>
|
||||
{this.renderClusterConfiguredByNodeWarning(cluster)}
|
||||
{this.renderCluster(cluster)}
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
);
|
||||
}
|
||||
|
||||
renderFlyoutFooter() {
|
||||
const {
|
||||
cluster,
|
||||
clusterName,
|
||||
closeDetailPanel,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
flush="left"
|
||||
onClick={closeDetailPanel}
|
||||
data-test-subj="remoteClusterDetailsPanelCloseButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.closeButtonLabel"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
{cluster && !cluster.isConfiguredByNode && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<RemoveClusterButtonProvider clusterNames={[clusterName]}>
|
||||
{(removeCluster) => (
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
onClick={removeCluster}
|
||||
data-test-subj="remoteClusterDetailPanelRemoveButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.removeButtonLabel"
|
||||
defaultMessage="Remove"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</RemoveClusterButtonProvider>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
{...getRouterLinkProps(`${CRUD_APP_BASE_PATH}/edit/${clusterName}`)}
|
||||
fill
|
||||
color="primary"
|
||||
data-test-subj="remoteClusterDetailPanelEditButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.detailPanel.editButtonLabel"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isOpen, closeDetailPanel, clusterName } = this.props;
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
data-test-subj="remoteClusterDetailFlyout"
|
||||
onClose={closeDetailPanel}
|
||||
aria-labelledby="remoteClusterDetailsFlyoutTitle"
|
||||
size="m"
|
||||
maxWidth={400}
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle
|
||||
size="m"
|
||||
id="remoteClusterDetailsFlyoutTitle"
|
||||
data-test-subj="remoteClusterDetailsFlyoutTitle"
|
||||
>
|
||||
<h2>{clusterName}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
{this.renderFlyoutBody()}
|
||||
|
||||
{this.renderFlyoutFooter()}
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -1,295 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component, 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 {
|
||||
EuiButton,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingKibana,
|
||||
EuiLoadingSpinner,
|
||||
EuiOverlayMask,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CRUD_APP_BASE_PATH } from '../../constants';
|
||||
import { getRouterLinkProps, extractQueryParams, listBreadcrumb } from '../../services';
|
||||
|
||||
import {
|
||||
RemoteClusterTable,
|
||||
} from './remote_cluster_table';
|
||||
|
||||
import {
|
||||
DetailPanel,
|
||||
} from './detail_panel';
|
||||
|
||||
const REFRESH_RATE_MS = 30000;
|
||||
|
||||
export const RemoteClusterList = injectI18n(
|
||||
class extends Component {
|
||||
static propTypes = {
|
||||
loadClusters: PropTypes.func.isRequired,
|
||||
refreshClusters: PropTypes.func.isRequired,
|
||||
openDetailPanel: PropTypes.func.isRequired,
|
||||
closeDetailPanel: PropTypes.func.isRequired,
|
||||
isDetailPanelOpen: PropTypes.bool,
|
||||
clusters: PropTypes.array,
|
||||
isLoading: PropTypes.bool,
|
||||
isCopyingCluster: PropTypes.bool,
|
||||
isRemovingCluster: PropTypes.bool,
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {
|
||||
openDetailPanel,
|
||||
closeDetailPanel,
|
||||
isDetailPanelOpen,
|
||||
history: {
|
||||
location: {
|
||||
search,
|
||||
},
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const { cluster: clusterName } = extractQueryParams(search);
|
||||
|
||||
// Show deeplinked remoteCluster whenever remoteClusters get loaded or the URL changes.
|
||||
if (clusterName != null) {
|
||||
openDetailPanel(clusterName);
|
||||
} else if (isDetailPanelOpen) {
|
||||
closeDetailPanel();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadClusters();
|
||||
this.interval = setInterval(this.props.refreshClusters, REFRESH_RATE_MS);
|
||||
chrome.breadcrumbs.set([ MANAGEMENT_BREADCRUMB, listBreadcrumb ]);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
getHeaderSection(isAuthorized) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterListTitle"
|
||||
defaultMessage="Remote Clusters"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeaderSection>
|
||||
|
||||
{ isAuthorized && (
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiButton
|
||||
{...getRouterLinkProps(`${CRUD_APP_BASE_PATH}/add`)}
|
||||
fill
|
||||
data-test-subj="remoteClusterCreateButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.connectButtonLabel"
|
||||
defaultMessage="Add a remote cluster"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiPageContentHeaderSection>
|
||||
)}
|
||||
</EuiPageContentHeader>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderBlockingAction() {
|
||||
const { isCopyingCluster, isRemovingCluster } = this.props;
|
||||
|
||||
if (isCopyingCluster || isRemovingCluster) {
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiLoadingKibana size="xl"/>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderNoPermission() {
|
||||
const { intl } = this.props;
|
||||
const title = intl.formatMessage({
|
||||
id: 'xpack.remoteClusters.remoteClusterList.noPermissionTitle',
|
||||
defaultMessage: 'Permission error',
|
||||
});
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={title}
|
||||
color="warning"
|
||||
iconType="help"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.noPermissionText"
|
||||
defaultMessage="You do not have permission to view or add remote clusters."
|
||||
/>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
renderError(error) {
|
||||
// We can safely depend upon the shape of this error coming from Angular $http, because we
|
||||
// handle unexpected error shapes in the API action.
|
||||
const {
|
||||
statusCode,
|
||||
error: errorString,
|
||||
} = error.data;
|
||||
|
||||
const { intl } = this.props;
|
||||
const title = intl.formatMessage({
|
||||
id: 'xpack.remoteClusters.remoteClusterList.loadingErrorTitle',
|
||||
defaultMessage: 'Error loading remote clusters',
|
||||
});
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={title}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
{statusCode} {errorString}
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmpty() {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="remoteClusterListEmptyPrompt"
|
||||
iconType="managementApp"
|
||||
title={(
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.emptyPromptTitle"
|
||||
defaultMessage="Add your first remote cluster"
|
||||
/>
|
||||
</h1>
|
||||
)}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.emptyPromptDescription"
|
||||
defaultMessage="Remote clusters create a uni-directional connection from your
|
||||
local cluster to other clusters."
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
{...getRouterLinkProps(`${CRUD_APP_BASE_PATH}/add`)}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="remoteClusterEmptyPromptCreateButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.emptyPrompt.connectButtonLabel"
|
||||
defaultMessage="Add a remote cluster"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
data-test-subj="remoteClustersTableLoading"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.remoteClusters.remoteClusterList.loadingTitle"
|
||||
defaultMessage="Loading remote clusters..."
|
||||
/>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderList() {
|
||||
const { clusters } = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<RemoteClusterTable clusters={clusters} />
|
||||
<DetailPanel />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, clusters, clusterLoadError } = this.props;
|
||||
const isEmpty = !isLoading && !clusters.length;
|
||||
const isAuthorized = !clusterLoadError || clusterLoadError.status !== 403;
|
||||
const isHeaderVisible = clusterLoadError || !isEmpty;
|
||||
|
||||
let content;
|
||||
|
||||
if (clusterLoadError) {
|
||||
if (!isAuthorized) {
|
||||
content = this.renderNoPermission();
|
||||
} else {
|
||||
content = this.renderError(clusterLoadError);
|
||||
}
|
||||
} else if (isEmpty) {
|
||||
content = this.renderEmpty();
|
||||
} else if (isLoading) {
|
||||
content = this.renderLoading();
|
||||
} else {
|
||||
content = this.renderList();
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
{(isHeaderVisible) && this.getHeaderSection(isAuthorized)}
|
||||
{content}
|
||||
{this.renderBlockingAction()}
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
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