From c9529201717a1bd5ef8534e9fa6eefec60b9ec61 Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Tue, 4 Jun 2019 12:04:43 +0300 Subject: [PATCH] [Vis: Default editor] EUIficate IP Ranges (#36896) * Create IpRangeType and IpRanges controls * Add validation * Refactoring * Add behavior when discarding changes * Refactoring: create common input list * Remove old template * Move add btn to input_list, add placeholder * Remove unused directives * Remove unused translations * Refactoring * Use EuiButtonGroup instead of toggle button * Update options ids, add aria-labels * Remove unused translations * Update mask model, update TS, update aria labels * Add validation for CIRD mask --- .../directives/validate_cidr_mask.js | 104 -------- .../__tests__/directives/validate_ip.js | 96 -------- .../ui/public/agg_types/buckets/ip_range.js | 8 +- .../controls/components/from_to_list.tsx | 124 ++++++++++ .../controls/components/input_list.tsx | 231 ++++++++++++++++++ .../controls/components/mask_list.tsx | 101 ++++++++ .../agg_types/controls/ip_range_type.tsx | 67 +++++ .../public/agg_types/controls/ip_ranges.html | 154 ------------ .../public/agg_types/controls/ip_ranges.tsx | 70 ++++++ .../directives/validate_cidr_mask.js | 50 ---- .../agg_types/directives/validate_ip.js | 55 ----- .../ui/public/utils/__tests__/cidr_mask.ts | 2 + src/legacy/ui/public/utils/cidr_mask.ts | 2 +- .../translations/translations/ja-JP.json | 13 - .../translations/translations/zh-CN.json | 13 - 15 files changed, 600 insertions(+), 490 deletions(-) delete mode 100644 src/legacy/ui/public/agg_types/__tests__/directives/validate_cidr_mask.js delete mode 100644 src/legacy/ui/public/agg_types/__tests__/directives/validate_ip.js create mode 100644 src/legacy/ui/public/agg_types/controls/components/from_to_list.tsx create mode 100644 src/legacy/ui/public/agg_types/controls/components/input_list.tsx create mode 100644 src/legacy/ui/public/agg_types/controls/components/mask_list.tsx create mode 100644 src/legacy/ui/public/agg_types/controls/ip_range_type.tsx delete mode 100644 src/legacy/ui/public/agg_types/controls/ip_ranges.html create mode 100644 src/legacy/ui/public/agg_types/controls/ip_ranges.tsx delete mode 100644 src/legacy/ui/public/agg_types/directives/validate_cidr_mask.js delete mode 100644 src/legacy/ui/public/agg_types/directives/validate_ip.js diff --git a/src/legacy/ui/public/agg_types/__tests__/directives/validate_cidr_mask.js b/src/legacy/ui/public/agg_types/__tests__/directives/validate_cidr_mask.js deleted file mode 100644 index 3d5d2531ee85f..0000000000000 --- a/src/legacy/ui/public/agg_types/__tests__/directives/validate_cidr_mask.js +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../../directives/validate_cidr_mask'; - - -describe('Validate CIDR mask directive', function () { - let $compile; - let $rootScope; - const html = ''; - - beforeEach(ngMock.module('kibana')); - - beforeEach(ngMock.inject(function (_$compile_, _$rootScope_) { - $compile = _$compile_; - $rootScope = _$rootScope_; - })); - - it('should allow empty input', function () { - const element = $compile(html)($rootScope); - - $rootScope.value = ''; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = null; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = undefined; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - }); - - it('should allow valid CIDR masks', function () { - const element = $compile(html)($rootScope); - - $rootScope.value = '0.0.0.0/1'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = '128.0.0.1/31'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = '1.2.3.4/2'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = '67.129.65.201/27'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - }); - - it('should disallow invalid CIDR masks', function () { - const element = $compile(html)($rootScope); - - $rootScope.value = 'hello, world'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '0.0.0.0'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '0.0.0.0/0'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '0.0.0.0/33'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '256.0.0.0/32'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '0.0.0.0/32/32'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '1.2.3/1'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - }); -}); diff --git a/src/legacy/ui/public/agg_types/__tests__/directives/validate_ip.js b/src/legacy/ui/public/agg_types/__tests__/directives/validate_ip.js deleted file mode 100644 index ad4c91e9081bf..0000000000000 --- a/src/legacy/ui/public/agg_types/__tests__/directives/validate_ip.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../../directives/validate_ip'; - - -describe('Validate IP directive', function () { - let $compile; - let $rootScope; - const html = ''; - - beforeEach(ngMock.module('kibana')); - - beforeEach(ngMock.inject(function (_$compile_, _$rootScope_) { - $compile = _$compile_; - $rootScope = _$rootScope_; - })); - - it('should allow empty input', function () { - const element = $compile(html)($rootScope); - - $rootScope.value = ''; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = null; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = undefined; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - }); - - it('should allow valid IP addresses', function () { - const element = $compile(html)($rootScope); - - $rootScope.value = '0.0.0.0'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = '0.0.0.1'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = '126.45.211.34'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - - $rootScope.value = '255.255.255.255'; - $rootScope.$digest(); - expect(element.hasClass('ng-valid')).to.be.ok(); - }); - - it('should disallow invalid IP addresses', function () { - const element = $compile(html)($rootScope); - - $rootScope.value = 'hello, world'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '0.0.0'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '256.0.0.0'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = '-1.0.0.0'; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - - $rootScope.value = Number.MAX_VALUE; - $rootScope.$digest(); - expect(element.hasClass('ng-invalid')).to.be.ok(); - }); -}); diff --git a/src/legacy/ui/public/agg_types/buckets/ip_range.js b/src/legacy/ui/public/agg_types/buckets/ip_range.js index 621e52e1a99ec..023f2d3cdb520 100644 --- a/src/legacy/ui/public/agg_types/buckets/ip_range.js +++ b/src/legacy/ui/public/agg_types/buckets/ip_range.js @@ -18,11 +18,10 @@ */ import _ from 'lodash'; -import '../directives/validate_ip'; -import '../directives/validate_cidr_mask'; import { BucketAggType } from './_bucket_agg_type'; import { createFilterIpRange } from './create_filter/ip_range'; -import ipRangesTemplate from '../controls/ip_ranges.html'; +import { IpRangeTypeParamEditor } from '../controls/ip_range_type'; +import { IpRangesParamEditor } from '../controls/ip_ranges'; import { i18n } from '@kbn/i18n'; export const ipRangeBucketAgg = new BucketAggType({ @@ -52,6 +51,7 @@ export const ipRangeBucketAgg = new BucketAggType({ filterFieldTypes: 'ip' }, { name: 'ipRangeType', + editorComponent: IpRangeTypeParamEditor, default: 'fromTo', write: _.noop }, { @@ -66,7 +66,7 @@ export const ipRangeBucketAgg = new BucketAggType({ { mask: '128.0.0.0/2' } ] }, - editor: ipRangesTemplate, + editorComponent: IpRangesParamEditor, write: function (aggConfig, output) { const ipRangeType = aggConfig.params.ipRangeType; let ranges = aggConfig.params.ranges[ipRangeType]; diff --git a/src/legacy/ui/public/agg_types/controls/components/from_to_list.tsx b/src/legacy/ui/public/agg_types/controls/components/from_to_list.tsx new file mode 100644 index 0000000000000..99d689199e24c --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/components/from_to_list.tsx @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiFieldText, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import Ipv4Address from '../../../utils/ipv4_address'; +import { InputList, InputListConfig, InputModel, InputObject, InputItem } from './input_list'; + +const EMPTY_STRING = ''; + +export interface FromToObject extends InputObject { + from?: string; + to?: string; +} + +type FromToModel = InputModel & { + from: InputItem; + to: InputItem; +}; + +interface FromToListProps { + list: FromToObject[]; + showValidation: boolean; + onBlur(): void; + onChange(list: FromToObject[]): void; + setValidity(isValid: boolean): void; +} + +function FromToList({ showValidation, onBlur, ...rest }: FromToListProps) { + const fromToListConfig: InputListConfig = { + defaultValue: { + from: { value: '0.0.0.0', model: '0.0.0.0', isInvalid: false }, + to: { value: '255.255.255.255', model: '255.255.255.255', isInvalid: false }, + }, + defaultEmptyValue: { + from: { value: EMPTY_STRING, model: EMPTY_STRING, isInvalid: false }, + to: { value: EMPTY_STRING, model: EMPTY_STRING, isInvalid: false }, + }, + validateClass: Ipv4Address, + getModelValue: (item: FromToObject) => ({ + from: { + value: item.from || EMPTY_STRING, + model: item.from || EMPTY_STRING, + isInvalid: false, + }, + to: { value: item.to || EMPTY_STRING, model: item.to || EMPTY_STRING, isInvalid: false }, + }), + getRemoveBtnAriaLabel: (item: FromToModel) => + i18n.translate('common.ui.aggTypes.ipRanges.removeRangeAriaLabel', { + defaultMessage: 'Remove the range of {from} to {to}', + values: { from: item.from.value || '*', to: item.to.value || '*' }, + }), + onChangeFn: ({ from, to }: FromToModel) => { + const result: FromToObject = {}; + if (from.model) { + result.from = from.model; + } + if (to.model) { + result.to = to.model; + } + return result; + }, + hasInvalidValuesFn: ({ from, to }: FromToModel) => from.isInvalid || to.isInvalid, + renderInputRow: (item: FromToModel, index, onChangeValue) => ( + <> + + { + onChangeValue(index, ev.target.value, 'from'); + }} + value={item.from.value} + onBlur={onBlur} + /> + + + { + onChangeValue(index, ev.target.value, 'to'); + }} + value={item.to.value} + onBlur={onBlur} + /> + + + ), + validateModel: (validateFn, object: FromToObject, model: FromToModel) => { + validateFn(object.from, model.from); + validateFn(object.to, model.to); + }, + }; + + return ; +} + +export { FromToList }; diff --git a/src/legacy/ui/public/agg_types/controls/components/input_list.tsx b/src/legacy/ui/public/agg_types/controls/components/input_list.tsx new file mode 100644 index 0000000000000..68960ca396b3d --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/components/input_list.tsx @@ -0,0 +1,231 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState, useEffect, Fragment } from 'react'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + htmlIdGenerator, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface InputListConfig { + defaultValue: InputItemModel; + defaultEmptyValue: InputItemModel; + validateClass: new (value: string) => { toString(): string }; + getModelValue(item: InputObject): InputItemModel; + getRemoveBtnAriaLabel(model: InputModel): string; + onChangeFn(model: InputModel): InputObject; + hasInvalidValuesFn(model: InputModel): boolean; + renderInputRow( + model: InputModel, + index: number, + onChangeFn: (index: number, value: string, modelName: string) => void + ): React.ReactNode; + validateModel( + validateFn: (value: string | undefined, modelObj: InputItem) => void, + object: InputObject, + model: InputModel + ): void; +} +interface InputModelBase { + id: string; +} +export type InputObject = object; +export interface InputItem { + model: string; + value: string; + isInvalid: boolean; +} + +interface InputItemModel { + [model: string]: InputItem; +} + +// InputModel can have the following implementations: +// for Mask List - { id: 'someId', mask: { model: '', value: '', isInvalid: false }} +// for FromTo List - { id: 'someId', from: { model: '', value: '', isInvalid: false }, to: { model: '', value: '', isInvalid: false }} +export type InputModel = InputModelBase & InputItemModel; + +interface InputListProps { + config: InputListConfig; + list: InputObject[]; + onChange(list: InputObject[]): void; + setValidity(isValid: boolean): void; +} + +const generateId = htmlIdGenerator(); + +function InputList({ config, list, onChange, setValidity }: InputListProps) { + const [models, setModels] = useState( + list.length + ? list.map( + item => + ({ + id: generateId(), + ...config.getModelValue(item), + } as InputModel) + ) + : [ + { + id: generateId(), + ...config.defaultValue, + } as InputModel, + ] + ); + + const onUpdate = (modelList: InputModel[]) => { + setModels(modelList); + onChange(modelList.map(config.onChangeFn)); + }; + + const onChangeValue = (index: number, value: string, modelName: string) => { + const range = models[index][modelName]; + const { model, isInvalid } = validateValue(value); + range.value = value; + range.model = model; + range.isInvalid = isInvalid; + onUpdate(models); + }; + const onDelete = (id: string) => { + const newArray = models.filter(model => model.id !== id); + onUpdate(newArray); + }; + + const onAdd = () => { + const newArray = [ + ...models, + { + id: generateId(), + ...config.defaultEmptyValue, + } as InputModel, + ]; + onUpdate(newArray); + }; + + const getUpdatedModels = (objList: InputObject[], modelList: InputModel[]) => { + if (!objList.length) { + return modelList; + } + return objList.map((item, index) => { + const model = modelList[index] || { + id: generateId(), + ...config.getModelValue(item), + }; + + config.validateModel(validateItem, item, model); + + return model; + }); + }; + + const validateItem = (value: string | undefined, modelObj: InputItem) => { + const { model, isInvalid } = validateValue(value); + if (value !== modelObj.model) { + modelObj.value = model; + } + modelObj.model = model; + modelObj.isInvalid = isInvalid; + }; + + const validateValue = (inputValue: string | undefined) => { + const result = { + model: inputValue || '', + isInvalid: false, + }; + if (!inputValue) { + result.isInvalid = false; + return result; + } + try { + const InputObject = config.validateClass; + result.model = new InputObject(inputValue).toString(); + result.isInvalid = false; + return result; + } catch (e) { + result.isInvalid = true; + return result; + } + }; + + const hasInvalidValues = (modelList: InputModel[]) => { + return !!modelList.find(config.hasInvalidValuesFn); + }; + + // responsible for discarding changes + useEffect( + () => { + setModels(getUpdatedModels(list, models)); + }, + [list] + ); + + useEffect( + () => { + setValidity(!hasInvalidValues(models)); + }, + [models] + ); + + // resposible for setting up an initial value when there is no default value + useEffect(() => { + onChange(models.map(config.onChangeFn)); + }, []); + + if (!list || !list.length) { + return null; + } + + return ( + <> + {models.map((item, index) => ( + + + {config.renderInputRow(item, index, onChangeValue)} + + onDelete(item.id)} + /> + + + + + ))} + + + + + + + + ); +} + +export { InputList }; diff --git a/src/legacy/ui/public/agg_types/controls/components/mask_list.tsx b/src/legacy/ui/public/agg_types/controls/components/mask_list.tsx new file mode 100644 index 0000000000000..efb5ca7643c93 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/components/mask_list.tsx @@ -0,0 +1,101 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiFieldText, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { CidrMask } from '../../../utils/cidr_mask'; +import { InputList, InputListConfig, InputObject, InputModel, InputItem } from './input_list'; + +const EMPTY_STRING = ''; + +export interface MaskObject extends InputObject { + mask?: string; +} + +type MaskModel = InputModel & { + mask: InputItem; +}; + +interface MaskListProps { + list: MaskObject[]; + showValidation: boolean; + onBlur(): void; + onChange(list: MaskObject[]): void; + setValidity(isValid: boolean): void; +} + +function MaskList({ showValidation, onBlur, ...rest }: MaskListProps) { + const maskListConfig: InputListConfig = { + defaultValue: { + mask: { model: '0.0.0.0/1', value: '0.0.0.0/1', isInvalid: false }, + }, + defaultEmptyValue: { + mask: { model: EMPTY_STRING, value: EMPTY_STRING, isInvalid: false }, + }, + validateClass: CidrMask, + getModelValue: (item: MaskObject) => ({ + mask: { + model: item.mask || EMPTY_STRING, + value: item.mask || EMPTY_STRING, + isInvalid: false, + }, + }), + getRemoveBtnAriaLabel: (item: MaskModel) => + item.mask.value + ? i18n.translate('common.ui.aggTypes.ipRanges.removeCidrMaskButtonAriaLabel', { + defaultMessage: 'Remove the CIDR mask value of {mask}', + values: { mask: item.mask.value }, + }) + : i18n.translate('common.ui.aggTypes.ipRanges.removeEmptyCidrMaskButtonAriaLabel', { + defaultMessage: 'Remove the CIDR mask default value', + }), + onChangeFn: ({ mask }: MaskModel) => { + if (mask.model) { + return { mask: mask.model }; + } + return {}; + }, + hasInvalidValuesFn: ({ mask }) => mask.isInvalid, + renderInputRow: ({ mask }: MaskModel, index, onChangeValue) => ( + + { + onChangeValue(index, ev.target.value, 'mask'); + }} + value={mask.value} + onBlur={onBlur} + /> + + ), + validateModel: (validateFn, object: MaskObject, model: MaskModel) => { + validateFn(object.mask, model.mask); + }, + }; + + return ; +} + +export { MaskList }; diff --git a/src/legacy/ui/public/agg_types/controls/ip_range_type.tsx b/src/legacy/ui/public/agg_types/controls/ip_range_type.tsx new file mode 100644 index 0000000000000..24964785b72df --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/ip_range_type.tsx @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { EuiButtonGroup, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AggParamEditorProps } from '../../vis/editors/default'; + +enum IpRangeTypes { + MASK = 'mask', + FROM_TO = 'fromTo', +} + +function IpRangeTypeParamEditor({ agg, value, setValue }: AggParamEditorProps) { + const options = [ + { + id: `visEditorIpRangeFromToLabel${agg.id}`, + label: i18n.translate('common.ui.aggTypes.ipRanges.fromToButtonLabel', { + defaultMessage: 'From/to', + }), + }, + { + id: `visEditorIpRangeCidrLabel${agg.id}`, + label: i18n.translate('common.ui.aggTypes.ipRanges.cidrMasksButtonLabel', { + defaultMessage: 'CIDR masks', + }), + }, + ]; + + const onClick = (optionId: string) => { + setValue(optionId === options[0].id ? IpRangeTypes.FROM_TO : IpRangeTypes.MASK); + }; + + return ( + <> + + + + ); +} + +export { IpRangeTypeParamEditor, IpRangeTypes }; diff --git a/src/legacy/ui/public/agg_types/controls/ip_ranges.html b/src/legacy/ui/public/agg_types/controls/ip_ranges.html deleted file mode 100644 index 5450fae62cb19..0000000000000 --- a/src/legacy/ui/public/agg_types/controls/ip_ranges.html +++ /dev/null @@ -1,154 +0,0 @@ -
- -

- - -

- -
- -
- - - - - - - - - - - -
- - - -
- - - - - -
- - -
-

- - - -

-
- - -
- -
- - - - - - - - - -
- -
- - - -
- - -
-

- - - -

-
- - -
-
diff --git a/src/legacy/ui/public/agg_types/controls/ip_ranges.tsx b/src/legacy/ui/public/agg_types/controls/ip_ranges.tsx new file mode 100644 index 0000000000000..9a1b804432b76 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/ip_ranges.tsx @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiFormRow } from '@elastic/eui'; + +import { AggParamEditorProps } from 'ui/vis/editors/default'; +import { FromToList, FromToObject } from './components/from_to_list'; +import { MaskList, MaskObject } from './components/mask_list'; +import { IpRangeTypes } from './ip_range_type'; +interface IpRange { + fromTo: FromToObject[]; + mask: MaskObject[]; +} + +function IpRangesParamEditor({ + agg, + value = { fromTo: [] as FromToObject[], mask: [] as MaskObject[] }, + setTouched, + setValue, + setValidity, + showValidation, +}: AggParamEditorProps) { + const handleChange = (modelName: IpRangeTypes, items: Array) => { + setValue({ + ...value, + [modelName]: items, + }); + }; + + return ( + + {agg.params.ipRangeType === IpRangeTypes.MASK ? ( + handleChange(IpRangeTypes.MASK, items)} + setValidity={setValidity} + /> + ) : ( + handleChange(IpRangeTypes.FROM_TO, items)} + setValidity={setValidity} + /> + )} + + ); +} + +export { IpRangesParamEditor }; diff --git a/src/legacy/ui/public/agg_types/directives/validate_cidr_mask.js b/src/legacy/ui/public/agg_types/directives/validate_cidr_mask.js deleted file mode 100644 index 6e33f8a141865..0000000000000 --- a/src/legacy/ui/public/agg_types/directives/validate_cidr_mask.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { CidrMask } from '../../utils/cidr_mask'; -import { uiModules } from '../../modules'; - -uiModules.get('kibana').directive('validateCidrMask', function () { - return { - restrict: 'A', - require: 'ngModel', - scope: { - 'ngModel': '=' - }, - link: function ($scope, elem, attr, ngModel) { - ngModel.$parsers.unshift(validateCidrMask); - ngModel.$formatters.unshift(validateCidrMask); - - function validateCidrMask(mask) { - if (mask == null || mask === '') { - ngModel.$setValidity('cidrMaskInput', true); - return null; - } - - try { - mask = new CidrMask(mask); - ngModel.$setValidity('cidrMaskInput', true); - return mask.toString(); - } catch (e) { - ngModel.$setValidity('cidrMaskInput', false); - } - } - } - }; -}); diff --git a/src/legacy/ui/public/agg_types/directives/validate_ip.js b/src/legacy/ui/public/agg_types/directives/validate_ip.js deleted file mode 100644 index a5e8704fb45fc..0000000000000 --- a/src/legacy/ui/public/agg_types/directives/validate_ip.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Ipv4Address from '../../utils/ipv4_address'; -import { uiModules } from '../../modules'; - -uiModules - .get('kibana') - .directive('validateIp', function () { - return { - restrict: 'A', - require: 'ngModel', - scope: { - 'ngModel': '=', - }, - link: function ($scope, elem, attr, ngModel) { - function validateIp(ipAddress) { - if (ipAddress == null || ipAddress === '') { - ngModel.$setValidity('ipInput', true); - return null; - } - - try { - ipAddress = new Ipv4Address(ipAddress); - ngModel.$setValidity('ipInput', true); - return ipAddress.toString(); - } catch (e) { - ngModel.$setValidity('ipInput', false); - } - } - - // From User - ngModel.$parsers.unshift(validateIp); - - // To user - ngModel.$formatters.unshift(validateIp); - } - }; - }); diff --git a/src/legacy/ui/public/utils/__tests__/cidr_mask.ts b/src/legacy/ui/public/utils/__tests__/cidr_mask.ts index 5343328e55786..5277344448bd8 100644 --- a/src/legacy/ui/public/utils/__tests__/cidr_mask.ts +++ b/src/legacy/ui/public/utils/__tests__/cidr_mask.ts @@ -43,6 +43,8 @@ describe('CidrMask', () => { expect(() => new CidrMask('0.0.0.0/32/32')).to.throwError(); expect(() => new CidrMask('1.2.3/1')).to.throwError(); + + expect(() => new CidrMask('0.0.0.0/123d')).to.throwError(); }); it('should correctly grab IP address and prefix length', () => { diff --git a/src/legacy/ui/public/utils/cidr_mask.ts b/src/legacy/ui/public/utils/cidr_mask.ts index 8dee0ab4e0c12..856fe6fd221eb 100644 --- a/src/legacy/ui/public/utils/cidr_mask.ts +++ b/src/legacy/ui/public/utils/cidr_mask.ts @@ -35,7 +35,7 @@ export class CidrMask { } this.initialAddress = new Ipv4Address(splits[0]); this.prefixLength = Number(splits[1]); - if (this.prefixLength < 1 || this.prefixLength > NUM_BITS) { + if (isNaN(this.prefixLength) || this.prefixLength < 1 || this.prefixLength > NUM_BITS) { throwError(mask); } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5500c34b99d81..aa29b8474df72 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -145,19 +145,6 @@ "common.ui.aggTypes.filters.removeFilterButtonAriaLabel": "このフィルターを削除", "common.ui.aggTypes.filters.toggleFilterButtonAriaLabel": "フィルターラベルを切り替える", "common.ui.aggTypes.histogram.missingMaxMinValuesWarning": "自動スケールヒストグラムバケットから最高値と最低値を取得できません。これによりビジュアライゼーションのパフォーマンスが低下する可能性があります。", - "common.ui.aggTypes.ipRanges.cidrMask.addRangeButtonLabel": "範囲を追加", - "common.ui.aggTypes.ipRanges.cidrMask.requiredIpRangeDescription": "IP 範囲を最低 1 つ指定する必要があります。", - "common.ui.aggTypes.ipRanges.cidrMask.requiredIpRangeLabel": "必須:", - "common.ui.aggTypes.ipRanges.cidrMaskLabel": "CIDR マスク", - "common.ui.aggTypes.ipRanges.fromLabel": "開始:", - "common.ui.aggTypes.ipRanges.fromTo.addRangeButtonLabel": "範囲を追加", - "common.ui.aggTypes.ipRanges.fromTo.requiredIpRangeDescription": "IP 範囲を最低 1 つ指定する必要があります。", - "common.ui.aggTypes.ipRanges.fromTo.requiredIpRangeLabel": "必須:", - "common.ui.aggTypes.ipRanges.removeCidrMaskButtonAriaLabel": "CIDR マスクを削除", - "common.ui.aggTypes.ipRanges.removeRangeAriaLabel": "この範囲を削除", - "common.ui.aggTypes.ipRanges.toLabel": "終了:", - "common.ui.aggTypes.ipRanges.useCidrMasksButtonLabel": "CIDR マスクを使用", - "common.ui.aggTypes.ipRanges.useFromToButtonLabel": "開始/終了を使用", "common.ui.aggTypes.jsonInputLabel": "JSON インプット", "common.ui.aggTypes.jsonInputTooltip": "ここに追加された JSON フォーマットのプロパティは、すべてこのセクションの Elasticsearch アグリゲーション定義に融合されます。用語集約における「shard_size」がその例です。", "common.ui.aggTypes.metricLabel": "メトリック", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4e53177c5ff9f..6a2170ea5e774 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -144,19 +144,6 @@ "common.ui.aggTypes.filters.removeFilterButtonAriaLabel": "移除此筛选", "common.ui.aggTypes.filters.toggleFilterButtonAriaLabel": "切换筛选标签", "common.ui.aggTypes.histogram.missingMaxMinValuesWarning": "无法检索最大值和最小值以自动缩放直方图存储桶。这可能会导致可视化性能低下。", - "common.ui.aggTypes.ipRanges.cidrMask.addRangeButtonLabel": "添加范围", - "common.ui.aggTypes.ipRanges.cidrMask.requiredIpRangeDescription": "必须指定至少一个 IP 范围。", - "common.ui.aggTypes.ipRanges.cidrMask.requiredIpRangeLabel": "必需:", - "common.ui.aggTypes.ipRanges.cidrMaskLabel": "CIDR 掩码", - "common.ui.aggTypes.ipRanges.fromLabel": "从", - "common.ui.aggTypes.ipRanges.fromTo.addRangeButtonLabel": "添加范围", - "common.ui.aggTypes.ipRanges.fromTo.requiredIpRangeDescription": "必须指定至少一个 IP 范围。", - "common.ui.aggTypes.ipRanges.fromTo.requiredIpRangeLabel": "必需:", - "common.ui.aggTypes.ipRanges.removeCidrMaskButtonAriaLabel": "移除此 CIDR 掩码", - "common.ui.aggTypes.ipRanges.removeRangeAriaLabel": "移除此范围", - "common.ui.aggTypes.ipRanges.toLabel": "到", - "common.ui.aggTypes.ipRanges.useCidrMasksButtonLabel": "使用 CIDR 掩码", - "common.ui.aggTypes.ipRanges.useFromToButtonLabel": "使用“从”/“到”", "common.ui.aggTypes.jsonInputLabel": "JSON 输入", "common.ui.aggTypes.jsonInputTooltip": "此处以 JSON 格式添加的任何属性将与此部分的 elasticsearch 聚合定义合并。例如,词聚合上的“shard_size”。", "common.ui.aggTypes.metricLabel": "指标",