Index Management new platform migration (#49359) (#50812)

This commit is contained in:
Alison Goryachev 2019-11-15 15:20:21 -05:00 committed by GitHub
parent 3a078811f8
commit f530fecdd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
179 changed files with 1328 additions and 772 deletions

View file

@ -0,0 +1,166 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useEffect, useState, useRef } from 'react';
import { HttpServiceBase } from '../../../../../src/core/public';
export interface SendRequestConfig {
path: string;
method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head';
body?: any;
}
export interface SendRequestResponse {
data: any;
error: Error | null;
}
export interface UseRequestConfig extends SendRequestConfig {
pollIntervalMs?: number;
initialData?: any;
deserializer?: (data: any) => any;
}
export interface UseRequestResponse {
isInitialRequest: boolean;
isLoading: boolean;
error: null | unknown;
data: any;
sendRequest: (...args: any[]) => Promise<SendRequestResponse>;
}
export const sendRequest = async (
httpClient: HttpServiceBase,
{ path, method, body }: SendRequestConfig
): Promise<SendRequestResponse> => {
try {
const response = await httpClient[method](path, { body });
return {
data: response.data ? response.data : response,
error: null,
};
} catch (e) {
return {
data: null,
error: e.response && e.response.data ? e.response.data : e.body,
};
}
};
export const useRequest = (
httpClient: HttpServiceBase,
{
path,
method,
body,
pollIntervalMs,
initialData,
deserializer = (data: any): any => data,
}: UseRequestConfig
): UseRequestResponse => {
// Main states for tracking request status and data
const [error, setError] = useState<null | any>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [data, setData] = useState<any>(initialData);
// Consumers can use isInitialRequest to implement a polling UX.
const [isInitialRequest, setIsInitialRequest] = useState<boolean>(true);
const pollInterval = useRef<any>(null);
const pollIntervalId = useRef<any>(null);
// We always want to use the most recently-set interval in scheduleRequest.
pollInterval.current = pollIntervalMs;
// Tied to every render and bound to each request.
let isOutdatedRequest = false;
const scheduleRequest = () => {
// Clear current interval
if (pollIntervalId.current) {
clearTimeout(pollIntervalId.current);
}
// Set new interval
if (pollInterval.current) {
pollIntervalId.current = setTimeout(_sendRequest, pollInterval.current);
}
};
const _sendRequest = async () => {
// We don't clear error or data, so it's up to the consumer to decide whether to display the
// "old" error/data or loading state when a new request is in-flight.
setIsLoading(true);
const requestBody = {
path,
method,
body,
};
const response = await sendRequest(httpClient, requestBody);
const { data: serializedResponseData, error: responseError } = response;
const responseData = deserializer(serializedResponseData);
// If an outdated request has resolved, DON'T update state, but DO allow the processData handler
// to execute side effects like update telemetry.
if (isOutdatedRequest) {
return { data: null, error: null };
}
setError(responseError);
setData(responseData);
setIsLoading(false);
setIsInitialRequest(false);
// If we're on an interval, we need to schedule the next request. This also allows us to reset
// the interval if the user has manually requested the data, to avoid doubled-up requests.
scheduleRequest();
return { data: serializedResponseData, error: responseError };
};
useEffect(() => {
_sendRequest();
// To be functionally correct we'd send a new request if the method, path, or body changes.
// But it doesn't seem likely that the method will change and body is likely to be a new
// object even if its shape hasn't changed, so for now we're just watching the path.
}, [path]);
useEffect(() => {
scheduleRequest();
// Clean up intervals and inflight requests and corresponding state changes
return () => {
isOutdatedRequest = true;
if (pollIntervalId.current) {
clearTimeout(pollIntervalId.current);
}
};
}, [pollIntervalMs]);
return {
isInitialRequest,
isLoading,
error,
data,
sendRequest: _sendRequest, // Gives the user the ability to manually request data
};
};

View file

