[Metrics UI] Add inventory view timeline (#77804)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Zacqary Adam Xeper 2020-09-22 16:21:09 -05:00 committed by GitHub
parent 0f8bbf11f4
commit a0f03ddbd1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 568 additions and 188 deletions

View file

@ -99,7 +99,7 @@ export const SnapshotRequestRT = rt.intersection([
rt.type({
timerange: InfraTimerangeInputRT,
metrics: rt.array(SnapshotMetricInputRT),
groupBy: SnapshotGroupByRT,
groupBy: rt.union([SnapshotGroupByRT, rt.null]),
nodeType: ItemTypeRT,
sourceId: rt.string,
}),

View file

@ -5,36 +5,8 @@
*/
import { i18n } from '@kbn/i18n';
import { SnapshotMetricType } from './types';
export const CPUUsage = i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', {
defaultMessage: 'CPU usage',
});
export const MemoryUsage = i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', {
defaultMessage: 'Memory usage',
});
export const InboundTraffic = i18n.translate(
'xpack.infra.waffle.metricOptions.inboundTrafficText',
{
defaultMessage: 'Inbound traffic',
}
);
export const OutboundTraffic = i18n.translate(
'xpack.infra.waffle.metricOptions.outboundTrafficText',
{
defaultMessage: 'Outbound traffic',
}
);
export const LogRate = i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', {
defaultMessage: 'Log rate',
});
export const Load = i18n.translate('xpack.infra.waffle.metricOptions.loadText', {
defaultMessage: 'Load',
});
import { toMetricOpt } from '../snapshot_metric_i18n';
import { SnapshotMetricType, SnapshotMetricTypeKeys } from './types';
interface Lookup {
[id: string]: string;
@ -70,80 +42,9 @@ export const fieldToName = (field: string) => {
return LOOKUP[field] || field;
};
export const SNAPSHOT_METRIC_TRANSLATIONS = {
cpu: i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', {
defaultMessage: 'CPU usage',
}),
memory: i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', {
defaultMessage: 'Memory usage',
}),
rx: i18n.translate('xpack.infra.waffle.metricOptions.inboundTrafficText', {
defaultMessage: 'Inbound traffic',
}),
tx: i18n.translate('xpack.infra.waffle.metricOptions.outboundTrafficText', {
defaultMessage: 'Outbound traffic',
}),
logRate: i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', {
defaultMessage: 'Log rate',
}),
load: i18n.translate('xpack.infra.waffle.metricOptions.loadText', {
defaultMessage: 'Load',
}),
count: i18n.translate('xpack.infra.waffle.metricOptions.countText', {
defaultMessage: 'Count',
}),
diskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', {
defaultMessage: 'Disk Reads',
}),
diskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', {
defaultMessage: 'Disk Writes',
}),
s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', {
defaultMessage: 'Bucket Size',
}),
s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', {
defaultMessage: 'Total Requests',
}),
s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', {
defaultMessage: 'Number of Objects',
}),
s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', {
defaultMessage: 'Downloads (Bytes)',
}),
s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', {
defaultMessage: 'Uploads (Bytes)',
}),
rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', {
defaultMessage: 'Connections',
}),
rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', {
defaultMessage: 'Queries Executed',
}),
rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', {
defaultMessage: 'Active Transactions',
}),
rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', {
defaultMessage: 'Latency',
}),
sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', {
defaultMessage: 'Messages Available',
}),
sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', {
defaultMessage: 'Messages Delayed',
}),
sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', {
defaultMessage: 'Messages Added',
}),
sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', {
defaultMessage: 'Messages Returned Empty',
}),
sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', {
defaultMessage: 'Oldest Message',
}),
} as Record<SnapshotMetricType, string>;
const snapshotTypeKeys = Object.keys(SnapshotMetricTypeKeys) as SnapshotMetricType[];
export const SNAPSHOT_METRIC_TRANSLATIONS = snapshotTypeKeys.reduce((result, metric) => {
const text = toMetricOpt(metric)?.text;
if (text) return { ...result, [metric]: text };
return result;
}, {}) as Record<SnapshotMetricType, string>;

