Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Index details page] Implement mappings tab #165038

Merged
merged 8 commits into from
Aug 29, 2023
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
Loading