[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 aliases


c44dc6a7-dc2b-4297-88ae-1ecc22f828da



#### (Stateful) Backing index of a data stream


5206e427-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:
Yulia Čech 2023-10-31 17:56:29 +01:00 committed by GitHub
parent 58adee01a0
commit f4e3807940
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 796 additions and 108 deletions

View file

@ -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');

View file

@ -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', () => {

View file

@ -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',

View file

@ -254,6 +254,10 @@ export const DetailsPage: FunctionComponent<
navigateToAllIndices={navigateToAllIndices}
/>,
]}
rightSideGroupProps={{
wrap: false,
}}
responsive="reverse"
tabs={headerTabs}
/>

View file

@ -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>
)}
</>
);
};

View file

@ -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>
),
}}
/>
);
};

View file

@ -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 />

View file

@ -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>
);

View file

@ -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>
);
};

View file

@ -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>
),
}}
/>
);
};

View file

@ -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>
),
}}
/>
);
};

View file

@ -40,6 +40,7 @@
"@kbn/core-http-browser",
"@kbn/search-api-panels",
"@kbn/cloud-plugin",
"@kbn/ui-theme",
],
"exclude": [
"target/**/*",