View file

@ -314,7 +314,7 @@ export const ESAggregationRT = rt.union([
export const MetricsUIAggregationRT = rt.record(rt.string, ESAggregationRT);
export type MetricsUIAggregation = rt.TypeOf<typeof MetricsUIAggregationRT>;
export const SnapshotMetricTypeRT = rt.keyof({
export const SnapshotMetricTypeKeys = {
count: null,
cpu: null,
load: null,
@ -339,7 +339,8 @@ export const SnapshotMetricTypeRT = rt.keyof({
sqsMessagesEmpty: null,
sqsOldestMessage: null,
custom: null,
});
};
export const SnapshotMetricTypeRT = rt.keyof(SnapshotMetricTypeKeys);
export type SnapshotMetricType = rt.TypeOf<typeof SnapshotMetricTypeRT>;

View file

@ -4,204 +4,235 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { mapValues } from 'lodash';
import { SnapshotMetricType } from './inventory_models/types';
const Translations = {
// Lowercase versions of all metrics, for when they need to be used in the middle of a sentence;
// these may need to be translated differently depending on language, e.g. still capitalizing "CPU"
const TranslationsLowercase = {
CPUUsage: i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', {
defaultMessage: 'CPU usage',
}),
MemoryUsage: i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', {
defaultMessage: 'Memory usage',
defaultMessage: 'memory usage',
}),
InboundTraffic: i18n.translate('xpack.infra.waffle.metricOptions.inboundTrafficText', {
defaultMessage: 'Inbound traffic',
defaultMessage: 'inbound traffic',
}),
OutboundTraffic: i18n.translate('xpack.infra.waffle.metricOptions.outboundTrafficText', {
defaultMessage: 'Outbound traffic',
defaultMessage: 'outbound traffic',
}),
LogRate: i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', {
defaultMessage: 'Log rate',
defaultMessage: 'log rate',
}),
Load: i18n.translate('xpack.infra.waffle.metricOptions.loadText', {
defaultMessage: 'Load',
defaultMessage: 'load',
}),
Count: i18n.translate('xpack.infra.waffle.metricOptions.countText', {
defaultMessage: 'Count',
defaultMessage: 'count',
}),
DiskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', {
defaultMessage: 'Disk Reads',
defaultMessage: 'disk reads',
}),
DiskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', {
defaultMessage: 'Disk Writes',
defaultMessage: 'disk writes',
}),
s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', {
defaultMessage: 'Bucket Size',
defaultMessage: 'bucket size',
}),
s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', {
defaultMessage: 'Total Requests',
defaultMessage: 'total requests',
}),
s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', {
defaultMessage: 'Number of Objects',
defaultMessage: 'number of objects',
}),
s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', {
defaultMessage: 'Downloads (Bytes)',
defaultMessage: 'downloads (bytes)',
}),
s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', {
defaultMessage: 'Uploads (Bytes)',
defaultMessage: 'uploads (bytes)',
}),
rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', {
defaultMessage: 'Connections',
defaultMessage: 'connections',
}),
rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', {
defaultMessage: 'Queries Executed',
defaultMessage: 'queries executed',
}),
rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', {
defaultMessage: 'Active Transactions',
defaultMessage: 'active transactions',
}),
rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', {
defaultMessage: 'Latency',
defaultMessage: 'latency',
}),
sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', {
defaultMessage: 'Messages Available',
defaultMessage: 'messages available',
}),
sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', {
defaultMessage: 'Messages Delayed',
defaultMessage: 'messages delayed',
}),
sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', {
defaultMessage: 'Messages Added',
defaultMessage: 'messages added',
}),
sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', {
defaultMessage: 'Messages Returned Empty',
defaultMessage: 'messages returned empty',
}),
sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', {
defaultMessage: 'Oldest Message',
defaultMessage: 'oldest message',
}),
};
const Translations = mapValues(
TranslationsLowercase,
(translation) => `${translation[0].toUpperCase()}${translation.slice(1)}`
);
export const toMetricOpt = (
metric: SnapshotMetricType
): { text: string; value: SnapshotMetricType } | undefined => {
): { text: string; textLC: string; value: SnapshotMetricType } | undefined => {
switch (metric) {
case 'cpu':
return {
text: Translations.CPUUsage,
textLC: TranslationsLowercase.CPUUsage,
value: 'cpu',
};
case 'memory':
return {
text: Translations.MemoryUsage,
textLC: TranslationsLowercase.MemoryUsage,
value: 'memory',
};
case 'rx':
return {
text: Translations.InboundTraffic,
textLC: TranslationsLowercase.InboundTraffic,
value: 'rx',
};
case 'tx':
return {
text: Translations.OutboundTraffic,
textLC: TranslationsLowercase.OutboundTraffic,
value: 'tx',
};
case 'logRate':
return {
text: Translations.LogRate,
textLC: TranslationsLowercase.LogRate,
value: 'logRate',
};
case 'load':
return {
text: Translations.Load,
textLC: TranslationsLowercase.Load,
value: 'load',
};
case 'count':
return {
text: Translations.Count,
textLC: TranslationsLowercase.Count,
value: 'count',
};
case 'diskIOReadBytes':
return {
text: Translations.DiskIOReadBytes,
textLC: TranslationsLowercase.DiskIOReadBytes,
value: 'diskIOReadBytes',
};
case 'diskIOWriteBytes':
return {
text: Translations.DiskIOWriteBytes,
textLC: TranslationsLowercase.DiskIOWriteBytes,
value: 'diskIOWriteBytes',
};
case 's3BucketSize':
return {
text: Translations.s3BucketSize,
textLC: TranslationsLowercase.s3BucketSize,
value: 's3BucketSize',
};
case 's3TotalRequests':
return {
text: Translations.s3TotalRequests,
textLC: TranslationsLowercase.s3TotalRequests,
value: 's3TotalRequests',
};
case 's3NumberOfObjects':
return {
text: Translations.s3NumberOfObjects,
textLC: TranslationsLowercase.s3NumberOfObjects,
value: 's3NumberOfObjects',
};
case 's3DownloadBytes':
return {
text: Translations.s3DownloadBytes,
textLC: TranslationsLowercase.s3DownloadBytes,
value: 's3DownloadBytes',
};
case 's3UploadBytes':
return {
text: Translations.s3UploadBytes,
textLC: TranslationsLowercase.s3UploadBytes,
value: 's3UploadBytes',
};
case 'rdsConnections':
return {
text: Translations.rdsConnections,
textLC: TranslationsLowercase.rdsConnections,
value: 'rdsConnections',
};
case 'rdsQueriesExecuted':
return {
text: Translations.rdsQueriesExecuted,
textLC: TranslationsLowercase.rdsQueriesExecuted,
value: 'rdsQueriesExecuted',
};
case 'rdsActiveTransactions':
return {
text: Translations.rdsActiveTransactions,
textLC: TranslationsLowercase.rdsActiveTransactions,
value: 'rdsActiveTransactions',
};
case 'rdsLatency':
return {
text: Translations.rdsLatency,
textLC: TranslationsLowercase.rdsLatency,
value: 'rdsLatency',
};
case 'sqsMessagesVisible':
return {
text: Translations.sqsMessagesVisible,
textLC: TranslationsLowercase.sqsMessagesVisible,
value: 'sqsMessagesVisible',
};
case 'sqsMessagesDelayed':
return {
text: Translations.sqsMessagesDelayed,
textLC: TranslationsLowercase.sqsMessagesDelayed,
value: 'sqsMessagesDelayed',
};
case 'sqsMessagesSent':
return {
text: Translations.sqsMessagesSent,
textLC: TranslationsLowercase.sqsMessagesSent,
value: 'sqsMessagesSent',
};
case 'sqsMessagesEmpty':
return {
text: Translations.sqsMessagesEmpty,
textLC: TranslationsLowercase.sqsMessagesEmpty,
value: 'sqsMessagesEmpty',
};
case 'sqsOldestMessage':
return {
text: Translations.sqsOldestMessage,
textLC: TranslationsLowercase.sqsOldestMessage,
value: 'sqsOldestMessage',
};
}

View file

@ -13,10 +13,9 @@ import {
EuiDescriptionListTitle,
EuiDescriptionListDescription,
} from '@elastic/eui';
import { EuiPopover } from '@elastic/eui';
import { EuiPopover, EuiLink } from '@elastic/eui';
import { EuiListGroup, EuiListGroupItem } from '@elastic/eui';
import { EuiFlexItem } from '@elastic/eui';
import { EuiButtonIcon } from '@elastic/eui';
import { SavedViewCreateModal } from './create_modal';
import { SavedViewUpdateModal } from './update_modal';
import { SavedViewManageViewsFlyout } from './manage_views_flyout';
@ -151,15 +150,6 @@ export function SavedViewsToolbarControls<ViewState>(props: Props<ViewState>) {
<EuiPopover
button={
<EuiFlexGroup gutterSize={'s'} alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonIcon
aria-label={i18n.translate('xpack.infra.savedView.changeView', {
defaultMessage: 'Change view',
})}
onClick={showSavedViewMenu}
iconType="globe"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiDescriptionList
style={{ cursor: 'pointer' }}
@ -172,13 +162,15 @@ export function SavedViewsToolbarControls<ViewState>(props: Props<ViewState>) {
id="xpack.infra.savedView.currentView"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{currentView
? currentView.name
: i18n.translate('xpack.infra.savedView.unknownView', {
defaultMessage: 'No view selected',
})}
</EuiDescriptionListDescription>
<EuiLink>
<EuiDescriptionListDescription>
{currentView
? currentView.name
: i18n.translate('xpack.infra.savedView.unknownView', {
defaultMessage: 'No view selected',
})}
</EuiDescriptionListDescription>
</EuiLink>
</EuiDescriptionList>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
import { euiStyled, useUiTracker } from '../../../../../../observability/public';
import { InfraFormatter } from '../../../../lib/lib';
import { Timeline } from './timeline/timeline';
const showHistory = i18n.translate('xpack.infra.showHistory', {
defaultMessage: 'Show history',
});
const hideHistory = i18n.translate('xpack.infra.hideHistory', {
defaultMessage: 'Hide history',
});
const TRANSITION_MS = 300;
export const BottomDrawer: React.FC<{
measureRef: (instance: HTMLElement | null) => void;
interval: string;
formatter: InfraFormatter;
}> = ({ measureRef, interval, formatter, children }) => {
const [isOpen, setIsOpen] = useState(false);
const trackDrawerOpen = useUiTracker({ app: 'infra_metrics' });
const onClick = useCallback(() => {
if (!isOpen) trackDrawerOpen({ metric: 'open_timeline_drawer__inventory' });
setIsOpen(!isOpen);
}, [isOpen, trackDrawerOpen]);
return (
<BottomActionContainer ref={isOpen ? measureRef : null} isOpen={isOpen}>
<BottomActionTopBar ref={isOpen ? null : measureRef}>
<EuiFlexItem grow={false}>
<ShowHideButton iconType={isOpen ? 'arrowDown' : 'arrowRight'} onClick={onClick}>
{isOpen ? hideHistory : showHistory}
</ShowHideButton>
</EuiFlexItem>
<EuiFlexItem
grow={false}
style={{
position: 'relative',
minWidth: 400,
alignSelf: 'center',
height: '16px',
}}
>
{children}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
</EuiFlexItem>
</BottomActionTopBar>
<EuiFlexGroup style={{ marginTop: 0 }}>
<Timeline isVisible={isOpen} interval={interval} yAxisFormatter={formatter} />
</EuiFlexGroup>
</BottomActionContainer>
);
};
const BottomActionContainer = euiStyled.div<{ isOpen: boolean }>`
padding: ${(props) => props.theme.eui.paddingSizes.m} 0;
position: fixed;
left: 0;
bottom: 0;
right: 0;
transition: transform ${TRANSITION_MS}ms;
transform: translateY(${(props) => (props.isOpen ? 0 : '224px')})
`;
const BottomActionTopBar = euiStyled(EuiFlexGroup).attrs({
justifyContent: 'spaceBetween',
alignItems: 'center',
})`
margin-bottom: 0;
height: 48px;
`;
const ShowHideButton = euiStyled(EuiButtonEmpty).attrs({ size: 's' })`
width: 140px;
`;

View file

@ -7,7 +7,7 @@
import React, { useCallback, useEffect } from 'react';
import { useInterval } from 'react-use';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { AutoSizer } from '../../../../components/auto_sizer';
import { convertIntervalToString } from '../../../../utils/convert_interval_to_string';
import { NodesOverview } from './nodes_overview';
@ -23,12 +23,13 @@ import { euiStyled } from '../../../../../../observability/public';
import { Toolbar } from './toolbars/toolbar';
import { ViewSwitcher } from './waffle/view_switcher';
import { IntervalLabel } from './waffle/interval_label';
import { Legend } from './waffle/legend';
import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter';
import { createLegend } from '../lib/create_legend';
import { useSavedViewContext } from '../../../../containers/saved_view/saved_view';
import { useWaffleViewState } from '../hooks/use_waffle_view_state';
import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control';
import { BottomDrawer } from './bottom_drawer';
import { Legend } from './waffle/legend';
export const Layout = () => {
const { sourceId, source } = useSourceContext();
@ -104,12 +105,19 @@ export const Layout = () => {
<PageContent>
<MainContainer>
<TopActionContainer>
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="m">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center" gutterSize="m">
<Toolbar nodeType={nodeType} />
<EuiFlexItem grow={false}>
<IntervalLabel intervalAsString={intervalAsString} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ViewSwitcher view={view} onChange={changeView} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<SavedViewContainer>
<SavedViewsToolbarControls viewState={viewState} />
</SavedViewContainer>
</TopActionContainer>
<AutoSizer bounds>
{({ measureRef, bounds: { height = 0 } }) => (
@ -128,24 +136,14 @@ export const Layout = () => {
formatter={formatter}
bottomMargin={height}
/>
<BottomActionContainer ref={measureRef}>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<SavedViewsToolbarControls viewState={viewState} />
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ position: 'relative', minWidth: 400 }}>
<Legend
formatter={formatter}
bounds={bounds}
dataBounds={dataBounds}
legend={options.legend}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<IntervalLabel intervalAsString={intervalAsString} />
</EuiFlexItem>
</EuiFlexGroup>
</BottomActionContainer>
<BottomDrawer measureRef={measureRef} interval={interval} formatter={formatter}>
<Legend
formatter={formatter}
bounds={bounds}
dataBounds={dataBounds}
legend={options.legend}
/>
</BottomDrawer>
</>
)}
</AutoSizer>
@ -164,12 +162,8 @@ const TopActionContainer = euiStyled.div`
padding: ${(props) => `12px ${props.theme.eui.paddingSizes.m}`};
`;
const BottomActionContainer = euiStyled.div`
background-color: ${(props) => props.theme.eui.euiPageBackgroundColor};
padding: ${(props) => props.theme.eui.paddingSizes.m} ${(props) =>
props.theme.eui.paddingSizes.m};
position: fixed;
left: 0;
bottom: 0;
right: 0;
const SavedViewContainer = euiStyled.div`
position: relative;
z-index: 1;
padding-left: ${(props) => props.theme.eui.paddingSizes.m};
`;

View file

@ -0,0 +1,228 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useMemo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
import { first, last } from 'lodash';
import { EuiLoadingChart, EuiText, EuiEmptyPrompt, EuiButton } from '@elastic/eui';
import {
Axis,
Chart,
Settings,
Position,
TooltipValue,
niceTimeFormatter,
ElementClickListener,
} from '@elastic/charts';
import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_react/public';
import { toMetricOpt } from '../../../../../../common/snapshot_metric_i18n';
import { MetricsExplorerAggregation } from '../../../../../../common/http_api';
import { Color } from '../../../../../../common/color_palette';
import { useSourceContext } from '../../../../../containers/source';
import { useTimeline } from '../../hooks/use_timeline';
import { useWaffleOptionsContext } from '../../hooks/use_waffle_options';
import { useWaffleTimeContext } from '../../hooks/use_waffle_time';
import { useWaffleFiltersContext } from '../../hooks/use_waffle_filters';
import { MetricExplorerSeriesChart } from '../../../metrics_explorer/components/series_chart';
import { MetricsExplorerChartType } from '../../../metrics_explorer/hooks/use_metrics_explorer_options';
import { getTimelineChartTheme } from '../../../metrics_explorer/components/helpers/get_chart_theme';
import { calculateDomain } from '../../../metrics_explorer/components/helpers/calculate_domain';
import { euiStyled } from '../../../../../../../observability/public';
import { InfraFormatter } from '../../../../../lib/lib';
interface Props {
interval: string;
yAxisFormatter: InfraFormatter;
isVisible: boolean;
}
export const Timeline: React.FC<Props> = ({ interval, yAxisFormatter, isVisible }) => {
const { sourceId } = useSourceContext();
const { metric, nodeType, accountId, region } = useWaffleOptionsContext();
const { currentTime, jumpToTime, stopAutoReload } = useWaffleTimeContext();
const { filterQueryAsJson } = useWaffleFiltersContext();
const { loading, error, timeseries, reload } = useTimeline(
filterQueryAsJson,
[metric],
nodeType,
sourceId,
currentTime,
accountId,
region,
interval,
isVisible
);
const metricLabel = toMetricOpt(metric.type)?.textLC;
const chartMetric = {
color: Color.color0,
aggregation: 'avg' as MetricsExplorerAggregation,
label: metricLabel,
};
const dateFormatter = useMemo(() => {
if (!timeseries) return () => '';
const firstTimestamp = first(timeseries.rows)?.timestamp;
const lastTimestamp = last(timeseries.rows)?.timestamp;
if (firstTimestamp == null || lastTimestamp == null) {
return (value: number) => `${value}`;
}
return niceTimeFormatter([firstTimestamp, lastTimestamp]);
}, [timeseries]);
const isDarkMode = useUiSetting<boolean>('theme:darkMode');
const tooltipProps = {
headerFormatter: (tooltipValue: TooltipValue) =>
moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'),
};
const dataDomain = timeseries ? calculateDomain(timeseries, [chartMetric], false) : null;
const domain = dataDomain
? {
max: dataDomain.max * 1.1, // add 10% headroom.
min: dataDomain.min,
}
: { max: 0, min: 0 };
const onClickPoint: ElementClickListener = useCallback(
([[geometryValue]]) => {
if (!Array.isArray(geometryValue)) {
const { x: timestamp } = geometryValue;
jumpToTime(timestamp);
stopAutoReload();
}
},
[jumpToTime, stopAutoReload]
);
if (loading) {
return (
<TimelineContainer>
<TimelineLoadingContainer>
<EuiLoadingChart size="xl" />
</TimelineLoadingContainer>
</TimelineContainer>
);
}
if (!loading && (error || !timeseries)) {
return (
<TimelineContainer>
<EuiEmptyPrompt
iconType="visArea"
title={<h4>{error ? errorTitle : noHistoryDataTitle}</h4>}
actions={
<EuiButton color="primary" fill onClick={reload}>
{error ? retryButtonLabel : checkNewDataButtonLabel}
</EuiButton>
}
/>
</TimelineContainer>
);
}
return (
<TimelineContainer>
<TimelineHeader>
<EuiText>
<strong>
<FormattedMessage
id="xpack.infra.inventoryTimeline.header"
defaultMessage="Average {metricLabel}"
values={{ metricLabel }}
/>
</strong>
</EuiText>
</TimelineHeader>
<TimelineChartContainer>
<Chart>
<MetricExplorerSeriesChart
type={MetricsExplorerChartType.area}
metric={chartMetric}
id="0"
series={timeseries!}
stack={false}
/>
<Axis
id={'timestamp'}
position={Position.Bottom}
showOverlappingTicks={true}
tickFormat={dateFormatter}
/>
<Axis
id={'values'}
position={Position.Left}
tickFormat={yAxisFormatter}
domain={domain}
ticks={6}
showGridLines
/>
<Settings
tooltip={tooltipProps}
theme={getTimelineChartTheme(isDarkMode)}
onElementClick={onClickPoint}
/>
</Chart>
</TimelineChartContainer>
</TimelineContainer>
);
};
const TimelineContainer = euiStyled.div`
background-color: ${(props) => props.theme.eui.euiPageBackgroundColor};
border-top: 1px solid ${(props) => props.theme.eui.euiColorLightShade};
height: 220px;
width: 100%;
padding: ${(props) => props.theme.eui.paddingSizes.s} ${(props) =>
props.theme.eui.paddingSizes.m};
display: flex;
flex-direction: column;
`;
const TimelineHeader = euiStyled.div`
display: flex;
width: 100%;
padding: ${(props) => props.theme.eui.paddingSizes.s} ${(props) =>
props.theme.eui.paddingSizes.m};
`;
const TimelineChartContainer = euiStyled.div`
padding-left: ${(props) => props.theme.eui.paddingSizes.xs};
width: 100%;
height: 100%;
`;
const TimelineLoadingContainer = euiStyled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
`;
const noHistoryDataTitle = i18n.translate('xpack.infra.inventoryTimeline.noHistoryDataTitle', {
defaultMessage: 'There is no history data to display.',
});
const errorTitle = i18n.translate('xpack.infra.inventoryTimeline.errorTitle', {
defaultMessage: 'Unable to display history data.',
});
const checkNewDataButtonLabel = i18n.translate(
'xpack.infra.inventoryTimeline.checkNewDataButtonLabel',
{
defaultMessage: 'Check for new data',
}
);
const retryButtonLabel = i18n.translate('xpack.infra.inventoryTimeline.retryButtonLabel', {
defaultMessage: 'Try again',
});

View file

@ -22,7 +22,7 @@ export const IntervalLabel = ({ intervalAsString }: Props) => {
<p>
<FormattedMessage
id="xpack.infra.homePage.toolbar.showingLastOneMinuteDataText"
defaultMessage="Last {duration} of data for the selected time"
defaultMessage="Last {duration} of data"
values={{ duration: intervalAsString }}
/>
</p>

View file

@ -43,7 +43,7 @@ export function useSnapshot(
const timerange: InfraTimerangeInput = {
interval: '1m',
to: currentTime,
from: currentTime - 360 * 1000,
from: currentTime - 1200 * 1000,
lookbackSize: 20,
};

View file

@ -0,0 +1,121 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { pipe } from 'fp-ts/lib/pipeable';
import { first } from 'lodash';
import { useEffect, useMemo, useCallback } from 'react';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { getIntervalInSeconds } from '../../../../../server/utils/get_interval_in_seconds';
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
import { useHTTPRequest } from '../../../../hooks/use_http_request';
import {
SnapshotNodeResponseRT,
SnapshotNodeResponse,
SnapshotRequest,
InfraTimerangeInput,
} from '../../../../../common/http_api/snapshot_api';
import {
InventoryItemType,
SnapshotMetricType,
} from '../../../../../common/inventory_models/types';
const ONE_MINUTE = 60;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;
const ONE_WEEK = ONE_DAY * 7;
const getTimeLengthFromInterval = (interval: string | undefined) => {
if (interval) {
const intervalInSeconds = getIntervalInSeconds(interval);
const multiplier =
intervalInSeconds < ONE_MINUTE
? ONE_HOUR / intervalInSeconds
: intervalInSeconds < ONE_HOUR
? 60
: intervalInSeconds < ONE_DAY
? 7
: intervalInSeconds < ONE_WEEK
? 30
: 1;
const timeLength = intervalInSeconds * multiplier;
return { timeLength, intervalInSeconds };
} else {
return { timeLength: 0, intervalInSeconds: 0 };
}
};
export function useTimeline(
filterQuery: string | null | undefined,
metrics: Array<{ type: SnapshotMetricType }>,
nodeType: InventoryItemType,
sourceId: string,
currentTime: number,
accountId: string,
region: string,
interval: string | undefined,
shouldReload: boolean
) {
const decodeResponse = (response: any) => {
return pipe(
SnapshotNodeResponseRT.decode(response),
fold(throwErrors(createPlainError), identity)
);
};
const timeLengthResult = useMemo(() => getTimeLengthFromInterval(interval), [interval]);
const { timeLength, intervalInSeconds } = timeLengthResult;
const timerange: InfraTimerangeInput = {
interval: interval ?? '',
to: currentTime + intervalInSeconds * 1000,
from: currentTime - timeLength * 1000,
lookbackSize: 0,
ignoreLookback: true,
};
const { error, loading, response, makeRequest } = useHTTPRequest<SnapshotNodeResponse>(
'/api/metrics/snapshot',
'POST',
JSON.stringify({
metrics,
groupBy: null,
nodeType,
timerange,
filterQuery,
sourceId,
accountId,
region,
includeTimeseries: true,
} as SnapshotRequest),
decodeResponse
);
const loadData = useCallback(() => {
if (shouldReload) return makeRequest();
return Promise.resolve();
}, [makeRequest, shouldReload]);
useEffect(() => {
(async () => {
if (timeLength) {
await loadData();
}
})();
}, [loadData, timeLength]);
const timeseries = response
? first(response.nodes.map((node) => first(node.metrics)?.timeseries))
: null;
return {
error: (error && error.message) || null,
loading: !interval ? true : loading,
timeseries,
reload: makeRequest,
};
}

View file

@ -4,8 +4,33 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts';
import {
Theme,
PartialTheme,
LIGHT_THEME,
DARK_THEME,
mergeWithDefaultTheme,
} from '@elastic/charts';
export function getChartTheme(isDarkMode: boolean): Theme {
return isDarkMode ? DARK_THEME : LIGHT_THEME;
}
export function getTimelineChartTheme(isDarkMode: boolean): Theme {
return isDarkMode ? DARK_THEME : mergeWithDefaultTheme(TIMELINE_LIGHT_THEME, LIGHT_THEME);
}
const TIMELINE_LIGHT_THEME: PartialTheme = {
crosshair: {
band: {
fill: '#D3DAE6',
},
},
axes: {
gridLine: {
horizontal: {
stroke: '#eaeaea',
},
},
},
};

View file

@ -56,8 +56,10 @@ export const transformRequestToMetricsAPIRequest = async (
snapshotRequest.nodeType,
source.configuration.fields
);
const groupBy = snapshotRequest.groupBy.map((g) => g.field).filter(Boolean) as string[];
metricsApiRequest.groupBy = [...groupBy, inventoryFields.id];
if (snapshotRequest.groupBy) {
const groupBy = snapshotRequest.groupBy.map((g) => g.field).filter(Boolean) as string[];
metricsApiRequest.groupBy = [...groupBy, inventoryFields.id];
}
const metaAggregation = {
id: META_KEY,

View file

@ -8815,7 +8815,6 @@
"xpack.infra.registerFeatures.logsDescription": "ログをリアルタイムでストリーするか、コンソール式の UI で履歴ビューをスクロールします。",
"xpack.infra.registerFeatures.logsTitle": "ログ",
"xpack.infra.sampleDataLinkLabel": "ログ",
"xpack.infra.savedView.changeView": "ビューの変更",
"xpack.infra.savedView.currentView": "現在のビュー",
"xpack.infra.savedView.defaultViewNameHosts": "デフォルトビュー",
"xpack.infra.savedView.errorOnCreate.duplicateViewName": "その名前のビューは既に存在します",

View file

@ -8821,7 +8821,6 @@
"xpack.infra.registerFeatures.logsDescription": "实时流式传输日志或在类似控制台的工具中滚动浏览历史视图。",
"xpack.infra.registerFeatures.logsTitle": "日志",
"xpack.infra.sampleDataLinkLabel": "日志",
"xpack.infra.savedView.changeView": "更改视图",
"xpack.infra.savedView.currentView": "当前视图",
"xpack.infra.savedView.defaultViewNameHosts": "默认视图",
"xpack.infra.savedView.errorOnCreate.duplicateViewName": "具有该名称的视图已存在。",