mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Index Management] Implement index overview cards (#168153)
## Summary This PR implements a new design for the Overview tab on the index details page. There are 4 cards, but only 3 can be displayed at one time: Storage, Status and Aliases/Data Stream (a backing index of the data stream can't have any aliases). On Serverless, there is no Storage and Status cards, so the whole section is either empty or 1 card is displayed for Aliases or Data Stream. ### Screenshots #### (Stateful) Index without aliases and data stream <img width="1335" alt="Screenshot 2023-10-27 at 17 25 56" src="38e4f96b
-d612-42a7-9d2d-431d3ff4cd7b"> #### (Stateful) Index with aliases <img width="1332" alt="Screenshot 2023-10-27 at 17 27 05" src="4a3b03ec
-8da8-47dd-b343-ba83ddfe5efd"> Aliases flyout <img width="645" alt="Screenshot 2023-10-27 at 17 27 39" src="4f4770dd
-ce3e-4bf7-9e50-d2a58645ea0b"> #### (Stateful) Backing index of a data stream <img width="1340" alt="Screenshot 2023-10-27 at 17 28 16" src="434e3aec
-06bb-479f-be2e-37d3d2b74951"> #### (Serverless) Index without cards <img width="1264" alt="Screenshot 2023-10-09 at 16 16 04" src="5b40f61e
-0e14-47ee-9ac5-a91ffe14775b"> #### (Serverless) Index with aliases <img width="1260" alt="Screenshot 2023-10-09 at 16 16 34" src="193b6a6f
-8caa-4e77-9ec8-31c133584e75"> #### (Serverless) Backing index of a data stream <img width="1598" alt="Screenshot 2023-10-24 at 17 29 53" src="602c09fb
-1b85-4d12-88b2-bf9eb78639b4"> ### Screen recordings #### (Stateful) Index with aliasesc44dc6a7
-dc2b-4297-88ae-1ecc22f828da #### (Stateful) Backing index of a data stream5206e427
-b373-4ec8-a2ca-253b125afaee ### How to test - Start either a stateful or serverless ES and Kibana: `yarn es snapshot` and `yarn start` or `yarn es serverless --ssl` and `yarn serverless-es --ssl` - Use following commands in Dev Tools Console to create an index and add some aliases ``` PUT /test_index POST _aliases { "actions": [ { "add": { "index": "test_index", "alias": "test_alias_1" } } ] } ``` - Use following commdans in Dev Tools to create a data stream ``` PUT _index_template/test-index-template { "index_patterns": ["test-data-stream*"], "data_stream": { }, "priority": 500 } PUT _data_stream/test-data-stream ``` ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Ignacio Rivas <rivasign@gmail.com>
This commit is contained in:
parent
58adee01a0
commit
f4e3807940
12 changed files with 796 additions and 108 deletions
|
@ -78,8 +78,15 @@ export interface IndexDetailsPageTestBed extends TestBed {
|
|||
isWarningDisplayed: () => boolean;
|
||||
};
|
||||
overview: {
|
||||
indexStatsContentExists: () => boolean;
|
||||
indexDetailsContentExists: () => boolean;
|
||||
storageDetailsExist: () => boolean;
|
||||
getStorageDetailsContent: () => string;
|
||||
statusDetailsExist: () => boolean;
|
||||
getStatusDetailsContent: () => string;
|
||||
aliasesDetailsExist: () => boolean;
|
||||
getAliasesDetailsContent: () => string;
|
||||
dataStreamDetailsExist: () => boolean;
|
||||
getDataStreamDetailsContent: () => string;
|
||||
reloadDataStreamDetails: () => Promise<void>;
|
||||
addDocCodeBlockExists: () => boolean;
|
||||
extensionSummaryExists: (index: number) => boolean;
|
||||
};
|
||||
|
@ -138,11 +145,35 @@ export const setup = async ({
|
|||
};
|
||||
|
||||
const overview = {
|
||||
indexStatsContentExists: () => {
|
||||
return exists('overviewTabIndexStats');
|
||||
storageDetailsExist: () => {
|
||||
return exists('indexDetailsStorage');
|
||||
},
|
||||
indexDetailsContentExists: () => {
|
||||
return exists('overviewTabIndexDetails');
|
||||
getStorageDetailsContent: () => {
|
||||
return find('indexDetailsStorage').text();
|
||||
},
|
||||
statusDetailsExist: () => {
|
||||
return exists('indexDetailsStatus');
|
||||
},
|
||||
getStatusDetailsContent: () => {
|
||||
return find('indexDetailsStatus').text();
|
||||
},
|
||||
aliasesDetailsExist: () => {
|
||||
return exists('indexDetailsAliases');
|
||||
},
|
||||
getAliasesDetailsContent: () => {
|
||||
return find('indexDetailsAliases').text();
|
||||
},
|
||||
dataStreamDetailsExist: () => {
|
||||
return exists('indexDetailsDataStream');
|
||||
},
|
||||
getDataStreamDetailsContent: () => {
|
||||
return find('indexDetailsDataStream').text();
|
||||
},
|
||||
reloadDataStreamDetails: async () => {
|
||||
await act(async () => {
|
||||
find('indexDetailsDataStreamReload').simulate('click');
|
||||
});
|
||||
component.update();
|
||||
},
|
||||
addDocCodeBlockExists: () => {
|
||||
return exists('codeBlockControlsPanel');
|
||||
|
|
|
@ -10,16 +10,20 @@ import { IndexDetailsPageTestBed, setup } from './index_details_page.helpers';
|
|||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
IndexDetailsSection,
|
||||
IndexDetailsTab,
|
||||
IndexDetailsTabIds,
|
||||
} from '../../../common/constants';
|
||||
import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common';
|
||||
import { API_BASE_PATH, Index, INTERNAL_API_BASE_PATH } from '../../../common';
|
||||
|
||||
import {
|
||||
breadcrumbService,
|
||||
IndexManagementBreadcrumb,
|
||||
} from '../../../public/application/services/breadcrumbs';
|
||||
import { humanizeTimeStamp } from '../../../public/application/sections/home/data_stream_list/humanize_time_stamp';
|
||||
import { createDataStreamPayload } from '../home/data_streams_tab.helpers';
|
||||
import {
|
||||
testIndexEditableSettingsAll,
|
||||
testIndexEditableSettingsLimited,
|
||||
|
@ -234,13 +238,149 @@ describe('<IndexDetailsPage />', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('renders index details', () => {
|
||||
expect(testBed.actions.overview.indexDetailsContentExists()).toBe(true);
|
||||
expect(testBed.actions.overview.indexStatsContentExists()).toBe(true);
|
||||
expect(testBed.actions.overview.addDocCodeBlockExists()).toBe(true);
|
||||
it('renders storage details', () => {
|
||||
const storageDetails = testBed.actions.overview.getStorageDetailsContent();
|
||||
expect(storageDetails).toBe(
|
||||
`Storage${testIndexMock.primary_size}Primary${testIndexMock.size}TotalShards${testIndexMock.primary} Primary / ${testIndexMock.replica} Replicas `
|
||||
);
|
||||
});
|
||||
|
||||
it('hides index stats from detail panels if enableIndexStats===false', async () => {
|
||||
it('renders status details', () => {
|
||||
const statusDetails = testBed.actions.overview.getStatusDetailsContent();
|
||||
expect(statusDetails).toBe(
|
||||
`Status${'Open'}${'Healthy'}${testIndexMock.documents} Document / ${
|
||||
testIndexMock.documents_deleted
|
||||
} Deleted`
|
||||
);
|
||||
});
|
||||
|
||||
describe('aliases', () => {
|
||||
it('not rendered when no aliases', async () => {
|
||||
const aliasesExist = testBed.actions.overview.aliasesDetailsExist();
|
||||
expect(aliasesExist).toBe(false);
|
||||
});
|
||||
|
||||
it('renders less than 3 aliases', async () => {
|
||||
const aliases = ['test_alias1', 'test_alias2'];
|
||||
const testWith2Aliases = {
|
||||
...testIndexMock,
|
||||
aliases,
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, testWith2Aliases);
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup({ httpSetup });
|
||||
});
|
||||
testBed.component.update();
|
||||
|
||||
const aliasesExist = testBed.actions.overview.aliasesDetailsExist();
|
||||
expect(aliasesExist).toBe(true);
|
||||
|
||||
const aliasesContent = testBed.actions.overview.getAliasesDetailsContent();
|
||||
expect(aliasesContent).toBe(
|
||||
`Aliases${aliases.length}AliasesView all aliases${aliases.join('')}`
|
||||
);
|
||||
});
|
||||
|
||||
it('renders more than 3 aliases', async () => {
|
||||
const aliases = ['test_alias1', 'test_alias2', 'test_alias3', 'test_alias4', 'test_alias5'];
|
||||
const testWith5Aliases = {
|
||||
...testIndexMock,
|
||||
aliases,
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, testWith5Aliases);
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup({ httpSetup });
|
||||
});
|
||||
testBed.component.update();
|
||||
|
||||
const aliasesExist = testBed.actions.overview.aliasesDetailsExist();
|
||||
expect(aliasesExist).toBe(true);
|
||||
|
||||
const aliasesContent = testBed.actions.overview.getAliasesDetailsContent();
|
||||
expect(aliasesContent).toBe(
|
||||
`Aliases${aliases.length}AliasesView all aliases${aliases.slice(0, 3).join('')}+${2}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data stream', () => {
|
||||
it('not rendered when no data stream', async () => {
|
||||
const aliasesExist = testBed.actions.overview.dataStreamDetailsExist();
|
||||
expect(aliasesExist).toBe(false);
|
||||
});
|
||||
|
||||
it('renders data stream details', async () => {
|
||||
const dataStreamName = 'test_data_stream';
|
||||
const testWithDataStream: Index = {
|
||||
...testIndexMock,
|
||||
data_stream: dataStreamName,
|
||||
};
|
||||
const dataStreamDetails = createDataStreamPayload({
|
||||
name: dataStreamName,
|
||||
generation: 5,
|
||||
maxTimeStamp: 1696600607689,
|
||||
});
|
||||
|
||||
httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, testWithDataStream);
|
||||
httpRequestsMockHelpers.setLoadDataStreamResponse(dataStreamName, dataStreamDetails);
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup({ httpSetup });
|
||||
});
|
||||
testBed.component.update();
|
||||
|
||||
const dataStreamDetailsExist = testBed.actions.overview.dataStreamDetailsExist();
|
||||
expect(dataStreamDetailsExist).toBe(true);
|
||||
|
||||
const dataStreamContent = testBed.actions.overview.getDataStreamDetailsContent();
|
||||
expect(dataStreamContent).toBe(
|
||||
`Data stream${
|
||||
dataStreamDetails.generation
|
||||
}GenerationsSee detailsRelated templateLast update${humanizeTimeStamp(
|
||||
dataStreamDetails.maxTimeStamp!
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('renders error message if the request fails', async () => {
|
||||
const dataStreamName = 'test_data_stream';
|
||||
const testWithDataStream: Index = {
|
||||
...testIndexMock,
|
||||
data_stream: dataStreamName,
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, testWithDataStream);
|
||||
httpRequestsMockHelpers.setLoadDataStreamResponse(dataStreamName, undefined, {
|
||||
statusCode: 400,
|
||||
message: `Unable to load data stream details`,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup({ httpSetup });
|
||||
});
|
||||
testBed.component.update();
|
||||
|
||||
const dataStreamDetailsExist = testBed.actions.overview.dataStreamDetailsExist();
|
||||
expect(dataStreamDetailsExist).toBe(true);
|
||||
|
||||
const dataStreamContent = testBed.actions.overview.getDataStreamDetailsContent();
|
||||
expect(dataStreamContent).toBe(
|
||||
`Data streamUnable to load data stream detailsReloadLast update`
|
||||
);
|
||||
|
||||
// already sent 3 requests while setting up the component
|
||||
const numberOfRequests = 3;
|
||||
expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests);
|
||||
await testBed.actions.overview.reloadDataStreamDetails();
|
||||
expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('hides storage and status details if enableIndexStats===false', async () => {
|
||||
await act(async () => {
|
||||
testBed = await setup({
|
||||
httpSetup,
|
||||
|
@ -251,8 +391,12 @@ describe('<IndexDetailsPage />', () => {
|
|||
});
|
||||
testBed.component.update();
|
||||
|
||||
expect(testBed.actions.overview.indexDetailsContentExists()).toBe(true);
|
||||
expect(testBed.actions.overview.indexStatsContentExists()).toBe(false);
|
||||
expect(testBed.actions.overview.statusDetailsExist()).toBe(false);
|
||||
expect(testBed.actions.overview.storageDetailsExist()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders code block', () => {
|
||||
expect(testBed.actions.overview.addDocCodeBlockExists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('extension service summary', () => {
|
||||
|
|
|
@ -14,10 +14,10 @@ export const testIndexMock: Index = {
|
|||
name: testIndexName,
|
||||
uuid: 'test1234',
|
||||
primary: '1',
|
||||
replica: '1',
|
||||
replica: '2',
|
||||
documents: 1,
|
||||
documents_deleted: 0,
|
||||
size: '10kb',
|
||||
size: '20kb',
|
||||
primary_size: '10kb',
|
||||
isFrozen: false,
|
||||
aliases: 'none',
|
||||
|
|
|
@ -254,6 +254,10 @@ export const DetailsPage: FunctionComponent<
|
|||
navigateToAllIndices={navigateToAllIndices}
|
||||
/>,
|
||||
]}
|
||||
rightSideGroupProps={{
|
||||
wrap: false,
|
||||
}}
|
||||
responsive="reverse"
|
||||
tabs={headerTabs}
|
||||
/>
|
||||
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiBadgeGroup,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiListGroup,
|
||||
EuiListGroupItem,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { Index } from '../../../../../../../common';
|
||||
import { OverviewCard } from './overview_card';
|
||||
|
||||
const MAX_VISIBLE_ALIASES = 3;
|
||||
|
||||
export const AliasesDetails: FunctionComponent<{ aliases: Index['aliases'] }> = ({ aliases }) => {
|
||||
const [isShowingAliases, setIsShowingAliases] = useState<boolean>(false);
|
||||
if (!Array.isArray(aliases)) {
|
||||
return null;
|
||||
}
|
||||
const aliasesBadges = aliases.slice(0, MAX_VISIBLE_ALIASES).map((alias) => (
|
||||
<EuiBadge
|
||||
css={css`
|
||||
max-width: 250px;
|
||||
`}
|
||||
>
|
||||
{alias}
|
||||
</EuiBadge>
|
||||
));
|
||||
return (
|
||||
<>
|
||||
<OverviewCard
|
||||
data-test-subj="indexDetailsAliases"
|
||||
title={i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.aliases.cardTitle', {
|
||||
defaultMessage: 'Aliases',
|
||||
})}
|
||||
content={{
|
||||
left: (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="baseline">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
css={css`
|
||||
font-size: ${euiThemeVars.euiFontSizeL};
|
||||
`}
|
||||
>
|
||||
{aliases.length}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.overviewTab.aliases.aliasesCountLabel',
|
||||
{
|
||||
defaultMessage: '{aliases, plural, one {Alias} other {Aliases}}',
|
||||
values: { aliases: aliases.length },
|
||||
}
|
||||
)}
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
right: (
|
||||
<EuiButton
|
||||
size="s"
|
||||
onClick={() => {
|
||||
setIsShowingAliases(true);
|
||||
}}
|
||||
>
|
||||
View all aliases
|
||||
</EuiButton>
|
||||
),
|
||||
}}
|
||||
footer={{
|
||||
left: (
|
||||
<EuiBadgeGroup gutterSize="s">
|
||||
{aliasesBadges}
|
||||
{aliases.length > MAX_VISIBLE_ALIASES && (
|
||||
<EuiBadge color="hollow">+{aliases.length - MAX_VISIBLE_ALIASES}</EuiBadge>
|
||||
)}
|
||||
</EuiBadgeGroup>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{isShowingAliases && (
|
||||
<EuiFlyout ownFocus onClose={() => setIsShowingAliases(false)}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
{i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.aliases.cardTitle', {
|
||||
defaultMessage: 'Aliases',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiListGroup maxWidth={false}>
|
||||
{aliases.map((alias) => (
|
||||
<EuiListGroupItem wrapText={true} key={alias} label={alias} />
|
||||
))}
|
||||
</EuiListGroup>
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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, ReactNode } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiTextColor } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
|
||||
import { SectionLoading } from '@kbn/es-ui-shared-plugin/public';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { getDataStreamDetailsLink } from '../../../../../services/routing';
|
||||
import { getTemplateDetailsLink } from '../../../../../..';
|
||||
import { useLoadDataStream } from '../../../../../services/api';
|
||||
import { useAppContext } from '../../../../../app_context';
|
||||
import { humanizeTimeStamp } from '../../../data_stream_list/humanize_time_stamp';
|
||||
import { OverviewCard } from './overview_card';
|
||||
|
||||
export const DataStreamDetails: FunctionComponent<{ dataStreamName: string }> = ({
|
||||
dataStreamName,
|
||||
}) => {
|
||||
const { error, data: dataStream, isLoading, resendRequest } = useLoadDataStream(dataStreamName);
|
||||
const { history } = useAppContext();
|
||||
const hasError = !isLoading && (error || !dataStream);
|
||||
let contentLeft: ReactNode = (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="baseline">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
css={css`
|
||||
font-size: ${euiThemeVars.euiFontSizeL};
|
||||
`}
|
||||
>
|
||||
{dataStream?.generation}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.dataStream.generationLabel', {
|
||||
defaultMessage: '{generations, plural, one {Generation} other {Generations}}',
|
||||
values: { generations: dataStream?.generation },
|
||||
})}
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
let contentRight: ReactNode = (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
size="s"
|
||||
{...reactRouterNavigate(history, getDataStreamDetailsLink(dataStream?.name ?? ''))}
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.dataStream.dataStreamLinkLabel', {
|
||||
defaultMessage: 'See details',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
size="s"
|
||||
{...reactRouterNavigate(
|
||||
history,
|
||||
getTemplateDetailsLink(dataStream?.indexTemplateName ?? '')
|
||||
)}
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.dataStream.templateLinkLabel', {
|
||||
defaultMessage: 'Related template',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
contentLeft = (
|
||||
<SectionLoading inline={true}>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.overviewTab.dataStream.loadingDescription"
|
||||
defaultMessage="Loading data stream details…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
contentRight = null;
|
||||
}
|
||||
if (hasError) {
|
||||
contentLeft = (
|
||||
<EuiText grow={false}>
|
||||
<EuiTextColor color="warning">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.overviewTab.dataStream.errorDescription"
|
||||
defaultMessage="Unable to load data stream details"
|
||||
/>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
);
|
||||
contentRight = (
|
||||
<EuiButton
|
||||
color="warning"
|
||||
onClick={resendRequest}
|
||||
data-test-subj="indexDetailsDataStreamReload"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.overviewTab.dataStream.reloadButtonLabel"
|
||||
defaultMessage="Reload"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<OverviewCard
|
||||
data-test-subj="indexDetailsDataStream"
|
||||
title={i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.dataStream.cardTitle', {
|
||||
defaultMessage: 'Data stream',
|
||||
})}
|
||||
content={{
|
||||
left: contentLeft,
|
||||
right: contentRight,
|
||||
}}
|
||||
footer={{
|
||||
left: (
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="calendar" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.dataStream.lastUpdateLabel', {
|
||||
defaultMessage: 'Last update',
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
{!isLoading && !hasError && (
|
||||
<EuiFlexItem>
|
||||
<EuiTextColor color="subdued">
|
||||
{dataStream?.maxTimeStamp
|
||||
? humanizeTimeStamp(dataStream.maxTimeStamp)
|
||||
: i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.overviewTab.dataStream.maxTimeStampNoneMessage',
|
||||
{
|
||||
defaultMessage: `Never`,
|
||||
}
|
||||
)}
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -10,14 +10,14 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiStat,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiLink,
|
||||
EuiFlexGrid,
|
||||
useIsWithinBreakpoints,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
CodeBox,
|
||||
|
@ -26,12 +26,16 @@ import {
|
|||
getLanguageDefinitionCodeSnippet,
|
||||
getConsoleRequest,
|
||||
} from '@kbn/search-api-panels';
|
||||
import { StatusDetails } from './status_details';
|
||||
import type { Index } from '../../../../../../../common';
|
||||
import { useAppContext } from '../../../../../app_context';
|
||||
import { documentationService } from '../../../../../services';
|
||||
import { breadcrumbService, IndexManagementBreadcrumb } from '../../../../../services/breadcrumbs';
|
||||
import { languageDefinitions, curlDefinition } from './languages';
|
||||
import { ExtensionsSummary } from './extensions_summary';
|
||||
import { DataStreamDetails } from './data_stream_details';
|
||||
import { StorageDetails } from './storage_details';
|
||||
import { AliasesDetails } from './aliases_details';
|
||||
|
||||
interface Props {
|
||||
indexDetails: Index;
|
||||
|
@ -41,13 +45,17 @@ export const DetailsPageOverview: React.FunctionComponent<Props> = ({ indexDetai
|
|||
const {
|
||||
name,
|
||||
status,
|
||||
health,
|
||||
documents,
|
||||
documents_deleted: documentsDeleted,
|
||||
primary,
|
||||
replica,
|
||||
aliases,
|
||||
data_stream: dataStream,
|
||||
size,
|
||||
primary_size: primarySize,
|
||||
} = indexDetails;
|
||||
const { config, core, plugins } = useAppContext();
|
||||
const { core, plugins } = useAppContext();
|
||||
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs(IndexManagementBreadcrumb.indexDetailsOverview);
|
||||
|
@ -65,99 +73,24 @@ export const DetailsPageOverview: React.FunctionComponent<Props> = ({ indexDetai
|
|||
indexName: name,
|
||||
};
|
||||
|
||||
const isLarge = useIsWithinBreakpoints(['xl']);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
{config.enableIndexStats && (
|
||||
<EuiFlexItem data-test-subj="overviewTabIndexStats">
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
title={status}
|
||||
titleColor={status === 'open' ? 'success' : 'danger'}
|
||||
description={i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.overviewTab.statusLabel',
|
||||
{
|
||||
defaultMessage: 'Status',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
title={documents}
|
||||
titleColor="primary"
|
||||
description={i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.overviewTab.documentsLabel',
|
||||
{
|
||||
defaultMessage: 'Documents',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
title={documentsDeleted}
|
||||
description={i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.overviewTab.documentsDeletedLabel',
|
||||
{
|
||||
defaultMessage: 'Documents deleted',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexGrid columns={isLarge ? 3 : 1}>
|
||||
<StorageDetails size={size} primarySize={primarySize} primary={primary} replica={replica} />
|
||||
|
||||
<EuiFlexItem data-test-subj="overviewTabIndexDetails">
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup>
|
||||
{primary && (
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
title={primary}
|
||||
description={i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.overviewTab.primaryLabel',
|
||||
{
|
||||
defaultMessage: 'Primaries',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<StatusDetails
|
||||
documents={documents}
|
||||
documentsDeleted={documentsDeleted!}
|
||||
status={status}
|
||||
health={health}
|
||||
/>
|
||||
|
||||
{replica && (
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
title={replica}
|
||||
description={i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.overviewTab.replicaLabel',
|
||||
{
|
||||
defaultMessage: 'Replicas',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<AliasesDetails aliases={aliases} />
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
title={Array.isArray(aliases) ? aliases.length : aliases}
|
||||
description={i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.overviewTab.aliasesLabel',
|
||||
{
|
||||
defaultMessage: 'Aliases',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{dataStream && <DataStreamDetails dataStreamName={dataStream} />}
|
||||
</EuiFlexGrid>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
|
|
|
@ -23,7 +23,9 @@ export const ExtensionsSummary: FunctionComponent<{ index: Index }> = ({ index }
|
|||
}
|
||||
return (
|
||||
<Fragment key={`extensionsSummary-${i}`}>
|
||||
<EuiPanel data-test-subj={`extensionsSummary-${i}`}>{summary}</EuiPanel>
|
||||
<EuiPanel data-test-subj={`extensionsSummary-${i}`} hasBorder={true}>
|
||||
{summary}
|
||||
</EuiPanel>
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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, ReactNode } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiSplitPanel, EuiTitle } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
content: {
|
||||
left: ReactNode;
|
||||
right: ReactNode;
|
||||
};
|
||||
footer?: {
|
||||
left?: ReactNode;
|
||||
right?: ReactNode;
|
||||
};
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
export const OverviewCard: FunctionComponent<Props> = ({
|
||||
title,
|
||||
content: { left: contentLeft, right: contentRight },
|
||||
footer: { left: footerLeft, right: footerRight } = {},
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiSplitPanel.Outer grow hasBorder={true} data-test-subj={dataTestSubj}>
|
||||
<EuiSplitPanel.Inner>
|
||||
<EuiTitle size="xxxs">
|
||||
<h4>{title}</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
justifyContent="spaceBetween"
|
||||
wrap={true}
|
||||
alignItems="center"
|
||||
css={css`
|
||||
min-height: ${euiThemeVars.euiButtonHeightSmall};
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow={false}>{contentLeft}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{contentRight}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner grow={false} color="subdued">
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
justifyContent="spaceBetween"
|
||||
alignItems="center"
|
||||
wrap={true}
|
||||
>
|
||||
{footerLeft && <EuiFlexItem grow={false}>{footerLeft}</EuiFlexItem>}
|
||||
{footerRight && <EuiFlexItem grow={false}>{footerRight}</EuiFlexItem>}
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiBadgeProps,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
|
||||
import { useAppContext } from '../../../../../app_context';
|
||||
import { Index } from '../../../../../../../common';
|
||||
import { OverviewCard } from './overview_card';
|
||||
|
||||
type NormalizedHealth = 'green' | 'red' | 'yellow';
|
||||
const healthToBadgeMapping: Record<
|
||||
NormalizedHealth,
|
||||
{ color: EuiBadgeProps['color']; label: string }
|
||||
> = {
|
||||
green: {
|
||||
color: 'success',
|
||||
label: i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.health.greenLabel', {
|
||||
defaultMessage: 'Healthy',
|
||||
}),
|
||||
},
|
||||
yellow: {
|
||||
color: 'warning',
|
||||
label: i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.health.yellowLabel', {
|
||||
defaultMessage: 'Warning',
|
||||
}),
|
||||
},
|
||||
red: {
|
||||
color: 'danger',
|
||||
label: i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.health.redLabel', {
|
||||
defaultMessage: 'Critical',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const StatusDetails: FunctionComponent<{
|
||||
documents: Index['documents'];
|
||||
documentsDeleted: Index['documents_deleted'];
|
||||
status: Index['status'];
|
||||
health: Index['health'];
|
||||
}> = ({ documents, documentsDeleted, status, health }) => {
|
||||
const { config } = useAppContext();
|
||||
if (!config.enableIndexStats || !health) {
|
||||
return null;
|
||||
}
|
||||
const badgeConfig = healthToBadgeMapping[health.toLowerCase() as NormalizedHealth];
|
||||
const healthBadge = <EuiBadge color={badgeConfig.color}>{badgeConfig.label}</EuiBadge>;
|
||||
|
||||
return (
|
||||
<OverviewCard
|
||||
data-test-subj="indexDetailsStatus"
|
||||
title={i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.status.cardTitle', {
|
||||
defaultMessage: 'Status',
|
||||
})}
|
||||
content={{
|
||||
left: (
|
||||
<EuiText
|
||||
color={status === 'close' ? 'danger' : 'success'}
|
||||
css={css`
|
||||
font-size: ${euiThemeVars.euiFontSizeL};
|
||||
`}
|
||||
>
|
||||
{status === 'close'
|
||||
? i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.status.closedLabel', {
|
||||
defaultMessage: 'Closed',
|
||||
})
|
||||
: i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.status.openLabel', {
|
||||
defaultMessage: 'Open',
|
||||
})}
|
||||
</EuiText>
|
||||
),
|
||||
right: (
|
||||
<div
|
||||
css={css`
|
||||
max-width: 100px;
|
||||
`}
|
||||
>
|
||||
{healthBadge}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
footer={{
|
||||
left: (
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="documents" color="subdued" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.status.documentsLabel', {
|
||||
defaultMessage:
|
||||
'{documents, plural, one {# Document} other {# Documents}} / {documentsDeleted} Deleted',
|
||||
values: {
|
||||
documents,
|
||||
documentsDeleted,
|
||||
},
|
||||
})}
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiTextColor } from '@elastic/eui';
|
||||
|
||||
import { useAppContext } from '../../../../../app_context';
|
||||
import { Index } from '../../../../../../../common';
|
||||
import { OverviewCard } from './overview_card';
|
||||
|
||||
export const StorageDetails: FunctionComponent<{
|
||||
primarySize: Index['primary_size'];
|
||||
size: Index['size'];
|
||||
primary: Index['primary'];
|
||||
replica: Index['replica'];
|
||||
}> = ({ primarySize, size, primary, replica }) => {
|
||||
const { config } = useAppContext();
|
||||
if (!config.enableIndexStats) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<OverviewCard
|
||||
data-test-subj="indexDetailsStorage"
|
||||
title={i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.storage.cardTitle', {
|
||||
defaultMessage: 'Storage',
|
||||
})}
|
||||
content={{
|
||||
left: (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="baseline">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
css={css`
|
||||
font-size: ${euiThemeVars.euiFontSizeL};
|
||||
`}
|
||||
>
|
||||
{primarySize}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.storage.primarySizeLabel', {
|
||||
defaultMessage: 'Primary',
|
||||
})}
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
right: (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="baseline">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
css={css`
|
||||
font-size: ${euiThemeVars.euiFontSizeL};
|
||||
`}
|
||||
>
|
||||
{size}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.storage.totalSizeLabel', {
|
||||
defaultMessage: 'Total',
|
||||
})}
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
}}
|
||||
footer={{
|
||||
left: (
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="shard" color="subdued" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.storage.shardsLabel', {
|
||||
defaultMessage: 'Shards',
|
||||
})}
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
right: (
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.overviewTab.storage.primariesReplicasLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'{primary, plural, one {# Primary} other {# Primaries}} / {replica, plural, one {# Replica} other {# Replicas}} ',
|
||||
values: {
|
||||
primary,
|
||||
replica,
|
||||
},
|
||||
}
|
||||
)}
|
||||
</EuiTextColor>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -40,6 +40,7 @@
|
|||
"@kbn/core-http-browser",
|
||||
"@kbn/search-api-panels",
|
||||
"@kbn/cloud-plugin",
|
||||
"@kbn/ui-theme",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue