[Infrastructure UI] inital hosts page (#138173)

* inital empty hosts page and navigation item

* lens table and unified search

* add data view id

* cleanup

* clear searchs session when unmounting

* cleanup

* add some basic error handling for Data View

* change back loading text for now because of breaking test
This commit is contained in:
Sandra G 2022-08-26 16:09:46 -04:00 committed by GitHub
parent a16fd1e033
commit 7e1b60fa22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 572 additions and 1 deletions

View file

@ -15,7 +15,8 @@
"triggersActionsUi",
"observability",
"ruleRegistry",
"unifiedSearch"
"unifiedSearch",
"lens"
],
"optionalPlugins": ["ml", "home", "embeddable", "osquery"],
"server": true,

View file

@ -46,6 +46,7 @@ export const renderApp = (
return () => {
ReactDOM.unmountComponentAtNode(element);
plugins.data.search.session.clear();
};
};

View file

@ -0,0 +1,300 @@
/*
* 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 { useKibana } from '@kbn/kibana-react-plugin/public';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import type { Query, TimeRange } from '@kbn/es-query';
import React from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { InfraClientStartDeps } from '../../../../types';
const getLensHostsTable = (
metricsDataView: DataView,
query: Query
): TypedLensByValueInput['attributes'] =>
({
visualizationType: 'lnsDatatable',
title: 'Lens visualization',
references: [
{
id: metricsDataView.id,
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: metricsDataView.id,
name: 'indexpattern-datasource-layer-cbe5d8a0-381d-49bf-b8ac-f8f312ec7129',
type: 'index-pattern',
},
],
state: {
datasourceStates: {
indexpattern: {
layers: {
'cbe5d8a0-381d-49bf-b8ac-f8f312ec7129': {
columns: {
'8d17458d-31af-41d1-a23c-5180fd960bee': {
label: 'Name',
dataType: 'string',
operationType: 'terms',
scale: 'ordinal',
sourceField: 'host.name',
isBucketed: true,
params: {
size: 10000,
orderBy: {
type: 'column',
columnId: '467de550-9186-4e18-8cfa-bce07087801a',
},
orderDirection: 'desc',
otherBucket: true,
missingBucket: false,
parentFormat: {
id: 'terms',
},
},
customLabel: true,
},
'155fc728-d010-498e-8126-0bc46cad2be2': {
label: 'Operating system',
dataType: 'string',
operationType: 'terms',
scale: 'ordinal',
sourceField: 'host.os.name',
isBucketed: true,
params: {
size: 10000,
orderBy: {
type: 'column',
columnId: '467de550-9186-4e18-8cfa-bce07087801a',
},
orderDirection: 'desc',
otherBucket: false,
missingBucket: false,
parentFormat: {
id: 'terms',
},
},
customLabel: true,
},
'467de550-9186-4e18-8cfa-bce07087801a': {
label: '# of CPUs',
dataType: 'number',
operationType: 'max',
sourceField: 'system.cpu.cores',
isBucketed: false,
scale: 'ratio',
params: {
emptyAsNull: true,
},
customLabel: true,
},
'0a9bd0fa-9966-489b-8c95-70997a7aad4cX0': {
label: 'Part of Memory Total (avg)',
dataType: 'number',
operationType: 'average',
sourceField: 'system.memory.total',
isBucketed: false,
scale: 'ratio',
params: {
emptyAsNull: false,
},
customLabel: true,
},
'0a9bd0fa-9966-489b-8c95-70997a7aad4c': {
label: 'Memory total (avg.)',
dataType: 'number',
operationType: 'formula',
isBucketed: false,
scale: 'ratio',
params: {
formula: 'average(system.memory.total)',
isFormulaBroken: false,
format: {
id: 'bytes',
params: {
decimals: 0,
},
},
},
references: ['0a9bd0fa-9966-489b-8c95-70997a7aad4cX0'],
customLabel: true,
},
'fe5a4d7d-6f48-45ab-974c-96bc864ac36fX0': {
label: 'Part of Memory Usage (avg)',
dataType: 'number',
operationType: 'average',
sourceField: 'system.memory.used.pct',
isBucketed: false,
scale: 'ratio',
params: {
emptyAsNull: false,
},
customLabel: true,
},
'fe5a4d7d-6f48-45ab-974c-96bc864ac36f': {
label: 'Memory usage (avg.)',
dataType: 'number',
operationType: 'formula',
isBucketed: false,
scale: 'ratio',
params: {
formula: 'average(system.memory.used.pct)',
isFormulaBroken: false,
format: {
id: 'percent',
params: {
decimals: 0,
},
},
},
references: ['fe5a4d7d-6f48-45ab-974c-96bc864ac36fX0'],
customLabel: true,
},
'3eca2307-228e-4842-a023-57e15c8c364dX0': {
label: 'Part of Disk Latency (avg ms)',
dataType: 'number',
operationType: 'average',
sourceField: 'system.diskio.io.time',
isBucketed: false,
scale: 'ratio',
params: {
emptyAsNull: false,
},
customLabel: true,
},
'3eca2307-228e-4842-a023-57e15c8c364dX1': {
label: 'Part of Disk Latency (avg ms)',
dataType: 'number',
operationType: 'math',
isBucketed: false,
scale: 'ratio',
params: {
tinymathAst: {
type: 'function',
name: 'divide',
args: ['3eca2307-228e-4842-a023-57e15c8c364dX0', 1000],
location: {
min: 0,
max: 37,
},
text: 'average(system.diskio.io.time) / 1000',
},
},
references: ['3eca2307-228e-4842-a023-57e15c8c364dX0'],
customLabel: true,
},
'3eca2307-228e-4842-a023-57e15c8c364d': {
label: 'Disk latency (avg.)',
dataType: 'number',
operationType: 'formula',
isBucketed: false,
scale: 'ratio',
params: {
formula: 'average(system.diskio.io.time) / 1000',
isFormulaBroken: false,
format: {
id: 'number',
params: {
decimals: 0,
suffix: 'ms',
},
},
},
references: ['3eca2307-228e-4842-a023-57e15c8c364dX1'],
customLabel: true,
},
},
columnOrder: [
'8d17458d-31af-41d1-a23c-5180fd960bee',
'155fc728-d010-498e-8126-0bc46cad2be2',
'467de550-9186-4e18-8cfa-bce07087801a',
'3eca2307-228e-4842-a023-57e15c8c364d',
'0a9bd0fa-9966-489b-8c95-70997a7aad4c',
'fe5a4d7d-6f48-45ab-974c-96bc864ac36f',
'0a9bd0fa-9966-489b-8c95-70997a7aad4cX0',
'fe5a4d7d-6f48-45ab-974c-96bc864ac36fX0',
'3eca2307-228e-4842-a023-57e15c8c364dX0',
'3eca2307-228e-4842-a023-57e15c8c364dX1',
],
incompleteColumns: {},
indexPatternId: '305688db-9e02-4046-acc1-5d0d8dd73ef6',
},
},
},
},
visualization: {
layerId: 'cbe5d8a0-381d-49bf-b8ac-f8f312ec7129',
layerType: 'data',
columns: [
{
columnId: '8d17458d-31af-41d1-a23c-5180fd960bee',
width: 296.16666666666663,
},
{
columnId: '155fc728-d010-498e-8126-0bc46cad2be2',
isTransposed: false,
width: 152.36666666666667,
},
{
columnId: '467de550-9186-4e18-8cfa-bce07087801a',
isTransposed: false,
width: 121.11666666666667,
},
{
columnId: '0a9bd0fa-9966-489b-8c95-70997a7aad4c',
isTransposed: false,
},
{
columnId: 'fe5a4d7d-6f48-45ab-974c-96bc864ac36f',
isTransposed: false,
},
{
columnId: '3eca2307-228e-4842-a023-57e15c8c364d',
isTransposed: false,
},
],
paging: {
size: 10,
enabled: true,
},
headerRowHeight: 'custom',
headerRowHeightLines: 2,
rowHeight: 'single',
rowHeightLines: 1,
},
filters: [],
query,
},
} as TypedLensByValueInput['attributes']);
interface Props {
dataView: DataView;
timeRange: TimeRange;
query: Query;
searchSessionId: string;
}
export const HostsTable: React.FunctionComponent<Props> = ({
dataView,
timeRange,
query,
searchSessionId,
}) => {
const {
services: { lens },
} = useKibana<InfraClientStartDeps>();
const LensComponent = lens?.EmbeddableComponent;
return (
<LensComponent
id="hostsView"
timeRange={timeRange}
attributes={getLensHostsTable(dataView, query)}
searchSessionId={searchSessionId}
/>
);
};

View file

@ -0,0 +1,76 @@
/*
* 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 { useCallback, useState, useEffect } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import createContainer from 'constate';
import type { DataView } from '@kbn/data-views-plugin/public';
import { InfraClientStartDeps } from '../../../../types';
import { useTrackedPromise } from '../../../../utils/use_tracked_promise';
export const useDataView = ({ metricAlias }: { metricAlias: string }) => {
const [metricsDataView, setMetricsDataView] = useState<DataView>();
const {
services: { dataViews },
} = useKibana<InfraClientStartDeps>();
const [createDataViewRequest, createDataView] = useTrackedPromise(
{
createPromise: (config): Promise<DataView> => {
return dataViews.createAndSave(config);
},
onResolve: (response: DataView) => {
setMetricsDataView(response);
},
cancelPreviousOn: 'creation',
},
[]
);
const [getDataViewRequest, getDataView] = useTrackedPromise(
{
createPromise: (indexPattern: string): Promise<DataView[]> => {
return dataViews.find(metricAlias, 1);
},
onResolve: (response: DataView[]) => {
setMetricsDataView(response[0]);
},
cancelPreviousOn: 'creation',
},
[]
);
const loadDataView = useCallback(async () => {
try {
let view = (await getDataView(metricAlias))[0];
if (!view) {
view = await createDataView({
title: metricAlias,
timeFieldName: '@timestamp',
});
}
} catch (error) {
setMetricsDataView(undefined);
}
}, [metricAlias, createDataView, getDataView]);
const hasFailedFetchingDataView = getDataViewRequest.state === 'rejected';
const hasFailedCreatingDataView = createDataViewRequest.state === 'rejected';
useEffect(() => {
loadDataView();
}, [metricAlias, loadDataView]);
return {
metricsDataView,
hasFailedCreatingDataView,
hasFailedFetchingDataView,
};
};
export const MetricsDataView = createContainer(useDataView);
export const [MetricsDataViewProvider, useMetricsDataViewContext] = MetricsDataView;

View file

@ -0,0 +1,82 @@
/*
* 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 type { Query, TimeRange } from '@kbn/es-query';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { i18n } from '@kbn/i18n';
import React, { useState, useCallback } from 'react';
import { SearchBar } from '@kbn/unified-search-plugin/public';
import { InfraLoadingPanel } from '../../../components/loading';
import { useMetricsDataViewContext } from './hooks/use_data_view';
import { HostsTable } from './components/hosts_table';
import { InfraClientStartDeps } from '../../../types';
import { useSourceContext } from '../../../containers/metrics_source';
export const HostsContent: React.FunctionComponent = () => {
const {
services: { data },
} = useKibana<InfraClientStartDeps>();
const { source } = useSourceContext();
const [dateRange, setDateRange] = useState<TimeRange>({ from: 'now-15m', to: 'now' });
const [query, setQuery] = useState<Query>({ query: '', language: 'kuery' });
const { metricsDataView, hasFailedCreatingDataView, hasFailedFetchingDataView } =
useMetricsDataViewContext();
// needed to refresh the lens table when filters havent changed
const [searchSessionId, setSearchSessionId] = useState(data.search.session.start());
const onQuerySubmit = useCallback(
(payload: { dateRange: TimeRange; query?: Query }) => {
setDateRange(payload.dateRange);
if (payload.query) {
setQuery(payload.query);
}
setSearchSessionId(data.search.session.start());
},
[setDateRange, setQuery, data.search.session]
);
return (
<div>
{metricsDataView ? (
<>
<SearchBar
showQueryBar={true}
showFilterBar={false}
showDatePicker={true}
showAutoRefreshOnly={false}
showSaveQuery={true}
showQueryInput={true}
query={query}
dateRangeFrom={dateRange.from}
dateRangeTo={dateRange.to}
indexPatterns={[metricsDataView]}
onQuerySubmit={onQuerySubmit}
/>
<HostsTable
dataView={metricsDataView}
timeRange={dateRange}
query={query}
searchSessionId={searchSessionId}
/>
</>
) : hasFailedCreatingDataView || hasFailedFetchingDataView ? (
<div>
<div>There was an error trying to load or create the Data View:</div>
{source?.configuration.metricAlias}
</div>
) : (
<InfraLoadingPanel
height="100vh"
width="auto"
text={i18n.translate('xpack.infra.waffle.loadingDataText', {
defaultMessage: 'Loading data',
})}
/>
)}
</div>
);
};

View file

@ -0,0 +1,95 @@
/*
* 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 { EuiErrorBoundary } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import React from 'react';
import { useTrackPageview } from '@kbn/observability-plugin/public';
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
import { DocumentTitle } from '../../../components/document_title';
import { SourceErrorPage } from '../../../components/source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { useSourceContext } from '../../../containers/metrics_source';
import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs';
import { MetricsPageTemplate } from '../page_template';
import { hostsTitle } from '../../../translations';
import { HostsContent } from './hosts_content';
import { MetricsDataViewProvider } from './hooks/use_data_view';
export const HostsPage = () => {
const {
hasFailedLoadingSource,
isLoading,
loadSourceFailureMessage,
loadSource,
source,
metricIndicesExist,
} = useSourceContext();
useTrackPageview({ app: 'infra_metrics', path: 'hosts' });
useTrackPageview({ app: 'infra_metrics', path: 'hosts', delay: 15000 });
useMetricsBreadcrumbs([
{
text: hostsTitle,
},
]);
return (
<EuiErrorBoundary>
<DocumentTitle
title={(previousTitle: string) =>
i18n.translate('xpack.infra.infrastructureHostsPage.documentTitle', {
defaultMessage: '{previousTitle} | Hosts',
values: {
previousTitle,
},
})
}
/>
{isLoading && !source ? (
<SourceLoadingPage />
) : metricIndicesExist && source ? (
<>
<HostsPageWrapper className={APP_WRAPPER_CLASS}>
<MetricsPageTemplate
hasData={metricIndicesExist}
pageHeader={{
pageTitle: hostsTitle,
}}
pageBodyProps={{
paddingSize: 'none',
}}
>
<MetricsDataViewProvider metricAlias={source.configuration.metricAlias}>
<HostsContent />
</MetricsDataViewProvider>
</MetricsPageTemplate>
</HostsPageWrapper>
</>
) : hasFailedLoadingSource ? (
<SourceErrorPage errorMessage={loadSourceFailureMessage || ''} retry={loadSource} />
) : (
<MetricsPageTemplate hasData={metricIndicesExist} data-test-subj="noMetricsIndicesPrompt" />
)}
</EuiErrorBoundary>
);
};
// This is added to facilitate a full height layout whereby the
// inner container will set it's own height and be scrollable.
// The "fullHeight" prop won't help us as it only applies to certain breakpoints.
const HostsPageWrapper = euiStyled.div`
.euiPage .euiPageContentBody {
display: flex;
flex-direction: column;
flex: 1 0 auto;
width: 100%;
height: 100%;
}
`;

View file

@ -30,6 +30,7 @@ import { MetricsExplorerPage } from './metrics_explorer';
import { SnapshotPage } from './inventory_view';
import { MetricDetail } from './metric_detail';
import { MetricsSettingsPage } from './settings';
import { HostsPage } from './hosts';
import { SourceLoadingPage } from '../../components/source_loading_page';
import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options';
import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time';
@ -126,6 +127,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
)}
/>
<Route path="/detail/:type/:node" component={MetricDetail} />
<Route path={'/hosts'} component={HostsPage} />
<Route path={'/settings'} component={MetricsSettingsPage} />
</Switch>
</InfraMLCapabilitiesProvider>

View file

@ -101,6 +101,7 @@ export class Plugin implements InfraClientPluginClass {
label: 'Infrastructure',
sortKey: 300,
entries: [
{ label: 'Hosts', app: 'metrics', path: '/hosts' },
{ label: 'Inventory', app: 'metrics', path: '/inventory' },
{ label: 'Metrics Explorer', app: 'metrics', path: '/explorer' },
],
@ -191,6 +192,13 @@ export class Plugin implements InfraClientPluginClass {
}),
path: '/explorer',
},
{
id: 'metrics-hosts',
title: i18n.translate('xpack.infra.homePage.metricsHostsTabTitle', {
defaultMessage: 'Hosts',
}),
path: '/hosts',
},
{
id: 'settings',
title: i18n.translate('xpack.infra.homePage.settingsTabTitle', {

View file

@ -45,3 +45,7 @@ export const inventoryTitle = i18n.translate('xpack.infra.metrics.inventoryPageT
export const metricsExplorerTitle = i18n.translate('xpack.infra.metrics.metricsExplorerTitle', {
defaultMessage: 'Metrics Explorer',
});
export const hostsTitle = i18n.translate('xpack.infra.metrics.hostsTitle', {
defaultMessage: 'Hosts',
});

View file

@ -28,6 +28,7 @@ import type {
} from '@kbn/observability-plugin/public';
// import type { OsqueryPluginStart } from '../../osquery/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
import { UnwrapPromise } from '../common/utility_types';
import type {
SourceProviderProps,
@ -73,6 +74,7 @@ export interface InfraClientStartDeps {
embeddable?: EmbeddableStart;
osquery?: unknown; // OsqueryPluginStart;
share: SharePluginStart;
lens: LensPublicStart;
}
export type InfraClientCoreSetup = CoreSetup<InfraClientStartDeps, InfraClientStartExports>;