[Infrastructure UI] Add unified search to hosts table (#143850)

* Add unified search to hosts table

* Add saved query support

* Adjust error handling

* Minor refactoring and unit tests

* Revert changes to translations

* CR fixes
This commit is contained in:
Carlos Crespo 2022-10-27 15:56:54 +02:00 committed by GitHub
parent 2efad9d15a
commit eff4ce0cd5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 437 additions and 159 deletions

View file

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

View file

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

View file

@ -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
onFiltersUpdated={onFilterChange}
/>
);
};

View file

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

View file

@ -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';
@ -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(
@ -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[]) => {
@ -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,
};
};

View file

@ -0,0 +1,105 @@
/*
* 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 createContainer from 'constate';
import { useCallback, useReducer } from 'react';
import { buildEsQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import DateMath from '@kbn/datemath';
import type { SavedQuery } from '@kbn/data-plugin/public';
import type { InfraClientStartDeps } from '../../../../types';
import { useMetricsDataViewContext } from './use_data_view';
import { useKibanaTimefilterTime } from '../../../../hooks/use_kibana_timefilter_time';
const DEFAULT_FROM_MINUTES_VALUE = 15;
export const useUnifiedSearch = () => {
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
const { metricsDataView } = useMetricsDataViewContext();
const { services } = useKibana<InfraClientStartDeps>();
const {
data: { query: queryManager },
} = services;
const [getTime, setTime] = useKibanaTimefilterTime({
from: `now-${DEFAULT_FROM_MINUTES_VALUE}m`,
to: 'now',
});
const { queryString, filterManager } = queryManager;
const currentDate = new Date();
const fromTS =
DateMath.parse(getTime().from)?.valueOf() ??
new Date(currentDate.getMinutes() - DEFAULT_FROM_MINUTES_VALUE).getTime();
const toTS = DateMath.parse(getTime().to)?.valueOf() ?? currentDate.getTime();
const currentTimeRange = {
from: fromTS,
to: toTS,
};
const submitFilterChange = useCallback(
(query?: Query, dateRange?: TimeRange, filters?: Filter[]) => {
if (filters) {
filterManager.setFilters(filters);
}
setTime({
...getTime(),
...dateRange,
});
queryString.setQuery({ ...queryString.getQuery(), ...query });
// Unified search holds the all state, we need to force the hook to rerender so that it can return the most recent values
// This can be removed once we get the state from the URL
forceUpdate();
},
[filterManager, queryString, getTime, setTime]
);
const saveQuery = useCallback(
(newSavedQuery: SavedQuery) => {
const savedQueryFilters = newSavedQuery.attributes.filters ?? [];
const globalFilters = filterManager.getGlobalFilters();
filterManager.setFilters([...savedQueryFilters, ...globalFilters]);
// Unified search holds the all state, we need to force the hook to rerender so that it can return the most recent values
// This can be removed once we get the state from the URL
forceUpdate();
},
[filterManager]
);
const clearSavedQUery = useCallback(() => {
filterManager.setFilters(filterManager.getGlobalFilters());
// Unified search holds the all state, we need to force the hook to rerender so that it can return the most recent values
// This can be removed once we get the state from the URL
forceUpdate();
}, [filterManager]);
const buildQuery = useCallback(() => {
if (!metricsDataView) {
return null;
}
return buildEsQuery(metricsDataView, queryString.getQuery(), filterManager.getFilters());
}, [filterManager, metricsDataView, queryString]);
return {
dateRangeTimestamp: currentTimeRange,
esQuery: buildQuery(),
submitFilterChange,
saveQuery,
clearSavedQUery,
unifiedSearchQuery: queryString.getQuery() as Query,
unifiedSearchDateRange: getTime(),
unifiedSearchFilters: filterManager.getFilters(),
};
};
export const UnifiedSearch = createContainer(useUnifiedSearch);
export const [UnifiedSearchProvider, useUnifiedSearchContext] = UnifiedSearch;

View file

@ -1,120 +0,0 @@
/*
* 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 { i18n } from '@kbn/i18n';
import React, { useState, useCallback } from 'react';
import { SearchBar } from '@kbn/unified-search-plugin/public';
import { EuiSpacer } from '@elastic/eui';
import { NoData } from '../../../components/empty_states';
import { InfraLoadingPanel } from '../../../components/loading';
import { useMetricsDataViewContext } from './hooks/use_data_view';
import { HostsTable } from './components/hosts_table';
import { useSourceContext } from '../../../containers/metrics_source';
import { useSnapshot } from '../inventory_view/hooks/use_snaphot';
import type { SnapshotMetricType } from '../../../../common/inventory_models/types';
export const HostsContent: React.FunctionComponent = () => {
const { source, sourceId } = 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 onQuerySubmit = useCallback(
(payload: { dateRange: TimeRange; query?: Query }) => {
setDateRange(payload.dateRange);
if (payload.query) {
setQuery(payload.query);
}
},
[setDateRange, setQuery]
);
const hostMetrics: Array<{ type: SnapshotMetricType }> = [
{ type: 'rx' },
{ type: 'tx' },
{ type: 'memory' },
{ type: 'cpuCores' },
{ type: 'memoryTotal' },
];
const { loading, nodes, reload } = useSnapshot(
'', // use the unified search query, supported type?
hostMetrics,
[],
'host',
sourceId,
1666710279338, // currentTime. need to add support for TimeRange?
'',
'',
true,
{
from: 1666710279338, // dynamic time range needs to be supported
interval: '1m',
lookbackSize: 5,
to: 1666711479338,
}
);
const noData = !loading && nodes && nodes.length === 0;
return (
<div>
{metricsDataView && !loading ? (
noData ? (
<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"
/>
) : (
<>
<SearchBar
showFilterBar={false}
showDatePicker={true}
showAutoRefreshOnly={false}
showSaveQuery={true}
showQueryInput={true}
query={query}
dateRangeFrom={dateRange.from}
dateRangeTo={dateRange.to}
indexPatterns={[metricsDataView]}
onQuerySubmit={onQuerySubmit}
/>
<EuiSpacer />
<HostsTable nodes={nodes} />
</>
)
) : 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

@ -9,16 +9,16 @@ import { EuiErrorBoundary } from '@elastic/eui';
import React from 'react';
import { useTrackPageview } from '@kbn/observability-plugin/public';
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
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';
import { fullHeightContentStyles } from '../../../page_template.styles';
import { UnifiedSearchProvider } from './hooks/use_unified_search';
import { HostContainer } from './components/hosts_container';
export const HostsPage = () => {
const {
@ -56,7 +56,9 @@ export const HostsPage = () => {
}}
>
<MetricsDataViewProvider metricAlias={source.configuration.metricAlias}>
<HostsContent />
<UnifiedSearchProvider>
<HostContainer />
</UnifiedSearchProvider>
</MetricsDataViewProvider>
</MetricsPageTemplate>
</div>

View file

@ -15595,15 +15595,15 @@
"xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel": "Voir les instructions de configuration",
"xpack.infra.homePage.settingsTabTitle": "Paramètres",
"xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder": "Rechercher des données d'infrastructure… (par exemple host.name:host-1)",
"xpack.infra.hostsTable.nameColumnHeader": "Nom",
"xpack.infra.hostsTable.operatingSystemColumnHeader": "Système d'exploitation",
"xpack.infra.hostsTable.numberOfCpusColumnHeader": "Nombre de processeurs",
"xpack.infra.hostsTable.diskLatencyColumnHeader": "",
"xpack.infra.hostsTable.averageTxColumnHeader": "",
"xpack.infra.hostsTable.averageRxColumnHeader": "",
"xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "Total de la mémoire (moy.)",
"xpack.infra.hostsTable.servicesOnHostColumnHeader": "",
"xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "Utilisation de la mémoire (moy.)",
"xpack.infra.hostsTable.averageRxColumnHeader": "",
"xpack.infra.hostsTable.averageTxColumnHeader": "",
"xpack.infra.hostsTable.diskLatencyColumnHeader": "",
"xpack.infra.hostsTable.nameColumnHeader": "Nom",
"xpack.infra.hostsTable.numberOfCpusColumnHeader": "Nombre de processeurs",
"xpack.infra.hostsTable.operatingSystemColumnHeader": "Système d'exploitation",
"xpack.infra.hostsTable.servicesOnHostColumnHeader": "",
"xpack.infra.infra.nodeDetails.apmTabLabel": "APM",
"xpack.infra.infra.nodeDetails.createAlertLink": "Créer une règle d'inventaire",
"xpack.infra.infra.nodeDetails.openAsPage": "Ouvrir en tant que page",
@ -33705,4 +33705,4 @@
"xpack.painlessLab.title": "Painless Lab",
"xpack.painlessLab.walkthroughButtonLabel": "Présentation"
}
}
}

View file

@ -15580,15 +15580,15 @@
"xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel": "セットアップの手順を表示",
"xpack.infra.homePage.settingsTabTitle": "設定",
"xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder": "インフラストラクチャデータを検索…host.name:host-1",
"xpack.infra.hostsTable.nameColumnHeader": "名前",
"xpack.infra.hostsTable.operatingSystemColumnHeader": "オペレーティングシステム",
"xpack.infra.hostsTable.numberOfCpusColumnHeader": "CPU数",
"xpack.infra.hostsTable.diskLatencyColumnHeader": "",
"xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "メモリ合計 (平均) ",
"xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "メモリー使用状況(平均)",
"xpack.infra.hostsTable.averageTxColumnHeader": "",
"xpack.infra.hostsTable.averageRxColumnHeader": "",
"xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "メモリ合計 (平均) ",
"xpack.infra.hostsTable.diskLatencyColumnHeader": "",
"xpack.infra.hostsTable.nameColumnHeader": "名前",
"xpack.infra.hostsTable.numberOfCpusColumnHeader": "CPU数",
"xpack.infra.hostsTable.operatingSystemColumnHeader": "オペレーティングシステム",
"xpack.infra.hostsTable.servicesOnHostColumnHeader": "",
"xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "メモリー使用状況(平均)",
"xpack.infra.infra.nodeDetails.apmTabLabel": "APM",
"xpack.infra.infra.nodeDetails.createAlertLink": "インベントリルールの作成",
"xpack.infra.infra.nodeDetails.openAsPage": "ページとして開く",
@ -33679,4 +33679,4 @@
"xpack.painlessLab.title": "Painless Lab",
"xpack.painlessLab.walkthroughButtonLabel": "実地検証"
}
}
}

View file

@ -15601,15 +15601,15 @@
"xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel": "查看设置说明",
"xpack.infra.homePage.settingsTabTitle": "设置",
"xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder": "搜索基础设施数据……(例如 host.name:host-1",
"xpack.infra.hostsTable.nameColumnHeader": "名称",
"xpack.infra.hostsTable.operatingSystemColumnHeader": "操作系统",
"xpack.infra.hostsTable.numberOfCpusColumnHeader": "# 个 CPU",
"xpack.infra.hostsTable.diskLatencyColumnHeader": "",
"xpack.infra.hostsTable.averageTxColumnHeader": "",
"xpack.infra.hostsTable.averageRxColumnHeader": "",
"xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "内存合计 (平均值)",
"xpack.infra.hostsTable.servicesOnHostColumnHeader": "",
"xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "内存使用率(平均值)",
"xpack.infra.hostsTable.averageRxColumnHeader": "",
"xpack.infra.hostsTable.averageTxColumnHeader": "",
"xpack.infra.hostsTable.diskLatencyColumnHeader": "",
"xpack.infra.hostsTable.nameColumnHeader": "名称",
"xpack.infra.hostsTable.numberOfCpusColumnHeader": "# 个 CPU",
"xpack.infra.hostsTable.operatingSystemColumnHeader": "操作系统",
"xpack.infra.hostsTable.servicesOnHostColumnHeader": "",
"xpack.infra.infra.nodeDetails.apmTabLabel": "APM",
"xpack.infra.infra.nodeDetails.createAlertLink": "创建库存规则",
"xpack.infra.infra.nodeDetails.openAsPage": "以页面形式打开",
@ -33716,4 +33716,4 @@
"xpack.painlessLab.title": "Painless 实验室",
"xpack.painlessLab.walkthroughButtonLabel": "指导"
}
}
}