Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,11 @@ const registerHttpRequestMockHelpers = (
const setCreateTemplateResponse = (response?: HttpResponse, error?: ResponseError) =>
mockResponse('POST', `${API_BASE_PATH}/index_templates`, response, error);

const setLoadIndexSettingsResponse = (response?: HttpResponse, error?: ResponseError) =>
mockResponse('GET', `${API_BASE_PATH}/settings/:name`, response, error);
const setLoadIndexSettingsResponse = (
indexName: string,
response?: HttpResponse,
error?: ResponseError
) => mockResponse('GET', `${API_BASE_PATH}/settings/${indexName}`, response, error);

const setLoadIndexMappingResponse = (
indexName: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ export interface IndexDetailsPageTestBed extends TestBed {
isErrorDisplayed: () => boolean;
clickErrorReloadButton: () => Promise<void>;
};
settings: {
getCodeBlockContent: () => string;
getDocsLinkHref: () => string;
isErrorDisplayed: () => boolean;
clickErrorReloadButton: () => Promise<void>;
clickEditModeSwitch: () => Promise<void>;
getCodeEditorContent: () => string;
updateCodeEditorContent: (value: string) => Promise<void>;
saveSettings: () => Promise<void>;
resetChanges: () => Promise<void>;
};
clickBackToIndicesButton: () => Promise<void>;
discoverLinkExists: () => boolean;
contextMenu: {
Expand Down Expand Up @@ -123,6 +134,53 @@ export const setup = async (
},
};

const settings = {
getCodeBlockContent: () => {
return find('indexDetailsSettingsCodeBlock').text();
},
getDocsLinkHref: () => {
return find('indexDetailsSettingsDocsLink').prop('href');
},
isErrorDisplayed: () => {
return exists('indexDetailsSettingsError');
},
clickErrorReloadButton: async () => {
await act(async () => {
find('indexDetailsSettingsReloadButton').simulate('click');
});
component.update();
},
clickEditModeSwitch: async () => {
await act(async () => {
find('indexDetailsSettingsEditModeSwitch').simulate('click');
});
component.update();
},
getCodeEditorContent: () => {
return find('indexDetailsSettingsEditor').prop('data-currentvalue');
},
updateCodeEditorContent: async (value: string) => {
// the code editor is mocked as an input so need to set data-currentvalue attribute to change the value
find('indexDetailsSettingsEditor').getDOMNode().setAttribute('data-currentvalue', value);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth adding a comment here indicating this is needed for the mock?

await act(async () => {
find('indexDetailsSettingsEditor').simulate('change');
});
component.update();
},
saveSettings: async () => {
await act(async () => {
find('indexDetailsSettingsSave').simulate('click');
});
component.update();
},
resetChanges: async () => {
await act(async () => {
find('indexDetailsSettingsResetChanges').simulate('click');
});
component.update();
},
};

const clickBackToIndicesButton = async () => {
await act(async () => {
find('indexDetailsBackToIndicesButton').simulate('click');
Expand Down Expand Up @@ -199,6 +257,7 @@ export const setup = async (
clickIndexDetailsTab,
getActiveTabContent,
mappings,
settings,
clickBackToIndicesButton,
discoverLinkExists,
contextMenu,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,33 @@ import { setupEnvironment } from '../helpers';
import { IndexDetailsPageTestBed, setup } from './index_details_page.helpers';
import { act } from 'react-dom/test-utils';
import { IndexDetailsSection } from '../../../public/application/sections/home/index_list/details_page';
import { testIndexMappings, testIndexMock, testIndexName, testIndexStats } from './mocks';
import {
testIndexEditableSettings,
testIndexMappings,
testIndexMock,
testIndexName,
testIndexSettings,
testIndexStats,
} from './mocks';
import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common';
import React from 'react';

jest.mock('@kbn/kibana-react-plugin/public', () => {
const original = jest.requireActual('@kbn/kibana-react-plugin/public');
return {
...original,
// Mocking CodeEditor, which uses React Monaco under the hood
CodeEditor: (props: any) => (
<input
data-test-subj={props['data-test-subj'] || 'mockCodeEditor'}
data-currentvalue={props.value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
props.onChange(e.currentTarget.getAttribute('data-currentvalue'));
}}
/>
),
};
});

describe('<IndexDetailsPage />', () => {
let testBed: IndexDetailsPageTestBed;
Expand All @@ -24,6 +49,7 @@ describe('<IndexDetailsPage />', () => {
httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, testIndexMock);
httpRequestsMockHelpers.setLoadIndexStatsResponse(testIndexName, testIndexStats);
httpRequestsMockHelpers.setLoadIndexMappingResponse(testIndexName, testIndexMappings);
httpRequestsMockHelpers.setLoadIndexSettingsResponse(testIndexName, testIndexSettings);

await act(async () => {
testBed = await setup(httpSetup, {
Expand Down Expand Up @@ -171,7 +197,7 @@ describe('<IndexDetailsPage />', () => {
expect(tabContent).toEqual('Documents');
});

describe('mappings tab', () => {
describe('Mappings tab', () => {
it('loads mappings from the API', async () => {
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Mappings);
expect(httpSetup.get).toHaveBeenLastCalledWith(`${API_BASE_PATH}/mapping/${testIndexName}`, {
Expand All @@ -189,7 +215,7 @@ describe('<IndexDetailsPage />', () => {
expect(tabContent).toEqual(JSON.stringify(testIndexMappings, null, 2));
});

it('sets the docs link href from the documenation service', async () => {
it('sets the docs link href from the documentation service', async () => {
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Mappings);
const docsLinkHref = testBed.actions.mappings.getDocsLinkHref();
// the url from the mocked docs mock
Expand Down Expand Up @@ -226,10 +252,112 @@ describe('<IndexDetailsPage />', () => {
});
});

it('settings tab', async () => {
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Settings);
const tabContent = testBed.actions.getActiveTabContent();
expect(tabContent).toEqual('Settings');
describe('Settings tab', () => {
it('loads settings from the API', async () => {
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Settings);
expect(httpSetup.get).toHaveBeenLastCalledWith(`${API_BASE_PATH}/settings/${testIndexName}`, {
asSystemRequest: undefined,
body: undefined,
query: undefined,
version: undefined,
});
});

it('displays the settings in the code block', async () => {
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Settings);

const tabContent = testBed.actions.settings.getCodeBlockContent();
expect(tabContent).toEqual(JSON.stringify(testIndexSettings, null, 2));
});

it('sets the docs link href from the documentation service', async () => {
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Settings);
const docsLinkHref = testBed.actions.settings.getDocsLinkHref();
// the url from the mocked docs mock
expect(docsLinkHref).toEqual(
'https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/index-modules.html#index-modules-settings'
);
});

describe('error loading settings', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadIndexSettingsResponse(testIndexName, undefined, {
statusCode: 400,
message: `Was not able to load settings`,
});
await act(async () => {
testBed = await setup(httpSetup);
});

testBed.component.update();
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Settings);
});

it('there is an error prompt', async () => {
expect(testBed.actions.settings.isErrorDisplayed()).toBe(true);
});

it('resends a request when reload button is clicked', async () => {
// already sent 3 requests while setting up the component
const numberOfRequests = 3;
expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests);
await testBed.actions.settings.clickErrorReloadButton();
expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1);
});
});

describe('edit settings', () => {
beforeEach(async () => {
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Settings);
await testBed.actions.settings.clickEditModeSwitch();
});

it('displays the editable settings (flattened and filtered)', () => {
const editorContent = testBed.actions.settings.getCodeEditorContent();
expect(editorContent).toEqual(JSON.stringify(testIndexEditableSettings, null, 2));
});

it('updates the settings', async () => {
const updatedSettings = { ...testIndexEditableSettings, 'index.priority': '2' };
await testBed.actions.settings.updateCodeEditorContent(JSON.stringify(updatedSettings));
await testBed.actions.settings.saveSettings();
expect(httpSetup.put).toHaveBeenLastCalledWith(
`${API_BASE_PATH}/settings/${testIndexName}`,
{
asSystemRequest: undefined,
body: JSON.stringify({ 'index.priority': '2' }),
query: undefined,
version: undefined,
}
);
});

it('reloads the settings after an update', async () => {
const numberOfRequests = 2;
expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests);
const updatedSettings = { ...testIndexEditableSettings, 'index.priority': '2' };
await testBed.actions.settings.updateCodeEditorContent(JSON.stringify(updatedSettings));
await testBed.actions.settings.saveSettings();
expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1);
expect(httpSetup.get).toHaveBeenLastCalledWith(
`${API_BASE_PATH}/settings/${testIndexName}`,
{
asSystemRequest: undefined,
body: undefined,
query: undefined,
version: undefined,
}
);
});

it('resets the changes in the editor', async () => {
const updatedSettings = { ...testIndexEditableSettings, 'index.priority': '2' };
await testBed.actions.settings.updateCodeEditorContent(JSON.stringify(updatedSettings));
await testBed.actions.settings.resetChanges();
const editorContent = testBed.actions.settings.getCodeEditorContent();
expect(editorContent).toEqual(JSON.stringify(testIndexEditableSettings, null, 2));
});
});
});

it('pipelines tab', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,37 @@ export const testIndexMappings = {
},
};

// Mocking partial index settings response
export const testIndexSettings = {
settings: {
index: {
routing: {
allocation: {
include: {
_tier_preference: 'data_content',
},
},
},
number_of_shards: '1',
},
},
defaults: {
index: {
flush_after_merge: '512mb',
max_script_fields: '32',
query: {
default_field: ['*'],
},
priority: '1',
},
},
};
export const testIndexEditableSettings = {
'index.priority': '1',
'index.query.default_field': ['*'],
'index.routing.allocation.include._tier_preference': 'data_content',
};

// Mocking partial index stats response
export const testIndexStats = {
_shards: {
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/index_management/common/types/indices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,8 @@ export interface Index {
primary_size?: string;
documents_deleted?: number;
}

export interface IndexSettingsResponse {
settings: IndexSettings;
defaults: IndexSettings;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

import _ from 'lodash';

export const flattenObject = (nestedObj, flattenArrays) => {
const stack = []; // track key stack
const flatObj = {};
export const flattenObject = (nestedObj: any) => {
const stack = [] as any[]; // track key stack
const flatObj = {} as any;
const dot = '.';
(function flattenObj(obj) {
_.keys(obj).forEach(function (key) {
stack.push(key);
if (!flattenArrays && Array.isArray(obj[key])) flatObj[stack.join(dot)] = obj[key];
if (Array.isArray(obj[key])) flatObj[stack.join(dot)] = obj[key];
else if (_.isObject(obj[key])) flattenObj(obj[key]);
else flatObj[stack.join(dot)] = obj[key];
stack.pop();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { SectionLoading } from '@kbn/es-ui-shared-plugin/public';

import { Index } from '../../../../../../common';
import { INDEX_OPEN } from '../../../../../../common/constants';
import { Error } from '../../../../../shared_imports';
import { loadIndex } from '../../../../services';
import { useAppContext } from '../../../../app_context';
import { DiscoverLink } from '../../../../lib/discover_link';
Expand All @@ -29,6 +30,7 @@ import { DetailsPageError } from './details_page_error';
import { ManageIndexButton } from './manage_index_button';
import { DetailsPageStats } from './details_page_stats';
import { DetailsPageMappings } from './details_page_mappings';
import { DetailsPageSettings } from './details_page_settings';

export enum IndexDetailsSection {
Overview = 'overview',
Expand Down Expand Up @@ -86,7 +88,7 @@ export const DetailsPage: React.FunctionComponent<
}) => {
const { config } = useAppContext();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const [error, setError] = useState<Error | null>(null);
const [index, setIndex] = useState<Index | null>();

const fetchIndexDetails = useCallback(async () => {
Expand Down Expand Up @@ -198,8 +200,10 @@ export const DetailsPage: React.FunctionComponent<
component={DetailsPageMappings}
/>
<Route
path={`/${Section.Indices}/${indexName}/${IndexDetailsSection.Settings}`}
render={() => <div>Settings</div>}
path={`/${Section.Indices}/:indexName/${IndexDetailsSection.Settings}`}
render={(props: RouteComponentProps<{ indexName: string }>) => (
<DetailsPageSettings {...props} isIndexOpen={index.status === INDEX_OPEN} />
)}
/>
<Route
path={`/${Section.Indices}/${indexName}/${IndexDetailsSection.Pipelines}`}
Expand Down
Loading