mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Refactor index management client integration tests for scalability (#67917)
This commit is contained in:
parent
a40076b658
commit
573409b9f0
24 changed files with 850 additions and 785 deletions
|
@ -4,18 +4,44 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { setup as homeSetup } from './home.helpers';
|
||||
import { setup as templateCreateSetup } from './template_create.helpers';
|
||||
import { setup as templateCloneSetup } from './template_clone.helpers';
|
||||
import { setup as templateEditSetup } from './template_edit.helpers';
|
||||
|
||||
export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../test_utils';
|
||||
|
||||
export { setupEnvironment } from './setup_environment';
|
||||
export { setupEnvironment, WithAppDependencies, services } from './setup_environment';
|
||||
|
||||
export const pageHelpers = {
|
||||
home: { setup: homeSetup },
|
||||
templateCreate: { setup: templateCreateSetup },
|
||||
templateClone: { setup: templateCloneSetup },
|
||||
templateEdit: { setup: templateEditSetup },
|
||||
};
|
||||
export type TestSubjects =
|
||||
| 'aliasesTab'
|
||||
| 'appTitle'
|
||||
| 'cell'
|
||||
| 'closeDetailsButton'
|
||||
| 'createTemplateButton'
|
||||
| 'deleteSystemTemplateCallOut'
|
||||
| 'deleteTemplateButton'
|
||||
| 'deleteTemplatesConfirmation'
|
||||
| 'documentationLink'
|
||||
| 'emptyPrompt'
|
||||
| 'manageTemplateButton'
|
||||
| 'mappingsTab'
|
||||
| 'noAliasesCallout'
|
||||
| 'noMappingsCallout'
|
||||
| 'noSettingsCallout'
|
||||
| 'indicesList'
|
||||
| 'indicesTab'
|
||||
| 'indexTableIncludeHiddenIndicesToggle'
|
||||
| 'indexTableIndexNameLink'
|
||||
| 'reloadButton'
|
||||
| 'reloadIndicesButton'
|
||||
| 'row'
|
||||
| 'sectionError'
|
||||
| 'sectionLoading'
|
||||
| 'settingsTab'
|
||||
| 'summaryTab'
|
||||
| 'summaryTitle'
|
||||
| 'systemTemplatesSwitch'
|
||||
| 'templateDetails'
|
||||
| 'templateDetails.manageTemplateButton'
|
||||
| 'templateDetails.sectionLoading'
|
||||
| 'templateDetails.tab'
|
||||
| 'templateDetails.title'
|
||||
| 'templateList'
|
||||
| 'templateTable'
|
||||
| 'templatesTab';
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @kbn/eslint/no-restricted-paths */
|
||||
import React from 'react';
|
||||
import axios from 'axios';
|
||||
|
|
|
@ -1,587 +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 { 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';
|
||||
import { API_BASE_PATH } from '../../common/constants';
|
||||
|
||||
const { setup } = pageHelpers.home;
|
||||
|
||||
const removeWhiteSpaceOnArrayValues = (array: any[]) =>
|
||||
array.map((value) => {
|
||||
if (!value.trim) {
|
||||
return value;
|
||||
}
|
||||
return value.trim();
|
||||
});
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
describe('<IndexManagementHome />', () => {
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: IdxMgmtHomeTestBed;
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('on component mount', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadIndicesResponse([]);
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
await act(async () => {
|
||||
const { component } = testBed;
|
||||
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('sets the hash query param base on include hidden indices toggle', () => {
|
||||
const { actions } = testBed;
|
||||
expect(actions.getIncludeHiddenIndicesToggleStatus()).toBe(true);
|
||||
expect(window.location.hash.includes('includeHidden=true')).toBe(true);
|
||||
actions.clickIncludeHiddenIndicesToggle();
|
||||
expect(window.location.hash.includes('includeHidden=true')).toBe(false);
|
||||
// Note: this test modifies the shared location.hash state, we put it back the way it was
|
||||
actions.clickIncludeHiddenIndicesToggle();
|
||||
expect(actions.getIncludeHiddenIndicesToggleStatus()).toBe(true);
|
||||
expect(window.location.hash.includes('includeHidden=true')).toBe(true);
|
||||
});
|
||||
|
||||
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;
|
||||
const templatesTab = find('templatesTab');
|
||||
const indicesTab = find('indicesTab');
|
||||
|
||||
expect(indicesTab.length).toBe(1);
|
||||
expect(indicesTab.text()).toEqual('Indices');
|
||||
expect(templatesTab.length).toBe(1);
|
||||
expect(templatesTab.text()).toEqual('Index Templates');
|
||||
});
|
||||
|
||||
test('should navigate to Index Templates tab', async () => {
|
||||
const { exists, actions, component } = testBed;
|
||||
|
||||
expect(exists('indicesList')).toBe(true);
|
||||
expect(exists('templateList')).toBe(false);
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplatesResponse([]);
|
||||
|
||||
actions.selectHomeTab('templatesTab');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(exists('indicesList')).toBe(false);
|
||||
expect(exists('templateList')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('index templates', () => {
|
||||
describe('when there are no index templates', () => {
|
||||
beforeEach(async () => {
|
||||
const { actions, component } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplatesResponse([]);
|
||||
|
||||
actions.selectHomeTab('templatesTab');
|
||||
|
||||
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'],
|
||||
template: {
|
||||
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.selectHomeTab('templatesTab');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should list them in the table', async () => {
|
||||
const { table } = testBed;
|
||||
|
||||
const { tableCellsValues } = table.getMetaData('templateTable');
|
||||
|
||||
tableCellsValues.forEach((row, i) => {
|
||||
const template = templates[i];
|
||||
const { name, indexPatterns, order, ilmPolicy } = template;
|
||||
|
||||
const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : '';
|
||||
const orderFormatted = order ? order.toString() : order;
|
||||
|
||||
expect(removeWhiteSpaceOnArrayValues(row)).toEqual([
|
||||
'',
|
||||
name,
|
||||
indexPatterns.join(', '),
|
||||
ilmPolicyName,
|
||||
orderFormatted,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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_BASE_PATH}/templates`
|
||||
);
|
||||
});
|
||||
|
||||
test('should have a button to create a new template', () => {
|
||||
const { exists } = testBed;
|
||||
expect(exists('createTemplateButton')).toBe(true);
|
||||
});
|
||||
|
||||
test('should have a switch to view system templates', async () => {
|
||||
const { table, exists, component, form } = testBed;
|
||||
const { rows } = table.getMetaData('templateTable');
|
||||
|
||||
expect(rows.length).toEqual(
|
||||
templates.filter((template) => !template.name.startsWith('.')).length
|
||||
);
|
||||
|
||||
expect(exists('systemTemplatesSwitch')).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
form.toggleEuiSwitch('systemTemplatesSwitch');
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const { rows: updatedRows } = table.getMetaData('templateTable');
|
||||
expect(updatedRows.length).toEqual(templates.length);
|
||||
});
|
||||
|
||||
test('each row should have a link to the template details panel', async () => {
|
||||
const { find, exists, actions } = testBed;
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
|
||||
expect(exists('templateList')).toBe(true);
|
||||
expect(exists('templateDetails')).toBe(true);
|
||||
expect(find('templateDetails.title').text()).toBe(template1.name);
|
||||
});
|
||||
|
||||
test('template actions column should have an option to delete', () => {
|
||||
const { actions, findAction } = testBed;
|
||||
const { name: templateName } = template1;
|
||||
|
||||
actions.clickActionMenu(templateName);
|
||||
|
||||
const deleteAction = findAction('delete');
|
||||
|
||||
expect(deleteAction.text()).toEqual('Delete');
|
||||
});
|
||||
|
||||
test('template actions column should have an option to clone', () => {
|
||||
const { actions, findAction } = testBed;
|
||||
const { name: templateName } = template1;
|
||||
|
||||
actions.clickActionMenu(templateName);
|
||||
|
||||
const cloneAction = findAction('clone');
|
||||
|
||||
expect(cloneAction.text()).toEqual('Clone');
|
||||
});
|
||||
|
||||
test('template actions column should have an option to edit', () => {
|
||||
const { actions, findAction } = testBed;
|
||||
const { name: templateName } = template1;
|
||||
|
||||
actions.clickActionMenu(templateName);
|
||||
|
||||
const editAction = findAction('edit');
|
||||
|
||||
expect(editAction.text()).toEqual('Edit');
|
||||
});
|
||||
|
||||
describe('delete index template', () => {
|
||||
test('should show a confirmation when clicking the delete template button', async () => {
|
||||
const { actions } = testBed;
|
||||
const { name: templateName } = template1;
|
||||
|
||||
await actions.clickTemplateAction(templateName, '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;
|
||||
|
||||
await act(async () => {
|
||||
form.toggleEuiSwitch('systemTemplatesSwitch');
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const { name: systemTemplateName } = template3;
|
||||
await actions.clickTemplateAction(systemTemplateName, '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('templateTable');
|
||||
|
||||
const templateId = rows[0].columns[2].value;
|
||||
|
||||
const {
|
||||
name: templateName,
|
||||
_kbnMeta: { formatVersion },
|
||||
} = template1;
|
||||
await actions.clickTemplateAction(templateName, 'delete');
|
||||
|
||||
const modal = document.body.querySelector(
|
||||
'[data-test-subj="deleteTemplatesConfirmation"]'
|
||||
);
|
||||
const confirmButton: HTMLButtonElement | null = modal!.querySelector(
|
||||
'[data-test-subj="confirmModalConfirmButton"]'
|
||||
);
|
||||
|
||||
httpRequestsMockHelpers.setDeleteTemplateResponse({
|
||||
results: {
|
||||
successes: [templateId],
|
||||
errors: [],
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
confirmButton!.click();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.method).toBe('POST');
|
||||
expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete-templates`);
|
||||
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({
|
||||
templates: [{ name: template1.name, formatVersion }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('detail panel', () => {
|
||||
beforeEach(async () => {
|
||||
const template = fixtures.getTemplate({
|
||||
name: `a${getRandomString()}`,
|
||||
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
|
||||
});
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplateResponse(template);
|
||||
});
|
||||
|
||||
test('should show details when clicking on a template', async () => {
|
||||
const { exists, actions } = testBed;
|
||||
|
||||
expect(exists('templateDetails')).toBe(false);
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
|
||||
expect(exists('templateDetails')).toBe(true);
|
||||
});
|
||||
|
||||
describe('on mount', () => {
|
||||
beforeEach(async () => {
|
||||
const { actions } = testBed;
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
});
|
||||
|
||||
test('should set the correct title', async () => {
|
||||
const { find } = testBed;
|
||||
const { name } = template1;
|
||||
|
||||
expect(find('templateDetails.title').text()).toEqual(name);
|
||||
});
|
||||
|
||||
it('should have a close button and be able to close flyout', async () => {
|
||||
const { actions, component, exists } = testBed;
|
||||
|
||||
expect(exists('closeDetailsButton')).toBe(true);
|
||||
expect(exists('summaryTab')).toBe(true);
|
||||
|
||||
actions.clickCloseDetailsButton();
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(exists('summaryTab')).toBe(false);
|
||||
});
|
||||
|
||||
it('should have a manage button', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
|
||||
expect(exists('templateDetails.manageTemplateButton')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tabs', () => {
|
||||
test('should have 4 tabs', async () => {
|
||||
const template = fixtures.getTemplate({
|
||||
name: `a${getRandomString()}`,
|
||||
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
|
||||
template: {
|
||||
settings: {
|
||||
index: {
|
||||
number_of_shards: '1',
|
||||
},
|
||||
},
|
||||
mappings: {
|
||||
_source: {
|
||||
enabled: false,
|
||||
},
|
||||
properties: {
|
||||
created_at: {
|
||||
type: 'date',
|
||||
format: 'EEE MMM dd HH:mm:ss Z yyyy',
|
||||
},
|
||||
},
|
||||
},
|
||||
aliases: {
|
||||
alias1: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { find, actions, exists } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplateResponse(template);
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
|
||||
expect(find('templateDetails.tab').length).toBe(4);
|
||||
expect(find('templateDetails.tab').map((t) => t.text())).toEqual([
|
||||
'Summary',
|
||||
'Settings',
|
||||
'Mappings',
|
||||
'Aliases',
|
||||
]);
|
||||
|
||||
// Summary tab should be initial active tab
|
||||
expect(exists('summaryTab')).toBe(true);
|
||||
|
||||
// Navigate and verify all tabs
|
||||
actions.selectDetailsTab('settings');
|
||||
expect(exists('summaryTab')).toBe(false);
|
||||
expect(exists('settingsTab')).toBe(true);
|
||||
|
||||
actions.selectDetailsTab('aliases');
|
||||
expect(exists('summaryTab')).toBe(false);
|
||||
expect(exists('settingsTab')).toBe(false);
|
||||
expect(exists('aliasesTab')).toBe(true);
|
||||
|
||||
actions.selectDetailsTab('mappings');
|
||||
expect(exists('summaryTab')).toBe(false);
|
||||
expect(exists('settingsTab')).toBe(false);
|
||||
expect(exists('aliasesTab')).toBe(false);
|
||||
expect(exists('mappingsTab')).toBe(true);
|
||||
});
|
||||
|
||||
test('should show an info callout if data is not present', async () => {
|
||||
const templateWithNoOptionalFields = fixtures.getTemplate({
|
||||
name: `a${getRandomString()}`,
|
||||
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
|
||||
});
|
||||
|
||||
const { actions, find, exists, component } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplateResponse(templateWithNoOptionalFields);
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(find('templateDetails.tab').length).toBe(4);
|
||||
expect(exists('summaryTab')).toBe(true);
|
||||
|
||||
// Navigate and verify callout message per tab
|
||||
actions.selectDetailsTab('settings');
|
||||
expect(exists('noSettingsCallout')).toBe(true);
|
||||
|
||||
actions.selectDetailsTab('mappings');
|
||||
expect(exists('noMappingsCallout')).toBe(true);
|
||||
|
||||
actions.selectDetailsTab('aliases');
|
||||
expect(exists('noAliasesCallout')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should render an error message if error fetching template details', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
const error = {
|
||||
status: 404,
|
||||
error: 'Not found',
|
||||
message: 'Template not found',
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplateResponse(undefined, { body: error });
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
|
||||
expect(exists('sectionError')).toBe(true);
|
||||
// Manage button should not render if error
|
||||
expect(exists('templateDetails.manageTemplateButton')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('index detail panel with % character in index name', () => {
|
||||
const indexName = 'test%';
|
||||
beforeEach(async () => {
|
||||
const index = {
|
||||
health: 'green',
|
||||
status: 'open',
|
||||
primary: 1,
|
||||
replica: 1,
|
||||
documents: 10000,
|
||||
documents_deleted: 100,
|
||||
size: '156kb',
|
||||
primary_size: '156kb',
|
||||
name: indexName,
|
||||
};
|
||||
httpRequestsMockHelpers.setLoadIndicesResponse([index]);
|
||||
|
||||
testBed = await setup();
|
||||
const { component, find } = testBed;
|
||||
|
||||
component.update();
|
||||
|
||||
find('indexTableIndexNameLink').at(0).simulate('click');
|
||||
});
|
||||
|
||||
test('should encode indexName when loading settings in detail panel', async () => {
|
||||
const { actions } = testBed;
|
||||
await actions.selectIndexDetailsTab('settings');
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
expect(latestRequest.url).toBe(`${API_BASE_PATH}/settings/${encodeURIComponent(indexName)}`);
|
||||
});
|
||||
|
||||
test('should encode indexName when loading mappings in detail panel', async () => {
|
||||
const { actions } = testBed;
|
||||
await actions.selectIndexDetailsTab('mappings');
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
expect(latestRequest.url).toBe(`${API_BASE_PATH}/mapping/${encodeURIComponent(indexName)}`);
|
||||
});
|
||||
|
||||
test('should encode indexName when loading stats in detail panel', async () => {
|
||||
const { actions } = testBed;
|
||||
await actions.selectIndexDetailsTab('stats');
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
expect(latestRequest.url).toBe(`${API_BASE_PATH}/stats/${encodeURIComponent(indexName)}`);
|
||||
});
|
||||
|
||||
test('should encode indexName when editing settings in detail panel', async () => {
|
||||
const { actions } = testBed;
|
||||
await actions.selectIndexDetailsTab('edit_settings');
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
expect(latestRequest.url).toBe(`${API_BASE_PATH}/settings/${encodeURIComponent(indexName)}`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils';
|
||||
import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths
|
||||
import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths
|
||||
import { WithAppDependencies, services, TestSubjects } from '../helpers';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
store: () => indexManagementStore(services as any),
|
||||
memoryRouter: {
|
||||
initialEntries: [`/indices?includeHidden=true`],
|
||||
componentRoutePath: `/:section(indices|templates)`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig);
|
||||
|
||||
export interface HomeTestBed extends TestBed<TestSubjects> {
|
||||
actions: {
|
||||
selectHomeTab: (tab: 'indicesTab' | 'templatesTab') => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (): Promise<HomeTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
/**
|
||||
* User Actions
|
||||
*/
|
||||
|
||||
const selectHomeTab = (tab: 'indicesTab' | 'templatesTab') => {
|
||||
testBed.find(tab).simulate('click');
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
selectHomeTab,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { setupEnvironment, nextTick } from '../helpers';
|
||||
|
||||
import { HomeTestBed, setup } from './home.helpers';
|
||||
|
||||
describe('<IndexManagementHome />', () => {
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: HomeTestBed;
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('on component mount', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadIndicesResponse([]);
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
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;
|
||||
const templatesTab = find('templatesTab');
|
||||
const indicesTab = find('indicesTab');
|
||||
|
||||
expect(indicesTab.length).toBe(1);
|
||||
expect(indicesTab.text()).toEqual('Indices');
|
||||
expect(templatesTab.length).toBe(1);
|
||||
expect(templatesTab.text()).toEqual('Index Templates');
|
||||
});
|
||||
|
||||
test('should navigate to Index Templates tab', async () => {
|
||||
const { exists, actions, component } = testBed;
|
||||
|
||||
expect(exists('indicesList')).toBe(true);
|
||||
expect(exists('templateList')).toBe(false);
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplatesResponse([]);
|
||||
|
||||
actions.selectHomeTab('templatesTab');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(exists('indicesList')).toBe(false);
|
||||
expect(exists('templateList')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import {
|
||||
registerTestBed,
|
||||
TestBed,
|
||||
|
@ -13,10 +14,12 @@ import {
|
|||
findTestSubject,
|
||||
nextTick,
|
||||
} from '../../../../../test_utils';
|
||||
// NOTE: We have to use the Home component instead of the TemplateList component because we depend
|
||||
// upon react router to provide the name of the template to load in the detail panel.
|
||||
import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths
|
||||
import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths
|
||||
import { TemplateDeserialized } from '../../../common';
|
||||
import { WithAppDependencies, services } from './setup_environment';
|
||||
import { WithAppDependencies, services, TestSubjects } from '../helpers';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
store: () => indexManagementStore(services as any),
|
||||
|
@ -29,12 +32,11 @@ const testBedConfig: TestBedConfig = {
|
|||
|
||||
const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig);
|
||||
|
||||
export interface IdxMgmtHomeTestBed extends TestBed<IdxMgmtTestSubjects> {
|
||||
export interface IndexTemplatesTabTestBed extends TestBed<TestSubjects> {
|
||||
findAction: (action: 'edit' | 'clone' | 'delete') => ReactWrapper;
|
||||
actions: {
|
||||
selectHomeTab: (tab: 'indicesTab' | 'templatesTab') => void;
|
||||
goToTemplatesList: () => void;
|
||||
selectDetailsTab: (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => void;
|
||||
selectIndexDetailsTab: (tab: 'settings' | 'mappings' | 'stats' | 'edit_settings') => void;
|
||||
clickReloadButton: () => void;
|
||||
clickTemplateAction: (
|
||||
name: TemplateDeserialized['name'],
|
||||
|
@ -43,12 +45,10 @@ export interface IdxMgmtHomeTestBed extends TestBed<IdxMgmtTestSubjects> {
|
|||
clickTemplateAt: (index: number) => void;
|
||||
clickCloseDetailsButton: () => void;
|
||||
clickActionMenu: (name: TemplateDeserialized['name']) => void;
|
||||
getIncludeHiddenIndicesToggleStatus: () => boolean;
|
||||
clickIncludeHiddenIndicesToggle: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (): Promise<IdxMgmtHomeTestBed> => {
|
||||
export const setup = async (): Promise<IndexTemplatesTabTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
/**
|
||||
|
@ -65,8 +65,8 @@ export const setup = async (): Promise<IdxMgmtHomeTestBed> => {
|
|||
* User Actions
|
||||
*/
|
||||
|
||||
const selectHomeTab = (tab: 'indicesTab' | 'templatesTab') => {
|
||||
testBed.find(tab).simulate('click');
|
||||
const goToTemplatesList = () => {
|
||||
testBed.find('templatesTab').simulate('click');
|
||||
};
|
||||
|
||||
const selectDetailsTab = (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => {
|
||||
|
@ -119,82 +119,17 @@ export const setup = async (): Promise<IdxMgmtHomeTestBed> => {
|
|||
find('closeDetailsButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickIncludeHiddenIndicesToggle = () => {
|
||||
const { find } = testBed;
|
||||
find('indexTableIncludeHiddenIndicesToggle').simulate('click');
|
||||
};
|
||||
|
||||
const getIncludeHiddenIndicesToggleStatus = () => {
|
||||
const { find } = testBed;
|
||||
const props = find('indexTableIncludeHiddenIndicesToggle').props();
|
||||
return Boolean(props['aria-checked']);
|
||||
};
|
||||
|
||||
const selectIndexDetailsTab = async (
|
||||
tab: 'settings' | 'mappings' | 'stats' | 'edit_settings'
|
||||
) => {
|
||||
const indexDetailsTabs = ['settings', 'mappings', 'stats', 'edit_settings'];
|
||||
const { find, component } = testBed;
|
||||
await act(async () => {
|
||||
find('detailPanelTab').at(indexDetailsTabs.indexOf(tab)).simulate('click');
|
||||
});
|
||||
component.update();
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
findAction,
|
||||
actions: {
|
||||
selectHomeTab,
|
||||
goToTemplatesList,
|
||||
selectDetailsTab,
|
||||
selectIndexDetailsTab,
|
||||
clickReloadButton,
|
||||
clickTemplateAction,
|
||||
clickTemplateAt,
|
||||
clickCloseDetailsButton,
|
||||
clickActionMenu,
|
||||
getIncludeHiddenIndicesToggleStatus,
|
||||
clickIncludeHiddenIndicesToggle,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type IdxMgmtTestSubjects = TestSubjects;
|
||||
|
||||
export type TestSubjects =
|
||||
| 'aliasesTab'
|
||||
| 'appTitle'
|
||||
| 'cell'
|
||||
| 'closeDetailsButton'
|
||||
| 'createTemplateButton'
|
||||
| 'deleteSystemTemplateCallOut'
|
||||
| 'deleteTemplateButton'
|
||||
| 'deleteTemplatesConfirmation'
|
||||
| 'documentationLink'
|
||||
| 'emptyPrompt'
|
||||
| 'manageTemplateButton'
|
||||
| 'mappingsTab'
|
||||
| 'noAliasesCallout'
|
||||
| 'noMappingsCallout'
|
||||
| 'noSettingsCallout'
|
||||
| 'indicesList'
|
||||
| 'indicesTab'
|
||||
| 'indexTableIncludeHiddenIndicesToggle'
|
||||
| 'indexTableIndexNameLink'
|
||||
| 'reloadButton'
|
||||
| 'reloadIndicesButton'
|
||||
| 'row'
|
||||
| 'sectionError'
|
||||
| 'sectionLoading'
|
||||
| 'settingsTab'
|
||||
| 'summaryTab'
|
||||
| 'summaryTitle'
|
||||
| 'systemTemplatesSwitch'
|
||||
| 'templateDetails'
|
||||
| 'templateDetails.manageTemplateButton'
|
||||
| 'templateDetails.sectionLoading'
|
||||
| 'templateDetails.tab'
|
||||
| 'templateDetails.title'
|
||||
| 'templateList'
|
||||
| 'templateTable'
|
||||
| 'templatesTab';
|
|
@ -0,0 +1,463 @@
|
|||
/*
|
||||
* 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 { API_BASE_PATH } from '../../../common/constants';
|
||||
import { setupEnvironment, nextTick, getRandomString } from '../helpers';
|
||||
|
||||
import { IndexTemplatesTabTestBed, setup } from './index_templates_tab.helpers';
|
||||
|
||||
const removeWhiteSpaceOnArrayValues = (array: any[]) =>
|
||||
array.map((value) => {
|
||||
if (!value.trim) {
|
||||
return value;
|
||||
}
|
||||
return value.trim();
|
||||
});
|
||||
|
||||
describe('Index Templates tab', () => {
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: IndexTemplatesTabTestBed;
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadIndicesResponse([]);
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
await act(async () => {
|
||||
const { component } = testBed;
|
||||
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no index templates', () => {
|
||||
beforeEach(async () => {
|
||||
const { actions, component } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplatesResponse([]);
|
||||
|
||||
actions.goToTemplatesList();
|
||||
|
||||
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'],
|
||||
template: {
|
||||
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.goToTemplatesList();
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should list them in the table', async () => {
|
||||
const { table } = testBed;
|
||||
|
||||
const { tableCellsValues } = table.getMetaData('templateTable');
|
||||
|
||||
tableCellsValues.forEach((row, i) => {
|
||||
const template = templates[i];
|
||||
const { name, indexPatterns, order, ilmPolicy } = template;
|
||||
|
||||
const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : '';
|
||||
const orderFormatted = order ? order.toString() : order;
|
||||
|
||||
expect(removeWhiteSpaceOnArrayValues(row)).toEqual([
|
||||
'',
|
||||
name,
|
||||
indexPatterns.join(', '),
|
||||
ilmPolicyName,
|
||||
orderFormatted,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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_BASE_PATH}/templates`);
|
||||
});
|
||||
|
||||
test('should have a button to create a new template', () => {
|
||||
const { exists } = testBed;
|
||||
expect(exists('createTemplateButton')).toBe(true);
|
||||
});
|
||||
|
||||
test('should have a switch to view system templates', async () => {
|
||||
const { table, exists, component, form } = testBed;
|
||||
const { rows } = table.getMetaData('templateTable');
|
||||
|
||||
expect(rows.length).toEqual(
|
||||
templates.filter((template) => !template.name.startsWith('.')).length
|
||||
);
|
||||
|
||||
expect(exists('systemTemplatesSwitch')).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
form.toggleEuiSwitch('systemTemplatesSwitch');
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const { rows: updatedRows } = table.getMetaData('templateTable');
|
||||
expect(updatedRows.length).toEqual(templates.length);
|
||||
});
|
||||
|
||||
test('each row should have a link to the template details panel', async () => {
|
||||
const { find, exists, actions } = testBed;
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
|
||||
expect(exists('templateList')).toBe(true);
|
||||
expect(exists('templateDetails')).toBe(true);
|
||||
expect(find('templateDetails.title').text()).toBe(template1.name);
|
||||
});
|
||||
|
||||
test('template actions column should have an option to delete', () => {
|
||||
const { actions, findAction } = testBed;
|
||||
const { name: templateName } = template1;
|
||||
|
||||
actions.clickActionMenu(templateName);
|
||||
|
||||
const deleteAction = findAction('delete');
|
||||
|
||||
expect(deleteAction.text()).toEqual('Delete');
|
||||
});
|
||||
|
||||
test('template actions column should have an option to clone', () => {
|
||||
const { actions, findAction } = testBed;
|
||||
const { name: templateName } = template1;
|
||||
|
||||
actions.clickActionMenu(templateName);
|
||||
|
||||
const cloneAction = findAction('clone');
|
||||
|
||||
expect(cloneAction.text()).toEqual('Clone');
|
||||
});
|
||||
|
||||
test('template actions column should have an option to edit', () => {
|
||||
const { actions, findAction } = testBed;
|
||||
const { name: templateName } = template1;
|
||||
|
||||
actions.clickActionMenu(templateName);
|
||||
|
||||
const editAction = findAction('edit');
|
||||
|
||||
expect(editAction.text()).toEqual('Edit');
|
||||
});
|
||||
|
||||
describe('delete index template', () => {
|
||||
test('should show a confirmation when clicking the delete template button', async () => {
|
||||
const { actions } = testBed;
|
||||
const { name: templateName } = template1;
|
||||
|
||||
await actions.clickTemplateAction(templateName, '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;
|
||||
|
||||
await act(async () => {
|
||||
form.toggleEuiSwitch('systemTemplatesSwitch');
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const { name: systemTemplateName } = template3;
|
||||
await actions.clickTemplateAction(systemTemplateName, '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('templateTable');
|
||||
|
||||
const templateId = rows[0].columns[2].value;
|
||||
|
||||
const {
|
||||
name: templateName,
|
||||
_kbnMeta: { formatVersion },
|
||||
} = template1;
|
||||
await actions.clickTemplateAction(templateName, 'delete');
|
||||
|
||||
const modal = document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]');
|
||||
const confirmButton: HTMLButtonElement | null = modal!.querySelector(
|
||||
'[data-test-subj="confirmModalConfirmButton"]'
|
||||
);
|
||||
|
||||
httpRequestsMockHelpers.setDeleteTemplateResponse({
|
||||
results: {
|
||||
successes: [templateId],
|
||||
errors: [],
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
confirmButton!.click();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.method).toBe('POST');
|
||||
expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete-templates`);
|
||||
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({
|
||||
templates: [{ name: template1.name, formatVersion }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('detail panel', () => {
|
||||
beforeEach(async () => {
|
||||
const template = fixtures.getTemplate({
|
||||
name: `a${getRandomString()}`,
|
||||
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
|
||||
});
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplateResponse(template);
|
||||
});
|
||||
|
||||
test('should show details when clicking on a template', async () => {
|
||||
const { exists, actions } = testBed;
|
||||
|
||||
expect(exists('templateDetails')).toBe(false);
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
|
||||
expect(exists('templateDetails')).toBe(true);
|
||||
});
|
||||
|
||||
describe('on mount', () => {
|
||||
beforeEach(async () => {
|
||||
const { actions } = testBed;
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
});
|
||||
|
||||
test('should set the correct title', async () => {
|
||||
const { find } = testBed;
|
||||
const { name } = template1;
|
||||
|
||||
expect(find('templateDetails.title').text()).toEqual(name);
|
||||
});
|
||||
|
||||
it('should have a close button and be able to close flyout', async () => {
|
||||
const { actions, component, exists } = testBed;
|
||||
|
||||
expect(exists('closeDetailsButton')).toBe(true);
|
||||
expect(exists('summaryTab')).toBe(true);
|
||||
|
||||
actions.clickCloseDetailsButton();
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(exists('summaryTab')).toBe(false);
|
||||
});
|
||||
|
||||
it('should have a manage button', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
|
||||
expect(exists('templateDetails.manageTemplateButton')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tabs', () => {
|
||||
test('should have 4 tabs', async () => {
|
||||
const template = fixtures.getTemplate({
|
||||
name: `a${getRandomString()}`,
|
||||
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
|
||||
template: {
|
||||
settings: {
|
||||
index: {
|
||||
number_of_shards: '1',
|
||||
},
|
||||
},
|
||||
mappings: {
|
||||
_source: {
|
||||
enabled: false,
|
||||
},
|
||||
properties: {
|
||||
created_at: {
|
||||
type: 'date',
|
||||
format: 'EEE MMM dd HH:mm:ss Z yyyy',
|
||||
},
|
||||
},
|
||||
},
|
||||
aliases: {
|
||||
alias1: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { find, actions, exists } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplateResponse(template);
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
|
||||
expect(find('templateDetails.tab').length).toBe(4);
|
||||
expect(find('templateDetails.tab').map((t) => t.text())).toEqual([
|
||||
'Summary',
|
||||
'Settings',
|
||||
'Mappings',
|
||||
'Aliases',
|
||||
]);
|
||||
|
||||
// Summary tab should be initial active tab
|
||||
expect(exists('summaryTab')).toBe(true);
|
||||
|
||||
// Navigate and verify all tabs
|
||||
actions.selectDetailsTab('settings');
|
||||
expect(exists('summaryTab')).toBe(false);
|
||||
expect(exists('settingsTab')).toBe(true);
|
||||
|
||||
actions.selectDetailsTab('aliases');
|
||||
expect(exists('summaryTab')).toBe(false);
|
||||
expect(exists('settingsTab')).toBe(false);
|
||||
expect(exists('aliasesTab')).toBe(true);
|
||||
|
||||
actions.selectDetailsTab('mappings');
|
||||
expect(exists('summaryTab')).toBe(false);
|
||||
expect(exists('settingsTab')).toBe(false);
|
||||
expect(exists('aliasesTab')).toBe(false);
|
||||
expect(exists('mappingsTab')).toBe(true);
|
||||
});
|
||||
|
||||
test('should show an info callout if data is not present', async () => {
|
||||
const templateWithNoOptionalFields = fixtures.getTemplate({
|
||||
name: `a${getRandomString()}`,
|
||||
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
|
||||
});
|
||||
|
||||
const { actions, find, exists, component } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplateResponse(templateWithNoOptionalFields);
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(find('templateDetails.tab').length).toBe(4);
|
||||
expect(exists('summaryTab')).toBe(true);
|
||||
|
||||
// Navigate and verify callout message per tab
|
||||
actions.selectDetailsTab('settings');
|
||||
expect(exists('noSettingsCallout')).toBe(true);
|
||||
|
||||
actions.selectDetailsTab('mappings');
|
||||
expect(exists('noMappingsCallout')).toBe(true);
|
||||
|
||||
actions.selectDetailsTab('aliases');
|
||||
expect(exists('noAliasesCallout')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should render an error message if error fetching template details', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
const error = {
|
||||
status: 404,
|
||||
error: 'Not found',
|
||||
message: 'Template not found',
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplateResponse(undefined, { body: error });
|
||||
|
||||
await actions.clickTemplateAt(0);
|
||||
|
||||
expect(exists('sectionError')).toBe(true);
|
||||
// Manage button should not render if error
|
||||
expect(exists('templateDetails.manageTemplateButton')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils';
|
||||
import { IndexList } from '../../../public/application/sections/home/index_list'; // eslint-disable-line @kbn/eslint/no-restricted-paths
|
||||
import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths
|
||||
import { WithAppDependencies, services, TestSubjects } from '../helpers';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
store: () => indexManagementStore(services as any),
|
||||
memoryRouter: {
|
||||
initialEntries: [`/indices?includeHidden=true`],
|
||||
componentRoutePath: `/:section(indices|templates)`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(WithAppDependencies(IndexList), testBedConfig);
|
||||
|
||||
export interface IndicesTestBed extends TestBed<TestSubjects> {
|
||||
actions: {
|
||||
selectIndexDetailsTab: (tab: 'settings' | 'mappings' | 'stats' | 'edit_settings') => void;
|
||||
getIncludeHiddenIndicesToggleStatus: () => boolean;
|
||||
clickIncludeHiddenIndicesToggle: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (): Promise<IndicesTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
/**
|
||||
* User Actions
|
||||
*/
|
||||
|
||||
const clickIncludeHiddenIndicesToggle = () => {
|
||||
const { find } = testBed;
|
||||
find('indexTableIncludeHiddenIndicesToggle').simulate('click');
|
||||
};
|
||||
|
||||
const getIncludeHiddenIndicesToggleStatus = () => {
|
||||
const { find } = testBed;
|
||||
const props = find('indexTableIncludeHiddenIndicesToggle').props();
|
||||
return Boolean(props['aria-checked']);
|
||||
};
|
||||
|
||||
const selectIndexDetailsTab = async (
|
||||
tab: 'settings' | 'mappings' | 'stats' | 'edit_settings'
|
||||
) => {
|
||||
const indexDetailsTabs = ['settings', 'mappings', 'stats', 'edit_settings'];
|
||||
const { find, component } = testBed;
|
||||
await act(async () => {
|
||||
find('detailPanelTab').at(indexDetailsTabs.indexOf(tab)).simulate('click');
|
||||
});
|
||||
component.update();
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
selectIndexDetailsTab,
|
||||
getIncludeHiddenIndicesToggleStatus,
|
||||
clickIncludeHiddenIndicesToggle,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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 { API_BASE_PATH } from '../../../common/constants';
|
||||
import { setupEnvironment, nextTick } from '../helpers';
|
||||
|
||||
import { IndicesTestBed, setup } from './indices_tab.helpers';
|
||||
|
||||
describe('<IndexManagementHome />', () => {
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: IndicesTestBed;
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('on component mount', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadIndicesResponse([]);
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
await act(async () => {
|
||||
const { component } = testBed;
|
||||
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('sets the hash query param base on include hidden indices toggle', () => {
|
||||
const { actions } = testBed;
|
||||
expect(actions.getIncludeHiddenIndicesToggleStatus()).toBe(true);
|
||||
expect(window.location.hash.includes('includeHidden=true')).toBe(true);
|
||||
actions.clickIncludeHiddenIndicesToggle();
|
||||
expect(window.location.hash.includes('includeHidden=true')).toBe(false);
|
||||
// Note: this test modifies the shared location.hash state, we put it back the way it was
|
||||
actions.clickIncludeHiddenIndicesToggle();
|
||||
expect(actions.getIncludeHiddenIndicesToggleStatus()).toBe(true);
|
||||
expect(window.location.hash.includes('includeHidden=true')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('index detail panel with % character in index name', () => {
|
||||
const indexName = 'test%';
|
||||
beforeEach(async () => {
|
||||
const index = {
|
||||
health: 'green',
|
||||
status: 'open',
|
||||
primary: 1,
|
||||
replica: 1,
|
||||
documents: 10000,
|
||||
documents_deleted: 100,
|
||||
size: '156kb',
|
||||
primary_size: '156kb',
|
||||
name: indexName,
|
||||
};
|
||||
httpRequestsMockHelpers.setLoadIndicesResponse([index]);
|
||||
|
||||
testBed = await setup();
|
||||
const { component, find } = testBed;
|
||||
|
||||
component.update();
|
||||
|
||||
find('indexTableIndexNameLink').at(0).simulate('click');
|
||||
});
|
||||
|
||||
test('should encode indexName when loading settings in detail panel', async () => {
|
||||
const { actions } = testBed;
|
||||
await actions.selectIndexDetailsTab('settings');
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
expect(latestRequest.url).toBe(`${API_BASE_PATH}/settings/${encodeURIComponent(indexName)}`);
|
||||
});
|
||||
|
||||
test('should encode indexName when loading mappings in detail panel', async () => {
|
||||
const { actions } = testBed;
|
||||
await actions.selectIndexDetailsTab('mappings');
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
expect(latestRequest.url).toBe(`${API_BASE_PATH}/mapping/${encodeURIComponent(indexName)}`);
|
||||
});
|
||||
|
||||
test('should encode indexName when loading stats in detail panel', async () => {
|
||||
const { actions } = testBed;
|
||||
await actions.selectIndexDetailsTab('stats');
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
expect(latestRequest.url).toBe(`${API_BASE_PATH}/stats/${encodeURIComponent(indexName)}`);
|
||||
});
|
||||
|
||||
test('should encode indexName when editing settings in detail panel', async () => {
|
||||
const { actions } = testBed;
|
||||
await actions.selectIndexDetailsTab('edit_settings');
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
expect(latestRequest.url).toBe(`${API_BASE_PATH}/settings/${encodeURIComponent(indexName)}`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,16 +5,16 @@
|
|||
*/
|
||||
|
||||
import { registerTestBed, TestBedConfig } from '../../../../../test_utils';
|
||||
import { BASE_PATH } from '../../../common/constants';
|
||||
import { TemplateClone } from '../../../public/application/sections/template_clone'; // eslint-disable-line @kbn/eslint/no-restricted-paths
|
||||
import { WithAppDependencies } from '../helpers';
|
||||
|
||||
import { formSetup } from './template_form.helpers';
|
||||
import { TEMPLATE_NAME } from './constants';
|
||||
import { WithAppDependencies } from './setup_environment';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
initialEntries: [`${BASE_PATH}clone_template/${TEMPLATE_NAME}`],
|
||||
componentRoutePath: `${BASE_PATH}clone_template/:name`,
|
||||
initialEntries: [`/clone_template/${TEMPLATE_NAME}`],
|
||||
componentRoutePath: `/clone_template/:name`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
|
@ -3,21 +3,16 @@
|
|||
* 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 { act } from 'react-dom/test-utils';
|
||||
|
||||
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
|
||||
import { TemplateFormTestBed } from './helpers/template_form.helpers';
|
||||
import { getTemplate } from '../../test/fixtures';
|
||||
import {
|
||||
TEMPLATE_NAME,
|
||||
INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS,
|
||||
MAPPINGS,
|
||||
} from './helpers/constants';
|
||||
import { getTemplate } from '../../../test/fixtures';
|
||||
import { setupEnvironment, nextTick } from '../helpers';
|
||||
|
||||
const { setup } = pageHelpers.templateClone;
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
import { TEMPLATE_NAME, INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS, MAPPINGS } from './constants';
|
||||
import { setup } from './template_clone.helpers';
|
||||
import { TemplateFormTestBed } from './template_form.helpers';
|
||||
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
...jest.requireActual('@elastic/eui'),
|
|
@ -5,15 +5,15 @@
|
|||
*/
|
||||
|
||||
import { registerTestBed, TestBedConfig } from '../../../../../test_utils';
|
||||
import { BASE_PATH } from '../../../common/constants';
|
||||
import { TemplateCreate } from '../../../public/application/sections/template_create'; // eslint-disable-line @kbn/eslint/no-restricted-paths
|
||||
import { WithAppDependencies } from '../helpers';
|
||||
|
||||
import { formSetup, TestSubjects } from './template_form.helpers';
|
||||
import { WithAppDependencies } from './setup_environment';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
initialEntries: [`${BASE_PATH}create_template`],
|
||||
componentRoutePath: `${BASE_PATH}create_template`,
|
||||
initialEntries: [`/create_template`],
|
||||
componentRoutePath: `/create_template`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
|
@ -3,23 +3,22 @@
|
|||
* 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 { act } from 'react-dom/test-utils';
|
||||
|
||||
import { DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../common';
|
||||
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
|
||||
import { TemplateFormTestBed } from './helpers/template_form.helpers';
|
||||
import { DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../common';
|
||||
import { setupEnvironment, nextTick } from '../helpers';
|
||||
|
||||
import {
|
||||
TEMPLATE_NAME,
|
||||
SETTINGS,
|
||||
MAPPINGS,
|
||||
ALIASES,
|
||||
INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS,
|
||||
} from './helpers/constants';
|
||||
|
||||
const { setup } = pageHelpers.templateCreate;
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
} from './constants';
|
||||
import { setup } from './template_create.helpers';
|
||||
import { TemplateFormTestBed } from './template_form.helpers';
|
||||
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
...jest.requireActual('@elastic/eui'),
|
|
@ -5,16 +5,16 @@
|
|||
*/
|
||||
|
||||
import { registerTestBed, TestBedConfig } from '../../../../../test_utils';
|
||||
import { BASE_PATH } from '../../../common/constants';
|
||||
import { TemplateEdit } from '../../../public/application/sections/template_edit'; // eslint-disable-line @kbn/eslint/no-restricted-paths
|
||||
import { WithAppDependencies } from '../helpers';
|
||||
|
||||
import { formSetup, TestSubjects } from './template_form.helpers';
|
||||
import { TEMPLATE_NAME } from './constants';
|
||||
import { WithAppDependencies } from './setup_environment';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
initialEntries: [`${BASE_PATH}edit_template/${TEMPLATE_NAME}`],
|
||||
componentRoutePath: `${BASE_PATH}edit_template/:name`,
|
||||
initialEntries: [`/edit_template/${TEMPLATE_NAME}`],
|
||||
componentRoutePath: `/edit_template/:name`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
|
@ -3,13 +3,16 @@
|
|||
* 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 { act } from 'react-dom/test-utils';
|
||||
|
||||
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
|
||||
import { TemplateFormTestBed } from './helpers/template_form.helpers';
|
||||
import * as fixtures from '../../test/fixtures';
|
||||
import { TEMPLATE_NAME, SETTINGS, ALIASES, MAPPINGS as DEFAULT_MAPPING } from './helpers/constants';
|
||||
import * as fixtures from '../../../test/fixtures';
|
||||
import { setupEnvironment, nextTick } from '../helpers';
|
||||
|
||||
import { TEMPLATE_NAME, SETTINGS, ALIASES, MAPPINGS as DEFAULT_MAPPING } from './constants';
|
||||
import { setup } from './template_edit.helpers';
|
||||
import { TemplateFormTestBed } from './template_form.helpers';
|
||||
|
||||
const UPDATED_INDEX_PATTERN = ['updatedIndexPattern'];
|
||||
const UPDATED_MAPPING_TEXT_FIELD_NAME = 'updated_text_datatype';
|
||||
|
@ -22,10 +25,6 @@ const MAPPING = {
|
|||
},
|
||||
};
|
||||
|
||||
const { setup } = pageHelpers.templateEdit;
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
...jest.requireActual('@elastic/eui'),
|
||||
// Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions,
|
|
@ -3,9 +3,10 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TestBed, SetupFunc, UnwrapPromise } from '../../../../../test_utils';
|
||||
import { TemplateDeserialized } from '../../../common';
|
||||
import { nextTick } from './index';
|
||||
import { nextTick } from '../helpers';
|
||||
|
||||
interface MappingField {
|
||||
name: string;
|
|
@ -37,8 +37,6 @@ import { findTestSubject } from '@elastic/eui/lib/test';
|
|||
/* eslint-disable @kbn/eslint/no-restricted-paths */
|
||||
import { notificationServiceMock } from '../../../../../src/core/public/notifications/notifications_service.mock';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
|
||||
|
||||
let server = null;
|
||||
|
|
|
@ -1,45 +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 default {
|
||||
edit: () => {
|
||||
return {
|
||||
navigateFileEnd() {},
|
||||
destroy() {},
|
||||
acequire() {
|
||||
return {
|
||||
setCompleters() {},
|
||||
};
|
||||
},
|
||||
setValue() {},
|
||||
setOptions() {},
|
||||
setTheme() {},
|
||||
setFontSize() {},
|
||||
setShowPrintMargin() {},
|
||||
getSession() {
|
||||
return {
|
||||
setUseWrapMode() {},
|
||||
setMode() {},
|
||||
setValue() {},
|
||||
on() {},
|
||||
};
|
||||
},
|
||||
renderer: {
|
||||
setShowGutter() {},
|
||||
setScrollMargin() {},
|
||||
},
|
||||
setBehavioursEnabled() {},
|
||||
};
|
||||
},
|
||||
acequire() {
|
||||
return {
|
||||
setCompleters() {},
|
||||
};
|
||||
},
|
||||
setCompleters() {
|
||||
return [{}];
|
||||
},
|
||||
};
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const settingsDocumentationLink = 'https://stuff.com/docs';
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const toastNotifications = {
|
||||
addInfo: () => {},
|
||||
addSuccess: () => {},
|
||||
addDanger: () => {},
|
||||
addWarning: () => {},
|
||||
addError: () => {},
|
||||
};
|
||||
|
||||
export function fatalError() {}
|
|
@ -4,7 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { flattenObject } from '../../public/application/lib/flatten_object';
|
||||
import { flattenObject } from './flatten_object';
|
||||
|
||||
describe('flatten_object', () => {
|
||||
test('it flattens an object', () => {
|
||||
const obj = {
|
||||
|
@ -17,6 +18,7 @@ describe('flatten_object', () => {
|
|||
};
|
||||
expect(flattenObject(obj)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it flattens an object that contains an array in a field', () => {
|
||||
const obj = {
|
||||
foo: {
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './index_list';
|
||||
export { IndexList } from './index_list';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue