[7.x] [Lens] Add more in-editor Advanced documentation (#86821) (#88865)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Wylie Conlon <wylieconlon@gmail.com>
Co-authored-by: Michael Marcialis <michael.marcialis@elastic.co>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Wylie Conlon <wylieconlon@gmail.com>
Co-authored-by: Michael Marcialis <michael.marcialis@elastic.co>
This commit is contained in:
Marco Liberati 2021-01-20 20:07:58 +01:00 committed by GitHub
parent 97aef867ba
commit 87ee412c01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 578 additions and 92 deletions

View file

@ -35,6 +35,17 @@ search: {
siblingPipelineType: string;
termsAggFilter: string[];
toAbsoluteDates: typeof toAbsoluteDates;
boundsDescendingRaw: ({
bound: number;
interval: import("moment").Duration;
boundLabel: string;
intervalLabel: string;
} | {
bound: import("moment").Duration;
interval: import("moment").Duration;
boundLabel: string;
intervalLabel: string;
})[];
};
getRequestInspectorStats: typeof getRequestInspectorStats;
getResponseInspectorStats: typeof getResponseInspectorStats;

View file

@ -6,75 +6,205 @@
* Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import moment from 'moment';
const boundsDescending = [
export const boundsDescendingRaw = [
{
bound: Infinity,
interval: Number(moment.duration(1, 'year')),
interval: moment.duration(1, 'year'),
boundLabel: i18n.translate('data.search.timeBuckets.infinityLabel', {
defaultMessage: 'More than a year',
}),
intervalLabel: i18n.translate('data.search.timeBuckets.yearLabel', {
defaultMessage: 'a year',
}),
},
{
bound: Number(moment.duration(1, 'year')),
interval: Number(moment.duration(1, 'month')),
bound: moment.duration(1, 'year'),
interval: moment.duration(1, 'month'),
boundLabel: i18n.translate('data.search.timeBuckets.yearLabel', {
defaultMessage: 'a year',
}),
intervalLabel: i18n.translate('data.search.timeBuckets.monthLabel', {
defaultMessage: 'a month',
}),
},
{
bound: Number(moment.duration(3, 'week')),
interval: Number(moment.duration(1, 'week')),
bound: moment.duration(3, 'week'),
interval: moment.duration(1, 'week'),
boundLabel: i18n.translate('data.search.timeBuckets.dayLabel', {
defaultMessage: '{amount, plural, one {a day} other {# days}}',
values: { amount: 21 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.dayLabel', {
defaultMessage: '{amount, plural, one {a day} other {# days}}',
values: { amount: 7 },
}),
},
{
bound: Number(moment.duration(1, 'week')),
interval: Number(moment.duration(1, 'd')),
bound: moment.duration(1, 'week'),
interval: moment.duration(1, 'd'),
boundLabel: i18n.translate('data.search.timeBuckets.dayLabel', {
defaultMessage: '{amount, plural, one {a day} other {# days}}',
values: { amount: 7 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.dayLabel', {
defaultMessage: '{amount, plural, one {a day} other {# days}}',
values: { amount: 1 },
}),
},
{
bound: Number(moment.duration(24, 'hour')),
interval: Number(moment.duration(12, 'hour')),
bound: moment.duration(24, 'hour'),
interval: moment.duration(12, 'hour'),
boundLabel: i18n.translate('data.search.timeBuckets.dayLabel', {
defaultMessage: '{amount, plural, one {a day} other {# days}}',
values: { amount: 1 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.hourLabel', {
defaultMessage: '{amount, plural, one {an hour} other {# hours}}',
values: { amount: 12 },
}),
},
{
bound: Number(moment.duration(6, 'hour')),
interval: Number(moment.duration(3, 'hour')),
bound: moment.duration(6, 'hour'),
interval: moment.duration(3, 'hour'),
boundLabel: i18n.translate('data.search.timeBuckets.hourLabel', {
defaultMessage: '{amount, plural, one {an hour} other {# hours}}',
values: { amount: 6 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.hourLabel', {
defaultMessage: '{amount, plural, one {an hour} other {# hours}}',
values: { amount: 3 },
}),
},
{
bound: Number(moment.duration(2, 'hour')),
interval: Number(moment.duration(1, 'hour')),
bound: moment.duration(2, 'hour'),
interval: moment.duration(1, 'hour'),
boundLabel: i18n.translate('data.search.timeBuckets.hourLabel', {
defaultMessage: '{amount, plural, one {an hour} other {# hours}}',
values: { amount: 2 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.hourLabel', {
defaultMessage: '{amount, plural, one {an hour} other {# hours}}',
values: { amount: 1 },
}),
},
{
bound: Number(moment.duration(45, 'minute')),
interval: Number(moment.duration(30, 'minute')),
bound: moment.duration(45, 'minute'),
interval: moment.duration(30, 'minute'),
boundLabel: i18n.translate('data.search.timeBuckets.minuteLabel', {
defaultMessage: '{amount, plural, one {a minute} other {# minutes}}',
values: { amount: 45 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.minuteLabel', {
defaultMessage: '{amount, plural, one {a minute} other {# minutes}}',
values: { amount: 30 },
}),
},
{
bound: Number(moment.duration(20, 'minute')),
interval: Number(moment.duration(10, 'minute')),
bound: moment.duration(20, 'minute'),
interval: moment.duration(10, 'minute'),
boundLabel: i18n.translate('data.search.timeBuckets.minuteLabel', {
defaultMessage: '{amount, plural, one {a minute} other {# minutes}}',
values: { amount: 20 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.minuteLabel', {
defaultMessage: '{amount, plural, one {a minute} other {# minutes}}',
values: { amount: 10 },
}),
},
{
bound: Number(moment.duration(9, 'minute')),
interval: Number(moment.duration(5, 'minute')),
bound: moment.duration(9, 'minute'),
interval: moment.duration(5, 'minute'),
boundLabel: i18n.translate('data.search.timeBuckets.minuteLabel', {
defaultMessage: '{amount, plural, one {a minute} other {# minutes}}',
values: { amount: 9 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.minuteLabel', {
defaultMessage: '{amount, plural, one {a minute} other {# minutes}}',
values: { amount: 5 },
}),
},
{
bound: Number(moment.duration(3, 'minute')),
interval: Number(moment.duration(1, 'minute')),
bound: moment.duration(3, 'minute'),
interval: moment.duration(1, 'minute'),
boundLabel: i18n.translate('data.search.timeBuckets.minuteLabel', {
defaultMessage: '{amount, plural, one {a minute} other {# minutes}}',
values: { amount: 3 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.minuteLabel', {
defaultMessage: '{amount, plural, one {a minute} other {# minutes}}',
values: { amount: 1 },
}),
},
{
bound: Number(moment.duration(45, 'second')),
interval: Number(moment.duration(30, 'second')),
bound: moment.duration(45, 'second'),
interval: moment.duration(30, 'second'),
boundLabel: i18n.translate('data.search.timeBuckets.secondLabel', {
defaultMessage: '{amount, plural, one {a second} other {# seconds}}',
values: { amount: 45 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.secondLabel', {
defaultMessage: '{amount, plural, one {a second} other {# seconds}}',
values: { amount: 30 },
}),
},
{
bound: Number(moment.duration(15, 'second')),
interval: Number(moment.duration(10, 'second')),
bound: moment.duration(15, 'second'),
interval: moment.duration(10, 'second'),
boundLabel: i18n.translate('data.search.timeBuckets.secondLabel', {
defaultMessage: '{amount, plural, one {a second} other {# seconds}}',
values: { amount: 15 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.secondLabel', {
defaultMessage: '{amount, plural, one {a second} other {# seconds}}',
values: { amount: 10 },
}),
},
{
bound: Number(moment.duration(7.5, 'second')),
interval: Number(moment.duration(5, 'second')),
bound: moment.duration(7.5, 'second'),
interval: moment.duration(5, 'second'),
boundLabel: i18n.translate('data.search.timeBuckets.secondLabel', {
defaultMessage: '{amount, plural, one {a second} other {# seconds}}',
values: { amount: 7.5 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.secondLabel', {
defaultMessage: '{amount, plural, one {a second} other {# seconds}}',
values: { amount: 5 },
}),
},
{
bound: Number(moment.duration(5, 'second')),
interval: Number(moment.duration(1, 'second')),
bound: moment.duration(5, 'second'),
interval: moment.duration(1, 'second'),
boundLabel: i18n.translate('data.search.timeBuckets.secondLabel', {
defaultMessage: '{amount, plural, one {a second} other {# seconds}}',
values: { amount: 5 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.secondLabel', {
defaultMessage: '{amount, plural, one {a second} other {# seconds}}',
values: { amount: 1 },
}),
},
{
bound: Number(moment.duration(500, 'ms')),
interval: Number(moment.duration(100, 'ms')),
bound: moment.duration(500, 'ms'),
interval: moment.duration(100, 'ms'),
boundLabel: i18n.translate('data.search.timeBuckets.millisecondLabel', {
defaultMessage: '{amount, plural, one {a millisecond} other {# milliseconds}}',
values: { amount: 500 },
}),
intervalLabel: i18n.translate('data.search.timeBuckets.millisecondLabel', {
defaultMessage: '{amount, plural, one {a millisecond} other {# milliseconds}}',
values: { amount: 100 },
}),
},
];
const boundsDescending = boundsDescendingRaw.map(({ bound, interval }) => ({
bound: Number(bound),
interval: Number(interval),
}));
function getPerBucketMs(count: number, duration: number) {
const ms = duration / count;
return isFinite(ms) ? ms : NaN;

View file

@ -307,6 +307,7 @@ import {
parseEsInterval,
parseInterval,
toAbsoluteDates,
boundsDescendingRaw,
// expressions utils
getRequestInspectorStats,
getResponseInspectorStats,
@ -416,6 +417,7 @@ export const search = {
siblingPipelineType,
termsAggFilter,
toAbsoluteDates,
boundsDescendingRaw,
},
getRequestInspectorStats,
getResponseInspectorStats,

View file

@ -2213,6 +2213,17 @@ export const search: {
siblingPipelineType: string;
termsAggFilter: string[];
toAbsoluteDates: typeof toAbsoluteDates;
boundsDescendingRaw: ({
bound: number;
interval: import("moment").Duration;
boundLabel: string;
intervalLabel: string;
} | {
bound: import("moment").Duration;
interval: import("moment").Duration;
boundLabel: string;
intervalLabel: string;
})[];
};
getRequestInspectorStats: typeof getRequestInspectorStats;
getResponseInspectorStats: typeof getResponseInspectorStats;
@ -2616,21 +2627,21 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/search/session/session_service.ts:41:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts

View file

@ -299,6 +299,17 @@ export function DimensionEditor(props: DimensionEditorProps) {
}
);
// Need to workout early on the error to decide whether to show this or an help text
const fieldErrorMessage =
(selectedOperationDefinition?.input !== 'fullReference' ||
(incompleteOperation && operationDefinitionMap[incompleteOperation].input === 'field')) &&
getErrorMessage(
selectedColumn,
Boolean(incompleteOperation),
selectedOperationDefinition?.input,
currentFieldIsInvalid
);
return (
<div id={columnId}>
<div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--shaded">
@ -342,6 +353,11 @@ export function DimensionEditor(props: DimensionEditorProps) {
existingFields={state.existingFields}
selectionStyle={selectedOperationDefinition.selectionStyle}
dateRange={dateRange}
labelAppend={selectedOperationDefinition?.getHelpMessage?.({
data: props.data,
uiSettings: props.uiSettings,
currentColumn: state.layers[layerId].columns[columnId],
})}
{...services}
/>
);
@ -360,12 +376,15 @@ export function DimensionEditor(props: DimensionEditorProps) {
})}
fullWidth
isInvalid={Boolean(incompleteOperation || currentFieldIsInvalid)}
error={getErrorMessage(
selectedColumn,
Boolean(incompleteOperation),
selectedOperationDefinition?.input,
currentFieldIsInvalid
)}
error={fieldErrorMessage}
labelAppend={
!fieldErrorMessage &&
selectedOperationDefinition?.getHelpMessage?.({
data: props.data,
uiSettings: props.uiSettings,
currentColumn: state.layers[layerId].columns[columnId],
})
}
>
<FieldSelect
fieldIsInvalid={currentFieldIsInvalid}

View file

@ -187,6 +187,9 @@ describe('IndexPatternDimensionEditorPanel', () => {
id: 'bytes',
title: 'Bytes',
}),
deserialize: jest.fn().mockReturnValue({
convert: () => 'formatted',
}),
} as unknown) as DataPublicPluginStart['fieldFormats'],
} as unknown) as DataPublicPluginStart,
core: {} as CoreSetup,

View file

@ -8,7 +8,13 @@ import './dimension_editor.scss';
import _ from 'lodash';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiSpacer, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import {
EuiFormRow,
EuiFormRowProps,
EuiSpacer,
EuiComboBox,
EuiComboBoxOptionOption,
} from '@elastic/eui';
import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import type { DataPublicPluginStart } from 'src/plugins/data/public';
@ -40,6 +46,7 @@ export interface ReferenceEditorProps {
currentIndexPattern: IndexPattern;
existingFields: IndexPatternPrivateState['existingFields'];
dateRange: DateRange;
labelAppend?: EuiFormRowProps['labelAppend'];
// Services
uiSettings: IUiSettingsClient;
@ -59,6 +66,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
validation,
selectionStyle,
dateRange,
labelAppend,
...services
} = props;
@ -251,6 +259,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
})}
fullWidth
isInvalid={showFieldInvalid}
labelAppend={labelAppend}
>
<FieldSelect
fieldIsInvalid={showFieldInvalid}

View file

@ -0,0 +1,13 @@
.lnsHelpPopover__panel {
max-width: $euiSize * 30;
}
.lnsHelpPopover__content {
@include euiYScrollWithShadows;
max-height: 40vh;
padding: $euiSizeM;
}
.lnsHelpPopover__buttonIcon {
margin-right: $euiSizeXS;
}

View file

@ -0,0 +1,70 @@
/*
* 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, { ReactNode } from 'react';
import {
EuiIcon,
EuiLink,
EuiLinkButtonProps,
EuiPopover,
EuiPopoverProps,
EuiPopoverTitle,
EuiText,
} from '@elastic/eui';
import './help_popover.scss';
export const HelpPopoverButton = ({
children,
onClick,
}: {
children: string;
onClick: EuiLinkButtonProps['onClick'];
}) => {
return (
<EuiText size="xs">
<EuiLink onClick={onClick}>
<EuiIcon className="lnsHelpPopover__buttonIcon" size="s" type="help" />
{children}
</EuiLink>
</EuiText>
);
};
export const HelpPopover = ({
anchorPosition,
button,
children,
closePopover,
isOpen,
title,
}: {
anchorPosition?: EuiPopoverProps['anchorPosition'];
button: EuiPopoverProps['button'];
children: ReactNode;
closePopover: EuiPopoverProps['closePopover'];
isOpen: EuiPopoverProps['isOpen'];
title?: string;
}) => {
return (
<EuiPopover
anchorPosition={anchorPosition}
button={button}
className="lnsHelpPopover"
closePopover={closePopover}
isOpen={isOpen}
ownFocus
panelClassName="lnsHelpPopover__panel"
panelPaddingSize="none"
>
{title && <EuiPopoverTitle>{title}</EuiPopoverTitle>}
<EuiText className="lnsHelpPopover__content" size="s">
{children}
</EuiText>
</EuiPopover>
);
};

View file

@ -5,10 +5,9 @@
*/
import { i18n } from '@kbn/i18n';
import { useState } from 'react';
import React from 'react';
import { EuiFormRow } from '@elastic/eui';
import { EuiFieldNumber } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState } from 'react';
import { EuiFieldNumber, EuiFormRow } from '@elastic/eui';
import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '../column_types';
import { IndexPatternLayer } from '../../../types';
import {
@ -21,6 +20,7 @@ import {
import { updateColumnParam } from '../../layer_helpers';
import { isValidNumber, useDebounceWithOptions } from '../helpers';
import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils';
import { HelpPopover, HelpPopoverButton } from '../../../help_popover';
import type { OperationDefinition, ParamEditorProps } from '..';
const ofName = buildLabelFunction((name?: string) => {
@ -111,6 +111,7 @@ export const movingAverageOperation: OperationDefinition<
})
);
},
getHelpMessage: () => <MovingAveragePopup />,
getDisabledStatus(indexPattern, layer) {
return checkForDateHistogram(
layer,
@ -168,3 +169,79 @@ function MovingAverageParamEditor({
</EuiFormRow>
);
}
const MovingAveragePopup = () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
return (
<HelpPopover
anchorPosition="upCenter"
button={
<HelpPopoverButton onClick={() => setIsPopoverOpen(!isPopoverOpen)}>
{i18n.translate('xpack.lens.indexPattern.movingAverage.helpText', {
defaultMessage: 'How it works',
})}
</HelpPopoverButton>
}
closePopover={() => setIsPopoverOpen(false)}
isOpen={isPopoverOpen}
title={i18n.translate('xpack.lens.indexPattern.movingAverage.titleHelp', {
defaultMessage: 'How moving average works',
})}
>
<p>
<FormattedMessage
id="xpack.lens.indexPattern.movingAverage.basicExplanation"
defaultMessage="Moving average slides a window across the data and displays the average value in the window."
/>
</p>
<p>
<FormattedMessage
id="xpack.lens.indexPattern.movingAverage.longerExplanation"
defaultMessage="To calculate the moving average, Lens uses the mean of the window and applies a skip policy for gaps.
For missing values, the bucket is skipped and the calculation is performed on the next value."
/>
</p>
<p>
<FormattedMessage
id="xpack.lens.indexPattern.movingAverage.tableExplanation"
defaultMessage="For example, given the data [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], we can calculate a simple moving average with a window size of 5 as follows:"
/>
</p>
<ul>
<li>(1 + 2 + 3 + 4 + 5) / 5 = 3</li>
<li>(2 + 3 + 4 + 5 + 6) / 5 = 4</li>
<li>...</li>
<li>(5 + 6 + 7 + 8 + 9) / 5 = 7</li>
</ul>
<p>
<FormattedMessage
id="xpack.lens.indexPattern.movingAverage.windowLimitations"
defaultMessage="The window does not include the current value."
/>
</p>
<p>
<FormattedMessage
id="xpack.lens.indexPattern.movingAverage.windowInitialPartial"
defaultMessage="For the initial part of the series the window is partial, until it reaches the requested number of items. For instance with a window size of 5:"
/>
</p>
<ul>
<li>(1 + 2) / 2 = 1.5</li>
<li>(1 + 2 + 3) / 3 = 2</li>
<li>(1 + 2 + 3 + 4) / 4 = 2.5</li>
<li>(1 + 2 + 3 + 4 + 5) / 5 = 3</li>
</ul>
<p>
<FormattedMessage
id="xpack.lens.indexPattern.movingAverage.limitations"
defaultMessage="Note the first moving average value start from the second item onward. The moving average is supported only for date histograms."
/>
</p>
</HelpPopover>
);
};

View file

@ -4,31 +4,36 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiBasicTable,
EuiCode,
EuiFieldNumber,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiSelect,
EuiSpacer,
EuiSwitch,
EuiSwitchEvent,
EuiFieldNumber,
EuiSelect,
EuiFlexItem,
EuiFlexGroup,
EuiTextColor,
EuiSpacer,
} from '@elastic/eui';
import { updateColumnParam } from '../layer_helpers';
import { OperationDefinition } from './index';
import { FieldBasedIndexPatternColumn } from './column_types';
import {
AggFunctionsMapping,
DataPublicPluginStart,
IndexPatternAggRestrictions,
search,
UI_SETTINGS,
} from '../../../../../../../src/plugins/data/public';
import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public';
import { getInvalidFieldMessage, getSafeName } from './helpers';
import { HelpPopover, HelpPopoverButton } from '../../help_popover';
const { isValidInterval } = search.aggs;
const autoInterval = 'auto';
@ -54,6 +59,7 @@ export const dateHistogramOperation: OperationDefinition<
priority: 5, // Highest priority level used
getErrorMessage: (layer, columnId, indexPattern) =>
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
getHelpMessage: (props) => <AutoDateHistogramPopover {...props} />,
getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => {
if (
type === 'date' &&
@ -334,3 +340,77 @@ function restrictedInterval(aggregationRestrictions?: Partial<IndexPatternAggRes
aggregationRestrictions.date_histogram.fixed_interval
);
}
const AutoDateHistogramPopover = ({ data }: { data: DataPublicPluginStart }) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const infiniteBound = i18n.translate('xpack.lens.indexPattern.dateHistogram.moreThanYear', {
defaultMessage: 'More than a year',
});
const upToLabel = i18n.translate('xpack.lens.indexPattern.dateHistogram.upTo', {
defaultMessage: 'Up to',
});
return (
<HelpPopover
anchorPosition="upCenter"
button={
<HelpPopoverButton onClick={() => setIsPopoverOpen(!isPopoverOpen)}>
{i18n.translate('xpack.lens.indexPattern.dateHistogram.autoHelpText', {
defaultMessage: 'How it works',
})}
</HelpPopoverButton>
}
closePopover={() => setIsPopoverOpen(false)}
isOpen={isPopoverOpen}
title={i18n.translate('xpack.lens.indexPattern.dateHistogram.titleHelp', {
defaultMessage: 'How auto date histogram works',
})}
>
<p>
{i18n.translate('xpack.lens.indexPattern.dateHistogram.autoBasicExplanation', {
defaultMessage: 'The auto date histogram splits a date field into buckets by interval.',
})}
</p>
<p>
<FormattedMessage
id="xpack.lens.indexPattern.dateHistogram.autoLongerExplanation"
defaultMessage="Lens automatically chooses an interval for you by dividing the specified time range by the
{targetBarSetting} advanced setting. The calculation tries to present nice time interval buckets. The maximum
number of bars is set by the {maxBarSetting} value."
values={{
maxBarSetting: <EuiCode>{UI_SETTINGS.HISTOGRAM_MAX_BARS}</EuiCode>,
targetBarSetting: <EuiCode>{UI_SETTINGS.HISTOGRAM_BAR_TARGET}</EuiCode>,
}}
/>
</p>
<p>
{i18n.translate('xpack.lens.indexPattern.dateHistogram.autoAdvancedExplanation', {
defaultMessage: 'The interval follows this logic:',
})}
</p>
<EuiBasicTable
items={search.aggs.boundsDescendingRaw.map(({ bound, boundLabel, intervalLabel }) => ({
bound: typeof bound === 'number' ? infiniteBound : `${upToLabel} ${boundLabel}`,
interval: intervalLabel,
}))}
columns={[
{
field: 'bound',
name: i18n.translate('xpack.lens.indexPattern.dateHistogram.autoBoundHeader', {
defaultMessage: 'Target interval measured',
}),
},
{
field: 'interval',
name: i18n.translate('xpack.lens.indexPattern.dateHistogram.autoIntervalHeader', {
defaultMessage: 'Interval used',
}),
},
]}
/>
</HelpPopover>
);
};

View file

@ -126,6 +126,12 @@ export interface ParamEditorProps<C> {
data: DataPublicPluginStart;
}
export interface HelpProps<C> {
currentColumn: C;
uiSettings: IUiSettingsClient;
data: DataPublicPluginStart;
}
export type TimeScalingMode = 'disabled' | 'mandatory' | 'optional';
interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn> {
@ -201,6 +207,8 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn> {
* If set to optional, time scaling won't be enabled by default and can be removed.
*/
timeScalingMode?: TimeScalingMode;
getHelpMessage?: (props: HelpProps<C>) => React.ReactNode;
}
interface BaseBuildColumnArgs {

View file

@ -6,21 +6,73 @@
import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiCode,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiRange,
EuiFlexItem,
EuiFlexGroup,
EuiButtonIcon,
EuiToolTip,
EuiIconTip,
} from '@elastic/eui';
import { IFieldFormat } from 'src/plugins/data/public';
import type { IFieldFormat } from 'src/plugins/data/public';
import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/public';
import { RangeColumnParams, UpdateParamsFnType, MODES_TYPES } from './ranges';
import { AdvancedRangeEditor } from './advanced_editor';
import { TYPING_DEBOUNCE_TIME, MODES, MIN_HISTOGRAM_BARS } from './constants';
import { useDebounceWithOptions } from '../helpers';
import { HelpPopover, HelpPopoverButton } from '../../../help_popover';
const GranularityHelpPopover = () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
return (
<HelpPopover
anchorPosition="upCenter"
button={
<HelpPopoverButton onClick={() => setIsPopoverOpen(!isPopoverOpen)}>
{i18n.translate('xpack.lens.indexPattern.ranges.granularityHelpText', {
defaultMessage: 'How it works',
})}
</HelpPopoverButton>
}
closePopover={() => setIsPopoverOpen(false)}
isOpen={isPopoverOpen}
title={i18n.translate('xpack.lens.indexPattern.ranges.granularityPopoverTitle', {
defaultMessage: 'How granularity interval works',
})}
>
<p>
{i18n.translate('xpack.lens.indexPattern.ranges.granularityPopoverBasicExplanation', {
defaultMessage:
'Interval granularity divides the field into evenly spaced intervals based on the minimum and maximum values for the field.',
})}
</p>
<p>
<FormattedMessage
id="xpack.lens.indexPattern.ranges.granularityPopoverExplanation"
defaultMessage='The size of the interval is chosen to be a "nice" value. It is possible for the chosen
interval to stay the same when changing the granularity slider if the "nice" interval is
the same. The minimum granularity is 1, and the maximum is
{setting}. To change the maximum granularity setting, go to Advanced settings.'
values={{
setting: <EuiCode>{UI_SETTINGS.HISTOGRAM_MAX_BARS}</EuiCode>,
}}
/>
</p>
<p>
{i18n.translate('xpack.lens.indexPattern.ranges.granularityPopoverAdvancedExplanation', {
defaultMessage:
'Intervals are incremented by 10, 5 or 2: for example an interval can be 100 or 0.2 .',
})}
</p>
</HelpPopover>
);
};
const BaseRangeEditor = ({
maxBars,
@ -49,12 +101,7 @@ const BaseRangeEditor = ({
const granularityLabel = i18n.translate('xpack.lens.indexPattern.ranges.granularity', {
defaultMessage: 'Intervals granularity',
});
const granularityLabelDescription = i18n.translate(
'xpack.lens.indexPattern.ranges.granularityDescription',
{
defaultMessage: 'Divides the field into evenly spaced intervals.',
}
);
const decreaseButtonLabel = i18n.translate('xpack.lens.indexPattern.ranges.decreaseButtonLabel', {
defaultMessage: 'Decrease granularity',
});
@ -65,21 +112,12 @@ const BaseRangeEditor = ({
return (
<>
<EuiFormRow
label={
<>
{granularityLabel}{' '}
<EuiIconTip
position="right"
content={granularityLabelDescription}
type="questionInCircle"
color="subdued"
/>
</>
}
label={granularityLabel}
data-test-subj="indexPattern-ranges-section-label"
labelType="legend"
fullWidth
display="rowCompressed"
labelAppend={<GranularityHelpPopover />}
>
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={false}>

View file

@ -1,3 +1,3 @@
.lnsXyToolbar__popover {
width: 320px;
width: 365px;
}

View file

@ -21,6 +21,7 @@ import {
EuiColorPickerProps,
EuiToolTip,
EuiIcon,
EuiIconTip,
} from '@elastic/eui';
import { PaletteRegistry } from 'src/plugins/charts/public';
import {
@ -327,9 +328,25 @@ export function XyToolbar(props: VisualizationToolbarProps<State>) {
{isFittingEnabled ? (
<EuiFormRow
display="columnCompressed"
label={i18n.translate('xpack.lens.xyChart.missingValuesLabel', {
defaultMessage: 'Missing values',
})}
label={
<>
{i18n.translate('xpack.lens.xyChart.missingValuesLabel', {
defaultMessage: 'Missing values',
})}{' '}
<EuiIconTip
color="subdued"
content={i18n.translate('xpack.lens.xyChart.missingValuesLabelHelpText', {
defaultMessage: `Gaps in the data are not shown by default, but can be represented as dotted lines with different modes.`,
})}
iconProps={{
className: 'eui-alignTop',
}}
position="top"
size="s"
type="questionInCircle"
/>
</>
}
>
<EuiSuperSelect
data-test-subj="lnsMissingValuesSelect"

View file

@ -11412,7 +11412,6 @@
"xpack.lens.indexPattern.ranges.decreaseButtonLabel": "粒度を下げる",
"xpack.lens.indexPattern.ranges.deleteRange": "範囲を削除",
"xpack.lens.indexPattern.ranges.granularity": "間隔粒度",
"xpack.lens.indexPattern.ranges.granularityDescription": "フィールドを均等な間隔に分割します。",
"xpack.lens.indexPattern.ranges.increaseButtonLabel": "粒度を上げる",
"xpack.lens.indexPattern.ranges.lessThanOrEqualAppend": "≤",
"xpack.lens.indexPattern.ranges.lessThanOrEqualTooltip": "以下",

View file

@ -11441,7 +11441,6 @@
"xpack.lens.indexPattern.ranges.decreaseButtonLabel": "减小粒度",
"xpack.lens.indexPattern.ranges.deleteRange": "删除范围",
"xpack.lens.indexPattern.ranges.granularity": "时间间隔粒度",
"xpack.lens.indexPattern.ranges.granularityDescription": "将字段分成间隔均匀的时间间隔。",
"xpack.lens.indexPattern.ranges.increaseButtonLabel": "增加粒度",
"xpack.lens.indexPattern.ranges.lessThanOrEqualAppend": "≤",
"xpack.lens.indexPattern.ranges.lessThanOrEqualTooltip": "小于或等于",