[InfraUI] Infrastructure navigation changes (#32892)

* Reorganise primary and sub navigation of Infrastructure

Add routed_tabs component

Amend routing (use an index and nested routes) and reorganise directory structure

Shift hosts / kubernetes / docker selection to subnavigation

Change path

Amend existing URLs

Use proper types

Amend document_title component

Reorganise title / source config button / info text

Maintain compatibility with old URLs

* Remove dead code

* Bump z-index on suggestions panel

* Ensure documentTitle responds to updates

* Use the same noop function and remove disabled option

* Use buttonSize="m" and add "Hosts" translation back

* Remove all "Home" wording

* Remove unused translations with script

* Don't nest <Link /> within a tab

* Comment out metrics-explorer tab until needed

* Don't create duplicate History entries
This commit is contained in:
Kerry Gallagher 2019-03-18 14:11:16 +00:00 committed by GitHub
parent f64fde7353
commit 4b2c2bd63f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 287 additions and 187 deletions

View file

@ -30,7 +30,7 @@ export function infra(kibana: any) {
defaultMessage: 'Infrastructure',
}),
listed: false,
url: `/app/${APP_ID}#/home`,
url: `/app/${APP_ID}#/infrastructure`,
},
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
home: ['plugins/infra/register_feature'],
@ -46,7 +46,7 @@ export function infra(kibana: any) {
title: i18n.translate('xpack.infra.linkInfrastructureTitle', {
defaultMessage: 'Infrastructure',
}),
url: `/app/${APP_ID}#/home`,
url: `/app/${APP_ID}#/infrastructure`,
},
{
description: i18n.translate('xpack.infra.linkLogsDescription', {

View file

@ -308,4 +308,5 @@ const SuggestionsPanel = styled(EuiPanel).attrs({
width: 100%;
margin-top: 2px;
overflow: hidden;
z-index: ${props => props.theme.eui.euiZLevel1};
`;

View file

@ -21,13 +21,16 @@ const wrapWithSharedState = () => {
const TITLE_SUFFIX = ' - Kibana';
return class extends React.Component<DocumentTitleProps, DocumentTitleState> {
public readonly state = {
index: titles.push('') - 1,
};
public componentDidMount() {
this.pushTitle(this.getTitle(this.props.title));
this.updateDocumentTitle();
this.setState(
() => {
return { index: titles.push('') - 1 };
},
() => {
this.pushTitle(this.getTitle(this.props.title));
this.updateDocumentTitle();
}
);
}
public componentDidUpdate() {
@ -53,7 +56,7 @@ const wrapWithSharedState = () => {
}
private removeTitle() {
titles.splice(this.state.index, 1);
titles.pop();
}
private updateDocumentTitle() {

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiTab, EuiTabs } from '@elastic/eui';
import React from 'react';
import { Route } from 'react-router-dom';
interface TabConfiguration {
title: string;
path: string;
}
interface RoutedTabsProps {
tabs: TabConfiguration[];
}
export class RoutedTabs extends React.Component<RoutedTabsProps> {
public render() {
return <EuiTabs>{this.renderTabs()}</EuiTabs>;
}
private renderTabs() {
return this.props.tabs.map(tab => {
return (
<Route
key={`${tab.path}${tab.title}`}
path={tab.path}
children={({ match, history }) => (
<EuiTab
onClick={() => (match ? undefined : history.push(tab.path))}
isSelected={match !== null}
>
{tab.title}
</EuiTab>
)}
/>
);
});
}
}

View file

@ -3,7 +3,9 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { get, max, min } from 'lodash';
import React from 'react';
import styled from 'styled-components';
@ -132,7 +134,21 @@ export const NodesOverview = injectI18n(
return (
<MainContainer>
<ViewSwitcherContainer>
<ViewSwitcher view={view} onChange={this.handleViewChange} />
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<ViewSwitcher view={view} onChange={this.handleViewChange} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued">
<p>
<FormattedMessage
id="xpack.infra.homePage.toolbar.showingLastOneMinuteDataText"
defaultMessage="Showing the last 1 minute of data from the time period"
/>
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</ViewSwitcherContainer>
{view === 'table' ? (
<TableContainer>

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiKeyPadMenu, EuiKeyPadMenuItemButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButtonGroup } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import {
InfraMetricInput,
@ -15,55 +15,52 @@ import {
} from '../../graphql/types';
interface Props {
intl: InjectedIntl;
nodeType: InfraNodeType;
changeNodeType: (nodeType: InfraNodeType) => void;
changeGroupBy: (groupBy: InfraPathInput[]) => void;
changeMetric: (metric: InfraMetricInput) => void;
}
export class WaffleNodeTypeSwitcher extends React.PureComponent<Props> {
export class WaffleNodeTypeSwitcherClass extends React.PureComponent<Props> {
public render() {
const { intl } = this.props;
const nodeOptions = [
{
id: InfraNodeType.host,
label: intl.formatMessage({
id: 'xpack.infra.waffle.nodeTypeSwitcher.hostsLabel',
defaultMessage: 'Hosts',
}),
},
{
id: InfraNodeType.pod,
label: 'Kubernetes',
},
{
id: InfraNodeType.container,
label: 'Docker',
},
];
return (
<EuiKeyPadMenu>
<EuiKeyPadMenuItemButton
label={
<FormattedMessage
id="xpack.infra.waffle.nodeTypeSwitcher.hostsLabel"
defaultMessage="Hosts"
/>
}
onClick={this.handleClick(InfraNodeType.host)}
>
<img
src="../plugins/infra/images/hosts.svg"
role="presentation"
alt=""
className="euiIcon euiIcon--large"
/>
</EuiKeyPadMenuItemButton>
<EuiKeyPadMenuItemButton label="Kubernetes" onClick={this.handleClick(InfraNodeType.pod)}>
<img
src="../plugins/infra/images/k8.svg"
role="presentation"
alt=""
className="euiIcon euiIcon--large"
/>
</EuiKeyPadMenuItemButton>
<EuiKeyPadMenuItemButton label="Docker" onClick={this.handleClick(InfraNodeType.container)}>
<img
src="../plugins/infra/images/docker.svg"
role="presentation"
alt=""
className="euiIcon euiIcon--large"
/>
</EuiKeyPadMenuItemButton>
</EuiKeyPadMenu>
<EuiButtonGroup
legend="Node type selection"
color="primary"
options={nodeOptions}
idSelected={this.props.nodeType}
onChange={this.handleClick}
buttonSize="m"
/>
);
}
private handleClick = (nodeType: InfraNodeType) => () => {
this.props.changeNodeType(nodeType);
private handleClick = (nodeType: string) => {
this.props.changeNodeType(nodeType as InfraNodeType);
this.props.changeGroupBy([]);
this.props.changeMetric({ type: InfraMetricType.cpu });
};
}
export const WaffleNodeTypeSwitcher = injectI18n(WaffleNodeTypeSwitcherClass);

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import { DocumentTitle } from '../../components/document_title';
import { HelpCenterContent } from '../../components/help_center_content';
import { RoutedTabs } from '../../components/navigation/routed_tabs';
import { ColumnarPage } from '../../components/page';
import { MetricsExplorerPage } from './metrics_explorer';
import { SnapshotPage } from './snapshot';
interface InfrastructurePageProps extends RouteComponentProps {
intl: InjectedIntl;
}
export const InfrastructurePage = injectI18n(({ match, intl }: InfrastructurePageProps) => (
<ColumnarPage>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.homePage.documentTitle',
defaultMessage: 'Infrastructure',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/infrastructure"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.infrastructure.infrastructureHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Infrastructure',
})}
/>
<RoutedTabs
tabs={[
{
title: 'Snapshot',
path: `${match.path}/snapshot`,
},
// {
// title: 'Metrics explorer',
// path: `${match.path}/metrics-explorer`,
// },
]}
/>
<Switch>
<Route path={`${match.path}/snapshot`} component={SnapshotPage} />
<Route path={`${match.path}/metrics-explorer`} component={MetricsExplorerPage} />
</Switch>
</ColumnarPage>
));

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { DocumentTitle } from '../../../components/document_title';
interface MetricsExplorerPageProps {
intl: InjectedIntl;
}
export const MetricsExplorerPage = injectI18n(({ intl }: MetricsExplorerPageProps) => (
<div>
<DocumentTitle
title={(previousTitle: string) =>
intl.formatMessage(
{
id: 'xpack.infra.infrastructureMetricsExplorerPage.documentTitle',
defaultMessage: '{previousTitle} | Metrics explorer',
},
{
previousTitle,
}
)
}
/>
Metrics Explorer
</div>
));

View file

@ -7,31 +7,31 @@
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { HomePageContent } from './page_content';
import { HomeToolbar } from './toolbar';
import { SnapshotPageContent } from './page_content';
import { SnapshotToolbar } from './toolbar';
import { DocumentTitle } from '../../components/document_title';
import { NoIndices } from '../../components/empty_states/no_indices';
import { Header } from '../../components/header';
import { HelpCenterContent } from '../../components/help_center_content';
import { ColumnarPage } from '../../components/page';
import { DocumentTitle } from '../../../components/document_title';
import { NoIndices } from '../../../components/empty_states/no_indices';
import { Header } from '../../../components/header';
import { ColumnarPage } from '../../../components/page';
import { SourceConfigurationFlyout } from '../../components/source_configuration';
import { WithSourceConfigurationFlyoutState } from '../../components/source_configuration/source_configuration_flyout_state';
import { WithWaffleFilterUrlState } from '../../containers/waffle/with_waffle_filters';
import { WithWaffleOptionsUrlState } from '../../containers/waffle/with_waffle_options';
import { WithWaffleTimeUrlState } from '../../containers/waffle/with_waffle_time';
import { WithKibanaChrome } from '../../containers/with_kibana_chrome';
import { SourceErrorPage, SourceLoadingPage, WithSource } from '../../containers/with_source';
import { SourceConfigurationFlyout } from '../../../components/source_configuration';
import { WithSourceConfigurationFlyoutState } from '../../../components/source_configuration/source_configuration_flyout_state';
import { WithWaffleFilterUrlState } from '../../../containers/waffle/with_waffle_filters';
import { WithWaffleOptionsUrlState } from '../../../containers/waffle/with_waffle_options';
import { WithWaffleTimeUrlState } from '../../../containers/waffle/with_waffle_time';
import { WithKibanaChrome } from '../../../containers/with_kibana_chrome';
import { SourceErrorPage, SourceLoadingPage, WithSource } from '../../../containers/with_source';
interface HomePageProps {
interface SnapshotPageProps extends RouteComponentProps {
intl: InjectedIntl;
}
export const HomePage = injectI18n(
class extends React.Component<HomePageProps, {}> {
public static displayName = 'HomePage';
export const SnapshotPage = injectI18n(
class extends React.Component<SnapshotPageProps, {}> {
public static displayName = 'SnapshotPage';
public render() {
const { intl } = this.props;
@ -39,17 +39,17 @@ export const HomePage = injectI18n(
return (
<ColumnarPage>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.homePage.documentTitle',
defaultMessage: 'Infrastructure',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/infrastructure"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.infrastructure.infrastructureHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Infrastructure',
})}
title={(previousTitle: string) =>
intl.formatMessage(
{
id: 'xpack.infra.infrastructureSnapshotPage.documentTitle',
defaultMessage: '{previousTitle} | Snapshot',
},
{
previousTitle,
}
)
}
/>
<Header
breadcrumbs={[
@ -79,8 +79,8 @@ export const HomePage = injectI18n(
<WithWaffleTimeUrlState />
<WithWaffleFilterUrlState indexPattern={derivedIndexPattern} />
<WithWaffleOptionsUrlState />
<HomeToolbar />
<HomePageContent />
<SnapshotToolbar />
<SnapshotPageContent />
</>
) : hasFailed ? (
<SourceErrorPage errorMessage={lastFailureMessage || ''} retry={load} />

View file

@ -6,17 +6,17 @@
import React from 'react';
import { NodesOverview } from '../../components/nodes_overview';
import { PageContent } from '../../components/page';
import { NodesOverview } from '../../../components/nodes_overview';
import { PageContent } from '../../../components/page';
import { WithWaffleFilter } from '../../containers/waffle/with_waffle_filters';
import { WithWaffleNodes } from '../../containers/waffle/with_waffle_nodes';
import { WithWaffleOptions } from '../../containers/waffle/with_waffle_options';
import { WithWaffleTime } from '../../containers/waffle/with_waffle_time';
import { WithOptions } from '../../containers/with_options';
import { WithSource } from '../../containers/with_source';
import { WithWaffleFilter } from '../../../containers/waffle/with_waffle_filters';
import { WithWaffleNodes } from '../../../containers/waffle/with_waffle_nodes';
import { WithWaffleOptions } from '../../../containers/waffle/with_waffle_options';
import { WithWaffleTime } from '../../../containers/waffle/with_waffle_time';
import { WithOptions } from '../../../containers/with_options';
import { WithSource } from '../../../containers/with_source';
export const HomePageContent: React.SFC = () => (
export const SnapshotPageContent: React.SFC = () => (
<PageContent>
<WithSource>
{({ configuration, derivedIndexPattern, sourceId }) => (

View file

@ -4,86 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
// import { i18n } from '@kbn/i18n';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { AutocompleteField } from '../../components/autocomplete_field';
import { Toolbar } from '../../components/eui/toolbar';
import { SourceConfigurationButton } from '../../components/source_configuration';
import { WaffleGroupByControls } from '../../components/waffle/waffle_group_by_controls';
import { WaffleMetricControls } from '../../components/waffle/waffle_metric_controls';
import { WaffleNodeTypeSwitcher } from '../../components/waffle/waffle_node_type_switcher';
import { WaffleTimeControls } from '../../components/waffle/waffle_time_controls';
import { WithWaffleFilter } from '../../containers/waffle/with_waffle_filters';
import { WithWaffleOptions } from '../../containers/waffle/with_waffle_options';
import { WithWaffleTime } from '../../containers/waffle/with_waffle_time';
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
import { WithSource } from '../../containers/with_source';
import { InfraNodeType } from '../../graphql/types';
import { AutocompleteField } from '../../../components/autocomplete_field';
import { Toolbar } from '../../../components/eui/toolbar';
import { SourceConfigurationButton } from '../../../components/source_configuration';
import { WaffleGroupByControls } from '../../../components/waffle/waffle_group_by_controls';
import { WaffleMetricControls } from '../../../components/waffle/waffle_metric_controls';
import { WaffleNodeTypeSwitcher } from '../../../components/waffle/waffle_node_type_switcher';
import { WaffleTimeControls } from '../../../components/waffle/waffle_time_controls';
import { WithWaffleFilter } from '../../../containers/waffle/with_waffle_filters';
import { WithWaffleOptions } from '../../../containers/waffle/with_waffle_options';
import { WithWaffleTime } from '../../../containers/waffle/with_waffle_time';
import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion';
import { WithSource } from '../../../containers/with_source';
const getTitle = (nodeType: string) => {
const TITLES = {
[InfraNodeType.host as string]: (
<FormattedMessage id="xpack.infra.homePage.toolbar.hostsTitle" defaultMessage="Hosts" />
),
[InfraNodeType.pod as string]: (
<FormattedMessage
id="xpack.infra.homePage.toolbar.kubernetesPodsTitle"
defaultMessage="Kubernetes Pods"
/>
),
[InfraNodeType.container as string]: (
<FormattedMessage
id="xpack.infra.homePage.toolbar.dockerContainersTitle"
defaultMessage="Docker Containers"
/>
),
};
return TITLES[nodeType];
};
export const HomeToolbar = injectI18n(({ intl }) => (
export const SnapshotToolbar = injectI18n(({ intl }) => (
<Toolbar>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiFlexGroup justifyContent="flexStart">
<EuiFlexItem grow={false}>
<WithWaffleOptions>
{({ nodeType }) => (
<EuiTitle size="m">
<h1>{getTitle(nodeType)}</h1>
</EuiTitle>
)}
</WithWaffleOptions>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SourceConfigurationButton />
</EuiFlexItem>
</EuiFlexGroup>
<EuiText color="subdued">
<p>
<FormattedMessage
id="xpack.infra.homePage.toolbar.showingLastOneMinuteDataText"
defaultMessage="Showing the last 1 minute of data from the time period"
/>
</p>
</EuiText>
</EuiFlexItem>
<WithWaffleOptions>
{({ nodeType, changeNodeType, changeGroupBy, changeMetric }) => (
<EuiFlexItem grow={false}>
<WaffleNodeTypeSwitcher
nodeType={nodeType}
changeNodeType={changeNodeType}
changeMetric={changeMetric}
changeGroupBy={changeGroupBy}
/>
</EuiFlexItem>
)}
</WithWaffleOptions>
</EuiFlexGroup>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="m">
<EuiFlexItem>
<WithSource>
@ -117,11 +56,27 @@ export const HomeToolbar = injectI18n(({ intl }) => (
)}
</WithSource>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<WithWaffleTime resetOnUnmount>
{({ currentTime, isAutoReloading, jumpToTime, startAutoReload, stopAutoReload }) => (
<WaffleTimeControls
currentTime={currentTime}
isLiveStreaming={isAutoReloading}
onChangeTime={jumpToTime}
startLiveStreaming={startAutoReload}
stopLiveStreaming={stopAutoReload}
/>
)}
</WithWaffleTime>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup alignItems="center" gutterSize="m">
<WithSource>
{({ derivedIndexPattern }) => (
<WithWaffleOptions>
{({
changeMetric,
changeNodeType,
changeGroupBy,
changeCustomOptions,
customOptions,
@ -130,6 +85,14 @@ export const HomeToolbar = injectI18n(({ intl }) => (
nodeType,
}) => (
<React.Fragment>
<EuiFlexItem grow={false}>
<WaffleNodeTypeSwitcher
nodeType={nodeType}
changeNodeType={changeNodeType}
changeMetric={changeMetric}
changeGroupBy={changeGroupBy}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<WaffleMetricControls
metric={metric}
@ -147,24 +110,14 @@ export const HomeToolbar = injectI18n(({ intl }) => (
customOptions={customOptions}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SourceConfigurationButton />
</EuiFlexItem>
</React.Fragment>
)}
</WithWaffleOptions>
)}
</WithSource>
<EuiFlexItem grow={false}>
<WithWaffleTime resetOnUnmount>
{({ currentTime, isAutoReloading, jumpToTime, startAutoReload, stopAutoReload }) => (
<WaffleTimeControls
currentTime={currentTime}
isLiveStreaming={isAutoReloading}
onChangeTime={jumpToTime}
startLiveStreaming={startAutoReload}
stopLiveStreaming={stopAutoReload}
/>
)}
</WithWaffleTime>
</EuiFlexItem>
</EuiFlexGroup>
</Toolbar>
));

View file

@ -30,7 +30,7 @@ export class LinkToPage extends React.Component<LinkToPageProps> {
component={RedirectToNodeDetail}
/>
<Route path={`${match.url}/logs`} component={RedirectToLogs} />
<Redirect to="/home" />
<Redirect to="/infrastructure" />
</Switch>
);
}

View file

@ -22,7 +22,7 @@ FeatureCatalogueRegistryProvider.register((i18n: I18nServiceType) => ({
'Explore infrastructure metrics and logs for common servers, containers, and services.',
}),
icon: 'infraApp',
path: `/app/${APP_ID}#home`,
path: `/app/${APP_ID}#infrastructure`,
showOnHomePage: true,
category: FeatureCatalogueCategory.DATA,
}));

View file

@ -9,7 +9,7 @@ import React from 'react';
import { Redirect, Route, Router, Switch } from 'react-router-dom';
import { NotFoundPage } from './pages/404';
import { HomePage } from './pages/home';
import { InfrastructurePage } from './pages/infrastructure';
import { LinkToPage } from './pages/link_to';
import { LogsPage } from './pages/logs';
import { MetricDetail } from './pages/metrics';
@ -22,9 +22,11 @@ export const PageRouter: React.SFC<RouterProps> = ({ history }) => {
return (
<Router history={history}>
<Switch>
<Redirect from="/" exact={true} to="/home" />
<Redirect from="/" exact={true} to="/infrastructure/snapshot" />
<Redirect from="/infrastructure" exact={true} to="/infrastructure/snapshot" />
<Redirect from="/home" exact={true} to="/infrastructure/snapshot" />
<Route path="/logs" component={LogsPage} />
<Route path="/home" component={HomePage} />
<Route path="/infrastructure" component={InfrastructurePage} />
<Route path="/link-to" component={LinkToPage} />
<Route path="/metrics/:type/:node" component={MetricDetail} />
<Route component={NotFoundPage} />

View file

@ -4190,10 +4190,7 @@
"xpack.infra.header.infrastructureTitle": "基础设施",
"xpack.infra.homePage.noMetricsIndicesDescription": "让我们添加一些!",
"xpack.infra.homePage.noMetricsIndicesTitle": "似乎您没有任何指标索引。",
"xpack.infra.homePage.toolbar.dockerContainersTitle": "Docker 容器",
"xpack.infra.homePage.toolbar.hostsTitle": "主机",
"xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder": "搜索基础设施数据……(例如 host.name:host-1",
"xpack.infra.homePage.toolbar.kubernetesPodsTitle": "Kubernetes Pod",
"xpack.infra.homePage.toolbar.showingLastOneMinuteDataText": "显示该时段过去 1 分钟的数据",
"xpack.infra.infrastructureDescription": "浏览您的基础设施",
"xpack.infra.infrastructureTitle": "基础设施",