@ -9,7 +9,7 @@ import { PLUGIN } from './common/constants';
import { registerLicenseChecker } from './server/lib/register_license_checker';
import { registerRoutes } from './server/routes/register_routes';
import { ccrDataEnricher } from './cross_cluster_replication_data';
import { addIndexManagementDataEnricher } from '../index_management/index_management_data';
import { addIndexManagementDataEnricher } from '../index_management/server/index_management_data';
export function crossClusterReplication(kibana) {
return new kibana.Plugin({
id: PLUGIN.ID,

View file

@ -7,7 +7,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
import { getIndexListUri } from '../../../../../../../../index_management/public/services/navigation';
import { getIndexListUri } from '../../../../../../../../index_management/public/app/services/navigation';
import {
EuiButton,

View file

@ -7,7 +7,7 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
import { getIndexListUri } from '../../../../../../../../index_management/public/services/navigation';
import { getIndexListUri } from '../../../../../../../../index_management/public/app/services/navigation';
import {
EuiButton,

View file

@ -36,7 +36,7 @@ import {
} from '@elastic/eui';
import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
import { getIndexListUri } from '../../../../../../index_management/public/services/navigation';
import { getIndexListUri } from '../../../../../../index_management/public/app/services/navigation';
import { BASE_PATH, UIM_EDIT_CLICK } from '../../../../../common/constants';
import { getPolicyPath } from '../../../../services/navigation';
import { flattenPanelTree } from '../../../../services/flatten_panel_tree';

View file

@ -13,9 +13,9 @@ import {
findTestSubject,
nextTick,
} from '../../../../../../test_utils';
import { IndexManagementHome } from '../../../public/sections/home';
import { IndexManagementHome } from '../../../public/app/sections/home';
import { BASE_PATH } from '../../../common/constants';
import { indexManagementStore } from '../../../public/store';
import { indexManagementStore } from '../../../public/app/store';
import { Template } from '../../../common/types';
const testBedConfig: TestBedConfig = {

View file

@ -5,15 +5,14 @@
*/
import sinon, { SinonFakeServer } from 'sinon';
const API_PATH = '/api/index_management';
import { API_BASE_PATH } from '../../../common/constants';
type HttpResponse = Record<string, any> | any[];
// Register helpers to mock HTTP Requests
const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
const setLoadTemplatesResponse = (response: HttpResponse = []) => {
server.respondWith('GET', `${API_PATH}/templates`, [
server.respondWith('GET', `${API_BASE_PATH}/templates`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(response),
@ -21,7 +20,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
};
const setLoadIndicesResponse = (response: HttpResponse = []) => {
server.respondWith('GET', `${API_PATH}/indices`, [
server.respondWith('GET', `${API_BASE_PATH}/indices`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(response),
@ -29,7 +28,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
};
const setDeleteTemplateResponse = (response: HttpResponse = []) => {
server.respondWith('DELETE', `${API_PATH}/templates`, [
server.respondWith('DELETE', `${API_BASE_PATH}/templates`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(response),
@ -40,7 +39,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
const status = error ? error.status || 400 : 200;
const body = error ? error.body : response;
server.respondWith('GET', `${API_PATH}/templates/:id`, [
server.respondWith('GET', `${API_BASE_PATH}/templates/:id`, [
status,
{ 'Content-Type': 'application/json' },
JSON.stringify(body),
@ -48,10 +47,10 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
};
const setCreateTemplateResponse = (response?: HttpResponse, error?: any) => {
const status = error ? error.status || 400 : 200;
const status = error ? error.body.status || 400 : 200;
const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
server.respondWith('PUT', `${API_PATH}/templates`, [
server.respondWith('PUT', `${API_BASE_PATH}/templates`, [
status,
{ 'Content-Type': 'application/json' },
body,
@ -62,7 +61,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
const status = error ? error.status || 400 : 200;
const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
server.respondWith('PUT', `${API_PATH}/templates/:name`, [
server.respondWith('PUT', `${API_BASE_PATH}/templates/:name`, [
status,
{ 'Content-Type': 'application/json' },
body,

View file

@ -7,15 +7,30 @@
import axios from 'axios';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import { init as initHttpRequests } from './http_requests';
import { setHttpClient } from '../../../public/services/api';
import { httpService } from '../../../public/app/services/http';
import { breadcrumbService } from '../../../public/app/services/breadcrumbs';
import { documentationService } from '../../../public/app/services/documentation';
import { notificationService } from '../../../public/app/services/notification';
import { uiMetricService } from '../../../public/app/services/ui_metric';
import { createUiStatsReporter } from '../../../../../../../src/legacy/core_plugins/ui_metric/public';
/* eslint-disable @kbn/eslint/no-restricted-paths */
import { notificationServiceMock } from '../../../../../../../src/core/public/notifications/notifications_service.mock';
import { chromeServiceMock } from '../../../../../../../src/core/public/chrome/chrome_service.mock';
import { docLinksServiceMock } from '../../../../../../../src/core/public/doc_links/doc_links_service.mock';
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
export const setupEnvironment = () => {
const { server, httpRequestsMockHelpers } = initHttpRequests();
// Mock initialization of services
// @ts-ignore
setHttpClient(mockHttpClient);
httpService.init(mockHttpClient);
breadcrumbService.init(chromeServiceMock.createStartContract(), '');
documentationService.init(docLinksServiceMock.createStartContract());
notificationService.init(notificationServiceMock.createStartContract());
uiMetricService.init(createUiStatsReporter);
const { server, httpRequestsMockHelpers } = initHttpRequests();
return {
server,

View file

@ -6,7 +6,7 @@
import { registerTestBed, TestBedConfig } from '../../../../../../test_utils';
import { BASE_PATH } from '../../../common/constants';
import { TemplateClone } from '../../../public/sections/template_clone';
import { TemplateClone } from '../../../public/app/sections/template_clone';
import { formSetup } from './template_form.helpers';
import { TEMPLATE_NAME } from './constants';

View file

@ -6,7 +6,7 @@
import { registerTestBed, TestBedConfig } from '../../../../../../test_utils';
import { BASE_PATH } from '../../../common/constants';
import { TemplateCreate } from '../../../public/sections/template_create';
import { TemplateCreate } from '../../../public/app/sections/template_create';
import { formSetup, TestSubjects } from './template_form.helpers';
const testBedConfig: TestBedConfig = {

View file

@ -6,7 +6,7 @@
import { registerTestBed, TestBedConfig } from '../../../../../../test_utils';
import { BASE_PATH } from '../../../common/constants';
import { TemplateEdit } from '../../../public/sections/template_edit';
import { TemplateEdit } from '../../../public/app/sections/template_edit';
import { formSetup, TestSubjects } from './template_form.helpers';
import { TEMPLATE_NAME } from './constants';

View file

@ -8,8 +8,7 @@ import { act } from 'react-dom/test-utils';
import * as fixtures from '../../test/fixtures';
import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers';
import { IdxMgmtHomeTestBed } from './helpers/home.helpers';
const API_PATH = '/api/index_management';
import { API_BASE_PATH } from '../../common/constants';
const { setup } = pageHelpers.home;
@ -21,16 +20,7 @@ const removeWhiteSpaceOnArrayValues = (array: any[]) =>
return value.trim();
});
jest.mock('ui/index_patterns', () => ({
ILLEGAL_CHARACTERS: '',
CONTAINS_SPACES: '',
validateIndexPattern: () => {},
}));
jest.mock('ui/chrome', () => ({
breadcrumbs: { set: () => {} },
addBasePath: (path: string) => path || '/api/index_management',
}));
jest.mock('ui/new_platform');
// We need to skip the tests until react 16.9.0 is released
// which supports asynchronous code inside act()
@ -204,7 +194,9 @@ describe.skip('<IndexManagementHome />', () => {
});
expect(server.requests.length).toBe(totalRequests + 1);
expect(server.requests[server.requests.length - 1].url).toBe(`${API_PATH}/templates`);
expect(server.requests[server.requests.length - 1].url).toBe(
`${API_BASE_PATH}/templates`
);
});
test('should have a button to create a new template', () => {
@ -346,7 +338,7 @@ describe.skip('<IndexManagementHome />', () => {
const latestRequest = server.requests[server.requests.length - 1];
expect(latestRequest.method).toBe('DELETE');
expect(latestRequest.url).toBe(`${API_PATH}/templates/${template1.name}`);
expect(latestRequest.url).toBe(`${API_BASE_PATH}/templates/${template1.name}`);
});
});

View file

@ -5,8 +5,6 @@
*/
import React from 'react';
import { act } from 'react-dom/test-utils';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import axios from 'axios';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { TemplateFormTestBed } from './helpers/template_form.helpers';
@ -15,27 +13,7 @@ import { TEMPLATE_NAME, INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS } from './helpe
const { setup } = pageHelpers.templateClone;
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
jest.mock('ui/index_patterns', () => ({
ILLEGAL_CHARACTERS: 'ILLEGAL_CHARACTERS',
CONTAINS_SPACES: 'CONTAINS_SPACES',
validateIndexPattern: () => {
return {
errors: {},
};
},
}));
jest.mock('ui/chrome', () => ({
breadcrumbs: { set: () => {} },
addBasePath: (path: string) => path || '/api/index_management',
}));
jest.mock('../../public/services/api', () => ({
...jest.requireActual('../../public/services/api'),
getHttpClient: () => mockHttpClient,
}));
jest.mock('ui/new_platform');
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
@ -135,16 +113,13 @@ describe.skip('<TemplateClone />', () => {
const latestRequest = server.requests[server.requests.length - 1];
const body = JSON.parse(latestRequest.requestBody);
const expected = {
const expected = JSON.stringify({
...templateToClone,
name: `${templateToClone.name}-copy`,
indexPatterns: DEFAULT_INDEX_PATTERNS,
aliases: {},
mappings: {},
settings: {},
};
expect(body).toEqual(expected);
});
expect(JSON.parse(latestRequest.requestBody).body).toEqual(expected);
});
});
});

View file

@ -5,8 +5,6 @@
*/
import React from 'react';
import { act } from 'react-dom/test-utils';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import axios from 'axios';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { TemplateFormTestBed } from './helpers/template_form.helpers';
@ -20,27 +18,7 @@ import {
const { setup } = pageHelpers.templateCreate;
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
jest.mock('ui/index_patterns', () => ({
ILLEGAL_CHARACTERS: 'ILLEGAL_CHARACTERS',
CONTAINS_SPACES: 'CONTAINS_SPACES',
validateIndexPattern: () => {
return {
errors: {},
};
},
}));
jest.mock('ui/chrome', () => ({
breadcrumbs: { set: () => {} },
addBasePath: (path: string) => path || '/api/index_management',
}));
jest.mock('../../public/services/api', () => ({
...jest.requireActual('../../public/services/api'),
getHttpClient: () => mockHttpClient,
}));
jest.mock('ui/new_platform');
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
@ -332,15 +310,16 @@ describe.skip('<TemplateCreate />', () => {
const latestRequest = server.requests[server.requests.length - 1];
const expected = {
const expected = JSON.stringify({
isManaged: false,
name: TEMPLATE_NAME,
indexPatterns: DEFAULT_INDEX_PATTERNS,
settings: SETTINGS,
mappings: MAPPINGS,
aliases: ALIASES,
isManaged: false,
};
expect(JSON.parse(latestRequest.requestBody)).toEqual(expected);
});
expect(JSON.parse(latestRequest.requestBody).body).toEqual(expected);
});
it('should surface the API errors from the put HTTP request', async () => {

View file

@ -5,8 +5,6 @@
*/
import React from 'react';
import { act } from 'react-dom/test-utils';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import axios from 'axios';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { TemplateFormTestBed } from './helpers/template_form.helpers';
@ -17,27 +15,7 @@ const UPDATED_INDEX_PATTERN = ['updatedIndexPattern'];
const { setup } = pageHelpers.templateEdit;
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
jest.mock('ui/index_patterns', () => ({
ILLEGAL_CHARACTERS: 'ILLEGAL_CHARACTERS',
CONTAINS_SPACES: 'CONTAINS_SPACES',
validateIndexPattern: () => {
return {
errors: {},
};
},
}));
jest.mock('ui/chrome', () => ({
breadcrumbs: { set: () => {} },
addBasePath: (path: string) => path || '/api/index_management',
}));
jest.mock('../../public/services/api', () => ({
...jest.requireActual('../../public/services/api'),
getHttpClient: () => mockHttpClient,
}));
jest.mock('ui/new_platform');
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
@ -140,17 +118,18 @@ describe.skip('<TemplateEdit />', () => {
const { version, order } = templateToEdit;
const expected = {
const expected = JSON.stringify({
name: TEMPLATE_NAME,
version,
order,
indexPatterns: UPDATED_INDEX_PATTERN,
isManaged: false,
settings: SETTINGS,
mappings: MAPPINGS,
aliases: ALIASES,
isManaged: false,
};
expect(JSON.parse(latestRequest.requestBody)).toEqual(expected);
});
expect(JSON.parse(latestRequest.requestBody).body).toEqual(expected);
});
});
});

View file

@ -5,33 +5,31 @@
*/
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { AppWithoutRouter } from '../../public/app';
import { Provider } from 'react-redux';
import { loadIndicesSuccess } from '../../public/store/actions';
import { indexManagementStore } from '../../public/store';
import { BASE_PATH } from '../../common/constants';
import { mountWithIntl } from '../../../../../test_utils/enzyme_helpers';
// axios has a $http like interface so using it to simulate $http
import axios from 'axios';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import { setHttpClient } from '../../public/services/api';
import { MemoryRouter } from 'react-router-dom';
import { AppWithoutRouter } from '../../public/app/app';
import { Provider } from 'react-redux';
import { loadIndicesSuccess } from '../../public/app/store/actions';
import { breadcrumbService } from '../../public/app/services/breadcrumbs';
import { uiMetricService } from '../../public/app/services/ui_metric';
import { notificationService } from '../../public/app/services/notification';
import { httpService } from '../../public/app/services/http';
import { createUiStatsReporter } from '../../../../../../src/legacy/core_plugins/ui_metric/public';
import { indexManagementStore } from '../../public/app/store';
import { BASE_PATH, API_BASE_PATH } from '../../common/constants';
import { mountWithIntl } from '../../../../../test_utils/enzyme_helpers';
import sinon from 'sinon';
import { findTestSubject } from '@elastic/eui/lib/test';
/* eslint-disable @kbn/eslint/no-restricted-paths */
import { notificationServiceMock } from '../../../../../../src/core/public/notifications/notifications_service.mock';
import { chromeServiceMock } from '../../../../../../src/core/public/chrome/chrome_service.mock';
jest.mock('ui/chrome', () => ({
breadcrumbs: { set: () => { } },
addBasePath: path => path || '/api/index_management',
}));
jest.mock('ui/new_platform');
jest.mock('ui/index_patterns', () => ({
ILLEGAL_CHARACTERS: '',
CONTAINS_SPACES: '',
validateIndexPattern: () => { },
}));
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
setHttpClient(axios.create({ adapter: axiosXhrAdapter }));
let server = null;
let store = null;
@ -114,6 +112,14 @@ const namesText = rendered => {
describe('index table', () => {
beforeEach(() => {
// Mock initialization of services
// @ts-ignore
httpService.init(mockHttpClient);
breadcrumbService.init(chromeServiceMock.createStartContract(), '');
uiMetricService.init(createUiStatsReporter);
notificationService.init(notificationServiceMock.createStartContract());
store = indexManagementStore();
component = (
<Provider store={store}>
@ -124,7 +130,7 @@ describe('index table', () => {
);
store.dispatch(loadIndicesSuccess({ indices }));
server = sinon.fakeServer.create();
server.respondWith('/api/index_management/indices', [
server.respondWith(`${API_BASE_PATH}/indices`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(indices)
@ -134,7 +140,7 @@ describe('index table', () => {
{ 'Content-Type': 'application/json' },
JSON.stringify({ acknowledged: true })
]);
server.respondWith('/api/index_management/indices/reload', [
server.respondWith(`${API_BASE_PATH}/indices/reload`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(indices)
@ -347,7 +353,8 @@ describe('index table', () => {
status: index.name === 'testy0' ? 'close' : index.status
};
});
server.respondWith('/api/index_management/indices/reload', [
server.respondWith(`${API_BASE_PATH}/indices/reload`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(modifiedIndices)
@ -361,7 +368,7 @@ describe('index table', () => {
status: index.name === 'testy1' ? 'open' : index.status
};
});
server.respondWith('/api/index_management/indices/reload', [
server.respondWith(`${API_BASE_PATH}/indices/reload`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(modifiedIndices)

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { flattenObject } from '../../public/lib/flatten_object';
import { flattenObject } from '../../public/app/lib/flatten_object';
describe('flatten_object', () => {
test('it flattens an object', () => {
const obj = {

View file

@ -4,6 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import './register_management_section';
import './register_routes';
import './index_management_extensions';
export const API_BASE_PATH = '/api/index_management';

View file

@ -6,6 +6,7 @@
export { PLUGIN } from './plugin';
export { BASE_PATH } from './base_path';
export { API_BASE_PATH } from './api_base_path';
export { INVALID_INDEX_PATTERN_CHARS, INVALID_TEMPLATE_NAME_CHARS } from './invalid_characters';
export * from './index_statuses';

View file

@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { LICENSE_TYPE_BASIC } from '../../../../common/constants';
export const PLUGIN = {
ID: 'index_management',
NAME: i18n.translate('xpack.idxMgmt.appTitle', {
defaultMessage: 'Index Management',
}),
getI18nName: (i18n: any): string =>
i18n.translate('xpack.idxMgmt.appTitle', {
defaultMessage: 'Index Management',
}),
MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC,
};

View file

@ -1,41 +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 { resolve } from 'path';
import { createRouter } from '../../server/lib/create_router';
import { registerIndicesRoutes } from './server/routes/api/indices';
import { registerTemplateRoutes } from './server/routes/api/templates';
import { registerMappingRoute } from './server/routes/api/mapping';
import { registerSettingsRoutes } from './server/routes/api/settings';
import { registerStatsRoute } from './server/routes/api/stats';
import { registerLicenseChecker } from '../../server/lib/register_license_checker';
import { PLUGIN } from './common/constants';
import { addIndexManagementDataEnricher } from './index_management_data';
export function indexManagement(kibana) {
return new kibana.Plugin({
id: PLUGIN.ID,
configPrefix: 'xpack.index_management',
publicDir: resolve(__dirname, 'public'),
require: ['kibana', 'elasticsearch', 'xpack_main'],
uiExports: {
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
managementSections: [
'plugins/index_management',
]
},
init: function (server) {
const router = createRouter(server, PLUGIN.ID, '/api/index_management/');
server.expose('addIndexManagementDataEnricher', addIndexManagementDataEnricher);
registerLicenseChecker(server, PLUGIN.ID, PLUGIN.NAME, PLUGIN.MINIMUM_LICENSE_REQUIRED);
registerIndicesRoutes(router);
registerTemplateRoutes(router, server);
registerSettingsRoutes(router);
registerStatsRoute(router);
registerMappingRoute(router);
}
});
}

View file

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { resolve } from 'path';
import { i18n } from '@kbn/i18n';
import { Legacy } from 'kibana';
import { createRouter } from '../../server/lib/create_router';
import { registerLicenseChecker } from '../../server/lib/register_license_checker';
import { PLUGIN, API_BASE_PATH } from './common/constants';
import { LegacySetup } from './server/plugin';
import { plugin as initServerPlugin } from './server';
export type ServerFacade = Legacy.Server;
export function indexManagement(kibana: any) {
return new kibana.Plugin({
id: PLUGIN.ID,
configPrefix: 'xpack.index_management',
publicDir: resolve(__dirname, 'public'),
require: ['kibana', 'elasticsearch', 'xpack_main'],
uiExports: {
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
managementSections: ['plugins/index_management'],
},
init(server: ServerFacade) {
const coreSetup = server.newPlatform.setup.core;
const pluginsSetup = {};
const __LEGACY: LegacySetup = {
router: createRouter(server, PLUGIN.ID, `${API_BASE_PATH}/`),
plugins: {
license: {
registerLicenseChecker: registerLicenseChecker.bind(
null,
server,
PLUGIN.ID,
PLUGIN.getI18nName(i18n),
PLUGIN.MINIMUM_LICENSE_REQUIRED as 'basic'
),
},
elasticsearch: server.plugins.elasticsearch,
},
};
const serverPlugin = initServerPlugin();
const indexMgmtSetup = serverPlugin.setup(coreSetup, pluginsSetup, __LEGACY);
server.expose(
'addIndexManagementDataEnricher',
indexMgmtSetup.addIndexManagementDataEnricher
);
},
});
}

View file

@ -1,23 +0,0 @@
.indTable {
// The index table is a bespoke table and can't make use of EuiBasicTable's width settings
thead th.indTable__header--name {
width: 25%;
}
// The index name can't contain spaces, so this is a rare case of break being OK.
.indTable__cell--name {
word-break: break-all;
}
}
.indTable__link {
text-align: left;
}
.indTable__link {
text-align: left;
}
.indDetail__codeBlock {
background: transparent;
}

View file

@ -6,15 +6,15 @@
import React, { useEffect } from 'react';
import { HashRouter, Switch, Route, Redirect } from 'react-router-dom';
import { BASE_PATH, UIM_APP_LOAD } from '../common/constants';
import { BASE_PATH, UIM_APP_LOAD } from '../../common/constants';
import { IndexManagementHome } from './sections/home';
import { TemplateCreate } from './sections/template_create';
import { TemplateClone } from './sections/template_clone';
import { TemplateEdit } from './sections/template_edit';
import { trackUiMetric } from './services';
import { uiMetricService } from './services/ui_metric';
export const App = () => {
useEffect(() => trackUiMetric('loaded', UIM_APP_LOAD), []);
useEffect(() => uiMetricService.trackMetric('loaded', UIM_APP_LOAD), []);
return (
<HashRouter>
@ -28,12 +28,8 @@ export const AppWithoutRouter = () => (
<Switch>
<Route exact path={`${BASE_PATH}create_template`} component={TemplateCreate} />
<Route exact path={`${BASE_PATH}clone_template/:name*`} component={TemplateClone} />
<Route
exact
path={`${BASE_PATH}edit_template/:name*`}
component={TemplateEdit}
/>
<Route exact path={`${BASE_PATH}edit_template/:name*`} component={TemplateEdit} />
<Route path={`${BASE_PATH}:section(indices|templates)`} component={IndexManagementHome} />
<Redirect from={`${BASE_PATH}`} to={`${BASE_PATH}indices`}/>
<Redirect from={`${BASE_PATH}`} to={`${BASE_PATH}indices`} />
</Switch>
);

View file

@ -8,11 +8,9 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import React, { Fragment } from 'react';
export interface Error {
data: {
error: string;
cause?: string[];
message?: string;
};
cause?: string[];
message?: string;
statusText?: string;
}
interface Props {
@ -22,14 +20,14 @@ interface Props {
export const SectionError: React.FunctionComponent<Props> = ({ title, error, ...rest }) => {
const {
error: errorString,
cause, // wrapEsError() on the server adds a "cause" array
message,
} = error.data;
statusText,
} = error;
return (
<EuiCallOut title={title} color="danger" iconType="alert" {...rest}>
<div>{message || errorString}</div>
<div>{message || statusText}</div>
{cause && (
<Fragment>
<EuiSpacer size="m" />

View file

@ -8,9 +8,9 @@ import { EuiConfirmModal, EuiOverlayMask, EuiCallOut, EuiCheckbox, EuiBadge } fr
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Fragment, useState } from 'react';
import { toastNotifications } from 'ui/notify';
import { deleteTemplates } from '../services/api';
import { Template } from '../../common/types';
import { notificationService } from '../services/notification';
import { Template } from '../../../common/types';
export const TemplateDeleteModal = ({
templatesToDelete,
@ -51,7 +51,7 @@ export const TemplateDeleteModal = ({
);
callback({ hasDeletedTemplates });
toastNotifications.addSuccess(successMessage);
notificationService.showSuccessToast(successMessage);
}
if (error || (errors && errors.length)) {
@ -71,7 +71,7 @@ export const TemplateDeleteModal = ({
defaultMessage: "Error deleting template '{name}'",
values: { name: (errors && errors[0].name) || templatesToDelete[0] },
});
toastNotifications.addDanger(errorMessage);
notificationService.showDangerToast(errorMessage);
}
});
};

View file

@ -18,7 +18,7 @@ import {
EuiCode,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { templatesDocumentationLink } from '../../../lib/documentation_links';
import { documentationService } from '../../../services/documentation';
import { StepProps } from '../types';
import { useJsonStep } from './use_json_step';
@ -63,7 +63,7 @@ export const StepAliases: React.FunctionComponent<StepProps> = ({
<EuiButtonEmpty
size="s"
flush="right"
href={templatesDocumentationLink}
href={documentationService.getTemplatesDocumentationLink()}
target="_blank"
iconType="help"
>

View file

@ -11,12 +11,12 @@ import {
useForm,
Form,
getUseField,
} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
import {
getFormRow,
Field,
} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/components';
import { templatesDocumentationLink } from '../../../lib/documentation_links';
} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/components';
import { documentationService } from '../../../services/documentation';
import { StepProps } from '../types';
import { schemas } from '../template_form_schemas';
@ -110,7 +110,7 @@ export const StepLogistics: React.FunctionComponent<StepProps> = ({
<EuiButtonEmpty
size="s"
flush="right"
href={templatesDocumentationLink}
href={documentationService.getTemplatesDocumentationLink()}
target="_blank"
iconType="help"
>

View file

@ -18,7 +18,7 @@ import {
EuiCodeEditor,
EuiCode,
} from '@elastic/eui';
import { mappingDocumentationLink } from '../../../lib/documentation_links';
import { documentationService } from '../../../services/documentation';
import { StepProps } from '../types';
import { useJsonStep } from './use_json_step';
@ -63,7 +63,7 @@ export const StepMappings: React.FunctionComponent<StepProps> = ({
<EuiButtonEmpty
size="s"
flush="right"
href={mappingDocumentationLink}
href={documentationService.getMappingDocumentationLink()}
target="_blank"
iconType="help"
>

View file

@ -20,10 +20,10 @@ import {
EuiCodeBlock,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { serializers } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers';
import { serializers } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers';
import { serializeTemplate } from '../../../../common/lib/template_serialization';
import { Template } from '../../../../common/types';
import { serializeTemplate } from '../../../../../common/lib/template_serialization';
import { Template } from '../../../../../common/types';
import { StepProps } from '../types';
const { stripEmptyFields } = serializers;

View file

@ -18,7 +18,7 @@ import {
EuiCode,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { settingsDocumentationLink } from '../../../lib/documentation_links';
import { documentationService } from '../../../services/documentation';
import { StepProps } from '../types';
import { useJsonStep } from './use_json_step';
@ -63,7 +63,7 @@ export const StepSettings: React.FunctionComponent<StepProps> = ({
<EuiButtonEmpty
size="s"
flush="right"
href={settingsDocumentationLink}
href={documentationService.getSettingsDocumentationLink()}
target="_blank"
iconType="help"
>

View file

@ -7,7 +7,7 @@
import { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { isJSON } from '../../../../../../../../src/plugins/es_ui_shared/static/validators/string';
import { isJSON } from '../../../../../../../../../src/plugins/es_ui_shared/static/validators/string';
import { StepProps } from '../types';
interface Parameters {

View file

@ -14,8 +14,8 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { serializers } from '../../../../../../../src/plugins/es_ui_shared/static/forms/helpers';
import { Template } from '../../../common/types';
import { serializers } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers';
import { Template } from '../../../../common/types';
import { TemplateSteps } from './template_steps';
import { StepAliases, StepLogistics, StepMappings, StepSettings, StepReview } from './steps';
import { StepProps, DataGetterFunc } from './types';

View file

@ -12,17 +12,17 @@ import {
FormSchema,
FIELD_TYPES,
VALIDATION_TYPES,
} from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
import {
fieldFormatters,
fieldValidators,
} from '../../../../../../../src/plugins/es_ui_shared/static/forms/helpers';
} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers';
import {
INVALID_INDEX_PATTERN_CHARS,
INVALID_TEMPLATE_NAME_CHARS,
} from '../../../common/constants';
} from '../../../../common/constants';
const { emptyField, containsCharsField, startsWithField, indexPatternField } = fieldValidators;
const { toInt } = fieldFormatters;

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Template } from '../../../common/types';
import { Template } from '../../../../common/types';
export interface StepProps {
template: Partial<Template>;

View file

@ -13,3 +13,5 @@ export {
TAB_STATS,
TAB_EDIT_SETTINGS,
} from './detail_panel_tabs';
export const REACT_ROOT_ID = 'indexManagementReactRoot';

View file

@ -0,0 +1,36 @@
/*
* 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 { Provider } from 'react-redux';
import { render, unmountComponentAtNode } from 'react-dom';
import { CoreStart } from '../../../../../../src/core/public';
import { App } from './app';
import { indexManagementStore } from './store';
export const mountReactApp = (elem: HTMLElement | null, { core }: { core: CoreStart }): void => {
if (elem) {
const { i18n } = core;
const { Context: I18nContext } = i18n;
render(
<I18nContext>
<Provider store={indexManagementStore()}>
<App />
</Provider>
</I18nContext>,
elem
);
}
};
export const unmountReactApp = (elem: HTMLElement | null) => {
if (elem) {
unmountComponentAtNode(elem);
}
};

View file

@ -14,7 +14,7 @@ import {
INDEX_REFRESHING,
INDEX_FLUSHING,
INDEX_FORCEMERGING,
} from '../../common/constants';
} from '../../../common/constants';
export const indexStatusLabels = {
[INDEX_CLEARING_CACHE]: i18n.translate('xpack.idxMgmt.indexStatusLabels.clearingCacheStatusLabel', {

View file

@ -6,7 +6,7 @@
import { unmountComponentAtNode } from 'react-dom';
export const manageAngularLifecycle = ($scope, $route, elem) => {
export const manageAngularLifecycle = ($scope: any, $route: any, elem: HTMLElement | null) => {
const lastRoute = $route.current;
const deregister = $scope.$on('$locationChangeSuccess', () => {
@ -17,7 +17,12 @@ export const manageAngularLifecycle = ($scope, $route, elem) => {
});
$scope.$on('$destroy', () => {
deregister && deregister();
elem && unmountComponentAtNode(elem);
if (deregister) {
deregister();
}
if (elem) {
unmountComponentAtNode(elem);
}
});
};

View file

@ -8,7 +8,7 @@
import React, { Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBadge, EuiSearchBar } from '@elastic/eui';
import { getBadgeExtensions } from '../index_management_extensions';
import { getBadgeExtensions } from '../../index_management_extensions';
export const renderBadges = (index, filterChanged) => {
const badgeLabels = [];
getBadgeExtensions().forEach(({ matchIndex, label, color, filterExpression }) => {

View file

@ -18,11 +18,11 @@ import {
EuiTabs,
EuiTitle,
} from '@elastic/eui';
import { BASE_PATH } from '../../../common/constants';
import { idxMgmtDocumentationLink } from '../../lib/documentation_links';
import { BASE_PATH } from '../../../../common/constants';
import { documentationService } from '../../services/documentation';
import { IndexList } from './index_list';
import { TemplateList } from './template_list';
import { setBreadcrumbs } from '../../services/set_breadcrumbs';
import { breadcrumbService } from '../../services/breadcrumbs';
type Section = 'indices' | 'templates';
@ -57,7 +57,7 @@ export const IndexManagementHome: React.FunctionComponent<RouteComponentProps<Ma
};
useEffect(() => {
setBreadcrumbs();
breadcrumbService.setBreadcrumbs('home');
}, []);
return (
@ -75,7 +75,7 @@ export const IndexManagementHome: React.FunctionComponent<RouteComponentProps<Ma
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
href={idxMgmtDocumentationLink}
href={documentationService.getIdxMgmtDocumentationLink()}
target="_blank"
iconType="help"
data-test-subj="documentationLink"

View file

@ -21,7 +21,7 @@ import {
EuiTitle,
} from '@elastic/eui';
import { renderBadges } from '../../../../lib/render_badges';
import { INDEX_OPEN } from '../../../../../common/constants';
import { INDEX_OPEN } from '../../../../../../common/constants';
import {
TAB_SUMMARY,
TAB_SETTINGS,

View file

@ -6,7 +6,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { settingsDocumentationLink } from '../../../../../lib/documentation_links';
import { documentationService } from '../../../../../services/documentation';
import {
EuiButton,
@ -22,7 +22,7 @@ import { TAB_SETTINGS } from '../../../../../constants';
import {
settingsToDisplay,
readOnlySettings
} from '../../../../../lib/editSettings';
} from '../../../../../lib/edit_settings';
import { createAceEditor } from '../../../../../lib/ace';
import _ from 'lodash';
@ -144,7 +144,7 @@ export class EditSettingsJson extends React.PureComponent {
</EuiFlexGroup>
<EuiSpacer />
<EuiLink
href={settingsDocumentationLink}
href={documentationService.getSettingsDocumentationLink()}
target="_blank"
rel="noopener noreferrer"
>

View file

@ -7,7 +7,6 @@
import React, { Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { healthToColor } from '../../../../../services';
import { getUrlService } from '../../../../../services/navigation';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
@ -20,7 +19,7 @@ import {
EuiSpacer,
EuiTitle
} from '@elastic/eui';
import { getSummaryExtensions } from '../../../../../index_management_extensions';
import { getSummaryExtensions } from '../../../../../../index_management_extensions';
const getHeaders = () =>{
return {
health: i18n.translate('xpack.idxMgmt.summary.headers.healthHeader', {
@ -61,7 +60,7 @@ export class Summary extends React.PureComponent {
return (
<Fragment key={`summaryExtension-${i}`}>
<EuiHorizontalRule />
{ summaryExtension(index, getUrlService()) }
{ summaryExtension(index) }
</Fragment>
);
});

View file

@ -23,8 +23,8 @@ import {
EuiCheckbox
} from '@elastic/eui';
import { flattenPanelTree } from '../../../../lib/flatten_panel_tree';
import { INDEX_OPEN } from '../../../../../common/constants';
import { getActionExtensions } from '../../../../index_management_extensions';
import { INDEX_OPEN } from '../../../../../../common/constants';
import { getActionExtensions } from '../../../../../index_management_extensions';
import { getHttpClient } from '../../../../services/api';
export class IndexActionsContextMenu extends Component {
constructor(props) {

View file

@ -35,14 +35,15 @@ import {
EuiTitle,
} from '@elastic/eui';
import { UIM_SHOW_DETAILS_CLICK } from '../../../../../common/constants';
import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants';
import { REFRESH_RATE_INDEX_LIST } from '../../../../constants';
import { healthToColor, trackUiMetric } from '../../../../services';
import { healthToColor } from '../../../../services';
import { uiMetricService } from '../../../../services/ui_metric';
import {
getBannerExtensions,
getFilterExtensions,
getToggleExtensions,
} from '../../../../index_management_extensions';
} from '../../../../../index_management_extensions';
import { renderBadges } from '../../../../lib/render_badges';
import { NoMatch, PageErrorForbidden } from '../../../../components';
import { IndexActionsContextMenu } from '../index_actions_context_menu';
@ -218,10 +219,9 @@ export class IndexTable extends Component {
return (
<Fragment>
<EuiLink
className="indTable__link"
data-test-subj="indexTableIndexNameLink"
onClick={() => {
trackUiMetric('click', UIM_SHOW_DETAILS_CLICK);
uiMetricService.trackMetric('click', UIM_SHOW_DETAILS_CLICK);
openDetailPanel(value);
}}
>
@ -271,7 +271,8 @@ export class IndexTable extends Component {
renderError() {
const { indicesError } = this.props;
const data = indicesError.data ? indicesError.data : indicesError;
const data = indicesError.body ? indicesError.body : indicesError;
const {
error: errorString,

View file

@ -7,7 +7,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
import { Template } from '../../../../../../common/types';
import { Template } from '../../../../../../../common/types';
interface Props {
templateDetails: Template;

View file

@ -7,7 +7,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
import { Template } from '../../../../../../common/types';
import { Template } from '../../../../../../../common/types';
interface Props {
templateDetails: Template;

View file

@ -7,7 +7,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
import { Template } from '../../../../../../common/types';
import { Template } from '../../../../../../../common/types';
interface Props {
templateDetails: Template;

View file

@ -14,7 +14,7 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import { Template } from '../../../../../../common/types';
import { Template } from '../../../../../../../common/types';
import { getILMPolicyPath } from '../../../../../services/navigation';
interface Props {

View file

@ -29,13 +29,13 @@ import {
UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB,
UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
} from '../../../../../common/constants';
import { Template } from '../../../../../common/types';
} from '../../../../../../common/constants';
import { Template } from '../../../../../../common/types';
import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components';
import { loadIndexTemplate } from '../../../../services/api';
import { decodePath } from '../../../../services/routing';
import { trackUiMetric, METRIC_TYPE } from '../../../../services/track_ui_metric';
import { SendRequestResponse } from '../../../../shared_imports';
import { uiMetricService } from '../../../../services/ui_metric';
import { SendRequestResponse } from '../../../../../shared_imports';
import { TabSummary, TabMappings, TabSettings, TabAliases } from './tabs';
interface Props {
@ -164,7 +164,7 @@ export const TemplateDetails: React.FunctionComponent<Props> = ({
{TABS.map(tab => (
<EuiTab
onClick={() => {
trackUiMetric(METRIC_TYPE.CLICK, tabToUiMetricMap[tab.id]);
uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]);
setActiveTab(tab.id);
}}
isSelected={tab.id === activeTab}

View file

@ -19,14 +19,14 @@ import {
import { SectionError, SectionLoading, Error } from '../../../components';
import { TemplateTable } from './template_table';
import { loadIndexTemplates } from '../../../services/api';
import { Template } from '../../../../common/types';
import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric';
import { Template } from '../../../../../common/types';
import { uiMetricService } from '../../../services/ui_metric';
import {
getTemplateEditLink,
getTemplateListLink,
getTemplateCloneLink,
} from '../../../services/routing';
import { UIM_TEMPLATE_LIST_LOAD } from '../../../../common/constants';
import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants';
import { TemplateDetails } from './template_details';
interface MatchParams {
@ -66,7 +66,7 @@ export const TemplateList: React.FunctionComponent<RouteComponentProps<MatchPara
// Track component loaded
useEffect(() => {
trackUiMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD);
uiMetricService.trackMetric('loaded', UIM_TEMPLATE_LIST_LOAD);
}, []);
if (isLoading) {

View file

@ -8,12 +8,12 @@ import React, { useState, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiInMemoryTable, EuiIcon, EuiButton, EuiLink } from '@elastic/eui';
import { TemplateListItem, Template } from '../../../../../common/types';
import { BASE_PATH, UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../common/constants';
import { TemplateListItem, Template } from '../../../../../../common/types';
import { BASE_PATH, UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../common/constants';
import { TemplateDeleteModal } from '../../../../components';
import { trackUiMetric, METRIC_TYPE } from '../../../../services/track_ui_metric';
import { uiMetricService } from '../../../../services/ui_metric';
import { getTemplateDetailsLink } from '../../../../services/routing';
import { SendRequestResponse } from '../../../../shared_imports';
import { SendRequestResponse } from '../../../../../shared_imports';
interface Props {
templates: TemplateListItem[];
@ -45,7 +45,7 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
<EuiLink
href={getTemplateDetailsLink(name, true)}
data-test-subj="templateDetailsLink"
onClick={() => trackUiMetric(METRIC_TYPE.CLICK, UIM_TEMPLATE_SHOW_DETAILS_CLICK)}
onClick={() => uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK)}
>
{name}
</EuiLink>

View file

@ -8,9 +8,9 @@ import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
import { TemplateForm, SectionLoading, SectionError, Error } from '../../components';
import { setBreadcrumbs } from '../../services/set_breadcrumbs';
import { breadcrumbService } from '../../services/breadcrumbs';
import { decodePath, getTemplateDetailsLink } from '../../services/routing';
import { Template } from '../../../common/types';
import { Template } from '../../../../common/types';
import { saveTemplate, loadIndexTemplate } from '../../services/api';
interface MatchParams {
@ -56,7 +56,7 @@ export const TemplateClone: React.FunctionComponent<RouteComponentProps<MatchPar
let content;
useEffect(() => {
setBreadcrumbs('templateClone');
breadcrumbService.setBreadcrumbs('templateClone');
}, []);
if (isLoading) {

View file

@ -8,8 +8,8 @@ import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
import { TemplateForm } from '../../components';
import { setBreadcrumbs } from '../../services/set_breadcrumbs';
import { Template } from '../../../common/types';
import { breadcrumbService } from '../../services/breadcrumbs';
import { Template } from '../../../../common/types';
import { saveTemplate } from '../../services/api';
import { getTemplateDetailsLink } from '../../services/routing';
@ -40,7 +40,7 @@ export const TemplateCreate: React.FunctionComponent<RouteComponentProps> = ({ h
};
useEffect(() => {
setBreadcrumbs('templateCreate');
breadcrumbService.setBreadcrumbs('templateCreate');
}, []);
return (

View file

@ -7,11 +7,11 @@ import React, { useEffect, useState, Fragment } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui';
import { setBreadcrumbs } from '../../services/set_breadcrumbs';
import { breadcrumbService } from '../../services/breadcrumbs';
import { loadIndexTemplate, updateTemplate } from '../../services/api';
import { decodePath, getTemplateDetailsLink } from '../../services/routing';
import { SectionLoading, SectionError, TemplateForm, Error } from '../../components';
import { Template } from '../../../common/types';
import { Template } from '../../../../common/types';
interface MatchParams {
name: string;
@ -30,7 +30,7 @@ export const TemplateEdit: React.FunctionComponent<RouteComponentProps<MatchPara
const { error, data: template, isLoading } = loadIndexTemplate(decodedTemplateName);
useEffect(() => {
setBreadcrumbs('templateEdit');
breadcrumbService.setBreadcrumbs('templateEdit');
}, []);
const onSave = async (updatedTemplate: Template) => {

View file

@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import chrome from 'ui/chrome';
import {
API_BASE_PATH,
UIM_UPDATE_SETTINGS,
UIM_INDEX_CLEAR_CACHE,
UIM_INDEX_CLEAR_CACHE_MANY,
@ -31,13 +30,14 @@ import {
UIM_TEMPLATE_CREATE,
UIM_TEMPLATE_UPDATE,
UIM_TEMPLATE_CLONE,
} from '../../common/constants';
} from '../../../common/constants';
import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants';
import { trackUiMetric, METRIC_TYPE } from './track_ui_metric';
import { uiMetricService } from './ui_metric';
import { useRequest, sendRequest } from './use_request';
import { Template } from '../../common/types';
import { httpService } from './http';
import { Template } from '../../../common/types';
let httpClient: ng.IHttpService;
@ -49,139 +49,143 @@ export const getHttpClient = () => {
return httpClient;
};
const apiPrefix = chrome.addBasePath('/api/index_management');
export async function loadIndices() {
const response = await httpClient.get(`${apiPrefix}/indices`);
return response.data;
const response = await httpService.httpClient.get(`${API_BASE_PATH}/indices`);
return response.data ? response.data : response;
}
export async function reloadIndices(indexNames: string[]) {
const body = {
const body = JSON.stringify({
indexNames,
};
const response = await httpClient.post(`${apiPrefix}/indices/reload`, body);
return response.data;
});
const response = await httpService.httpClient.post(`${API_BASE_PATH}/indices/reload`, { body });
return response.data ? response.data : response;
}
export async function closeIndices(indices: string[]) {
const body = {
const body = JSON.stringify({
indices,
};
const response = await httpClient.post(`${apiPrefix}/indices/close`, body);
});
const response = await httpService.httpClient.post(`${API_BASE_PATH}/indices/close`, { body });
// Only track successful requests.
const eventName = indices.length > 1 ? UIM_INDEX_CLOSE_MANY : UIM_INDEX_CLOSE;
trackUiMetric(METRIC_TYPE.COUNT, eventName);
return response.data;
uiMetricService.trackMetric('count', eventName);
return response;
}
export async function deleteIndices(indices: string[]) {
const body = {
const body = JSON.stringify({
indices,
};
const response = await httpClient.post(`${apiPrefix}/indices/delete`, body);
});
const response = await httpService.httpClient.post(`${API_BASE_PATH}/indices/delete`, { body });
// Only track successful requests.
const eventName = indices.length > 1 ? UIM_INDEX_DELETE_MANY : UIM_INDEX_DELETE;
trackUiMetric(METRIC_TYPE.COUNT, eventName);
return response.data;
uiMetricService.trackMetric('count', eventName);
return response;
}
export async function openIndices(indices: string[]) {
const body = {
const body = JSON.stringify({
indices,
};
const response = await httpClient.post(`${apiPrefix}/indices/open`, body);
});
const response = await httpService.httpClient.post(`${API_BASE_PATH}/indices/open`, { body });
// Only track successful requests.
const eventName = indices.length > 1 ? UIM_INDEX_OPEN_MANY : UIM_INDEX_OPEN;
trackUiMetric(METRIC_TYPE.COUNT, eventName);
return response.data;
uiMetricService.trackMetric('count', eventName);
return response;
}
export async function refreshIndices(indices: string[]) {
const body = {
const body = JSON.stringify({
indices,
};
const response = await httpClient.post(`${apiPrefix}/indices/refresh`, body);
});
const response = await httpService.httpClient.post(`${API_BASE_PATH}/indices/refresh`, { body });
// Only track successful requests.
const eventName = indices.length > 1 ? UIM_INDEX_REFRESH_MANY : UIM_INDEX_REFRESH;
trackUiMetric(METRIC_TYPE.COUNT, eventName);
return response.data;
uiMetricService.trackMetric('count', eventName);
return response;
}
export async function flushIndices(indices: string[]) {
const body = {
const body = JSON.stringify({
indices,
};
const response = await httpClient.post(`${apiPrefix}/indices/flush`, body);
});
const response = await httpService.httpClient.post(`${API_BASE_PATH}/indices/flush`, { body });
// Only track successful requests.
const eventName = indices.length > 1 ? UIM_INDEX_FLUSH_MANY : UIM_INDEX_FLUSH;
trackUiMetric(METRIC_TYPE.COUNT, eventName);
return response.data;
uiMetricService.trackMetric('count', eventName);
return response;
}
export async function forcemergeIndices(indices: string[], maxNumSegments: string) {
const body = {
const body = JSON.stringify({
indices,
maxNumSegments,
};
const response = await httpClient.post(`${apiPrefix}/indices/forcemerge`, body);
});
const response = await httpService.httpClient.post(`${API_BASE_PATH}/indices/forcemerge`, {
body,
});
// Only track successful requests.
const eventName = indices.length > 1 ? UIM_INDEX_FORCE_MERGE_MANY : UIM_INDEX_FORCE_MERGE;
trackUiMetric(METRIC_TYPE.COUNT, eventName);
return response.data;
uiMetricService.trackMetric('count', eventName);
return response;
}
export async function clearCacheIndices(indices: string[]) {
const body = {
const body = JSON.stringify({
indices,
};
const response = await httpClient.post(`${apiPrefix}/indices/clear_cache`, body);
});
const response = await httpService.httpClient.post(`${API_BASE_PATH}/indices/clear_cache`, {
body,
});
// Only track successful requests.
const eventName = indices.length > 1 ? UIM_INDEX_CLEAR_CACHE_MANY : UIM_INDEX_CLEAR_CACHE;
trackUiMetric(METRIC_TYPE.COUNT, eventName);
return response.data;
uiMetricService.trackMetric('count', eventName);
return response;
}
export async function freezeIndices(indices: string[]) {
const body = {
const body = JSON.stringify({
indices,
};
const response = await httpClient.post(`${apiPrefix}/indices/freeze`, body);
});
const response = await httpService.httpClient.post(`${API_BASE_PATH}/indices/freeze`, { body });
// Only track successful requests.
const eventName = indices.length > 1 ? UIM_INDEX_FREEZE_MANY : UIM_INDEX_FREEZE;
trackUiMetric(METRIC_TYPE.COUNT, eventName);
return response.data;
uiMetricService.trackMetric('count', eventName);
return response;
}
export async function unfreezeIndices(indices: string[]) {
const body = {
const body = JSON.stringify({
indices,
};
const response = await httpClient.post(`${apiPrefix}/indices/unfreeze`, body);
});
const response = await httpService.httpClient.post(`${API_BASE_PATH}/indices/unfreeze`, { body });
// Only track successful requests.
const eventName = indices.length > 1 ? UIM_INDEX_UNFREEZE_MANY : UIM_INDEX_UNFREEZE;
trackUiMetric(METRIC_TYPE.COUNT, eventName);
return response.data;
uiMetricService.trackMetric('count', eventName);
return response;
}
export async function loadIndexSettings(indexName: string) {
const response = await httpClient.get(`${apiPrefix}/settings/${indexName}`);
return response.data;
const response = await httpService.httpClient.get(`${API_BASE_PATH}/settings/${indexName}`);
return response;
}
export async function updateIndexSettings(indexName: string, settings: object) {
const response = await httpClient.put(`${apiPrefix}/settings/${indexName}`, settings);
export async function updateIndexSettings(indexName: string, body: object) {
const response = await httpService.httpClient.put(`${API_BASE_PATH}/settings/${indexName}`, {
body: JSON.stringify(body),
});
// Only track successful requests.
trackUiMetric(METRIC_TYPE.COUNT, UIM_UPDATE_SETTINGS);
uiMetricService.trackMetric('count', UIM_UPDATE_SETTINGS);
return response;
}
export async function loadIndexStats(indexName: string) {
const response = await httpClient.get(`${apiPrefix}/stats/${indexName}`);
return response.data;
const response = await httpService.httpClient.get(`${API_BASE_PATH}/stats/${indexName}`);
return response;
}
export async function loadIndexMapping(indexName: string) {
const response = await httpClient.get(`${apiPrefix}/mapping/${indexName}`);
return response.data;
const response = await httpService.httpClient.get(`${API_BASE_PATH}/mapping/${indexName}`);
return response;
}
export async function loadIndexData(type: string, indexName: string) {
@ -199,61 +203,54 @@ export async function loadIndexData(type: string, indexName: string) {
export function loadIndexTemplates() {
return useRequest({
path: `${apiPrefix}/templates`,
path: `${API_BASE_PATH}/templates`,
method: 'get',
});
}
export async function deleteTemplates(names: Array<Template['name']>) {
const result = sendRequest({
path: `${apiPrefix}/templates/${names.map(name => encodeURIComponent(name)).join(',')}`,
path: `${API_BASE_PATH}/templates/${names.map(name => encodeURIComponent(name)).join(',')}`,
method: 'delete',
});
const uimActionType = names.length > 1 ? UIM_TEMPLATE_DELETE_MANY : UIM_TEMPLATE_DELETE;
trackUiMetric(METRIC_TYPE.COUNT, uimActionType);
uiMetricService.trackMetric('count', uimActionType);
return result;
}
export function loadIndexTemplate(name: Template['name']) {
return useRequest({
path: `${apiPrefix}/templates/${encodeURIComponent(name)}`,
path: `${API_BASE_PATH}/templates/${encodeURIComponent(name)}`,
method: 'get',
});
}
export async function saveTemplate(template: Template, isClone?: boolean) {
const result = sendRequest({
path: `${apiPrefix}/templates`,
const result = await sendRequest({
path: `${API_BASE_PATH}/templates`,
method: 'put',
body: template,
body: JSON.stringify(template),
});
const uimActionType = isClone ? UIM_TEMPLATE_CLONE : UIM_TEMPLATE_CREATE;
trackUiMetric(METRIC_TYPE.COUNT, uimActionType);
uiMetricService.trackMetric('count', uimActionType);
return result;
}
export async function updateTemplate(template: Template) {
const { name } = template;
const result = sendRequest({
path: `${apiPrefix}/templates/${encodeURIComponent(name)}`,
const result = await sendRequest({
path: `${API_BASE_PATH}/templates/${encodeURIComponent(name)}`,
method: 'put',
body: template,
body: JSON.stringify(template),
});
trackUiMetric(METRIC_TYPE.COUNT, UIM_TEMPLATE_UPDATE);
uiMetricService.trackMetric('count', UIM_TEMPLATE_UPDATE);
return result;
}
export async function loadTemplateToClone(name: Template['name']) {
return sendRequest({
path: `${apiPrefix}/templates/${encodeURIComponent(name)}`,
method: 'get',
});
}

View file

@ -0,0 +1,97 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { BASE_PATH } from '../../../common/constants';
import { ChromeStart } from '../../../../../../../src/core/public';
class BreadcrumbService {
private chrome: ChromeStart | undefined;
private breadcrumbs: {
[key: string]: Array<{
text: string;
href?: string;
}>;
} = {
management: [],
home: [],
};
public init(chrome: ChromeStart, managementBreadcrumb: any): void {
this.chrome = chrome;
this.breadcrumbs.management = [managementBreadcrumb];
this.breadcrumbs.home = [
...this.breadcrumbs.management,
{
text: i18n.translate('xpack.idxMgmt.breadcrumb.homeLabel', {
defaultMessage: 'Index Management',
}),
href: `#${BASE_PATH}`,
},
];
this.breadcrumbs.templates = [
...this.breadcrumbs.home,
{
text: i18n.translate('xpack.idxMgmt.breadcrumb.templatesLabel', {
defaultMessage: 'Templates',
}),
href: `#${BASE_PATH}templates`,
},
];
this.breadcrumbs.templateCreate = [
...this.breadcrumbs.templates,
{
text: i18n.translate('xpack.idxMgmt.breadcrumb.createTemplateLabel', {
defaultMessage: 'Create template',
}),
},
];
this.breadcrumbs.templateEdit = [
...this.breadcrumbs.templates,
{
text: i18n.translate('xpack.idxMgmt.breadcrumb.editTemplateLabel', {
defaultMessage: 'Edit template',
}),
},
];
this.breadcrumbs.templateClone = [
...this.breadcrumbs.templates,
{
text: i18n.translate('xpack.idxMgmt.breadcrumb.cloneTemplateLabel', {
defaultMessage: 'Clone template',
}),
},
];
}
public setBreadcrumbs(type: string): void {
const newBreadcrumbs = this.breadcrumbs[type]
? [...this.breadcrumbs[type]]
: [...this.breadcrumbs.home];
// Pop off last breadcrumb
const lastBreadcrumb = newBreadcrumbs.pop() as {
text: string;
href?: string;
};
// Put last breadcrumb back without href
newBreadcrumbs.push({
...lastBreadcrumb,
href: undefined,
});
if (this.chrome) {
this.chrome.setBreadcrumbs(newBreadcrumbs);
}
}
}
export const breadcrumbService = new BreadcrumbService();

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { DocLinksStart } from '../../../../../../../src/core/public';
class DocumentationService {
private esDocsBase: string = '';
private kibanaDocsBase: string = '';
public init(docLinks: DocLinksStart): void {
const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks;
const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`;
this.esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`;
this.kibanaDocsBase = `${docsBase}/kibana/${DOC_LINK_VERSION}`;
}
public getSettingsDocumentationLink() {
return `${this.esDocsBase}/index-modules.html#index-modules-settings`;
}
public getMappingDocumentationLink() {
return `${this.esDocsBase}/mapping.html`;
}
public getTemplatesDocumentationLink() {
return `${this.esDocsBase}/indices-templates.html`;
}
public getIdxMgmtDocumentationLink() {
return `${this.kibanaDocsBase}/managing-indices.html`;
}
}
export const documentationService = new DocumentationService();

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const healthToColor = health => {
export const healthToColor = (health: 'green' | 'yellow' | 'red') => {
switch (health) {
case 'green':
return 'success';

View 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 { HttpServiceBase } from '../../../../../../../src/core/public';
class HttpService {
private client: any;
public init(httpClient: HttpServiceBase): void {
this.client = httpClient;
}
public get httpClient(): HttpServiceBase {
return this.client;
}
}
export const httpService = new HttpService();

Some files were not shown because too many files have changed in this diff Show more