translate InfraOps visualization component (Part 2) (#25212)

* translate InfraOps visualization component (Part 2 - part of folder components and root files)

* update translation of Beats Management vizualization component (Part 2)

* update translation of Infra Ops vizualization component (Part 2)

* update translation of Infra Ops vizualization component (Part 2)

* update translation of Infra Ops vizualization component (Part 2)

* change some ids and add some logic

* update Infra Ops Part 2 - directly wrap some classes by injectI18n()

* Update Infra Part-II change some code

* update Infra-II - add static to displayName, add needed translations

* update Infra-II - fix errors wich broke CI

* update Infra-II - fix errors wich broke CI

* update Infra-II - fix errors

* update Infra-II - fix errors in group_by_controls

* update Infra-II - update nodeType in files with errors

* update Infra-II

* update Infra-II

* update Infra-II - update one type

* add one empty line, use lodash get method
This commit is contained in:
tibmt 2018-11-29 15:08:55 +02:00 committed by GitHub
parent 9e94fef2ee
commit 584e68fb3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1405 additions and 855 deletions

View file

@ -17,6 +17,8 @@
* under the License.
*/
import { I18nServiceType } from '@kbn/i18n/angular';
export enum FeatureCatalogueCategory {
ADMIN = 'admin',
DATA = 'data',
@ -33,7 +35,7 @@ interface FeatureCatalogueObject {
category: FeatureCatalogueCategory;
}
type FeatureCatalogueRegistryFunction = () => FeatureCatalogueObject;
type FeatureCatalogueRegistryFunction = (i18n: I18nServiceType) => FeatureCatalogueObject;
export const FeatureCatalogueRegistryProvider: {
register: (fn: FeatureCatalogueRegistryFunction) => void;

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import JoiNamespace from 'joi';
import { resolve } from 'path';
@ -19,31 +20,43 @@ export function infra(kibana: any) {
require: ['kibana', 'elasticsearch'],
uiExports: {
app: {
description: 'Explore your infrastructure',
description: i18n.translate('xpack.infra.infrastructureDescription', {
defaultMessage: 'Explore your infrastructure',
}),
icon: 'plugins/infra/images/infra_mono_white.svg',
main: 'plugins/infra/app',
title: 'Infrastructure',
title: i18n.translate('xpack.infra.infrastructureTitle', {
defaultMessage: 'Infrastructure',
}),
listed: false,
url: `/app/${APP_ID}#/home`,
},
home: ['plugins/infra/register_feature'],
links: [
{
description: 'Explore your infrastructure',
description: i18n.translate('xpack.infra.linkInfrastructureDescription', {
defaultMessage: 'Explore your infrastructure',
}),
icon: 'plugins/infra/images/infra_mono_white.svg',
euiIconType: 'infraApp',
id: 'infra:home',
order: 8000,
title: 'Infrastructure',
title: i18n.translate('xpack.infra.linkInfrastructureTitle', {
defaultMessage: 'Infrastructure',
}),
url: `/app/${APP_ID}#/home`,
},
{
description: 'Explore your logs',
description: i18n.translate('xpack.infra.linkLogsDescription', {
defaultMessage: 'Explore your logs',
}),
icon: 'plugins/infra/images/logging_mono_white.svg',
euiIconType: 'loggingApp',
id: 'infra:logs',
order: 8001,
title: 'Logs',
title: i18n.translate('xpack.infra.linkLogsTitle', {
defaultMessage: 'Logs',
}),
url: `/app/${APP_ID}#/logs`,
},
],

View file

@ -5,6 +5,7 @@
*/
import { EuiPageContentBody, EuiTitle } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { InfraMetricData } from '../../../common/graphql/types';
@ -19,66 +20,87 @@ interface Props {
loading: boolean;
nodeName: string;
onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void;
intl: InjectedIntl;
}
interface State {
crosshairValue: number | null;
}
export class Metrics extends React.PureComponent<Props, State> {
public readonly state = {
crosshairValue: null,
};
export const Metrics = injectI18n(
class extends React.PureComponent<Props, State> {
public static displayName = 'Metrics';
public readonly state = {
crosshairValue: null,
};
public render() {
if (this.props.loading) {
public render() {
const { intl } = this.props;
if (this.props.loading) {
return (
<InfraLoadingPanel
height="100vh"
width="auto"
text={intl.formatMessage(
{
id: 'xpack.infra.metrics.loadingNodeDataText',
defaultMessage: 'Loading data for {nodeName}',
},
{
nodeName: this.props.nodeName,
}
)}
/>
);
}
return <React.Fragment>{this.props.layouts.map(this.renderLayout)}</React.Fragment>;
}
private renderLayout = (layout: InfraMetricLayout) => {
return (
<InfraLoadingPanel
height="100vh"
width="auto"
text={`Loading data for ${this.props.nodeName}`}
<React.Fragment key={layout.id}>
<EuiPageContentBody>
<EuiTitle size="m">
<h2 id={layout.id}>
<FormattedMessage
id="xpack.infra.metrics.layoutLabelOverviewTitle"
defaultMessage="{layoutLabel} Overview"
values={{
layoutLabel: layout.label,
}}
/>
</h2>
</EuiTitle>
</EuiPageContentBody>
{layout.sections.map(this.renderSection(layout))}
</React.Fragment>
);
};
private renderSection = (layout: InfraMetricLayout) => (section: InfraMetricLayoutSection) => {
let sectionProps = {};
if (section.type === 'chart') {
const { onChangeRangeTime } = this.props;
sectionProps = {
onChangeRangeTime,
crosshairValue: this.state.crosshairValue,
onCrosshairUpdate: this.onCrosshairUpdate,
};
}
return (
<Section
section={section}
metrics={this.props.metrics}
key={`${layout.id}-${section.id}`}
{...sectionProps}
/>
);
}
return <React.Fragment>{this.props.layouts.map(this.renderLayout)}</React.Fragment>;
};
private onCrosshairUpdate = (crosshairValue: number) => {
this.setState({
crosshairValue,
});
};
}
private renderLayout = (layout: InfraMetricLayout) => {
return (
<React.Fragment key={layout.id}>
<EuiPageContentBody>
<EuiTitle size="m">
<h2 id={layout.id}>{`${layout.label} Overview`}</h2>
</EuiTitle>
</EuiPageContentBody>
{layout.sections.map(this.renderSection(layout))}
</React.Fragment>
);
};
private renderSection = (layout: InfraMetricLayout) => (section: InfraMetricLayoutSection) => {
let sectionProps = {};
if (section.type === 'chart') {
const { onChangeRangeTime } = this.props;
sectionProps = {
onChangeRangeTime,
crosshairValue: this.state.crosshairValue,
onCrosshairUpdate: this.onCrosshairUpdate,
};
}
return (
<Section
section={section}
metrics={this.props.metrics}
key={`${layout.id}-${section.id}`}
{...sectionProps}
/>
);
};
private onCrosshairUpdate = (crosshairValue: number) => {
this.setState({
crosshairValue,
});
};
}
);

View file

@ -16,6 +16,7 @@ import {
EuiXAxis,
EuiYAxis,
} from '@elastic/eui/lib/experimental';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import Color from 'color';
import { get } from 'lodash';
import moment from 'moment';
@ -42,6 +43,7 @@ interface Props {
onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void;
crosshairValue?: number;
onCrosshairUpdate?: (crosshairValue: number) => void;
intl: InjectedIntl;
}
const isInfraMetricLayoutVisualizationType = (
@ -115,106 +117,114 @@ const seriesHasLessThen2DataPoints = (series: InfraDataSeries): boolean => {
return series.data.length < 2;
};
export class ChartSection extends React.PureComponent<Props> {
public render() {
const { crosshairValue, section, metric, onCrosshairUpdate } = this.props;
const { visConfig } = section;
const crossHairProps = {
crosshairValue,
onCrosshairUpdate,
};
const chartProps: EuiSeriesChartProps = {
xType: 'time',
showCrosshair: false,
showDefaultAxis: false,
enableSelectionBrush: true,
onSelectionBrushEnd: this.handleSelectionBrushEnd,
};
const stacked = visConfig && visConfig.stacked;
if (stacked) {
chartProps.stackBy = 'y';
export const ChartSection = injectI18n(
class extends React.PureComponent<Props> {
public static displayName = 'ChartSection';
public render() {
const { crosshairValue, section, metric, onCrosshairUpdate, intl } = this.props;
const { visConfig } = section;
const crossHairProps = {
crosshairValue,
onCrosshairUpdate,
};
const chartProps: EuiSeriesChartProps = {
xType: 'time',
showCrosshair: false,
showDefaultAxis: false,
enableSelectionBrush: true,
onSelectionBrushEnd: this.handleSelectionBrushEnd,
};
const stacked = visConfig && visConfig.stacked;
if (stacked) {
chartProps.stackBy = 'y';
}
const bounds = visConfig && visConfig.bounds;
if (bounds) {
chartProps.yDomain = [bounds.min, bounds.max];
}
if (!metric) {
chartProps.statusText = intl.formatMessage({
id: 'xpack.infra.chartSection.missingMetricDataText',
defaultMessage: 'Missing data',
});
}
if (metric.series.some(seriesHasLessThen2DataPoints)) {
chartProps.statusText = intl.formatMessage({
id: 'xpack.infra.chartSection.notEnoughDataPointsToRenderText',
defaultMessage: 'Not enough data points to render chart, try increasing the time range.',
});
}
const formatter = get(visConfig, 'formatter', InfraFormatterType.number);
const formatterTemplate = get(visConfig, 'formatterTemplate', '{{value}}');
const formatterFunction = getFormatter(formatter, formatterTemplate);
const seriesLabels = get(metric, 'series', [] as InfraDataSeries[]).map(s =>
getChartName(section, s.id)
);
const seriesColors = get(metric, 'series', [] as InfraDataSeries[]).map(
s => getChartColor(section, s.id) || ''
);
const itemsFormatter = createItemsFormatter(formatterFunction, seriesLabels, seriesColors);
return (
<EuiPageContentBody>
<EuiTitle size="s">
<h3 id={section.id}>{section.label}</h3>
</EuiTitle>
<div style={{ height: 200 }}>
<EuiSeriesChart {...chartProps}>
<EuiXAxis marginLeft={MARGIN_LEFT} />
<EuiYAxis tickFormat={formatterFunction} marginLeft={MARGIN_LEFT} />
<EuiCrosshairX
seriesNames={seriesLabels}
itemsFormat={itemsFormatter}
titleFormat={titleFormatter}
{...crossHairProps}
/>
{metric &&
metric.series.map(series => {
if (!series || series.data.length < 2) {
return null;
}
const data = series.data.map(d => {
return { x: d.timestamp, y: d.value || 0, y0: 0 };
});
const chartType = getChartType(section, series.id);
const name = getChartName(section, series.id);
const seriesProps: EuiSeriesProps = {
data,
name,
lineSize: 2,
};
const color = getChartColor(section, series.id);
if (color) {
seriesProps.color = color;
}
const EuiChartComponent = chartComponentsByType[chartType];
return (
<EuiChartComponent
key={`${section.id}-${series.id}`}
{...seriesProps}
marginLeft={MARGIN_LEFT}
/>
);
})}
</EuiSeriesChart>
</div>
</EuiPageContentBody>
);
}
const bounds = visConfig && visConfig.bounds;
if (bounds) {
chartProps.yDomain = [bounds.min, bounds.max];
}
if (!metric) {
chartProps.statusText = 'Missing data';
}
if (metric.series.some(seriesHasLessThen2DataPoints)) {
chartProps.statusText =
'Not enough data points to render chart, try increasing the time range.';
}
const formatter = get(visConfig, 'formatter', InfraFormatterType.number);
const formatterTemplate = get(visConfig, 'formatterTemplate', '{{value}}');
const formatterFunction = getFormatter(formatter, formatterTemplate);
const seriesLabels = get(metric, 'series', [] as InfraDataSeries[]).map(s =>
getChartName(section, s.id)
);
const seriesColors = get(metric, 'series', [] as InfraDataSeries[]).map(
s => getChartColor(section, s.id) || ''
);
const itemsFormatter = createItemsFormatter(formatterFunction, seriesLabels, seriesColors);
return (
<EuiPageContentBody>
<EuiTitle size="s">
<h3 id={section.id}>{section.label}</h3>
</EuiTitle>
<div style={{ height: 200 }}>
<EuiSeriesChart {...chartProps}>
<EuiXAxis marginLeft={MARGIN_LEFT} />
<EuiYAxis tickFormat={formatterFunction} marginLeft={MARGIN_LEFT} />
<EuiCrosshairX
seriesNames={seriesLabels}
itemsFormat={itemsFormatter}
titleFormat={titleFormatter}
{...crossHairProps}
/>
{metric &&
metric.series.map(series => {
if (!series || series.data.length < 2) {
return null;
}
const data = series.data.map(d => {
return { x: d.timestamp, y: d.value || 0, y0: 0 };
});
const chartType = getChartType(section, series.id);
const name = getChartName(section, series.id);
const seriesProps: EuiSeriesProps = {
data,
name,
lineSize: 2,
};
const color = getChartColor(section, series.id);
if (color) {
seriesProps.color = color;
}
const EuiChartComponent = chartComponentsByType[chartType];
return (
<EuiChartComponent
key={`${section.id}-${series.id}`}
{...seriesProps}
marginLeft={MARGIN_LEFT}
/>
);
})}
</EuiSeriesChart>
</div>
</EuiPageContentBody>
);
}
private handleSelectionBrushEnd = (area: Area) => {
const { onChangeRangeTime } = this.props;
const { startX, endX } = area.domainArea;
if (onChangeRangeTime) {
onChangeRangeTime({
to: endX.valueOf(),
from: startX.valueOf(),
} as metricTimeActions.MetricRangeTimeState);
}
};
}
private handleSelectionBrushEnd = (area: Area) => {
const { onChangeRangeTime } = this.props;
const { startX, endX } = area.domainArea;
if (onChangeRangeTime) {
onChangeRangeTime({
to: endX.valueOf(),
from: startX.valueOf(),
} as metricTimeActions.MetricRangeTimeState);
}
};
}
);
interface DomainArea {
startX: moment.Moment;

View file

@ -5,6 +5,7 @@
*/
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import moment, { Moment } from 'moment';
import React from 'react';
import styled from 'styled-components';
@ -53,16 +54,27 @@ export class MetricsTimeControls extends React.Component<
iconType="pause"
onClick={this.stopLiveStreaming}
>
Stop refreshing
<FormattedMessage
id="xpack.infra.metricsTimeControls.stopRefreshingButtonLabel"
defaultMessage="Stop refreshing"
/>
</EuiButton>
) : (
<EuiButton iconSide="left" iconType="play" onClick={this.startLiveStreaming}>
Auto-refresh
<FormattedMessage
id="xpack.infra.metricsTimeControls.autoRefreshButtonLabel"
defaultMessage="Auto-refresh"
/>
</EuiButton>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={this.resetSearch}>Reset</EuiButtonEmpty>
<EuiButtonEmpty onClick={this.resetSearch}>
<FormattedMessage
id="xpack.infra.metricsTimeControls.resetButtonLabel"
defaultMessage="Reset"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
);
@ -72,11 +84,19 @@ export class MetricsTimeControls extends React.Component<
<EuiFlexGroup gutterSize="s" justifyContent="flexStart">
<EuiFlexItem grow={false}>
<EuiButton color={goColor} fill onClick={this.searchRangeTime}>
Go
<FormattedMessage
id="xpack.infra.metricsTimeControls.goButtonLabel"
defaultMessage="Go"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={this.cancelSearch}>Cancel</EuiButtonEmpty>
<EuiButtonEmpty onClick={this.cancelSearch}>
<FormattedMessage
id="xpack.infra.metricsTimeControls.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
) : (

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 { find } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { find, get } from 'lodash';
import moment from 'moment';
import React, { Fragment } from 'react';
@ -28,15 +30,156 @@ import {
EuiTitle,
} from '@elastic/eui';
const commonDates = [
'Today',
'Yesterday',
'This week',
'Week to date',
'This month',
'Month to date',
'This year',
'Year to date',
enum DatePickerDateOptions {
today = 'today',
yesterday = 'yesterday',
thisWeek = 'this_week',
weekToDate = 'week_to_date',
thisMonth = 'this_month',
monthToDate = 'month_to_date',
thisYear = 'this_year',
yearToDate = 'year_to_date',
}
const commonDates: Array<{ id: string; label: any }> = [
{
id: DatePickerDateOptions.today,
label: i18n.translate('xpack.infra.rangeDatePicker.todayText', {
defaultMessage: 'Today',
}),
},
{
id: DatePickerDateOptions.yesterday,
label: i18n.translate('xpack.infra.rangeDatePicker.yesterdayText', {
defaultMessage: 'Yesterday',
}),
},
{
id: DatePickerDateOptions.thisWeek,
label: i18n.translate('xpack.infra.rangeDatePicker.thisWeekText', {
defaultMessage: 'This week',
}),
},
{
id: DatePickerDateOptions.weekToDate,
label: i18n.translate('xpack.infra.rangeDatePicker.weekToDateText', {
defaultMessage: 'Week to date',
}),
},
{
id: DatePickerDateOptions.thisMonth,
label: i18n.translate('xpack.infra.rangeDatePicker.thisMonthText', {
defaultMessage: 'This month',
}),
},
{
id: DatePickerDateOptions.monthToDate,
label: i18n.translate('xpack.infra.rangeDatePicker.monthToDateText', {
defaultMessage: 'Month to date',
}),
},
{
id: DatePickerDateOptions.thisYear,
label: i18n.translate('xpack.infra.rangeDatePicker.thisYearText', {
defaultMessage: 'This year',
}),
},
{
id: DatePickerDateOptions.yearToDate,
label: i18n.translate('xpack.infra.rangeDatePicker.yearToDateText', {
defaultMessage: 'Year to date',
}),
},
];
const singleLastOptions: Array<{ value: string; text: any }> = [
{
value: 'seconds',
text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.secondLabel', {
defaultMessage: 'second',
}),
},
{
value: 'minutes',
text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.minuteLabel', {
defaultMessage: 'minute',
}),
},
{
value: 'hours',
text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.hourLabel', {
defaultMessage: 'hour',
}),
},
{
value: 'days',
text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.dayLabel', {
defaultMessage: 'day',
}),
},
{
value: 'weeks',
text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.weekLabel', {
defaultMessage: 'week',
}),
},
{
value: 'months',
text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.monthLabel', {
defaultMessage: 'month',
}),
},
{
value: 'years',
text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.yearLabel', {
defaultMessage: 'year',
}),
},
];
const pluralLastOptions: Array<{ value: string; text: any }> = [
{
value: 'seconds',
text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.secondsLabel', {
defaultMessage: 'seconds',
}),
},
{
value: 'minutes',
text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.minutesLabel', {
defaultMessage: 'minutes',
}),
},
{
value: 'hours',
text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.hoursLabel', {
defaultMessage: 'hours',
}),
},
{
value: 'days',
text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.daysLabel', {
defaultMessage: 'days',
}),
},
{
value: 'weeks',
text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.weeksLabel', {
defaultMessage: 'weeks',
}),
},
{
value: 'months',
text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.monthsLabel', {
defaultMessage: 'months',
}),
},
{
value: 'years',
text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.yearsLabel', {
defaultMessage: 'years',
}),
},
];
interface RangeDatePickerProps {
@ -51,6 +194,7 @@ interface RangeDatePickerProps {
disabled?: boolean;
isLoading?: boolean;
ref?: React.RefObject<any>;
intl: InjectedIntl;
}
export interface RecentlyUsed {
@ -67,350 +211,391 @@ interface RangeDatePickerState {
quickSelectUnit: string;
}
export class RangeDatePicker extends React.PureComponent<
RangeDatePickerProps,
RangeDatePickerState
> {
public readonly state = {
startDate: this.props.startDate,
endDate: this.props.endDate,
isPopoverOpen: false,
recentlyUsed: [],
quickSelectTime: 1,
quickSelectUnit: 'hours',
};
export const RangeDatePicker = injectI18n(
class extends React.PureComponent<RangeDatePickerProps, RangeDatePickerState> {
public static displayName = 'RangeDatePicker';
public readonly state = {
startDate: this.props.startDate,
endDate: this.props.endDate,
isPopoverOpen: false,
recentlyUsed: [],
quickSelectTime: 1,
quickSelectUnit: 'hours',
};
public render() {
const { isLoading, disabled } = this.props;
const { startDate, endDate } = this.state;
const quickSelectButton = (
<EuiButtonEmpty
className="euiFormControlLayout__prepend"
style={{ borderRight: 'none' }}
onClick={this.onButtonClick}
disabled={disabled}
aria-label="Date quick select"
size="xs"
iconType="arrowDown"
iconSide="right"
>
<EuiIcon type="calendar" />
</EuiButtonEmpty>
);
const commonlyUsed = this.renderCommonlyUsed(commonDates);
const recentlyUsed = this.renderRecentlyUsed([
...this.state.recentlyUsed,
...this.props.recentlyUsed,
]);
const quickSelectPopover = (
<EuiPopover
id="QuickSelectPopover"
button={quickSelectButton}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover.bind(this)}
anchorPosition="downLeft"
ownFocus
>
<div style={{ width: '400px' }}>
{this.renderQuickSelect()}
<EuiHorizontalRule />
{commonlyUsed}
<EuiHorizontalRule />
{recentlyUsed}
</div>
</EuiPopover>
);
return (
<EuiFormControlLayout prepend={quickSelectPopover}>
<EuiDatePickerRange
className="euiDatePickerRange--inGroup"
iconType={false}
public render() {
const { isLoading, disabled, intl } = this.props;
const { startDate, endDate } = this.state;
const quickSelectButton = (
<EuiButtonEmpty
className="euiFormControlLayout__prepend"
style={{ borderRight: 'none' }}
onClick={this.onButtonClick}
disabled={disabled}
fullWidth
startDateControl={
<EuiDatePicker
dateFormat="L LTS"
selected={startDate}
onChange={this.handleChangeStart}
isInvalid={startDate && endDate ? startDate > endDate : false}
fullWidth
aria-label="Start date"
disabled={disabled}
shouldCloseOnSelect
showTimeSelect
/>
}
endDateControl={
<EuiDatePicker
dateFormat="L LTS"
selected={endDate}
onChange={this.handleChangeEnd}
isInvalid={startDate && endDate ? startDate > endDate : false}
fullWidth
disabled={disabled}
isLoading={isLoading}
aria-label="End date"
shouldCloseOnSelect
showTimeSelect
popperPlacement="top-end"
/>
}
/>
</EuiFormControlLayout>
);
}
aria-label={intl.formatMessage({
id: 'xpack.infra.rangeDatePicker.dateQuickSelectAriaLabel',
defaultMessage: 'Date quick select',
})}
size="xs"
iconType="arrowDown"
iconSide="right"
>
<EuiIcon type="calendar" />
</EuiButtonEmpty>
);
public resetRangeDate(startDate: moment.Moment, endDate: moment.Moment) {
this.setState({
...this.state,
startDate,
endDate,
});
}
const commonlyUsed = this.renderCommonlyUsed(commonDates);
const recentlyUsed = this.renderRecentlyUsed([
...this.state.recentlyUsed,
...this.props.recentlyUsed,
]);
private handleChangeStart = (date: moment.Moment | null) => {
if (date && this.state.startDate !== date) {
this.props.onChangeRangeTime(date, this.state.endDate, false);
this.setState({
startDate: date,
});
const quickSelectPopover = (
<EuiPopover
id="QuickSelectPopover"
button={quickSelectButton}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover.bind(this)}
anchorPosition="downLeft"
ownFocus
>
<div style={{ width: '400px' }}>
{this.renderQuickSelect()}
<EuiHorizontalRule />
{commonlyUsed}
<EuiHorizontalRule />
{recentlyUsed}
</div>
</EuiPopover>
);
return (
<EuiFormControlLayout prepend={quickSelectPopover}>
<EuiDatePickerRange
className="euiDatePickerRange--inGroup"
iconType={false}
disabled={disabled}
fullWidth
startDateControl={
<EuiDatePicker
dateFormat="L LTS"
selected={startDate}
onChange={this.handleChangeStart}
isInvalid={startDate && endDate ? startDate > endDate : false}
fullWidth
aria-label={intl.formatMessage({
id: 'xpack.infra.rangeDatePicker.startDateAriaLabel',
defaultMessage: 'Start date',
})}
disabled={disabled}
shouldCloseOnSelect
showTimeSelect
/>
}
endDateControl={
<EuiDatePicker
dateFormat="L LTS"
selected={endDate}
onChange={this.handleChangeEnd}
isInvalid={startDate && endDate ? startDate > endDate : false}
fullWidth
disabled={disabled}
isLoading={isLoading}
aria-label={intl.formatMessage({
id: 'xpack.infra.rangeDatePicker.endDateAriaLabel',
defaultMessage: 'End date',
})}
shouldCloseOnSelect
showTimeSelect
popperPlacement="top-end"
/>
}
/>
</EuiFormControlLayout>
);
}
};
private handleChangeEnd = (date: moment.Moment | null) => {
if (date && this.state.endDate !== date) {
this.props.onChangeRangeTime(this.state.startDate, date, false);
public resetRangeDate(startDate: moment.Moment, endDate: moment.Moment) {
this.setState({
endDate: date,
});
}
};
private onButtonClick = () => {
this.setState({
isPopoverOpen: !this.state.isPopoverOpen,
});
};
private closePopover = (type: string, from?: string, to?: string) => {
const { startDate, endDate, recentlyUsed } = this.managedStartEndDateFromType(type, from, to);
this.setState(
{
...this.state,
isPopoverOpen: false,
startDate,
endDate,
});
}
private handleChangeStart = (date: moment.Moment | null) => {
if (date && this.state.startDate !== date) {
this.props.onChangeRangeTime(date, this.state.endDate, false);
this.setState({
startDate: date,
});
}
};
private handleChangeEnd = (date: moment.Moment | null) => {
if (date && this.state.endDate !== date) {
this.props.onChangeRangeTime(this.state.startDate, date, false);
this.setState({
endDate: date,
});
}
};
private onButtonClick = () => {
this.setState({
isPopoverOpen: !this.state.isPopoverOpen,
});
};
private closePopover = (type: string, from?: string, to?: string) => {
const { startDate, endDate, recentlyUsed } = this.managedStartEndDateFromType(type, from, to);
this.setState(
{
...this.state,
isPopoverOpen: false,
startDate,
endDate,
recentlyUsed,
},
() => {
if (type) {
this.props.onChangeRangeTime(startDate, endDate, true);
}
}
);
};
private managedStartEndDateFromType(type: string, from?: string, to?: string) {
const { intl } = this.props;
let { startDate, endDate } = this.state;
let recentlyUsed: RecentlyUsed[] = this.state.recentlyUsed;
let textJustUsed = type;
if (type === 'quick-select') {
textJustUsed = intl.formatMessage(
{
id: 'xpack.infra.rangeDatePicker.lastQuickSelectTimeText',
defaultMessage: 'Last {quickSelectTime} {quickSelectUnit}',
},
{
quickSelectTime: this.state.quickSelectTime,
quickSelectUnit:
this.state.quickSelectTime === 1
? get(find(singleLastOptions, { value: this.state.quickSelectUnit }), 'text')
: get(find(pluralLastOptions, { value: this.state.quickSelectUnit }), 'text'),
}
);
startDate = moment().subtract(this.state.quickSelectTime, this.state
.quickSelectUnit as moment.unitOfTime.DurationConstructor);
endDate = moment();
} else if (type === DatePickerDateOptions.today) {
startDate = moment().startOf('day');
endDate = moment()
.startOf('day')
.add(24, 'hour');
} else if (type === DatePickerDateOptions.yesterday) {
startDate = moment()
.subtract(1, 'day')
.startOf('day');
endDate = moment()
.subtract(1, 'day')
.startOf('day')
.add(24, 'hour');
} else if (type === DatePickerDateOptions.thisWeek) {
startDate = moment().startOf('week');
endDate = moment()
.startOf('week')
.add(1, 'week');
} else if (type === DatePickerDateOptions.weekToDate) {
startDate = moment().subtract(1, 'week');
endDate = moment();
} else if (type === DatePickerDateOptions.thisMonth) {
startDate = moment().startOf('month');
endDate = moment()
.startOf('month')
.add(1, 'month');
} else if (type === DatePickerDateOptions.monthToDate) {
startDate = moment().subtract(1, 'month');
endDate = moment();
} else if (type === DatePickerDateOptions.thisYear) {
startDate = moment().startOf('year');
endDate = moment()
.startOf('year')
.add(1, 'year');
} else if (type === DatePickerDateOptions.yearToDate) {
startDate = moment().subtract(1, 'year');
endDate = moment();
} else if (type === 'date-range' && to && from) {
startDate = moment(from);
endDate = moment(to);
}
textJustUsed =
type === 'date-range' || !type ? type : get(find(commonDates, { id: type }), 'label');
if (textJustUsed !== undefined && !find(recentlyUsed, ['text', textJustUsed])) {
recentlyUsed.unshift({ type, text: textJustUsed });
recentlyUsed = recentlyUsed.slice(0, 5);
}
return {
startDate,
endDate,
recentlyUsed,
},
() => {
if (type) {
this.props.onChangeRangeTime(startDate, endDate, true);
}
};
}
private renderQuickSelect = () => {
const { intl } = this.props;
return (
<Fragment>
<EuiTitle size="xxxs">
<span>
<FormattedMessage
id="xpack.infra.rangeDatePicker.quickSelectTitle"
defaultMessage="Quick select"
/>
</span>
</EuiTitle>
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="s" responsive={false}>
<EuiFlexItem>
<EuiTitle size="s">
<span>
<FormattedMessage
id="xpack.infra.rangeDatePicker.lastQuickSelectTitle"
defaultMessage="Last"
/>
</span>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow>
<EuiFieldNumber
aria-label={intl.formatMessage({
id: 'xpack.infra.rangeDatePicker.countOfFormRowAriaLabel',
defaultMessage: 'Count of',
})}
defaultValue="1"
value={this.state.quickSelectTime}
step={0}
onChange={arg => {
this.onChange('quickSelectTime', arg);
}}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow>
<EuiSelect
value={this.state.quickSelectUnit}
options={this.state.quickSelectTime === 1 ? singleLastOptions : pluralLastOptions}
onChange={arg => {
this.onChange('quickSelectUnit', arg);
}}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow>
<EuiButton
onClick={() => this.closePopover('quick-select')}
style={{ minWidth: 0 }}
>
<FormattedMessage
id="xpack.infra.rangeDatePicker.applyFormRowButtonLabel"
defaultMessage="Apply"
/>
</EuiButton>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
);
};
private onChange = (stateType: string, args: any) => {
let value = args.currentTarget.value;
if (stateType === 'quickSelectTime' && value !== '') {
value = parseInt(args.currentTarget.value, 10);
}
);
};
this.setState({
...this.state,
[stateType]: value,
});
};
private managedStartEndDateFromType(type: string, from?: string, to?: string) {
let { startDate, endDate } = this.state;
let recentlyUsed: RecentlyUsed[] = this.state.recentlyUsed;
let textJustUsed = type;
private renderCommonlyUsed = (recentlyCommonDates: Array<{ id: string; label: any }>) => {
const links = recentlyCommonDates.map(date => {
return (
<EuiFlexItem key={date.id}>
<EuiLink onClick={() => this.closePopover(date.id)}>{date.label}</EuiLink>
</EuiFlexItem>
);
});
if (type === 'quick-select') {
textJustUsed = `Last ${this.state.quickSelectTime} ${singularize(
this.state.quickSelectUnit,
this.state.quickSelectTime
)}`;
startDate = moment().subtract(this.state.quickSelectTime, this.state
.quickSelectUnit as moment.unitOfTime.DurationConstructor);
endDate = moment();
} else if (type === 'Today') {
startDate = moment().startOf('day');
endDate = moment()
.startOf('day')
.add(24, 'hour');
} else if (type === 'Yesterday') {
startDate = moment()
.subtract(1, 'day')
.startOf('day');
endDate = moment()
.subtract(1, 'day')
.startOf('day')
.add(24, 'hour');
} else if (type === 'This week') {
startDate = moment().startOf('week');
endDate = moment()
.startOf('week')
.add(1, 'week');
} else if (type === 'Week to date') {
startDate = moment().subtract(1, 'week');
endDate = moment();
} else if (type === 'This month') {
startDate = moment().startOf('month');
endDate = moment()
.startOf('month')
.add(1, 'month');
} else if (type === 'Month to date') {
startDate = moment().subtract(1, 'month');
endDate = moment();
} else if (type === 'This year') {
startDate = moment().startOf('year');
endDate = moment()
.startOf('year')
.add(1, 'year');
} else if (type === 'Year to date') {
startDate = moment().subtract(1, 'year');
endDate = moment();
} else if (type === 'date-range' && to && from) {
startDate = moment(from);
endDate = moment(to);
}
return (
<Fragment>
<EuiTitle size="xxxs">
<span>
<FormattedMessage
id="xpack.infra.rangeDatePicker.renderCommonlyUsedLinksTitle"
defaultMessage="Commonly used"
/>
</span>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s">
<EuiFlexGrid gutterSize="s" columns={2}>
{links}
</EuiFlexGrid>
</EuiText>
</Fragment>
);
};
if (textJustUsed !== undefined && !find(recentlyUsed, ['text', textJustUsed])) {
recentlyUsed.unshift({ type, text: textJustUsed });
recentlyUsed = recentlyUsed.slice(0, 5);
}
private renderRecentlyUsed = (recentDates: RecentlyUsed[]) => {
const links = recentDates.map((date: RecentlyUsed) => {
let dateRange;
let dateLink = (
<EuiLink onClick={() => this.closePopover(date.type)}>{dateRange || date.text}</EuiLink>
);
if (typeof date.text !== 'string') {
dateRange = `${date.text[0]} ${date.text[1]}`;
dateLink = (
<EuiLink onClick={() => this.closePopover(date.type, date.text[0], date.text[1])}>
{dateRange || date.type}
</EuiLink>
);
}
return {
startDate,
endDate,
recentlyUsed,
return (
<EuiFlexItem grow={false} key={`${dateRange || date.type}`}>
{dateLink}
</EuiFlexItem>
);
});
return (
<Fragment>
<EuiTitle size="xxxs">
<span>
<FormattedMessage
id="xpack.infra.rangeDatePicker.recentlyUsedDateRangesTitle"
defaultMessage="Recently used date ranges"
/>
</span>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s">
<EuiFlexGroup gutterSize="s" style={{ flexDirection: 'column' }}>
{links}
</EuiFlexGroup>
</EuiText>
</Fragment>
);
};
}
private renderQuickSelect = () => {
const lastOptions = [
{ value: 'seconds', text: singularize('seconds', this.state.quickSelectTime) },
{ value: 'minutes', text: singularize('minutes', this.state.quickSelectTime) },
{ value: 'hours', text: singularize('hours', this.state.quickSelectTime) },
{ value: 'days', text: singularize('days', this.state.quickSelectTime) },
{ value: 'weeks', text: singularize('weeks', this.state.quickSelectTime) },
{ value: 'months', text: singularize('months', this.state.quickSelectTime) },
{ value: 'years', text: singularize('years', this.state.quickSelectTime) },
];
return (
<Fragment>
<EuiTitle size="xxxs">
<span>Quick select</span>
</EuiTitle>
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="s" responsive={false}>
<EuiFlexItem>
<EuiTitle size="s">
<span>Last</span>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow>
<EuiFieldNumber
aria-label="Count of"
defaultValue="1"
value={this.state.quickSelectTime}
step={0}
onChange={arg => {
this.onChange('quickSelectTime', arg);
}}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow>
<EuiSelect
value={this.state.quickSelectUnit}
options={lastOptions}
onChange={arg => {
this.onChange('quickSelectUnit', arg);
}}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow>
<EuiButton onClick={() => this.closePopover('quick-select')} style={{ minWidth: 0 }}>
Apply
</EuiButton>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
);
};
private onChange = (stateType: string, args: any) => {
let value = args.currentTarget.value;
if (stateType === 'quickSelectTime' && value !== '') {
value = parseInt(args.currentTarget.value, 10);
}
this.setState({
...this.state,
[stateType]: value,
});
};
private renderCommonlyUsed = (recentlyCommonDates: string[]) => {
const links = recentlyCommonDates.map(date => {
return (
<EuiFlexItem key={date}>
<EuiLink onClick={() => this.closePopover(date)}>{date}</EuiLink>
</EuiFlexItem>
);
});
return (
<Fragment>
<EuiTitle size="xxxs">
<span>Commonly used</span>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s">
<EuiFlexGrid gutterSize="s" columns={2}>
{links}
</EuiFlexGrid>
</EuiText>
</Fragment>
);
};
private renderRecentlyUsed = (recentDates: RecentlyUsed[]) => {
const links = recentDates.map((date: RecentlyUsed) => {
let dateRange;
let dateLink = (
<EuiLink onClick={() => this.closePopover(date.type)}>{dateRange || date.text}</EuiLink>
);
if (typeof date.text !== 'string') {
dateRange = `${date.text[0]} ${date.text[1]}`;
dateLink = (
<EuiLink onClick={() => this.closePopover(date.type, date.text[0], date.text[1])}>
{dateRange || date.type}
</EuiLink>
);
}
return (
<EuiFlexItem grow={false} key={`${dateRange || date.type}`}>
{dateLink}
</EuiFlexItem>
);
});
return (
<Fragment>
<EuiTitle size="xxxs">
<span>Recently used date ranges</span>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s">
<EuiFlexGroup gutterSize="s" style={{ flexDirection: 'column' }}>
{links}
</EuiFlexGroup>
</EuiText>
</Fragment>
);
};
}
const singularize = (str: string, qty: number) => (qty === 1 ? str.slice(0, -1) : str);
);

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, EuiEmptyPrompt } 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';
@ -36,6 +37,7 @@ interface Props {
reload: () => void;
onDrilldown: (filter: KueryFilterQuery) => void;
timeRange: InfraTimerangeInput;
intl: InjectedIntl;
}
interface MetricFormatter {
@ -89,121 +91,150 @@ const calculateBoundsFromMap = (map: InfraWaffleData): InfraWaffleMapBounds => {
return { min: min(values), max: max(values) };
};
export class Waffle extends React.Component<Props, {}> {
public render() {
const { loading, map, reload, timeRange } = this.props;
if (loading) {
return <InfraLoadingPanel height="100%" width="100%" text="Loading data" />;
} else if (!loading && map && map.length === 0) {
export const Waffle = injectI18n(
class extends React.Component<Props, {}> {
public static displayName = 'Waffle';
public render() {
const { loading, map, reload, timeRange, intl } = this.props;
if (loading) {
return (
<InfraLoadingPanel
height="100%"
width="100%"
text={intl.formatMessage({
id: 'xpack.infra.waffle.loadingDataText',
defaultMessage: 'Loading data',
})}
/>
);
} else if (!loading && map && map.length === 0) {
return (
<CenteredEmptyPrompt
title={
<h2>
<FormattedMessage
id="xpack.infra.waffle.noDataTitle"
defaultMessage="There is no data to display."
/>
</h2>
}
titleSize="m"
body={
<p>
<FormattedMessage
id="xpack.infra.waffle.noDataDescription"
defaultMessage="Try adjusting your time or filter."
/>
</p>
}
actions={
<EuiButton
iconType="refresh"
color="primary"
fill
onClick={() => {
reload();
}}
>
<FormattedMessage
id="xpack.infra.waffle.checkNewDataButtonLabel"
defaultMessage="Check for new data"
/>
</EuiButton>
}
data-test-subj="noMetricsDataPrompt"
/>
);
}
const { metric } = this.props.options;
const metricFormatter = get(
METRIC_FORMATTERS,
metric.type,
METRIC_FORMATTERS[InfraMetricType.count]
);
const bounds = (metricFormatter && metricFormatter.bounds) || calculateBoundsFromMap(map);
return (
<CenteredEmptyPrompt
title={<h2>There is no data to display.</h2>}
titleSize="m"
body={<p>Try adjusting your time or filter.</p>}
actions={
<EuiButton
iconType="refresh"
color="primary"
fill
onClick={() => {
reload();
}}
>
Check for new data
</EuiButton>
}
data-test-subj="noMetricsDataPrompt"
/>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => {
const groupsWithLayout = applyWaffleMapLayout(map, width, height);
return (
<WaffleMapOuterContiner
innerRef={(el: any) => measureRef(el)}
data-test-subj="waffleMap"
>
<WaffleMapInnerContainer>
{groupsWithLayout.map(this.renderGroup(bounds, timeRange))}
</WaffleMapInnerContainer>
<Legend
formatter={this.formatter}
bounds={bounds}
legend={this.props.options.legend}
/>
</WaffleMapOuterContiner>
);
}}
</AutoSizer>
);
}
const { metric } = this.props.options;
const metricFormatter = get(
METRIC_FORMATTERS,
metric.type,
METRIC_FORMATTERS[InfraMetricType.count]
);
const bounds = (metricFormatter && metricFormatter.bounds) || calculateBoundsFromMap(map);
return (
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => {
const groupsWithLayout = applyWaffleMapLayout(map, width, height);
return (
<WaffleMapOuterContiner
innerRef={(el: any) => measureRef(el)}
data-test-subj="waffleMap"
>
<WaffleMapInnerContainer>
{groupsWithLayout.map(this.renderGroup(bounds, timeRange))}
</WaffleMapInnerContainer>
<Legend
formatter={this.formatter}
bounds={bounds}
legend={this.props.options.legend}
/>
</WaffleMapOuterContiner>
);
}}
</AutoSizer>
);
// TODO: Change this to a real implimentation using the tickFormatter from the prototype as an example.
private formatter = (val: string | number) => {
const { metric } = this.props.options;
const metricFormatter = get(
METRIC_FORMATTERS,
metric.type,
METRIC_FORMATTERS[InfraMetricType.count]
);
if (val == null) {
return '';
}
const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template);
return formatter(val);
};
private handleDrilldown = (filter: string) => {
this.props.onDrilldown({
kind: 'kuery',
expression: filter,
});
return;
};
private renderGroup = (bounds: InfraWaffleMapBounds, timeRange: InfraTimerangeInput) => (
group: InfraWaffleMapGroup
) => {
if (isWaffleMapGroupWithGroups(group)) {
return (
<GroupOfGroups
onDrilldown={this.handleDrilldown}
key={group.id}
options={this.props.options}
group={group}
formatter={this.formatter}
bounds={bounds}
nodeType={this.props.nodeType}
timeRange={timeRange}
/>
);
}
if (isWaffleMapGroupWithNodes(group)) {
return (
<GroupOfNodes
key={group.id}
options={this.props.options}
group={group}
onDrilldown={this.handleDrilldown}
formatter={this.formatter}
isChild={false}
bounds={bounds}
nodeType={this.props.nodeType}
timeRange={timeRange}
/>
);
}
};
}
// TODO: Change this to a real implimentation using the tickFormatter from the prototype as an example.
private formatter = (val: string | number) => {
const { metric } = this.props.options;
const metricFormatter = get(
METRIC_FORMATTERS,
metric.type,
METRIC_FORMATTERS[InfraMetricType.count]
);
if (val == null) {
return '';
}
const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template);
return formatter(val);
};
private handleDrilldown = (filter: string) => {
this.props.onDrilldown({
kind: 'kuery',
expression: filter,
});
return;
};
private renderGroup = (bounds: InfraWaffleMapBounds, timeRange: InfraTimerangeInput) => (
group: InfraWaffleMapGroup
) => {
if (isWaffleMapGroupWithGroups(group)) {
return (
<GroupOfGroups
onDrilldown={this.handleDrilldown}
key={group.id}
options={this.props.options}
group={group}
formatter={this.formatter}
bounds={bounds}
nodeType={this.props.nodeType}
timeRange={timeRange}
/>
);
}
if (isWaffleMapGroupWithNodes(group)) {
return (
<GroupOfNodes
key={group.id}
options={this.props.options}
group={group}
onDrilldown={this.handleDrilldown}
formatter={this.formatter}
isChild={false}
bounds={bounds}
nodeType={this.props.nodeType}
timeRange={timeRange}
/>
);
}
};
}
);
const WaffleMapOuterContiner = styled.div`
flex: 1 0 0%;

View file

@ -5,6 +5,7 @@
*/
import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiPopover } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { InfraNodeType, InfraTimerangeInput } from '../../../common/graphql/types';
@ -14,72 +15,74 @@ import { getNodeDetailUrl, getNodeLogsUrl } from '../../pages/link_to';
interface Props {
options: InfraWaffleMapOptions;
timeRange: InfraTimerangeInput;
children: any;
node: InfraWaffleMapNode;
nodeType: InfraNodeType;
isPopoverOpen: boolean;
closePopover: () => void;
intl: InjectedIntl;
}
export const NodeContextMenu: React.SFC<Props> = ({
options,
timeRange,
children,
node,
isPopoverOpen,
closePopover,
nodeType,
}) => {
const nodeName = node.path.length > 0 ? node.path[node.path.length - 1].value : undefined;
const nodeLogsUrl = nodeName
? getNodeLogsUrl({
nodeType,
nodeName,
time: timeRange.to,
})
: undefined;
const nodeDetailUrl = nodeName
? getNodeDetailUrl({
nodeType,
nodeName,
from: timeRange.from,
to: timeRange.to,
})
: undefined;
export const NodeContextMenu = injectI18n(
({ options, timeRange, children, node, isPopoverOpen, closePopover, nodeType, intl }: Props) => {
const nodeName = node.path.length > 0 ? node.path[node.path.length - 1].value : undefined;
const nodeLogsUrl = nodeName
? getNodeLogsUrl({
nodeType,
nodeName,
time: timeRange.to,
})
: undefined;
const nodeDetailUrl = nodeName
? getNodeDetailUrl({
nodeType,
nodeName,
from: timeRange.from,
to: timeRange.to,
})
: undefined;
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: '',
items: [
...(nodeLogsUrl
? [
{
name: `View logs`,
href: nodeLogsUrl,
},
]
: []),
...(nodeDetailUrl
? [
{
name: `View metrics`,
href: nodeDetailUrl,
},
]
: []),
],
},
];
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: '',
items: [
...(nodeLogsUrl
? [
{
name: intl.formatMessage({
id: 'xpack.infra.nodeContextMenu.viewLogsName',
defaultMessage: 'View logs',
}),
href: nodeLogsUrl,
},
]
: []),
...(nodeDetailUrl
? [
{
name: intl.formatMessage({
id: 'xpack.infra.nodeContextMenu.viewMetricsName',
defaultMessage: 'View metrics',
}),
href: nodeDetailUrl,
},
]
: []),
],
},
];
return (
<EuiPopover
closePopover={closePopover}
id={`${node.id}-popover`}
isOpen={isPopoverOpen}
button={children}
panelPaddingSize="none"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
};
return (
<EuiPopover
closePopover={closePopover}
id={`${node.id}-popover`}
isOpen={isPopoverOpen}
button={children}
panelPaddingSize="none"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
}
);

View file

@ -12,6 +12,7 @@ import {
EuiFilterGroup,
EuiPopover,
} from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { InfraNodeType, InfraPathInput, InfraPathType } from '../../../common/graphql/types';
@ -19,55 +20,161 @@ interface Props {
nodeType: InfraNodeType;
groupBy: InfraPathInput[];
onChange: (groupBy: InfraPathInput[]) => void;
intl: InjectedIntl;
}
const OPTIONS = {
[InfraNodeType.pod]: [
{ text: 'Namespace', type: InfraPathType.terms, field: 'kubernetes.namespace' },
{ text: 'Node', type: InfraPathType.terms, field: 'kubernetes.node.name' },
],
[InfraNodeType.container]: [
{ text: 'Host', type: InfraPathType.terms, field: 'host.name' },
{ text: 'Availability Zone', type: InfraPathType.terms, field: 'meta.cloud.availability_zone' },
{ text: 'Machine Type', type: InfraPathType.terms, field: 'meta.cloud.machine_type' },
{ text: 'Project ID', type: InfraPathType.terms, field: 'meta.cloud.project_id' },
{ text: 'Provider', type: InfraPathType.terms, field: 'meta.cloud.provider' },
],
[InfraNodeType.host]: [
{ text: 'Availability Zone', type: InfraPathType.terms, field: 'meta.cloud.availability_zone' },
{ text: 'Machine Type', type: InfraPathType.terms, field: 'meta.cloud.machine_type' },
{ text: 'Project ID', type: InfraPathType.terms, field: 'meta.cloud.project_id' },
{ text: 'Cloud Provider', type: InfraPathType.terms, field: 'meta.cloud.provider' },
],
let OPTIONS: { [P in InfraNodeType]: Array<{ text: string; type: InfraPathType; field: string }> };
const getOptions = (
nodeType: InfraNodeType,
intl: InjectedIntl
): Array<{ text: string; type: InfraPathType; field: string }> => {
if (!OPTIONS) {
OPTIONS = {
[InfraNodeType.pod]: [
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.podGroupByOptions.namespaceLabel',
defaultMessage: 'Namespace',
}),
type: InfraPathType.terms,
field: 'kubernetes.namespace',
},
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.podGroupByOptions.nodeLabel',
defaultMessage: 'Node',
}),
type: InfraPathType.terms,
field: 'kubernetes.node.name',
},
],
[InfraNodeType.container]: [
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.containerGroupByOptions.hostLabel',
defaultMessage: 'Host',
}),
type: InfraPathType.terms,
field: 'host.name',
},
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.containerGroupByOptions.availabilityZoneLabel',
defaultMessage: 'Availability Zone',
}),
type: InfraPathType.terms,
field: 'meta.cloud.availability_zone',
},
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.containerGroupByOptions.machineTypeLabel',
defaultMessage: 'Machine Type',
}),
type: InfraPathType.terms,
field: 'meta.cloud.machine_type',
},
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.containerGroupByOptions.projectIDLabel',
defaultMessage: 'Project ID',
}),
type: InfraPathType.terms,
field: 'meta.cloud.project_id',
},
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.containerGroupByOptions.providerLabel',
defaultMessage: 'Provider',
}),
type: InfraPathType.terms,
field: 'meta.cloud.provider',
},
],
[InfraNodeType.host]: [
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.hostGroupByOptions.availabilityZoneLabel',
defaultMessage: 'Availability Zone',
}),
type: InfraPathType.terms,
field: 'meta.cloud.availability_zone',
},
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.hostGroupByOptions.machineTypeLabel',
defaultMessage: 'Machine Type',
}),
type: InfraPathType.terms,
field: 'meta.cloud.machine_type',
},
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.hostGroupByOptions.projectIDLabel',
defaultMessage: 'Project ID',
}),
type: InfraPathType.terms,
field: 'meta.cloud.project_id',
},
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.hostGroupByOptions.cloudProviderLabel',
defaultMessage: 'Cloud Provider',
}),
type: InfraPathType.terms,
field: 'meta.cloud.provider',
},
],
};
}
return OPTIONS[nodeType];
};
const initialState = {
isPopoverOpen: false,
};
type State = Readonly<typeof initialState>;
export class WaffleGroupByControls extends React.PureComponent<Props, State> {
public readonly state: State = initialState;
public render() {
const { nodeType, groupBy } = this.props;
const options = OPTIONS[nodeType];
if (!options.length) {
throw Error(`Unable to select group by options for ${nodeType}`);
}
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 'firstPanel',
title: 'Select up to two groupings',
items: options.map(o => {
const icon = groupBy.some(g => g.field === o.field) ? 'check' : 'empty';
const panel = { name: o.text, onClick: this.handleClick(o.field), icon };
return panel;
}),
},
];
const buttonBody =
groupBy.length > 0
? groupBy
export const WaffleGroupByControls = injectI18n(
class extends React.PureComponent<Props, State> {
public static displayName = 'WaffleGroupByControls';
public readonly state: State = initialState;
public render() {
const { nodeType, groupBy, intl } = this.props;
const options = getOptions(nodeType, intl);
if (!options.length) {
throw Error(
intl.formatMessage(
{
id: 'xpack.infra.waffle.unableToSelectGroupErrorMessage',
defaultMessage: 'Unable to select group by options for {nodeType}',
},
{
nodeType,
}
)
);
}
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 'firstPanel',
title: intl.formatMessage({
id: 'xpack.infra.waffle.selectTwoGroupingsTitle',
defaultMessage: 'Select up to two groupings',
}),
items: options.map(o => {
const icon = groupBy.some(g => g.field === o.field) ? 'check' : 'empty';
const panel = { name: o.text, onClick: this.handleClick(o.field), icon };
return panel;
}),
},
];
const buttonBody =
groupBy.length > 0 ? (
groupBy
.map(g => options.find(o => o.field === g.field))
.filter(o => o != null)
// In this map the `o && o.field` is totally unnecessary but Typescript is
@ -77,56 +184,71 @@ export class WaffleGroupByControls extends React.PureComponent<Props, State> {
key={o && o.field}
iconType="cross"
iconOnClick={this.handleRemove((o && o.field) || '')}
iconOnClickAriaLabel={`Remove ${o && o.text} grouping`}
iconOnClickAriaLabel={intl.formatMessage(
{
id: 'xpack.infra.waffle.removeGroupingItemAriaLabel',
defaultMessage: 'Remove {groupingItem} grouping',
},
{
groupingItem: o && o.text,
}
)}
>
{o && o.text}
</EuiBadge>
))
: 'All';
const button = (
<EuiFilterButton iconType="arrowDown" onClick={this.handleToggle}>
Group By: {buttonBody}
</EuiFilterButton>
);
) : (
<FormattedMessage id="xpack.infra.waffle.groupByAllTitle" defaultMessage="All" />
);
const button = (
<EuiFilterButton iconType="arrowDown" onClick={this.handleToggle}>
<FormattedMessage
id="xpack.infra.waffle.groupByButtonLabel"
defaultMessage="Group By: "
/>
{buttonBody}
</EuiFilterButton>
);
return (
<EuiFilterGroup>
<EuiPopover
isOpen={this.state.isPopoverOpen}
id="groupByPanel"
button={button}
panelPaddingSize="none"
closePopover={this.handleClose}
>
<EuiContextMenu initialPanelId="firstPanel" panels={panels} />
</EuiPopover>
</EuiFilterGroup>
);
}
private handleRemove = (field: string) => () => {
const { groupBy } = this.props;
this.props.onChange(groupBy.filter(g => g.field !== field));
// We need to close the panel after we rmeove the pill icon otherwise
// it will remain open because the click is still captured by the EuiFilterButton
setTimeout(() => this.handleClose());
};
private handleClose = () => {
this.setState({ isPopoverOpen: false });
};
private handleToggle = () => {
this.setState(state => ({ isPopoverOpen: !state.isPopoverOpen }));
};
private handleClick = (field: string) => () => {
const { groupBy } = this.props;
if (groupBy.some(g => g.field === field)) {
this.handleRemove(field)();
} else if (this.props.groupBy.length < 2) {
this.props.onChange([...groupBy, { type: InfraPathType.terms, field }]);
this.handleClose();
return (
<EuiFilterGroup>
<EuiPopover
isOpen={this.state.isPopoverOpen}
id="groupByPanel"
button={button}
panelPaddingSize="none"
closePopover={this.handleClose}
>
<EuiContextMenu initialPanelId="firstPanel" panels={panels} />
</EuiPopover>
</EuiFilterGroup>
);
}
};
}
private handleRemove = (field: string) => () => {
const { groupBy } = this.props;
this.props.onChange(groupBy.filter(g => g.field !== field));
// We need to close the panel after we rmeove the pill icon otherwise
// it will remain open because the click is still captured by the EuiFilterButton
setTimeout(() => this.handleClose());
};
private handleClose = () => {
this.setState({ isPopoverOpen: false });
};
private handleToggle = () => {
this.setState(state => ({ isPopoverOpen: !state.isPopoverOpen }));
};
private handleClick = (field: string) => () => {
const { groupBy } = this.props;
if (groupBy.some(g => g.field === field)) {
this.handleRemove(field)();
} else if (this.props.groupBy.length < 2) {
this.props.onChange([...groupBy, { type: InfraPathType.terms, field }]);
this.handleClose();
}
};
}
);

View file

@ -11,35 +11,115 @@ import {
EuiFilterGroup,
EuiPopover,
} from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { InfraMetricInput, InfraMetricType, InfraNodeType } from '../../../common/graphql/types';
interface Props {
nodeType: InfraNodeType;
metric: InfraMetricInput;
onChange: (metric: InfraMetricInput) => void;
intl: InjectedIntl;
}
const OPTIONS = {
[InfraNodeType.pod]: [
{ text: 'CPU Usage', value: InfraMetricType.cpu },
{ text: 'Memory Usage', value: InfraMetricType.memory },
{ text: 'Inbound Traffic', value: InfraMetricType.rx },
{ text: 'Outbound Traffic', value: InfraMetricType.tx },
],
[InfraNodeType.container]: [
{ text: 'CPU Usage', value: InfraMetricType.cpu },
{ text: 'Memory Usage', value: InfraMetricType.memory },
{ text: 'Inbound Traffic', value: InfraMetricType.rx },
{ text: 'Outbound Traffic', value: InfraMetricType.tx },
],
[InfraNodeType.host]: [
{ text: 'CPU Usage', value: InfraMetricType.cpu },
{ text: 'Memory Usage', value: InfraMetricType.memory },
{ text: 'Load', value: InfraMetricType.load },
{ text: 'Inbound Traffic', value: InfraMetricType.rx },
{ text: 'Outbound Traffic', value: InfraMetricType.tx },
{ text: 'Log Rate', value: InfraMetricType.logRate },
],
let OPTIONS: { [P in InfraNodeType]: Array<{ text: string; value: InfraMetricType }> };
const getOptions = (
nodeType: InfraNodeType,
intl: InjectedIntl
): Array<{ text: string; value: InfraMetricType }> => {
if (!OPTIONS) {
const CPUUsage = intl.formatMessage({
id: 'xpack.infra.waffle.metricOptions.cpuUsageText',
defaultMessage: 'CPU Usage',
});
const MemoryUsage = intl.formatMessage({
id: 'xpack.infra.waffle.metricOptions.memoryUsageText',
defaultMessage: 'Memory Usage',
});
const InboundTraffic = intl.formatMessage({
id: 'xpack.infra.waffle.metricOptions.inboundTrafficText',
defaultMessage: 'Inbound Traffic',
});
const OutboundTraffic = intl.formatMessage({
id: 'xpack.infra.waffle.metricOptions.outboundTrafficText',
defaultMessage: 'Outbound Traffic',
});
OPTIONS = {
[InfraNodeType.pod]: [
{
text: CPUUsage,
value: InfraMetricType.cpu,
},
{
text: MemoryUsage,
value: InfraMetricType.memory,
},
{
text: InboundTraffic,
value: InfraMetricType.rx,
},
{
text: OutboundTraffic,
value: InfraMetricType.tx,
},
],
[InfraNodeType.container]: [
{
text: CPUUsage,
value: InfraMetricType.cpu,
},
{
text: MemoryUsage,
value: InfraMetricType.memory,
},
{
text: InboundTraffic,
value: InfraMetricType.rx,
},
{
text: OutboundTraffic,
value: InfraMetricType.tx,
},
],
[InfraNodeType.host]: [
{
text: CPUUsage,
value: InfraMetricType.cpu,
},
{
text: MemoryUsage,
value: InfraMetricType.memory,
},
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.metricOptions.loadText',
defaultMessage: 'Load',
}),
value: InfraMetricType.load,
},
{
text: InboundTraffic,
value: InfraMetricType.rx,
},
{
text: OutboundTraffic,
value: InfraMetricType.tx,
},
{
text: intl.formatMessage({
id: 'xpack.infra.waffle.metricOptions.hostLogRateText',
defaultMessage: 'Log Rate',
}),
value: InfraMetricType.logRate,
},
],
};
}
return OPTIONS[nodeType];
};
const initialState = {
@ -47,60 +127,73 @@ const initialState = {
};
type State = Readonly<typeof initialState>;
export class WaffleMetricControls extends React.PureComponent<Props, State> {
public readonly state: State = initialState;
public render() {
const { metric } = this.props;
const options = OPTIONS[this.props.nodeType];
const value = metric.type;
if (!options.length || !value) {
throw Error('Unable to select options or value for metric.');
}
const currentLabel = options.find(o => o.value === metric.type);
if (!currentLabel) {
return 'null';
}
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: '',
items: options.map(o => {
const icon = o.value === metric.type ? 'check' : 'empty';
const panel = { name: o.text, onClick: this.handleClick(o.value), icon };
return panel;
}),
},
];
const button = (
<EuiFilterButton iconType="arrowDown" onClick={this.handleToggle}>
Metric: {currentLabel.text}
</EuiFilterButton>
);
export const WaffleMetricControls = injectI18n(
class extends React.PureComponent<Props, State> {
public static displayName = 'WaffleMetricControls';
public readonly state: State = initialState;
public render() {
const { metric, nodeType, intl } = this.props;
const options = getOptions(nodeType, intl);
const value = metric.type;
return (
<EuiFilterGroup>
<EuiPopover
isOpen={this.state.isPopoverOpen}
id="metricsPanel"
button={button}
panelPaddingSize="none"
closePopover={this.handleClose}
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</EuiFilterGroup>
);
if (!options.length || !value) {
throw Error(
intl.formatMessage({
id: 'xpack.infra.waffle.unableToSelectMetricErrorTitle',
defaultMessage: 'Unable to select options or value for metric.',
})
);
}
const currentLabel = options.find(o => o.value === metric.type);
if (!currentLabel) {
return 'null';
}
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: '',
items: options.map(o => {
const icon = o.value === metric.type ? 'check' : 'empty';
const panel = { name: o.text, onClick: this.handleClick(o.value), icon };
return panel;
}),
},
];
const button = (
<EuiFilterButton iconType="arrowDown" onClick={this.handleToggle}>
<FormattedMessage
id="xpack.infra.waffle.metricButtonLabel"
defaultMessage="Metric: {selectedMetric}"
values={{ selectedMetric: currentLabel.text }}
/>
</EuiFilterButton>
);
return (
<EuiFilterGroup>
<EuiPopover
isOpen={this.state.isPopoverOpen}
id="metricsPanel"
button={button}
panelPaddingSize="none"
closePopover={this.handleClose}
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</EuiFilterGroup>
);
}
private handleClose = () => {
this.setState({ isPopoverOpen: false });
};
private handleToggle = () => {
this.setState(state => ({ isPopoverOpen: !state.isPopoverOpen }));
};
private handleClick = (value: InfraMetricType) => () => {
this.props.onChange({ type: value });
this.handleClose();
};
}
private handleClose = () => {
this.setState({ isPopoverOpen: false });
};
private handleToggle = () => {
this.setState(state => ({ isPopoverOpen: !state.isPopoverOpen }));
};
private handleClick = (value: InfraMetricType) => () => {
this.props.onChange({ type: value });
this.handleClose();
};
}
);

View file

@ -5,6 +5,7 @@
*/
import { EuiKeyPadMenu, EuiKeyPadMenuItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import {
InfraMetricInput,
@ -24,7 +25,15 @@ export class WaffleNodeTypeSwitcher extends React.PureComponent<Props> {
public render() {
return (
<EuiKeyPadMenu>
<EuiKeyPadMenuItem label="Hosts" onClick={this.handleClick(InfraNodeType.host)}>
<EuiKeyPadMenuItem
label={
<FormattedMessage
id="xpack.infra.waffle.nodeTypeSwitcher.hostsLabel"
defaultMessage="Hosts"
/>
}
onClick={this.handleClick(InfraNodeType.host)}
>
<img src="../plugins/infra/images/hosts.svg" className="euiIcon euiIcon--large" />
</EuiKeyPadMenuItem>
<EuiKeyPadMenuItem label="Kubernetes" onClick={this.handleClick(InfraNodeType.pod)}>

View file

@ -5,6 +5,7 @@
*/
import { EuiButtonEmpty, EuiDatePicker, EuiFormControlLayout } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import moment, { Moment } from 'moment';
import React from 'react';
@ -29,11 +30,17 @@ export class WaffleTimeControls extends React.Component<WaffleTimeControlsProps>
iconType="pause"
onClick={this.stopLiveStreaming}
>
Stop refreshing
<FormattedMessage
id="xpack.infra.waffleTime.stopRefreshingButtonLabel"
defaultMessage="Stop refreshing"
/>
</EuiButtonEmpty>
) : (
<EuiButtonEmpty iconSide="left" iconType="play" onClick={this.startLiveStreaming}>
Auto-refresh
<FormattedMessage
id="xpack.infra.waffleTime.autoRefreshButtonLabel"
defaultMessage="Auto-refresh"
/>
</EuiButtonEmpty>
);

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
@ -28,27 +29,39 @@ export const WithLogMinimap = asChildFunctionRenderer(withLogMinimap);
export const availableIntervalSizes = [
{
label: '1 Year',
label: i18n.translate('xpack.infra.mapLogs.oneYearLabel', {
defaultMessage: '1 Year',
}),
intervalSize: 1000 * 60 * 60 * 24 * 365,
},
{
label: '1 Month',
label: i18n.translate('xpack.infra.mapLogs.oneMonthLabel', {
defaultMessage: '1 Month',
}),
intervalSize: 1000 * 60 * 60 * 24 * 30,
},
{
label: '1 Week',
label: i18n.translate('xpack.infra.mapLogs.oneWeekLabel', {
defaultMessage: '1 Week',
}),
intervalSize: 1000 * 60 * 60 * 24 * 7,
},
{
label: '1 Day',
label: i18n.translate('xpack.infra.mapLogs.oneDayLabel', {
defaultMessage: '1 Day',
}),
intervalSize: 1000 * 60 * 60 * 24,
},
{
label: '1 Hour',
label: i18n.translate('xpack.infra.mapLogs.oneHourLabel', {
defaultMessage: '1 Hour',
}),
intervalSize: 1000 * 60 * 60,
},
{
label: '1 Minute',
label: i18n.translate('xpack.infra.mapLogs.oneMinuteLabel', {
defaultMessage: '1 Minute',
}),
intervalSize: 1000 * 60,
},
];

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { first, last } from 'lodash';
import { InfraNode, InfraNodePath } from '../../../common/graphql/types';
import {
@ -44,7 +45,12 @@ function findOrCreateGroupWithNodes(
}
return {
id,
name: id === '__all__' ? 'All' : last(path).value,
name:
id === '__all__'
? i18n.translate('xpack.infra.nodesToWaffleMap.groupsWithNodes.allName', {
defaultMessage: 'All',
})
: last(path).value,
count: 0,
width: 0,
squareSize: 0,
@ -63,7 +69,12 @@ function findOrCreateGroupWithGroups(
}
return {
id,
name: id === '__all__' ? 'All' : last(path).value,
name:
id === '__all__'
? i18n.translate('xpack.infra.nodesToWaffleMap.groupsWithGroups.allName', {
defaultMessage: 'All',
})
: last(path).value,
count: 0,
width: 0,
squareSize: 0,

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { I18nServiceType } from '@kbn/i18n/angular';
import {
FeatureCatalogueCategory,
FeatureCatalogueRegistryProvider,
@ -11,22 +12,30 @@ import {
const APP_ID = 'infra';
FeatureCatalogueRegistryProvider.register(() => ({
FeatureCatalogueRegistryProvider.register((i18n: I18nServiceType) => ({
id: 'infraops',
title: 'Infrastructure',
description:
'Explore infrastructure metrics and logs for common servers, containers, and services.',
title: i18n('xpack.infra.registerFeatures.infraOpsTitle', {
defaultMessage: 'Infrastructure',
}),
description: i18n('xpack.infra.registerFeatures.infraOpsDescription', {
defaultMessage:
'Explore infrastructure metrics and logs for common servers, containers, and services.',
}),
icon: 'infraApp',
path: `/app/${APP_ID}#home`,
showOnHomePage: true,
category: FeatureCatalogueCategory.DATA,
}));
FeatureCatalogueRegistryProvider.register(() => ({
FeatureCatalogueRegistryProvider.register((i18n: I18nServiceType) => ({
id: 'infralogging',
title: 'Logs',
description:
'Stream logs in real time or scroll through historical views in a console-like experience.',
title: i18n('xpack.infra.registerFeatures.logsTitle', {
defaultMessage: 'Logs',
}),
description: i18n('xpack.infra.registerFeatures.logsDescription', {
defaultMessage:
'Stream logs in real time or scroll through historical views in a console-like experience.',
}),
icon: 'loggingApp',
path: `/app/${APP_ID}#logs`,
showOnHomePage: true,