[Metrics UI] Add inventory view timeline (#77804)
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
0f8bbf11f4
commit
a0f03ddbd1
|
@ -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,
|
||||
}),
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
`;
|
|
@ -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};
|
||||
`;
|
||||
|
|
|
@ -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',
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -43,7 +43,7 @@ export function useSnapshot(
|
|||
const timerange: InfraTimerangeInput = {
|
||||
interval: '1m',
|
||||
to: currentTime,
|
||||
from: currentTime - 360 * 1000,
|
||||
from: currentTime - 1200 * 1000,
|
||||
lookbackSize: 20,
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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": "その名前のビューは既に存在します",
|
||||
|
|
|
@ -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": "具有该名称的视图已存在。",
|
||||
|
|
Loading…
Reference in a new issue