mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Metrics UI] Show descriptive loading, empty and error states in the metrics table (#133947)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
fabe146739
commit
87e72207bb
25 changed files with 1773 additions and 396 deletions
|
@ -5,5 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { NoIndices } from './no_indices';
|
||||
export * from './no_metric_indices';
|
||||
export { NoData } from './no_data';
|
||||
export { NoIndices } from './no_indices';
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const noMetricIndicesPromptPrimaryActionTitle = i18n.translate(
|
||||
'xpack.infra.metrics.noDataConfig.beatsCard.title',
|
||||
{
|
||||
defaultMessage: 'Add a metrics integration',
|
||||
}
|
||||
);
|
||||
|
||||
export const noMetricIndicesPromptDescription = i18n.translate(
|
||||
'xpack.infra.metrics.noDataConfig.beatsCard.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Use Beats to send metrics data to Elasticsearch. We make it easy with modules for many popular systems and apps.',
|
||||
}
|
||||
);
|
||||
|
||||
export const noMetricIndicesPromptTitle = i18n.translate(
|
||||
'xpack.infra.metrics.noDataConfig.promptTitle',
|
||||
{ defaultMessage: 'Add metrics data' }
|
||||
);
|
|
@ -7,12 +7,13 @@
|
|||
|
||||
import { EuiCard } from '@elastic/eui';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import type { Meta } from '@storybook/react/types-6-0';
|
||||
import React from 'react';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { Meta, Story } from '@storybook/react/types-6-0';
|
||||
import React from 'react';
|
||||
import { decorateWithGlobalStorybookThemeProviders } from '../../../test_utils/use_global_storybook_theme';
|
||||
import { ContainerMetricsTable } from './container_metrics_table';
|
||||
import type { ContainerMetricsTableProps } from './container_metrics_table';
|
||||
import { ContainerMetricsTable } from './container_metrics_table';
|
||||
import { ContainerNodeMetricsRow } from './use_container_metrics_table';
|
||||
|
||||
const mockServices = {
|
||||
application: {
|
||||
|
@ -32,6 +33,20 @@ export default {
|
|||
decorateWithGlobalStorybookThemeProviders,
|
||||
],
|
||||
component: ContainerMetricsTable,
|
||||
args: {
|
||||
data: {
|
||||
state: 'empty-indices',
|
||||
},
|
||||
isLoading: false,
|
||||
sortState: {
|
||||
direction: 'desc',
|
||||
field: 'averageCpuUsagePercent',
|
||||
},
|
||||
timerange: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
setSortState: {
|
||||
action: 'Sort field or direction changed',
|
||||
|
@ -42,53 +57,95 @@ export default {
|
|||
},
|
||||
} as Meta;
|
||||
|
||||
const storyArgs: Omit<ContainerMetricsTableProps, 'setSortState' | 'setCurrentPageIndex'> = {
|
||||
isLoading: false,
|
||||
containers: [
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg1',
|
||||
uptime: 23000000,
|
||||
averageCpuUsagePercent: 99,
|
||||
averageMemoryUsageMegabytes: 34,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg2',
|
||||
uptime: 43000000,
|
||||
averageCpuUsagePercent: 72,
|
||||
averageMemoryUsageMegabytes: 68,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg3',
|
||||
uptime: 53000000,
|
||||
averageCpuUsagePercent: 54,
|
||||
averageMemoryUsageMegabytes: 132,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg4',
|
||||
uptime: 63000000,
|
||||
averageCpuUsagePercent: 34,
|
||||
averageMemoryUsageMegabytes: 264,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg5',
|
||||
uptime: 83000000,
|
||||
averageCpuUsagePercent: 13,
|
||||
averageMemoryUsageMegabytes: 512,
|
||||
},
|
||||
],
|
||||
currentPageIndex: 0,
|
||||
pageCount: 10,
|
||||
sortState: {
|
||||
direction: 'desc',
|
||||
field: 'averageCpuUsagePercent',
|
||||
const loadedContainers: ContainerNodeMetricsRow[] = [
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg1',
|
||||
uptime: 23000000,
|
||||
averageCpuUsagePercent: 99,
|
||||
averageMemoryUsageMegabytes: 34,
|
||||
},
|
||||
timerange: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg2',
|
||||
uptime: 43000000,
|
||||
averageCpuUsagePercent: 72,
|
||||
averageMemoryUsageMegabytes: 68,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg3',
|
||||
uptime: 53000000,
|
||||
averageCpuUsagePercent: 54,
|
||||
averageMemoryUsageMegabytes: 132,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg4',
|
||||
uptime: 63000000,
|
||||
averageCpuUsagePercent: 34,
|
||||
averageMemoryUsageMegabytes: 264,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg5',
|
||||
uptime: 83000000,
|
||||
averageCpuUsagePercent: 13,
|
||||
averageMemoryUsageMegabytes: 512,
|
||||
},
|
||||
];
|
||||
|
||||
const Template: Story<ContainerMetricsTableProps> = (args) => {
|
||||
return <ContainerMetricsTable {...args} />;
|
||||
};
|
||||
|
||||
export const Basic = Template.bind({});
|
||||
Basic.args = {
|
||||
data: {
|
||||
state: 'data',
|
||||
currentPageIndex: 1,
|
||||
pageCount: 10,
|
||||
rows: loadedContainers,
|
||||
},
|
||||
};
|
||||
|
||||
export const Demo = (args: ContainerMetricsTableProps) => {
|
||||
return <ContainerMetricsTable {...args} />;
|
||||
export const Loading = Template.bind({});
|
||||
Loading.args = {
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
export const Reloading = Template.bind({});
|
||||
Reloading.args = {
|
||||
data: {
|
||||
state: 'data',
|
||||
currentPageIndex: 1,
|
||||
pageCount: 10,
|
||||
rows: loadedContainers,
|
||||
},
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
export const MissingIndices = Template.bind({});
|
||||
MissingIndices.args = {
|
||||
data: {
|
||||
state: 'no-indices',
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyIndices = Template.bind({});
|
||||
EmptyIndices.args = {
|
||||
data: {
|
||||
state: 'empty-indices',
|
||||
},
|
||||
};
|
||||
|
||||
export const FailedToLoadSource = Template.bind({});
|
||||
FailedToLoadSource.args = {
|
||||
data: {
|
||||
state: 'error',
|
||||
errors: [new Error('Failed to load source configuration')],
|
||||
},
|
||||
};
|
||||
|
||||
export const FailedToLoadMetrics = Template.bind({});
|
||||
FailedToLoadMetrics.args = {
|
||||
data: {
|
||||
state: 'error',
|
||||
errors: [new Error('Failed to load metrics')],
|
||||
},
|
||||
};
|
||||
Demo.args = storyArgs;
|
||||
|
|
|
@ -5,19 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { HttpFetchOptions } from '@kbn/core/public';
|
||||
import { MetricsExplorerSeries } from '../../../../common/http_api';
|
||||
import { CoreProviders } from '../../../apps/common_providers';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import type { HttpFetchOptions } from '@kbn/core/public';
|
||||
import type {
|
||||
DataResponseMock,
|
||||
NodeMetricsTableFetchMock,
|
||||
SourceResponseMock,
|
||||
} from '../test_helpers';
|
||||
import { createStartServicesAccessorMock } from '../test_helpers';
|
||||
import { ContainerMetricsTable } from './container_metrics_table';
|
||||
import { createLazyContainerMetricsTable } from './create_lazy_container_metrics_table';
|
||||
import IntegratedContainerMetricsTable from './integrated_container_metrics_table';
|
||||
import { metricByField } from './use_container_metrics_table';
|
||||
import type { MetricsExplorerSeries } from '../../../../common/http_api';
|
||||
|
||||
describe('ContainerMetricsTable', () => {
|
||||
const timerange = {
|
||||
|
@ -40,6 +42,8 @@ describe('ContainerMetricsTable', () => {
|
|||
|
||||
const fetchMock = createFetchMock();
|
||||
|
||||
const loadingIndicatorTestId = 'metricsTableLoadingContent';
|
||||
|
||||
describe('createLazyContainerMetricsTable', () => {
|
||||
it('should lazily load and render the table', async () => {
|
||||
const { fetch, getStartServices } = createStartServicesAccessorMock(fetchMock);
|
||||
|
@ -47,7 +51,7 @@ describe('ContainerMetricsTable', () => {
|
|||
|
||||
render(<LazyContainerMetricsTable timerange={timerange} filterClauseDsl={filterClauseDsl} />);
|
||||
|
||||
expect(screen.queryByTestId('containerMetricsTableLoader')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(loadingIndicatorTestId)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('containerMetricsTable')).not.toBeInTheDocument();
|
||||
|
||||
// Using longer time out since resolving dynamic import can be slow
|
||||
|
@ -56,7 +60,7 @@ describe('ContainerMetricsTable', () => {
|
|||
timeout: 10000,
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('containerMetricsTableLoader')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(loadingIndicatorTestId)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('containerMetricsTable')).toBeInTheDocument();
|
||||
}, 10000);
|
||||
});
|
||||
|
@ -79,6 +83,44 @@ describe('ContainerMetricsTable', () => {
|
|||
expect(await findByText(/some-container/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a loading indicator on first load', () => {
|
||||
const { coreProvidersPropsMock } = createStartServicesAccessorMock(jest.fn());
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<CoreProviders {...coreProvidersPropsMock}>
|
||||
<ContainerMetricsTable
|
||||
data={{ state: 'unknown' }}
|
||||
isLoading={true}
|
||||
setCurrentPageIndex={jest.fn()}
|
||||
setSortState={jest.fn()}
|
||||
sortState={{ field: 'name', direction: 'asc' }}
|
||||
timerange={{ from: new Date().toISOString(), to: new Date().toISOString() }}
|
||||
/>
|
||||
</CoreProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId(loadingIndicatorTestId)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a prompt when indices are missing', () => {
|
||||
const { coreProvidersPropsMock } = createStartServicesAccessorMock(jest.fn());
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<CoreProviders {...coreProvidersPropsMock}>
|
||||
<ContainerMetricsTable
|
||||
data={{ state: 'no-indices' }}
|
||||
isLoading={false}
|
||||
setCurrentPageIndex={jest.fn()}
|
||||
setSortState={jest.fn()}
|
||||
sortState={{ field: 'name', direction: 'asc' }}
|
||||
timerange={{ from: new Date().toISOString(), to: new Date().toISOString() }}
|
||||
/>
|
||||
</CoreProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId('metricsTableLoadingContent')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function createFetchMock(): NodeMetricsTableFetchMock {
|
||||
|
@ -87,6 +129,9 @@ function createFetchMock(): NodeMetricsTableFetchMock {
|
|||
configuration: {
|
||||
metricAlias: 'some-index-pattern',
|
||||
},
|
||||
status: {
|
||||
metricIndicesExist: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -97,7 +142,7 @@ function createFetchMock(): NodeMetricsTableFetchMock {
|
|||
],
|
||||
};
|
||||
|
||||
return (path: string, options: HttpFetchOptions) => {
|
||||
return (path: string, _options: HttpFetchOptions) => {
|
||||
// options can be used to read body for filter clause
|
||||
if (path === '/api/metrics/source/default') {
|
||||
return Promise.resolve(sourceMock);
|
||||
|
|
|
@ -10,44 +10,37 @@ import type {
|
|||
EuiBasicTableColumn,
|
||||
EuiTableSortingType,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { SortState } from '../shared';
|
||||
import { MetricsNodeDetailsLink, NumberCell, StepwisePagination, UptimeCell } from '../shared';
|
||||
import {
|
||||
MetricsNodeDetailsLink,
|
||||
MetricsTableEmptyIndicesContent,
|
||||
MetricsTableErrorContent,
|
||||
MetricsTableLoadingContent,
|
||||
MetricsTableNoIndicesContent,
|
||||
NodeMetricsTableData,
|
||||
NumberCell,
|
||||
StepwisePagination,
|
||||
UptimeCell,
|
||||
} from '../shared';
|
||||
import type { ContainerNodeMetricsRow } from './use_container_metrics_table';
|
||||
|
||||
export interface ContainerMetricsTableProps {
|
||||
data: NodeMetricsTableData<ContainerNodeMetricsRow>;
|
||||
isLoading: boolean;
|
||||
setCurrentPageIndex: (value: number) => void;
|
||||
setSortState: (state: SortState<ContainerNodeMetricsRow>) => void;
|
||||
sortState: SortState<ContainerNodeMetricsRow>;
|
||||
timerange: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
isLoading: boolean;
|
||||
containers: ContainerNodeMetricsRow[];
|
||||
pageCount: number;
|
||||
currentPageIndex: number;
|
||||
setCurrentPageIndex: (value: number) => void;
|
||||
sortState: SortState<ContainerNodeMetricsRow>;
|
||||
setSortState: (state: SortState<ContainerNodeMetricsRow>) => void;
|
||||
}
|
||||
|
||||
export const ContainerMetricsTable = (props: ContainerMetricsTableProps) => {
|
||||
const {
|
||||
timerange,
|
||||
isLoading,
|
||||
containers,
|
||||
pageCount,
|
||||
currentPageIndex,
|
||||
setCurrentPageIndex,
|
||||
sortState,
|
||||
setSortState,
|
||||
} = props;
|
||||
const { data, isLoading, setCurrentPageIndex, setSortState, sortState, timerange } = props;
|
||||
|
||||
const columns = useMemo(() => containerNodeColumns(timerange), [timerange]);
|
||||
|
||||
|
@ -68,42 +61,54 @@ export const ContainerMetricsTable = (props: ContainerMetricsTableProps) => {
|
|||
[setSortState, setCurrentPageIndex]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
if (data.state === 'error') {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center" direction="column">
|
||||
<EuiLoadingSpinner size="xl" data-test-subj="containerMetricsTableLoader" />
|
||||
</EuiFlexGroup>
|
||||
<>
|
||||
{data.errors.map((error) => (
|
||||
<MetricsTableErrorContent error={error} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
} else if (isLoading && data.state !== 'data') {
|
||||
return <MetricsTableLoadingContent />;
|
||||
} else if (data.state === 'no-indices') {
|
||||
return <MetricsTableNoIndicesContent />;
|
||||
} else if (data.state === 'empty-indices') {
|
||||
return <MetricsTableEmptyIndicesContent />;
|
||||
} else if (data.state === 'data') {
|
||||
return (
|
||||
<>
|
||||
<EuiBasicTable
|
||||
tableCaption={i18n.translate('xpack.infra.metricsTable.container.tableCaption', {
|
||||
defaultMessage: 'Infrastructure metrics for containers',
|
||||
})}
|
||||
items={data.rows}
|
||||
columns={columns}
|
||||
sorting={sortSettings}
|
||||
onChange={onTableSortChange}
|
||||
loading={isLoading}
|
||||
noItemsMessage={<MetricsTableLoadingContent />}
|
||||
data-test-subj="containerMetricsTable"
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center" responsive={false} wrap>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StepwisePagination
|
||||
ariaLabel={i18n.translate('xpack.infra.metricsTable.container.paginationAriaLabel', {
|
||||
defaultMessage: 'Container metrics pagination',
|
||||
})}
|
||||
pageCount={data.pageCount}
|
||||
currentPageIndex={data.currentPageIndex}
|
||||
setCurrentPageIndex={setCurrentPageIndex}
|
||||
data-test-subj="containerMetricsTablePagination"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiBasicTable
|
||||
tableCaption={i18n.translate('xpack.infra.metricsTable.container.tableCaption', {
|
||||
defaultMessage: 'Infrastructure metrics for containers',
|
||||
})}
|
||||
items={containers}
|
||||
columns={columns}
|
||||
sorting={sortSettings}
|
||||
onChange={onTableSortChange}
|
||||
data-test-subj="containerMetricsTable"
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center" responsive={false} wrap>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StepwisePagination
|
||||
ariaLabel={i18n.translate('xpack.infra.metricsTable.container.paginationAriaLabel', {
|
||||
defaultMessage: 'Container metrics pagination',
|
||||
})}
|
||||
pageCount={pageCount}
|
||||
currentPageIndex={currentPageIndex}
|
||||
setCurrentPageIndex={setCurrentPageIndex}
|
||||
data-test-subj="containerMetricsTablePagination"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function containerNodeColumns(
|
||||
|
|
|
@ -73,11 +73,7 @@ export function useContainerMetricsTable({
|
|||
[filterClauseDsl]
|
||||
);
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
nodes: containers,
|
||||
pageCount,
|
||||
} = useInfrastructureNodeMetrics<ContainerNodeMetricsRow>({
|
||||
const { data, isLoading } = useInfrastructureNodeMetrics<ContainerNodeMetricsRow>({
|
||||
metricsExplorerOptions: containerMetricsOptions,
|
||||
timerange,
|
||||
transform: seriesToContainerNodeMetricsRow,
|
||||
|
@ -86,14 +82,12 @@ export function useContainerMetricsTable({
|
|||
});
|
||||
|
||||
return {
|
||||
timerange,
|
||||
data,
|
||||
isLoading,
|
||||
containers,
|
||||
pageCount,
|
||||
currentPageIndex,
|
||||
setCurrentPageIndex,
|
||||
sortState,
|
||||
setSortState,
|
||||
sortState,
|
||||
timerange,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
|
||||
import { EuiCard } from '@elastic/eui';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import type { Meta } from '@storybook/react/types-6-0';
|
||||
import React from 'react';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { Meta, Story } from '@storybook/react/types-6-0';
|
||||
import React from 'react';
|
||||
import { decorateWithGlobalStorybookThemeProviders } from '../../../test_utils/use_global_storybook_theme';
|
||||
import { HostMetricsTable } from './host_metrics_table';
|
||||
import type { HostMetricsTableProps } from './host_metrics_table';
|
||||
import { HostMetricsTable } from './host_metrics_table';
|
||||
import { HostNodeMetricsRow } from './use_host_metrics_table';
|
||||
|
||||
const mockServices = {
|
||||
application: {
|
||||
|
@ -32,6 +33,20 @@ export default {
|
|||
decorateWithGlobalStorybookThemeProviders,
|
||||
],
|
||||
component: HostMetricsTable,
|
||||
args: {
|
||||
data: {
|
||||
state: 'empty-indices',
|
||||
},
|
||||
isLoading: false,
|
||||
sortState: {
|
||||
direction: 'desc',
|
||||
field: 'averageCpuUsagePercent',
|
||||
},
|
||||
timerange: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
setSortState: {
|
||||
action: 'Sort field or direction changed',
|
||||
|
@ -40,60 +55,102 @@ export default {
|
|||
action: 'Page changed',
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
} as Meta<HostMetricsTableProps>;
|
||||
|
||||
const storyArgs: Omit<HostMetricsTableProps, 'setSortState' | 'setCurrentPageIndex'> = {
|
||||
isLoading: false,
|
||||
hosts: [
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg1',
|
||||
cpuCount: 2,
|
||||
averageCpuUsagePercent: 99,
|
||||
totalMemoryMegabytes: 1024,
|
||||
averageMemoryUsagePercent: 34,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg2',
|
||||
cpuCount: 4,
|
||||
averageCpuUsagePercent: 74,
|
||||
totalMemoryMegabytes: 2450,
|
||||
averageMemoryUsagePercent: 13,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg3',
|
||||
cpuCount: 8,
|
||||
averageCpuUsagePercent: 56,
|
||||
totalMemoryMegabytes: 4810,
|
||||
averageMemoryUsagePercent: 74,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg4',
|
||||
cpuCount: 16,
|
||||
averageCpuUsagePercent: 34,
|
||||
totalMemoryMegabytes: 8123,
|
||||
averageMemoryUsagePercent: 56,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg5',
|
||||
cpuCount: 32,
|
||||
averageCpuUsagePercent: 13,
|
||||
totalMemoryMegabytes: 16792,
|
||||
averageMemoryUsagePercent: 99,
|
||||
},
|
||||
],
|
||||
currentPageIndex: 0,
|
||||
pageCount: 10,
|
||||
sortState: {
|
||||
direction: 'desc',
|
||||
field: 'averageCpuUsagePercent',
|
||||
const loadedHosts: HostNodeMetricsRow[] = [
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg1',
|
||||
cpuCount: 2,
|
||||
averageCpuUsagePercent: 99,
|
||||
totalMemoryMegabytes: 1024,
|
||||
averageMemoryUsagePercent: 34,
|
||||
},
|
||||
timerange: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg2',
|
||||
cpuCount: 4,
|
||||
averageCpuUsagePercent: 74,
|
||||
totalMemoryMegabytes: 2450,
|
||||
averageMemoryUsagePercent: 13,
|
||||
},
|
||||
};
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg3',
|
||||
cpuCount: 8,
|
||||
averageCpuUsagePercent: 56,
|
||||
totalMemoryMegabytes: 4810,
|
||||
averageMemoryUsagePercent: 74,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg4',
|
||||
cpuCount: 16,
|
||||
averageCpuUsagePercent: 34,
|
||||
totalMemoryMegabytes: 8123,
|
||||
averageMemoryUsagePercent: 56,
|
||||
},
|
||||
{
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg5',
|
||||
cpuCount: 32,
|
||||
averageCpuUsagePercent: 13,
|
||||
totalMemoryMegabytes: 16792,
|
||||
averageMemoryUsagePercent: 99,
|
||||
},
|
||||
];
|
||||
|
||||
export const Demo = (args: HostMetricsTableProps) => {
|
||||
const Template: Story<HostMetricsTableProps> = (args) => {
|
||||
return <HostMetricsTable {...args} />;
|
||||
};
|
||||
Demo.args = storyArgs;
|
||||
|
||||
export const Basic = Template.bind({});
|
||||
Basic.args = {
|
||||
data: {
|
||||
state: 'data',
|
||||
currentPageIndex: 1,
|
||||
pageCount: 10,
|
||||
rows: loadedHosts,
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading = Template.bind({});
|
||||
Loading.args = {
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
export const Reloading = Template.bind({});
|
||||
Reloading.args = {
|
||||
data: {
|
||||
state: 'data',
|
||||
currentPageIndex: 1,
|
||||
pageCount: 10,
|
||||
rows: loadedHosts,
|
||||
},
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
export const MissingIndices = Template.bind({});
|
||||
MissingIndices.args = {
|
||||
data: {
|
||||
state: 'no-indices',
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyIndices = Template.bind({});
|
||||
EmptyIndices.args = {
|
||||
data: {
|
||||
state: 'empty-indices',
|
||||
},
|
||||
};
|
||||
|
||||
export const FailedToLoadSource = Template.bind({});
|
||||
FailedToLoadSource.args = {
|
||||
data: {
|
||||
state: 'error',
|
||||
errors: [new Error('Failed to load source configuration')],
|
||||
},
|
||||
};
|
||||
|
||||
export const FailedToLoadMetrics = Template.bind({});
|
||||
FailedToLoadMetrics.args = {
|
||||
data: {
|
||||
state: 'error',
|
||||
errors: [new Error('Failed to load metrics')],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { HttpFetchOptions } from '@kbn/core/public';
|
||||
import { CoreProviders } from '../../../apps/common_providers';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import type { HttpFetchOptions } from '@kbn/core/public';
|
||||
import type { MetricsExplorerSeries } from '../../../../common/http_api';
|
||||
import type {
|
||||
DataResponseMock,
|
||||
NodeMetricsTableFetchMock,
|
||||
|
@ -15,9 +17,9 @@ import type {
|
|||
} from '../test_helpers';
|
||||
import { createStartServicesAccessorMock } from '../test_helpers';
|
||||
import { createLazyHostMetricsTable } from './create_lazy_host_metrics_table';
|
||||
import { HostMetricsTable } from './host_metrics_table';
|
||||
import IntegratedHostMetricsTable from './integrated_host_metrics_table';
|
||||
import { metricByField } from './use_host_metrics_table';
|
||||
import type { MetricsExplorerSeries } from '../../../../common/http_api';
|
||||
|
||||
describe('HostMetricsTable', () => {
|
||||
const timerange = {
|
||||
|
@ -40,6 +42,8 @@ describe('HostMetricsTable', () => {
|
|||
|
||||
const fetchMock = createFetchMock();
|
||||
|
||||
const loadingIndicatorTestId = 'metricsTableLoadingContent';
|
||||
|
||||
describe('createLazyHostMetricsTable', () => {
|
||||
it('should lazily load and render the table', async () => {
|
||||
const { fetch, getStartServices } = createStartServicesAccessorMock(fetchMock);
|
||||
|
@ -47,7 +51,7 @@ describe('HostMetricsTable', () => {
|
|||
|
||||
render(<LazyHostMetricsTable timerange={timerange} filterClauseDsl={filterClauseDsl} />);
|
||||
|
||||
expect(screen.queryByTestId('hostMetricsTableLoader')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(loadingIndicatorTestId)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('hostMetricsTable')).not.toBeInTheDocument();
|
||||
|
||||
// Using longer time out since resolving dynamic import can be slow
|
||||
|
@ -56,7 +60,7 @@ describe('HostMetricsTable', () => {
|
|||
timeout: 10000,
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('hostMetricsTableLoader')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(loadingIndicatorTestId)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('hostMetricsTable')).toBeInTheDocument();
|
||||
}, 10000);
|
||||
});
|
||||
|
@ -79,6 +83,44 @@ describe('HostMetricsTable', () => {
|
|||
expect(await findByText(/some-host/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a loading indicator on first load', () => {
|
||||
const { coreProvidersPropsMock } = createStartServicesAccessorMock(jest.fn());
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<CoreProviders {...coreProvidersPropsMock}>
|
||||
<HostMetricsTable
|
||||
data={{ state: 'unknown' }}
|
||||
isLoading={true}
|
||||
setCurrentPageIndex={jest.fn()}
|
||||
setSortState={jest.fn()}
|
||||
sortState={{ field: 'name', direction: 'asc' }}
|
||||
timerange={{ from: new Date().toISOString(), to: new Date().toISOString() }}
|
||||
/>
|
||||
</CoreProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId(loadingIndicatorTestId)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a prompt when indices are missing', () => {
|
||||
const { coreProvidersPropsMock } = createStartServicesAccessorMock(jest.fn());
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<CoreProviders {...coreProvidersPropsMock}>
|
||||
<HostMetricsTable
|
||||
data={{ state: 'no-indices' }}
|
||||
isLoading={false}
|
||||
setCurrentPageIndex={jest.fn()}
|
||||
setSortState={jest.fn()}
|
||||
sortState={{ field: 'name', direction: 'asc' }}
|
||||
timerange={{ from: new Date().toISOString(), to: new Date().toISOString() }}
|
||||
/>
|
||||
</CoreProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId('metricsTableLoadingContent')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function createFetchMock(): NodeMetricsTableFetchMock {
|
||||
|
@ -87,6 +129,9 @@ function createFetchMock(): NodeMetricsTableFetchMock {
|
|||
configuration: {
|
||||
metricAlias: 'some-index-pattern',
|
||||
},
|
||||
status: {
|
||||
metricIndicesExist: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -97,7 +142,7 @@ function createFetchMock(): NodeMetricsTableFetchMock {
|
|||
],
|
||||
};
|
||||
|
||||
return (path: string, options: HttpFetchOptions) => {
|
||||
return (path: string, _options: HttpFetchOptions) => {
|
||||
// options can be used to read body for filter clause
|
||||
if (path === '/api/metrics/source/default') {
|
||||
return Promise.resolve(sourceMock);
|
||||
|
|
|
@ -10,44 +10,36 @@ import type {
|
|||
EuiBasicTableColumn,
|
||||
EuiTableSortingType,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { SortState } from '../shared';
|
||||
import { MetricsNodeDetailsLink, NumberCell, StepwisePagination } from '../shared';
|
||||
import {
|
||||
MetricsNodeDetailsLink,
|
||||
MetricsTableEmptyIndicesContent,
|
||||
MetricsTableErrorContent,
|
||||
MetricsTableLoadingContent,
|
||||
MetricsTableNoIndicesContent,
|
||||
NodeMetricsTableData,
|
||||
NumberCell,
|
||||
StepwisePagination,
|
||||
} from '../shared';
|
||||
import type { HostNodeMetricsRow } from './use_host_metrics_table';
|
||||
|
||||
export interface HostMetricsTableProps {
|
||||
data: NodeMetricsTableData<HostNodeMetricsRow>;
|
||||
isLoading: boolean;
|
||||
setCurrentPageIndex: (value: number) => void;
|
||||
setSortState: (state: SortState<HostNodeMetricsRow>) => void;
|
||||
sortState: SortState<HostNodeMetricsRow>;
|
||||
timerange: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
isLoading: boolean;
|
||||
hosts: HostNodeMetricsRow[];
|
||||
pageCount: number;
|
||||
currentPageIndex: number;
|
||||
setCurrentPageIndex: (value: number) => void;
|
||||
sortState: SortState<HostNodeMetricsRow>;
|
||||
setSortState: (state: SortState<HostNodeMetricsRow>) => void;
|
||||
}
|
||||
|
||||
export const HostMetricsTable = (props: HostMetricsTableProps) => {
|
||||
const {
|
||||
timerange,
|
||||
isLoading,
|
||||
hosts,
|
||||
pageCount,
|
||||
currentPageIndex,
|
||||
setCurrentPageIndex,
|
||||
sortState,
|
||||
setSortState,
|
||||
} = props;
|
||||
const { data, isLoading, setCurrentPageIndex, setSortState, sortState, timerange } = props;
|
||||
|
||||
const columns = useMemo(() => hostMetricsColumns(timerange), [timerange]);
|
||||
|
||||
|
@ -68,42 +60,54 @@ export const HostMetricsTable = (props: HostMetricsTableProps) => {
|
|||
[setSortState, setCurrentPageIndex]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
if (data.state === 'error') {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center" direction="column">
|
||||
<EuiLoadingSpinner size="xl" data-test-subj="hostMetricsTableLoader" />
|
||||
</EuiFlexGroup>
|
||||
<>
|
||||
{data.errors.map((error) => (
|
||||
<MetricsTableErrorContent error={error} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
} else if (isLoading && data.state !== 'data') {
|
||||
return <MetricsTableLoadingContent />;
|
||||
} else if (data.state === 'no-indices') {
|
||||
return <MetricsTableNoIndicesContent />;
|
||||
} else if (data.state === 'empty-indices') {
|
||||
return <MetricsTableEmptyIndicesContent />;
|
||||
} else if (data.state === 'data') {
|
||||
return (
|
||||
<>
|
||||
<EuiBasicTable
|
||||
tableCaption={i18n.translate('xpack.infra.metricsTable.host.tableCaption', {
|
||||
defaultMessage: 'Infrastructure metrics for hosts',
|
||||
})}
|
||||
items={data.rows}
|
||||
columns={columns}
|
||||
sorting={sortSettings}
|
||||
onChange={onTableSortChange}
|
||||
loading={isLoading}
|
||||
noItemsMessage={<MetricsTableLoadingContent />}
|
||||
data-test-subj="hostMetricsTable"
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center" responsive={false} wrap>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StepwisePagination
|
||||
ariaLabel={i18n.translate('xpack.infra.metricsTable.host.paginationAriaLabel', {
|
||||
defaultMessage: 'Host metrics pagination',
|
||||
})}
|
||||
pageCount={data.pageCount}
|
||||
currentPageIndex={data.currentPageIndex}
|
||||
setCurrentPageIndex={setCurrentPageIndex}
|
||||
data-test-subj="hostMetricsTablePagination"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiBasicTable
|
||||
tableCaption={i18n.translate('xpack.infra.metricsTable.host.tableCaption', {
|
||||
defaultMessage: 'Infrastructure metrics for hosts',
|
||||
})}
|
||||
items={hosts}
|
||||
columns={columns}
|
||||
sorting={sortSettings}
|
||||
onChange={onTableSortChange}
|
||||
data-test-subj="hostMetricsTable"
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center" responsive={false} wrap>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StepwisePagination
|
||||
ariaLabel={i18n.translate('xpack.infra.metricsTable.host.paginationAriaLabel', {
|
||||
defaultMessage: 'Host metrics pagination',
|
||||
})}
|
||||
pageCount={pageCount}
|
||||
currentPageIndex={currentPageIndex}
|
||||
setCurrentPageIndex={setCurrentPageIndex}
|
||||
data-test-subj="hostMetricsTablePagination"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function hostMetricsColumns(
|
||||
|
|
|
@ -70,11 +70,7 @@ export function useHostMetricsTable({ timerange, filterClauseDsl }: UseNodeMetri
|
|||
[filterClauseDsl]
|
||||
);
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
nodes: hosts,
|
||||
pageCount,
|
||||
} = useInfrastructureNodeMetrics<HostNodeMetricsRow>({
|
||||
const { data, isLoading } = useInfrastructureNodeMetrics<HostNodeMetricsRow>({
|
||||
metricsExplorerOptions: hostMetricsOptions,
|
||||
timerange,
|
||||
transform: seriesToHostNodeMetricsRow,
|
||||
|
@ -83,14 +79,12 @@ export function useHostMetricsTable({ timerange, filterClauseDsl }: UseNodeMetri
|
|||
});
|
||||
|
||||
return {
|
||||
timerange,
|
||||
data,
|
||||
isLoading,
|
||||
hosts,
|
||||
pageCount,
|
||||
currentPageIndex,
|
||||
setCurrentPageIndex,
|
||||
sortState,
|
||||
setSortState,
|
||||
sortState,
|
||||
timerange,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
|
||||
import { EuiCard } from '@elastic/eui';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import type { Meta } from '@storybook/react/types-6-0';
|
||||
import React from 'react';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { Meta, Story } from '@storybook/react/types-6-0';
|
||||
import React from 'react';
|
||||
import { decorateWithGlobalStorybookThemeProviders } from '../../../test_utils/use_global_storybook_theme';
|
||||
import { PodMetricsTable } from './pod_metrics_table';
|
||||
import type { PodMetricsTableProps } from './pod_metrics_table';
|
||||
import { PodMetricsTable } from './pod_metrics_table';
|
||||
import { PodNodeMetricsRow } from './use_pod_metrics_table';
|
||||
|
||||
const mockServices = {
|
||||
application: {
|
||||
|
@ -32,6 +33,20 @@ export default {
|
|||
decorateWithGlobalStorybookThemeProviders,
|
||||
],
|
||||
component: PodMetricsTable,
|
||||
args: {
|
||||
data: {
|
||||
state: 'empty-indices',
|
||||
},
|
||||
isLoading: false,
|
||||
sortState: {
|
||||
direction: 'desc',
|
||||
field: 'averageCpuUsagePercent',
|
||||
},
|
||||
timerange: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
setSortState: {
|
||||
action: 'Sort field or direction changed',
|
||||
|
@ -42,58 +57,100 @@ export default {
|
|||
},
|
||||
} as Meta;
|
||||
|
||||
const storyArgs: Omit<PodMetricsTableProps, 'setSortState' | 'setCurrentPageIndex'> = {
|
||||
isLoading: false,
|
||||
pods: [
|
||||
{
|
||||
id: '358d96e3-026f-4440-a487-f6c2301884c0',
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg1',
|
||||
uptime: 23000000,
|
||||
averageCpuUsagePercent: 99,
|
||||
averageMemoryUsageMegabytes: 34,
|
||||
},
|
||||
{
|
||||
id: '358d96e3-026f-4440-a487-f6c2301884c1',
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg2',
|
||||
uptime: 43000000,
|
||||
averageCpuUsagePercent: 72,
|
||||
averageMemoryUsageMegabytes: 68,
|
||||
},
|
||||
{
|
||||
id: '358d96e3-026f-4440-a487-f6c2301884c0',
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg3',
|
||||
uptime: 53000000,
|
||||
averageCpuUsagePercent: 54,
|
||||
averageMemoryUsageMegabytes: 132,
|
||||
},
|
||||
{
|
||||
id: '358d96e3-026f-4440-a487-f6c2301884c0',
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg4',
|
||||
uptime: 63000000,
|
||||
averageCpuUsagePercent: 34,
|
||||
averageMemoryUsageMegabytes: 264,
|
||||
},
|
||||
{
|
||||
id: '358d96e3-026f-4440-a487-f6c2301884c0',
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg5',
|
||||
uptime: 83000000,
|
||||
averageCpuUsagePercent: 13,
|
||||
averageMemoryUsageMegabytes: 512,
|
||||
},
|
||||
],
|
||||
currentPageIndex: 0,
|
||||
pageCount: 10,
|
||||
sortState: {
|
||||
direction: 'desc',
|
||||
field: 'averageCpuUsagePercent',
|
||||
const loadedPods: PodNodeMetricsRow[] = [
|
||||
{
|
||||
id: '358d96e3-026f-4440-a487-f6c2301884c0',
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg1',
|
||||
uptime: 23000000,
|
||||
averageCpuUsagePercent: 99,
|
||||
averageMemoryUsageMegabytes: 34,
|
||||
},
|
||||
timerange: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
{
|
||||
id: '358d96e3-026f-4440-a487-f6c2301884c1',
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg2',
|
||||
uptime: 43000000,
|
||||
averageCpuUsagePercent: 72,
|
||||
averageMemoryUsageMegabytes: 68,
|
||||
},
|
||||
{
|
||||
id: '358d96e3-026f-4440-a487-f6c2301884c0',
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg3',
|
||||
uptime: 53000000,
|
||||
averageCpuUsagePercent: 54,
|
||||
averageMemoryUsageMegabytes: 132,
|
||||
},
|
||||
{
|
||||
id: '358d96e3-026f-4440-a487-f6c2301884c0',
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg4',
|
||||
uptime: 63000000,
|
||||
averageCpuUsagePercent: 34,
|
||||
averageMemoryUsageMegabytes: 264,
|
||||
},
|
||||
{
|
||||
id: '358d96e3-026f-4440-a487-f6c2301884c0',
|
||||
name: 'gke-edge-oblt-pool-1-9a60016d-lgg5',
|
||||
uptime: 83000000,
|
||||
averageCpuUsagePercent: 13,
|
||||
averageMemoryUsageMegabytes: 512,
|
||||
},
|
||||
];
|
||||
|
||||
const Template: Story<PodMetricsTableProps> = (args) => {
|
||||
return <PodMetricsTable {...args} />;
|
||||
};
|
||||
|
||||
export const Basic = Template.bind({});
|
||||
Basic.args = {
|
||||
data: {
|
||||
state: 'data',
|
||||
currentPageIndex: 1,
|
||||
pageCount: 10,
|
||||
rows: loadedPods,
|
||||
},
|
||||
};
|
||||
|
||||
export const Demo = (args: PodMetricsTableProps) => {
|
||||
return <PodMetricsTable {...args} />;
|
||||
export const Loading = Template.bind({});
|
||||
Loading.args = {
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
export const Reloading = Template.bind({});
|
||||
Reloading.args = {
|
||||
data: {
|
||||
state: 'data',
|
||||
currentPageIndex: 1,
|
||||
pageCount: 10,
|
||||
rows: loadedPods,
|
||||
},
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
export const MissingIndices = Template.bind({});
|
||||
MissingIndices.args = {
|
||||
data: {
|
||||
state: 'no-indices',
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyIndices = Template.bind({});
|
||||
EmptyIndices.args = {
|
||||
data: {
|
||||
state: 'empty-indices',
|
||||
},
|
||||
};
|
||||
|
||||
export const FailedToLoadSource = Template.bind({});
|
||||
FailedToLoadSource.args = {
|
||||
data: {
|
||||
state: 'error',
|
||||
errors: [new Error('Failed to load source configuration')],
|
||||
},
|
||||
};
|
||||
|
||||
export const FailedToLoadMetrics = Template.bind({});
|
||||
FailedToLoadMetrics.args = {
|
||||
data: {
|
||||
state: 'error',
|
||||
errors: [new Error('Failed to load metrics')],
|
||||
},
|
||||
};
|
||||
Demo.args = storyArgs;
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { HttpFetchOptions } from '@kbn/core/public';
|
||||
import { CoreProviders } from '../../../apps/common_providers';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import type { HttpFetchOptions } from '@kbn/core/public';
|
||||
import type { MetricsExplorerSeries } from '../../../../common/http_api';
|
||||
import type {
|
||||
DataResponseMock,
|
||||
NodeMetricsTableFetchMock,
|
||||
|
@ -16,8 +18,8 @@ import type {
|
|||
import { createStartServicesAccessorMock } from '../test_helpers';
|
||||
import { createLazyPodMetricsTable } from './create_lazy_pod_metrics_table';
|
||||
import IntegratedPodMetricsTable from './integrated_pod_metrics_table';
|
||||
import { PodMetricsTable } from './pod_metrics_table';
|
||||
import { metricByField } from './use_pod_metrics_table';
|
||||
import type { MetricsExplorerSeries } from '../../../../common/http_api';
|
||||
|
||||
describe('PodMetricsTable', () => {
|
||||
const timerange = {
|
||||
|
@ -40,6 +42,8 @@ describe('PodMetricsTable', () => {
|
|||
|
||||
const fetchMock = createFetchMock();
|
||||
|
||||
const loadingIndicatorTestId = 'metricsTableLoadingContent';
|
||||
|
||||
describe('createLazyPodMetricsTable', () => {
|
||||
it('should lazily load and render the table', async () => {
|
||||
const { fetch, getStartServices } = createStartServicesAccessorMock(fetchMock);
|
||||
|
@ -47,7 +51,7 @@ describe('PodMetricsTable', () => {
|
|||
|
||||
render(<LazyPodMetricsTable timerange={timerange} filterClauseDsl={filterClauseDsl} />);
|
||||
|
||||
expect(screen.queryByTestId('podMetricsTableLoader')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(loadingIndicatorTestId)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('podMetricsTable')).not.toBeInTheDocument();
|
||||
|
||||
// Using longer time out since resolving dynamic import can be slow
|
||||
|
@ -56,7 +60,7 @@ describe('PodMetricsTable', () => {
|
|||
timeout: 10000,
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('podMetricsTableLoader')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(loadingIndicatorTestId)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('podMetricsTable')).toBeInTheDocument();
|
||||
}, 10000);
|
||||
});
|
||||
|
@ -79,6 +83,44 @@ describe('PodMetricsTable', () => {
|
|||
expect(await findByText(/some-pod/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a loading indicator on first load', () => {
|
||||
const { coreProvidersPropsMock } = createStartServicesAccessorMock(jest.fn());
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<CoreProviders {...coreProvidersPropsMock}>
|
||||
<PodMetricsTable
|
||||
data={{ state: 'unknown' }}
|
||||
isLoading={true}
|
||||
setCurrentPageIndex={jest.fn()}
|
||||
setSortState={jest.fn()}
|
||||
sortState={{ field: 'id', direction: 'asc' }}
|
||||
timerange={{ from: new Date().toISOString(), to: new Date().toISOString() }}
|
||||
/>
|
||||
</CoreProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId(loadingIndicatorTestId)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a prompt when indices are missing', () => {
|
||||
const { coreProvidersPropsMock } = createStartServicesAccessorMock(jest.fn());
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<CoreProviders {...coreProvidersPropsMock}>
|
||||
<PodMetricsTable
|
||||
data={{ state: 'no-indices' }}
|
||||
isLoading={false}
|
||||
setCurrentPageIndex={jest.fn()}
|
||||
setSortState={jest.fn()}
|
||||
sortState={{ field: 'id', direction: 'asc' }}
|
||||
timerange={{ from: new Date().toISOString(), to: new Date().toISOString() }}
|
||||
/>
|
||||
</CoreProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId('metricsTableLoadingContent')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function createFetchMock(): NodeMetricsTableFetchMock {
|
||||
|
@ -87,6 +129,9 @@ function createFetchMock(): NodeMetricsTableFetchMock {
|
|||
configuration: {
|
||||
metricAlias: 'some-index-pattern',
|
||||
},
|
||||
status: {
|
||||
metricIndicesExist: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -97,7 +142,7 @@ function createFetchMock(): NodeMetricsTableFetchMock {
|
|||
],
|
||||
};
|
||||
|
||||
return (path: string, options: HttpFetchOptions) => {
|
||||
return (path: string, _options: HttpFetchOptions) => {
|
||||
// options can be used to read body for filter clause
|
||||
if (path === '/api/metrics/source/default') {
|
||||
return Promise.resolve(sourceMock);
|
||||
|
|
|
@ -10,44 +10,37 @@ import type {
|
|||
EuiBasicTableColumn,
|
||||
EuiTableSortingType,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { SortState } from '../shared';
|
||||
import { MetricsNodeDetailsLink, NumberCell, StepwisePagination, UptimeCell } from '../shared';
|
||||
import {
|
||||
MetricsNodeDetailsLink,
|
||||
MetricsTableEmptyIndicesContent,
|
||||
MetricsTableErrorContent,
|
||||
MetricsTableLoadingContent,
|
||||
MetricsTableNoIndicesContent,
|
||||
NodeMetricsTableData,
|
||||
NumberCell,
|
||||
StepwisePagination,
|
||||
UptimeCell,
|
||||
} from '../shared';
|
||||
import type { PodNodeMetricsRow } from './use_pod_metrics_table';
|
||||
|
||||
export interface PodMetricsTableProps {
|
||||
data: NodeMetricsTableData<PodNodeMetricsRow>;
|
||||
isLoading: boolean;
|
||||
setCurrentPageIndex: (value: number) => void;
|
||||
setSortState: (state: SortState<PodNodeMetricsRow>) => void;
|
||||
sortState: SortState<PodNodeMetricsRow>;
|
||||
timerange: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
isLoading: boolean;
|
||||
pods: PodNodeMetricsRow[];
|
||||
pageCount: number;
|
||||
currentPageIndex: number;
|
||||
setCurrentPageIndex: (value: number) => void;
|
||||
sortState: SortState<PodNodeMetricsRow>;
|
||||
setSortState: (state: SortState<PodNodeMetricsRow>) => void;
|
||||
}
|
||||
|
||||
export const PodMetricsTable = (props: PodMetricsTableProps) => {
|
||||
const {
|
||||
timerange,
|
||||
isLoading,
|
||||
pods,
|
||||
pageCount,
|
||||
currentPageIndex,
|
||||
setCurrentPageIndex,
|
||||
sortState,
|
||||
setSortState,
|
||||
} = props;
|
||||
const { data, isLoading, setCurrentPageIndex, setSortState, sortState, timerange } = props;
|
||||
|
||||
const columns = useMemo(() => podNodeColumns(timerange), [timerange]);
|
||||
|
||||
|
@ -66,42 +59,54 @@ export const PodMetricsTable = (props: PodMetricsTableProps) => {
|
|||
setCurrentPageIndex(0);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
if (data.state === 'error') {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center" direction="column">
|
||||
<EuiLoadingSpinner size="xl" data-test-subj="podMetricsTableLoader" />
|
||||
</EuiFlexGroup>
|
||||
<>
|
||||
{data.errors.map((error) => (
|
||||
<MetricsTableErrorContent error={error} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
} else if (isLoading && data.state !== 'data') {
|
||||
return <MetricsTableLoadingContent />;
|
||||
} else if (data.state === 'no-indices') {
|
||||
return <MetricsTableNoIndicesContent />;
|
||||
} else if (data.state === 'empty-indices') {
|
||||
return <MetricsTableEmptyIndicesContent />;
|
||||
} else if (data.state === 'data') {
|
||||
return (
|
||||
<>
|
||||
<EuiBasicTable
|
||||
tableCaption={i18n.translate('xpack.infra.metricsTable.pod.tableCaption', {
|
||||
defaultMessage: 'Infrastructure metrics for pods',
|
||||
})}
|
||||
items={data.rows}
|
||||
columns={columns}
|
||||
sorting={sorting}
|
||||
onChange={onTableSortChange}
|
||||
loading={isLoading}
|
||||
noItemsMessage={<MetricsTableLoadingContent />}
|
||||
data-test-subj="podMetricsTable"
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center" responsive={false} wrap>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StepwisePagination
|
||||
ariaLabel={i18n.translate('xpack.infra.metricsTable.pod.paginationAriaLabel', {
|
||||
defaultMessage: 'Pod metrics pagination',
|
||||
})}
|
||||
pageCount={data.pageCount}
|
||||
currentPageIndex={data.currentPageIndex}
|
||||
setCurrentPageIndex={setCurrentPageIndex}
|
||||
data-test-subj="podMetricsTablePagination"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiBasicTable
|
||||
tableCaption={i18n.translate('xpack.infra.metricsTable.pod.tableCaption', {
|
||||
defaultMessage: 'Infrastructure metrics for pods',
|
||||
})}
|
||||
items={pods}
|
||||
columns={columns}
|
||||
sorting={sorting}
|
||||
onChange={onTableSortChange}
|
||||
data-test-subj="podMetricsTable"
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center" responsive={false} wrap>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StepwisePagination
|
||||
ariaLabel={i18n.translate('xpack.infra.metricsTable.pod.paginationAriaLabel', {
|
||||
defaultMessage: 'Pod metrics pagination',
|
||||
})}
|
||||
pageCount={pageCount}
|
||||
currentPageIndex={currentPageIndex}
|
||||
setCurrentPageIndex={setCurrentPageIndex}
|
||||
data-test-subj="podMetricsTablePagination"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function podNodeColumns(
|
||||
|
|
|
@ -71,11 +71,7 @@ export function usePodMetricsTable({ timerange, filterClauseDsl }: UseNodeMetric
|
|||
[filterClauseDsl]
|
||||
);
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
nodes: pods,
|
||||
pageCount,
|
||||
} = useInfrastructureNodeMetrics<PodNodeMetricsRow>({
|
||||
const { data, isLoading } = useInfrastructureNodeMetrics<PodNodeMetricsRow>({
|
||||
metricsExplorerOptions: podMetricsOptions,
|
||||
timerange,
|
||||
transform: seriesToPodNodeMetricsRow,
|
||||
|
@ -84,14 +80,13 @@ export function usePodMetricsTable({ timerange, filterClauseDsl }: UseNodeMetric
|
|||
});
|
||||
|
||||
return {
|
||||
timerange,
|
||||
isLoading,
|
||||
pods,
|
||||
pageCount,
|
||||
currentPageIndex,
|
||||
data,
|
||||
isLoading,
|
||||
setCurrentPageIndex,
|
||||
sortState,
|
||||
setSortState,
|
||||
sortState,
|
||||
timerange,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 206 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 206 KiB |
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
export const MetricsTableErrorContent = ({ error }: { error: Error }) => (
|
||||
<EuiEmptyPrompt
|
||||
body={
|
||||
<EuiCodeBlock className="eui-textLeft" isCopyable language="jsstacktrace">
|
||||
{error.stack ?? `${error}`}
|
||||
</EuiCodeBlock>
|
||||
}
|
||||
color="danger"
|
||||
data-test-subj="metricsTableErrorContent"
|
||||
iconType="alert"
|
||||
title={<h2>{error.message}</h2>}
|
||||
titleSize="s"
|
||||
/>
|
||||
);
|
|
@ -5,7 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { MetricsTableErrorContent } from './error_content';
|
||||
export { MetricsNodeDetailsLink } from './metrics_node_details_link';
|
||||
export {
|
||||
MetricsTableEmptyIndicesContent,
|
||||
MetricsTableLoadingContent,
|
||||
MetricsTableNoIndicesContent,
|
||||
} from './no_data_content';
|
||||
export { NumberCell } from './number_cell';
|
||||
export { StepwisePagination } from './stepwise_pagination';
|
||||
export { UptimeCell } from './uptime_cell';
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 {
|
||||
COLOR_MODES_STANDARD,
|
||||
EuiButton,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListDescription,
|
||||
EuiDescriptionListTitle,
|
||||
EuiEmptyPrompt,
|
||||
EuiImage,
|
||||
EuiLoadingLogo,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useLinkProps } from '@kbn/observability-plugin/public';
|
||||
import React from 'react';
|
||||
import {
|
||||
noMetricIndicesPromptDescription,
|
||||
noMetricIndicesPromptPrimaryActionTitle,
|
||||
noMetricIndicesPromptTitle,
|
||||
} from '../../../empty_states';
|
||||
import noResultsIllustrationDark from './assets/no_results_dark.svg';
|
||||
import noResultsIllustrationLight from './assets/no_results_light.svg';
|
||||
|
||||
export const MetricsTableLoadingContent = () => (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="metricsTableLoadingContent"
|
||||
icon={<EuiLoadingLogo logo="logoMetrics" size="xl" />}
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metricsTable.loadingContentTitle"
|
||||
defaultMessage="Loading metrics"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export const MetricsTableNoIndicesContent = () => {
|
||||
const integrationsLinkProps = useLinkProps({ app: 'integrations', pathname: 'browse' });
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="metricsTableLoadingContent"
|
||||
iconType="logoMetrics"
|
||||
title={<h2>{noMetricIndicesPromptTitle}</h2>}
|
||||
body={<p>{noMetricIndicesPromptDescription}</p>}
|
||||
actions={
|
||||
<EuiButton color="primary" fill {...integrationsLinkProps}>
|
||||
{noMetricIndicesPromptPrimaryActionTitle}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const MetricsTableEmptyIndicesContent = () => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
body={
|
||||
<EuiDescriptionList compressed>
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metricsTable.emptyIndicesPromptTimeRangeHintTitle"
|
||||
defaultMessage="Expand your time range"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metricsTable.emptyIndicesPromptTimeRangeHintDescription"
|
||||
defaultMessage="Try searching over a longer period of time."
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metricsTable.emptyIndicesPromptQueryHintTitle"
|
||||
defaultMessage="Adjust your query"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metricsTable.emptyIndicesPromptQueryHintDescription"
|
||||
defaultMessage="Try searching for a different combination of terms."
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
}
|
||||
color="subdued"
|
||||
data-test-subj="metricsTableEmptyIndicesContent"
|
||||
icon={<NoResultsIllustration />}
|
||||
layout="horizontal"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metricsTable.emptyIndicesPromptTitle"
|
||||
defaultMessage="No results match your search criteria"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="m"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const NoResultsIllustration = () => {
|
||||
const { colorMode } = useEuiTheme();
|
||||
|
||||
const illustration =
|
||||
colorMode === COLOR_MODES_STANDARD.dark
|
||||
? noResultsIllustrationDark
|
||||
: noResultsIllustrationLight;
|
||||
|
||||
return (
|
||||
<EuiImage alt={noResultsIllustrationAlternativeText} size="fullWidth" src={illustration} />
|
||||
);
|
||||
};
|
||||
|
||||
const noResultsIllustrationAlternativeText = i18n.translate(
|
||||
'xpack.infra.metricsTable.noResultsIllustrationAlternativeText',
|
||||
{ defaultMessage: 'A magnifying glass with an exclamation mark' }
|
||||
);
|
|
@ -19,6 +19,7 @@ import type {
|
|||
MetricsExplorerTimeOptions,
|
||||
} from '../../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { useTrackedPromise } from '../../../../utils/use_tracked_promise';
|
||||
import { NodeMetricsTableData } from '../types';
|
||||
|
||||
export interface SortState<T> {
|
||||
field: keyof T;
|
||||
|
@ -51,10 +52,10 @@ export const useInfrastructureNodeMetrics = <T>(
|
|||
|
||||
const [transformedNodes, setTransformedNodes] = useState<T[]>([]);
|
||||
const fetch = useKibanaHttpFetch();
|
||||
const { source, isLoadingSource } = useSourceContext();
|
||||
const { source, isLoadingSource, loadSourceRequest, metricIndicesExist } = useSourceContext();
|
||||
const timerangeWithInterval = useTimerangeWithInterval(timerange);
|
||||
|
||||
const [{ state: promiseState }, fetchNodes] = useTrackedPromise(
|
||||
const [fetchNodesRequest, fetchNodes] = useTrackedPromise(
|
||||
{
|
||||
createPromise: (): Promise<MetricsExplorerResponse> => {
|
||||
if (!source) {
|
||||
|
@ -78,16 +79,22 @@ export const useInfrastructureNodeMetrics = <T>(
|
|||
onResolve: (response: MetricsExplorerResponse) => {
|
||||
setTransformedNodes(response.series.map(transform));
|
||||
},
|
||||
onReject: (error) => {
|
||||
// What to do about this?
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
},
|
||||
cancelPreviousOn: 'creation',
|
||||
},
|
||||
[source, metricsExplorerOptions, timerangeWithInterval]
|
||||
);
|
||||
const isLoadingNodes = promiseState === 'pending' || promiseState === 'uninitialized';
|
||||
|
||||
const isLoadingNodes =
|
||||
fetchNodesRequest.state === 'pending' || fetchNodesRequest.state === 'uninitialized';
|
||||
const isLoading = isLoadingSource || isLoadingNodes;
|
||||
|
||||
const errors = useMemo<Error[]>(
|
||||
() => [
|
||||
...(loadSourceRequest.state === 'rejected' ? [wrapAsError(loadSourceRequest.value)] : []),
|
||||
...(fetchNodesRequest.state === 'rejected' ? [wrapAsError(fetchNodesRequest.value)] : []),
|
||||
],
|
||||
[fetchNodesRequest, loadSourceRequest]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchNodes();
|
||||
|
@ -109,10 +116,23 @@ export const useInfrastructureNodeMetrics = <T>(
|
|||
|
||||
const pageCount = useMemo(() => Math.ceil(top100Nodes.length / TABLE_PAGE_SIZE), [top100Nodes]);
|
||||
|
||||
const data = useMemo<NodeMetricsTableData<T>>(
|
||||
() =>
|
||||
errors.length > 0
|
||||
? { state: 'error', errors }
|
||||
: metricIndicesExist == null
|
||||
? { state: 'unknown' }
|
||||
: !metricIndicesExist
|
||||
? { state: 'no-indices' }
|
||||
: nodes.length <= 0
|
||||
? { state: 'empty-indices' }
|
||||
: { state: 'data', currentPageIndex, pageCount, rows: nodes },
|
||||
[currentPageIndex, errors, metricIndicesExist, nodes, pageCount]
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading: isLoadingSource || isLoadingNodes,
|
||||
nodes,
|
||||
pageCount,
|
||||
isLoading,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -188,3 +208,5 @@ function sortDescending(nodeAValue: unknown, nodeBValue: unknown) {
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const wrapAsError = (value: any): Error => (value instanceof Error ? value : new Error(`${value}`));
|
||||
|
|
|
@ -5,7 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { MetricsNodeDetailsLink, NumberCell, StepwisePagination, UptimeCell } from './components';
|
||||
export {
|
||||
MetricsNodeDetailsLink,
|
||||
MetricsTableEmptyIndicesContent,
|
||||
MetricsTableErrorContent,
|
||||
MetricsTableLoadingContent,
|
||||
MetricsTableNoIndicesContent,
|
||||
NumberCell,
|
||||
StepwisePagination,
|
||||
UptimeCell,
|
||||
} from './components';
|
||||
export {
|
||||
averageOfValues,
|
||||
createMetricByFieldLookup,
|
||||
|
@ -17,6 +26,7 @@ export {
|
|||
export type { MetricsMap, MetricsQueryOptions, SortState } from './hooks';
|
||||
export type {
|
||||
IntegratedNodeMetricsTableProps,
|
||||
NodeMetricsTableData,
|
||||
SourceProviderProps,
|
||||
UseNodeMetricsTableOptions,
|
||||
} from './types';
|
||||
|
|
|
@ -21,3 +21,24 @@ export interface SourceProviderProps {
|
|||
export type IntegratedNodeMetricsTableProps = UseNodeMetricsTableOptions &
|
||||
SourceProviderProps &
|
||||
CoreProvidersProps;
|
||||
|
||||
export type NodeMetricsTableData<NodeMetricsRow> =
|
||||
| {
|
||||
state: 'unknown';
|
||||
}
|
||||
| {
|
||||
state: 'no-indices';
|
||||
}
|
||||
| {
|
||||
state: 'empty-indices';
|
||||
}
|
||||
| {
|
||||
state: 'data';
|
||||
currentPageIndex: number;
|
||||
pageCount: number;
|
||||
rows: NodeMetricsRow[];
|
||||
}
|
||||
| {
|
||||
state: 'error';
|
||||
errors: Error[];
|
||||
};
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DeepPartial } from 'utility-types';
|
||||
import type { HttpFetchOptions } from '@kbn/core/public';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { DeepPartial } from 'utility-types';
|
||||
import type { MetricsExplorerResponse } from '../../../common/http_api/metrics_explorer';
|
||||
import type { MetricsSourceConfigurationResponse } from '../../../common/metrics_sources';
|
||||
import type { CoreProvidersProps } from '../../apps/common_providers';
|
||||
|
@ -28,6 +29,7 @@ export function createStartServicesAccessorMock(fetchMock: NodeMetricsTableFetch
|
|||
const core = coreMock.createStart();
|
||||
// @ts-expect-error core.http.fetch has overloads, Jest/TypeScript only picks the first definition when mocking
|
||||
core.http.fetch.mockImplementation(fetchMock);
|
||||
core.i18n.Context.mockImplementation(I18nProvider as () => JSX.Element);
|
||||
|
||||
const coreProvidersPropsMock: CoreProvidersProps = {
|
||||
core,
|
||||
|
|
|
@ -145,6 +145,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => {
|
|||
isUninitialized,
|
||||
hasFailedLoadingSource: loadSourceRequest.state === 'rejected',
|
||||
loadSource,
|
||||
loadSourceRequest,
|
||||
loadSourceFailureMessage:
|
||||
loadSourceRequest.state === 'rejected' ? `${loadSourceRequest.value}` : undefined,
|
||||
metricIndicesExist,
|
||||
|
|
|
@ -5,10 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-plugin/public';
|
||||
import { KibanaPageTemplateProps } from '@kbn/shared-ux-components';
|
||||
import React from 'react';
|
||||
import {
|
||||
noMetricIndicesPromptDescription,
|
||||
noMetricIndicesPromptPrimaryActionTitle,
|
||||
} from '../../components/empty_states';
|
||||
import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
|
||||
|
||||
interface MetricsPageTemplateProps extends LazyObservabilityPageTemplateProps {
|
||||
|
@ -37,13 +41,8 @@ export const MetricsPageTemplate: React.FC<MetricsPageTemplateProps> = ({
|
|||
}),
|
||||
action: {
|
||||
beats: {
|
||||
title: i18n.translate('xpack.infra.metrics.noDataConfig.beatsCard.title', {
|
||||
defaultMessage: 'Add a metrics integration',
|
||||
}),
|
||||
description: i18n.translate('xpack.infra.metrics.noDataConfig.beatsCard.description', {
|
||||
defaultMessage:
|
||||
'Use Beats to send metrics data to Elasticsearch. We make it easy with modules for many popular systems and apps.',
|
||||
}),
|
||||
title: noMetricIndicesPromptPrimaryActionTitle,
|
||||
description: noMetricIndicesPromptDescription,
|
||||
},
|
||||
},
|
||||
docsLink: docLinks.links.observability.guide,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue