From 9ddd49a9f0d080dda5161eab0dd4f48d7baa9ebd Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 31 Aug 2020 12:40:44 +0300 Subject: [PATCH 001/106] Hides advanced json for count metric (#74636) * remove advanced json for count agg * Remove only advanced json from count agg * use Constant from data plugin * add the logic to data plugin * remove json arg from function definition * remove unecessary translations Co-authored-by: Elastic Machine --- .../data/common/search/aggs/agg_type.test.ts | 11 +++++++++++ src/plugins/data/common/search/aggs/agg_type.ts | 17 +++++++++++------ .../data/common/search/aggs/metrics/count.ts | 1 + .../common/search/aggs/metrics/count_fn.test.ts | 14 -------------- .../data/common/search/aggs/metrics/count_fn.ts | 12 +----------- .../public/components/agg_params_helper.ts | 2 +- .../apps/visualize/_point_series_options.js | 5 +++++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 9 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/plugins/data/common/search/aggs/agg_type.test.ts b/src/plugins/data/common/search/aggs/agg_type.test.ts index 2fcc6b97b1cc6f..bf1136159dfe86 100644 --- a/src/plugins/data/common/search/aggs/agg_type.test.ts +++ b/src/plugins/data/common/search/aggs/agg_type.test.ts @@ -99,6 +99,17 @@ describe('AggType Class', () => { expect(aggType.params[1].name).toBe('customLabel'); }); + test('disables json param', () => { + const aggType = new AggType({ + name: 'name', + title: 'title', + json: false, + }); + + expect(aggType.params.length).toBe(1); + expect(aggType.params[0].name).toBe('customLabel'); + }); + test('can disable customLabel', () => { const aggType = new AggType({ name: 'smart agg', diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 0ba2bb66e77581..2ee604c1bf25da 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -47,6 +47,7 @@ export interface AggTypeConfig< getRequestAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void); getResponseAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void); customLabels?: boolean; + json?: boolean; decorateAggConfig?: () => any; postFlightRequest?: ( resp: any, @@ -235,13 +236,17 @@ export class AggType< if (config.params && config.params.length && config.params[0] instanceof BaseParamType) { this.params = config.params as TParam[]; } else { - // always append the raw JSON param + // always append the raw JSON param unless it is configured to false const params: any[] = config.params ? [...config.params] : []; - params.push({ - name: 'json', - type: 'json', - advanced: true, - }); + + if (config.json !== false) { + params.push({ + name: 'json', + type: 'json', + advanced: true, + }); + } + // always append custom label if (config.customLabels !== false) { diff --git a/src/plugins/data/common/search/aggs/metrics/count.ts b/src/plugins/data/common/search/aggs/metrics/count.ts index d990599586e81c..9c9f36651f4d23 100644 --- a/src/plugins/data/common/search/aggs/metrics/count.ts +++ b/src/plugins/data/common/search/aggs/metrics/count.ts @@ -28,6 +28,7 @@ export const getCountMetricAgg = () => defaultMessage: 'Count', }), hasNoDsl: true, + json: false, makeLabel() { return i18n.translate('data.search.aggs.metrics.countLabel', { defaultMessage: 'Count', diff --git a/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts index 846feb9296fca2..32189f07581e6e 100644 --- a/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts @@ -34,7 +34,6 @@ describe('agg_expression_functions', () => { "id": undefined, "params": Object { "customLabel": undefined, - "json": undefined, }, "schema": undefined, "type": "count", @@ -42,18 +41,5 @@ describe('agg_expression_functions', () => { } `); }); - - test('correctly parses json string argument', () => { - const actual = fn({ - json: '{ "foo": true }', - }); - - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); - }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/count_fn.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.ts index 338ca18209299f..7d4616ffdc6191 100644 --- a/src/plugins/data/common/search/aggs/metrics/count_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/count_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; const fnName = 'aggCount'; @@ -55,12 +54,6 @@ export const aggCount = (): FunctionDefinition => ({ defaultMessage: 'Schema to use for this aggregation', }), }, - json: { - types: ['string'], - help: i18n.translate('data.search.aggs.metrics.count.json.help', { - defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', - }), - }, customLabel: { types: ['string'], help: i18n.translate('data.search.aggs.metrics.count.customLabel.help', { @@ -78,10 +71,7 @@ export const aggCount = (): FunctionDefinition => ({ enabled, schema, type: METRIC_TYPES.COUNT, - params: { - ...rest, - json: getParsedValue(args, 'json'), - }, + params: rest, }, }; }, diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts index ef2f937c8547cf..b13ca32601aa95 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -26,7 +26,7 @@ import { IAggType, IndexPattern, IndexPatternField, -} from 'src/plugins/data/public'; +} from '../../../data/public'; import { filterAggTypes, filterAggTypeFields } from '../agg_filters'; import { groupAndSortBy, ComboBoxGroupedOptions } from '../utils'; import { AggTypeState, AggParamsState } from './agg_params_state'; diff --git a/test/functional/apps/visualize/_point_series_options.js b/test/functional/apps/visualize/_point_series_options.js index d08bfe3b909139..c88670ee8b7414 100644 --- a/test/functional/apps/visualize/_point_series_options.js +++ b/test/functional/apps/visualize/_point_series_options.js @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }) { const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects([ 'visualize', 'header', @@ -148,6 +149,10 @@ export default function ({ getService, getPageObjects }) { }); }); + it('should not show advanced json for count agg', async function () { + await testSubjects.missingOrFail('advancedParams-1'); + }); + it('should put secondary axis on the right', async function () { const length = await PageObjects.visChart.getRightValueAxes(); expect(length).to.be(1); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dc07044ce8ed72..07b646df74b9f5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1115,7 +1115,6 @@ "data.search.aggs.metrics.count.customLabel.help": "このアグリゲーションのカスタムラベルを表します", "data.search.aggs.metrics.count.enabled.help": "このアグリゲーションが有効かどうかを指定します", "data.search.aggs.metrics.count.id.help": "このアグリゲーションのID", - "data.search.aggs.metrics.count.json.help": "アグリゲーションがElasticsearchに送信されるときに含める高度なJSON", "data.search.aggs.metrics.count.schema.help": "このアグリゲーションで使用するスキーマ", "data.search.aggs.metrics.countLabel": "カウント", "data.search.aggs.metrics.countTitle": "カウント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 144bc1cac18526..ffd7d0cfb0f87f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1116,7 +1116,6 @@ "data.search.aggs.metrics.count.customLabel.help": "表示此聚合的定制标签", "data.search.aggs.metrics.count.enabled.help": "指定是否启用此聚合", "data.search.aggs.metrics.count.id.help": "此聚合的 ID", - "data.search.aggs.metrics.count.json.help": "聚合发送至 Elasticsearch 时要包括的高级 json", "data.search.aggs.metrics.count.schema.help": "要用于此聚合的方案", "data.search.aggs.metrics.countLabel": "计数", "data.search.aggs.metrics.countTitle": "计数", From aac57fb1a8494d386b37bdfe21ada3cd9c8d7494 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 31 Aug 2020 11:45:57 +0200 Subject: [PATCH 002/106] [ILM] Add support for frozen phase in UI (#75968) * ILM public side changes to support frozen phase This is a very faithful duplication of the cold phase. We are also excluding the snapshot action as well as the unfollow action as these are features that require higher than basic license privilege. * added "frozen" to the server side schema * add frozen phases component * fix i18n and update jest tests * Slight restructuring to phase types and fix copy paste issues. - also fixed TS error introduced after TS v4 upgrade (delete) * fix hot phase type and remove type constraint from error messages * update validation logic Co-authored-by: Elastic Machine --- .../extend_index_management.test.js.snap | 4 + .../public/application/constants/policy.ts | 11 + .../edit_policy/components/min_age_input.tsx | 11 +- .../components/node_allocation.tsx | 6 +- .../components/set_priority_input.tsx | 6 +- .../sections/edit_policy/edit_policy.tsx | 16 +- .../edit_policy/phases/cold_phase.tsx | 7 +- .../edit_policy/phases/delete_phase.tsx | 7 +- .../edit_policy/phases/frozen_phase.tsx | 210 ++++++++++++++++++ .../sections/edit_policy/phases/hot_phase.tsx | 7 +- .../sections/edit_policy/phases/index.ts | 1 + .../edit_policy/phases/warm_phase.tsx | 7 +- .../services/policies/cold_phase.ts | 4 +- .../services/policies/frozen_phase.ts | 161 ++++++++++++++ .../services/policies/policy_serialization.ts | 11 + .../services/policies/policy_validation.ts | 18 +- .../application/services/policies/types.ts | 66 ++++-- .../public/extend_index_management/index.js | 6 + .../api/policies/register_create_route.ts | 18 ++ 19 files changed, 527 insertions(+), 50 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap index 38dd49a286b588..39eb54b941ac4b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap @@ -76,6 +76,10 @@ Array [ "value": "warm", "view": "Warm", }, + Object { + "value": "frozen", + "view": "Frozen", + }, Object { "value": "cold", "view": "Cold", diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index 3a19f03547b5b2..fb626e7d7fe769 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -10,6 +10,7 @@ import { DeletePhase, HotPhase, WarmPhase, + FrozenPhase, } from '../services/policies/types'; export const defaultNewHotPhase: HotPhase = { @@ -47,6 +48,16 @@ export const defaultNewColdPhase: ColdPhase = { phaseIndexPriority: '0', }; +export const defaultNewFrozenPhase: FrozenPhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + selectedReplicaCount: '', + freezeEnabled: false, + phaseIndexPriority: '0', +}; + export const defaultNewDeletePhase: DeletePhase = { phaseEnabled: false, selectedMinimumAge: '0', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx index 11b743ecc4bb65..5128ba1c881a0c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx @@ -12,7 +12,7 @@ import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; -import { ColdPhase, DeletePhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseWithMinAge, Phases } from '../../../services/policies/types'; function getTimingLabelForPhase(phase: keyof Phases) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. @@ -27,6 +27,11 @@ function getTimingLabelForPhase(phase: keyof Phases) { defaultMessage: 'Timing for cold phase', }); + case 'frozen': + return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseFrozen.minimumAgeLabel', { + defaultMessage: 'Timing for frozen phase', + }); + case 'delete': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel', { defaultMessage: 'Timing for delete phase', @@ -63,7 +68,7 @@ function getUnitsAriaLabelForPhase(phase: keyof Phases) { } } -interface Props { +interface Props { rolloverEnabled: boolean; errors?: PhaseValidationErrors; phase: keyof Phases & string; @@ -72,7 +77,7 @@ interface Props { isShowingErrors: boolean; } -export const MinAgeInput = ({ +export const MinAgeInput = ({ rolloverEnabled, errors, phaseData, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx index 0ce2c0d7ea5663..b4ff62bfb03dcd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx @@ -20,7 +20,7 @@ import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; import { useLoadNodes } from '../../../services/api'; import { NodeAttrsDetails } from './node_attrs_details'; -import { ColdPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseWithAllocationAction, Phases } from '../../../services/policies/types'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; const learnMoreLink = ( @@ -38,14 +38,14 @@ const learnMoreLink = ( ); -interface Props { +interface Props { phase: keyof Phases & string; errors?: PhaseValidationErrors; phaseData: T; setPhaseData: (dataKey: keyof T & string, value: string) => void; isShowingErrors: boolean; } -export const NodeAllocation = ({ +export const NodeAllocation = ({ phase, setPhaseData, errors, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx index 1da7508049f247..1505532a2b16e7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx @@ -10,17 +10,17 @@ import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eu import { LearnMoreLink } from './'; import { OptionalLabel } from './'; import { ErrableFormRow } from './'; -import { ColdPhase, HotPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseWithIndexPriority, Phases } from '../../../services/policies/types'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; -interface Props { +interface Props { errors?: PhaseValidationErrors; phase: keyof Phases & string; phaseData: T; setPhaseData: (dataKey: keyof T & string, value: any) => void; isShowingErrors: boolean; } -export const SetPriorityInput = ({ +export const SetPriorityInput = ({ errors, phaseData, phase, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index c99d01b5466792..db58c64a8ae8cd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -28,7 +28,7 @@ import { import { toasts } from '../../services/notification'; -import { Policy, PolicyFromES } from '../../services/policies/types'; +import { Phases, Policy, PolicyFromES } from '../../services/policies/types'; import { validatePolicy, ValidationErrors, @@ -42,7 +42,7 @@ import { } from '../../services/policies/policy_serialization'; import { ErrableFormRow, LearnMoreLink, PolicyJsonFlyout } from './components'; -import { ColdPhase, DeletePhase, HotPhase, WarmPhase } from './phases'; +import { ColdPhase, DeletePhase, FrozenPhase, HotPhase, WarmPhase } from './phases'; interface Props { policies: PolicyFromES[]; @@ -118,7 +118,7 @@ export const EditPolicy: React.FunctionComponent = ({ setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout); }; - const setPhaseData = (phase: 'hot' | 'warm' | 'cold' | 'delete', key: string, value: any) => { + const setPhaseData = (phase: keyof Phases, key: string, value: any) => { setPolicy({ ...policy, phases: { @@ -303,6 +303,16 @@ export const EditPolicy: React.FunctionComponent = ({ + 0} + setPhaseData={(key, value) => setPhaseData('frozen', key, value)} + phaseData={policy.phases.frozen} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + 0} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx index fb32752fe24ea6..9df6da7a88b2f8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { ColdPhase as ColdPhaseInterface, Phases } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { LearnMoreLink, @@ -36,9 +36,8 @@ const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeInd defaultMessage: 'Freeze index', }); -const coldProperty = propertyof('cold'); -const phaseProperty = (propertyName: keyof ColdPhaseInterface) => - propertyof(propertyName); +const coldProperty: keyof Phases = 'cold'; +const phaseProperty = (propertyName: keyof ColdPhaseInterface) => propertyName; interface Props { setPhaseData: (key: keyof ColdPhaseInterface & string, value: string | boolean) => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx index d3c73090f25f2d..eab93777a72bde 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; import { DeletePhase as DeletePhaseInterface, Phases } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { ActiveBadge, @@ -20,9 +20,8 @@ import { SnapshotPolicies, } from '../components'; -const deleteProperty = propertyof('delete'); -const phaseProperty = (propertyName: keyof DeletePhaseInterface) => - propertyof(propertyName); +const deleteProperty: keyof Phases = 'delete'; +const phaseProperty = (propertyName: keyof DeletePhaseInterface) => propertyName; interface Props { setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx new file mode 100644 index 00000000000000..782906a56a9baf --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx @@ -0,0 +1,210 @@ +/* + * 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, { PureComponent, Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiFieldNumber, + EuiDescribedFormGroup, + EuiSwitch, + EuiTextColor, +} from '@elastic/eui'; + +import { FrozenPhase as FrozenPhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; + +import { + LearnMoreLink, + ActiveBadge, + PhaseErrorMessage, + OptionalLabel, + ErrableFormRow, + MinAgeInput, + NodeAllocation, + SetPriorityInput, +} from '../components'; + +const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', +}); + +const frozenProperty: keyof Phases = 'frozen'; +const phaseProperty = (propertyName: keyof FrozenPhaseInterface) => propertyName; + +interface Props { + setPhaseData: (key: keyof FrozenPhaseInterface & string, value: string | boolean) => void; + phaseData: FrozenPhaseInterface; + isShowingErrors: boolean; + errors?: PhaseValidationErrors; + hotPhaseRolloverEnabled: boolean; +} +export class FrozenPhase extends PureComponent { + render() { + const { + setPhaseData, + phaseData, + errors, + isShowingErrors, + hotPhaseRolloverEnabled, + } = this.props; + + return ( +
+ +

+ +

{' '} + {phaseData.phaseEnabled && !isShowingErrors ? : null} + +
+ } + titleSize="s" + description={ + +

+ +

+ + } + id={`${frozenProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} + onChange={(e) => { + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); + }} + aria-controls="frozenPhaseContent" + /> +
+ } + fullWidth + > + + {phaseData.phaseEnabled ? ( + + + errors={errors} + phaseData={phaseData} + phase={frozenProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + rolloverEnabled={hotPhaseRolloverEnabled} + /> + + + + phase={frozenProperty} + setPhaseData={setPhaseData} + errors={errors} + phaseData={phaseData} + isShowingErrors={isShowingErrors} + /> + + + + + + + + } + isShowingErrors={isShowingErrors} + errors={errors?.freezeEnabled} + helpText={i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.replicaCountHelpText', + { + defaultMessage: 'By default, the number of replicas remains the same.', + } + )} + > + { + setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); + }} + min={0} + /> + + + + + ) : ( +
+ )} + + + {phaseData.phaseEnabled ? ( + + + + + } + description={ + + {' '} + + + } + fullWidth + titleSize="xs" + > + { + setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); + }} + label={freezeLabel} + aria-label={freezeLabel} + /> + + + errors={errors} + phaseData={phaseData} + phase={frozenProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + /> + + ) : null} +
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx index 22f0114d16afe4..106e3b9139a9b1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { HotPhase as HotPhaseInterface, Phases } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { LearnMoreLink, @@ -112,9 +112,8 @@ const maxAgeUnits = [ }), }, ]; -const hotProperty = propertyof('hot'); -const phaseProperty = (propertyName: keyof HotPhaseInterface) => - propertyof(propertyName); +const hotProperty: keyof Phases = 'hot'; +const phaseProperty = (propertyName: keyof HotPhaseInterface) => propertyName; interface Props { errors?: PhaseValidationErrors; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts index 8d1ace5950497b..d59f2ff6413fd4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts @@ -7,4 +7,5 @@ export { HotPhase } from './hot_phase'; export { WarmPhase } from './warm_phase'; export { ColdPhase } from './cold_phase'; +export { FrozenPhase } from './frozen_phase'; export { DeletePhase } from './delete_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx index f7b8c60a5c71f1..2733d01ac222d3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx @@ -30,7 +30,7 @@ import { } from '../components'; import { Phases, WarmPhase as WarmPhaseInterface } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { defaultMessage: 'Shrink index', @@ -47,9 +47,8 @@ const forcemergeLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.force defaultMessage: 'Force merge data', }); -const warmProperty = propertyof('warm'); -const phaseProperty = (propertyName: keyof WarmPhaseInterface) => - propertyof(propertyName); +const warmProperty: keyof Phases = 'warm'; +const phaseProperty = (propertyName: keyof WarmPhaseInterface) => propertyName; interface Props { setPhaseData: (key: keyof WarmPhaseInterface & string, value: boolean | string) => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts index 6cc43042ed4ff0..7fa82a004b872e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts @@ -152,9 +152,9 @@ export const validateColdPhase = (phase: ColdPhase): PhaseValidationErrors { + const phase = { ...frozenPhaseInitialization }; + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + + if (phaseSerialized.min_age) { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + if (actions.allocate) { + const allocate = actions.allocate; + if (allocate.require) { + Object.entries(allocate.require).forEach((entry) => { + phase.selectedNodeAttrs = entry.join(':'); + }); + if (allocate.number_of_replicas) { + phase.selectedReplicaCount = allocate.number_of_replicas.toString(); + } + } + } + + if (actions.freeze) { + phase.freezeEnabled = true; + } + + if (actions.set_priority) { + phase.phaseIndexPriority = actions.set_priority.priority + ? actions.set_priority.priority.toString() + : ''; + } + } + + return phase; +}; + +export const frozenPhaseToES = ( + phase: FrozenPhase, + originalPhase?: SerializedFrozenPhase +): SerializedFrozenPhase => { + if (!originalPhase) { + originalPhase = { ...serializedPhaseInitialization }; + } + + const esPhase = { ...originalPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.selectedNodeAttrs) { + const [name, value] = phase.selectedNodeAttrs.split(':'); + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.require = { + [name]: value, + }; + } else { + if (esPhase.actions.allocate) { + // @ts-expect-error + delete esPhase.actions.allocate.require; + } + } + + if (isNumber(phase.selectedReplicaCount)) { + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); + } else { + if (esPhase.actions.allocate) { + // @ts-expect-error + delete esPhase.actions.allocate.number_of_replicas; + } + } + + if ( + esPhase.actions.allocate && + !esPhase.actions.allocate.require && + !isNumber(esPhase.actions.allocate.number_of_replicas) && + isEmpty(esPhase.actions.allocate.include) && + isEmpty(esPhase.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete esPhase.actions.allocate; + } + + if (phase.freezeEnabled) { + esPhase.actions.freeze = {}; + } else { + delete esPhase.actions.freeze; + } + + if (isNumber(phase.phaseIndexPriority)) { + esPhase.actions.set_priority = { + priority: parseInt(phase.phaseIndexPriority, 10), + }; + } else { + delete esPhase.actions.set_priority; + } + + return esPhase; +}; + +export const validateFrozenPhase = (phase: FrozenPhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // index priority is optional, but if it's set, it needs to be a positive number + if (phase.phaseIndexPriority) { + if (!isNumber(phase.phaseIndexPriority)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + } + + // min age needs to be a positive number + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.selectedMinimumAge = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; + } + + return { ...phaseErrors }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts index 3953521df18179..807a6fe8ec3959 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts @@ -9,6 +9,7 @@ import { defaultNewDeletePhase, defaultNewHotPhase, defaultNewWarmPhase, + defaultNewFrozenPhase, serializedPhaseInitialization, } from '../../constants'; @@ -17,6 +18,7 @@ import { Policy, PolicyFromES, SerializedPolicy } from './types'; import { hotPhaseFromES, hotPhaseToES } from './hot_phase'; import { warmPhaseFromES, warmPhaseToES } from './warm_phase'; import { coldPhaseFromES, coldPhaseToES } from './cold_phase'; +import { frozenPhaseFromES, frozenPhaseToES } from './frozen_phase'; import { deletePhaseFromES, deletePhaseToES } from './delete_phase'; export const splitSizeAndUnits = (field: string): { size: string; units: string } => { @@ -53,6 +55,7 @@ export const initializeNewPolicy = (newPolicyName: string = ''): Policy => { hot: { ...defaultNewHotPhase }, warm: { ...defaultNewWarmPhase }, cold: { ...defaultNewColdPhase }, + frozen: { ...defaultNewFrozenPhase }, delete: { ...defaultNewDeletePhase }, }, }; @@ -70,6 +73,7 @@ export const deserializePolicy = (policy: PolicyFromES): Policy => { hot: hotPhaseFromES(phases.hot), warm: warmPhaseFromES(phases.warm), cold: coldPhaseFromES(phases.cold), + frozen: frozenPhaseFromES(phases.frozen), delete: deletePhaseFromES(phases.delete), }, }; @@ -94,6 +98,13 @@ export const serializePolicy = ( serializedPolicy.phases.cold = coldPhaseToES(policy.phases.cold, originalEsPolicy.phases.cold); } + if (policy.phases.frozen.phaseEnabled) { + serializedPolicy.phases.frozen = frozenPhaseToES( + policy.phases.frozen, + originalEsPolicy.phases.frozen + ); + } + if (policy.phases.delete.phaseEnabled) { serializedPolicy.phases.delete = deletePhaseToES( policy.phases.delete, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts index 545488be2cd5e0..6fdbc4babd3f3f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts @@ -9,7 +9,17 @@ import { validateHotPhase } from './hot_phase'; import { validateWarmPhase } from './warm_phase'; import { validateColdPhase } from './cold_phase'; import { validateDeletePhase } from './delete_phase'; -import { ColdPhase, DeletePhase, HotPhase, Phase, Policy, PolicyFromES, WarmPhase } from './types'; +import { validateFrozenPhase } from './frozen_phase'; + +import { + ColdPhase, + DeletePhase, + FrozenPhase, + HotPhase, + Policy, + PolicyFromES, + WarmPhase, +} from './types'; export const propertyof = (propertyName: keyof T & string) => propertyName; @@ -100,7 +110,7 @@ export const policyNameAlreadyUsedErrorMessage = i18n.translate( defaultMessage: 'That policy name is already used.', } ); -export type PhaseValidationErrors = { +export type PhaseValidationErrors = { [P in keyof Partial]: string[]; }; @@ -108,6 +118,7 @@ export interface ValidationErrors { hot: PhaseValidationErrors; warm: PhaseValidationErrors; cold: PhaseValidationErrors; + frozen: PhaseValidationErrors; delete: PhaseValidationErrors; policyName: string[]; } @@ -148,12 +159,14 @@ export const validatePolicy = ( const hotPhaseErrors = validateHotPhase(policy.phases.hot); const warmPhaseErrors = validateWarmPhase(policy.phases.warm); const coldPhaseErrors = validateColdPhase(policy.phases.cold); + const frozenPhaseErrors = validateFrozenPhase(policy.phases.frozen); const deletePhaseErrors = validateDeletePhase(policy.phases.delete); const isValid = policyNameErrors.length === 0 && Object.keys(hotPhaseErrors).length === 0 && Object.keys(warmPhaseErrors).length === 0 && Object.keys(coldPhaseErrors).length === 0 && + Object.keys(frozenPhaseErrors).length === 0 && Object.keys(deletePhaseErrors).length === 0; return [ isValid, @@ -162,6 +175,7 @@ export const validatePolicy = ( hot: hotPhaseErrors, warm: warmPhaseErrors, cold: coldPhaseErrors, + frozen: frozenPhaseErrors, delete: deletePhaseErrors, }, ]; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts index 2e2ed5b38bb871..3d4c73cf4a82cf 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts @@ -13,6 +13,7 @@ export interface Phases { hot?: SerializedHotPhase; warm?: SerializedWarmPhase; cold?: SerializedColdPhase; + frozen?: SerializedFrozenPhase; delete?: SerializedDeletePhase; } @@ -68,6 +69,16 @@ export interface SerializedColdPhase extends SerializedPhase { }; } +export interface SerializedFrozenPhase extends SerializedPhase { + actions: { + freeze?: {}; + allocate?: AllocateAction; + set_priority?: { + priority: number | null; + }; + }; +} + export interface SerializedDeletePhase extends SerializedPhase { actions: { wait_for_snapshot?: { @@ -94,47 +105,66 @@ export interface Policy { hot: HotPhase; warm: WarmPhase; cold: ColdPhase; + frozen: FrozenPhase; delete: DeletePhase; }; } -export interface Phase { +export interface CommonPhaseSettings { phaseEnabled: boolean; } -export interface HotPhase extends Phase { + +export interface PhaseWithMinAge { + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; +} + +export interface PhaseWithAllocationAction { + selectedNodeAttrs: string; + selectedReplicaCount: string; +} + +export interface PhaseWithIndexPriority { + phaseIndexPriority: string; +} + +export interface HotPhase extends CommonPhaseSettings, PhaseWithIndexPriority { rolloverEnabled: boolean; selectedMaxSizeStored: string; selectedMaxSizeStoredUnits: string; selectedMaxDocuments: string; selectedMaxAge: string; selectedMaxAgeUnits: string; - phaseIndexPriority: string; } -export interface WarmPhase extends Phase { +export interface WarmPhase + extends CommonPhaseSettings, + PhaseWithMinAge, + PhaseWithAllocationAction, + PhaseWithIndexPriority { warmPhaseOnRollover: boolean; - selectedMinimumAge: string; - selectedMinimumAgeUnits: string; - selectedNodeAttrs: string; - selectedReplicaCount: string; shrinkEnabled: boolean; selectedPrimaryShardCount: string; forceMergeEnabled: boolean; selectedForceMergeSegments: string; - phaseIndexPriority: string; } -export interface ColdPhase extends Phase { - selectedMinimumAge: string; - selectedMinimumAgeUnits: string; - selectedNodeAttrs: string; - selectedReplicaCount: string; +export interface ColdPhase + extends CommonPhaseSettings, + PhaseWithMinAge, + PhaseWithAllocationAction, + PhaseWithIndexPriority { freezeEnabled: boolean; - phaseIndexPriority: string; } -export interface DeletePhase extends Phase { - selectedMinimumAge: string; - selectedMinimumAgeUnits: string; +export interface FrozenPhase + extends CommonPhaseSettings, + PhaseWithMinAge, + PhaseWithAllocationAction, + PhaseWithIndexPriority { + freezeEnabled: boolean; +} + +export interface DeletePhase extends CommonPhaseSettings, PhaseWithMinAge { waitForSnapshotPolicy: string; } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js index a1eac5264bb6a8..8d01f4a4c200e9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js @@ -176,6 +176,12 @@ export const ilmFilterExtension = (indices) => { defaultMessage: 'Warm', }), }, + { + value: 'frozen', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.frozenLabel', { + defaultMessage: 'Frozen', + }), + }, { value: 'cold', view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.coldLabel', { diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts index 2d02802119e47b..9b51164fd4c28e 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -104,6 +104,23 @@ const coldPhaseSchema = schema.maybe( }) ); +const frozenPhaseSchema = schema.maybe( + schema.object({ + min_age: minAgeSchema, + actions: schema.object({ + set_priority: setPrioritySchema, + unfollow: unfollowSchema, + allocate: allocateSchema, + freeze: schema.maybe(schema.object({})), // Freeze has no options + searchable_snapshot: schema.maybe( + schema.object({ + snapshot_repository: schema.string(), + }) + ), + }), + }) +); + const deletePhaseSchema = schema.maybe( schema.object({ min_age: minAgeSchema, @@ -129,6 +146,7 @@ const bodySchema = schema.object({ hot: hotPhaseSchema, warm: warmPhaseSchema, cold: coldPhaseSchema, + frozen: frozenPhaseSchema, delete: deletePhaseSchema, }), }); From 647f397c50a74cf72c268a432e01311745a5b303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 31 Aug 2020 10:46:29 +0100 Subject: [PATCH 003/106] [APM] Chart units don't update when toggling the chart legends (#74931) * changing unit when legend is disabled * changing unit when legend is disabled * show individual units in the tooltip * addressing PR comment * increasing duration threshold * change formatter based on available legends * addressing PR comment Co-authored-by: Elastic Machine --- .../shared/charts/CustomPlot/index.js | 5 + .../TransactionCharts/BrowserLineChart.tsx | 14 +- .../TransactionLineChart/index.tsx | 16 +- .../charts/TransactionCharts/helper.test.ts | 69 +++++ .../charts/TransactionCharts/helper.tsx | 35 +++ .../shared/charts/TransactionCharts/index.tsx | 277 ++++++------------ .../charts/TransactionCharts/ml_header.tsx | 96 ++++++ .../TransactionCharts/use_formatter.test.tsx | 109 +++++++ .../charts/TransactionCharts/use_formatter.ts | 30 ++ .../formatters/__test__/duration.test.ts | 7 +- .../apm/public/utils/formatters/duration.ts | 4 +- 11 files changed, 457 insertions(+), 205 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.test.ts create mode 100644 x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js index 7e74961e57ea1c..41925d651e3617 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js @@ -79,6 +79,10 @@ export class InnerCustomPlot extends PureComponent { return i === _i ? !disabledValue : !!disabledValue; }); + if (typeof this.props.onToggleLegend === 'function') { + this.props.onToggleLegend(nextSeriesEnabledState); + } + return { seriesEnabledState: nextSeriesEnabledState, }; @@ -235,6 +239,7 @@ InnerCustomPlot.propTypes = { }) ), noHits: PropTypes.bool, + onToggleLegend: PropTypes.func, }; InnerCustomPlot.defaultProps = { diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx index 0e19c63775d311..40caf351559183 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiTitle } from '@elastic/eui'; -import { TransactionLineChart } from './TransactionLineChart'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useAvgDurationByBrowser } from '../../../../hooks/useAvgDurationByBrowser'; +import { getDurationFormatter } from '../../../../utils/formatters'; import { - getMaxY, getResponseTimeTickFormatter, getResponseTimeTooltipFormatter, -} from '.'; -import { getDurationFormatter } from '../../../../utils/formatters'; -import { useAvgDurationByBrowser } from '../../../../hooks/useAvgDurationByBrowser'; + getMaxY, +} from './helper'; +import { TransactionLineChart } from './TransactionLineChart'; export function BrowserLineChart() { const { data } = useAvgDurationByBrowser(); diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx index eaad883d2f9f63..07b7f01194d5c8 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx @@ -5,22 +5,13 @@ */ import React, { useCallback } from 'react'; -import { - Coordinate, - RectCoordinate, -} from '../../../../../../typings/timeseries'; +import { Coordinate, TimeSeries } from '../../../../../../typings/timeseries'; import { useChartsSync } from '../../../../../hooks/useChartsSync'; // @ts-ignore import CustomPlot from '../../CustomPlot'; interface Props { - series: Array<{ - color: string; - title: React.ReactNode; - titleShort?: React.ReactNode; - data: Array; - type: string; - }>; + series: TimeSeries[]; truncateLegends?: boolean; tickFormatY: (y: number) => React.ReactNode; formatTooltipValue: (c: Coordinate) => React.ReactNode; @@ -28,6 +19,7 @@ interface Props { height?: number; stacked?: boolean; onHover?: () => void; + onToggleLegend?: (disabledSeriesState: boolean[]) => void; } function TransactionLineChart(props: Props) { @@ -40,6 +32,7 @@ function TransactionLineChart(props: Props) { truncateLegends, stacked = false, onHover, + onToggleLegend, } = props; const syncedChartsProps = useChartsSync(); @@ -66,6 +59,7 @@ function TransactionLineChart(props: Props) { height={height} truncateLegends={truncateLegends} {...(stacked ? { stackBy: 'y' } : {})} + onToggleLegend={onToggleLegend} /> ); } diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.test.ts b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.test.ts new file mode 100644 index 00000000000000..a476892fa4a3f4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { + getResponseTimeTickFormatter, + getResponseTimeTooltipFormatter, + getMaxY, +} from './helper'; +import { + getDurationFormatter, + toMicroseconds, +} from '../../../../utils/formatters'; +import { TimeSeries } from '../../../../../typings/timeseries'; + +describe('transaction chart helper', () => { + describe('getResponseTimeTickFormatter', () => { + it('formattes time tick in minutes', () => { + const formatter = getDurationFormatter(toMicroseconds(11, 'minutes')); + const timeTickFormatter = getResponseTimeTickFormatter(formatter); + expect(timeTickFormatter(toMicroseconds(60, 'seconds'))).toEqual( + '1.0 min' + ); + }); + it('formattes time tick in seconds', () => { + const formatter = getDurationFormatter(toMicroseconds(11, 'seconds')); + const timeTickFormatter = getResponseTimeTickFormatter(formatter); + expect(timeTickFormatter(toMicroseconds(6, 'seconds'))).toEqual('6.0 s'); + }); + }); + describe('getResponseTimeTooltipFormatter', () => { + const formatter = getDurationFormatter(toMicroseconds(11, 'minutes')); + const tooltipFormatter = getResponseTimeTooltipFormatter(formatter); + it("doesn't format invalid y coordinate", () => { + expect(tooltipFormatter({ x: 1, y: undefined })).toEqual('N/A'); + expect(tooltipFormatter({ x: 1, y: null })).toEqual('N/A'); + }); + it('formattes tooltip in minutes', () => { + expect( + tooltipFormatter({ x: 1, y: toMicroseconds(60, 'seconds') }) + ).toEqual('1.0 min'); + }); + }); + describe('getMaxY', () => { + it('returns zero when empty time series', () => { + expect(getMaxY([])).toEqual(0); + }); + it('returns zero for invalid y coordinate', () => { + const timeSeries = ([ + { data: [{ x: 1 }, { x: 2 }, { x: 3, y: -1 }] }, + ] as unknown) as TimeSeries[]; + expect(getMaxY(timeSeries)).toEqual(0); + }); + it('returns the max y coordinate', () => { + const timeSeries = ([ + { + data: [ + { x: 1, y: 10 }, + { x: 2, y: 5 }, + { x: 3, y: 1 }, + ], + }, + ] as unknown) as TimeSeries[]; + expect(getMaxY(timeSeries)).toEqual(10); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.tsx new file mode 100644 index 00000000000000..f11a33f932553b --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.tsx @@ -0,0 +1,35 @@ +/* + * 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 { flatten } from 'lodash'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; +import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; +import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; +import { TimeFormatter } from '../../../../utils/formatters'; + +export function getResponseTimeTickFormatter(formatter: TimeFormatter) { + return (t: number) => { + return formatter(t).formatted; + }; +} + +export function getResponseTimeTooltipFormatter(formatter: TimeFormatter) { + return (coordinate: Coordinate) => { + return isValidCoordinateValue(coordinate.y) + ? formatter(coordinate.y).formatted + : NOT_AVAILABLE_LABEL; + }; +} + +export function getMaxY(timeSeries: TimeSeries[]) { + const coordinates = flatten( + timeSeries.map((serie: TimeSeries) => serie.data as Coordinate[]) + ); + + const numbers: number[] = coordinates.map((c: Coordinate) => (c.y ? c.y : 0)); + + return Math.max(...numbers, 0); +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 1f80dbf5f4d95f..d11925dc0303d2 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -8,38 +8,34 @@ import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, - EuiIconTip, EuiPanel, - EuiText, - EuiTitle, EuiSpacer, + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Location } from 'history'; -import React, { Component } from 'react'; -import { isEmpty, flatten } from 'lodash'; -import styled from 'styled-components'; +import React from 'react'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; -import { ITransactionChartData } from '../../../../selectors/chartSelectors'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { - tpmUnit, - TimeFormatter, - getDurationFormatter, - asDecimal, -} from '../../../../utils/formatters'; -import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, + TRANSACTION_ROUTE_CHANGE, +} from '../../../../../common/transaction_types'; +import { Coordinate } from '../../../../../typings/timeseries'; import { LicenseContext } from '../../../../context/LicenseContext'; -import { TransactionLineChart } from './TransactionLineChart'; +import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import { ITransactionChartData } from '../../../../selectors/chartSelectors'; +import { asDecimal, tpmUnit } from '../../../../utils/formatters'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { BrowserLineChart } from './BrowserLineChart'; import { DurationByCountryMap } from './DurationByCountryMap'; import { - TRANSACTION_PAGE_LOAD, - TRANSACTION_ROUTE_CHANGE, - TRANSACTION_REQUEST, -} from '../../../../../common/transaction_types'; + getResponseTimeTickFormatter, + getResponseTimeTooltipFormatter, +} from './helper'; +import { MLHeader } from './ml_header'; +import { TransactionLineChart } from './TransactionLineChart'; +import { useFormatter } from './use_formatter'; interface TransactionChartProps { charts: ITransactionChartData; @@ -47,181 +43,96 @@ interface TransactionChartProps { urlParams: IUrlParams; } -const ShiftedIconWrapper = styled.span` - padding-right: 5px; - position: relative; - top: -1px; - display: inline-block; -`; - -const ShiftedEuiText = styled(EuiText)` - position: relative; - top: 5px; -`; - -export function getResponseTimeTickFormatter(formatter: TimeFormatter) { - return (t: number) => formatter(t).formatted; -} - -export function getResponseTimeTooltipFormatter(formatter: TimeFormatter) { - return (p: Coordinate) => { - return isValidCoordinateValue(p.y) - ? formatter(p.y).formatted - : NOT_AVAILABLE_LABEL; - }; -} - -export function getMaxY(responseTimeSeries: TimeSeries[]) { - const coordinates = flatten( - responseTimeSeries.map((serie: TimeSeries) => serie.data as Coordinate[]) - ); - - const numbers: number[] = coordinates.map((c: Coordinate) => (c.y ? c.y : 0)); - - return Math.max(...numbers, 0); -} - -export class TransactionCharts extends Component { - public getTPMFormatter = (t: number) => { - const { urlParams } = this.props; +export function TransactionCharts({ + charts, + location, + urlParams, +}: TransactionChartProps) { + const getTPMFormatter = (t: number) => { const unit = tpmUnit(urlParams.transactionType); return `${asDecimal(t)} ${unit}`; }; - public getTPMTooltipFormatter = (p: Coordinate) => { + const getTPMTooltipFormatter = (p: Coordinate) => { return isValidCoordinateValue(p.y) - ? this.getTPMFormatter(p.y) + ? getTPMFormatter(p.y) : NOT_AVAILABLE_LABEL; }; - public renderMLHeader(hasValidMlLicense: boolean | undefined) { - const { mlJobId } = this.props.charts; - - if (!hasValidMlLicense || !mlJobId) { - return null; - } - - const { serviceName, kuery, transactionType } = this.props.urlParams; - if (!serviceName) { - return null; - } + const { transactionType } = urlParams; - const hasKuery = !isEmpty(kuery); - const icon = hasKuery ? ( - - ) : ( - - ); - - return ( - - - {icon} - - {i18n.translate( - 'xpack.apm.metrics.transactionChart.machineLearningLabel', - { - defaultMessage: 'Machine learning:', - } - )}{' '} - - - View Job - - - - ); - } + const { responseTimeSeries, tpmSeries } = charts; - public render() { - const { charts, urlParams } = this.props; - const { responseTimeSeries, tpmSeries } = charts; - const { transactionType } = urlParams; - const maxY = getMaxY(responseTimeSeries); - const formatter = getDurationFormatter(maxY); + const { formatter, setDisabledSeriesState } = useFormatter( + responseTimeSeries + ); - return ( - <> - - - - - - - - {responseTimeLabel(transactionType)} - - - - {(license) => - this.renderMLHeader(license?.getFeature('ml').isAvailable) - } - - - + + + + + + + + {responseTimeLabel(transactionType)} + + + + {(license) => ( + )} - /> - - - - - - - - - {tpmLabel(transactionType)} - - - - - - - {transactionType === TRANSACTION_PAGE_LOAD && ( - <> - - - - - - - - - - - - - - - )} - - ); - } + + + + + + + + + + + + {tpmLabel(transactionType)} + + + + + + + {transactionType === TRANSACTION_PAGE_LOAD && ( + <> + + + + + + + + + + + + + + + )} + + ); } function tpmLabel(type?: string) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx new file mode 100644 index 00000000000000..f829b5841efa93 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx @@ -0,0 +1,96 @@ +/* + * 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 { EuiIconTip } from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import React from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; + +interface Props { + hasValidMlLicense?: boolean; + mlJobId?: string; +} + +const ShiftedIconWrapper = styled.span` + padding-right: 5px; + position: relative; + top: -1px; + display: inline-block; +`; + +const ShiftedEuiText = styled(EuiText)` + position: relative; + top: 5px; +`; + +export function MLHeader({ hasValidMlLicense, mlJobId }: Props) { + const { urlParams } = useUrlParams(); + + if (!hasValidMlLicense || !mlJobId) { + return null; + } + + const { serviceName, kuery, transactionType } = urlParams; + if (!serviceName) { + return null; + } + + const hasKuery = !isEmpty(kuery); + const icon = hasKuery ? ( +