mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
parent
1616ba5ffc
commit
4dcc99fe34
78 changed files with 1897 additions and 118 deletions
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
registerTestBed,
|
||||
TestBed,
|
||||
TestBedConfig,
|
||||
findTestSubject,
|
||||
} from '../../../../../../test_utils';
|
||||
import { IndexManagementHome } from '../../../public/sections/home';
|
||||
import { BASE_PATH } from '../../../common/constants';
|
||||
import { indexManagementStore } from '../../../public/store';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
store: indexManagementStore,
|
||||
memoryRouter: {
|
||||
initialEntries: [`${BASE_PATH}indices`],
|
||||
componentRoutePath: `${BASE_PATH}:section(indices|templates)`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(IndexManagementHome, testBedConfig);
|
||||
|
||||
export interface IdxMgmtHomeTestBed extends TestBed<IdxMgmtTestSubjects> {
|
||||
actions: {
|
||||
selectTab: (tab: 'indices' | 'index templates') => void;
|
||||
clickReloadButton: () => void;
|
||||
clickTemplateActionAt: (index: number, action: 'delete') => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (): Promise<IdxMgmtHomeTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
/**
|
||||
* User Actions
|
||||
*/
|
||||
|
||||
const selectTab = (tab: 'indices' | 'index templates') => {
|
||||
const tabs = ['indices', 'index templates'];
|
||||
|
||||
testBed
|
||||
.find('tab')
|
||||
.at(tabs.indexOf(tab))
|
||||
.simulate('click');
|
||||
};
|
||||
|
||||
const clickReloadButton = () => {
|
||||
const { find } = testBed;
|
||||
find('reloadButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickTemplateActionAt = async (index: number, action: 'delete') => {
|
||||
const { component, table } = testBed;
|
||||
const { rows } = table.getMetaData('templatesTable');
|
||||
const currentRow = rows[index];
|
||||
const lastColumn = currentRow.columns[currentRow.columns.length - 1].reactWrapper;
|
||||
const button = findTestSubject(lastColumn, `${action}TemplateButton`);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
button.simulate('click');
|
||||
component.update();
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
selectTab,
|
||||
clickReloadButton,
|
||||
clickTemplateActionAt,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type IdxMgmtTestSubjects = TestSubjects;
|
||||
|
||||
export type TestSubjects =
|
||||
| 'appTitle'
|
||||
| 'cell'
|
||||
| 'deleteSystemTemplateCallOut'
|
||||
| 'deleteTemplateButton'
|
||||
| 'deleteTemplatesButton'
|
||||
| 'deleteTemplatesConfirmation'
|
||||
| 'documentationLink'
|
||||
| 'emptyPrompt'
|
||||
| 'indicesList'
|
||||
| 'reloadButton'
|
||||
| 'row'
|
||||
| 'sectionLoading'
|
||||
| 'systemTemplatesSwitch'
|
||||
| 'tab'
|
||||
| 'templatesList'
|
||||
| 'templatesTable';
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import sinon, { SinonFakeServer } from 'sinon';
|
||||
|
||||
const API_PATH = '/api/index_management';
|
||||
|
||||
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`, [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify(response),
|
||||
]);
|
||||
};
|
||||
|
||||
const setLoadIndicesResponse = (response: HttpResponse = []) => {
|
||||
server.respondWith('GET', `${API_PATH}/indices`, [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify(response),
|
||||
]);
|
||||
};
|
||||
|
||||
const setDeleteTemplateResponse = (response: HttpResponse = []) => {
|
||||
server.respondWith('DELETE', `${API_PATH}/templates`, [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify(response),
|
||||
]);
|
||||
};
|
||||
|
||||
return {
|
||||
setLoadTemplatesResponse,
|
||||
setLoadIndicesResponse,
|
||||
setDeleteTemplateResponse,
|
||||
};
|
||||
};
|
||||
|
||||
export const init = () => {
|
||||
const server = sinon.fakeServer.create();
|
||||
server.respondImmediately = true;
|
||||
|
||||
// Define default response for unhandled requests.
|
||||
// We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry,
|
||||
// and we can mock them all with a 200 instead of mocking each one individually.
|
||||
server.respondWith([200, {}, 'DefaultResponse']);
|
||||
|
||||
const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server);
|
||||
|
||||
return {
|
||||
server,
|
||||
httpRequestsMockHelpers,
|
||||
};
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import { setup as homeSetup } from './home.helpers';
|
||||
|
||||
export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../../test_utils';
|
||||
|
||||
export { setupEnvironment } from './setup_environment';
|
||||
|
||||
export const pageHelpers = {
|
||||
home: { setup: homeSetup },
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
|
||||
import { init as initHttpRequests } from './http_requests';
|
||||
import { setHttpClient } from '../../../public/services/api';
|
||||
|
||||
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
|
||||
|
||||
export const setupEnvironment = () => {
|
||||
const { server, httpRequestsMockHelpers } = initHttpRequests();
|
||||
|
||||
// @ts-ignore
|
||||
setHttpClient(mockHttpClient);
|
||||
|
||||
return {
|
||||
server,
|
||||
httpRequestsMockHelpers,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,304 @@
|
|||
/*
|
||||
* 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 { act } from 'react-dom/test-utils';
|
||||
import * as fixtures from '../../test/fixtures';
|
||||
import {
|
||||
setupEnvironment,
|
||||
pageHelpers,
|
||||
nextTick,
|
||||
getRandomString,
|
||||
findTestSubject,
|
||||
} from './helpers';
|
||||
import { IdxMgmtHomeTestBed } from './helpers/home.helpers';
|
||||
|
||||
const API_PATH = '/api/index_management';
|
||||
|
||||
const { setup } = pageHelpers.home;
|
||||
|
||||
const removeWhiteSpaceOnArrayValues = (array: any[]) =>
|
||||
array.map(value => {
|
||||
if (!value.trim) {
|
||||
return value;
|
||||
}
|
||||
return value.trim();
|
||||
});
|
||||
|
||||
// We need to skip the tests until react 16.9.0 is released
|
||||
// which supports asynchronous code inside act()
|
||||
describe.skip('<IndexManagementHome />', () => {
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: IdxMgmtHomeTestBed;
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('on component mount', async () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadIndicesResponse([]);
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
const { component } = testBed;
|
||||
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should set the correct app title', () => {
|
||||
const { exists, find } = testBed;
|
||||
expect(exists('appTitle')).toBe(true);
|
||||
expect(find('appTitle').text()).toEqual('Index Management');
|
||||
});
|
||||
|
||||
test('should have a link to the documentation', () => {
|
||||
const { exists, find } = testBed;
|
||||
expect(exists('documentationLink')).toBe(true);
|
||||
expect(find('documentationLink').text()).toBe('Index Management docs');
|
||||
});
|
||||
|
||||
describe('tabs', () => {
|
||||
test('should have 2 tabs', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('tab').length).toBe(2);
|
||||
expect(find('tab').map(t => t.text())).toEqual(['Indices', 'Index Templates']);
|
||||
});
|
||||
|
||||
test('should navigate to Index Templates tab', async () => {
|
||||
const { exists, actions, component } = testBed;
|
||||
|
||||
expect(exists('indicesList')).toBe(true);
|
||||
expect(exists('templatesList')).toBe(false);
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplatesResponse([]);
|
||||
|
||||
actions.selectTab('index templates');
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(exists('indicesList')).toBe(false);
|
||||
expect(exists('templatesList')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('index templates', () => {
|
||||
describe('when there are no index templates', () => {
|
||||
beforeEach(async () => {
|
||||
const { actions, component } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplatesResponse([]);
|
||||
|
||||
actions.selectTab('index templates');
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should display an empty prompt', async () => {
|
||||
const { exists } = testBed;
|
||||
|
||||
expect(exists('sectionLoading')).toBe(false);
|
||||
expect(exists('emptyPrompt')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are index templates', () => {
|
||||
const template1 = fixtures.getTemplate({
|
||||
name: `a${getRandomString()}`,
|
||||
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
|
||||
settings: {
|
||||
index: {
|
||||
number_of_shards: '1',
|
||||
lifecycle: {
|
||||
name: 'my_ilm_policy',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const template2 = fixtures.getTemplate({
|
||||
name: `b${getRandomString()}`,
|
||||
indexPatterns: ['template2Pattern1*'],
|
||||
});
|
||||
const template3 = fixtures.getTemplate({
|
||||
name: `.c${getRandomString()}`, // mock system template
|
||||
indexPatterns: ['template3Pattern1*', 'template3Pattern2', 'template3Pattern3'],
|
||||
});
|
||||
|
||||
const templates = [template1, template2, template3];
|
||||
|
||||
beforeEach(async () => {
|
||||
const { actions, component } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplatesResponse(templates);
|
||||
|
||||
actions.selectTab('index templates');
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should list them in the table', async () => {
|
||||
const { table } = testBed;
|
||||
|
||||
const { tableCellsValues } = table.getMetaData('templatesTable');
|
||||
|
||||
tableCellsValues.forEach((row, i) => {
|
||||
const template = templates[i];
|
||||
const { name, indexPatterns, order, settings } = template;
|
||||
const ilmPolicyName =
|
||||
settings && settings.index && settings.index.lifecycle
|
||||
? settings.index.lifecycle.name
|
||||
: '';
|
||||
|
||||
expect(removeWhiteSpaceOnArrayValues(row)).toEqual([
|
||||
'',
|
||||
name,
|
||||
indexPatterns.join(', '),
|
||||
ilmPolicyName,
|
||||
order.toString(),
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should have a button to reload the index templates', async () => {
|
||||
const { component, exists, actions } = testBed;
|
||||
const totalRequests = server.requests.length;
|
||||
|
||||
expect(exists('reloadButton')).toBe(true);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickReloadButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(server.requests.length).toBe(totalRequests + 1);
|
||||
expect(server.requests[server.requests.length - 1].url).toBe(`${API_PATH}/templates`);
|
||||
});
|
||||
|
||||
test('should have a switch to view system templates', async () => {
|
||||
const { table, exists, component, form } = testBed;
|
||||
const { rows } = table.getMetaData('templatesTable');
|
||||
|
||||
expect(rows.length).toEqual(
|
||||
templates.filter(template => !template.name.startsWith('.')).length
|
||||
);
|
||||
|
||||
expect(exists('systemTemplatesSwitch')).toBe(true);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
form.toggleEuiSwitch('systemTemplatesSwitch');
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const { rows: updatedRows } = table.getMetaData('templatesTable');
|
||||
expect(updatedRows.length).toEqual(templates.length);
|
||||
});
|
||||
|
||||
describe('delete index template', () => {
|
||||
test('should have action buttons on each row to delete an index template', () => {
|
||||
const { table } = testBed;
|
||||
const { rows } = table.getMetaData('templatesTable');
|
||||
const lastColumn = rows[0].columns[rows[0].columns.length - 1].reactWrapper;
|
||||
|
||||
expect(findTestSubject(lastColumn, 'deleteTemplateButton').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should show a confirmation when clicking the delete template button', async () => {
|
||||
const { actions } = testBed;
|
||||
|
||||
await actions.clickTemplateActionAt(0, 'delete');
|
||||
|
||||
// We need to read the document "body" as the modal is added there and not inside
|
||||
// the component DOM tree.
|
||||
expect(
|
||||
document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]')
|
||||
).not.toBe(null);
|
||||
|
||||
expect(
|
||||
document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]')!
|
||||
.textContent
|
||||
).toContain('Delete template');
|
||||
});
|
||||
|
||||
test('should show a warning message when attempting to delete a system template', async () => {
|
||||
const { component, form, actions } = testBed;
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
form.toggleEuiSwitch('systemTemplatesSwitch');
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
await actions.clickTemplateActionAt(0, 'delete');
|
||||
|
||||
expect(
|
||||
document.body.querySelector('[data-test-subj="deleteSystemTemplateCallOut"]')
|
||||
).not.toBe(null);
|
||||
});
|
||||
|
||||
test('should send the correct HTTP request to delete an index template', async () => {
|
||||
const { component, actions, table } = testBed;
|
||||
const { rows } = table.getMetaData('templatesTable');
|
||||
|
||||
const watchId = rows[0].columns[2].value;
|
||||
|
||||
await actions.clickTemplateActionAt(0, 'delete');
|
||||
|
||||
const modal = document.body.querySelector(
|
||||
'[data-test-subj="deleteTemplatesConfirmation"]'
|
||||
);
|
||||
const confirmButton: HTMLButtonElement | null = modal!.querySelector(
|
||||
'[data-test-subj="confirmModalConfirmButton"]'
|
||||
);
|
||||
|
||||
httpRequestsMockHelpers.setDeleteTemplateResponse({
|
||||
results: {
|
||||
successes: [watchId],
|
||||
errors: [],
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
confirmButton!.click();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.method).toBe('DELETE');
|
||||
expect(latestRequest.url).toBe(`${API_PATH}/templates/${template1.name}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -37,4 +37,7 @@ export {
|
|||
UIM_DETAIL_PANEL_MAPPING_TAB,
|
||||
UIM_DETAIL_PANEL_STATS_TAB,
|
||||
UIM_DETAIL_PANEL_EDIT_SETTINGS_TAB,
|
||||
UIM_TEMPLATE_LIST_LOAD,
|
||||
UIM_TEMPLATE_DELETE,
|
||||
UIM_TEMPLATE_DELETE_MANY,
|
||||
} from './ui_metric';
|
|
@ -4,13 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
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'
|
||||
defaultMessage: 'Index Management',
|
||||
}),
|
||||
MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC,
|
||||
};
|
|
@ -33,3 +33,6 @@ export const UIM_DETAIL_PANEL_MAPPING_TAB = 'detail_panel_mapping_tab';
|
|||
export const UIM_DETAIL_PANEL_SETTINGS_TAB = 'detail_panel_settings_tab';
|
||||
export const UIM_DETAIL_PANEL_STATS_TAB = 'detail_panel_stats_tab';
|
||||
export const UIM_DETAIL_PANEL_SUMMARY_TAB = 'detail_panel_summary_tab';
|
||||
export const UIM_TEMPLATE_LIST_LOAD = 'template_list_load';
|
||||
export const UIM_TEMPLATE_DELETE = 'template_delete';
|
||||
export const UIM_TEMPLATE_DELETE_MANY = 'template_delete_many';
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { IndexList } from './index_list';
|
||||
export * from './templates';
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface Template {
|
||||
name: string;
|
||||
version: number;
|
||||
order: number;
|
||||
indexPatterns: string[];
|
||||
settings?: {
|
||||
index?: {
|
||||
[key: string]: any;
|
||||
lifecycle?: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
aliases?: object;
|
||||
mappings?: object;
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
import { resolve } from 'path';
|
||||
import { createRouter } from '../../server/lib/create_router';
|
||||
import { registerIndicesRoutes } from './server/routes/api/indices';
|
||||
import { registerTemplatesRoutes } 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';
|
||||
|
@ -31,6 +32,7 @@ export function indexManagement(kibana) {
|
|||
server.expose('addIndexManagementDataEnricher', addIndexManagementDataEnricher);
|
||||
registerLicenseChecker(server, PLUGIN.ID, PLUGIN.NAME, PLUGIN.MINIMUM_LICENSE_REQUIRED);
|
||||
registerIndicesRoutes(router);
|
||||
registerTemplatesRoutes(router);
|
||||
registerSettingsRoutes(router);
|
||||
registerStatsRoute(router);
|
||||
registerMappingRoute(router);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { HashRouter, Switch, Route, Redirect } from 'react-router-dom';
|
||||
import { BASE_PATH, UIM_APP_LOAD } from '../common/constants';
|
||||
import { IndexList } from './sections/index_list';
|
||||
import { IndexManagementHome } from './sections/home';
|
||||
import { trackUiMetric } from './services';
|
||||
|
||||
export const App = () => {
|
||||
|
@ -23,8 +23,7 @@ export const App = () => {
|
|||
// Exoprt this so we can test it with a different router.
|
||||
export const AppWithoutRouter = () => (
|
||||
<Switch>
|
||||
<Redirect exact from={`${BASE_PATH}`} to={`${BASE_PATH}indices`}/>
|
||||
<Route exact path={`${BASE_PATH}indices`} component={IndexList} />
|
||||
<Route path={`${BASE_PATH}indices/filter/:filter?`} component={IndexList}/>
|
||||
<Route path={`${BASE_PATH}:section(indices|templates)`} component={IndexManagementHome} />
|
||||
<Redirect from={`${BASE_PATH}`} to={`${BASE_PATH}indices`}/>
|
||||
</Switch>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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 { EuiConfirmModal, EuiOverlayMask, EuiCallOut, EuiCheckbox } from '@elastic/eui';
|
||||
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';
|
||||
|
||||
export const DeleteTemplatesModal = ({
|
||||
templatesToDelete,
|
||||
callback,
|
||||
}: {
|
||||
templatesToDelete: Array<Template['name']>;
|
||||
callback: (data?: { hasDeletedTemplates: boolean }) => void;
|
||||
}) => {
|
||||
const numTemplatesToDelete = templatesToDelete.length;
|
||||
|
||||
if (!numTemplatesToDelete) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasSystemTemplate = Boolean(
|
||||
templatesToDelete.find(templateName => templateName.startsWith('.'))
|
||||
);
|
||||
|
||||
const handleDeleteTemplates = () => {
|
||||
deleteTemplates(templatesToDelete).then(({ data: { templatesDeleted, errors }, error }) => {
|
||||
const hasDeletedTemplates = templatesDeleted && templatesDeleted.length;
|
||||
|
||||
if (hasDeletedTemplates) {
|
||||
const successMessage = i18n.translate(
|
||||
'xpack.idxMgmt.deleteTemplatesModal.successNotificationMessageText',
|
||||
{
|
||||
defaultMessage: 'Deleted {numSuccesses, plural, one {# template} other {# templates}}',
|
||||
values: { numSuccesses: templatesDeleted.length },
|
||||
}
|
||||
);
|
||||
|
||||
callback({ hasDeletedTemplates });
|
||||
toastNotifications.addSuccess(successMessage);
|
||||
}
|
||||
|
||||
if (error || (errors && errors.length)) {
|
||||
const hasMultipleErrors =
|
||||
(errors && errors.length > 1) || (error && templatesToDelete.length > 1);
|
||||
const errorMessage = hasMultipleErrors
|
||||
? i18n.translate(
|
||||
'xpack.idxMgmt.deleteTemplatesModal.multipleErrorsNotificationMessageText',
|
||||
{
|
||||
defaultMessage: 'Error deleting {count} templates',
|
||||
values: {
|
||||
count: (errors && errors.length) || templatesToDelete.length,
|
||||
},
|
||||
}
|
||||
)
|
||||
: i18n.translate('xpack.idxMgmt.deleteTemplatesModal.errorNotificationMessageText', {
|
||||
defaultMessage: "Error deleting template '{name}'",
|
||||
values: { name: (errors && errors[0].name) || templatesToDelete[0] },
|
||||
});
|
||||
toastNotifications.addDanger(errorMessage);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnCancel = () => {
|
||||
setIsDeleteConfirmed(false);
|
||||
callback();
|
||||
};
|
||||
|
||||
const [isDeleteConfirmed, setIsDeleteConfirmed] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
buttonColor="danger"
|
||||
data-test-subj="deleteTemplatesConfirmation"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.deleteTemplatesModal.modalTitleText"
|
||||
defaultMessage="Delete {numTemplatesToDelete, plural, one {template} other {# templates}}"
|
||||
values={{ numTemplatesToDelete }}
|
||||
/>
|
||||
}
|
||||
onCancel={handleOnCancel}
|
||||
onConfirm={handleDeleteTemplates}
|
||||
cancelButtonText={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.deleteTemplatesModal.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
}
|
||||
confirmButtonText={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.deleteTemplatesModal.confirmButtonLabel"
|
||||
defaultMessage="Delete {numTemplatesToDelete, plural, one {template} other {templates} }"
|
||||
values={{ numTemplatesToDelete }}
|
||||
/>
|
||||
}
|
||||
confirmButtonDisabled={hasSystemTemplate ? !isDeleteConfirmed : false}
|
||||
>
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.deleteTemplatesModal.deleteDescription"
|
||||
defaultMessage="You are about to delete {numTemplatesToDelete, plural, one {this template} other {these templates} }:"
|
||||
values={{ numTemplatesToDelete }}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
{templatesToDelete.map(template => (
|
||||
<li key={template}>
|
||||
{template}
|
||||
{template.startsWith('.') ? (
|
||||
<Fragment>
|
||||
{' '}
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.deleteTemplatesModal.systemTemplateLabel"
|
||||
defaultMessage="(System template)"
|
||||
/>
|
||||
</Fragment>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{hasSystemTemplate && (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.deleteTemplatesModal.proceedWithCautionCallOutTitle"
|
||||
defaultMessage="Deleting a system template can break Kibana"
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
data-test-subj="deleteSystemTemplateCallOut"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.deleteTemplatesModal.proceedWithCautionCallOutDescription"
|
||||
defaultMessage="System templates are critical for internal operations.
|
||||
Deleting a template cannot be undone."
|
||||
/>
|
||||
</p>
|
||||
<EuiCheckbox
|
||||
id="confirmDeleteTemplatesCheckbox"
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.deleteTemplatesModal.confirmDeleteCheckboxLabel"
|
||||
defaultMessage="I understand the consequences of deleting a system template"
|
||||
/>
|
||||
}
|
||||
checked={isDeleteConfirmed}
|
||||
onChange={e => setIsDeleteConfirmed(e.target.checked)}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
</Fragment>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { SectionError } from './section_error';
|
||||
export { SectionLoading } from './section_loading';
|
||||
export { NoMatch } from './no_match';
|
||||
export { PageErrorForbidden } from './page_error';
|
||||
export { DeleteTemplatesModal } from './delete_templates_modal';
|
|
@ -15,4 +15,3 @@ export const NoMatch = () => (
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
interface Props {
|
||||
title: React.ReactNode;
|
||||
error: {
|
||||
data: {
|
||||
error: string;
|
||||
cause?: string[];
|
||||
message?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const SectionError: React.FunctionComponent<Props> = ({ title, error, ...rest }) => {
|
||||
const {
|
||||
error: errorString,
|
||||
cause, // wrapEsError() on the server adds a "cause" array
|
||||
message,
|
||||
} = error.data;
|
||||
|
||||
return (
|
||||
<EuiCallOut title={title} color="danger" iconType="alert" {...rest}>
|
||||
<div>{message || errorString}</div>
|
||||
{cause && (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<ul>
|
||||
{cause.map((causeMsg, i) => (
|
||||
<li key={i}>{causeMsg}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SectionLoading: React.FunctionComponent<Props> = ({ children }) => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={<EuiLoadingSpinner size="xl" />}
|
||||
body={<EuiText color="subdued">{children}</EuiText>}
|
||||
data-test-subj="sectionLoading"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -6,5 +6,10 @@
|
|||
|
||||
import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
|
||||
|
||||
const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`;
|
||||
const base = `${ELASTIC_WEBSITE_URL}guide/en`;
|
||||
const esBase = `${base}/elasticsearch/reference/${DOC_LINK_VERSION}`;
|
||||
const kibanaBase = `${base}/kibana/${DOC_LINK_VERSION}`;
|
||||
|
||||
export const settingsDocumentationLink = `${esBase}/index-modules.html#index-modules-settings`;
|
||||
|
||||
export const idxMgmtDocumentationLink = `${kibanaBase}/managing-indices.html`;
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 { Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiSpacer,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { BASE_PATH } from '../../../common/constants';
|
||||
import { idxMgmtDocumentationLink } from '../../lib/documentation_links';
|
||||
import { IndexList } from './index_list';
|
||||
import { TemplatesList } from './templates_list';
|
||||
|
||||
type Section = 'indices' | 'templates';
|
||||
|
||||
interface MatchParams {
|
||||
section: Section;
|
||||
}
|
||||
|
||||
export const IndexManagementHome: React.FunctionComponent<RouteComponentProps<MatchParams>> = ({
|
||||
match: {
|
||||
params: { section },
|
||||
},
|
||||
history,
|
||||
}) => {
|
||||
const tabs = [
|
||||
{
|
||||
id: 'indices' as Section,
|
||||
name: <FormattedMessage id="xpack.idxMgmt.home.indicesTabTitle" defaultMessage="Indices" />,
|
||||
},
|
||||
{
|
||||
id: 'templates' as Section,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.home.indexTemplatesTabTitle"
|
||||
defaultMessage="Index Templates"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const onSectionChange = (newSection: Section) => {
|
||||
history.push(`${BASE_PATH}${newSection}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="l">
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={true}>
|
||||
<h1 data-test-subj="appTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.home.appTitle"
|
||||
defaultMessage="Index Management"
|
||||
/>
|
||||
</h1>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
href={idxMgmtDocumentationLink}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
data-test-subj="documentationLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.home.idxMgmtDocsLinkText"
|
||||
defaultMessage="Index Management docs"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiTabs>
|
||||
{tabs.map(tab => (
|
||||
<EuiTab
|
||||
onClick={() => onSectionChange(tab.id)}
|
||||
isSelected={tab.id === section}
|
||||
key={tab.id}
|
||||
data-test-subj="tab"
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<Switch>
|
||||
<Route exact path={`${BASE_PATH}indices`} component={IndexList} />
|
||||
<Route exact path={`${BASE_PATH}indices/filter/:filter?`} component={IndexList} />
|
||||
<Route exact path={`${BASE_PATH}templates`} component={TemplatesList} />
|
||||
</Switch>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { NoMatch } from './components/no_match';
|
||||
export { IndexManagementHome } from './home';
|
|
@ -29,7 +29,7 @@ import {
|
|||
TAB_STATS,
|
||||
TAB_EDIT_SETTINGS,
|
||||
} from '../../../../constants';
|
||||
import { IndexActionsContextMenu } from '../../components';
|
||||
import { IndexActionsContextMenu } from '../index_actions_context_menu';
|
||||
import { ShowJson } from './show_json';
|
||||
import { Summary } from './summary';
|
||||
import { EditSettingsJson } from './edit_settings_json';
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { PageErrorForbidden } from './components';
|
||||
export * from './index_list';
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { IndexList } from './components/index_list';
|
||||
export declare function IndexList(match: any): any;
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { DetailPanel } from './detail_panel';
|
||||
import { IndexTable } from './index_table';
|
||||
|
||||
export function IndexList({
|
||||
match: {
|
||||
params: {
|
||||
filter
|
||||
}
|
||||
}
|
||||
}) {
|
||||
return (
|
||||
<div className="im-snapshotTestSubject" data-test-subj="indicesList" >
|
||||
<IndexTable filterFromURI={filter}/>
|
||||
<DetailPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -30,9 +30,8 @@ import {
|
|||
EuiTableRow,
|
||||
EuiTableRowCell,
|
||||
EuiTableRowCellCheckbox,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiPageContent,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { UIM_SHOW_DETAILS_CLICK } from '../../../../../common/constants';
|
||||
|
@ -44,9 +43,8 @@ import {
|
|||
getToggleExtensions,
|
||||
} from '../../../../index_management_extensions';
|
||||
import { renderBadges } from '../../../../lib/render_badges';
|
||||
import { NoMatch } from '../../../no_match';
|
||||
import { PageErrorForbidden } from '../../../page_error';
|
||||
import { IndexActionsContextMenu } from '../../components';
|
||||
import { NoMatch, PageErrorForbidden } from '../../../../components';
|
||||
import { IndexActionsContextMenu } from '../index_actions_context_menu';
|
||||
|
||||
const HEADERS = {
|
||||
name: i18n.translate('xpack.idxMgmt.indexTable.headers.nameHeader', {
|
||||
|
@ -410,26 +408,17 @@ export class IndexTable extends Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiPageContent>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="m">
|
||||
<h1 data-test-subj="sectionHeading">
|
||||
<Fragment>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiTitle size="s">
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexTable.sectionHeading"
|
||||
defaultMessage="Index Management"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexTable.sectionDescription"
|
||||
id="xpack.idxMgmt.home.idxMgmtDescription"
|
||||
defaultMessage="Update your Elasticsearch indices individually or in bulk."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiText>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{((indicesLoading && allIndices.length === 0) || indicesError) ? null : (
|
||||
|
@ -454,7 +443,7 @@ export class IndexTable extends Component {
|
|||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EuiSpacer size="l" />
|
||||
{this.renderBanners()}
|
||||
{indicesError && this.renderError()}
|
||||
<EuiFlexGroup gutterSize="l" alignItems="center">
|
||||
|
@ -537,7 +526,7 @@ export class IndexTable extends Component {
|
|||
)}
|
||||
<EuiSpacer size="m" />
|
||||
{indices.length > 0 ? this.renderPager() : null}
|
||||
</EuiPageContent>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { TemplatesList } from './templates_list';
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState, useEffect, useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiSwitch,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import { SectionError, SectionLoading } from '../../../components';
|
||||
import { TemplatesTable } from './templates_table';
|
||||
import { loadIndexTemplates } from '../../../services/api';
|
||||
import { Template } from '../../../../common/types';
|
||||
import { trackUiMetric } from '../../../services/track_ui_metric';
|
||||
import { UIM_TEMPLATE_LIST_LOAD } from '../../../../common/constants';
|
||||
|
||||
export const TemplatesList: React.FunctionComponent = () => {
|
||||
const { error, isLoading, data: templates, createRequest: reload } = loadIndexTemplates();
|
||||
|
||||
let content;
|
||||
|
||||
const [showSystemTemplates, setShowSystemTemplates] = useState<boolean>(false);
|
||||
|
||||
// Filter out system index templates
|
||||
const filteredTemplates = useMemo(
|
||||
() =>
|
||||
templates ? templates.filter((template: Template) => !template.name.startsWith('.')) : [],
|
||||
[templates]
|
||||
);
|
||||
|
||||
// Track component loaded
|
||||
useEffect(() => {
|
||||
trackUiMetric(UIM_TEMPLATE_LIST_LOAD);
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
content = (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription"
|
||||
defaultMessage="Loading index templates…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
} else if (error) {
|
||||
content = (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage"
|
||||
defaultMessage="Error loading index templates"
|
||||
/>
|
||||
}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
} else if (Array.isArray(templates) && templates.length === 0) {
|
||||
content = (
|
||||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1 data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesTitle"
|
||||
defaultMessage="You don't have any index templates yet"
|
||||
/>
|
||||
</h1>
|
||||
}
|
||||
data-test-subj="emptyPrompt"
|
||||
/>
|
||||
);
|
||||
} else if (Array.isArray(templates) && templates.length > 0) {
|
||||
content = (
|
||||
<Fragment>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiTitle size="s">
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.home.indexTemplatesDescription"
|
||||
defaultMessage="Use index templates to automatically apply mappings and other properties to new indices."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSwitch
|
||||
id="checkboxShowSystemIndexTemplates"
|
||||
data-test-subj="systemTemplatesSwitch"
|
||||
checked={showSystemTemplates}
|
||||
onChange={event => setShowSystemTemplates(event.target.checked)}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexTemplatesTable.systemIndexTemplatesSwitchLabel"
|
||||
defaultMessage="Include system index templates"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
<TemplatesTable
|
||||
templates={showSystemTemplates ? templates : filteredTemplates}
|
||||
reload={reload}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return <section data-test-subj="templatesList">{content}</section>;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { TemplatesTable } from './templates_table';
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* 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, { useState, Fragment } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiInMemoryTable, EuiIcon, EuiButton, EuiToolTip, EuiButtonIcon } from '@elastic/eui';
|
||||
import { Template } from '../../../../../common/types';
|
||||
import { DeleteTemplatesModal } from '../../../../components';
|
||||
|
||||
interface Props {
|
||||
templates: Template[];
|
||||
reload: () => Promise<void>;
|
||||
}
|
||||
|
||||
const Checkmark = ({ tableCellData }: { tableCellData: object }) => {
|
||||
const isChecked = Object.entries(tableCellData).length > 0;
|
||||
|
||||
return isChecked ? <EuiIcon type="check" /> : null;
|
||||
};
|
||||
|
||||
export const TemplatesTable: React.FunctionComponent<Props> = ({ templates, reload }) => {
|
||||
const [selection, setSelection] = useState<Template[]>([]);
|
||||
const [templatesToDelete, setTemplatesToDelete] = useState<Array<Template['name']>>([]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate('xpack.idxMgmt.templatesList.table.nameColumnTitle', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'indexPatterns',
|
||||
name: i18n.translate('xpack.idxMgmt.templatesList.table.indexPatternsColumnTitle', {
|
||||
defaultMessage: 'Index patterns',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
render: (indexPatterns: string[]) => <strong>{indexPatterns.join(', ')}</strong>,
|
||||
},
|
||||
{
|
||||
field: 'settings',
|
||||
name: i18n.translate('xpack.idxMgmt.templatesList.table.ilmPolicyColumnTitle', {
|
||||
defaultMessage: 'ILM policy',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
render: (settings?: {
|
||||
index: {
|
||||
lifecycle: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}) => {
|
||||
if (settings && settings.index && settings.index.lifecycle) {
|
||||
return settings.index.lifecycle.name;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'order',
|
||||
name: i18n.translate('xpack.idxMgmt.templatesList.table.orderColumnTitle', {
|
||||
defaultMessage: 'Order',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'mappings',
|
||||
name: i18n.translate('xpack.idxMgmt.templatesList.table.mappingsColumnTitle', {
|
||||
defaultMessage: 'Mappings',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (mappings: object) => <Checkmark tableCellData={mappings} />,
|
||||
},
|
||||
{
|
||||
field: 'settings',
|
||||
name: i18n.translate('xpack.idxMgmt.templatesList.table.settingsColumnTitle', {
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (settings: object) => <Checkmark tableCellData={settings} />,
|
||||
},
|
||||
{
|
||||
field: 'aliases',
|
||||
name: i18n.translate('xpack.idxMgmt.templatesList.table.aliasesColumnTitle', {
|
||||
defaultMessage: 'Aliases',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (aliases: object) => {
|
||||
return <Checkmark tableCellData={aliases} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.idxMgmt.templatesList.table.actionColumnTitle', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
width: '75px',
|
||||
actions: [
|
||||
{
|
||||
render: (template: Template) => {
|
||||
const { name } = template;
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.idxMgmt.templatesList.table.actionDeleteTooltipLabel',
|
||||
{
|
||||
defaultMessage: 'Delete',
|
||||
}
|
||||
)}
|
||||
delay="long"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.idxMgmt.templatesList.table.actionDeleteAriaLabel',
|
||||
{
|
||||
defaultMessage: "Delete template '{name}'",
|
||||
values: { name },
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={() => {
|
||||
setTemplatesToDelete([name]);
|
||||
}}
|
||||
data-test-subj="deleteTemplateButton"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const pagination = {
|
||||
initialPageSize: 20,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
};
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: 'name',
|
||||
direction: 'asc',
|
||||
},
|
||||
};
|
||||
|
||||
const selectionConfig = {
|
||||
onSelectionChange: setSelection,
|
||||
};
|
||||
|
||||
const searchConfig = {
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
type: 'is',
|
||||
field: 'settings.index.lifecycle.name',
|
||||
name: i18n.translate('xpack.idxMgmt.templatesList.table.ilmPolicyFilterLabel', {
|
||||
defaultMessage: 'ILM policy',
|
||||
}),
|
||||
},
|
||||
],
|
||||
toolsLeft: selection.length && (
|
||||
<EuiButton
|
||||
data-test-subj="deleteTemplatesButton"
|
||||
onClick={() => setTemplatesToDelete(selection.map((selected: Template) => selected.name))}
|
||||
color="danger"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templatesList.table.deleteTemplatesButtonLabel"
|
||||
defaultMessage="Delete {count, plural, one {template} other {templates} }"
|
||||
values={{ count: selection.length }}
|
||||
/>
|
||||
</EuiButton>
|
||||
),
|
||||
toolsRight: (
|
||||
<EuiButton
|
||||
color="secondary"
|
||||
iconType="refresh"
|
||||
onClick={reload}
|
||||
data-test-subj="reloadButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templatesList.table.reloadTemplatesButtonLabel"
|
||||
defaultMessage="Reload"
|
||||
/>
|
||||
</EuiButton>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<DeleteTemplatesModal
|
||||
callback={data => {
|
||||
if (data && data.hasDeletedTemplates) {
|
||||
reload();
|
||||
}
|
||||
setTemplatesToDelete([]);
|
||||
}}
|
||||
templatesToDelete={templatesToDelete}
|
||||
/>
|
||||
<EuiInMemoryTable
|
||||
items={templates}
|
||||
itemId="name"
|
||||
columns={columns}
|
||||
search={searchConfig}
|
||||
sorting={sorting}
|
||||
isSelectable={true}
|
||||
selection={selectionConfig}
|
||||
pagination={pagination}
|
||||
rowProps={() => ({
|
||||
'data-test-subj': 'row',
|
||||
})}
|
||||
cellProps={() => ({
|
||||
'data-test-subj': 'cell',
|
||||
})}
|
||||
data-test-subj="templatesTable"
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templatesList.table.noIndexTemplatesMessage"
|
||||
defaultMessage="No templates found"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -1,10 +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 { IndexActionsContextMenu } from './index_actions_context_menu';
|
||||
export { IndexList } from './index_list';
|
||||
export { DetailPanel } from './detail_panel';
|
||||
export { IndexTable } from './index_table';
|
|
@ -1,30 +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 from 'react';
|
||||
|
||||
import {
|
||||
DetailPanel,
|
||||
IndexTable,
|
||||
} from '../../components';
|
||||
|
||||
export class IndexList extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
match: {
|
||||
params: {
|
||||
filter
|
||||
}
|
||||
},
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="im-snapshotTestSubject">
|
||||
<IndexTable filterFromURI={filter}/>
|
||||
<DetailPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -26,19 +26,19 @@ import {
|
|||
UIM_INDEX_REFRESH_MANY,
|
||||
UIM_INDEX_UNFREEZE,
|
||||
UIM_INDEX_UNFREEZE_MANY,
|
||||
UIM_TEMPLATE_DELETE,
|
||||
UIM_TEMPLATE_DELETE_MANY,
|
||||
} from '../../common/constants';
|
||||
|
||||
import {
|
||||
TAB_SETTINGS,
|
||||
TAB_MAPPING,
|
||||
TAB_STATS,
|
||||
} from '../constants';
|
||||
import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants';
|
||||
|
||||
import { trackUiMetric } from './track_ui_metric';
|
||||
import { useRequest, sendRequest } from './use_request';
|
||||
import { Template } from '../../common/types';
|
||||
|
||||
let httpClient;
|
||||
let httpClient: ng.IHttpService;
|
||||
|
||||
export const setHttpClient = (client) => {
|
||||
export const setHttpClient = (client: ng.IHttpService) => {
|
||||
httpClient = client;
|
||||
};
|
||||
|
||||
|
@ -53,17 +53,17 @@ export async function loadIndices() {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
export async function reloadIndices(indexNames) {
|
||||
export async function reloadIndices(indexNames: string[]) {
|
||||
const body = {
|
||||
indexNames
|
||||
indexNames,
|
||||
};
|
||||
const response = await httpClient.post(`${apiPrefix}/indices/reload`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function closeIndices(indices) {
|
||||
export async function closeIndices(indices: string[]) {
|
||||
const body = {
|
||||
indices
|
||||
indices,
|
||||
};
|
||||
const response = await httpClient.post(`${apiPrefix}/indices/close`, body);
|
||||
// Only track successful requests.
|
||||
|
@ -72,9 +72,9 @@ export async function closeIndices(indices) {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteIndices(indices) {
|
||||
export async function deleteIndices(indices: string[]) {
|
||||
const body = {
|
||||
indices
|
||||
indices,
|
||||
};
|
||||
const response = await httpClient.post(`${apiPrefix}/indices/delete`, body);
|
||||
// Only track successful requests.
|
||||
|
@ -83,9 +83,9 @@ export async function deleteIndices(indices) {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
export async function openIndices(indices) {
|
||||
export async function openIndices(indices: string[]) {
|
||||
const body = {
|
||||
indices
|
||||
indices,
|
||||
};
|
||||
const response = await httpClient.post(`${apiPrefix}/indices/open`, body);
|
||||
// Only track successful requests.
|
||||
|
@ -94,9 +94,9 @@ export async function openIndices(indices) {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
export async function refreshIndices(indices) {
|
||||
export async function refreshIndices(indices: string[]) {
|
||||
const body = {
|
||||
indices
|
||||
indices,
|
||||
};
|
||||
const response = await httpClient.post(`${apiPrefix}/indices/refresh`, body);
|
||||
// Only track successful requests.
|
||||
|
@ -105,9 +105,9 @@ export async function refreshIndices(indices) {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
export async function flushIndices(indices) {
|
||||
export async function flushIndices(indices: string[]) {
|
||||
const body = {
|
||||
indices
|
||||
indices,
|
||||
};
|
||||
const response = await httpClient.post(`${apiPrefix}/indices/flush`, body);
|
||||
// Only track successful requests.
|
||||
|
@ -116,10 +116,10 @@ export async function flushIndices(indices) {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
export async function forcemergeIndices(indices, maxNumSegments) {
|
||||
export async function forcemergeIndices(indices: string[], maxNumSegments: string) {
|
||||
const body = {
|
||||
indices,
|
||||
maxNumSegments
|
||||
maxNumSegments,
|
||||
};
|
||||
const response = await httpClient.post(`${apiPrefix}/indices/forcemerge`, body);
|
||||
// Only track successful requests.
|
||||
|
@ -128,9 +128,9 @@ export async function forcemergeIndices(indices, maxNumSegments) {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
export async function clearCacheIndices(indices) {
|
||||
export async function clearCacheIndices(indices: string[]) {
|
||||
const body = {
|
||||
indices
|
||||
indices,
|
||||
};
|
||||
const response = await httpClient.post(`${apiPrefix}/indices/clear_cache`, body);
|
||||
// Only track successful requests.
|
||||
|
@ -138,9 +138,9 @@ export async function clearCacheIndices(indices) {
|
|||
trackUiMetric(actionType);
|
||||
return response.data;
|
||||
}
|
||||
export async function freezeIndices(indices) {
|
||||
export async function freezeIndices(indices: string[]) {
|
||||
const body = {
|
||||
indices
|
||||
indices,
|
||||
};
|
||||
const response = await httpClient.post(`${apiPrefix}/indices/freeze`, body);
|
||||
// Only track successful requests.
|
||||
|
@ -148,9 +148,9 @@ export async function freezeIndices(indices) {
|
|||
trackUiMetric(actionType);
|
||||
return response.data;
|
||||
}
|
||||
export async function unfreezeIndices(indices) {
|
||||
export async function unfreezeIndices(indices: string[]) {
|
||||
const body = {
|
||||
indices
|
||||
indices,
|
||||
};
|
||||
const response = await httpClient.post(`${apiPrefix}/indices/unfreeze`, body);
|
||||
// Only track successful requests.
|
||||
|
@ -159,29 +159,29 @@ export async function unfreezeIndices(indices) {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
export async function loadIndexSettings(indexName) {
|
||||
export async function loadIndexSettings(indexName: string) {
|
||||
const response = await httpClient.get(`${apiPrefix}/settings/${indexName}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateIndexSettings(indexName, settings) {
|
||||
export async function updateIndexSettings(indexName: string, settings: object) {
|
||||
const response = await httpClient.put(`${apiPrefix}/settings/${indexName}`, settings);
|
||||
// Only track successful requests.
|
||||
trackUiMetric(UIM_UPDATE_SETTINGS);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function loadIndexStats(indexName) {
|
||||
export async function loadIndexStats(indexName: string) {
|
||||
const response = await httpClient.get(`${apiPrefix}/stats/${indexName}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function loadIndexMapping(indexName) {
|
||||
export async function loadIndexMapping(indexName: string) {
|
||||
const response = await httpClient.get(`${apiPrefix}/mapping/${indexName}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function loadIndexData(type, indexName) {
|
||||
export async function loadIndexData(type: string, indexName: string) {
|
||||
switch (type) {
|
||||
case TAB_MAPPING:
|
||||
return loadIndexMapping(indexName);
|
||||
|
@ -193,3 +193,20 @@ export async function loadIndexData(type, indexName) {
|
|||
return loadIndexStats(indexName);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadIndexTemplates() {
|
||||
return useRequest({
|
||||
path: `${apiPrefix}/templates`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
export const deleteTemplates = async (names: Array<Template['name']>) => {
|
||||
const uimActionType = names.length > 1 ? UIM_TEMPLATE_DELETE_MANY : UIM_TEMPLATE_DELETE;
|
||||
|
||||
return sendRequest({
|
||||
path: `${apiPrefix}/templates/${names.map(name => encodeURIComponent(name)).join(',')}`,
|
||||
method: 'delete',
|
||||
uimActionType,
|
||||
});
|
||||
};
|
|
@ -4,7 +4,25 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './api';
|
||||
export {
|
||||
loadIndices,
|
||||
reloadIndices,
|
||||
closeIndices,
|
||||
deleteIndices,
|
||||
openIndices,
|
||||
refreshIndices,
|
||||
flushIndices,
|
||||
forcemergeIndices,
|
||||
clearCacheIndices,
|
||||
freezeIndices,
|
||||
unfreezeIndices,
|
||||
loadIndexSettings,
|
||||
updateIndexSettings,
|
||||
loadIndexStats,
|
||||
loadIndexMapping,
|
||||
loadIndexData,
|
||||
loadIndexTemplates,
|
||||
} from './api';
|
||||
export { sortTable } from './sort_table';
|
||||
export { filterItems } from './filter_items';
|
||||
export { healthToColor } from './health_to_color';
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
import { trackUiMetric as track } from '../../../../../../src/legacy/core_plugins/ui_metric/public';
|
||||
import { UIM_APP_NAME } from '../../common/constants';
|
||||
|
||||
export function trackUiMetric(metricType) {
|
||||
export function trackUiMetric(metricType: string) {
|
||||
track(UIM_APP_NAME, metricType);
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { getHttpClient } from './api';
|
||||
import { trackUiMetric } from './track_ui_metric';
|
||||
|
||||
interface SendRequest {
|
||||
path?: string;
|
||||
method: string;
|
||||
body?: any;
|
||||
uimActionType?: string;
|
||||
}
|
||||
|
||||
interface SendRequestResponse {
|
||||
data: any;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
export const sendRequest = async ({
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
uimActionType,
|
||||
}: SendRequest): Promise<Partial<SendRequestResponse>> => {
|
||||
try {
|
||||
const response = await (getHttpClient() as any)[method](path, body);
|
||||
|
||||
if (typeof response.data === 'undefined') {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
// Track successful request
|
||||
if (uimActionType) {
|
||||
trackUiMetric(uimActionType);
|
||||
}
|
||||
|
||||
return {
|
||||
data: response.data,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e.response ? e.response : e,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
interface UseRequest extends SendRequest {
|
||||
interval?: number;
|
||||
initialData?: any;
|
||||
processData?: any;
|
||||
}
|
||||
|
||||
export const useRequest = ({
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
interval,
|
||||
initialData,
|
||||
processData,
|
||||
}: UseRequest) => {
|
||||
const [error, setError] = useState<null | any>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [data, setData] = useState<any>(initialData);
|
||||
|
||||
// Tied to every render and bound to each request.
|
||||
let isOutdatedRequest = false;
|
||||
|
||||
const createRequest = async (isInitialRequest = true) => {
|
||||
// Set a neutral state for a non-request.
|
||||
if (!path) {
|
||||
setError(null);
|
||||
setData(initialData);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
|
||||
// Only set loading state to true and initial data on the first request
|
||||
if (isInitialRequest) {
|
||||
setIsLoading(true);
|
||||
setData(initialData);
|
||||
}
|
||||
|
||||
const { data: responseData, error: responseError } = await sendRequest({
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
});
|
||||
|
||||
// Don't update state if an outdated request has resolved.
|
||||
if (isOutdatedRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError(responseError);
|
||||
setData(processData && responseData ? processData(responseData) : responseData);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function cancelOutdatedRequest() {
|
||||
isOutdatedRequest = true;
|
||||
}
|
||||
|
||||
createRequest();
|
||||
|
||||
if (interval) {
|
||||
const intervalRequest = setInterval(createRequest.bind(null, false), interval);
|
||||
|
||||
return () => {
|
||||
cancelOutdatedRequest();
|
||||
clearInterval(intervalRequest);
|
||||
};
|
||||
}
|
||||
|
||||
// Called when a new render will trigger this effect.
|
||||
return cancelOutdatedRequest;
|
||||
}, [path]);
|
||||
|
||||
return {
|
||||
error,
|
||||
isLoading,
|
||||
data,
|
||||
createRequest,
|
||||
};
|
||||
};
|
7
x-pack/legacy/plugins/index_management/public/store/store.d.ts
vendored
Normal file
7
x-pack/legacy/plugins/index_management/public/store/store.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export declare function indexManagementStore(): any;
|
|
@ -11,7 +11,7 @@ import { defaultTableState } from './reducers/table_state';
|
|||
|
||||
import { indexManagement } from './reducers/';
|
||||
|
||||
export const indexManagementStore = () => {
|
||||
export function indexManagementStore() {
|
||||
const toggleNameToVisibleMap = {};
|
||||
getToggleExtensions().forEach((toggleExtension) => {
|
||||
toggleNameToVisibleMap[toggleExtension.name] = false;
|
||||
|
@ -25,4 +25,4 @@ export const indexManagementStore = () => {
|
|||
initialState,
|
||||
compose(...enhancers)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const fetchTemplates = async (callWithRequest: any) => {
|
||||
const indexTemplatesByName = await callWithRequest('indices.getTemplate');
|
||||
const indexTemplateNames = Object.keys(indexTemplatesByName);
|
||||
|
||||
const indexTemplates = indexTemplateNames.map(name => {
|
||||
const {
|
||||
version,
|
||||
order,
|
||||
index_patterns: indexPatterns = [],
|
||||
settings = {},
|
||||
aliases = {},
|
||||
mappings = {},
|
||||
} = indexTemplatesByName[name];
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
order,
|
||||
indexPatterns: indexPatterns.sort(),
|
||||
settings,
|
||||
aliases,
|
||||
mappings,
|
||||
};
|
||||
});
|
||||
|
||||
return indexTemplates;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { registerTemplatesRoutes } from './register_templates_routes';
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 {
|
||||
Router,
|
||||
RouterRouteHandler,
|
||||
wrapCustomError,
|
||||
} from '../../../../../../server/lib/create_router';
|
||||
import { Template } from '../../../../common/types';
|
||||
|
||||
const handler: RouterRouteHandler = async (req, callWithRequest) => {
|
||||
const {
|
||||
name = '',
|
||||
order,
|
||||
version,
|
||||
settings = {},
|
||||
mappings = {},
|
||||
aliases = {},
|
||||
indexPatterns = [],
|
||||
} = req.payload as Template;
|
||||
|
||||
const conflictError = wrapCustomError(
|
||||
new Error(
|
||||
i18n.translate('xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage', {
|
||||
defaultMessage: "There is already a template with name '{name}'.",
|
||||
values: {
|
||||
name,
|
||||
},
|
||||
})
|
||||
),
|
||||
409
|
||||
);
|
||||
|
||||
// Check that template with the same name doesn't already exist
|
||||
try {
|
||||
const templateExists = await callWithRequest('indices.existsTemplate', { name });
|
||||
|
||||
if (templateExists) {
|
||||
throw conflictError;
|
||||
}
|
||||
} catch (e) {
|
||||
// Rethrow conflict error but silently swallow all others
|
||||
if (e === conflictError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise create new index template
|
||||
return await callWithRequest('indices.putTemplate', {
|
||||
name,
|
||||
order,
|
||||
body: {
|
||||
index_patterns: indexPatterns,
|
||||
version,
|
||||
settings,
|
||||
mappings,
|
||||
aliases,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export function registerCreateRoute(router: Router) {
|
||||
router.put('templates', handler);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Router,
|
||||
RouterRouteHandler,
|
||||
wrapEsError,
|
||||
} from '../../../../../../server/lib/create_router';
|
||||
import { Template } from '../../../../common/types';
|
||||
|
||||
const handler: RouterRouteHandler = async (req, callWithRequest) => {
|
||||
const { names } = req.params;
|
||||
const templateNames = names.split(',');
|
||||
const response: { templatesDeleted: Array<Template['name']>; errors: any[] } = {
|
||||
templatesDeleted: [],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
templateNames.map(async name => {
|
||||
try {
|
||||
await callWithRequest('indices.deleteTemplate', { name });
|
||||
return response.templatesDeleted.push(name);
|
||||
} catch (e) {
|
||||
return response.errors.push({
|
||||
name,
|
||||
error: wrapEsError(e),
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
export function registerDeleteRoute(router: Router) {
|
||||
router.delete('templates/{names}', handler);
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
|
||||
import { fetchTemplates } from '../../../lib/fetch_templates';
|
||||
|
||||
const handler: RouterRouteHandler = async (_req, callWithRequest) => {
|
||||
return fetchTemplates(callWithRequest);
|
||||
};
|
||||
|
||||
export function registerListRoute(router: Router) {
|
||||
router.get('templates', handler);
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Router } from '../../../../../../server/lib/create_router';
|
||||
import { registerListRoute } from './register_list_route';
|
||||
import { registerDeleteRoute } from './register_delete_route';
|
||||
import { registerCreateRoute } from './register_create_route';
|
||||
|
||||
export function registerTemplatesRoutes(router: Router) {
|
||||
registerListRoute(router);
|
||||
registerDeleteRoute(router);
|
||||
registerCreateRoute(router);
|
||||
}
|
7
x-pack/legacy/plugins/index_management/test/fixtures/index.ts
vendored
Normal file
7
x-pack/legacy/plugins/index_management/test/fixtures/index.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './template';
|
26
x-pack/legacy/plugins/index_management/test/fixtures/template.ts
vendored
Normal file
26
x-pack/legacy/plugins/index_management/test/fixtures/template.ts
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getRandomString, getRandomNumber } from '../../../../../test_utils';
|
||||
import { Template } from '../../common/types';
|
||||
|
||||
export const getTemplate = ({
|
||||
name = getRandomString(),
|
||||
version = getRandomNumber(),
|
||||
order = getRandomNumber(),
|
||||
indexPatterns = [],
|
||||
settings = {},
|
||||
aliases = {},
|
||||
mappings = {},
|
||||
}: Partial<Template> = {}): Template => ({
|
||||
name,
|
||||
version,
|
||||
order,
|
||||
indexPatterns,
|
||||
settings,
|
||||
aliases,
|
||||
mappings,
|
||||
});
|
|
@ -4883,8 +4883,6 @@
|
|||
"xpack.idxMgmt.indexTable.headers.storageSizeHeader": "ストレージサイズ",
|
||||
"xpack.idxMgmt.indexTable.invalidSearchErrorMessage": "無効な検索: {errorMessage}",
|
||||
"xpack.idxMgmt.indexTable.reloadIndicesButton": "インデックスを再読み込み",
|
||||
"xpack.idxMgmt.indexTable.sectionDescription": "Elasticsearch インデックスを個々に、または一斉に更新します",
|
||||
"xpack.idxMgmt.indexTable.sectionHeading": "インデックス管理",
|
||||
"xpack.idxMgmt.indexTable.serverErrorTitle": "インデックスの読み込み中にエラーが発生",
|
||||
"xpack.idxMgmt.indexTable.systemIndicesSearchIndicesAriaLabel": "インデックスの検索",
|
||||
"xpack.idxMgmt.indexTable.systemIndicesSearchInputPlaceholder": "検索",
|
||||
|
|
|
@ -4884,8 +4884,6 @@
|
|||
"xpack.idxMgmt.indexTable.headers.storageSizeHeader": "存储大小",
|
||||
"xpack.idxMgmt.indexTable.invalidSearchErrorMessage": "无效搜索:{errorMessage}",
|
||||
"xpack.idxMgmt.indexTable.reloadIndicesButton": "重载索引",
|
||||
"xpack.idxMgmt.indexTable.sectionDescription": "单个或批量更新您的 Elasticsearch 索引。",
|
||||
"xpack.idxMgmt.indexTable.sectionHeading": "索引管理",
|
||||
"xpack.idxMgmt.indexTable.serverErrorTitle": "加载索引时出错",
|
||||
"xpack.idxMgmt.indexTable.systemIndicesSearchIndicesAriaLabel": "搜索索引",
|
||||
"xpack.idxMgmt.indexTable.systemIndicesSearchInputPlaceholder": "搜索",
|
||||
|
|
|
@ -5,3 +5,5 @@
|
|||
*/
|
||||
|
||||
export const API_BASE_PATH = '/api/index_management';
|
||||
|
||||
export const INDEX_PATTERNS = ['test*'];
|
||||
|
|
|
@ -10,5 +10,6 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./mapping'));
|
||||
loadTestFile(require.resolve('./settings'));
|
||||
loadTestFile(require.resolve('./stats'));
|
||||
loadTestFile(require.resolve('./templates'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -39,6 +39,10 @@ export const initElasticsearchHelpers = (es) => {
|
|||
deleteAllIndices()
|
||||
);
|
||||
|
||||
const catTemplate = name => (
|
||||
es.cat.templates({ name, format: 'json' })
|
||||
);
|
||||
|
||||
return ({
|
||||
createIndex,
|
||||
deleteIndex,
|
||||
|
@ -46,5 +50,6 @@ export const initElasticsearchHelpers = (es) => {
|
|||
catIndex,
|
||||
indexStats,
|
||||
cleanUp,
|
||||
catTemplate,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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, INDEX_PATTERNS } from './constants';
|
||||
|
||||
export const registerHelpers = ({ supertest }) => {
|
||||
const list = () => supertest.get(`${API_BASE_PATH}/templates`);
|
||||
|
||||
const getTemplatePayload = name => ({
|
||||
name,
|
||||
order: 1,
|
||||
indexPatterns: INDEX_PATTERNS,
|
||||
version: 1,
|
||||
settings: {
|
||||
number_of_shards: 1
|
||||
},
|
||||
mappings: {
|
||||
_source: {
|
||||
enabled: false
|
||||
},
|
||||
properties: {
|
||||
host_name: {
|
||||
type: 'keyword'
|
||||
},
|
||||
created_at: {
|
||||
type: 'date',
|
||||
format: 'EEE MMM dd HH:mm:ss Z yyyy'
|
||||
}
|
||||
}
|
||||
},
|
||||
aliases: {
|
||||
alias1: {}
|
||||
}
|
||||
});
|
||||
|
||||
const createTemplate = payload =>
|
||||
supertest
|
||||
.put(`${API_BASE_PATH}/templates`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send(payload);
|
||||
|
||||
const deleteTemplates = templatesToDelete =>
|
||||
supertest
|
||||
.delete(`${API_BASE_PATH}/templates/${templatesToDelete.map(template => encodeURIComponent(template)).join(',')}`)
|
||||
.set('kbn-xsrf', 'xxx');
|
||||
|
||||
return {
|
||||
list,
|
||||
getTemplatePayload,
|
||||
createTemplate,
|
||||
deleteTemplates,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { initElasticsearchHelpers, getRandomString } from './lib';
|
||||
import { registerHelpers } from './templates.helpers';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
const {
|
||||
cleanUp: cleanUpEsResources,
|
||||
catTemplate,
|
||||
} = initElasticsearchHelpers(es);
|
||||
|
||||
const {
|
||||
list,
|
||||
createTemplate,
|
||||
getTemplatePayload,
|
||||
deleteTemplates,
|
||||
} = registerHelpers({ supertest });
|
||||
|
||||
describe('index templates', () => {
|
||||
after(() => Promise.all([cleanUpEsResources()]));
|
||||
|
||||
describe('list', function () {
|
||||
it('should list all the index templates with the expected properties', async function () {
|
||||
const { body } = await list().expect(200);
|
||||
const expectedKeys = ['name', 'indexPatterns', 'settings', 'aliases', 'mappings'];
|
||||
|
||||
expectedKeys.forEach(key => expect(Object.keys(body[0]).includes(key)).to.be(true));
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create an index template', async () => {
|
||||
const templateName = `template-${getRandomString()}`;
|
||||
const payload = getTemplatePayload(templateName);
|
||||
|
||||
await createTemplate(payload).expect(200);
|
||||
});
|
||||
|
||||
it('should throw a 409 conflict when trying to create 2 templates with the same name', async () => {
|
||||
const templateName = `template-${getRandomString()}`;
|
||||
const payload = getTemplatePayload(templateName);
|
||||
|
||||
await createTemplate(payload);
|
||||
|
||||
await createTemplate(payload).expect(409);
|
||||
});
|
||||
|
||||
it('should handle ES errors', async () => {
|
||||
const templateName = `template-${getRandomString()}`;
|
||||
const payload = getTemplatePayload(templateName);
|
||||
|
||||
delete payload.indexPatterns; // index patterns are required
|
||||
|
||||
const { body } = await createTemplate(payload);
|
||||
expect(body.message).to.contain('index patterns are missing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete an index template', async () => {
|
||||
const templateName = `template-${getRandomString()}`;
|
||||
const payload = getTemplatePayload(templateName);
|
||||
|
||||
await createTemplate(payload).expect(200);
|
||||
|
||||
let catTemplateResponse = await catTemplate(templateName);
|
||||
|
||||
expect(catTemplateResponse.find(template => template.name === payload.name).name).to.equal(templateName);
|
||||
|
||||
const { body } = await deleteTemplates([templateName]).expect(200);
|
||||
|
||||
expect(body.errors).to.be.empty;
|
||||
expect(body.templatesDeleted[0]).to.equal(templateName);
|
||||
|
||||
catTemplateResponse = await catTemplate(templateName);
|
||||
|
||||
expect(catTemplateResponse.find(template => template.name === payload.name)).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -13,7 +13,7 @@ export const IndexManagementPageProvider = ({
|
|||
|
||||
return {
|
||||
async sectionHeadingText() {
|
||||
return await testSubjects.getVisibleText('sectionHeading');
|
||||
return await testSubjects.getVisibleText('appTitle');
|
||||
},
|
||||
async reloadIndicesButton() {
|
||||
return await testSubjects.find('reloadIndicesButton');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue