Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 from 'react';
import { EuiSpacer } from '@elastic/eui';

import { i18n } from '@kbn/i18n';
import { InfraLoadingPanel } from '../../../../components/loading';
import { useMetricsDataViewContext } from '../hooks/use_data_view';
import { UnifiedSearchBar } from './unified_search_bar';
import { HostsTable } from './hosts_table';

export const HostContainer = () => {
const { metricsDataView, isDataViewLoading, hasFailedLoadingDataView } =
useMetricsDataViewContext();

if (isDataViewLoading) {
return (
<InfraLoadingPanel
height="100%"
width="auto"
text={i18n.translate('xpack.infra.waffle.loadingDataText', {
defaultMessage: 'Loading data',
})}
/>
);
}

return hasFailedLoadingDataView || !metricsDataView ? null : (
<>
<UnifiedSearchBar dataView={metricsDataView} />
<EuiSpacer />
<HostsTable />
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,86 @@

import React from 'react';
import { EuiInMemoryTable } from '@elastic/eui';
import type { SnapshotNode } from '../../../../../common/http_api';
import { i18n } from '@kbn/i18n';
import { HostsTableColumns } from './hosts_table_columns';
import { NoData } from '../../../../components/empty_states';
import { InfraLoadingPanel } from '../../../../components/loading';
import { useHostTable } from '../hooks/use_host_table';
import { useSnapshot } from '../../inventory_view/hooks/use_snaphot';
import type { SnapshotMetricType } from '../../../../../common/inventory_models/types';
import type { InfraTimerangeInput } from '../../../../../common/http_api';
import { useUnifiedSearchContext } from '../hooks/use_unified_search';
import { useSourceContext } from '../../../../containers/metrics_source';

interface Props {
nodes: SnapshotNode[];
}
const HOST_METRICS: Array<{ type: SnapshotMetricType }> = [
{ type: 'rx' },
{ type: 'tx' },
{ type: 'memory' },
{ type: 'cpuCores' },
{ type: 'memoryTotal' },
];

export const HostsTable = () => {
const { sourceId } = useSourceContext();
const { esQuery, dateRangeTimestamp } = useUnifiedSearchContext();

const timeRange: InfraTimerangeInput = {
from: dateRangeTimestamp.from,
to: dateRangeTimestamp.to,
interval: '1m',
ignoreLookback: true,
};

// Snapshot endpoint internally uses the indices stored in source.configuration.metricAlias.
// For the Unified Search, we create a data view, which for now will be built off of source.configuration.metricAlias too
// if we introduce data view selection, we'll have to change this hook and the endpoint to accept a new parameter for the indices
const { loading, nodes, reload } = useSnapshot(
esQuery && JSON.stringify(esQuery),
HOST_METRICS,
[],
'host',
sourceId,
dateRangeTimestamp.to,
'',
'',
true,
timeRange
);

export const HostsTable: React.FunctionComponent<Props> = ({ nodes }) => {
const items = useHostTable(nodes);
const noData = items.length === 0;

return <EuiInMemoryTable pagination sorting items={items} columns={HostsTableColumns} />;
return (
<>
{loading ? (
<InfraLoadingPanel
height="100%"
width="auto"
text={i18n.translate('xpack.infra.waffle.loadingDataText', {
defaultMessage: 'Loading data',
})}
/>
) : noData ? (
<div>
<NoData
titleText={i18n.translate('xpack.infra.waffle.noDataTitle', {
defaultMessage: 'There is no data to display.',
})}
bodyText={i18n.translate('xpack.infra.waffle.noDataDescription', {
defaultMessage: 'Try adjusting your time or filter.',
})}
refetchText={i18n.translate('xpack.infra.waffle.checkNewDataButtonLabel', {
defaultMessage: 'Check for new data',
})}
onRefetch={() => {
reload();
}}
testString="noMetricsDataPrompt"
/>
</div>
) : (
<EuiInMemoryTable pagination sorting items={items} columns={HostsTableColumns} />
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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 from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { Filter, Query, TimeRange } from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { SavedQuery } from '@kbn/data-plugin/public';
import type { InfraClientStartDeps } from '../../../../types';
import { useUnifiedSearchContext } from '../hooks/use_unified_search';

interface Props {
dataView: DataView;
}

export const UnifiedSearchBar = ({ dataView }: Props) => {
const {
services: { unifiedSearch },
} = useKibana<InfraClientStartDeps>();
const {
unifiedSearchDateRange,
unifiedSearchQuery,
submitFilterChange,
saveQuery,
clearSavedQUery,
} = useUnifiedSearchContext();

const { SearchBar } = unifiedSearch.ui;

const onFilterChange = (filters: Filter[]) => {
onQueryChange({ filters });
};

const onQuerySubmit = (payload: { dateRange: TimeRange; query?: Query }) => {
onQueryChange({ payload });
};

const onClearSavedQuery = () => {
clearSavedQUery();
};

const onQuerySave = (savedQuery: SavedQuery) => {
saveQuery(savedQuery);
};

const onQueryChange = ({
payload,
filters,
}: {
payload?: { dateRange: TimeRange; query?: Query };
filters?: Filter[];
}) => {
submitFilterChange(payload?.query, payload?.dateRange, filters);
};

return (
<SearchBar
appName={'Infra Hosts'}
indexPatterns={[dataView]}
query={unifiedSearchQuery}
dateRangeFrom={unifiedSearchDateRange.from}
dateRangeTo={unifiedSearchDateRange.to}
onQuerySubmit={onQuerySubmit}
onSaved={onQuerySave}
onSavedQueryUpdated={onQuerySave}
onClearSavedQuery={onClearSavedQuery}
showSaveQuery
showQueryInput
// @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove it once this PR is merged

onFiltersUpdated={onFilterChange}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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 { useDataView } from './use_data_view';
import { renderHook } from '@testing-library/react-hooks';
import { KibanaReactContextValue, useKibana } from '@kbn/kibana-react-plugin/public';
import { coreMock, notificationServiceMock } from '@kbn/core/public/mocks';
import type { DataView } from '@kbn/data-views-plugin/public';
import { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types';
import { InfraClientStartDeps } from '../../../../types';
import { CoreStart } from '@kbn/core/public';

jest.mock('@kbn/i18n');
jest.mock('@kbn/kibana-react-plugin/public');

let dataViewMock: jest.Mocked<DataViewsServicePublic>;
const useKibanaMock = useKibana as jest.MockedFunction<typeof useKibana>;
const notificationMock = notificationServiceMock.createStartContract();
const prop = { metricAlias: 'test' };

const mockUseKibana = () => {
useKibanaMock.mockReturnValue({
services: {
...coreMock.createStart(),
notifications: notificationMock,
dataViews: dataViewMock,
} as Partial<CoreStart> & Partial<InfraClientStartDeps>,
} as unknown as KibanaReactContextValue<Partial<CoreStart> & Partial<InfraClientStartDeps>>);
};

const mockDataView = {
id: 'mock-id',
title: 'mock-title',
timeFieldName: 'mock-time-field-name',
isPersisted: () => false,
getName: () => 'mock-data-view',
toSpec: () => ({}),
} as jest.Mocked<DataView>;

describe('useHostTable hook', () => {
beforeEach(() => {
dataViewMock = {
createAndSave: jest.fn(),
find: jest.fn(),
} as Partial<DataViewsServicePublic> as jest.Mocked<DataViewsServicePublic>;

mockUseKibana();
});

it('should find an existing Data view', async () => {
dataViewMock.find.mockReturnValue(Promise.resolve([mockDataView]));
const { result, waitForNextUpdate } = renderHook(() => useDataView(prop));

await waitForNextUpdate();
expect(result.current.isDataViewLoading).toEqual(false);
expect(result.current.hasFailedLoadingDataView).toEqual(false);
expect(result.current.metricsDataView).toEqual(mockDataView);
});

it('should create a new Data view', async () => {
dataViewMock.find.mockReturnValue(Promise.resolve([]));
dataViewMock.createAndSave.mockReturnValue(Promise.resolve(mockDataView));
const { result, waitForNextUpdate } = renderHook(() => useDataView(prop));

await waitForNextUpdate();
expect(result.current.isDataViewLoading).toEqual(false);
expect(result.current.hasFailedLoadingDataView).toEqual(false);
expect(result.current.metricsDataView).toEqual(mockDataView);
});

it('should display a toast when it fails to load the data view', async () => {
dataViewMock.find.mockReturnValue(Promise.reject());
const { result, waitForNextUpdate } = renderHook(() => useDataView(prop));

await waitForNextUpdate();
expect(result.current.isDataViewLoading).toEqual(false);
expect(result.current.hasFailedLoadingDataView).toEqual(true);
expect(result.current.metricsDataView).toBeUndefined();
expect(notificationMock.toasts.addDanger).toBeCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* 2.0.
*/

import { useCallback, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { useCallback, useState, useEffect, useMemo } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import createContainer from 'constate';
import type { DataView } from '@kbn/data-views-plugin/public';
Expand All @@ -15,7 +16,7 @@ import { useTrackedPromise } from '../../../../utils/use_tracked_promise';
export const useDataView = ({ metricAlias }: { metricAlias: string }) => {
const [metricsDataView, setMetricsDataView] = useState<DataView>();
const {
services: { dataViews },
services: { dataViews, notifications },
} = useKibana<InfraClientStartDeps>();

const [createDataViewRequest, createDataView] = useTrackedPromise(
Expand All @@ -33,7 +34,7 @@ export const useDataView = ({ metricAlias }: { metricAlias: string }) => {

const [getDataViewRequest, getDataView] = useTrackedPromise(
{
createPromise: (indexPattern: string): Promise<DataView[]> => {
createPromise: (_indexPattern: string): Promise<DataView[]> => {
return dataViews.find(metricAlias, 1);
},
onResolve: (response: DataView[]) => {
Expand All @@ -58,17 +59,36 @@ export const useDataView = ({ metricAlias }: { metricAlias: string }) => {
}
}, [metricAlias, createDataView, getDataView]);

const hasFailedFetchingDataView = getDataViewRequest.state === 'rejected';
const hasFailedCreatingDataView = createDataViewRequest.state === 'rejected';
const isDataViewLoading = useMemo(
() => getDataViewRequest.state === 'pending' || createDataViewRequest.state === 'pending',
[getDataViewRequest.state, createDataViewRequest.state]
);

const hasFailedLoadingDataView = useMemo(
() => getDataViewRequest.state === 'rejected' || createDataViewRequest.state === 'rejected',
[getDataViewRequest.state, createDataViewRequest.state]
);

useEffect(() => {
loadDataView();
}, [metricAlias, loadDataView]);

useEffect(() => {
if (hasFailedLoadingDataView && notifications) {
notifications.toasts.addDanger(
i18n.translate('xpack.infra.hostsTable.errorOnCreateOrLoadDataview', {
defaultMessage:
'There was an error trying to load or create the Data View: {metricAlias}',
values: { metricAlias },
})
);
}
}, [hasFailedLoadingDataView, notifications, metricAlias]);

return {
metricsDataView,
hasFailedCreatingDataView,
hasFailedFetchingDataView,
isDataViewLoading,
hasFailedLoadingDataView,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for improving this hook!

};
};

Expand Down
Loading