[7.x] [Infra/Logs] Replace injectI18n with i18n.translate (#44950) (#48174)

* [Infra/Logs] Replace injectI18n with i18n.translate (#44950)

* [Infra/Logs] Replace injectI18n with i18n.translate

* Remove some missed instances of injecti18n

* Fix type errors

* Remove extraneous dive call in tests

* Remove extraneous whitespace

* Fix lint

* Rebase MetricDetail component

* Fix i18n values

* Fix bad merge
This commit is contained in:
Zacqary Adam Xeper 2019-10-15 12:39:37 -05:00 committed by GitHub
parent e17c19e7bd
commit 96f84aef78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 2303 additions and 2628 deletions

View file

@ -6,7 +6,8 @@
import React from 'react';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { Breadcrumb } from 'ui/chrome/api/breadcrumbs';
import { WithKibanaChrome } from '../../containers/with_kibana_chrome';
import { ExternalHeader } from './external_header';
@ -14,34 +15,29 @@ import { ExternalHeader } from './external_header';
interface HeaderProps {
breadcrumbs?: Breadcrumb[];
readOnlyBadge?: boolean;
intl: InjectedIntl;
}
export const Header = injectI18n(
({ breadcrumbs = [], readOnlyBadge = false, intl }: HeaderProps) => (
<WithKibanaChrome>
{({ setBreadcrumbs, setBadge }) => (
<ExternalHeader
breadcrumbs={breadcrumbs}
setBreadcrumbs={setBreadcrumbs}
badge={
readOnlyBadge
? {
text: intl.formatMessage({
defaultMessage: 'Read only',
id: 'xpack.infra.header.badge.readOnly.text',
}),
tooltip: intl.formatMessage({
defaultMessage: 'Unable to change source configuration',
id: 'xpack.infra.header.badge.readOnly.tooltip',
}),
iconType: 'glasses',
}
: undefined
}
setBadge={setBadge}
/>
)}
</WithKibanaChrome>
)
export const Header = ({ breadcrumbs = [], readOnlyBadge = false }: HeaderProps) => (
<WithKibanaChrome>
{({ setBreadcrumbs, setBadge }) => (
<ExternalHeader
breadcrumbs={breadcrumbs}
setBreadcrumbs={setBreadcrumbs}
badge={
readOnlyBadge
? {
text: i18n.translate('xpack.infra.header.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n.translate('xpack.infra.header.badge.readOnly.tooltip', {
defaultMessage: 'Unable to change source configuration',
}),
iconType: 'glasses',
}
: undefined
}
setBadge={setBadge}
/>
)}
</WithKibanaChrome>
);

View file

@ -8,13 +8,13 @@ import testSubject from '@kbn/test-subj-selector';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithIntl } from '../../../utils/enzyme_helpers';
import { mount } from 'enzyme';
import { LogEntryActionsMenu } from './log_entry_actions_menu';
describe('LogEntryActionsMenu component', () => {
describe('uptime link', () => {
it('renders with a host ip filter when present in log entry', () => {
const elementWrapper = mountWithIntl(
const elementWrapper = mount(
<LogEntryActionsMenu
logItem={{
fields: [{ field: 'host.ip', value: 'HOST_IP' }],
@ -42,7 +42,7 @@ describe('LogEntryActionsMenu component', () => {
});
it('renders with a container id filter when present in log entry', () => {
const elementWrapper = mountWithIntl(
const elementWrapper = mount(
<LogEntryActionsMenu
logItem={{
fields: [{ field: 'container.id', value: 'CONTAINER_ID' }],
@ -70,7 +70,7 @@ describe('LogEntryActionsMenu component', () => {
});
it('renders with a pod uid filter when present in log entry', () => {
const elementWrapper = mountWithIntl(
const elementWrapper = mount(
<LogEntryActionsMenu
logItem={{
fields: [{ field: 'kubernetes.pod.uid', value: 'POD_UID' }],
@ -98,7 +98,7 @@ describe('LogEntryActionsMenu component', () => {
});
it('renders with a disjunction of filters when multiple present in log entry', () => {
const elementWrapper = mountWithIntl(
const elementWrapper = mount(
<LogEntryActionsMenu
logItem={{
fields: [
@ -132,7 +132,7 @@ describe('LogEntryActionsMenu component', () => {
});
it('renders as disabled when no supported field is present in log entry', () => {
const elementWrapper = mountWithIntl(
const elementWrapper = mount(
<LogEntryActionsMenu
logItem={{
fields: [],
@ -164,7 +164,7 @@ describe('LogEntryActionsMenu component', () => {
describe('apm link', () => {
it('renders with a trace id filter when present in log entry', () => {
const elementWrapper = mountWithIntl(
const elementWrapper = mount(
<LogEntryActionsMenu
logItem={{
fields: [{ field: 'trace.id', value: '1234567' }],
@ -197,7 +197,7 @@ describe('LogEntryActionsMenu component', () => {
it('renders with a trace id filter and timestamp when present in log entry', () => {
const timestamp = '2019-06-27T17:44:08.693Z';
const elementWrapper = mountWithIntl(
const elementWrapper = mount(
<LogEntryActionsMenu
logItem={{
fields: [
@ -232,7 +232,7 @@ describe('LogEntryActionsMenu component', () => {
});
it('renders as disabled when no supported field is present in log entry', () => {
const elementWrapper = mountWithIntl(
const elementWrapper = mount(
<LogEntryActionsMenu
logItem={{
fields: [],

View file

@ -15,7 +15,8 @@ import {
EuiTitle,
EuiToolTip,
} from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
import React, { useCallback, useMemo } from 'react';
@ -30,117 +31,116 @@ interface Props {
setFlyoutVisibility: (visible: boolean) => void;
setFilter: (filter: string) => void;
setTarget: (timeKey: TimeKey, flyoutItemId: string) => void;
intl: InjectedIntl;
loading: boolean;
}
export const LogEntryFlyout = injectI18n(
({ flyoutItem, loading, setFlyoutVisibility, setFilter, setTarget, intl }: Props) => {
const createFilterHandler = useCallback(
(field: InfraLogItemField) => () => {
const filter = `${field.field}:"${field.value}"`;
setFilter(filter);
export const LogEntryFlyout = ({
flyoutItem,
loading,
setFlyoutVisibility,
setFilter,
setTarget,
}: Props) => {
const createFilterHandler = useCallback(
(field: InfraLogItemField) => () => {
const filter = `${field.field}:"${field.value}"`;
setFilter(filter);
if (flyoutItem && flyoutItem.key) {
const timestampMoment = moment(flyoutItem.key.time);
if (timestampMoment.isValid()) {
setTarget(
{
time: timestampMoment.valueOf(),
tiebreaker: flyoutItem.key.tiebreaker,
},
flyoutItem.id
);
}
if (flyoutItem && flyoutItem.key) {
const timestampMoment = moment(flyoutItem.key.time);
if (timestampMoment.isValid()) {
setTarget(
{
time: timestampMoment.valueOf(),
tiebreaker: flyoutItem.key.tiebreaker,
},
flyoutItem.id
);
}
}
},
[flyoutItem, setFilter, setTarget]
);
const closeFlyout = useCallback(() => setFlyoutVisibility(false), [setFlyoutVisibility]);
const columns = useMemo(
() => [
{
field: 'field',
name: i18n.translate('xpack.infra.logFlyout.fieldColumnLabel', {
defaultMessage: 'Field',
}),
sortable: true,
},
[flyoutItem, setFilter, setTarget]
);
const closeFlyout = useCallback(() => setFlyoutVisibility(false), [setFlyoutVisibility]);
const columns = useMemo(
() => [
{
field: 'field',
name: intl.formatMessage({
defaultMessage: 'Field',
id: 'xpack.infra.logFlyout.fieldColumnLabel',
}),
sortable: true,
},
{
field: 'value',
name: intl.formatMessage({
defaultMessage: 'Value',
id: 'xpack.infra.logFlyout.valueColumnLabel',
}),
sortable: true,
render: (_name: string, item: InfraLogItemField) => (
<span>
<EuiToolTip
content={intl.formatMessage({
id: 'xpack.infra.logFlyout.setFilterTooltip',
defaultMessage: 'View event with filter',
})}
>
<EuiButtonIcon
color="text"
iconType="filter"
aria-label={intl.formatMessage({
id: 'xpack.infra.logFlyout.filterAriaLabel',
defaultMessage: 'Filter',
})}
onClick={createFilterHandler(item)}
/>
</EuiToolTip>
{item.value}
</span>
),
},
],
[createFilterHandler, intl.formatMessage]
);
return (
<EuiFlyout onClose={closeFlyout} size="m">
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle size="s">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="Log event document details"
id="xpack.infra.logFlyout.flyoutTitle"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{flyoutItem !== null ? <LogEntryActionsMenu logItem={flyoutItem} /> : null}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{loading || flyoutItem === null ? (
<InfraFlyoutLoadingPanel>
<InfraLoadingPanel
height="100%"
width="100%"
text={intl.formatMessage({
id: 'xpack.infra.logFlyout.loadingMessage',
defaultMessage: 'Loading Event',
{
field: 'value',
name: i18n.translate('xpack.infra.logFlyout.valueColumnLabel', {
defaultMessage: 'Value',
}),
sortable: true,
render: (_name: string, item: InfraLogItemField) => (
<span>
<EuiToolTip
content={i18n.translate('xpack.infra.logFlyout.setFilterTooltip', {
defaultMessage: 'View event with filter',
})}
>
<EuiButtonIcon
color="text"
iconType="filter"
aria-label={i18n.translate('xpack.infra.logFlyout.filterAriaLabel', {
defaultMessage: 'Filter',
})}
onClick={createFilterHandler(item)}
/>
</InfraFlyoutLoadingPanel>
) : (
<EuiBasicTable columns={columns} items={flyoutItem.fields} />
)}
</EuiFlyoutBody>
</EuiFlyout>
);
}
);
</EuiToolTip>
{item.value}
</span>
),
},
],
[createFilterHandler]
);
return (
<EuiFlyout onClose={closeFlyout} size="m">
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle size="s">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="Log event document details"
id="xpack.infra.logFlyout.flyoutTitle"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{flyoutItem !== null ? <LogEntryActionsMenu logItem={flyoutItem} /> : null}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{loading || flyoutItem === null ? (
<InfraFlyoutLoadingPanel>
<InfraLoadingPanel
height="100%"
width="100%"
text={i18n.translate('xpack.infra.logFlyout.loadingMessage', {
defaultMessage: 'Loading Event',
})}
/>
</InfraFlyoutLoadingPanel>
) : (
<EuiBasicTable columns={columns} items={flyoutItem.fields} />
)}
</EuiFlyoutBody>
</EuiFlyout>
);
};
export const InfraFlyoutLoadingPanel = euiStyled.div`
position: absolute;

View file

@ -5,7 +5,8 @@
*/
import { EuiFieldSearch } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import classNames from 'classnames';
import * as React from 'react';
@ -16,66 +17,64 @@ interface LogSearchInputProps {
isLoading: boolean;
onSearch: (query: string) => void;
onClear: () => void;
intl: InjectedIntl;
}
interface LogSearchInputState {
query: string;
}
export const LogSearchInput = injectI18n(
class extends React.PureComponent<LogSearchInputProps, LogSearchInputState> {
public static displayName = 'LogSearchInput';
public readonly state = {
query: '',
};
export const LogSearchInput = class extends React.PureComponent<
LogSearchInputProps,
LogSearchInputState
> {
public static displayName = 'LogSearchInput';
public readonly state = {
query: '',
};
public handleSubmit: React.FormEventHandler<HTMLFormElement> = evt => {
evt.preventDefault();
public handleSubmit: React.FormEventHandler<HTMLFormElement> = evt => {
evt.preventDefault();
const { query } = this.state;
const { query } = this.state;
if (query === '') {
this.props.onClear();
} else {
this.props.onSearch(this.state.query);
}
};
public handleChangeQuery: React.ChangeEventHandler<HTMLInputElement> = evt => {
this.setState({
query: evt.target.value,
});
};
public render() {
const { className, isLoading, intl } = this.props;
const { query } = this.state;
const classes = classNames('loggingSearchInput', className);
return (
<form onSubmit={this.handleSubmit}>
<PlainSearchField
aria-label={intl.formatMessage({
id: 'xpack.infra.logs.search.searchInLogsAriaLabel',
defaultMessage: 'search',
})}
className={classes}
fullWidth
isLoading={isLoading}
onChange={this.handleChangeQuery}
placeholder={intl.formatMessage({
id: 'xpack.infra.logs.search.searchInLogsPlaceholder',
defaultMessage: 'Search',
})}
value={query}
/>
</form>
);
if (query === '') {
this.props.onClear();
} else {
this.props.onSearch(this.state.query);
}
};
public handleChangeQuery: React.ChangeEventHandler<HTMLInputElement> = evt => {
this.setState({
query: evt.target.value,
});
};
public render() {
const { className, isLoading } = this.props;
const { query } = this.state;
const classes = classNames('loggingSearchInput', className);
return (
<form onSubmit={this.handleSubmit}>
<PlainSearchField
aria-label={i18n.translate('xpack.infra.logs.search.searchInLogsAriaLabel', {
defaultMessage: 'search',
})}
className={classes}
fullWidth
isLoading={isLoading}
onChange={this.handleChangeQuery}
placeholder={i18n.translate('xpack.infra.logs.search.searchInLogsPlaceholder', {
defaultMessage: 'Search',
})}
value={query}
/>
</form>
);
}
);
};
const PlainSearchField = euiStyled(EuiFieldSearch)`
background: transparent;

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { injectI18n } from '@kbn/i18n/react';
import React from 'react';
import euiStyled from '../../../../../../common/eui_styled_components';
@ -22,10 +21,10 @@ import {
} from './log_entry_column';
import { ASSUMED_SCROLLBAR_WIDTH } from './vertical_scroll_panel';
export const LogColumnHeaders = injectI18n<{
export const LogColumnHeaders: React.FunctionComponent<{
columnConfigurations: LogColumnConfiguration[];
columnWidths: LogEntryColumnWidths;
}>(({ columnConfigurations, columnWidths, intl }) => {
}> = ({ columnConfigurations, columnWidths }) => {
return (
<LogColumnHeadersWrapper>
{columnConfigurations.map(columnConfiguration => {
@ -63,7 +62,7 @@ export const LogColumnHeaders = injectI18n<{
})}
</LogColumnHeadersWrapper>
);
});
};
const LogColumnHeader: React.FunctionComponent<{
columnWidth: LogEntryColumnWidth;

View file

@ -5,7 +5,7 @@
*/
import { EuiButtonIcon } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { LogEntryColumnContent } from './log_entry_column';
@ -29,13 +29,12 @@ export const LogEntryIconColumn: React.FunctionComponent<LogEntryIconColumnProps
);
};
export const LogEntryDetailsIconColumn = injectI18n<
export const LogEntryDetailsIconColumn: React.FunctionComponent<
LogEntryIconColumnProps & {
openFlyout: () => void;
}
>(({ intl, isHighlighted, isHovered, openFlyout }) => {
const label = intl.formatMessage({
id: 'xpack.infra.logEntryItemView.viewDetailsToolTip',
> = ({ isHighlighted, isHovered, openFlyout }) => {
const label = i18n.translate('xpack.infra.logEntryItemView.viewDetailsToolTip', {
defaultMessage: 'View Details',
});
@ -48,7 +47,7 @@ export const LogEntryDetailsIconColumn = injectI18n<
) : null}
</LogEntryIconColumn>
);
});
};
const IconColumnContent = LogEntryColumnContent.extend.attrs<{
isHighlighted: boolean;

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useMemo } from 'react';
import euiStyled from '../../../../../../common/eui_styled_components';
@ -50,7 +51,6 @@ interface ScrollableLogTextStreamViewProps {
loadNewerItems: () => void;
setFlyoutItem: (id: string) => void;
setFlyoutVisibility: (visible: boolean) => void;
intl: InjectedIntl;
highlightedItem: string | null;
currentHighlightKey: UniqueTimeKey | null;
scrollLock: {
@ -66,7 +66,7 @@ interface ScrollableLogTextStreamViewState {
items: StreamItem[];
}
class ScrollableLogTextStreamViewClass extends React.PureComponent<
export class ScrollableLogTextStreamView extends React.PureComponent<
ScrollableLogTextStreamViewProps,
ScrollableLogTextStreamViewState
> {
@ -121,7 +121,6 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
hasMoreAfterEnd,
hasMoreBeforeStart,
highlightedItem,
intl,
isLoadingMore,
isReloading,
isStreaming,
@ -147,16 +146,13 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
/>
) : !hasItems ? (
<NoData
titleText={intl.formatMessage({
id: 'xpack.infra.logs.emptyView.noLogMessageTitle',
titleText={i18n.translate('xpack.infra.logs.emptyView.noLogMessageTitle', {
defaultMessage: 'There are no log messages to display.',
})}
bodyText={intl.formatMessage({
id: 'xpack.infra.logs.emptyView.noLogMessageDescription',
bodyText={i18n.translate('xpack.infra.logs.emptyView.noLogMessageDescription', {
defaultMessage: 'Try adjusting your filter.',
})}
refetchText={intl.formatMessage({
id: 'xpack.infra.logs.emptyView.checkForNewDataButtonLabel',
refetchText={i18n.translate('xpack.infra.logs.emptyView.checkForNewDataButtonLabel', {
defaultMessage: 'Check for new data',
})}
onRefetch={this.handleReload}
@ -312,8 +308,6 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
};
}
export const ScrollableLogTextStreamView = injectI18n(ScrollableLogTextStreamViewClass);
/**
* This function-as-child component calculates the column widths based on the
* given configuration. It depends on the `CharacterDimensionsProbe` it returns

View file

@ -5,7 +5,8 @@
*/
import { EuiDatePicker, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import moment, { Moment } from 'moment';
import React from 'react';
@ -17,12 +18,11 @@ interface LogTimeControlsProps {
stopLiveStreaming: () => any;
isLiveStreaming: boolean;
jumpToTime: (time: number) => any;
intl: InjectedIntl;
}
class LogTimeControlsUI extends React.PureComponent<LogTimeControlsProps> {
export class LogTimeControls extends React.PureComponent<LogTimeControlsProps> {
public render() {
const { currentTime, isLiveStreaming, intl } = this.props;
const { currentTime, isLiveStreaming } = this.props;
const currentMoment = currentTime ? moment(currentTime) : null;
if (isLiveStreaming) {
@ -32,8 +32,7 @@ class LogTimeControlsUI extends React.PureComponent<LogTimeControlsProps> {
<EuiDatePicker
disabled
onChange={noop}
value={intl.formatMessage({
id: 'xpack.infra.logs.streamingDescription',
value={i18n.translate('xpack.infra.logs.streamingDescription', {
defaultMessage: 'Streaming new entries…',
})}
/>
@ -95,5 +94,3 @@ class LogTimeControlsUI extends React.PureComponent<LogTimeControlsProps> {
this.props.stopLiveStreaming();
};
}
export const LogTimeControls = injectI18n(LogTimeControlsUI);

View file

@ -5,7 +5,8 @@
*/
import { EuiPageContentBody, EuiTitle } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { InfraMetricData } from '../../graphql/types';
@ -25,109 +26,100 @@ interface Props {
onChangeRangeTime?: (time: MetricsTimeInput) => void;
isLiveStreaming?: boolean;
stopLiveStreaming?: () => void;
intl: InjectedIntl;
}
interface State {
crosshairValue: number | null;
}
export const Metrics = injectI18n(
class extends React.PureComponent<Props, State> {
public static displayName = 'Metrics';
public readonly state = {
crosshairValue: null,
};
export const Metrics = class extends React.PureComponent<Props, State> {
public static displayName = 'Metrics';
public readonly state = {
crosshairValue: null,
};
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',
})}
/>
);
} else if (!this.props.loading && this.props.metrics && this.props.metrics.length === 0) {
return (
<NoData
titleText={intl.formatMessage({
id: 'xpack.infra.metrics.emptyViewTitle',
defaultMessage: 'There is no data to display.',
})}
bodyText={intl.formatMessage({
id: 'xpack.infra.metrics.emptyViewDescription',
defaultMessage: 'Try adjusting your time or filter.',
})}
refetchText={intl.formatMessage({
id: 'xpack.infra.metrics.refetchButtonLabel',
defaultMessage: 'Check for new data',
})}
onRefetch={this.handleRefetch}
testString="metricsEmptyViewState"
/>
);
}
return <React.Fragment>{this.props.layouts.map(this.renderLayout)}</React.Fragment>;
}
private handleRefetch = () => {
this.props.refetch();
};
private renderLayout = (layout: InfraMetricLayout) => {
public render() {
if (this.props.loading) {
return (
<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, isLiveStreaming, stopLiveStreaming } = this.props;
sectionProps = {
onChangeRangeTime,
isLiveStreaming,
stopLiveStreaming,
crosshairValue: this.state.crosshairValue,
onCrosshairUpdate: this.onCrosshairUpdate,
};
}
return (
<Section
section={section}
metrics={this.props.metrics}
key={`${layout.id}-${section.id}`}
{...sectionProps}
<InfraLoadingPanel
height="100vh"
width="auto"
text={i18n.translate('xpack.infra.metrics.loadingNodeDataText', {
defaultMessage: 'Loading data',
})}
/>
);
};
} else if (!this.props.loading && this.props.metrics && this.props.metrics.length === 0) {
return (
<NoData
titleText={i18n.translate('xpack.infra.metrics.emptyViewTitle', {
defaultMessage: 'There is no data to display.',
})}
bodyText={i18n.translate('xpack.infra.metrics.emptyViewDescription', {
defaultMessage: 'Try adjusting your time or filter.',
})}
refetchText={i18n.translate('xpack.infra.metrics.refetchButtonLabel', {
defaultMessage: 'Check for new data',
})}
onRefetch={this.handleRefetch}
testString="metricsEmptyViewState"
/>
);
}
private onCrosshairUpdate = (crosshairValue: number) => {
this.setState({
crosshairValue,
});
};
return <React.Fragment>{this.props.layouts.map(this.renderLayout)}</React.Fragment>;
}
);
private handleRefetch = () => {
this.props.refetch();
};
private renderLayout = (layout: InfraMetricLayout) => {
return (
<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, isLiveStreaming, stopLiveStreaming } = this.props;
sectionProps = {
onChangeRangeTime,
isLiveStreaming,
stopLiveStreaming,
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

@ -5,7 +5,7 @@
*/
import { EuiHideFor, EuiPageSideBar, EuiShowFor, EuiSideNav } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import euiStyled from '../../../../../common/eui_styled_components';
@ -16,59 +16,56 @@ interface Props {
loading: boolean;
nodeName: string;
handleClick: (section: InfraMetricLayoutSection) => () => void;
intl: InjectedIntl;
}
export const MetricsSideNav = injectI18n(
class extends React.PureComponent<Props> {
public static displayName = 'MetricsSideNav';
export const MetricsSideNav = class extends React.PureComponent<Props> {
public static displayName = 'MetricsSideNav';
public readonly state = {
isOpenOnMobile: false,
};
public readonly state = {
isOpenOnMobile: false,
};
public render() {
let content;
let mobileContent;
if (!this.props.loading) {
const entries = this.props.layouts.map(item => {
return {
name: item.label,
id: item.id,
items: item.sections.map(section => ({
id: section.id,
name: section.label,
onClick: this.props.handleClick(section),
})),
};
});
content = <EuiSideNav items={entries} />;
mobileContent = (
<EuiSideNav
items={entries}
mobileTitle={this.props.nodeName}
toggleOpenOnMobile={this.toggleOpenOnMobile}
isOpenOnMobile={this.state.isOpenOnMobile}
/>
);
}
return (
<EuiPageSideBar>
<EuiHideFor sizes={['xs', 's']}>
<SideNavContainer>{content}</SideNavContainer>
</EuiHideFor>
<EuiShowFor sizes={['xs', 's']}>{mobileContent}</EuiShowFor>
</EuiPageSideBar>
public render() {
let content;
let mobileContent;
if (!this.props.loading) {
const entries = this.props.layouts.map(item => {
return {
name: item.label,
id: item.id,
items: item.sections.map(section => ({
id: section.id,
name: section.label,
onClick: this.props.handleClick(section),
})),
};
});
content = <EuiSideNav items={entries} />;
mobileContent = (
<EuiSideNav
items={entries}
mobileTitle={this.props.nodeName}
toggleOpenOnMobile={this.toggleOpenOnMobile}
isOpenOnMobile={this.state.isOpenOnMobile}
/>
);
}
private toggleOpenOnMobile = () => {
this.setState({
isOpenOnMobile: !this.state.isOpenOnMobile,
});
};
return (
<EuiPageSideBar>
<EuiHideFor sizes={['xs', 's']}>
<SideNavContainer>{content}</SideNavContainer>
</EuiHideFor>
<EuiShowFor sizes={['xs', 's']}>{mobileContent}</EuiShowFor>
</EuiPageSideBar>
);
}
);
private toggleOpenOnMobile = () => {
this.setState({
isOpenOnMobile: !this.state.isOpenOnMobile,
});
};
};
const SideNavContainer = euiStyled.div`
position: fixed;

View file

@ -5,13 +5,13 @@
*/
import { EuiSelect } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React, { useCallback } from 'react';
import { MetricsExplorerAggregation } from '../../../server/routes/metrics_explorer/types';
import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options';
interface Props {
intl: InjectedIntl;
options: MetricsExplorerOptions;
fullWidth: boolean;
onChange: (aggregation: MetricsExplorerAggregation) => void;
@ -21,32 +21,32 @@ const isMetricsExplorerAggregation = (subject: any): subject is MetricsExplorerA
return Object.keys(MetricsExplorerAggregation).includes(subject);
};
export const MetricsExplorerAggregationPicker = injectI18n(({ intl, options, onChange }: Props) => {
export const MetricsExplorerAggregationPicker = ({ options, onChange }: Props) => {
const AGGREGATION_LABELS = {
[MetricsExplorerAggregation.avg]: intl.formatMessage({
id: 'xpack.infra.metricsExplorer.aggregationLables.avg',
defaultMessage: 'Average',
}),
[MetricsExplorerAggregation.max]: intl.formatMessage({
id: 'xpack.infra.metricsExplorer.aggregationLables.max',
defaultMessage: 'Max',
}),
[MetricsExplorerAggregation.min]: intl.formatMessage({
id: 'xpack.infra.metricsExplorer.aggregationLables.min',
defaultMessage: 'Min',
}),
[MetricsExplorerAggregation.cardinality]: intl.formatMessage({
id: 'xpack.infra.metricsExplorer.aggregationLables.cardinality',
defaultMessage: 'Cardinality',
}),
[MetricsExplorerAggregation.rate]: intl.formatMessage({
id: 'xpack.infra.metricsExplorer.aggregationLables.rate',
defaultMessage: 'Rate',
}),
[MetricsExplorerAggregation.count]: intl.formatMessage({
id: 'xpack.infra.metricsExplorer.aggregationLables.count',
defaultMessage: 'Document count',
}),
[MetricsExplorerAggregation.avg]: i18n.translate(
'xpack.infra.metricsExplorer.aggregationLables.avg',
{ defaultMessage: 'Average' }
),
[MetricsExplorerAggregation.max]: i18n.translate(
'xpack.infra.metricsExplorer.aggregationLables.max',
{ defaultMessage: 'Max' }
),
[MetricsExplorerAggregation.min]: i18n.translate(
'xpack.infra.metricsExplorer.aggregationLables.min',
{ defaultMessage: 'Min' }
),
[MetricsExplorerAggregation.cardinality]: i18n.translate(
'xpack.infra.metricsExplorer.aggregationLables.cardinality',
{ defaultMessage: 'Cardinality' }
),
[MetricsExplorerAggregation.rate]: i18n.translate(
'xpack.infra.metricsExplorer.aggregationLables.rate',
{ defaultMessage: 'Rate' }
),
[MetricsExplorerAggregation.count]: i18n.translate(
'xpack.infra.metricsExplorer.aggregationLables.count',
{ defaultMessage: 'Document count' }
),
};
const handleChange = useCallback(
@ -61,8 +61,7 @@ export const MetricsExplorerAggregationPicker = injectI18n(({ intl, options, onC
return (
<EuiSelect
placeholder={intl.formatMessage({
id: 'xpack.infra.metricsExplorer.aggregationSelectLabel',
placeholder={i18n.translate('xpack.infra.metricsExplorer.aggregationSelectLabel', {
defaultMessage: 'Select an aggregation',
})}
fullWidth
@ -74,4 +73,4 @@ export const MetricsExplorerAggregationPicker = injectI18n(({ intl, options, onC
onChange={handleChange}
/>
);
});
};

View file

@ -6,7 +6,7 @@
import React from 'react';
import { MetricsExplorerChartContextMenu, createNodeDetailLink } from './chart_context_menu';
import { mountWithIntl } from '../../utils/enzyme_helpers';
import { mount } from 'enzyme';
import { options, source, timeRange, chartOptions } from '../../utils/fixtures/metrics_explorer';
import { UICapabilities } from 'ui/capabilities';
import { InfraNodeType } from '../../graphql/types';
@ -29,7 +29,7 @@ describe('MetricsExplorerChartContextMenu', () => {
describe('component', () => {
it('should just work', async () => {
const onFilter = jest.fn().mockImplementation((query: string) => void 0);
const component = mountWithIntl(
const component = mount(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
@ -50,7 +50,7 @@ describe('MetricsExplorerChartContextMenu', () => {
it('should not display View metrics for incompatible groupBy', async () => {
const customOptions = { ...options, groupBy: 'system.network.name' };
const onFilter = jest.fn().mockImplementation((query: string) => void 0);
const component = mountWithIntl(
const component = mount(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
@ -66,7 +66,7 @@ describe('MetricsExplorerChartContextMenu', () => {
});
it('should not display "Add Filter" without onFilter', async () => {
const component = mountWithIntl(
const component = mount(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
@ -84,7 +84,7 @@ describe('MetricsExplorerChartContextMenu', () => {
it('should not display "Add Filter" without options.groupBy', async () => {
const customOptions = { ...options, groupBy: void 0 };
const onFilter = jest.fn().mockImplementation((query: string) => void 0);
const component = mountWithIntl(
const component = mount(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
@ -102,7 +102,7 @@ describe('MetricsExplorerChartContextMenu', () => {
it('should disable "Open in Visualize" when options.metrics is empty', async () => {
const customOptions = { ...options, metrics: [] };
const component = mountWithIntl(
const component = mount(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
@ -122,7 +122,7 @@ describe('MetricsExplorerChartContextMenu', () => {
it('should not display "Open in Visualize" when unavailble in uiCapabilities', async () => {
const customUICapabilities = { ...uiCapabilities, visualize: { show: false } };
const onFilter = jest.fn().mockImplementation((query: string) => void 0);
const component = mountWithIntl(
const component = mount(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
@ -142,7 +142,7 @@ describe('MetricsExplorerChartContextMenu', () => {
const customUICapabilities = { ...uiCapabilities, visualize: { show: false } };
const onFilter = jest.fn().mockImplementation((query: string) => void 0);
const customOptions = { ...options, groupBy: void 0 };
const component = mountWithIntl(
const component = mount(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useCallback, useState } from 'react';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonEmpty,
EuiContextMenu,
@ -25,7 +26,6 @@ import { getNodeDetailUrl } from '../../pages/link_to/redirect_to_node_detail';
import { SourceConfiguration } from '../../utils/source_configuration';
interface Props {
intl: InjectedIntl;
options: MetricsExplorerOptions;
onFilter?: (query: string) => void;
series: MetricsExplorerSeries;
@ -67,116 +67,113 @@ export const createNodeDetailLink = (
});
};
export const MetricsExplorerChartContextMenu = injectI18n(
({ intl, onFilter, options, series, source, timeRange, uiCapabilities, chartOptions }: Props) => {
const [isPopoverOpen, setPopoverState] = useState(false);
const supportFiltering = options.groupBy != null && onFilter != null;
const handleFilter = useCallback(() => {
// onFilter needs check for Typescript even though it's
// covered by supportFiltering variable
if (supportFiltering && onFilter) {
onFilter(`${options.groupBy}: "${series.id}"`);
}
setPopoverState(false);
}, [supportFiltering, options.groupBy, series.id, onFilter]);
export const MetricsExplorerChartContextMenu = ({
onFilter,
options,
series,
source,
timeRange,
uiCapabilities,
chartOptions,
}: Props) => {
const [isPopoverOpen, setPopoverState] = useState(false);
const supportFiltering = options.groupBy != null && onFilter != null;
const handleFilter = useCallback(() => {
// onFilter needs check for Typescript even though it's
// covered by supportFiltering variable
if (supportFiltering && onFilter) {
onFilter(`${options.groupBy}: "${series.id}"`);
}
setPopoverState(false);
}, [supportFiltering, options.groupBy, series.id, onFilter]);
const tsvbUrl = createTSVBLink(source, options, series, timeRange, chartOptions);
const tsvbUrl = createTSVBLink(source, options, series, timeRange, chartOptions);
// Only display the "Add Filter" option if it's supported
const filterByItem = supportFiltering
? [
{
name: intl.formatMessage({
id: 'xpack.infra.metricsExplorer.filterByLabel',
defaultMessage: 'Add filter',
}),
icon: 'infraApp',
onClick: handleFilter,
'data-test-subj': 'metricsExplorerAction-AddFilter',
},
]
: [];
// Only display the "Add Filter" option if it's supported
const filterByItem = supportFiltering
? [
{
name: i18n.translate('xpack.infra.metricsExplorer.filterByLabel', {
defaultMessage: 'Add filter',
}),
icon: 'infraApp',
onClick: handleFilter,
'data-test-subj': 'metricsExplorerAction-AddFilter',
},
]
: [];
const nodeType = source && options.groupBy && fieldToNodeType(source, options.groupBy);
const viewNodeDetail = nodeType
? [
{
name: intl.formatMessage(
{
id: 'xpack.infra.metricsExplorer.viewNodeDetail',
defaultMessage: 'View metrics for {name}',
},
{ name: nodeType }
),
icon: 'infraApp',
href: createNodeDetailLink(nodeType, series.id, timeRange.from, timeRange.to),
'data-test-subj': 'metricsExplorerAction-ViewNodeMetrics',
},
]
: [];
const nodeType = source && options.groupBy && fieldToNodeType(source, options.groupBy);
const viewNodeDetail = nodeType
? [
{
name: i18n.translate('xpack.infra.metricsExplorer.viewNodeDetail', {
defaultMessage: 'View metrics for {name}',
values: { name: nodeType },
}),
icon: 'infraApp',
href: createNodeDetailLink(nodeType, series.id, timeRange.from, timeRange.to),
'data-test-subj': 'metricsExplorerAction-ViewNodeMetrics',
},
]
: [];
const openInVisualize = uiCapabilities.visualize.show
? [
{
name: intl.formatMessage({
id: 'xpack.infra.metricsExplorer.openInTSVB',
defaultMessage: 'Open in Visualize',
}),
href: tsvbUrl,
icon: 'visualizeApp',
disabled: options.metrics.length === 0,
'data-test-subj': 'metricsExplorerAction-OpenInTSVB',
},
]
: [];
const openInVisualize = uiCapabilities.visualize.show
? [
{
name: i18n.translate('xpack.infra.metricsExplorer.openInTSVB', {
defaultMessage: 'Open in Visualize',
}),
href: tsvbUrl,
icon: 'visualizeApp',
disabled: options.metrics.length === 0,
'data-test-subj': 'metricsExplorerAction-OpenInTSVB',
},
]
: [];
const itemPanels = [...filterByItem, ...openInVisualize, ...viewNodeDetail];
const itemPanels = [...filterByItem, ...openInVisualize, ...viewNodeDetail];
// If there are no itemPanels then there is no reason to show the actions button.
if (itemPanels.length === 0) return null;
// If there are no itemPanels then there is no reason to show the actions button.
if (itemPanels.length === 0) return null;
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: 'Actions',
items: itemPanels,
},
];
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: 'Actions',
items: itemPanels,
},
];
const handleClose = () => setPopoverState(false);
const handleOpen = () => setPopoverState(true);
const actionAriaLabel = intl.formatMessage(
{
id: 'xpack.infra.metricsExplorer.actionsLabel.aria',
defaultMessage: 'Actions for {grouping}',
},
{ grouping: series.id }
);
const actionLabel = intl.formatMessage({
id: 'xpack.infra.metricsExplorer.actionsLabel.button',
defaultMessage: 'Actions',
});
const button = (
<EuiButtonEmpty
contentProps={{ 'aria-label': actionAriaLabel }}
onClick={handleOpen}
size="s"
iconType="arrowDown"
iconSide="right"
>
{actionLabel}
</EuiButtonEmpty>
);
return (
<EuiPopover
closePopover={handleClose}
id={`${series.id}-popover`}
button={button}
isOpen={isPopoverOpen}
panelPaddingSize="none"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
}
);
const handleClose = () => setPopoverState(false);
const handleOpen = () => setPopoverState(true);
const actionAriaLabel = i18n.translate('xpack.infra.metricsExplorer.actionsLabel.aria', {
defaultMessage: 'Actions for {grouping}',
values: { grouping: series.id },
});
const actionLabel = i18n.translate('xpack.infra.metricsExplorer.actionsLabel.button', {
defaultMessage: 'Actions',
});
const button = (
<EuiButtonEmpty
contentProps={{ 'aria-label': actionAriaLabel }}
onClick={handleOpen}
size="s"
iconType="arrowDown"
iconSide="right"
>
{actionLabel}
</EuiButtonEmpty>
);
return (
<EuiPopover
closePopover={handleClose}
id={`${series.id}-popover`}
button={button}
isOpen={isPopoverOpen}
panelPaddingSize="none"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
};

View file

@ -5,7 +5,8 @@
*/
import { EuiButton, EuiFlexGrid, EuiFlexItem, EuiText, EuiHorizontalRule } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { MetricsExplorerResponse } from '../../../server/routes/metrics_explorer/types';
import {
@ -27,111 +28,104 @@ interface Props {
onFilter: (filter: string) => void;
onTimeChange: (start: string, end: string) => void;
data: MetricsExplorerResponse | null;
intl: InjectedIntl;
source: SourceQuery.Query['source']['configuration'] | undefined;
timeRange: MetricsExplorerTimeOptions;
}
export const MetricsExplorerCharts = injectI18n(
({
loading,
data,
onLoadMore,
options,
chartOptions,
onRefetch,
intl,
onFilter,
source,
timeRange,
onTimeChange,
}: Props) => {
if (!data && loading) {
return (
<InfraLoadingPanel
height={800}
width="100%"
text={intl.formatMessage({
defaultMessage: 'Loading charts',
id: 'xpack.infra.metricsExplorer.loadingCharts',
})}
/>
);
}
if (!data || data.series.length === 0) {
return (
<NoData
titleText={intl.formatMessage({
id: 'xpack.infra.metricsExplorer.noDataTitle',
defaultMessage: 'There is no data to display.',
})}
bodyText={intl.formatMessage({
id: 'xpack.infra.metricsExplorer.noDataBodyText',
defaultMessage: 'Try adjusting your time, filters or group by settings.',
})}
refetchText={intl.formatMessage({
id: 'xpack.infra.metricsExplorer.noDataRefetchText',
defaultMessage: 'Check for new data',
})}
testString="metrics-explorer-no-data"
onRefetch={onRefetch}
/>
);
}
export const MetricsExplorerCharts = ({
loading,
data,
onLoadMore,
options,
chartOptions,
onRefetch,
onFilter,
source,
timeRange,
onTimeChange,
}: Props) => {
if (!data && loading) {
return (
<div style={{ width: '100%' }}>
<EuiFlexGrid gutterSize="s" columns={data.series.length === 1 ? 1 : 3}>
{data.series.map(series => (
<EuiFlexItem key={series.id} style={{ minWidth: 0 }}>
<MetricsExplorerChart
key={`chart-${series.id}`}
onFilter={onFilter}
options={options}
chartOptions={chartOptions}
title={options.groupBy ? series.id : null}
height={data.series.length > 1 ? 200 : 400}
series={series}
source={source}
timeRange={timeRange}
onTimeChange={onTimeChange}
/>
</EuiFlexItem>
))}
</EuiFlexGrid>
{data.series.length > 1 ? (
<div style={{ textAlign: 'center', marginBottom: 16 }}>
<EuiHorizontalRule />
<EuiText color="subdued">
<p>
<FormattedMessage
id="xpack.infra.metricsExplorer.footerPaginationMessage"
defaultMessage='Displaying {length} of {total} charts grouped by "{groupBy}".'
values={{
length: data.series.length,
total: data.pageInfo.total,
groupBy: options.groupBy,
}}
/>
</p>
</EuiText>
{data.pageInfo.afterKey ? (
<div style={{ margin: '16px 0' }}>
<EuiButton
isLoading={loading}
size="s"
onClick={() => onLoadMore(data.pageInfo.afterKey || null)}
>
<FormattedMessage
id="xpack.infra.metricsExplorer.loadMoreChartsButton"
defaultMessage="Load More Charts"
/>
</EuiButton>
</div>
) : null}
</div>
) : null}
</div>
<InfraLoadingPanel
height={800}
width="100%"
text={i18n.translate('xpack.infra.metricsExplorer.loadingCharts', {
defaultMessage: 'Loading charts',
})}
/>
);
}
);
if (!data || data.series.length === 0) {
return (
<NoData
titleText={i18n.translate('xpack.infra.metricsExplorer.noDataTitle', {
defaultMessage: 'There is no data to display.',
})}
bodyText={i18n.translate('xpack.infra.metricsExplorer.noDataBodyText', {
defaultMessage: 'Try adjusting your time, filters or group by settings.',
})}
refetchText={i18n.translate('xpack.infra.metricsExplorer.noDataRefetchText', {
defaultMessage: 'Check for new data',
})}
testString="metrics-explorer-no-data"
onRefetch={onRefetch}
/>
);
}
return (
<div style={{ width: '100%' }}>
<EuiFlexGrid gutterSize="s" columns={data.series.length === 1 ? 1 : 3}>
{data.series.map(series => (
<EuiFlexItem key={series.id} style={{ minWidth: 0 }}>
<MetricsExplorerChart
key={`chart-${series.id}`}
onFilter={onFilter}
options={options}
chartOptions={chartOptions}
title={options.groupBy ? series.id : null}
height={data.series.length > 1 ? 200 : 400}
series={series}
source={source}
timeRange={timeRange}
onTimeChange={onTimeChange}
/>
</EuiFlexItem>
))}
</EuiFlexGrid>
{data.series.length > 1 ? (
<div style={{ textAlign: 'center', marginBottom: 16 }}>
<EuiHorizontalRule />
<EuiText color="subdued">
<p>
<FormattedMessage
id="xpack.infra.metricsExplorer.footerPaginationMessage"
defaultMessage='Displaying {length} of {total} charts grouped by "{groupBy}".'
values={{
length: data.series.length,
total: data.pageInfo.total,
groupBy: options.groupBy,
}}
/>
</p>
</EuiText>
{data.pageInfo.afterKey ? (
<div style={{ margin: '16px 0' }}>
<EuiButton
isLoading={loading}
size="s"
onClick={() => onLoadMore(data.pageInfo.afterKey || null)}
>
<FormattedMessage
id="xpack.infra.metricsExplorer.loadMoreChartsButton"
defaultMessage="Load More Charts"
/>
</EuiButton>
</div>
) : null}
</div>
) : null}
</div>
);
};

View file

@ -5,10 +5,10 @@
*/
import React from 'react';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiEmptyPrompt } from '@elastic/eui';
export const MetricsExplorerEmptyChart = injectI18n(() => {
export const MetricsExplorerEmptyChart = () => {
return (
<EuiEmptyPrompt
iconType="stats"
@ -30,4 +30,4 @@ export const MetricsExplorerEmptyChart = injectI18n(() => {
}
/>
);
});
};

View file

@ -5,20 +5,20 @@
*/
import { EuiComboBox } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React, { useCallback } from 'react';
import { FieldType } from 'ui/index_patterns';
import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options';
import { isDisplayable } from '../../utils/is_displayable';
interface Props {
intl: InjectedIntl;
options: MetricsExplorerOptions;
onChange: (groupBy: string | null) => void;
fields: FieldType[];
}
export const MetricsExplorerGroupBy = injectI18n(({ intl, options, onChange, fields }: Props) => {
export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) => {
const handleChange = useCallback(
selectedOptions => {
const groupBy = (selectedOptions.length === 1 && selectedOptions[0].label) || null;
@ -41,8 +41,7 @@ export const MetricsExplorerGroupBy = injectI18n(({ intl, options, onChange, fie
return (
<EuiComboBox
placeholder={intl.formatMessage({
id: 'xpack.infra.metricsExplorer.groupByLabel',
placeholder={i18n.translate('xpack.infra.metricsExplorer.groupByLabel', {
defaultMessage: 'Everything',
})}
fullWidth
@ -55,4 +54,4 @@ export const MetricsExplorerGroupBy = injectI18n(({ intl, options, onChange, fie
isClearable={true}
/>
);
});
};

View file

@ -5,7 +5,8 @@
*/
import { fromKueryExpression } from '@kbn/es-query';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
@ -13,7 +14,6 @@ import { AutocompleteField } from '../autocomplete_field';
import { isDisplayable } from '../../utils/is_displayable';
interface Props {
intl: InjectedIntl;
derivedIndexPattern: StaticIndexPattern;
onSubmit: (query: string) => void;
value?: string | null;
@ -28,46 +28,43 @@ function validateQuery(query: string) {
return true;
}
export const MetricsExplorerKueryBar = injectI18n(
({ intl, derivedIndexPattern, onSubmit, value }: Props) => {
const [draftQuery, setDraftQuery] = useState<string>(value || '');
const [isValid, setValidation] = useState<boolean>(true);
export const MetricsExplorerKueryBar = ({ derivedIndexPattern, onSubmit, value }: Props) => {
const [draftQuery, setDraftQuery] = useState<string>(value || '');
const [isValid, setValidation] = useState<boolean>(true);
// This ensures that if value changes out side this component it will update.
useEffect(() => {
if (value) {
setDraftQuery(value);
}
}, [value]);
// This ensures that if value changes out side this component it will update.
useEffect(() => {
if (value) {
setDraftQuery(value);
}
}, [value]);
const handleChange = (query: string) => {
setValidation(validateQuery(query));
setDraftQuery(query);
};
const handleChange = (query: string) => {
setValidation(validateQuery(query));
setDraftQuery(query);
};
const filteredDerivedIndexPattern = {
...derivedIndexPattern,
fields: derivedIndexPattern.fields.filter(field => isDisplayable(field)),
};
const filteredDerivedIndexPattern = {
...derivedIndexPattern,
fields: derivedIndexPattern.fields.filter(field => isDisplayable(field)),
};
return (
<WithKueryAutocompletion indexPattern={filteredDerivedIndexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isValid}
loadSuggestions={loadSuggestions}
onChange={handleChange}
onSubmit={onSubmit}
placeholder={intl.formatMessage({
id: 'xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder',
defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)',
})}
suggestions={suggestions}
value={draftQuery}
/>
)}
</WithKueryAutocompletion>
);
}
);
return (
<WithKueryAutocompletion indexPattern={filteredDerivedIndexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isValid}
loadSuggestions={loadSuggestions}
onChange={handleChange}
onSubmit={onSubmit}
placeholder={i18n.translate('xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder', {
defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)',
})}
suggestions={suggestions}
value={draftQuery}
/>
)}
</WithKueryAutocompletion>
);
};

View file

@ -5,7 +5,8 @@
*/
import { EuiComboBox } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useState, useEffect } from 'react';
import { FieldType } from 'ui/index_patterns';
import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette';
@ -17,7 +18,6 @@ import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_me
import { isDisplayable } from '../../utils/is_displayable';
interface Props {
intl: InjectedIntl;
autoFocus?: boolean;
options: MetricsExplorerOptions;
onChange: (metrics: MetricsExplorerMetric[]) => void;
@ -29,67 +29,64 @@ interface SelectedOption {
label: string;
}
export const MetricsExplorerMetrics = injectI18n(
({ intl, options, onChange, fields, autoFocus = false }: Props) => {
const colors = Object.keys(MetricsExplorerColor) as MetricsExplorerColor[];
const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
const [focusOnce, setFocusState] = useState<boolean>(false);
export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = false }: Props) => {
const colors = Object.keys(MetricsExplorerColor) as MetricsExplorerColor[];
const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
const [focusOnce, setFocusState] = useState<boolean>(false);
useEffect(() => {
if (inputRef && autoFocus && !focusOnce) {
inputRef.focus();
setFocusState(true);
}
}, [inputRef]);
useEffect(() => {
if (inputRef && autoFocus && !focusOnce) {
inputRef.focus();
setFocusState(true);
}
}, [inputRef]);
// I tried to use useRef originally but the EUIComboBox component's type definition
// would only accept an actual input element or a callback function (with the same type).
// This effectivly does the same thing but is compatible with EuiComboBox.
const handleInputRef = (ref: HTMLInputElement) => {
if (ref) {
setInputRef(ref);
}
};
const handleChange = useCallback(
selectedOptions => {
onChange(
selectedOptions.map((opt: SelectedOption, index: number) => ({
aggregation: options.aggregation,
field: opt.value,
color: colors[index],
}))
);
},
[options, onChange]
);
// I tried to use useRef originally but the EUIComboBox component's type definition
// would only accept an actual input element or a callback function (with the same type).
// This effectivly does the same thing but is compatible with EuiComboBox.
const handleInputRef = (ref: HTMLInputElement) => {
if (ref) {
setInputRef(ref);
}
};
const handleChange = useCallback(
selectedOptions => {
onChange(
selectedOptions.map((opt: SelectedOption, index: number) => ({
aggregation: options.aggregation,
field: opt.value,
color: colors[index],
}))
);
},
[options, onChange]
);
const comboOptions = fields
.filter(field => isDisplayable(field))
.map(field => ({ label: field.name, value: field.name }));
const selectedOptions = options.metrics
.filter(m => m.aggregation !== MetricsExplorerAggregation.count)
.map(metric => ({
label: metric.field || '',
value: metric.field || '',
color: colorTransformer(metric.color || MetricsExplorerColor.color0),
}));
const comboOptions = fields
.filter(field => isDisplayable(field))
.map(field => ({ label: field.name, value: field.name }));
const selectedOptions = options.metrics
.filter(m => m.aggregation !== MetricsExplorerAggregation.count)
.map(metric => ({
label: metric.field || '',
value: metric.field || '',
color: colorTransformer(metric.color || MetricsExplorerColor.color0),
}));
const placeholderText = intl.formatMessage({
id: 'xpack.infra.metricsExplorer.metricComboBoxPlaceholder',
defaultMessage: 'choose a metric to plot',
});
const placeholderText = i18n.translate('xpack.infra.metricsExplorer.metricComboBoxPlaceholder', {
defaultMessage: 'choose a metric to plot',
});
return (
<EuiComboBox
isDisabled={options.aggregation === MetricsExplorerAggregation.count}
placeholder={placeholderText}
fullWidth
options={comboOptions}
selectedOptions={selectedOptions}
onChange={handleChange}
isClearable={true}
inputRef={handleInputRef}
/>
);
}
);
return (
<EuiComboBox
isDisabled={options.aggregation === MetricsExplorerAggregation.count}
placeholder={placeholderText}
fullWidth
options={comboOptions}
selectedOptions={selectedOptions}
onChange={handleChange}
isClearable={true}
inputRef={handleInputRef}
/>
);
};

View file

@ -5,10 +5,10 @@
*/
import React from 'react';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiEmptyPrompt } from '@elastic/eui';
export const MetricsExplorerNoMetrics = injectI18n(() => {
export const MetricsExplorerNoMetrics = () => {
return (
<EuiEmptyPrompt
iconType="stats"
@ -30,4 +30,4 @@ export const MetricsExplorerNoMetrics = injectI18n(() => {
}
/>
);
});
};

View file

@ -5,7 +5,7 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiText } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
import {
@ -25,7 +25,6 @@ import { MetricsExplorerAggregationPicker } from './aggregation';
import { MetricsExplorerChartOptions as MetricsExplorerChartOptionsComponent } from './chart_options';
interface Props {
intl: InjectedIntl;
derivedIndexPattern: StaticIndexPattern;
timeRange: MetricsExplorerTimeOptions;
options: MetricsExplorerOptions;
@ -39,88 +38,86 @@ interface Props {
onChartOptionsChange: (chartOptions: MetricsExplorerChartOptions) => void;
}
export const MetricsExplorerToolbar = injectI18n(
({
timeRange,
derivedIndexPattern,
options,
onTimeChange,
onRefresh,
onGroupByChange,
onFilterQuerySubmit,
onMetricsChange,
onAggregationChange,
chartOptions,
onChartOptionsChange,
}: Props) => {
const isDefaultOptions =
options.aggregation === MetricsExplorerAggregation.avg && options.metrics.length === 0;
return (
<Toolbar>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={options.aggregation === MetricsExplorerAggregation.count ? 2 : false}>
<MetricsExplorerAggregationPicker
fullWidth
options={options}
onChange={onAggregationChange}
/>
</EuiFlexItem>
{options.aggregation !== MetricsExplorerAggregation.count && (
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.infra.metricsExplorer.aggregationLabel"
defaultMessage="of"
/>
</EuiText>
)}
{options.aggregation !== MetricsExplorerAggregation.count && (
<EuiFlexItem grow={2}>
<MetricsExplorerMetrics
autoFocus={isDefaultOptions}
fields={derivedIndexPattern.fields}
options={options}
onChange={onMetricsChange}
/>
</EuiFlexItem>
)}
export const MetricsExplorerToolbar = ({
timeRange,
derivedIndexPattern,
options,
onTimeChange,
onRefresh,
onGroupByChange,
onFilterQuerySubmit,
onMetricsChange,
onAggregationChange,
chartOptions,
onChartOptionsChange,
}: Props) => {
const isDefaultOptions =
options.aggregation === MetricsExplorerAggregation.avg && options.metrics.length === 0;
return (
<Toolbar>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={options.aggregation === MetricsExplorerAggregation.count ? 2 : false}>
<MetricsExplorerAggregationPicker
fullWidth
options={options}
onChange={onAggregationChange}
/>
</EuiFlexItem>
{options.aggregation !== MetricsExplorerAggregation.count && (
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.infra.metricsExplorer.groupByToolbarLabel"
defaultMessage="graph per"
id="xpack.infra.metricsExplorer.aggregationLabel"
defaultMessage="of"
/>
</EuiText>
<EuiFlexItem grow={1}>
<MetricsExplorerGroupBy
onChange={onGroupByChange}
)}
{options.aggregation !== MetricsExplorerAggregation.count && (
<EuiFlexItem grow={2}>
<MetricsExplorerMetrics
autoFocus={isDefaultOptions}
fields={derivedIndexPattern.fields}
options={options}
onChange={onMetricsChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem>
<MetricsExplorerKueryBar
derivedIndexPattern={derivedIndexPattern}
onSubmit={onFilterQuerySubmit}
value={options.filterQuery}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<MetricsExplorerChartOptionsComponent
onChange={onChartOptionsChange}
chartOptions={chartOptions}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ marginRight: 5 }}>
<EuiSuperDatePicker
start={timeRange.from}
end={timeRange.to}
onTimeChange={({ start, end }) => onTimeChange(start, end)}
onRefresh={onRefresh}
/>
</EuiFlexItem>
</EuiFlexGroup>
</Toolbar>
);
}
);
)}
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.infra.metricsExplorer.groupByToolbarLabel"
defaultMessage="graph per"
/>
</EuiText>
<EuiFlexItem grow={1}>
<MetricsExplorerGroupBy
onChange={onGroupByChange}
fields={derivedIndexPattern.fields}
options={options}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem>
<MetricsExplorerKueryBar
derivedIndexPattern={derivedIndexPattern}
onSubmit={onFilterQuerySubmit}
value={options.filterQuery}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<MetricsExplorerChartOptionsComponent
onChange={onChartOptionsChange}
chartOptions={chartOptions}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ marginRight: 5 }}>
<EuiSuperDatePicker
start={timeRange.from}
end={timeRange.to}
onTimeChange={({ start, end }) => onTimeChange(start, end)}
onRefresh={onRefresh}
/>
</EuiFlexItem>
</EuiFlexGroup>
</Toolbar>
);
};

View file

@ -5,7 +5,8 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { get, max, min } from 'lodash';
import React from 'react';
@ -35,7 +36,6 @@ interface Props {
timeRange: InfraTimerangeInput;
onViewChange: (view: string) => void;
view: string;
intl: InjectedIntl;
boundsOverride: InfraWaffleMapBounds;
autoBounds: boolean;
}
@ -80,131 +80,124 @@ const calculateBoundsFromNodes = (nodes: InfraSnapshotNode[]): InfraWaffleMapBou
return { min: min(minValues) || 0, max: max(maxValues) || 0 };
};
export const NodesOverview = injectI18n(
class extends React.Component<Props, {}> {
public static displayName = 'Waffle';
public render() {
const {
autoBounds,
boundsOverride,
loading,
nodes,
nodeType,
reload,
intl,
view,
options,
timeRange,
} = this.props;
if (loading) {
return (
<InfraLoadingPanel
height="100%"
width="100%"
text={intl.formatMessage({
id: 'xpack.infra.waffle.loadingDataText',
defaultMessage: 'Loading data',
})}
/>
);
} else if (!loading && nodes && nodes.length === 0) {
return (
<NoData
titleText={intl.formatMessage({
id: 'xpack.infra.waffle.noDataTitle',
defaultMessage: 'There is no data to display.',
})}
bodyText={intl.formatMessage({
id: 'xpack.infra.waffle.noDataDescription',
defaultMessage: 'Try adjusting your time or filter.',
})}
refetchText={intl.formatMessage({
id: 'xpack.infra.waffle.checkNewDataButtonLabel',
defaultMessage: 'Check for new data',
})}
onRefetch={() => {
reload();
}}
testString="noMetricsDataPrompt"
/>
);
}
const dataBounds = calculateBoundsFromNodes(nodes);
const bounds = autoBounds ? dataBounds : boundsOverride;
export const NodesOverview = class extends React.Component<Props, {}> {
public static displayName = 'Waffle';
public render() {
const {
autoBounds,
boundsOverride,
loading,
nodes,
nodeType,
reload,
view,
options,
timeRange,
} = this.props;
if (loading) {
return (
<MainContainer>
<ViewSwitcherContainer>
<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 at the selected time"
/>
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</ViewSwitcherContainer>
{view === 'table' ? (
<TableContainer>
<TableView
nodeType={nodeType}
nodes={nodes}
options={options}
formatter={this.formatter}
timeRange={timeRange}
onFilter={this.handleDrilldown}
/>
</TableContainer>
) : (
<MapContainer>
<Map
nodeType={nodeType}
nodes={nodes}
options={options}
formatter={this.formatter}
timeRange={timeRange}
onFilter={this.handleDrilldown}
bounds={bounds}
dataBounds={dataBounds}
/>
</MapContainer>
)}
</MainContainer>
<InfraLoadingPanel
height="100%"
width="100%"
text={i18n.translate('xpack.infra.waffle.loadingDataText', {
defaultMessage: 'Loading data',
})}
/>
);
} else if (!loading && nodes && nodes.length === 0) {
return (
<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"
/>
);
}
private handleViewChange = (view: string) => this.props.onViewChange(view);
// TODO: Change this to a real implementation 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[InfraSnapshotMetricType.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;
};
const dataBounds = calculateBoundsFromNodes(nodes);
const bounds = autoBounds ? dataBounds : boundsOverride;
return (
<MainContainer>
<ViewSwitcherContainer>
<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 at the selected time"
/>
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</ViewSwitcherContainer>
{view === 'table' ? (
<TableContainer>
<TableView
nodeType={nodeType}
nodes={nodes}
options={options}
formatter={this.formatter}
timeRange={timeRange}
onFilter={this.handleDrilldown}
/>
</TableContainer>
) : (
<MapContainer>
<Map
nodeType={nodeType}
nodes={nodes}
options={options}
formatter={this.formatter}
timeRange={timeRange}
onFilter={this.handleDrilldown}
bounds={bounds}
dataBounds={dataBounds}
/>
</MapContainer>
)}
</MainContainer>
);
}
);
private handleViewChange = (view: string) => this.props.onViewChange(view);
// 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[InfraSnapshotMetricType.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;
};
};
const MainContainer = euiStyled.div`
position: relative;

View file

@ -5,7 +5,8 @@
*/
import { EuiButtonEmpty, EuiInMemoryTable, EuiToolTip } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { last } from 'lodash';
import React from 'react';
import { createWaffleMapNode } from '../../containers/waffle/nodes_to_wafflemap';
@ -25,7 +26,6 @@ interface Props {
options: InfraWaffleMapOptions;
formatter: (subject: string | number) => string;
timeRange: InfraTimerangeInput;
intl: InjectedIntl;
onFilter: (filter: string) => void;
}
@ -46,138 +46,126 @@ const getGroupPaths = (path: InfraSnapshotNodePath[]) => {
}
};
export const TableView = injectI18n(
class extends React.PureComponent<Props, State> {
public readonly state: State = initialState;
public render() {
const { nodes, options, formatter, intl, timeRange, nodeType } = this.props;
const columns = [
{
field: 'name',
name: intl.formatMessage({
id: 'xpack.infra.tableView.columnName.name',
defaultMessage: 'Name',
}),
sortable: true,
truncateText: true,
textOnly: true,
render: (value: string, item: { node: InfraWaffleMapNode }) => {
const tooltipText = item.node.id === value ? `${value}` : `${value} (${item.node.id})`;
// For the table we need to create a UniqueID that takes into to account the groupings
// as well as the node name. There is the possibility that a node can be present in two
// different groups and be on the screen at the same time.
const uniqueID = [...item.node.path.map(p => p.value), item.node.name].join(':');
return (
<NodeContextMenu
node={item.node}
nodeType={nodeType}
closePopover={this.closePopoverFor(uniqueID)}
timeRange={timeRange}
isPopoverOpen={this.state.isPopoverOpen.includes(uniqueID)}
options={options}
popoverPosition="rightCenter"
>
<EuiToolTip content={tooltipText}>
<EuiButtonEmpty onClick={this.openPopoverFor(uniqueID)}>{value}</EuiButtonEmpty>
</EuiToolTip>
</NodeContextMenu>
);
},
},
...options.groupBy.map((grouping, index) => ({
field: `group_${index}`,
name: fieldToName((grouping && grouping.field) || '', intl),
sortable: true,
truncateText: true,
textOnly: true,
render: (value: string) => {
const handleClick = () => this.props.onFilter(`${grouping.field}:"${value}"`);
return (
<EuiToolTip content="Set Filter">
<EuiButtonEmpty onClick={handleClick}>{value}</EuiButtonEmpty>
export const TableView = class extends React.PureComponent<Props, State> {
public readonly state: State = initialState;
public render() {
const { nodes, options, formatter, timeRange, nodeType } = this.props;
const columns = [
{
field: 'name',
name: i18n.translate('xpack.infra.tableView.columnName.name', { defaultMessage: 'Name' }),
sortable: true,
truncateText: true,
textOnly: true,
render: (value: string, item: { node: InfraWaffleMapNode }) => {
const tooltipText = item.node.id === value ? `${value}` : `${value} (${item.node.id})`;
// For the table we need to create a UniqueID that takes into to account the groupings
// as well as the node name. There is the possibility that a node can be present in two
// different groups and be on the screen at the same time.
const uniqueID = [...item.node.path.map(p => p.value), item.node.name].join(':');
return (
<NodeContextMenu
node={item.node}
nodeType={nodeType}
closePopover={this.closePopoverFor(uniqueID)}
timeRange={timeRange}
isPopoverOpen={this.state.isPopoverOpen.includes(uniqueID)}
options={options}
popoverPosition="rightCenter"
>
<EuiToolTip content={tooltipText}>
<EuiButtonEmpty onClick={this.openPopoverFor(uniqueID)}>{value}</EuiButtonEmpty>
</EuiToolTip>
);
},
})),
{
field: 'value',
name: intl.formatMessage({
id: 'xpack.infra.tableView.columnName.last1m',
defaultMessage: 'Last 1m',
}),
sortable: true,
truncateText: true,
dataType: 'number',
render: (value: number) => <span>{formatter(value)}</span>,
</NodeContextMenu>
);
},
{
field: 'avg',
name: intl.formatMessage({
id: 'xpack.infra.tableView.columnName.avg',
defaultMessage: 'Avg',
}),
sortable: true,
truncateText: true,
dataType: 'number',
render: (value: number) => <span>{formatter(value)}</span>,
},
...options.groupBy.map((grouping, index) => ({
field: `group_${index}`,
name: fieldToName((grouping && grouping.field) || ''),
sortable: true,
truncateText: true,
textOnly: true,
render: (value: string) => {
const handleClick = () => this.props.onFilter(`${grouping.field}:"${value}"`);
return (
<EuiToolTip content="Set Filter">
<EuiButtonEmpty onClick={handleClick}>{value}</EuiButtonEmpty>
</EuiToolTip>
);
},
{
field: 'max',
name: intl.formatMessage({
id: 'xpack.infra.tableView.columnName.max',
defaultMessage: 'Max',
})),
{
field: 'value',
name: i18n.translate('xpack.infra.tableView.columnName.last1m', {
defaultMessage: 'Last 1m',
}),
sortable: true,
truncateText: true,
dataType: 'number',
render: (value: number) => <span>{formatter(value)}</span>,
},
{
field: 'avg',
name: i18n.translate('xpack.infra.tableView.columnName.avg', { defaultMessage: 'Avg' }),
sortable: true,
truncateText: true,
dataType: 'number',
render: (value: number) => <span>{formatter(value)}</span>,
},
{
field: 'max',
name: i18n.translate('xpack.infra.tableView.columnName.max', { defaultMessage: 'Max' }),
sortable: true,
truncateText: true,
dataType: 'number',
render: (value: number) => <span>{formatter(value)}</span>,
},
];
const items = nodes.map(node => {
const name = last(node.path);
return {
name: (name && name.label) || 'unknown',
...getGroupPaths(node.path).reduce(
(acc, path, index) => ({
...acc,
[`group_${index}`]: path.label,
}),
sortable: true,
truncateText: true,
dataType: 'number',
render: (value: number) => <span>{formatter(value)}</span>,
},
];
const items = nodes.map(node => {
const name = last(node.path);
{}
),
value: node.metric.value,
avg: node.metric.avg,
max: node.metric.max,
node: createWaffleMapNode(node),
};
});
const initialSorting = {
sort: {
field: 'value',
direction: 'desc',
},
};
return (
<EuiInMemoryTable
pagination={true}
sorting={initialSorting}
items={items}
columns={columns}
/>
);
}
private openPopoverFor = (id: string) => () => {
this.setState(prevState => ({ isPopoverOpen: [...prevState.isPopoverOpen, id] }));
};
private closePopoverFor = (id: string) => () => {
if (this.state.isPopoverOpen.includes(id)) {
this.setState(prevState => {
return {
name: (name && name.label) || 'unknown',
...getGroupPaths(node.path).reduce(
(acc, path, index) => ({
...acc,
[`group_${index}`]: path.label,
}),
{}
),
value: node.metric.value,
avg: node.metric.avg,
max: node.metric.max,
node: createWaffleMapNode(node),
isPopoverOpen: prevState.isPopoverOpen.filter(subject => subject !== id),
};
});
const initialSorting = {
sort: {
field: 'value',
direction: 'desc',
},
};
return (
<EuiInMemoryTable
pagination={true}
sorting={initialSorting}
items={items}
columns={columns}
/>
);
}
private openPopoverFor = (id: string) => () => {
this.setState(prevState => ({ isPopoverOpen: [...prevState.isPopoverOpen, id] }));
};
private closePopoverFor = (id: string) => () => {
if (this.state.isPopoverOpen.includes(id)) {
this.setState(prevState => {
return {
isPopoverOpen: prevState.isPopoverOpen.filter(subject => subject !== id),
};
});
}
};
}
);
};
};

View file

@ -17,7 +17,7 @@ import {
EuiPageContentBody,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useCallback, useContext, useMemo } from 'react';
import { Prompt } from 'react-router-dom';
@ -29,185 +29,184 @@ import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'
import { useSourceConfigurationFormState } from './source_configuration_form_state';
interface SourceConfigurationSettingsProps {
intl: InjectedIntl;
shouldAllowEdit: boolean;
}
export const SourceConfigurationSettings = injectI18n(
({ shouldAllowEdit }: SourceConfigurationSettingsProps) => {
const {
createSourceConfiguration,
source,
sourceExists,
isLoading,
updateSourceConfiguration,
} = useContext(Source.Context);
export const SourceConfigurationSettings = ({
shouldAllowEdit,
}: SourceConfigurationSettingsProps) => {
const {
createSourceConfiguration,
source,
sourceExists,
isLoading,
updateSourceConfiguration,
} = useContext(Source.Context);
const availableFields = useMemo(
() => (source && source.status ? source.status.indexFields.map(field => field.name) : []),
[source]
);
const availableFields = useMemo(
() => (source && source.status ? source.status.indexFields.map(field => field.name) : []),
[source]
);
const {
addLogColumn,
moveLogColumn,
indicesConfigurationProps,
logColumnConfigurationProps,
errors,
resetForm,
isFormDirty,
isFormValid,
formState,
formStateChanges,
} = useSourceConfigurationFormState(source && source.configuration);
const {
addLogColumn,
moveLogColumn,
indicesConfigurationProps,
logColumnConfigurationProps,
errors,
resetForm,
isFormDirty,
isFormValid,
formState,
formStateChanges,
} = useSourceConfigurationFormState(source && source.configuration);
const persistUpdates = useCallback(async () => {
if (sourceExists) {
await updateSourceConfiguration(formStateChanges);
} else {
await createSourceConfiguration(formState);
}
resetForm();
}, [
sourceExists,
updateSourceConfiguration,
createSourceConfiguration,
resetForm,
formState,
formStateChanges,
]);
const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [
shouldAllowEdit,
source,
]);
if (!source || !source.configuration) {
return null;
const persistUpdates = useCallback(async () => {
if (sourceExists) {
await updateSourceConfiguration(formStateChanges);
} else {
await createSourceConfiguration(formState);
}
resetForm();
}, [
sourceExists,
updateSourceConfiguration,
createSourceConfiguration,
resetForm,
formState,
formStateChanges,
]);
return (
<>
<EuiPage>
<EuiPageBody>
<EuiPageContent
verticalPosition="center"
horizontalPosition="center"
data-test-subj="sourceConfigurationContent"
>
<EuiPageContentBody>
<Prompt
when={isFormDirty}
message={i18n.translate('xpack.infra.sourceConfiguration.unsavedFormPrompt', {
defaultMessage: 'Are you sure you want to leave? Changes will be lost',
})}
const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [
shouldAllowEdit,
source,
]);
if (!source || !source.configuration) {
return null;
}
return (
<>
<EuiPage>
<EuiPageBody>
<EuiPageContent
verticalPosition="center"
horizontalPosition="center"
data-test-subj="sourceConfigurationContent"
>
<EuiPageContentBody>
<Prompt
when={isFormDirty}
message={i18n.translate('xpack.infra.sourceConfiguration.unsavedFormPrompt', {
defaultMessage: 'Are you sure you want to leave? Changes will be lost',
})}
/>
<EuiPanel paddingSize="l">
<NameConfigurationPanel
isLoading={isLoading}
nameFieldProps={indicesConfigurationProps.name}
readOnly={!isWriteable}
/>
<EuiPanel paddingSize="l">
<NameConfigurationPanel
isLoading={isLoading}
nameFieldProps={indicesConfigurationProps.name}
readOnly={!isWriteable}
/>
</EuiPanel>
<EuiSpacer />
<EuiPanel paddingSize="l">
<IndicesConfigurationPanel
isLoading={isLoading}
logAliasFieldProps={indicesConfigurationProps.logAlias}
metricAliasFieldProps={indicesConfigurationProps.metricAlias}
readOnly={!isWriteable}
/>
</EuiPanel>
<EuiSpacer />
<EuiPanel paddingSize="l">
<FieldsConfigurationPanel
containerFieldProps={indicesConfigurationProps.containerField}
hostFieldProps={indicesConfigurationProps.hostField}
isLoading={isLoading}
podFieldProps={indicesConfigurationProps.podField}
readOnly={!isWriteable}
tiebreakerFieldProps={indicesConfigurationProps.tiebreakerField}
timestampFieldProps={indicesConfigurationProps.timestampField}
/>
</EuiPanel>
<EuiSpacer />
<EuiPanel paddingSize="l">
<LogColumnsConfigurationPanel
addLogColumn={addLogColumn}
moveLogColumn={moveLogColumn}
availableFields={availableFields}
isLoading={isLoading}
logColumnConfiguration={logColumnConfigurationProps}
/>
</EuiPanel>
{errors.length > 0 ? (
<>
<EuiCallOut color="danger">
<ul>
{errors.map((error, errorIndex) => (
<li key={errorIndex}>{error}</li>
))}
</ul>
</EuiCallOut>
<EuiSpacer size="m" />
</>
) : null}
<EuiSpacer size="m" />
<EuiFlexGroup>
{isWriteable && (
<EuiFlexItem>
{isLoading ? (
</EuiPanel>
<EuiSpacer />
<EuiPanel paddingSize="l">
<IndicesConfigurationPanel
isLoading={isLoading}
logAliasFieldProps={indicesConfigurationProps.logAlias}
metricAliasFieldProps={indicesConfigurationProps.metricAlias}
readOnly={!isWriteable}
/>
</EuiPanel>
<EuiSpacer />
<EuiPanel paddingSize="l">
<FieldsConfigurationPanel
containerFieldProps={indicesConfigurationProps.containerField}
hostFieldProps={indicesConfigurationProps.hostField}
isLoading={isLoading}
podFieldProps={indicesConfigurationProps.podField}
readOnly={!isWriteable}
tiebreakerFieldProps={indicesConfigurationProps.tiebreakerField}
timestampFieldProps={indicesConfigurationProps.timestampField}
/>
</EuiPanel>
<EuiSpacer />
<EuiPanel paddingSize="l">
<LogColumnsConfigurationPanel
addLogColumn={addLogColumn}
moveLogColumn={moveLogColumn}
availableFields={availableFields}
isLoading={isLoading}
logColumnConfiguration={logColumnConfigurationProps}
/>
</EuiPanel>
{errors.length > 0 ? (
<>
<EuiCallOut color="danger">
<ul>
{errors.map((error, errorIndex) => (
<li key={errorIndex}>{error}</li>
))}
</ul>
</EuiCallOut>
<EuiSpacer size="m" />
</>
) : null}
<EuiSpacer size="m" />
<EuiFlexGroup>
{isWriteable && (
<EuiFlexItem>
{isLoading ? (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton color="primary" isLoading fill>
Loading
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton color="primary" isLoading fill>
Loading
<EuiButton
data-test-subj="discardSettingsButton"
color="danger"
iconType="cross"
isDisabled={isLoading || !isFormDirty}
onClick={() => {
resetForm();
}}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.discardSettingsButtonLabel"
defaultMessage="Discard"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="applySettingsButton"
color="primary"
isDisabled={!isFormDirty || !isFormValid}
fill
onClick={persistUpdates}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.applySettingsButtonLabel"
defaultMessage="Apply"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="discardSettingsButton"
color="danger"
iconType="cross"
isDisabled={isLoading || !isFormDirty}
onClick={() => {
resetForm();
}}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.discardSettingsButtonLabel"
defaultMessage="Discard"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="applySettingsButton"
color="primary"
isDisabled={!isFormDirty || !isFormValid}
fill
onClick={persistUpdates}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.applySettingsButtonLabel"
defaultMessage="Apply"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
</>
);
}
);
</>
)}
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
</>
);
};

View file

@ -5,13 +5,13 @@
*/
import { EuiButton, EuiComboBox, EuiForm, EuiFormRow } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { FieldType } from 'ui/index_patterns';
interface Props {
onSubmit: (field: string) => void;
fields: FieldType[];
intl: InjectedIntl;
}
interface SelectedOption {
@ -24,62 +24,56 @@ const initialState = {
type State = Readonly<typeof initialState>;
export const CustomFieldPanel = injectI18n(
class extends React.PureComponent<Props, State> {
public static displayName = 'CustomFieldPanel';
public readonly state: State = initialState;
public render() {
const { fields, intl } = this.props;
const options = fields
.filter(f => f.aggregatable && f.type === 'string')
.map(f => ({ label: f.name }));
return (
<div style={{ padding: 16 }}>
<EuiForm>
<EuiFormRow
label={intl.formatMessage({
id: 'xpack.infra.waffle.customGroupByFieldLabel',
defaultMessage: 'Field',
})}
helpText={intl.formatMessage({
id: 'xpack.infra.waffle.customGroupByHelpText',
defaultMessage: 'This is the field used for the terms aggregation',
})}
export const CustomFieldPanel = class extends React.PureComponent<Props, State> {
public static displayName = 'CustomFieldPanel';
public readonly state: State = initialState;
public render() {
const { fields } = this.props;
const options = fields
.filter(f => f.aggregatable && f.type === 'string')
.map(f => ({ label: f.name }));
return (
<div style={{ padding: 16 }}>
<EuiForm>
<EuiFormRow
label={i18n.translate('xpack.infra.waffle.customGroupByFieldLabel', {
defaultMessage: 'Field',
})}
helpText={i18n.translate('xpack.infra.waffle.customGroupByHelpText', {
defaultMessage: 'This is the field used for the terms aggregation',
})}
compressed
>
<EuiComboBox
compressed
>
<EuiComboBox
compressed
placeholder={intl.formatMessage({
id: 'xpack.infra.waffle.customGroupByDropdownPlacehoder',
defaultMessage: 'Select one',
})}
singleSelection={{ asPlainText: true }}
selectedOptions={this.state.selectedOptions}
options={options}
onChange={this.handleFieldSelection}
isClearable={false}
/>
</EuiFormRow>
<EuiButton
disabled={!this.state.selectedOptions.length}
type="submit"
size="s"
fill
onClick={this.handleSubmit}
>
Add
</EuiButton>
</EuiForm>
</div>
);
}
private handleSubmit = () => {
this.props.onSubmit(this.state.selectedOptions[0].label);
};
private handleFieldSelection = (selectedOptions: SelectedOption[]) => {
this.setState({ selectedOptions });
};
placeholder={i18n.translate('xpack.infra.waffle.customGroupByDropdownPlacehoder', {
defaultMessage: 'Select one',
})}
singleSelection={{ asPlainText: true }}
selectedOptions={this.state.selectedOptions}
options={options}
onChange={this.handleFieldSelection}
isClearable={false}
/>
</EuiFormRow>
<EuiButton
disabled={!this.state.selectedOptions.length}
type="submit"
size="s"
fill
onClick={this.handleSubmit}
>
Add
</EuiButton>
</EuiForm>
</div>
);
}
);
private handleSubmit = () => {
this.props.onSubmit(this.state.selectedOptions[0].label);
};
private handleFieldSelection = (selectedOptions: SelectedOption[]) => {
this.setState({ selectedOptions });
};
};

View file

@ -18,7 +18,8 @@ import {
EuiSwitch,
EuiText,
} from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { SyntheticEvent, useState } from 'react';
import euiStyled from '../../../../../common/eui_styled_components';
@ -30,140 +31,130 @@ interface Props {
dataBounds: InfraWaffleMapBounds;
autoBounds: boolean;
boundsOverride: InfraWaffleMapBounds;
intl: InjectedIntl;
}
export const LegendControls = injectI18n(
({ intl, autoBounds, boundsOverride, onChange, dataBounds }: Props) => {
const [isPopoverOpen, setPopoverState] = useState(false);
const [draftAuto, setDraftAuto] = useState(autoBounds);
const [draftBounds, setDraftBounds] = useState(autoBounds ? dataBounds : boundsOverride); // should come from bounds prop
const buttonComponent = (
<EuiButtonIcon
iconType="gear"
color="text"
aria-label={intl.formatMessage({
id: 'xpack.infra.legendControls.buttonLabel',
defaultMessage: 'configure legend',
})}
onClick={() => setPopoverState(true)}
/>
);
export const LegendControls = ({ autoBounds, boundsOverride, onChange, dataBounds }: Props) => {
const [isPopoverOpen, setPopoverState] = useState(false);
const [draftAuto, setDraftAuto] = useState(autoBounds);
const [draftBounds, setDraftBounds] = useState(autoBounds ? dataBounds : boundsOverride); // should come from bounds prop
const buttonComponent = (
<EuiButtonIcon
iconType="gear"
color="text"
aria-label={i18n.translate('xpack.infra.legendControls.buttonLabel', {
defaultMessage: 'configure legend',
})}
onClick={() => setPopoverState(true)}
/>
);
const handleAutoChange = (e: SyntheticEvent<HTMLInputElement>) => {
setDraftAuto(e.currentTarget.checked);
};
const handleAutoChange = (e: SyntheticEvent<HTMLInputElement>) => {
setDraftAuto(e.currentTarget.checked);
};
const createBoundsHandler = (name: string) => (e: SyntheticEvent<HTMLInputElement>) => {
const value = parseFloat(e.currentTarget.value);
setDraftBounds({ ...draftBounds, [name]: value });
};
const createBoundsHandler = (name: string) => (e: SyntheticEvent<HTMLInputElement>) => {
const value = parseFloat(e.currentTarget.value);
setDraftBounds({ ...draftBounds, [name]: value });
};
const handlePopoverClose = () => {
setPopoverState(false);
};
const handlePopoverClose = () => {
setPopoverState(false);
};
const handleApplyClick = () => {
onChange({ auto: draftAuto, bounds: draftBounds });
};
const handleApplyClick = () => {
onChange({ auto: draftAuto, bounds: draftBounds });
};
const commited =
draftAuto === autoBounds &&
boundsOverride.min === draftBounds.min &&
boundsOverride.max === draftBounds.max;
const commited =
draftAuto === autoBounds &&
boundsOverride.min === draftBounds.min &&
boundsOverride.max === draftBounds.max;
const boundsValidRange = draftBounds.min < draftBounds.max;
const boundsValidRange = draftBounds.min < draftBounds.max;
return (
<ControlContainer>
<EuiPopover
isOpen={isPopoverOpen}
closePopover={handlePopoverClose}
id="legendControls"
button={buttonComponent}
withTitle
>
<EuiPopoverTitle>Legend Options</EuiPopoverTitle>
<EuiForm>
<EuiFormRow>
<EuiSwitch
name="bounds"
label={intl.formatMessage({
id: 'xpack.infra.legendControls.switchLabel',
defaultMessage: 'Auto calculate range',
return (
<ControlContainer>
<EuiPopover
isOpen={isPopoverOpen}
closePopover={handlePopoverClose}
id="legendControls"
button={buttonComponent}
withTitle
>
<EuiPopoverTitle>Legend Options</EuiPopoverTitle>
<EuiForm>
<EuiFormRow>
<EuiSwitch
name="bounds"
label={i18n.translate('xpack.infra.legendControls.switchLabel', {
defaultMessage: 'Auto calculate range',
})}
checked={draftAuto}
onChange={handleAutoChange}
/>
</EuiFormRow>
<EuiSpacer />
{(!boundsValidRange && (
<EuiText color="danger" grow={false} size="s">
<p>
<FormattedMessage
id="xpack.infra.legendControls.errorMessage"
defaultMessage="Min should be less than max"
/>
</p>
</EuiText>
)) ||
null}
<EuiFlexGroup style={{ marginTop: 0 }}>
<EuiFlexItem>
<EuiFormRow
label={i18n.translate('xpack.infra.legendControls.minLabel', {
defaultMessage: 'Min',
})}
checked={draftAuto}
onChange={handleAutoChange}
/>
</EuiFormRow>
<EuiSpacer />
{(!boundsValidRange && (
<EuiText color="danger" grow={false} size="s">
<p>
<FormattedMessage
id="xpack.infra.legendControls.errorMessage"
defaultMessage="Min should be less than max"
/>
</p>
</EuiText>
)) ||
null}
<EuiFlexGroup style={{ marginTop: 0 }}>
<EuiFlexItem>
<EuiFormRow
label={intl.formatMessage({
id: 'xpack.infra.legendControls.minLabel',
defaultMessage: 'Min',
})}
isInvalid={!boundsValidRange}
>
<EuiFieldNumber
disabled={draftAuto}
step={0.1}
value={isNaN(draftBounds.min) ? '' : draftBounds.min}
isInvalid={!boundsValidRange}
>
<EuiFieldNumber
disabled={draftAuto}
step={0.1}
value={isNaN(draftBounds.min) ? '' : draftBounds.min}
isInvalid={!boundsValidRange}
name="legendMin"
onChange={createBoundsHandler('min')}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label={intl.formatMessage({
id: 'xpack.infra.legendControls.maxLabel',
defaultMessage: 'Max',
})}
name="legendMin"
onChange={createBoundsHandler('min')}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label={i18n.translate('xpack.infra.legendControls.maxLabel', {
defaultMessage: 'Max',
})}
isInvalid={!boundsValidRange}
>
<EuiFieldNumber
disabled={draftAuto}
step={0.1}
isInvalid={!boundsValidRange}
>
<EuiFieldNumber
disabled={draftAuto}
step={0.1}
isInvalid={!boundsValidRange}
value={isNaN(draftBounds.max) ? '' : draftBounds.max}
name="legendMax"
onChange={createBoundsHandler('max')}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiButton
type="submit"
size="s"
fill
disabled={commited || !boundsValidRange}
onClick={handleApplyClick}
>
<FormattedMessage
id="xpack.infra.legendControls.applyButton"
defaultMessage="Apply"
/>
</EuiButton>
</EuiForm>
</EuiPopover>
</ControlContainer>
);
}
);
value={isNaN(draftBounds.max) ? '' : draftBounds.max}
name="legendMax"
onChange={createBoundsHandler('max')}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiButton
type="submit"
size="s"
fill
disabled={commited || !boundsValidRange}
onClick={handleApplyClick}
>
<FormattedMessage id="xpack.infra.legendControls.applyButton" defaultMessage="Apply" />
</EuiButton>
</EuiForm>
</EuiPopover>
</ControlContainer>
);
};
const ControlContainer = euiStyled.div`
position: absolute;

View file

@ -4,44 +4,36 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { InjectedIntl } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
interface Lookup {
[id: string]: string;
}
export const fieldToName = (field: string, intl: InjectedIntl) => {
export const fieldToName = (field: string) => {
const LOOKUP: Lookup = {
'kubernetes.namespace': intl.formatMessage({
id: 'xpack.infra.groupByDisplayNames.kubernetesNamespace',
'kubernetes.namespace': i18n.translate('xpack.infra.groupByDisplayNames.kubernetesNamespace', {
defaultMessage: 'Namespace',
}),
'kubernetes.node.name': intl.formatMessage({
id: 'xpack.infra.groupByDisplayNames.kubernetesNodeName',
'kubernetes.node.name': i18n.translate('xpack.infra.groupByDisplayNames.kubernetesNodeName', {
defaultMessage: 'Node',
}),
'host.name': intl.formatMessage({
id: 'xpack.infra.groupByDisplayNames.hostName',
'host.name': i18n.translate('xpack.infra.groupByDisplayNames.hostName', {
defaultMessage: 'Host',
}),
'cloud.availability_zone': intl.formatMessage({
id: 'xpack.infra.groupByDisplayNames.availabilityZone',
'cloud.availability_zone': i18n.translate('xpack.infra.groupByDisplayNames.availabilityZone', {
defaultMessage: 'Availability zone',
}),
'cloud.machine.type': intl.formatMessage({
id: 'xpack.infra.groupByDisplayNames.machineType',
'cloud.machine.type': i18n.translate('xpack.infra.groupByDisplayNames.machineType', {
defaultMessage: 'Machine type',
}),
'cloud.project.id': intl.formatMessage({
id: 'xpack.infra.groupByDisplayNames.projectID',
'cloud.project.id': i18n.translate('xpack.infra.groupByDisplayNames.projectID', {
defaultMessage: 'Project ID',
}),
'cloud.provider': intl.formatMessage({
id: 'xpack.infra.groupByDisplayNames.provider',
'cloud.provider': i18n.translate('xpack.infra.groupByDisplayNames.provider', {
defaultMessage: 'Cloud provider',
}),
'service.type': intl.formatMessage({
id: 'xpack.infra.groupByDisplayNames.serviceType',
'service.type': i18n.translate('xpack.infra.groupByDisplayNames.serviceType', {
defaultMessage: 'Service type',
}),
};

View file

@ -8,7 +8,8 @@ import moment from 'moment';
import { darken, readableColor } from 'polished';
import React from 'react';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { ConditionalToolTip } from './conditional_tooltip';
import euiStyled from '../../../../../common/eui_styled_components';
import { InfraTimerangeInput, InfraNodeType } from '../../graphql/types';
@ -30,97 +31,82 @@ interface Props {
bounds: InfraWaffleMapBounds;
nodeType: InfraNodeType;
timeRange: InfraTimerangeInput;
intl: InjectedIntl;
}
export const Node = injectI18n(
class extends React.PureComponent<Props, State> {
public readonly state: State = initialState;
public render() {
const {
nodeType,
node,
options,
squareSize,
bounds,
formatter,
timeRange,
intl,
} = this.props;
const { isPopoverOpen } = this.state;
const { metric } = node;
const valueMode = squareSize > 70;
const ellipsisMode = squareSize > 30;
const rawValue = (metric && metric.value) || 0;
const color = colorFromValue(options.legend, rawValue, bounds);
const value = formatter(rawValue);
const newTimerange = {
...timeRange,
from: moment(timeRange.to)
.subtract(1, 'hour')
.valueOf(),
};
const nodeAriaLabel = intl.formatMessage(
{
id: 'xpack.infra.node.ariaLabel',
defaultMessage: '{nodeName}, click to open menu',
},
{ nodeName: node.name }
);
return (
<NodeContextMenu
node={node}
nodeType={nodeType}
isPopoverOpen={isPopoverOpen}
closePopover={this.closePopover}
options={options}
timeRange={newTimerange}
popoverPosition="downCenter"
export const Node = class extends React.PureComponent<Props, State> {
public readonly state: State = initialState;
public render() {
const { nodeType, node, options, squareSize, bounds, formatter, timeRange } = this.props;
const { isPopoverOpen } = this.state;
const { metric } = node;
const valueMode = squareSize > 70;
const ellipsisMode = squareSize > 30;
const rawValue = (metric && metric.value) || 0;
const color = colorFromValue(options.legend, rawValue, bounds);
const value = formatter(rawValue);
const newTimerange = {
...timeRange,
from: moment(timeRange.to)
.subtract(1, 'hour')
.valueOf(),
};
const nodeAriaLabel = i18n.translate('xpack.infra.node.ariaLabel', {
defaultMessage: '{nodeName}, click to open menu',
values: { nodeName: node.name },
});
return (
<NodeContextMenu
node={node}
nodeType={nodeType}
isPopoverOpen={isPopoverOpen}
closePopover={this.closePopover}
options={options}
timeRange={newTimerange}
popoverPosition="downCenter"
>
<ConditionalToolTip
delay="regular"
hidden={isPopoverOpen}
position="top"
content={`${node.name} | ${value}`}
>
<ConditionalToolTip
delay="regular"
hidden={isPopoverOpen}
position="top"
content={`${node.name} | ${value}`}
<NodeContainer
data-test-subj="nodeContainer"
style={{ width: squareSize || 0, height: squareSize || 0 }}
onClick={this.togglePopover}
>
<NodeContainer
data-test-subj="nodeContainer"
style={{ width: squareSize || 0, height: squareSize || 0 }}
onClick={this.togglePopover}
>
<SquareOuter color={color}>
<SquareInner color={color}>
{valueMode ? (
<SquareOuter color={color}>
<SquareInner color={color}>
{valueMode ? (
<ValueInner aria-label={nodeAriaLabel}>
<Label color={color}>{node.name}</Label>
<Value color={color}>{value}</Value>
</ValueInner>
) : (
ellipsisMode && (
<ValueInner aria-label={nodeAriaLabel}>
<Label color={color}>{node.name}</Label>
<Value color={color}>{value}</Value>
<Label color={color}>...</Label>
</ValueInner>
) : (
ellipsisMode && (
<ValueInner aria-label={nodeAriaLabel}>
<Label color={color}>...</Label>
</ValueInner>
)
)}
</SquareInner>
</SquareOuter>
</NodeContainer>
</ConditionalToolTip>
</NodeContextMenu>
);
}
private togglePopover = () => {
this.setState(prevState => ({ isPopoverOpen: !prevState.isPopoverOpen }));
};
private closePopover = () => {
if (this.state.isPopoverOpen) {
this.setState({ isPopoverOpen: false });
}
};
)
)}
</SquareInner>
</SquareOuter>
</NodeContainer>
</ConditionalToolTip>
</NodeContextMenu>
);
}
);
private togglePopover = () => {
this.setState(prevState => ({ isPopoverOpen: !prevState.isPopoverOpen }));
};
private closePopover = () => {
if (this.state.isPopoverOpen) {
this.setState({ isPopoverOpen: false });
}
};
};
const NodeContainer = euiStyled.div`
position: relative;

View file

@ -10,7 +10,8 @@ import {
EuiPopover,
EuiPopoverProps,
} from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { UICapabilities } from 'ui/capabilities';
import { injectUICapabilities } from 'ui/capabilities/react';
@ -27,114 +28,103 @@ interface Props {
nodeType: InfraNodeType;
isPopoverOpen: boolean;
closePopover: () => void;
intl: InjectedIntl;
uiCapabilities: UICapabilities;
popoverPosition: EuiPopoverProps['anchorPosition'];
}
export const NodeContextMenu = injectUICapabilities(
injectI18n(
({
options,
timeRange,
children,
node,
isPopoverOpen,
closePopover,
nodeType,
intl,
uiCapabilities,
popoverPosition,
}: Props) => {
// Due to the changing nature of the fields between APM and this UI,
// We need to have some exceptions until 7.0 & ECS is finalized. Reference
// #26620 for the details for these fields.
// TODO: This is tech debt, remove it after 7.0 & ECS migration.
const APM_FIELDS = {
[InfraNodeType.host]: 'host.hostname',
[InfraNodeType.container]: 'container.id',
[InfraNodeType.pod]: 'kubernetes.pod.uid',
};
({
options,
timeRange,
children,
node,
isPopoverOpen,
closePopover,
nodeType,
const nodeLogsMenuItem = {
name: intl.formatMessage({
id: 'xpack.infra.nodeContextMenu.viewLogsName',
defaultMessage: 'View logs',
}),
href: getNodeLogsUrl({
nodeType,
nodeId: node.id,
time: timeRange.to,
}),
'data-test-subj': 'viewLogsContextMenuItem',
};
uiCapabilities,
popoverPosition,
}: Props) => {
// Due to the changing nature of the fields between APM and this UI,
// We need to have some exceptions until 7.0 & ECS is finalized. Reference
// #26620 for the details for these fields.
// TODO: This is tech debt, remove it after 7.0 & ECS migration.
const APM_FIELDS = {
[InfraNodeType.host]: 'host.hostname',
[InfraNodeType.container]: 'container.id',
[InfraNodeType.pod]: 'kubernetes.pod.uid',
};
const nodeDetailMenuItem = {
name: intl.formatMessage({
id: 'xpack.infra.nodeContextMenu.viewMetricsName',
defaultMessage: 'View metrics',
}),
href: getNodeDetailUrl({
nodeType,
nodeId: node.id,
from: timeRange.from,
to: timeRange.to,
}),
};
const nodeLogsMenuItem = {
name: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', {
defaultMessage: 'View logs',
}),
href: getNodeLogsUrl({
nodeType,
nodeId: node.id,
time: timeRange.to,
}),
'data-test-subj': 'viewLogsContextMenuItem',
};
const apmTracesMenuItem = {
name: intl.formatMessage(
{
id: 'xpack.infra.nodeContextMenu.viewAPMTraces',
defaultMessage: 'View {nodeType} APM traces',
},
{ nodeType }
),
href: `../app/apm#/traces?_g=()&kuery=${APM_FIELDS[nodeType]}:"${node.id}"`,
'data-test-subj': 'viewApmTracesContextMenuItem',
};
const nodeDetailMenuItem = {
name: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', {
defaultMessage: 'View metrics',
}),
href: getNodeDetailUrl({
nodeType,
nodeId: node.id,
from: timeRange.from,
to: timeRange.to,
}),
};
const uptimeMenuItem = {
name: intl.formatMessage(
{
id: 'xpack.infra.nodeContextMenu.viewUptimeLink',
defaultMessage: 'View {nodeType} in Uptime',
},
{ nodeType }
),
href: createUptimeLink(options, nodeType, node),
};
const apmTracesMenuItem = {
name: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', {
defaultMessage: 'View {nodeType} APM traces',
values: { nodeType },
}),
href: `../app/apm#/traces?_g=()&kuery=${APM_FIELDS[nodeType]}:"${node.id}"`,
'data-test-subj': 'viewApmTracesContextMenuItem',
};
const showLogsLink = node.id && uiCapabilities.logs.show;
const showAPMTraceLink = uiCapabilities.apm && uiCapabilities.apm.show;
const showUptimeLink =
[InfraNodeType.pod, InfraNodeType.container].includes(nodeType) || node.ip;
const uptimeMenuItem = {
name: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', {
defaultMessage: 'View {nodeType} in Uptime',
values: { nodeType },
}),
href: createUptimeLink(options, nodeType, node),
};
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: '',
items: [
...(showLogsLink ? [nodeLogsMenuItem] : []),
nodeDetailMenuItem,
...(showAPMTraceLink ? [apmTracesMenuItem] : []),
...(showUptimeLink ? [uptimeMenuItem] : []),
],
},
];
const showLogsLink = node.id && uiCapabilities.logs.show;
const showAPMTraceLink = uiCapabilities.apm && uiCapabilities.apm.show;
const showUptimeLink =
[InfraNodeType.pod, InfraNodeType.container].includes(nodeType) || node.ip;
return (
<EuiPopover
closePopover={closePopover}
id={`${node.pathId}-popover`}
isOpen={isPopoverOpen}
button={children}
panelPaddingSize="none"
anchorPosition={popoverPosition}
>
<EuiContextMenu initialPanelId={0} panels={panels} data-test-subj="nodeContextMenu" />
</EuiPopover>
);
}
)
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: '',
items: [
...(showLogsLink ? [nodeLogsMenuItem] : []),
nodeDetailMenuItem,
...(showAPMTraceLink ? [apmTracesMenuItem] : []),
...(showUptimeLink ? [uptimeMenuItem] : []),
],
},
];
return (
<EuiPopover
closePopover={closePopover}
id={`${node.pathId}-popover`}
isOpen={isPopoverOpen}
button={children}
panelPaddingSize="none"
anchorPosition={popoverPosition}
>
<EuiContextMenu initialPanelId={0} panels={panels} data-test-subj="nodeContextMenu" />
</EuiPopover>
);
}
);

View file

@ -5,29 +5,27 @@
*/
import { EuiButtonGroup, EuiButtonGroupProps } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
interface Props {
view: string;
onChange: EuiButtonGroupProps['onChange'];
intl: InjectedIntl;
}
export const ViewSwitcher = injectI18n(({ view, onChange, intl }: Props) => {
export const ViewSwitcher = ({ view, onChange }: Props) => {
const buttons = [
{
id: 'map',
label: intl.formatMessage({
id: 'xpack.infra.viewSwitcher.mapViewLabel',
label: i18n.translate('xpack.infra.viewSwitcher.mapViewLabel', {
defaultMessage: 'Map view',
}),
iconType: 'apps',
},
{
id: 'table',
label: intl.formatMessage({
id: 'xpack.infra.viewSwitcher.tableViewLabel',
label: i18n.translate('xpack.infra.viewSwitcher.tableViewLabel', {
defaultMessage: 'Table view',
}),
iconType: 'editorUnorderedList',
@ -35,8 +33,7 @@ export const ViewSwitcher = injectI18n(({ view, onChange, intl }: Props) => {
];
return (
<EuiButtonGroup
legend={intl.formatMessage({
id: 'xpack.infra.viewSwitcher.lenged',
legend={i18n.translate('xpack.infra.viewSwitcher.lenged', {
defaultMessage: 'Switch between table and map view',
})}
options={buttons}
@ -45,4 +42,4 @@ export const ViewSwitcher = injectI18n(({ view, onChange, intl }: Props) => {
onChange={onChange}
/>
);
});
};

View file

@ -13,7 +13,8 @@ import {
EuiFilterGroup,
EuiPopover,
} from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { FieldType } from 'ui/index_patterns';
import { InfraNodeType, InfraSnapshotGroupbyInput } from '../../graphql/types';
@ -28,22 +29,18 @@ interface Props {
onChange: (groupBy: InfraSnapshotGroupbyInput[]) => void;
onChangeCustomOptions: (options: InfraGroupByOptions[]) => void;
fields: FieldType[];
intl: InjectedIntl;
customOptions: InfraGroupByOptions[];
}
const createFieldToOptionMapper = (intl: InjectedIntl) => (field: string) => ({
text: fieldToName(field, intl),
field,
});
let OPTIONS: { [P in InfraNodeType]: InfraGroupByOptions[] };
const getOptions = (
nodeType: InfraNodeType,
intl: InjectedIntl
nodeType: InfraNodeType
): Array<{ text: string; field: string; toolTipContent?: string }> => {
if (!OPTIONS) {
const mapFieldToOption = createFieldToOptionMapper(intl);
const mapFieldToOption = (field: string) => ({
text: fieldToName(field),
field,
});
OPTIONS = {
[InfraNodeType.pod]: ['kubernetes.namespace', 'kubernetes.node.name', 'service.type'].map(
mapFieldToOption
@ -75,151 +72,138 @@ const initialState = {
type State = Readonly<typeof initialState>;
export const WaffleGroupByControls = injectI18n(
class extends React.PureComponent<Props, State> {
public static displayName = 'WaffleGroupByControls';
public readonly state: State = initialState;
export const WaffleGroupByControls = class extends React.PureComponent<Props, State> {
public static displayName = 'WaffleGroupByControls';
public readonly state: State = initialState;
public render() {
const { nodeType, groupBy, intl } = this.props;
const customOptions = this.props.customOptions.map(option => ({
...option,
toolTipContent: option.text,
}));
const options = getOptions(nodeType, intl).concat(customOptions);
public render() {
const { nodeType, groupBy } = this.props;
const customOptions = this.props.customOptions.map(option => ({
...option,
toolTipContent: option.text,
}));
const options = getOptions(nodeType).concat(customOptions);
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: [
{
name: intl.formatMessage({
id: 'xpack.infra.waffle.customGroupByOptionName',
defaultMessage: 'Custom field',
}),
icon: 'empty',
panel: 'customPanel',
},
...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,
} as EuiContextMenuPanelItemDescriptor;
if (o.toolTipContent) {
panel.toolTipContent = o.toolTipContent;
}
return panel;
}),
],
},
{
id: 'customPanel',
title: intl.formatMessage({
id: 'xpack.infra.waffle.customGroupByPanelTitle',
defaultMessage: 'Group By Custom Field',
}),
content: (
<CustomFieldPanel onSubmit={this.handleCustomField} fields={this.props.fields} />
),
},
];
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
// too stupid to realize that the filter above prevents the next map from being null
.map(o => <EuiBadge key={o && o.field}>{o && o.text}</EuiBadge>)
) : (
<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}
>
<StyledContextMenu initialPanelId="firstPanel" panels={panels} />
</EuiPopover>
</EuiFilterGroup>
if (!options.length) {
throw Error(
i18n.translate('xpack.infra.waffle.unableToSelectGroupErrorMessage', {
defaultMessage: 'Unable to select group by options for {nodeType}',
values: {
nodeType,
},
})
);
}
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 'firstPanel',
title: i18n.translate('xpack.infra.waffle.selectTwoGroupingsTitle', {
defaultMessage: 'Select up to two groupings',
}),
items: [
{
name: i18n.translate('xpack.infra.waffle.customGroupByOptionName', {
defaultMessage: 'Custom field',
}),
icon: 'empty',
panel: 'customPanel',
},
...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,
} as EuiContextMenuPanelItemDescriptor;
if (o.toolTipContent) {
panel.toolTipContent = o.toolTipContent;
}
return panel;
}),
],
},
{
id: 'customPanel',
title: i18n.translate('xpack.infra.waffle.customGroupByPanelTitle', {
defaultMessage: 'Group By Custom Field',
}),
content: <CustomFieldPanel onSubmit={this.handleCustomField} fields={this.props.fields} />,
},
];
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
// too stupid to realize that the filter above prevents the next map from being null
.map(o => <EuiBadge key={o && o.field}>{o && o.text}</EuiBadge>)
) : (
<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>
);
private handleRemove = (field: string) => () => {
const { groupBy } = this.props;
this.props.onChange(groupBy.filter(g => g.field !== field));
const options = this.props.customOptions.filter(g => g.field !== field);
this.props.onChangeCustomOptions(options);
// 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 handleCustomField = (field: string) => {
const options = [
...this.props.customOptions,
{
text: field,
field,
},
];
this.props.onChangeCustomOptions(options);
const fn = this.handleClick(field);
fn();
};
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, { field }]);
this.handleClose();
}
};
return (
<EuiFilterGroup>
<EuiPopover
isOpen={this.state.isPopoverOpen}
id="groupByPanel"
button={button}
panelPaddingSize="none"
closePopover={this.handleClose}
>
<StyledContextMenu initialPanelId="firstPanel" panels={panels} />
</EuiPopover>
</EuiFilterGroup>
);
}
);
private handleRemove = (field: string) => () => {
const { groupBy } = this.props;
this.props.onChange(groupBy.filter(g => g.field !== field));
const options = this.props.customOptions.filter(g => g.field !== field);
this.props.onChangeCustomOptions(options);
// 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 handleCustomField = (field: string) => {
const options = [
...this.props.customOptions,
{
text: field,
field,
},
];
this.props.onChangeCustomOptions(options);
const fn = this.handleClick(field);
fn();
};
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, { field }]);
this.handleClose();
}
};
};
const StyledContextMenu = euiStyled(EuiContextMenu)`
width: 320px;

View file

@ -11,7 +11,8 @@ import {
EuiFilterGroup,
EuiPopover,
} from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import {
@ -24,42 +25,34 @@ interface Props {
nodeType: InfraNodeType;
metric: InfraSnapshotMetricInput;
onChange: (metric: InfraSnapshotMetricInput) => void;
intl: InjectedIntl;
}
let OPTIONS: { [P in InfraNodeType]: Array<{ text: string; value: InfraSnapshotMetricType }> };
const getOptions = (
nodeType: InfraNodeType,
intl: InjectedIntl
nodeType: InfraNodeType
): Array<{ text: string; value: InfraSnapshotMetricType }> => {
if (!OPTIONS) {
const CPUUsage = intl.formatMessage({
id: 'xpack.infra.waffle.metricOptions.cpuUsageText',
const CPUUsage = i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', {
defaultMessage: 'CPU usage',
});
const MemoryUsage = intl.formatMessage({
id: 'xpack.infra.waffle.metricOptions.memoryUsageText',
const MemoryUsage = i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', {
defaultMessage: 'Memory usage',
});
const InboundTraffic = intl.formatMessage({
id: 'xpack.infra.waffle.metricOptions.inboundTrafficText',
const InboundTraffic = i18n.translate('xpack.infra.waffle.metricOptions.inboundTrafficText', {
defaultMessage: 'Inbound traffic',
});
const OutboundTraffic = intl.formatMessage({
id: 'xpack.infra.waffle.metricOptions.outboundTrafficText',
const OutboundTraffic = i18n.translate('xpack.infra.waffle.metricOptions.outboundTrafficText', {
defaultMessage: 'Outbound traffic',
});
const LogRate = intl.formatMessage({
id: 'xpack.infra.waffle.metricOptions.hostLogRateText',
const LogRate = i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', {
defaultMessage: 'Log rate',
});
const Load = intl.formatMessage({
id: 'xpack.infra.waffle.metricOptions.loadText',
const Load = i18n.translate('xpack.infra.waffle.metricOptions.loadText', {
defaultMessage: 'Load',
});
@ -137,73 +130,70 @@ const initialState = {
};
type State = Readonly<typeof initialState>;
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;
export const WaffleMetricControls = class extends React.PureComponent<Props, State> {
public static displayName = 'WaffleMetricControls';
public readonly state: State = initialState;
public render() {
const { metric, nodeType } = this.props;
const options = getOptions(nodeType);
const value = metric.type;
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>
if (!options.length || !value) {
throw Error(
i18n.translate('xpack.infra.waffle.unableToSelectMetricErrorTitle', {
defaultMessage: 'Unable to select options or value for metric.',
})
);
}
private handleClose = () => {
this.setState({ isPopoverOpen: false });
};
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>
);
private handleToggle = () => {
this.setState(state => ({ isPopoverOpen: !state.isPopoverOpen }));
};
private handleClick = (value: InfraSnapshotMetricType) => () => {
this.props.onChange({ type: value });
this.handleClose();
};
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: InfraSnapshotMetricType) => () => {
this.props.onChange({ type: value });
this.handleClose();
};
};

View file

@ -5,7 +5,8 @@
*/
import { EuiButtonGroup } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import {
InfraSnapshotMetricInput,
@ -15,22 +16,18 @@ import {
} from '../../graphql/types';
interface Props {
intl: InjectedIntl;
nodeType: InfraNodeType;
changeNodeType: (nodeType: InfraNodeType) => void;
changeGroupBy: (groupBy: InfraSnapshotGroupbyInput[]) => void;
changeMetric: (metric: InfraSnapshotMetricInput) => void;
}
export class WaffleNodeTypeSwitcherClass extends React.PureComponent<Props> {
export class WaffleNodeTypeSwitcher extends React.PureComponent<Props> {
public render() {
const { intl } = this.props;
const nodeOptions = [
{
id: InfraNodeType.host,
label: intl.formatMessage({
id: 'xpack.infra.waffle.nodeTypeSwitcher.hostsLabel',
label: i18n.translate('xpack.infra.waffle.nodeTypeSwitcher.hostsLabel', {
defaultMessage: 'Hosts',
}),
},
@ -62,5 +59,3 @@ export class WaffleNodeTypeSwitcherClass extends React.PureComponent<Props> {
this.props.changeMetric({ type: InfraSnapshotMetricType.cpu });
};
}
export const WaffleNodeTypeSwitcher = injectI18n(WaffleNodeTypeSwitcherClass);

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import { UICapabilities } from 'ui/capabilities';
@ -25,34 +26,31 @@ import { SettingsPage } from '../shared/settings';
import { AppNavigation } from '../../components/navigation/app_navigation';
interface InfrastructurePageProps extends RouteComponentProps {
intl: InjectedIntl;
uiCapabilities: UICapabilities;
}
export const InfrastructurePage = injectUICapabilities(
injectI18n(({ match, intl, uiCapabilities }: InfrastructurePageProps) => (
({ match, uiCapabilities }: InfrastructurePageProps) => (
<Source.Provider sourceId="default">
<ColumnarPage>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.homePage.documentTitle',
title={i18n.translate('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',
})}
feedbackLinkText={i18n.translate(
'xpack.infra.infrastructure.infrastructureHelpContent.feedbackLinkText',
{ defaultMessage: 'Provide feedback for Infrastructure' }
)}
/>
<Header
breadcrumbs={[
{
text: intl.formatMessage({
id: 'xpack.infra.header.infrastructureTitle',
text: i18n.translate('xpack.infra.header.infrastructureTitle', {
defaultMessage: 'Infrastructure',
}),
},
@ -64,22 +62,19 @@ export const InfrastructurePage = injectUICapabilities(
<RoutedTabs
tabs={[
{
title: intl.formatMessage({
id: 'xpack.infra.homePage.inventoryTabTitle',
title: i18n.translate('xpack.infra.homePage.inventoryTabTitle', {
defaultMessage: 'Inventory',
}),
path: `${match.path}/inventory`,
},
{
title: intl.formatMessage({
id: 'xpack.infra.homePage.metricsExplorerTabTitle',
title: i18n.translate('xpack.infra.homePage.metricsExplorerTabTitle', {
defaultMessage: 'Metrics Explorer',
}),
path: `${match.path}/metrics-explorer`,
},
{
title: intl.formatMessage({
id: 'xpack.infra.homePage.settingsTabTitle',
title: i18n.translate('xpack.infra.homePage.settingsTabTitle', {
defaultMessage: 'Settings',
}),
path: `${match.path}/settings`,
@ -111,5 +106,5 @@ export const InfrastructurePage = injectUICapabilities(
</Switch>
</ColumnarPage>
</Source.Provider>
))
)
);

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
import { DocumentTitle } from '../../../components/document_title';
@ -16,93 +17,84 @@ import { useMetricsExplorerState } from './use_metric_explorer_state';
import { useTrackPageview } from '../../../hooks/use_track_metric';
interface MetricsExplorerPageProps {
intl: InjectedIntl;
source: SourceQuery.Query['source']['configuration'] | undefined;
derivedIndexPattern: StaticIndexPattern;
}
export const MetricsExplorerPage = injectI18n(
({ intl, source, derivedIndexPattern }: MetricsExplorerPageProps) => {
if (!source) {
return null;
}
export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExplorerPageProps) => {
if (!source) {
return null;
}
const {
loading,
error,
data,
currentTimerange,
options,
chartOptions,
setChartOptions,
handleAggregationChange,
handleMetricsChange,
handleFilterQuerySubmit,
handleGroupByChange,
handleTimeChange,
handleRefresh,
handleLoadMore,
} = useMetricsExplorerState(source, derivedIndexPattern);
const {
loading,
error,
data,
currentTimerange,
options,
chartOptions,
setChartOptions,
handleAggregationChange,
handleMetricsChange,
handleFilterQuerySubmit,
handleGroupByChange,
handleTimeChange,
handleRefresh,
handleLoadMore,
} = useMetricsExplorerState(source, derivedIndexPattern);
useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer' });
useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 });
useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer' });
useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 });
return (
<React.Fragment>
<DocumentTitle
title={(previousTitle: string) =>
intl.formatMessage(
{
id: 'xpack.infra.infrastructureMetricsExplorerPage.documentTitle',
defaultMessage: '{previousTitle} | Metrics Explorer',
},
{
previousTitle,
}
)
}
return (
<React.Fragment>
<DocumentTitle
title={(previousTitle: string) =>
i18n.translate('xpack.infra.infrastructureMetricsExplorerPage.documentTitle', {
defaultMessage: '{previousTitle} | Metrics Explorer',
values: {
previousTitle,
},
})
}
/>
<MetricsExplorerToolbar
derivedIndexPattern={derivedIndexPattern}
timeRange={currentTimerange}
options={options}
chartOptions={chartOptions}
onRefresh={handleRefresh}
onTimeChange={handleTimeChange}
onGroupByChange={handleGroupByChange}
onFilterQuerySubmit={handleFilterQuerySubmit}
onMetricsChange={handleMetricsChange}
onAggregationChange={handleAggregationChange}
onChartOptionsChange={setChartOptions}
/>
{error ? (
<NoData
titleText="Whoops!"
bodyText={i18n.translate('xpack.infra.metricsExplorer.errorMessage', {
defaultMessage: 'It looks like the request failed with "{message}"',
values: { message: error.message },
})}
onRefetch={handleRefresh}
refetchText="Try Again"
/>
<MetricsExplorerToolbar
derivedIndexPattern={derivedIndexPattern}
) : (
<MetricsExplorerCharts
timeRange={currentTimerange}
loading={loading}
data={data}
source={source}
options={options}
chartOptions={chartOptions}
onRefresh={handleRefresh}
onLoadMore={handleLoadMore}
onFilter={handleFilterQuerySubmit}
onRefetch={handleRefresh}
onTimeChange={handleTimeChange}
onGroupByChange={handleGroupByChange}
onFilterQuerySubmit={handleFilterQuerySubmit}
onMetricsChange={handleMetricsChange}
onAggregationChange={handleAggregationChange}
onChartOptionsChange={setChartOptions}
/>
{error ? (
<NoData
titleText="Whoops!"
bodyText={intl.formatMessage(
{
id: 'xpack.infra.metricsExplorer.errorMessage',
defaultMessage: 'It looks like the request failed with "{message}"',
},
{ message: error.message }
)}
onRefetch={handleRefresh}
refetchText="Try Again"
/>
) : (
<MetricsExplorerCharts
timeRange={currentTimerange}
loading={loading}
data={data}
source={source}
options={options}
chartOptions={chartOptions}
onLoadMore={handleLoadMore}
onFilter={handleFilterQuerySubmit}
onRefetch={handleRefresh}
onTimeChange={handleTimeChange}
/>
)}
</React.Fragment>
);
}
);
)}
</React.Fragment>
);
};

View file

@ -5,7 +5,8 @@
*/
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { injectI18n, InjectedIntl } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React, { useContext } from 'react';
import { UICapabilities } from 'ui/capabilities';
import { injectUICapabilities } from 'ui/capabilities/react';
@ -30,100 +31,91 @@ import { WithKibanaChrome } from '../../../containers/with_kibana_chrome';
import { useTrackPageview } from '../../../hooks/use_track_metric';
interface SnapshotPageProps {
intl: InjectedIntl;
uiCapabilities: UICapabilities;
}
export const SnapshotPage = injectUICapabilities(
injectI18n((props: SnapshotPageProps) => {
const { intl, uiCapabilities } = props;
const {
createDerivedIndexPattern,
hasFailedLoadingSource,
isLoading,
loadSourceFailureMessage,
loadSource,
metricIndicesExist,
} = useContext(Source.Context);
export const SnapshotPage = injectUICapabilities((props: SnapshotPageProps) => {
const { uiCapabilities } = props;
const {
createDerivedIndexPattern,
hasFailedLoadingSource,
isLoading,
loadSourceFailureMessage,
loadSource,
metricIndicesExist,
} = useContext(Source.Context);
useTrackPageview({ app: 'infra_metrics', path: 'inventory' });
useTrackPageview({ app: 'infra_metrics', path: 'inventory', delay: 15000 });
useTrackPageview({ app: 'infra_metrics', path: 'inventory' });
useTrackPageview({ app: 'infra_metrics', path: 'inventory', delay: 15000 });
return (
<ColumnarPage>
<DocumentTitle
title={(previousTitle: string) =>
intl.formatMessage(
{
id: 'xpack.infra.infrastructureSnapshotPage.documentTitle',
defaultMessage: '{previousTitle} | Inventory',
},
{
previousTitle,
}
)
}
/>
{isLoading ? (
<SourceLoadingPage />
) : metricIndicesExist ? (
<>
<WithWaffleTimeUrlState />
<WithWaffleFilterUrlState indexPattern={createDerivedIndexPattern('metrics')} />
<WithWaffleOptionsUrlState />
<SnapshotToolbar />
<SnapshotPageContent />
</>
) : hasFailedLoadingSource ? (
<SourceErrorPage errorMessage={loadSourceFailureMessage || ''} retry={loadSource} />
) : (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
title={intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesTitle',
defaultMessage: "Looks like you don't have any metrics indices.",
})}
message={intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesDescription',
defaultMessage: "Let's add some!",
})}
actions={
<EuiFlexGroup>
return (
<ColumnarPage>
<DocumentTitle
title={(previousTitle: string) =>
i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', {
defaultMessage: '{previousTitle} | Inventory',
values: {
previousTitle,
},
})
}
/>
{isLoading ? (
<SourceLoadingPage />
) : metricIndicesExist ? (
<>
<WithWaffleTimeUrlState />
<WithWaffleFilterUrlState indexPattern={createDerivedIndexPattern('metrics')} />
<WithWaffleOptionsUrlState />
<SnapshotToolbar />
<SnapshotPageContent />
</>
) : hasFailedLoadingSource ? (
<SourceErrorPage errorMessage={loadSourceFailureMessage || ''} retry={loadSource} />
) : (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
title={i18n.translate('xpack.infra.homePage.noMetricsIndicesTitle', {
defaultMessage: "Looks like you don't have any metrics indices.",
})}
message={i18n.translate('xpack.infra.homePage.noMetricsIndicesDescription', {
defaultMessage: "Let's add some!",
})}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/metrics`}
color="primary"
fill
data-test-subj="infrastructureViewSetupInstructionsButton"
>
{i18n.translate(
'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel',
{ defaultMessage: 'View setup instructions' }
)}
</EuiButton>
</EuiFlexItem>
{uiCapabilities.infrastructure.configureSource ? (
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/metrics`}
color="primary"
fill
data-test-subj="infrastructureViewSetupInstructionsButton"
<ViewSourceConfigurationButton
data-test-subj="configureSourceButton"
hrefBase={ViewSourceConfigurationButtonHrefBase.infrastructure}
>
{intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel',
defaultMessage: 'View setup instructions',
{i18n.translate('xpack.infra.configureSourceActionLabel', {
defaultMessage: 'Change source configuration',
})}
</EuiButton>
</ViewSourceConfigurationButton>
</EuiFlexItem>
{uiCapabilities.infrastructure.configureSource ? (
<EuiFlexItem>
<ViewSourceConfigurationButton
data-test-subj="configureSourceButton"
hrefBase={ViewSourceConfigurationButtonHrefBase.infrastructure}
>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',
defaultMessage: 'Change source configuration',
})}
</ViewSourceConfigurationButton>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
}
data-test-subj="noMetricsIndicesPrompt"
/>
)}
</WithKibanaChrome>
)}
</ColumnarPage>
);
})
);
) : null}
</EuiFlexGroup>
}
data-test-subj="noMetricsIndicesPrompt"
/>
)}
</WithKibanaChrome>
)}
</ColumnarPage>
);
});

View file

@ -5,7 +5,7 @@
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { AutocompleteField } from '../../../components/autocomplete_field';
@ -20,7 +20,7 @@ import { WithWaffleTime } from '../../../containers/waffle/with_waffle_time';
import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion';
import { WithSource } from '../../../containers/with_source';
export const SnapshotToolbar = injectI18n(({ intl }) => (
export const SnapshotToolbar = () => (
<Toolbar>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="m">
<EuiFlexItem>
@ -41,10 +41,12 @@ export const SnapshotToolbar = injectI18n(({ intl }) => (
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={intl.formatMessage({
id: 'xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder',
defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)',
})}
placeholder={i18n.translate(
'xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder',
{
defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)',
}
)}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
autoFocus={true}
@ -117,4 +119,4 @@ export const SnapshotToolbar = injectI18n(({ intl }) => (
</WithSource>
</EuiFlexGroup>
</Toolbar>
));
);

View file

@ -6,7 +6,8 @@
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { replaceMetricTimeInQueryString } from '../../containers/metrics/with_metrics_time';
import { useHostIpToName } from './use_host_ip_to_name';
import { getFromFromLocation, getToFromLocation } from './query_params';
@ -18,58 +19,45 @@ type RedirectToHostDetailType = RouteComponentProps<{
hostIp: string;
}>;
interface RedirectToHostDetailProps extends RedirectToHostDetailType {
intl: InjectedIntl;
}
export const RedirectToHostDetailViaIP = ({
match: {
params: { hostIp },
},
location,
}: RedirectToHostDetailType) => {
const { source } = useSource({ sourceId: 'default' });
export const RedirectToHostDetailViaIP = injectI18n(
({
match: {
params: { hostIp },
},
location,
intl,
}: RedirectToHostDetailProps) => {
const { source } = useSource({ sourceId: 'default' });
const { error, name } = useHostIpToName(
hostIp,
(source && source.configuration && source.configuration.metricAlias) || null
);
if (error) {
return (
<Error
message={intl.formatMessage(
{
id: 'xpack.infra.linkTo.hostWithIp.error',
defaultMessage: 'Host not found with IP address "{hostIp}".',
},
{ hostIp }
)}
/>
);
}
const searchString = replaceMetricTimeInQueryString(
getFromFromLocation(location),
getToFromLocation(location)
)('');
if (name) {
return <Redirect to={`/infrastructure/metrics/host/${name}?${searchString}`} />;
}
const { error, name } = useHostIpToName(
hostIp,
(source && source.configuration && source.configuration.metricAlias) || null
);
if (error) {
return (
<LoadingPage
message={intl.formatMessage(
{
id: 'xpack.infra.linkTo.hostWithIp.loading',
defaultMessage: 'Loading host with IP address "{hostIp}".',
},
{ hostIp }
)}
<Error
message={i18n.translate('xpack.infra.linkTo.hostWithIp.error', {
defaultMessage: 'Host not found with IP address "{hostIp}".',
values: { hostIp },
})}
/>
);
}
);
const searchString = replaceMetricTimeInQueryString(
getFromFromLocation(location),
getToFromLocation(location)
)('');
if (name) {
return <Redirect to={`/metrics/host/${name}?${searchString}`} />;
}
return (
<LoadingPage
message={i18n.translate('xpack.infra.linkTo.hostWithIp.loading', {
defaultMessage: 'Loading host with IP address "{hostIp}".',
values: { hostIp },
})}
/>
);
};

View file

@ -7,15 +7,15 @@
import { createLocation } from 'history';
import React from 'react';
import { matchPath } from 'react-router-dom';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { shallow } from 'enzyme';
import { RedirectToLogs } from './redirect_to_logs';
describe('RedirectToLogs component', () => {
it('renders a redirect with the correct position', () => {
const component = shallowWithIntl(
const component = shallow(
<RedirectToLogs {...createRouteComponentProps('/logs?time=1550671089404')} />
).dive();
);
expect(component).toMatchInlineSnapshot(`
<Redirect
@ -26,11 +26,11 @@ describe('RedirectToLogs component', () => {
});
it('renders a redirect with the correct user-defined filter', () => {
const component = shallowWithIntl(
const component = shallow(
<RedirectToLogs
{...createRouteComponentProps('/logs?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE')}
/>
).dive();
);
expect(component).toMatchInlineSnapshot(`
<Redirect
@ -41,9 +41,9 @@ describe('RedirectToLogs component', () => {
});
it('renders a redirect with the correct custom source id', () => {
const component = shallowWithIntl(
const component = shallow(
<RedirectToLogs {...createRouteComponentProps('/SOME-OTHER-SOURCE/logs')} />
).dive();
);
expect(component).toMatchInlineSnapshot(`
<Redirect

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { injectI18n } from '@kbn/i18n/react';
import compose from 'lodash/fp/compose';
import React from 'react';
import { match as RouteMatch, Redirect, RouteComponentProps } from 'react-router-dom';
@ -22,7 +21,7 @@ interface RedirectToLogsProps extends RedirectToLogsType {
}>;
}
export const RedirectToLogs = injectI18n(({ location, match }: RedirectToLogsProps) => {
export const RedirectToLogs = ({ location, match }: RedirectToLogsProps) => {
const sourceId = match.params.sourceId || 'default';
const filter = getFilterFromLocation(location);
const searchString = compose(
@ -32,4 +31,4 @@ export const RedirectToLogs = injectI18n(({ location, match }: RedirectToLogsPro
)('');
return <Redirect to={`/logs/stream?${searchString}`} />;
});
};

View file

@ -7,7 +7,7 @@
import { createLocation } from 'history';
import React from 'react';
import { matchPath } from 'react-router-dom';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { shallow } from 'enzyme';
import { RedirectToNodeLogs } from './redirect_to_node_logs';
@ -29,9 +29,9 @@ jest.mock('../../containers/source/source', () => ({
describe('RedirectToNodeLogs component', () => {
it('renders a redirect with the correct host filter', () => {
const component = shallowWithIntl(
const component = shallow(
<RedirectToNodeLogs {...createRouteComponentProps('/host-logs/HOST_NAME')} />
).dive();
);
expect(component).toMatchInlineSnapshot(`
<Redirect
@ -42,9 +42,9 @@ describe('RedirectToNodeLogs component', () => {
});
it('renders a redirect with the correct container filter', () => {
const component = shallowWithIntl(
const component = shallow(
<RedirectToNodeLogs {...createRouteComponentProps('/container-logs/CONTAINER_ID')} />
).dive();
);
expect(component).toMatchInlineSnapshot(`
<Redirect
@ -55,9 +55,9 @@ describe('RedirectToNodeLogs component', () => {
});
it('renders a redirect with the correct pod filter', () => {
const component = shallowWithIntl(
const component = shallow(
<RedirectToNodeLogs {...createRouteComponentProps('/pod-logs/POD_ID')} />
).dive();
);
expect(component).toMatchInlineSnapshot(`
<Redirect
@ -68,11 +68,11 @@ describe('RedirectToNodeLogs component', () => {
});
it('renders a redirect with the correct position', () => {
const component = shallowWithIntl(
const component = shallow(
<RedirectToNodeLogs
{...createRouteComponentProps('/host-logs/HOST_NAME?time=1550671089404')}
/>
).dive();
);
expect(component).toMatchInlineSnapshot(`
<Redirect
@ -83,13 +83,13 @@ describe('RedirectToNodeLogs component', () => {
});
it('renders a redirect with the correct user-defined filter', () => {
const component = shallowWithIntl(
const component = shallow(
<RedirectToNodeLogs
{...createRouteComponentProps(
'/host-logs/HOST_NAME?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE'
)}
/>
).dive();
);
expect(component).toMatchInlineSnapshot(`
<Redirect
@ -100,11 +100,11 @@ describe('RedirectToNodeLogs component', () => {
});
it('renders a redirect with the correct custom source id', () => {
const component = shallowWithIntl(
const component = shallow(
<RedirectToNodeLogs
{...createRouteComponentProps('/SOME-OTHER-SOURCE/host-logs/HOST_NAME')}
/>
).dive();
);
expect(component).toMatchInlineSnapshot(`
<Redirect

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import compose from 'lodash/fp/compose';
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
@ -23,54 +24,44 @@ type RedirectToNodeLogsType = RouteComponentProps<{
sourceId?: string;
}>;
interface RedirectToNodeLogsProps extends RedirectToNodeLogsType {
intl: InjectedIntl;
}
export const RedirectToNodeLogs = ({
match: {
params: { nodeId, nodeType, sourceId = 'default' },
},
location,
}: RedirectToNodeLogsType) => {
const { source, isLoading } = useSource({ sourceId });
const configuration = source && source.configuration;
export const RedirectToNodeLogs = injectI18n(
({
match: {
params: { nodeId, nodeType, sourceId = 'default' },
},
location,
intl,
}: RedirectToNodeLogsProps) => {
const { source, isLoading } = useSource({ sourceId });
const configuration = source && source.configuration;
if (isLoading) {
return (
<LoadingPage
message={intl.formatMessage(
{
id: 'xpack.infra.redirectToNodeLogs.loadingNodeLogsMessage',
defaultMessage: 'Loading {nodeType} logs',
},
{
nodeType,
}
)}
/>
);
}
if (!configuration) {
return null;
}
const nodeFilter = `${configuration.fields[nodeType]}: ${nodeId}`;
const userFilter = getFilterFromLocation(location);
const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter;
const searchString = compose(
replaceLogFilterInQueryString(filter),
replaceLogPositionInQueryString(getTimeFromLocation(location)),
replaceSourceIdInQueryString(sourceId)
)('');
return <Redirect to={`/logs?${searchString}`} />;
if (isLoading) {
return (
<LoadingPage
message={i18n.translate('xpack.infra.redirectToNodeLogs.loadingNodeLogsMessage', {
defaultMessage: 'Loading {nodeType} logs',
values: {
nodeType,
},
})}
/>
);
}
);
if (!configuration) {
return null;
}
const nodeFilter = `${configuration.fields[nodeType]}: ${nodeId}`;
const userFilter = getFilterFromLocation(location);
const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter;
const searchString = compose(
replaceLogFilterInQueryString(filter),
replaceLogPositionInQueryString(getTimeFromLocation(location)),
replaceSourceIdInQueryString(sourceId)
)('');
return <Redirect to={`/logs?${searchString}`} />;
};
export const getNodeLogsUrl = ({
nodeId,

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiBetaBadge } from '@elastic/eui';
import React from 'react';
@ -31,133 +30,124 @@ import {
import { useSourceId } from '../../containers/source_id';
interface LogsPageProps extends RouteComponentProps {
intl: InjectedIntl;
uiCapabilities: UICapabilities;
}
export const LogsPage = injectUICapabilities(
injectI18n(({ match, intl, uiCapabilities }: LogsPageProps) => {
const [sourceId] = useSourceId();
const source = useSource({ sourceId });
const logAnalysisCapabilities = useLogAnalysisCapabilities();
export const LogsPage = injectUICapabilities(({ match, uiCapabilities }: LogsPageProps) => {
const [sourceId] = useSourceId();
const source = useSource({ sourceId });
const logAnalysisCapabilities = useLogAnalysisCapabilities();
const streamTab = {
title: intl.formatMessage({
id: 'xpack.infra.logs.index.streamTabTitle',
defaultMessage: 'Stream',
}),
path: `${match.path}/stream`,
};
const analysisBetaBadgeTitle = i18n.translate('xpack.infra.logs.index.analysisBetaBadgeTitle', {
defaultMessage: 'Analysis',
});
const analysisBetaBadgeLabel = i18n.translate('xpack.infra.logs.index.analysisBetaBadgeLabel', {
defaultMessage: 'Beta',
});
const analysisBetaBadgeTooltipContent = i18n.translate(
'xpack.infra.logs.index.analysisBetaBadgeTooltipContent',
{
defaultMessage:
'This feature is under active development. Extra functionality is coming, and some functionality may change.',
}
);
const analysisBetaBadge = (
<EuiBetaBadge
label={analysisBetaBadgeLabel}
aria-label={analysisBetaBadgeLabel}
title={analysisBetaBadgeTitle}
tooltipContent={analysisBetaBadgeTooltipContent}
/>
);
const analysisTab = {
title: (
<>
<span
style={{
display: 'inline-block',
position: 'relative',
top: '-4px',
marginRight: '5px',
}}
>
{intl.formatMessage({
id: 'xpack.infra.logs.index.analysisTabTitle',
defaultMessage: 'Analysis',
const streamTab = {
title: i18n.translate('xpack.infra.logs.index.streamTabTitle', { defaultMessage: 'Stream' }),
path: `${match.path}/stream`,
};
const analysisBetaBadgeTitle = i18n.translate('xpack.infra.logs.index.analysisBetaBadgeTitle', {
defaultMessage: 'Analysis',
});
const analysisBetaBadgeLabel = i18n.translate('xpack.infra.logs.index.analysisBetaBadgeLabel', {
defaultMessage: 'Beta',
});
const analysisBetaBadgeTooltipContent = i18n.translate(
'xpack.infra.logs.index.analysisBetaBadgeTooltipContent',
{
defaultMessage:
'This feature is under active development. Extra functionality is coming, and some functionality may change.',
}
);
const analysisBetaBadge = (
<EuiBetaBadge
label={analysisBetaBadgeLabel}
aria-label={analysisBetaBadgeLabel}
title={analysisBetaBadgeTitle}
tooltipContent={analysisBetaBadgeTooltipContent}
/>
);
const analysisTab = {
title: (
<>
<span
style={{
display: 'inline-block',
position: 'relative',
top: '-4px',
marginRight: '5px',
}}
>
{i18n.translate('xpack.infra.logs.index.analysisTabTitle', {
defaultMessage: 'Analysis',
})}
</span>
{analysisBetaBadge}
</>
),
path: `${match.path}/analysis`,
};
const settingsTab = {
title: i18n.translate('xpack.infra.logs.index.settingsTabTitle', {
defaultMessage: 'Settings',
}),
path: `${match.path}/settings`,
};
return (
<Source.Context.Provider value={source}>
<LogAnalysisCapabilities.Context.Provider value={logAnalysisCapabilities}>
<ColumnarPage>
<DocumentTitle
title={i18n.translate('xpack.infra.logs.index.documentTitle', {
defaultMessage: 'Logs',
})}
</span>
{analysisBetaBadge}
</>
),
path: `${match.path}/analysis`,
};
const settingsTab = {
title: intl.formatMessage({
id: 'xpack.infra.logs.index.settingsTabTitle',
defaultMessage: 'Settings',
}),
path: `${match.path}/settings`,
};
return (
<Source.Context.Provider value={source}>
<LogAnalysisCapabilities.Context.Provider value={logAnalysisCapabilities}>
<ColumnarPage>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.logs.index.documentTitle',
defaultMessage: 'Logs',
})}
/>
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/logs"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.logsPage.logsHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Logs',
})}
/>
<Header
breadcrumbs={[
{
text: i18n.translate('xpack.infra.header.logsTitle', {
defaultMessage: 'Logs',
}),
},
]}
readOnlyBadge={!uiCapabilities.logs.save}
/>
{source.isLoadingSource ||
(!source.isLoadingSource &&
!source.hasFailedLoadingSource &&
source.source === undefined) ? (
<SourceLoadingPage />
) : source.hasFailedLoadingSource ? (
<SourceErrorPage
errorMessage={source.loadSourceFailureMessage || ''}
retry={source.loadSource}
/>
) : (
<>
<AppNavigation>
<RoutedTabs
tabs={
logAnalysisCapabilities.hasLogAnalysisCapabilites
? [streamTab, analysisTab, settingsTab]
: [streamTab, settingsTab]
}
/>
</AppNavigation>
<Switch>
<Route path={`${match.path}/stream`} component={StreamPage} />
<Route path={`${match.path}/analysis`} component={AnalysisPage} />
<Route path={`${match.path}/settings`} component={SettingsPage} />
</Switch>
</>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/logs"
feedbackLinkText={i18n.translate(
'xpack.infra.logsPage.logsHelpContent.feedbackLinkText',
{ defaultMessage: 'Provide feedback for Logs' }
)}
</ColumnarPage>
</LogAnalysisCapabilities.Context.Provider>
</Source.Context.Provider>
);
})
);
/>
<Header
breadcrumbs={[
{
text: i18n.translate('xpack.infra.header.logsTitle', {
defaultMessage: 'Logs',
}),
},
]}
readOnlyBadge={!uiCapabilities.logs.save}
/>
{source.isLoadingSource ||
(!source.isLoadingSource &&
!source.hasFailedLoadingSource &&
source.source === undefined) ? (
<SourceLoadingPage />
) : source.hasFailedLoadingSource ? (
<SourceErrorPage
errorMessage={source.loadSourceFailureMessage || ''}
retry={source.loadSource}
/>
) : (
<>
<AppNavigation>
<RoutedTabs
tabs={
logAnalysisCapabilities.hasLogAnalysisCapabilites
? [streamTab, analysisTab, settingsTab]
: [streamTab, settingsTab]
}
/>
</AppNavigation>
<Switch>
<Route path={`${match.path}/stream`} component={StreamPage} />
<Route path={`${match.path}/analysis`} component={AnalysisPage} />
<Route path={`${match.path}/settings`} component={SettingsPage} />
</Switch>
</>
)}
</ColumnarPage>
</LogAnalysisCapabilities.Context.Provider>
</Source.Context.Provider>
);
});

View file

@ -5,7 +5,8 @@
*/
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { injectI18n, InjectedIntl } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { UICapabilities } from 'ui/capabilities';
@ -18,25 +19,22 @@ import {
} from '../../../components/source_configuration';
interface LogsPageNoIndicesContentProps {
intl: InjectedIntl;
uiCapabilities: UICapabilities;
}
export const LogsPageNoIndicesContent = injectUICapabilities(
injectI18n((props: LogsPageNoIndicesContentProps) => {
const { intl, uiCapabilities } = props;
(props: LogsPageNoIndicesContentProps) => {
const { uiCapabilities } = props;
return (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
data-test-subj="noLogsIndicesPrompt"
title={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesTitle',
title={i18n.translate('xpack.infra.logsPage.noLoggingIndicesTitle', {
defaultMessage: "Looks like you don't have any logging indices.",
})}
message={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesDescription',
message={i18n.translate('xpack.infra.logsPage.noLoggingIndicesDescription', {
defaultMessage: "Let's add some!",
})}
actions={
@ -48,10 +46,10 @@ export const LogsPageNoIndicesContent = injectUICapabilities(
fill
data-test-subj="logsViewSetupInstructionsButton"
>
{intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel',
defaultMessage: 'View setup instructions',
})}
{i18n.translate(
'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel',
{ defaultMessage: 'View setup instructions' }
)}
</EuiButton>
</EuiFlexItem>
{uiCapabilities.logs.configureSource ? (
@ -60,8 +58,7 @@ export const LogsPageNoIndicesContent = injectUICapabilities(
data-test-subj="configureSourceButton"
hrefBase={ViewSourceConfigurationButtonHrefBase.logs}
>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',
{i18n.translate('xpack.infra.configureSourceActionLabel', {
defaultMessage: 'Change source configuration',
})}
</ViewSourceConfigurationButton>
@ -73,5 +70,5 @@ export const LogsPageNoIndicesContent = injectUICapabilities(
)}
</WithKibanaChrome>
);
})
}
);

View file

@ -5,7 +5,7 @@
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React, { useContext } from 'react';
import { AutocompleteField } from '../../../components/autocomplete_field';
@ -24,7 +24,7 @@ import { WithLogPosition } from '../../../containers/logs/with_log_position';
import { Source } from '../../../containers/source';
import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion';
export const LogsToolbar = injectI18n(({ intl }) => {
export const LogsToolbar = () => {
const { createDerivedIndexPattern } = useContext(Source.Context);
const derivedIndexPattern = createDerivedIndexPattern('logs');
const {
@ -74,16 +74,16 @@ export const LogsToolbar = injectI18n(({ intl }) => {
setSurroundingLogsId(null);
applyFilterQueryFromKueryExpression(expression);
}}
placeholder={intl.formatMessage({
id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder',
defaultMessage: 'Search for log entries… (e.g. host.name:host-1)',
})}
placeholder={i18n.translate(
'xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder',
{ defaultMessage: 'Search for log entries… (e.g. host.name:host-1)' }
)}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
aria-label={intl.formatMessage({
id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldAriaLabel',
defaultMessage: 'Search for log entries',
})}
aria-label={i18n.translate(
'xpack.infra.logsPage.toolbar.kqlSearchFieldAriaLabel',
{ defaultMessage: 'Search for log entries' }
)}
/>
)}
</WithLogFilter>
@ -126,7 +126,6 @@ export const LogsToolbar = injectI18n(({ intl }) => {
jumpToTargetPositionTime,
startLiveStreaming,
stopLiveStreaming,
targetPosition,
}) => (
<LogTimeControls
currentTime={visibleMidpointTime}
@ -144,4 +143,4 @@ export const LogsToolbar = injectI18n(({ intl }) => {
</EuiFlexGroup>
</Toolbar>
);
});
};

View file

@ -13,7 +13,7 @@ import {
EuiPageHeaderSection,
EuiTitle,
} from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { GraphQLFormattedError } from 'graphql';
import React, { useCallback, useContext } from 'react';
import { UICapabilities } from 'ui/capabilities';
@ -60,209 +60,195 @@ interface Props {
node: string;
};
};
intl: InjectedIntl;
uiCapabilities: UICapabilities;
}
export const MetricDetail = withMetricPageProviders(
injectUICapabilities(
withTheme(
injectI18n(({ intl, uiCapabilities, match, theme }: Props) => {
const nodeId = match.params.node;
const nodeType = match.params.type as InfraNodeType;
const layoutCreator = layoutCreators[nodeType];
if (!layoutCreator) {
return (
<Error
message={intl.formatMessage(
{
id: 'xpack.infra.metricDetailPage.invalidNodeTypeErrorMessage',
defaultMessage: '{nodeType} is not a valid node type',
},
{
nodeType: `"${nodeType}"`,
}
)}
/>
);
}
const { sourceId } = useContext(Source.Context);
const layouts = layoutCreator(theme);
const { name, filteredLayouts, loading: metadataLoading, cloudId, metadata } = useMetadata(
nodeId,
nodeType,
layouts,
sourceId
);
const breadcrumbs = [
{
href: '#/',
text: intl.formatMessage({
id: 'xpack.infra.header.infrastructureTitle',
defaultMessage: 'Infrastructure',
}),
},
{ text: name },
];
const handleClick = useCallback(
(section: InfraMetricLayoutSection) => () => {
const id = section.linkToId || section.id;
const el = document.getElementById(id);
if (el) {
el.scrollIntoView();
}
},
[]
);
if (metadataLoading && !filteredLayouts.length) {
return (
<InfraLoadingPanel
height="100vh"
width="100%"
text={intl.formatMessage({
id: 'xpack.infra.metrics.loadingNodeDataText',
defaultMessage: 'Loading data',
})}
/>
);
}
withTheme(({ uiCapabilities, match, theme }: Props) => {
const nodeId = match.params.node;
const nodeType = match.params.type as InfraNodeType;
const layoutCreator = layoutCreators[nodeType];
if (!layoutCreator) {
return (
<WithMetricsTime>
{({
timeRange,
parsedTimeRange,
setTimeRange,
refreshInterval,
setRefreshInterval,
isAutoReloading,
setAutoReload,
triggerRefresh,
}) => (
<ColumnarPage>
<Header
breadcrumbs={breadcrumbs}
readOnlyBadge={!uiCapabilities.infrastructure.save}
/>
<WithMetricsTimeUrlState />
<DocumentTitle
title={intl.formatMessage(
{
id: 'xpack.infra.metricDetailPage.documentTitle',
defaultMessage: 'Infrastructure | Metrics | {name}',
},
{
name,
}
)}
/>
<DetailPageContent data-test-subj="infraMetricsPage">
<WithMetrics
layouts={filteredLayouts}
sourceId={sourceId}
timerange={parsedTimeRange}
nodeType={nodeType}
nodeId={nodeId}
cloudId={cloudId}
>
{({ metrics, error, loading, refetch }) => {
if (error) {
const invalidNodeError = error.graphQLErrors.some(
(err: GraphQLFormattedError) =>
err.code === InfraMetricsErrorCodes.invalid_node
);
return (
<>
<DocumentTitle
title={(previousTitle: string) =>
intl.formatMessage(
{
id: 'xpack.infra.metricDetailPage.documentTitleError',
defaultMessage: '{previousTitle} | Uh oh',
},
{
previousTitle,
}
)
}
/>
{invalidNodeError ? (
<InvalidNodeError nodeName={name} />
) : (
<ErrorPageBody message={error.message} />
)}
</>
);
}
return (
<EuiPage style={{ flex: '1 0 auto' }}>
<MetricsSideNav
layouts={filteredLayouts}
loading={metadataLoading}
nodeName={name}
handleClick={handleClick}
/>
<AutoSizer content={false} bounds detectAnyWindowResize>
{({ measureRef, bounds: { width = 0 } }) => {
const w = width ? `${width}px` : `100%`;
return (
<MetricsDetailsPageColumn innerRef={measureRef}>
<EuiPageBody style={{ width: w }}>
<EuiPageHeader style={{ flex: '0 0 auto' }}>
<EuiPageHeaderSection style={{ width: '100%' }}>
<MetricsTitleTimeRangeContainer>
<EuiHideFor sizes={['xs', 's']}>
<EuiTitle size="m">
<h1>{name}</h1>
</EuiTitle>
</EuiHideFor>
<MetricsTimeControls
currentTimeRange={timeRange}
isLiveStreaming={isAutoReloading}
refreshInterval={refreshInterval}
setRefreshInterval={setRefreshInterval}
onChangeTimeRange={setTimeRange}
setAutoReload={setAutoReload}
onRefresh={triggerRefresh}
/>
</MetricsTitleTimeRangeContainer>
</EuiPageHeaderSection>
</EuiPageHeader>
<NodeDetails metadata={metadata} />
<EuiPageContentWithRelative>
<Metrics
label={name}
nodeId={nodeId}
layouts={filteredLayouts}
metrics={metrics}
loading={
metrics.length > 0 && isAutoReloading ? false : loading
}
refetch={refetch}
onChangeRangeTime={setTimeRange}
isLiveStreaming={isAutoReloading}
stopLiveStreaming={() => setAutoReload(false)}
/>
</EuiPageContentWithRelative>
</EuiPageBody>
</MetricsDetailsPageColumn>
);
}}
</AutoSizer>
</EuiPage>
);
}}
</WithMetrics>
</DetailPageContent>
</ColumnarPage>
)}
</WithMetricsTime>
<Error
message={i18n.translate('xpack.infra.metricDetailPage.invalidNodeTypeErrorMessage', {
defaultMessage: '{nodeType} is not a valid node type',
values: {
nodeType: `"${nodeType}"`,
},
})}
/>
);
})
)
}
const { sourceId } = useContext(Source.Context);
const layouts = layoutCreator(theme);
const { name, filteredLayouts, loading: metadataLoading, cloudId, metadata } = useMetadata(
nodeId,
nodeType,
layouts,
sourceId
);
const breadcrumbs = [
{
href: '#/',
text: i18n.translate('xpack.infra.header.infrastructureTitle', {
defaultMessage: 'Infrastructure',
}),
},
{ text: name },
];
const handleClick = useCallback(
(section: InfraMetricLayoutSection) => () => {
const id = section.linkToId || section.id;
const el = document.getElementById(id);
if (el) {
el.scrollIntoView();
}
},
[]
);
if (metadataLoading && !filteredLayouts.length) {
return (
<InfraLoadingPanel
height="100vh"
width="100%"
text={i18n.translate('xpack.infra.metrics.loadingNodeDataText', {
defaultMessage: 'Loading data',
})}
/>
);
}
return (
<WithMetricsTime>
{({
timeRange,
parsedTimeRange,
setTimeRange,
refreshInterval,
setRefreshInterval,
isAutoReloading,
setAutoReload,
triggerRefresh,
}) => (
<ColumnarPage>
<Header
breadcrumbs={breadcrumbs}
readOnlyBadge={!uiCapabilities.infrastructure.save}
/>
<WithMetricsTimeUrlState />
<DocumentTitle
title={i18n.translate('xpack.infra.metricDetailPage.documentTitle', {
defaultMessage: 'Infrastructure | Metrics | {name}',
values: {
name,
},
})}
/>
<DetailPageContent data-test-subj="infraMetricsPage">
<WithMetrics
layouts={filteredLayouts}
sourceId={sourceId}
timerange={parsedTimeRange}
nodeType={nodeType}
nodeId={nodeId}
cloudId={cloudId}
>
{({ metrics, error, loading, refetch }) => {
if (error) {
const invalidNodeError = error.graphQLErrors.some(
(err: GraphQLFormattedError) =>
err.code === InfraMetricsErrorCodes.invalid_node
);
return (
<>
<DocumentTitle
title={(previousTitle: string) =>
i18n.translate('xpack.infra.metricDetailPage.documentTitleError', {
defaultMessage: '{previousTitle} | Uh oh',
values: {
previousTitle,
},
})
}
/>
{invalidNodeError ? (
<InvalidNodeError nodeName={name} />
) : (
<ErrorPageBody message={error.message} />
)}
</>
);
}
return (
<EuiPage style={{ flex: '1 0 auto' }}>
<MetricsSideNav
layouts={filteredLayouts}
loading={metadataLoading}
nodeName={name}
handleClick={handleClick}
/>
<AutoSizer content={false} bounds detectAnyWindowResize>
{({ measureRef, bounds: { width = 0 } }) => {
const w = width ? `${width}px` : `100%`;
return (
<MetricsDetailsPageColumn innerRef={measureRef}>
<EuiPageBody style={{ width: w }}>
<EuiPageHeader style={{ flex: '0 0 auto' }}>
<EuiPageHeaderSection style={{ width: '100%' }}>
<MetricsTitleTimeRangeContainer>
<EuiHideFor sizes={['xs', 's']}>
<EuiTitle size="m">
<h1>{name}</h1>
</EuiTitle>
</EuiHideFor>
<MetricsTimeControls
currentTimeRange={timeRange}
isLiveStreaming={isAutoReloading}
refreshInterval={refreshInterval}
setRefreshInterval={setRefreshInterval}
onChangeTimeRange={setTimeRange}
setAutoReload={setAutoReload}
onRefresh={triggerRefresh}
/>
</MetricsTitleTimeRangeContainer>
</EuiPageHeaderSection>
</EuiPageHeader>
<NodeDetails metadata={metadata} />
<EuiPageContentWithRelative>
<Metrics
label={name}
nodeId={nodeId}
layouts={filteredLayouts}
metrics={metrics}
loading={
metrics.length > 0 && isAutoReloading ? false : loading
}
refetch={refetch}
onChangeRangeTime={setTimeRange}
isLiveStreaming={isAutoReloading}
stopLiveStreaming={() => setAutoReload(false)}
/>
</EuiPageContentWithRelative>
</EuiPageBody>
</MetricsDetailsPageColumn>
);
}}
</AutoSizer>
</EuiPage>
);
}}
</WithMetrics>
</DetailPageContent>
</ColumnarPage>
)}
</WithMetricsTime>
);
})
)
);

View file

@ -4,117 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
/**
* Components using the react-intl module require access to the intl context.
* This is not available when mounting single components in Enzyme.
* These helper functions aim to address that and wrap a valid,
* intl context around them.
*/
import { I18nProvider, InjectedIntl, intlShape } from '@kbn/i18n/react';
import { mount, ReactWrapper, render, shallow } from 'enzyme';
import React, { ReactElement, ValidationMap } from 'react';
import { mount, ReactWrapper } from 'enzyme';
import React from 'react';
import { act as reactAct } from 'react-dom/test-utils';
// Use fake component to extract `intl` property to use in tests.
const { intl } = (mount(
<I18nProvider>
<br />
</I18nProvider>
).find('IntlProvider') as ReactWrapper<{}, {}, import('react-intl').IntlProvider>)
.instance()
.getChildContext();
function getOptions(context = {}, childContextTypes = {}, props = {}) {
return {
context: {
...context,
intl,
},
childContextTypes: {
...childContextTypes,
intl: intlShape,
},
...props,
};
}
/**
* When using React-Intl `injectIntl` on components, props.intl is required.
*/
function nodeWithIntlProp<T>(node: ReactElement<T>): ReactElement<T & { intl: InjectedIntl }> {
return React.cloneElement<any>(node, { intl });
}
/**
* Creates the wrapper instance using shallow with provided intl object into context
*
* @param node The React element or cheerio wrapper
* @param options properties to pass into shallow wrapper
* @return The wrapper instance around the rendered output with intl object in context
*/
export function shallowWithIntl<T>(
node: ReactElement<T>,
{
context,
childContextTypes,
...props
}: {
context?: any;
childContextTypes?: ValidationMap<any>;
} = {}
) {
const options = getOptions(context, childContextTypes, props);
return shallow(nodeWithIntlProp(node), options);
}
/**
* Creates the wrapper instance using mount with provided intl object into context
*
* @param node The React element or cheerio wrapper
* @param options properties to pass into mount wrapper
* @return The wrapper instance around the rendered output with intl object in context
*/
export function mountWithIntl<T>(
node: ReactElement<T>,
{
context,
childContextTypes,
...props
}: {
context?: any;
childContextTypes?: ValidationMap<any>;
} = {}
) {
const options = getOptions(context, childContextTypes, props);
return mount(nodeWithIntlProp(node), options);
}
/**
* Creates the wrapper instance using render with provided intl object into context
*
* @param node The React element or cheerio wrapper
* @param options properties to pass into render wrapper
* @return The wrapper instance around the rendered output with intl object in context
*/
export function renderWithIntl<T>(
node: ReactElement<T>,
{
context,
childContextTypes,
...props
}: {
context?: any;
childContextTypes?: ValidationMap<any>;
} = {}
) {
const options = getOptions(context, childContextTypes, props);
return render(nodeWithIntlProp(node), options);
}
/**
* A wrapper object to provide access to the state of a hook under test and to
* enable interaction with that hook.