mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* [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:
parent
e17c19e7bd
commit
96f84aef78
46 changed files with 2303 additions and 2628 deletions
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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(() => {
|
|||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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(() => {
|
|||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
))
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
));
|
||||
);
|
||||
|
|
|
@ -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 },
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}`} />;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
})
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue