Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,11 @@ const registerHttpRequestMockHelpers = (
const setLoadIndexSettingsResponse = (response?: HttpResponse, error?: ResponseError) =>
mockResponse('GET', `${API_BASE_PATH}/settings/:name`, response, error);

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

const setLoadIndexStatsResponse = (response?: HttpResponse, error?: ResponseError) =>
mockResponse('GET', `${API_BASE_PATH}/stats/:name`, response, error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export interface IndexDetailsPageTestBed extends TestBed {
getHeader: () => string;
clickIndexDetailsTab: (tab: IndexDetailsSection) => Promise<void>;
getActiveTabContent: () => string;
mappings: {
getCodeBlockContent: () => string;
getDocsLinkHref: () => string;
isErrorDisplayed: () => boolean;
clickErrorReloadButton: () => Promise<void>;
};
clickBackToIndicesButton: () => Promise<void>;
discoverLinkExists: () => boolean;
contextMenu: {
Expand Down Expand Up @@ -91,6 +97,24 @@ export const setup = async (
return find('indexDetailsContent').text();
};

const mappings = {
getCodeBlockContent: () => {
return find('indexDetailsMappingsCodeBlock').text();
},
getDocsLinkHref: () => {
return find('indexDetailsMappingsDocsLink').prop('href');
},
isErrorDisplayed: () => {
return exists('indexDetailsMappingsError');
},
clickErrorReloadButton: async () => {
await act(async () => {
find('indexDetailsMappingsReloadButton').simulate('click');
});
component.update();
},
};

const clickBackToIndicesButton = async () => {
await act(async () => {
find('indexDetailsBackToIndicesButton').simulate('click');
Expand Down Expand Up @@ -142,6 +166,7 @@ export const setup = async (
getHeader,
clickIndexDetailsTab,
getActiveTabContent,
mappings,
clickBackToIndicesButton,
discoverLinkExists,
contextMenu,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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 { testIndexMock, testIndexName } from './mocks';
import { testIndexMappings, testIndexMock, testIndexName } from './mocks';
import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common';

describe('<IndexDetailsPage />', () => {
Expand All @@ -22,6 +22,7 @@ describe('<IndexDetailsPage />', () => {
({ httpSetup, httpRequestsMockHelpers } = mockEnvironment);
// testIndexName is configured in initialEntries of the memory router
httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, testIndexMock);
httpRequestsMockHelpers.setLoadIndexMappingResponse(testIndexName, testIndexMappings);

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

it('mappings tab', async () => {
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Mappings);
const tabContent = testBed.actions.getActiveTabContent();
expect(tabContent).toEqual('Mappings');
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}`, {
asSystemRequest: undefined,
body: undefined,
query: undefined,
version: undefined,
});
});

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

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

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

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

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

it('there is an error prompt', async () => {
expect(testBed.actions.mappings.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.mappings.clickErrorReloadButton();
expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1);
});
});
});

it('settings tab', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,15 @@ export const testIndexMock: Index = {
},
isFollowerIndex: false,
};

export const testIndexMappings = {
mappings: {
dynamic: 'false',
dynamic_templates: [],
properties: {
'@timestamp': {
type: 'date',
},
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import {
} from '@elastic/eui';
import { SectionLoading } from '@kbn/es-ui-shared-plugin/public';

import { css } from '@emotion/react';
import { Index } from '../../../../../../common';
import { loadIndex } from '../../../../services';
import { DiscoverLink } from '../../../../lib/discover_link';
import { Section } from '../../home';
import { DetailsPageError } from './details_page_error';
import { ManageIndexButton } from './manage_index_button';
import { DetailsPageMappings } from './details_page_mappings';

export enum IndexDetailsSection {
Overview = 'overview',
Expand Down Expand Up @@ -164,7 +166,12 @@ export const DetailsPage: React.FunctionComponent<

<EuiSpacer size="l" />

<div data-test-subj={`indexDetailsContent`}>
<div
data-test-subj={`indexDetailsContent`}
css={css`
height: 100%;
`}
>
<Routes>
<Route
path={`/${Section.Indices}/${indexName}/${IndexDetailsSection.Overview}`}
Expand All @@ -175,8 +182,8 @@ export const DetailsPage: React.FunctionComponent<
render={() => <div>Documents</div>}
/>
<Route
path={`/${Section.Indices}/${indexName}/${IndexDetailsSection.Mappings}`}
render={() => <div>Mappings</div>}
path={`/${Section.Indices}/:indexName/${IndexDetailsSection.Mappings}`}
component={DetailsPageMappings}
/>
<Route
path={`/${Section.Indices}/${indexName}/${IndexDetailsSection.Settings}`}
Expand All @@ -192,11 +199,6 @@ export const DetailsPage: React.FunctionComponent<
/>
</Routes>
</div>

<EuiSpacer size="l" />
<div>
<pre>{JSON.stringify(index, null, 2)}</pre>
</div>
</>
);
};
Original file line number Diff line number Diff line change
@@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { FunctionComponent } from 'react';
import {
EuiButton,
EuiCodeBlock,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiPageTemplate,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n-react';
import { SectionLoading } from '@kbn/es-ui-shared-plugin/public';
import { useLoadIndexMappings, documentationService } from '../../../../services';

export const DetailsPageMappings: FunctionComponent<RouteComponentProps<{ indexName: string }>> = ({
match: {
params: { indexName },
},
}) => {
const { isLoading, data, error, resendRequest } = useLoadIndexMappings(indexName);

if (isLoading) {
return (
<SectionLoading>
<FormattedMessage
id="xpack.idxMgmt.indexDetails.mappings.loadingDescription"
defaultMessage="Loading index mappings…"
/>
</SectionLoading>
);
}
if (error) {
return (
<EuiPageTemplate.EmptyPrompt
data-test-subj="indexDetailsMappingsError"
color="danger"
iconType="warning"
title={
<h2>
<FormattedMessage
id="xpack.idxMgmt.indexDetails.mappings.errorTitle"
defaultMessage="Unable to load index mappings"
/>
</h2>
}
body={
<>
<EuiText color="subdued">
<FormattedMessage
id="xpack.idxMgmt.indexDetails.mappings.errorDescription"
defaultMessage="There was an error loading mappings for index {indexName}: {error}"
values={{
indexName,
error: error.error,
}}
/>
</EuiText>
<EuiSpacer />
<EuiButton
iconSide="right"
onClick={resendRequest}
iconType="refresh"
color="danger"
data-test-subj="indexDetailsMappingsReloadButton"
>
<FormattedMessage
id="xpack.idxMgmt.indexDetails.mappings.reloadButtonLabel"
defaultMessage="Reload"
/>
</EuiButton>
</>
}
/>
);
}

return (
// using "rowReverse" to keep docs links on the top of the mappings code block on smaller screen
<EuiFlexGroup
wrap
direction="rowReverse"
css={css`
height: 100%;
`}
>
<EuiFlexItem
grow={1}
css={css`
min-width: 400px;
`}
>
<EuiPanel grow={false} paddingSize="l">
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiIcon type="iInCircle" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xs">
<h2>
<FormattedMessage
id="xpack.idxMgmt.indexDetails.mappings.docsCardTitle"
defaultMessage="About index mappings"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiText>
<p>
<FormattedMessage
id="xpack.idxMgmt.indexDetails.mappings.docsCardDescription"
defaultMessage="Your documents are made up of a set of fields. Index mappings give each field a type
(such as keyword, number, or date) and additional subfields. These index mappings determine the functions
available in your relevance tuning and search experience."
/>
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiLink
data-test-subj="indexDetailsMappingsDocsLink"
href={documentationService.getMappingDocumentationLink()}
target="_blank"
external
>
<FormattedMessage
id="xpack.idxMgmt.indexDetails.mappings.docsCardLink"
defaultMessage="Learn more"
/>
</EuiLink>
</EuiPanel>
</EuiFlexItem>

<EuiFlexItem
grow={3}
css={css`
min-width: 600px;
`}
>
<EuiPanel>
<EuiCodeBlock
language="json"
isCopyable
data-test-subj="indexDetailsMappingsCodeBlock"
css={css`
height: 100%;
`}
>
{JSON.stringify(data, null, 2)}
</EuiCodeBlock>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,14 @@ export function useLoadNodesPlugins() {

export function loadIndex(indexName: string) {
return sendRequest<Index>({
path: `${INTERNAL_API_BASE_PATH}/indices/${indexName}`,
path: `${INTERNAL_API_BASE_PATH}/indices/${encodeURIComponent(indexName)}`,
method: 'get',
});
}

export function useLoadIndexMappings(indexName: string) {
return useRequest({
path: `${API_BASE_PATH}/mapping/${encodeURIComponent(indexName)}`,
method: 'get',
});
}
Loading