[TSVB] Type public code. Step 2 - panel configs (#94403)

* Remove request facade and update search strategies

* Use typescript

* Type files

* Update structure

* Update tests

* Type annotations

* Fix type for infra

* Type editor_controller

* Type vis_editor

* Type vis_picker

* Fix types

* Type panel_config

* Fix vis data type

* Enhance types

* Remove generics

* Use constant

* Update docs

* Use empty object as default data

* Convert yes_no component to typescript

* Type color rules

* Type panel configs

* Type helpers

* Type color rules

* Type collection actions

* Get rid of create_text_handler

* Fix collection actions types

* Revert get_request_params changes, do some code refactoring, type create_number_handler and get rid of detect_ie

Co-authored-by: Daniil Suleiman <daniil_suleiman@epam.com>
Co-authored-by: Diana Derevyankina <dziyana_dzeraviankina@epam.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Daniil 2021-03-23 16:31:50 +03:00 committed by GitHub
parent 5e31f91614
commit 8961f8523e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 714 additions and 711 deletions

View file

@ -10,6 +10,7 @@ import { schema } from '@kbn/config-schema';
import { TypeOptions } from '@kbn/config-schema/target/types/types';
const stringOptionalNullable = schema.maybe(schema.nullable(schema.string()));
const stringOptional = schema.maybe(schema.string());
const stringRequired = schema.string();
@ -205,23 +206,15 @@ export const panel = schema.object({
background_color_rules: schema.maybe(schema.arrayOf(backgroundColorRulesItems)),
default_index_pattern: stringOptionalNullable,
default_timefield: stringOptionalNullable,
drilldown_url: stringOptionalNullable,
drilldown_url: stringOptional,
drop_last_bucket: numberIntegerOptional,
filter: schema.nullable(
schema.oneOf([
stringOptionalNullable,
schema.object({
language: stringOptionalNullable,
query: stringOptionalNullable,
}),
])
),
filter: schema.maybe(queryObject),
gauge_color_rules: schema.maybe(schema.arrayOf(gaugeColorRulesItems)),
gauge_width: schema.nullable(schema.oneOf([stringOptionalNullable, numberOptional])),
gauge_inner_color: stringOptionalNullable,
gauge_inner_width: stringOrNumberOptionalNullable,
gauge_style: stringOptionalNullable,
gauge_max: stringOrNumberOptionalNullable,
gauge_max: numberOptionalOrEmptyString,
id: stringRequired,
ignore_global_filters: numberOptional,
ignore_global_filter: numberOptional,

View file

@ -13,7 +13,6 @@ import { EuiDraggable, EuiDroppable } from '@elastic/eui';
import { Agg } from './agg';
// @ts-ignore
import { seriesChangeHandler } from '../lib/series_change_handler';
// @ts-ignore
import { handleAdd, handleDelete } from '../lib/collection_actions';
import { newMetricAggFn } from '../lib/new_metric_agg_fn';
import { PanelSchema, SeriesItemsSchema } from '../../../../common/types';
@ -23,10 +22,12 @@ import { IFieldType } from '../../../../../data/common/index_patterns/fields';
const DROPPABLE_ID = 'aggs_dnd';
export interface AggsProps {
name: keyof SeriesItemsSchema;
panel: PanelSchema;
model: SeriesItemsSchema;
fields: IFieldType[];
uiRestrictions: TimeseriesUIRestrictions;
onChange(): void;
}
export class Aggs extends PureComponent<AggsProps> {

View file

@ -21,7 +21,7 @@ interface FieldSelectProps {
type: string;
fields: Record<string, SanitizedFieldType[]>;
indexPattern: string;
value: string;
value?: string | null;
onChange: (options: Array<EuiComboBoxOptionOption<string>>) => void;
disabled?: boolean;
restrict?: string[];

View file

@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
export interface PercentileHdrProps {
value: number | undefined;
onChange: () => void;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
export const PercentileHdr = ({ value, onChange }: PercentileHdrProps) => (

View file

@ -17,19 +17,16 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { AggSelect } from '../agg_select';
// @ts-ignore
import { FieldSelect } from '../field_select';
// @ts-ignore
import { createChangeHandler } from '../../lib/create_change_handler';
// @ts-ignore
import { createSelectHandler } from '../../lib/create_select_handler';
// @ts-ignore
import { createNumberHandler } from '../../lib/create_number_handler';
import { AggRow } from '../agg_row';
import { PercentileRankValues } from './percentile_rank_values';
import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public';
import { KBN_FIELD_TYPES } from '../../../../../../data/public';
import { MetricsItemsSchema, PanelSchema, SanitizedFieldType } from '../../../../../common/types';
import { DragHandleProps } from '../../../../types';
import { PercentileHdr } from '../percentile_hdr';

View file

@ -26,7 +26,7 @@ interface ColorProps {
export interface ColorPickerProps {
name: string;
value: string | null;
value?: string | null;
disableTrash?: boolean;
onChange: (props: ColorProps) => void;
}
@ -39,16 +39,12 @@ export function ColorPicker({ name, value, disableTrash = false, onChange }: Col
const handleColorChange: EuiColorPickerProps['onChange'] = (text: string, { rgba, hex }) => {
setColor(text);
const part: ColorProps = {};
part[name] = hex ? `rgba(${rgba.join(',')})` : '';
onChange(part);
onChange({ [name]: hex ? `rgba(${rgba.join(',')})` : '' });
};
const handleClear = () => {
setColor('');
const part: ColorProps = {};
part[name] = null;
onChange(part);
onChange({ [name]: null });
};
const label = value

View file

@ -7,59 +7,58 @@
*/
import React from 'react';
import { collectionActions } from './lib/collection_actions';
import { ColorRules } from './color_rules';
import { keys } from '@elastic/eui';
import { findTestSubject } from '@elastic/eui/lib/test';
import { mountWithIntl } from '@kbn/test/jest';
import { collectionActions } from './lib/collection_actions';
import { ColorRules, ColorRulesProps } from './color_rules';
describe('src/legacy/core_plugins/metrics/public/components/color_rules.test.js', () => {
let defaultProps;
beforeAll(() => {
defaultProps = {
name: 'gauge_color_rules',
model: {
gauge_color_rules: [
{
gauge: null,
value: 0,
id: 'unique value',
},
],
},
onChange: jest.fn(),
};
});
const defaultProps = ({
name: 'gauge_color_rules',
model: {
gauge_color_rules: [
{
gauge: null,
value: 0,
id: 'unique value',
},
],
},
onChange: jest.fn(),
} as unknown) as ColorRulesProps;
describe('ColorRules', () => {
it('should render empty <div/> node', () => {
const emptyProps = {
const emptyProps = ({
name: 'gauge_color_rules',
model: {},
onChange: jest.fn(),
};
const wrapper = mountWithIntl(<ColorRules.WrappedComponent {...emptyProps} />);
} as unknown) as ColorRulesProps;
const wrapper = mountWithIntl(<ColorRules {...emptyProps} />);
const isNode = wrapper.find('div').children().exists();
expect(isNode).toBeFalsy();
});
it('should render non-empty <div/> node', () => {
const wrapper = mountWithIntl(<ColorRules.WrappedComponent {...defaultProps} />);
const wrapper = mountWithIntl(<ColorRules {...defaultProps} />);
const isNode = wrapper.find('div.tvbColorPicker').exists();
expect(isNode).toBeTruthy();
});
it('should handle change of operator and value correctly', () => {
collectionActions.handleChange = jest.fn();
const wrapper = mountWithIntl(<ColorRules.WrappedComponent {...defaultProps} />);
const wrapper = mountWithIntl(<ColorRules {...defaultProps} />);
const operatorInput = findTestSubject(wrapper, 'colorRuleOperator');
operatorInput.simulate('keyDown', { key: keys.ARROW_DOWN });
operatorInput.simulate('keyDown', { key: keys.ARROW_DOWN });
operatorInput.simulate('keyDown', { key: keys.ENTER });
expect(collectionActions.handleChange.mock.calls[0][1].operator).toEqual('gt');
expect((collectionActions.handleChange as jest.Mock).mock.calls[0][1].operator).toEqual('gt');
const numberInput = findTestSubject(wrapper, 'colorRuleValue');
numberInput.simulate('change', { target: { value: '123' } });
expect(collectionActions.handleChange.mock.calls[1][1].value).toEqual(123);
expect((collectionActions.handleChange as jest.Mock).mock.calls[1][1].value).toEqual(123);
});
});
});

View file

@ -6,12 +6,7 @@
* Side Public License, v 1.
*/
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import _ from 'lodash';
import { AddDeleteButtons } from './add_delete_buttons';
import { collectionActions } from './lib/collection_actions';
import { ColorPicker } from './color_picker';
import {
htmlIdGenerator,
EuiComboBox,
@ -19,76 +14,117 @@ import {
EuiFormLabel,
EuiFlexGroup,
EuiFlexItem,
EuiComboBoxOptionOption,
} from '@elastic/eui';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
class ColorRulesUI extends Component {
constructor(props) {
import { AddDeleteButtons } from './add_delete_buttons';
import { collectionActions } from './lib/collection_actions';
import { ColorPicker, ColorPickerProps } from './color_picker';
import { TimeseriesVisParams } from '../../types';
export interface ColorRulesProps {
name: keyof TimeseriesVisParams;
model: TimeseriesVisParams;
onChange: (partialModel: Partial<TimeseriesVisParams>) => void;
primaryName?: string;
primaryVarName?: string;
secondaryName?: string;
secondaryVarName?: string;
hideSecondary?: boolean;
}
interface ColorRule {
value?: number;
id: string;
background_color?: string;
color?: string;
operator?: string;
text?: string;
}
const defaultSecondaryName = i18n.translate(
'visTypeTimeseries.colorRules.defaultSecondaryNameLabel',
{
defaultMessage: 'text',
}
);
const defaultPrimaryName = i18n.translate('visTypeTimeseries.colorRules.defaultPrimaryNameLabel', {
defaultMessage: 'background',
});
const operatorOptions = [
{
label: i18n.translate('visTypeTimeseries.colorRules.greaterThanLabel', {
defaultMessage: '> greater than',
}),
value: 'gt',
},
{
label: i18n.translate('visTypeTimeseries.colorRules.greaterThanOrEqualLabel', {
defaultMessage: '>= greater than or equal',
}),
value: 'gte',
},
{
label: i18n.translate('visTypeTimeseries.colorRules.lessThanLabel', {
defaultMessage: '< less than',
}),
value: 'lt',
},
{
label: i18n.translate('visTypeTimeseries.colorRules.lessThanOrEqualLabel', {
defaultMessage: '<= less than or equal',
}),
value: 'lte',
},
];
export class ColorRules extends Component<ColorRulesProps> {
constructor(props: ColorRulesProps) {
super(props);
this.renderRow = this.renderRow.bind(this);
}
handleChange(item, name, cast = String) {
return (e) => {
const handleChange = collectionActions.handleChange.bind(null, this.props);
const part = {};
part[name] = cast(_.get(e, '[0].value', _.get(e, 'target.value')));
if (part[name] === 'undefined') part[name] = undefined;
if (cast === Number && isNaN(part[name])) part[name] = undefined;
handleChange(_.assign({}, item, part));
handleValueChange(item: ColorRule) {
return (e: React.ChangeEvent<HTMLInputElement>) => {
let value: number | undefined = Number(e.target.value);
if (isNaN(value)) value = undefined;
collectionActions.handleChange(this.props, {
...item,
value,
});
};
}
renderRow(row, i, items) {
handleOperatorChange = (item: ColorRule) => {
return (options: Array<EuiComboBoxOptionOption<string>>) => {
collectionActions.handleChange(this.props, {
...item,
operator: options[0].value,
});
};
};
renderRow(row: ColorRule, i: number, items: ColorRule[]) {
const defaults = { value: 0 };
const model = { ...defaults, ...row };
const handleAdd = () => collectionActions.handleAdd(this.props);
const handleDelete = collectionActions.handleDelete.bind(null, this.props, model);
const { intl } = this.props;
const operatorOptions = [
{
label: intl.formatMessage({
id: 'visTypeTimeseries.colorRules.greaterThanLabel',
defaultMessage: '> greater than',
}),
value: 'gt',
},
{
label: intl.formatMessage({
id: 'visTypeTimeseries.colorRules.greaterThanOrEqualLabel',
defaultMessage: '>= greater than or equal',
}),
value: 'gte',
},
{
label: intl.formatMessage({
id: 'visTypeTimeseries.colorRules.lessThanLabel',
defaultMessage: '< less than',
}),
value: 'lt',
},
{
label: intl.formatMessage({
id: 'visTypeTimeseries.colorRules.lessThanOrEqualLabel',
defaultMessage: '<= less than or equal',
}),
value: 'lte',
},
];
const handleColorChange = (part) => {
const handleChange = collectionActions.handleChange.bind(null, this.props);
handleChange(_.assign({}, model, part));
const handleDelete = () => collectionActions.handleDelete(this.props, model);
const handleColorChange: ColorPickerProps['onChange'] = (part) => {
collectionActions.handleChange(this.props, { ...model, ...part });
};
const htmlId = htmlIdGenerator(model.id);
const selectedOperatorOption = operatorOptions.find((option) => {
return model.operator === option.value;
});
const selectedOperatorOption = operatorOptions.find(
(option) => model.operator === option.value
);
const labelStyle = { marginBottom: 0 };
let secondary;
if (!this.props.hideSecondary) {
const secondaryVarName = this.props.secondaryVarName ?? 'color';
secondary = (
<Fragment>
<EuiFlexItem grow={false}>
@ -96,7 +132,7 @@ class ColorRulesUI extends Component {
<FormattedMessage
id="visTypeTimeseries.colorRules.setSecondaryColorLabel"
defaultMessage="and {secondaryName} to"
values={{ secondaryName: this.props.secondaryName }}
values={{ secondaryName: this.props.secondaryName ?? defaultSecondaryName }}
description="Part of a larger string: Set {primaryName} to {color} and {secondaryName} to {color} if
metric is {greaterOrLessThan} {value}."
/>
@ -105,8 +141,8 @@ class ColorRulesUI extends Component {
<EuiFlexItem grow={false}>
<ColorPicker
onChange={handleColorChange}
name={this.props.secondaryVarName}
value={model[this.props.secondaryVarName]}
name={secondaryVarName}
value={model[secondaryVarName as keyof ColorRule] as string | undefined}
/>
</EuiFlexItem>
</Fragment>
@ -126,7 +162,7 @@ class ColorRulesUI extends Component {
<FormattedMessage
id="visTypeTimeseries.colorRules.setPrimaryColorLabel"
defaultMessage="Set {primaryName} to"
values={{ primaryName: this.props.primaryName }}
values={{ primaryName: this.props.primaryName ?? defaultPrimaryName }}
description="Part of a larger string: Set {primaryName} to {color} and {secondaryName} to {color} if
metric is {greaterOrLessThan} {value}."
/>
@ -135,8 +171,12 @@ class ColorRulesUI extends Component {
<EuiFlexItem grow={false}>
<ColorPicker
onChange={handleColorChange}
name={this.props.primaryVarName}
value={model[this.props.primaryVarName]}
name={this.props.primaryVarName ?? 'background_color'}
value={
model[(this.props.primaryVarName ?? 'background_color') as keyof ColorRule] as
| string
| undefined
}
/>
</EuiFlexItem>
@ -157,7 +197,7 @@ class ColorRulesUI extends Component {
id={htmlId('ifMetricIs')}
options={operatorOptions}
selectedOptions={selectedOperatorOption ? [selectedOperatorOption] : []}
onChange={this.handleChange(model, 'operator')}
onChange={this.handleOperatorChange(model)}
singleSelection={{ asPlainText: true }}
data-test-subj="colorRuleOperator"
fullWidth
@ -166,12 +206,11 @@ class ColorRulesUI extends Component {
<EuiFlexItem>
<EuiFieldNumber
aria-label={intl.formatMessage({
id: 'visTypeTimeseries.colorRules.valueAriaLabel',
aria-label={i18n.translate('visTypeTimeseries.colorRules.valueAriaLabel', {
defaultMessage: 'Value',
})}
value={model.value}
onChange={this.handleChange(model, 'value', Number)}
value={model.value ?? ''}
onChange={this.handleValueChange(model)}
data-test-subj="colorRuleValue"
fullWidth
/>
@ -191,34 +230,6 @@ class ColorRulesUI extends Component {
render() {
const { model, name } = this.props;
if (!model[name]) return <div />;
const rows = model[name].map(this.renderRow);
return <div>{rows}</div>;
return !model[name] ? <div /> : <div>{(model[name] as ColorRule[]).map(this.renderRow)}</div>;
}
}
ColorRulesUI.defaultProps = {
name: 'color_rules',
primaryName: i18n.translate('visTypeTimeseries.colorRules.defaultPrimaryNameLabel', {
defaultMessage: 'background',
}),
primaryVarName: 'background_color',
secondaryName: i18n.translate('visTypeTimeseries.colorRules.defaultSecondaryNameLabel', {
defaultMessage: 'text',
}),
secondaryVarName: 'color',
hideSecondary: false,
};
ColorRulesUI.propTypes = {
name: PropTypes.string,
model: PropTypes.object,
onChange: PropTypes.func,
primaryName: PropTypes.string,
primaryVarName: PropTypes.string,
secondaryName: PropTypes.string,
secondaryVarName: PropTypes.string,
hideSecondary: PropTypes.bool,
};
export const ColorRules = injectI18n(ColorRulesUI);

View file

@ -1,40 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import uuid from 'uuid';
const newFn = () => ({ id: uuid.v1() });
export function handleChange(props, doc) {
const { model, name } = props;
const collection = model[name] || [];
const part = {};
part[name] = collection.map((row) => {
if (row.id === doc.id) return doc;
return row;
});
props.onChange?.({ ...model, ...part });
}
export function handleDelete(props, doc) {
const { model, name } = props;
const collection = model[name] || [];
const part = {};
part[name] = collection.filter((row) => row.id !== doc.id);
props.onChange?.({ ...model, ...part });
}
export function handleAdd(props, fn = newFn) {
const { model, name } = props;
const collection = model[name] || [];
const part = {};
part[name] = collection.concat([fn()]);
props.onChange?.({ ...model, ...part });
}
export const collectionActions = { handleAdd, handleDelete, handleChange };

View file

@ -6,50 +6,55 @@
* Side Public License, v 1.
*/
import { handleChange, handleAdd, handleDelete } from './collection_actions';
import {
handleChange,
handleAdd,
handleDelete,
CollectionActionsProps,
} from './collection_actions';
describe('collection actions', () => {
test('handleChange() calls props.onChange() with updated collection', () => {
const fn = jest.fn();
const props = {
model: { test: [{ id: 1, title: 'foo' }] },
const props = ({
model: { test: [{ id: '1', title: 'foo' }] },
name: 'test',
onChange: fn,
};
handleChange.call(null, props, { id: 1, title: 'bar' });
} as unknown) as CollectionActionsProps<any>;
handleChange.call(null, props, { id: '1', type: 'bar' });
expect(fn.mock.calls.length).toEqual(1);
expect(fn.mock.calls[0][0]).toEqual({
test: [{ id: 1, title: 'bar' }],
test: [{ id: '1', type: 'bar' }],
});
});
test('handleAdd() calls props.onChange() with update collection', () => {
const newItemFn = jest.fn(() => ({ id: 2, title: 'example' }));
const newItemFn = jest.fn(() => ({ id: '2', text: 'example' }));
const fn = jest.fn();
const props = {
model: { test: [{ id: 1, title: 'foo' }] },
const props = ({
model: { test: [{ id: '1', text: 'foo' }] },
name: 'test',
onChange: fn,
};
} as unknown) as CollectionActionsProps<any>;
handleAdd.call(null, props, newItemFn);
expect(fn.mock.calls.length).toEqual(1);
expect(newItemFn.mock.calls.length).toEqual(1);
expect(fn.mock.calls[0][0]).toEqual({
test: [
{ id: 1, title: 'foo' },
{ id: 2, title: 'example' },
{ id: '1', text: 'foo' },
{ id: '2', text: 'example' },
],
});
});
test('handleDelete() calls props.onChange() with update collection', () => {
const fn = jest.fn();
const props = {
model: { test: [{ id: 1, title: 'foo' }] },
const props = ({
model: { test: [{ id: '1', title: 'foo' }] },
name: 'test',
onChange: fn,
};
handleDelete.call(null, props, { id: 1 });
} as unknown) as CollectionActionsProps<any>;
handleDelete.call(null, props, { id: '1' });
expect(fn.mock.calls.length).toEqual(1);
expect(fn.mock.calls[0][0]).toEqual({
test: [],

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import uuid from 'uuid';
interface DocType {
id: string;
type?: string;
}
const newFn = (): DocType => ({ id: uuid.v1() });
export interface CollectionActionsProps<T> {
model: T;
name: keyof T;
onChange: (partialModel: Partial<T>) => void;
}
export function handleChange<T, P extends DocType>(props: CollectionActionsProps<T>, doc: P) {
const { model, name } = props;
const collection = ((model[name] as unknown) as DocType[]) || [];
const part = { [name]: collection.map((row) => (row.id === doc.id ? doc : row)) };
props.onChange({ ...model, ...part });
}
export function handleDelete<T, P extends DocType>(props: CollectionActionsProps<T>, doc: P) {
const { model, name } = props;
const collection = ((model[name] as unknown) as DocType[]) || [];
const part = { [name]: collection.filter((row) => row.id !== doc.id) };
props.onChange?.({ ...model, ...part });
}
export function handleAdd<T>(props: CollectionActionsProps<T>, fn = newFn) {
const { model, name } = props;
const collection = ((model[name] as unknown) as DocType[]) || [];
const part = { [name]: collection.concat([fn()]) };
props.onChange?.({ ...model, ...part });
}
export const collectionActions = { handleAdd, handleDelete, handleChange };

View file

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import { detectIE } from './detect_ie';
export const createNumberHandler = (handleChange) => {
return (name, defaultValue) => (e) => {
if (!detectIE() || e.keyCode === 13) e.preventDefault();
const value = Number(_.get(e, 'target.value', defaultValue));
return handleChange?.({ [name]: value });
};
};

View file

@ -9,23 +9,25 @@
import { createNumberHandler } from './create_number_handler';
describe('createNumberHandler()', () => {
let handleChange;
let changeHandler;
let event;
let handleChange: jest.Mock;
let changeHandler: ReturnType<typeof createNumberHandler>;
let event: React.ChangeEvent<HTMLInputElement>;
beforeEach(() => {
handleChange = jest.fn();
changeHandler = createNumberHandler(handleChange);
event = { preventDefault: jest.fn(), target: { value: '1' } };
const fn = changeHandler('test');
event = ({
preventDefault: jest.fn(),
target: { value: '1' },
} as unknown) as React.ChangeEvent<HTMLInputElement>;
const fn = changeHandler('unit');
fn(event);
});
test('calls handleChange() function with partial', () => {
expect(event.preventDefault.mock.calls.length).toEqual(1);
expect(handleChange.mock.calls.length).toEqual(1);
expect(handleChange.mock.calls[0][0]).toEqual({
test: 1,
unit: 1,
});
});
});

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { MetricsItemsSchema } from '../../../../common/types';
import { TimeseriesVisParams } from '../../../types';
export const createNumberHandler = (
handleChange: (partialModel: Partial<TimeseriesVisParams>) => void
) => {
return (name: keyof MetricsItemsSchema, defaultValue?: string) => (
e: React.ChangeEvent<HTMLInputElement>
) => handleChange?.({ [name]: Number(e.target.value ?? defaultValue) });
};

View file

@ -1,20 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import { detectIE } from './detect_ie';
export const createTextHandler = (handleChange) => {
return (name, defaultValue) => (e) => {
// IE preventDefault breaks input, but we still need top prevent enter from being pressed
if (!detectIE() || e.keyCode === 13) e.preventDefault();
const value = _.get(e, 'target.value', defaultValue);
return handleChange?.({ [name]: value });
};
};

View file

@ -9,23 +9,25 @@
import { createTextHandler } from './create_text_handler';
describe('createTextHandler()', () => {
let handleChange;
let changeHandler;
let event;
let handleChange: jest.Mock;
let changeHandler: ReturnType<typeof createTextHandler>;
let event: React.ChangeEvent<HTMLInputElement>;
beforeEach(() => {
handleChange = jest.fn();
changeHandler = createTextHandler(handleChange);
event = { preventDefault: jest.fn(), target: { value: 'foo' } };
const fn = changeHandler('test');
event = ({
preventDefault: jest.fn(),
target: { value: 'foo' },
} as unknown) as React.ChangeEvent<HTMLInputElement>;
const fn = changeHandler('axis_scale');
fn(event);
});
test('calls handleChange() function with partial', () => {
expect(event.preventDefault.mock.calls.length).toEqual(1);
expect(handleChange.mock.calls.length).toEqual(1);
expect(handleChange.mock.calls[0][0]).toEqual({
test: 'foo',
axis_scale: 'foo',
});
});
});

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { TimeseriesVisParams } from '../../../types';
// TODO: replace with explicit callback in each component
export const createTextHandler = (
handleChange: (partialModel: Partial<TimeseriesVisParams>) => void
) => {
return (name: keyof TimeseriesVisParams, defaultValue?: string) => (
e: React.ChangeEvent<HTMLInputElement>
) => handleChange({ [name]: e.target.value ?? defaultValue });
};

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export function detectIE() {
const ua = window.navigator.userAgent;
const msie = ua.indexOf('MSIE ');
if (msie > 0) {
// IE 10 or older => return version number
return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
}
const trident = ua.indexOf('Trident/');
if (trident > 0) {
// IE 11 => return version number
const rv = ua.indexOf('rv:');
return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
}
const edge = ua.indexOf('Edge/');
if (edge > 0) {
// Edge (IE 12+) => return version number
return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
}
// other browser
return false;
}

View file

@ -7,34 +7,35 @@
*/
import React from 'react';
import { shallowWithIntl } from '@kbn/test/jest';
import { shallow } from 'enzyme';
jest.mock('../lib/get_default_query_language', () => ({
getDefaultQueryLanguage: () => 'kuery',
}));
import { GaugePanelConfig } from './gauge';
import { PanelConfigProps } from './types';
describe('GaugePanelConfig', () => {
it('call switch tab onChange={handleChange}', () => {
const props = {
const props = ({
fields: {},
model: {},
onChange: jest.fn(),
};
const wrapper = shallowWithIntl(<GaugePanelConfig.WrappedComponent {...props} />);
} as unknown) as PanelConfigProps;
const wrapper = shallow(<GaugePanelConfig {...props} />);
wrapper.find('EuiTab').first().simulate('onClick');
expect(props.onChange).toBeCalled();
});
it('call onChange={handleChange}', () => {
const props = {
const props = ({
fields: {},
model: {},
onChange: jest.fn(),
};
const wrapper = shallowWithIntl(<GaugePanelConfig.WrappedComponent {...props} />);
} as unknown) as PanelConfigProps;
const wrapper = shallow(<GaugePanelConfig {...props} />);
wrapper.simulate('onClick');
expect(props.onChange).toBeCalled();

View file

@ -6,16 +6,8 @@
* Side Public License, v 1.
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { SeriesEditor } from '../series_editor';
import { IndexPattern } from '../index_pattern';
import { createSelectHandler } from '../lib/create_select_handler';
import { createTextHandler } from '../lib/create_text_handler';
import { ColorRules } from '../color_rules';
import { ColorPicker } from '../color_picker';
import uuid from 'uuid';
import { YesNo } from '../yes_no';
import {
htmlIdGenerator,
EuiComboBox,
@ -31,26 +23,40 @@ import {
EuiTitle,
EuiHorizontalRule,
} from '@elastic/eui';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import type { Writable } from '@kbn/utility-types';
// @ts-ignore
import { SeriesEditor } from '../series_editor';
// @ts-ignore should be typed after https://github.com/elastic/kibana/pull/92812 to reduce conflicts
import { IndexPattern } from '../index_pattern';
import { createSelectHandler } from '../lib/create_select_handler';
import { ColorRules } from '../color_rules';
import { ColorPicker } from '../color_picker';
// @ts-ignore this is typed in https://github.com/elastic/kibana/pull/92812, remove ignore after merging
import { QueryBarWrapper } from '../query_bar_wrapper';
import { getDefaultQueryLanguage } from '../lib/get_default_query_language';
import { YesNo } from '../yes_no';
import { limitOfSeries } from '../../../../common/ui_restrictions';
import { PANEL_TYPES } from '../../../../common/panel_types';
import { TimeseriesVisParams } from '../../../types';
import { PanelConfigProps, PANEL_CONFIG_TABS } from './types';
class GaugePanelConfigUi extends Component {
constructor(props) {
export class GaugePanelConfig extends Component<
PanelConfigProps,
{ selectedTab: PANEL_CONFIG_TABS }
> {
constructor(props: PanelConfigProps) {
super(props);
this.state = { selectedTab: 'data' };
this.state = { selectedTab: PANEL_CONFIG_TABS.DATA };
}
UNSAFE_componentWillMount() {
const { model } = this.props;
const parts = {};
if (
!model.gauge_color_rules ||
(model.gauge_color_rules && model.gauge_color_rules.length === 0)
) {
const parts: Writable<Partial<TimeseriesVisParams>> = {};
if (!model.gauge_color_rules || !model.gauge_color_rules.length) {
parts.gauge_color_rules = [{ id: uuid.v1() }];
}
if (model.gauge_width == null) parts.gauge_width = 10;
@ -59,14 +65,17 @@ class GaugePanelConfigUi extends Component {
this.props.onChange(parts);
}
switchTab(selectedTab) {
switchTab(selectedTab: PANEL_CONFIG_TABS) {
this.setState({ selectedTab });
}
handleTextChange = (name: keyof TimeseriesVisParams) => (
e: React.ChangeEvent<HTMLInputElement>
) => this.props.onChange({ [name]: e.target.value });
render() {
const { selectedTab } = this.state;
const { intl } = this.props;
const defaults = {
const defaults: Partial<TimeseriesVisParams> = {
gauge_max: '',
filter: { query: '', language: getDefaultQueryLanguage() },
gauge_style: 'circle',
@ -75,41 +84,34 @@ class GaugePanelConfigUi extends Component {
};
const model = { ...defaults, ...this.props.model };
const handleSelectChange = createSelectHandler(this.props.onChange);
const handleTextChange = createTextHandler(this.props.onChange);
const styleOptions = [
{
label: intl.formatMessage({
id: 'visTypeTimeseries.gauge.styleOptions.circleLabel',
label: i18n.translate('visTypeTimeseries.gauge.styleOptions.circleLabel', {
defaultMessage: 'Circle',
}),
value: 'circle',
},
{
label: intl.formatMessage({
id: 'visTypeTimeseries.gauge.styleOptions.halfCircleLabel',
label: i18n.translate('visTypeTimeseries.gauge.styleOptions.halfCircleLabel', {
defaultMessage: 'Half Circle',
}),
value: 'half',
},
];
const htmlId = htmlIdGenerator();
const selectedGaugeStyleOption = styleOptions.find((option) => {
return model.gauge_style === option.value;
});
let view;
if (selectedTab === 'data') {
view = (
const selectedGaugeStyleOption = styleOptions.find(
(option) => model.gauge_style === option.value
);
const view =
selectedTab === PANEL_CONFIG_TABS.DATA ? (
<SeriesEditor
colorPicker={true}
fields={this.props.fields}
limit={limitOfSeries[PANEL_TYPES.GAUGE]}
model={this.props.model}
name={this.props.name}
onChange={this.props.onChange}
/>
);
} else {
view = (
) : (
<div className="tvbPanelConfig__container">
<EuiPanel>
<EuiTitle size="s">
@ -144,10 +146,12 @@ class GaugePanelConfigUi extends Component {
>
<QueryBarWrapper
query={{
language: model.filter.language || getDefaultQueryLanguage(),
query: model.filter.query || '',
language: model.filter?.language || getDefaultQueryLanguage(),
query: model.filter?.query || '',
}}
onChange={(filter) => this.props.onChange({ filter })}
onChange={(filter: PanelConfigProps['model']['filter']) =>
this.props.onChange({ filter })
}
indexPatterns={[model.index_pattern || model.default_index_pattern]}
/>
</EuiFormRow>
@ -193,15 +197,8 @@ class GaugePanelConfigUi extends Component {
/>
}
>
{/*
EUITODO: The following input couldn't be converted to EUI because of type mis-match.
It accepts a null value, but is passed a empty string.
*/}
<input
id={htmlId('gaugeMax')}
className="tvbAgg__input"
type="number"
onChange={handleTextChange('gauge_max')}
<EuiFieldNumber
onChange={this.handleTextChange('gauge_max')}
value={model.gauge_max}
/>
</EuiFormRow>
@ -236,7 +233,7 @@ class GaugePanelConfigUi extends Component {
}
>
<EuiFieldNumber
onChange={handleTextChange('gauge_inner_width')}
onChange={this.handleTextChange('gauge_inner_width')}
value={Number(model.gauge_inner_width)}
/>
</EuiFormRow>
@ -252,7 +249,7 @@ class GaugePanelConfigUi extends Component {
}
>
<EuiFieldNumber
onChange={handleTextChange('gauge_width')}
onChange={this.handleTextChange('gauge_width')}
value={Number(model.gauge_width)}
/>
</EuiFormRow>
@ -317,17 +314,23 @@ class GaugePanelConfigUi extends Component {
</EuiPanel>
</div>
);
}
return (
<>
<EuiTabs size="s">
<EuiTab isSelected={selectedTab === 'data'} onClick={() => this.switchTab('data')}>
<EuiTab
isSelected={selectedTab === PANEL_CONFIG_TABS.DATA}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.DATA)}
>
<FormattedMessage
id="visTypeTimeseries.gauge.dataTab.dataButtonLabel"
defaultMessage="Data"
/>
</EuiTab>
<EuiTab isSelected={selectedTab === 'options'} onClick={() => this.switchTab('options')}>
<EuiTab
isSelected={selectedTab === PANEL_CONFIG_TABS.OPTIONS}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.OPTIONS)}
>
<FormattedMessage
id="visTypeTimeseries.gauge.optionsTab.panelOptionsButtonLabel"
defaultMessage="Panel options"
@ -339,11 +342,3 @@ class GaugePanelConfigUi extends Component {
);
}
}
GaugePanelConfigUi.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
};
export const GaugePanelConfig = injectI18n(GaugePanelConfigUi);

View file

@ -6,25 +6,4 @@
* Side Public License, v 1.
*/
// these are not typed yet
// @ts-expect-error
import { TimeseriesPanelConfig as timeseries } from './timeseries';
// @ts-expect-error
import { MetricPanelConfig as metric } from './metric';
// @ts-expect-error
import { TopNPanelConfig as topN } from './top_n';
// @ts-expect-error
import { TablePanelConfig as table } from './table';
// @ts-expect-error
import { GaugePanelConfig as gauge } from './gauge';
// @ts-expect-error
import { MarkdownPanelConfig as markdown } from './markdown';
export const panelConfigTypes = {
timeseries,
table,
metric,
top_n: topN,
gauge,
markdown,
};
export { PanelConfig } from './panel_config';

View file

@ -6,16 +6,7 @@
* Side Public License, v 1.
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { SeriesEditor } from '../series_editor';
import { IndexPattern } from '../index_pattern';
import 'brace/mode/less';
import { createSelectHandler } from '../lib/create_select_handler';
import { ColorPicker } from '../color_picker';
import { YesNo } from '../yes_no';
import { MarkdownEditor } from '../markdown_editor';
import less from 'less/lib/less-browser';
import {
htmlIdGenerator,
EuiComboBox,
@ -31,35 +22,61 @@ import {
EuiHorizontalRule,
EuiCodeEditor,
} from '@elastic/eui';
const lessC = less(window, { env: 'production' });
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
// @ts-expect-error
import less from 'less/lib/less-browser';
import 'brace/mode/less';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import type { Writable } from '@kbn/utility-types';
// @ts-expect-error not typed yet
import { SeriesEditor } from '../series_editor';
// @ts-ignore should be typed after https://github.com/elastic/kibana/pull/92812 to reduce conflicts
import { IndexPattern } from '../index_pattern';
import { createSelectHandler } from '../lib/create_select_handler';
import { ColorPicker } from '../color_picker';
import { YesNo } from '../yes_no';
// @ts-expect-error not typed yet
import { MarkdownEditor } from '../markdown_editor';
// @ts-ignore this is typed in https://github.com/elastic/kibana/pull/92812, remove ignore after merging
import { QueryBarWrapper } from '../query_bar_wrapper';
import { getDefaultQueryLanguage } from '../lib/get_default_query_language';
import { VisDataContext } from '../../contexts/vis_data_context';
import { PanelConfigProps, PANEL_CONFIG_TABS } from './types';
import { TimeseriesVisParams } from '../../../types';
class MarkdownPanelConfigUi extends Component {
constructor(props) {
const lessC = less(window, { env: 'production' });
export class MarkdownPanelConfig extends Component<
PanelConfigProps,
{ selectedTab: PANEL_CONFIG_TABS }
> {
constructor(props: PanelConfigProps) {
super(props);
this.state = { selectedTab: 'markdown' };
this.state = { selectedTab: PANEL_CONFIG_TABS.MARKDOWN };
this.handleCSSChange = this.handleCSSChange.bind(this);
}
switchTab(selectedTab) {
switchTab(selectedTab: PANEL_CONFIG_TABS) {
this.setState({ selectedTab });
}
handleCSSChange(value) {
handleCSSChange(value: string) {
const { model } = this.props;
const lessSrc = `#markdown-${model.id} {
${value}
}`;
lessC.render(lessSrc, { compress: true, javascriptEnabled: false }, (e, output) => {
const parts = { markdown_less: value };
if (output) {
parts.markdown_css = output.css;
const lessSrc = `#markdown-${model.id} {${value}}`;
lessC.render(
lessSrc,
{ compress: true, javascriptEnabled: false },
(e: unknown, output: any) => {
const parts: Writable<Pick<TimeseriesVisParams, 'markdown_less' | 'markdown_css'>> = {
markdown_less: value,
};
if (output) {
parts.markdown_css = output.css;
}
this.props.onChange(parts);
}
this.props.onChange(parts);
});
);
}
render() {
@ -67,28 +84,23 @@ class MarkdownPanelConfigUi extends Component {
const model = { ...defaults, ...this.props.model };
const { selectedTab } = this.state;
const handleSelectChange = createSelectHandler(this.props.onChange);
const { intl } = this.props;
const htmlId = htmlIdGenerator();
const alignOptions = [
{
label: intl.formatMessage({
id: 'visTypeTimeseries.markdown.alignOptions.topLabel',
label: i18n.translate('visTypeTimeseries.markdown.alignOptions.topLabel', {
defaultMessage: 'Top',
}),
value: 'top',
},
{
label: intl.formatMessage({
id: 'visTypeTimeseries.markdown.alignOptions.middleLabel',
label: i18n.translate('visTypeTimeseries.markdown.alignOptions.middleLabel', {
defaultMessage: 'Middle',
}),
value: 'middle',
},
{
label: intl.formatMessage({
id: 'visTypeTimeseries.markdown.alignOptions.bottomLabel',
label: i18n.translate('visTypeTimeseries.markdown.alignOptions.bottomLabel', {
defaultMessage: 'Bottom',
}),
value: 'bottom',
@ -98,19 +110,18 @@ class MarkdownPanelConfigUi extends Component {
return model.markdown_vertical_align === option.value;
});
let view;
if (selectedTab === 'markdown') {
if (selectedTab === PANEL_CONFIG_TABS.MARKDOWN) {
view = (
<VisDataContext.Consumer>
{(visData) => <MarkdownEditor visData={visData} {...this.props} />}
</VisDataContext.Consumer>
);
} else if (selectedTab === 'data') {
} else if (selectedTab === PANEL_CONFIG_TABS.DATA) {
view = (
<SeriesEditor
colorPicker={false}
fields={this.props.fields}
model={this.props.model}
name={this.props.name}
onChange={this.props.onChange}
/>
);
@ -150,12 +161,12 @@ class MarkdownPanelConfigUi extends Component {
>
<QueryBarWrapper
query={{
language: model.filter.language
? model.filter.language
: getDefaultQueryLanguage(),
language: model.filter.language || getDefaultQueryLanguage(),
query: model.filter.query || '',
}}
onChange={(filter) => this.props.onChange({ filter })}
onChange={(filter: PanelConfigProps['model']['filter']) =>
this.props.onChange({ filter })
}
indexPatterns={[model.index_pattern || model.default_index_pattern]}
/>
</EuiFormRow>
@ -275,7 +286,7 @@ class MarkdownPanelConfigUi extends Component {
width="100%"
name={`ace-css-${model.id}`}
setOptions={{ fontSize: '14px' }}
value={model.markdown_less}
value={model.markdown_less ?? ''}
onChange={this.handleCSSChange}
/>
</EuiPanel>
@ -286,16 +297,16 @@ class MarkdownPanelConfigUi extends Component {
<>
<EuiTabs size="s">
<EuiTab
isSelected={selectedTab === 'markdown'}
onClick={() => this.switchTab('markdown')}
isSelected={selectedTab === PANEL_CONFIG_TABS.MARKDOWN}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.MARKDOWN)}
data-test-subj="markdown-subtab"
>
Markdown
</EuiTab>
<EuiTab
data-test-subj="data-subtab"
isSelected={selectedTab === 'data'}
onClick={() => this.switchTab('data')}
isSelected={selectedTab === PANEL_CONFIG_TABS.DATA}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.DATA)}
>
<FormattedMessage
id="visTypeTimeseries.markdown.dataTab.dataButtonLabel"
@ -303,8 +314,8 @@ class MarkdownPanelConfigUi extends Component {
/>
</EuiTab>
<EuiTab
isSelected={selectedTab === 'options'}
onClick={() => this.switchTab('options')}
isSelected={selectedTab === PANEL_CONFIG_TABS.OPTIONS}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.OPTIONS)}
data-test-subj="options-subtab"
>
<FormattedMessage
@ -318,11 +329,3 @@ class MarkdownPanelConfigUi extends Component {
);
}
}
MarkdownPanelConfigUi.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
};
export const MarkdownPanelConfig = injectI18n(MarkdownPanelConfigUi);

View file

@ -6,12 +6,7 @@
* Side Public License, v 1.
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { SeriesEditor } from '../series_editor';
import { IndexPattern } from '../index_pattern';
import { ColorRules } from '../color_rules';
import { YesNo } from '../yes_no';
import uuid from 'uuid';
import {
htmlIdGenerator,
@ -28,15 +23,27 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
// @ts-expect-error
import { SeriesEditor } from '../series_editor';
// @ts-ignore should be typed after https://github.com/elastic/kibana/pull/92812 to reduce conflicts
import { IndexPattern } from '../index_pattern';
import { ColorRules } from '../color_rules';
import { YesNo } from '../yes_no';
// @ts-ignore this is typed in https://github.com/elastic/kibana/pull/92812, remove ignore after merging
import { QueryBarWrapper } from '../query_bar_wrapper';
import { getDefaultQueryLanguage } from '../lib/get_default_query_language';
import { limitOfSeries } from '../../../../common/ui_restrictions';
import { PANEL_TYPES } from '../../../../common/panel_types';
import { PanelConfigProps, PANEL_CONFIG_TABS } from './types';
export class MetricPanelConfig extends Component {
constructor(props) {
export class MetricPanelConfig extends Component<
PanelConfigProps,
{ selectedTab: PANEL_CONFIG_TABS }
> {
constructor(props: PanelConfigProps) {
super(props);
this.state = { selectedTab: 'data' };
this.state = { selectedTab: PANEL_CONFIG_TABS.DATA };
}
UNSAFE_componentWillMount() {
@ -51,7 +58,7 @@ export class MetricPanelConfig extends Component {
}
}
switchTab(selectedTab) {
switchTab(selectedTab: PANEL_CONFIG_TABS) {
this.setState({ selectedTab });
}
@ -60,20 +67,16 @@ export class MetricPanelConfig extends Component {
const defaults = { filter: { query: '', language: getDefaultQueryLanguage() } };
const model = { ...defaults, ...this.props.model };
const htmlId = htmlIdGenerator();
let view;
if (selectedTab === 'data') {
view = (
const view =
selectedTab === PANEL_CONFIG_TABS.DATA ? (
<SeriesEditor
colorPicker={false}
fields={this.props.fields}
limit={limitOfSeries[PANEL_TYPES.METRIC]}
model={this.props.model}
name={this.props.name}
onChange={this.props.onChange}
/>
);
} else {
view = (
) : (
<div className="tvbPanelConfig__container">
<EuiPanel>
<EuiTitle size="s">
@ -111,7 +114,9 @@ export class MetricPanelConfig extends Component {
language: model.filter.language || getDefaultQueryLanguage(),
query: model.filter.query || '',
}}
onChange={(filter) => this.props.onChange({ filter })}
onChange={(filter: PanelConfigProps['model']['filter']) =>
this.props.onChange({ filter })
}
indexPatterns={[model.index_pattern || model.default_index_pattern]}
/>
</EuiFormRow>
@ -154,19 +159,22 @@ export class MetricPanelConfig extends Component {
</EuiPanel>
</div>
);
}
return (
<>
<EuiTabs size="s">
<EuiTab isSelected={selectedTab === 'data'} onClick={() => this.switchTab('data')}>
<EuiTab
isSelected={selectedTab === PANEL_CONFIG_TABS.DATA}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.DATA)}
>
<FormattedMessage
id="visTypeTimeseries.metric.dataTab.dataButtonLabel"
defaultMessage="Data"
/>
</EuiTab>
<EuiTab
isSelected={selectedTab === 'options'}
onClick={() => this.switchTab('options')}
isSelected={selectedTab === PANEL_CONFIG_TABS.OPTIONS}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.OPTIONS)}
data-test-subj="metricEditorPanelOptionsBtn"
>
<FormattedMessage
@ -180,9 +188,3 @@ export class MetricPanelConfig extends Component {
);
}
}
MetricPanelConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
};

View file

@ -8,28 +8,31 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { Observable } from 'rxjs';
import { IUiSettingsClient } from 'kibana/public';
import { TimeseriesVisData } from '../../../common/types';
import { FormValidationContext } from '../contexts/form_validation_context';
import { VisDataContext } from '../contexts/vis_data_context';
import { panelConfigTypes } from './panel_config/index';
import { TimeseriesVisParams } from '../../types';
import { VisFields } from '../lib/fetch_fields';
import { TimeseriesVisData } from '../../../../common/types';
import { FormValidationContext } from '../../contexts/form_validation_context';
import { VisDataContext } from '../../contexts/vis_data_context';
import { PanelConfigProps } from './types';
import { TimeseriesPanelConfig as timeseries } from './timeseries';
import { MetricPanelConfig as metric } from './metric';
import { TopNPanelConfig as topN } from './top_n';
import { TablePanelConfig as table } from './table';
import { GaugePanelConfig as gauge } from './gauge';
import { MarkdownPanelConfig as markdown } from './markdown';
const panelConfigTypes = {
timeseries,
table,
metric,
top_n: topN,
gauge,
markdown,
};
interface FormValidationResults {
[key: string]: boolean;
}
interface PanelConfigProps {
fields?: VisFields;
model: TimeseriesVisParams;
visData$: Observable<TimeseriesVisData | undefined>;
getConfig: IUiSettingsClient['get'];
onChange: (partialModel: Partial<TimeseriesVisParams>) => void;
}
const checkModelValidity = (validationResults: FormValidationResults) =>
Object.values(validationResults).every((isValid) => isValid);

View file

@ -7,14 +7,8 @@
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FieldSelect } from '../aggs/field_select';
import { SeriesEditor } from '../series_editor';
import { IndexPattern } from '../index_pattern';
import { createTextHandler } from '../lib/create_text_handler';
import { get } from 'lodash';
import uuid from 'uuid';
import { YesNo } from '../yes_no';
import {
htmlIdGenerator,
EuiTabs,
@ -30,36 +24,49 @@ import {
EuiHorizontalRule,
EuiCode,
EuiText,
EuiComboBoxOptionOption,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { FieldSelect } from '../aggs/field_select';
// @ts-expect-error not typed yet
import { SeriesEditor } from '../series_editor';
// @ts-ignore should be typed after https://github.com/elastic/kibana/pull/92812 to reduce conflicts
import { IndexPattern } from '../index_pattern';
import { YesNo } from '../yes_no';
// @ts-ignore this is typed in https://github.com/elastic/kibana/pull/92812, remove ignore after merging
import { QueryBarWrapper } from '../query_bar_wrapper';
import { getDefaultQueryLanguage } from '../lib/get_default_query_language';
import { VisDataContext } from '../../contexts/vis_data_context';
import { BUCKET_TYPES } from '../../../../common/metric_types';
export class TablePanelConfig extends Component {
import { PanelConfigProps, PANEL_CONFIG_TABS } from './types';
import { TimeseriesVisParams } from '../../../types';
export class TablePanelConfig extends Component<
PanelConfigProps,
{ selectedTab: PANEL_CONFIG_TABS }
> {
static contextType = VisDataContext;
constructor(props) {
constructor(props: PanelConfigProps) {
super(props);
this.state = { selectedTab: 'data' };
this.state = { selectedTab: PANEL_CONFIG_TABS.DATA };
}
UNSAFE_componentWillMount() {
const { model } = this.props;
const parts = {};
if (!model.bar_color_rules || (model.bar_color_rules && model.bar_color_rules.length === 0)) {
parts.bar_color_rules = [{ id: uuid.v1() }];
if (!model.bar_color_rules || !model.bar_color_rules.length) {
this.props.onChange({ bar_color_rules: [{ id: uuid.v1() }] });
}
this.props.onChange(parts);
}
switchTab(selectedTab) {
switchTab(selectedTab: PANEL_CONFIG_TABS) {
this.setState({ selectedTab });
}
handlePivotChange = (selectedOption) => {
handlePivotChange = (selectedOption: Array<EuiComboBoxOptionOption<string>>) => {
const { fields, model } = this.props;
const pivotId = get(selectedOption, '[0].value', null);
const field = fields[model.index_pattern].find((field) => field.name === pivotId);
const field = fields[model.index_pattern].find((f) => f.name === pivotId);
const pivotType = get(field, 'type', model.pivot_type);
this.props.onChange({
@ -68,6 +75,10 @@ export class TablePanelConfig extends Component {
});
};
handleTextChange = (name: keyof TimeseriesVisParams) => (
e: React.ChangeEvent<HTMLInputElement>
) => this.props.onChange({ [name]: e.target.value });
render() {
const { selectedTab } = this.state;
const defaults = {
@ -78,11 +89,9 @@ export class TablePanelConfig extends Component {
pivot_type: '',
};
const model = { ...defaults, ...this.props.model };
const handleTextChange = createTextHandler(this.props.onChange);
const htmlId = htmlIdGenerator();
let view;
if (selectedTab === 'data') {
view = (
const view =
selectedTab === PANEL_CONFIG_TABS.DATA ? (
<div>
<div className="tvbPanelConfig__container">
<EuiPanel>
@ -114,7 +123,6 @@ export class TablePanelConfig extends Component {
onChange={this.handlePivotChange}
uiRestrictions={this.context.uiRestrictions}
type={BUCKET_TYPES.TERMS}
fullWidth
/>
</EuiFormRow>
</EuiFlexItem>
@ -131,8 +139,8 @@ export class TablePanelConfig extends Component {
>
<EuiFieldText
data-test-subj="columnLabelName"
onChange={handleTextChange('pivot_label')}
value={model.pivot_label}
onChange={this.handleTextChange('pivot_label')}
value={model.pivot_label ?? ''}
fullWidth
/>
</EuiFormRow>
@ -154,8 +162,8 @@ export class TablePanelConfig extends Component {
<input
className="tvbAgg__input"
type="number"
onChange={handleTextChange('pivot_rows')}
value={model.pivot_rows}
onChange={this.handleTextChange('pivot_rows')}
value={model.pivot_rows ?? ''}
/>
</EuiFormRow>
</EuiFlexItem>
@ -166,13 +174,10 @@ export class TablePanelConfig extends Component {
<SeriesEditor
fields={this.props.fields}
model={this.props.model}
name={this.props.name}
onChange={this.props.onChange}
/>
</div>
);
} else {
view = (
) : (
<div className="tvbPanelConfig__container">
<EuiPanel>
<EuiTitle size="s">
@ -203,8 +208,8 @@ export class TablePanelConfig extends Component {
}
>
<EuiFieldText
onChange={handleTextChange('drilldown_url')}
value={model.drilldown_url}
onChange={this.handleTextChange('drilldown_url')}
value={model.drilldown_url ?? ''}
/>
</EuiFormRow>
@ -237,7 +242,9 @@ export class TablePanelConfig extends Component {
: getDefaultQueryLanguage(),
query: model.filter.query || '',
}}
onChange={(filter) => this.props.onChange({ filter })}
onChange={(filter: PanelConfigProps['model']['filter']) =>
this.props.onChange({ filter })
}
indexPatterns={[model.index_pattern || model.default_index_pattern]}
/>
</EuiFormRow>
@ -251,7 +258,6 @@ export class TablePanelConfig extends Component {
</EuiFormLabel>
<EuiSpacer size="m" />
<YesNo
id={htmlId('globalFilterOption')}
value={model.ignore_global_filter}
name="ignore_global_filter"
onChange={this.props.onChange}
@ -261,17 +267,23 @@ export class TablePanelConfig extends Component {
</EuiPanel>
</div>
);
}
return (
<>
<EuiTabs size="s">
<EuiTab isSelected={selectedTab === 'data'} onClick={() => this.switchTab('data')}>
<EuiTab
isSelected={selectedTab === PANEL_CONFIG_TABS.DATA}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.DATA)}
>
<FormattedMessage
id="visTypeTimeseries.table.dataTab.columnsButtonLabel"
defaultMessage="Columns"
/>
</EuiTab>
<EuiTab isSelected={selectedTab === 'options'} onClick={() => this.switchTab('options')}>
<EuiTab
isSelected={selectedTab === PANEL_CONFIG_TABS.OPTIONS}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.OPTIONS)}
>
<FormattedMessage
id="visTypeTimeseries.table.optionsTab.panelOptionsButtonLabel"
defaultMessage="Panel options"
@ -283,9 +295,3 @@ export class TablePanelConfig extends Component {
);
}
}
TablePanelConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
};

View file

@ -6,15 +6,7 @@
* Side Public License, v 1.
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { SeriesEditor } from '../series_editor';
import { AnnotationsEditor } from '../annotations_editor';
import { IndexPattern } from '../index_pattern';
import { createSelectHandler } from '../lib/create_select_handler';
import { createTextHandler } from '../lib/create_text_handler';
import { ColorPicker } from '../color_picker';
import { YesNo } from '../yes_no';
import {
htmlIdGenerator,
EuiComboBox,
@ -30,20 +22,104 @@ import {
EuiTitle,
EuiHorizontalRule,
} from '@elastic/eui';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { getDefaultQueryLanguage } from '../lib/get_default_query_language';
import { QueryBarWrapper } from '../query_bar_wrapper';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
class TimeseriesPanelConfigUi extends Component {
constructor(props) {
// @ts-expect-error not typed yet
import { SeriesEditor } from '../series_editor';
// @ts-ignore should be typed after https://github.com/elastic/kibana/pull/92812 to reduce conflicts
import { AnnotationsEditor } from '../annotations_editor';
// @ts-ignore should be typed after https://github.com/elastic/kibana/pull/92812 to reduce conflicts
import { IndexPattern } from '../index_pattern';
import { createSelectHandler } from '../lib/create_select_handler';
import { ColorPicker } from '../color_picker';
import { YesNo } from '../yes_no';
import { getDefaultQueryLanguage } from '../lib/get_default_query_language';
// @ts-ignore this is typed in https://github.com/elastic/kibana/pull/92812, remove ignore after merging
import { QueryBarWrapper } from '../query_bar_wrapper';
import { PanelConfigProps, PANEL_CONFIG_TABS } from './types';
import { TimeseriesVisParams } from '../../../types';
const positionOptions = [
{
label: i18n.translate('visTypeTimeseries.timeseries.positionOptions.rightLabel', {
defaultMessage: 'Right',
}),
value: 'right',
},
{
label: i18n.translate('visTypeTimeseries.timeseries.positionOptions.leftLabel', {
defaultMessage: 'Left',
}),
value: 'left',
},
];
const tooltipModeOptions = [
{
label: i18n.translate('visTypeTimeseries.timeseries.tooltipOptions.showAll', {
defaultMessage: 'Show all values',
}),
value: 'show_all',
},
{
label: i18n.translate('visTypeTimeseries.timeseries.tooltipOptions.showFocused', {
defaultMessage: 'Show focused values',
}),
value: 'show_focused',
},
];
const scaleOptions = [
{
label: i18n.translate('visTypeTimeseries.timeseries.scaleOptions.normalLabel', {
defaultMessage: 'Normal',
}),
value: 'normal',
},
{
label: i18n.translate('visTypeTimeseries.timeseries.scaleOptions.logLabel', {
defaultMessage: 'Log',
}),
value: 'log',
},
];
const legendPositionOptions = [
{
label: i18n.translate('visTypeTimeseries.timeseries.legendPositionOptions.rightLabel', {
defaultMessage: 'Right',
}),
value: 'right',
},
{
label: i18n.translate('visTypeTimeseries.timeseries.legendPositionOptions.leftLabel', {
defaultMessage: 'Left',
}),
value: 'left',
},
{
label: i18n.translate('visTypeTimeseries.timeseries.legendPositionOptions.bottomLabel', {
defaultMessage: 'Bottom',
}),
value: 'bottom',
},
];
export class TimeseriesPanelConfig extends Component<
PanelConfigProps,
{ selectedTab: PANEL_CONFIG_TABS }
> {
constructor(props: PanelConfigProps) {
super(props);
this.state = { selectedTab: 'data' };
this.state = { selectedTab: PANEL_CONFIG_TABS.DATA };
}
switchTab(selectedTab) {
switchTab(selectedTab: PANEL_CONFIG_TABS) {
this.setState({ selectedTab });
}
handleTextChange = (name: keyof TimeseriesVisParams) => (
e: React.ChangeEvent<HTMLInputElement>
) => this.props.onChange({ [name]: e.target.value });
render() {
const defaults = {
filter: { query: '', language: getDefaultQueryLanguage() },
@ -56,106 +132,31 @@ class TimeseriesPanelConfigUi extends Component {
const model = { ...defaults, ...this.props.model };
const { selectedTab } = this.state;
const handleSelectChange = createSelectHandler(this.props.onChange);
const handleTextChange = createTextHandler(this.props.onChange);
const htmlId = htmlIdGenerator();
const { intl } = this.props;
const positionOptions = [
{
label: intl.formatMessage({
id: 'visTypeTimeseries.timeseries.positionOptions.rightLabel',
defaultMessage: 'Right',
}),
value: 'right',
},
{
label: intl.formatMessage({
id: 'visTypeTimeseries.timeseries.positionOptions.leftLabel',
defaultMessage: 'Left',
}),
value: 'left',
},
];
const tooltipModeOptions = [
{
label: intl.formatMessage({
id: 'visTypeTimeseries.timeseries.tooltipOptions.showAll',
defaultMessage: 'Show all values',
}),
value: 'show_all',
},
{
label: intl.formatMessage({
id: 'visTypeTimeseries.timeseries.tooltipOptions.showFocused',
defaultMessage: 'Show focused values',
}),
value: 'show_focused',
},
];
const selectedPositionOption = positionOptions.find((option) => {
return model.axis_position === option.value;
});
const scaleOptions = [
{
label: intl.formatMessage({
id: 'visTypeTimeseries.timeseries.scaleOptions.normalLabel',
defaultMessage: 'Normal',
}),
value: 'normal',
},
{
label: intl.formatMessage({
id: 'visTypeTimeseries.timeseries.scaleOptions.logLabel',
defaultMessage: 'Log',
}),
value: 'log',
},
];
const selectedAxisScaleOption = scaleOptions.find((option) => {
return model.axis_scale === option.value;
});
const legendPositionOptions = [
{
label: intl.formatMessage({
id: 'visTypeTimeseries.timeseries.legendPositionOptions.rightLabel',
defaultMessage: 'Right',
}),
value: 'right',
},
{
label: intl.formatMessage({
id: 'visTypeTimeseries.timeseries.legendPositionOptions.leftLabel',
defaultMessage: 'Left',
}),
value: 'left',
},
{
label: intl.formatMessage({
id: 'visTypeTimeseries.timeseries.legendPositionOptions.bottomLabel',
defaultMessage: 'Bottom',
}),
value: 'bottom',
},
];
const selectedLegendPosOption = legendPositionOptions.find((option) => {
return model.legend_position === option.value;
});
const selectedTooltipMode = tooltipModeOptions.find((option) => {
return model.tooltip_mode === option.value;
});
const selectedPositionOption = positionOptions.find(
(option) => model.axis_position === option.value
);
const selectedAxisScaleOption = scaleOptions.find(
(option) => model.axis_scale === option.value
);
const selectedLegendPosOption = legendPositionOptions.find(
(option) => model.legend_position === option.value
);
const selectedTooltipMode = tooltipModeOptions.find(
(option) => model.tooltip_mode === option.value
);
let view;
if (selectedTab === 'data') {
if (selectedTab === PANEL_CONFIG_TABS.DATA) {
view = (
<SeriesEditor
fields={this.props.fields}
model={this.props.model}
name={this.props.name}
onChange={this.props.onChange}
/>
);
} else if (selectedTab === 'annotations') {
} else if (selectedTab === PANEL_CONFIG_TABS.ANNOTATIONS) {
view = (
<AnnotationsEditor
fields={this.props.fields}
@ -204,7 +205,9 @@ class TimeseriesPanelConfigUi extends Component {
language: model.filter.language || getDefaultQueryLanguage(),
query: model.filter.query || '',
}}
onChange={(filter) => this.props.onChange({ filter })}
onChange={(filter: PanelConfigProps['model']['filter']) =>
this.props.onChange({ filter })
}
indexPatterns={[model.index_pattern || model.default_index_pattern]}
/>
</EuiFormRow>
@ -250,7 +253,10 @@ class TimeseriesPanelConfigUi extends Component {
/>
}
>
<EuiFieldText onChange={handleTextChange('axis_min')} value={model.axis_min} />
<EuiFieldText
onChange={this.handleTextChange('axis_min')}
value={model.axis_min ?? ''}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
@ -263,7 +269,10 @@ class TimeseriesPanelConfigUi extends Component {
/>
}
>
<EuiFieldText onChange={handleTextChange('axis_max')} value={model.axis_max} />
<EuiFieldText
onChange={this.handleTextChange('axis_max')}
value={model.axis_max ?? ''}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
@ -394,15 +403,18 @@ class TimeseriesPanelConfigUi extends Component {
return (
<>
<EuiTabs size="s">
<EuiTab isSelected={selectedTab === 'data'} onClick={() => this.switchTab('data')}>
<EuiTab
isSelected={selectedTab === PANEL_CONFIG_TABS.DATA}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.DATA)}
>
<FormattedMessage
id="visTypeTimeseries.timeseries.dataTab.dataButtonLabel"
defaultMessage="Data"
/>
</EuiTab>
<EuiTab
isSelected={selectedTab === 'options'}
onClick={() => this.switchTab('options')}
isSelected={selectedTab === PANEL_CONFIG_TABS.OPTIONS}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.OPTIONS)}
data-test-subj="timeSeriesEditorPanelOptionsBtn"
>
<FormattedMessage
@ -411,8 +423,8 @@ class TimeseriesPanelConfigUi extends Component {
/>
</EuiTab>
<EuiTab
isSelected={selectedTab === 'annotations'}
onClick={() => this.switchTab('annotations')}
isSelected={selectedTab === PANEL_CONFIG_TABS.ANNOTATIONS}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.ANNOTATIONS)}
>
<FormattedMessage
id="visTypeTimeseries.timeseries.annotationsTab.annotationsButtonLabel"
@ -425,11 +437,3 @@ class TimeseriesPanelConfigUi extends Component {
);
}
}
TimeseriesPanelConfigUi.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
};
export const TimeseriesPanelConfig = injectI18n(TimeseriesPanelConfigUi);

View file

@ -6,15 +6,8 @@
* Side Public License, v 1.
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { SeriesEditor } from '../series_editor';
import { IndexPattern } from '../index_pattern';
import { createTextHandler } from '../lib/create_text_handler';
import { ColorRules } from '../color_rules';
import { ColorPicker } from '../color_picker';
import uuid from 'uuid';
import { YesNo } from '../yes_no';
import {
htmlIdGenerator,
EuiTabs,
@ -31,28 +24,44 @@ import {
EuiCode,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { getDefaultQueryLanguage } from '../lib/get_default_query_language';
import { QueryBarWrapper } from '../query_bar_wrapper';
export class TopNPanelConfig extends Component {
constructor(props) {
// @ts-expect-error not typed yet
import { SeriesEditor } from '../series_editor';
// @ts-ignore should be typed after https://github.com/elastic/kibana/pull/92812 to reduce conflicts
import { IndexPattern } from '../index_pattern';
import { ColorRules } from '../color_rules';
import { ColorPicker } from '../color_picker';
import { YesNo } from '../yes_no';
import { getDefaultQueryLanguage } from '../lib/get_default_query_language';
// @ts-ignore this is typed in https://github.com/elastic/kibana/pull/92812, remove ignore after merging
import { QueryBarWrapper } from '../query_bar_wrapper';
import { PanelConfigProps, PANEL_CONFIG_TABS } from './types';
import { TimeseriesVisParams } from '../../../types';
export class TopNPanelConfig extends Component<
PanelConfigProps,
{ selectedTab: PANEL_CONFIG_TABS }
> {
constructor(props: PanelConfigProps) {
super(props);
this.state = { selectedTab: 'data' };
this.state = { selectedTab: PANEL_CONFIG_TABS.DATA };
}
UNSAFE_componentWillMount() {
const { model } = this.props;
const parts = {};
if (!model.bar_color_rules || (model.bar_color_rules && model.bar_color_rules.length === 0)) {
parts.bar_color_rules = [{ id: uuid.v1() }];
if (!model.bar_color_rules || !model.bar_color_rules.length) {
this.props.onChange({ bar_color_rules: [{ id: uuid.v1() }] });
}
this.props.onChange(parts);
}
switchTab(selectedTab) {
switchTab(selectedTab: PANEL_CONFIG_TABS) {
this.setState({ selectedTab });
}
handleTextChange = (name: keyof TimeseriesVisParams) => (
e: React.ChangeEvent<HTMLInputElement>
) => this.props.onChange({ [name]: e.target.value });
render() {
const { selectedTab } = this.state;
const defaults = {
@ -61,20 +70,15 @@ export class TopNPanelConfig extends Component {
};
const model = { ...defaults, ...this.props.model };
const htmlId = htmlIdGenerator();
const handleTextChange = createTextHandler(this.props.onChange);
let view;
if (selectedTab === 'data') {
view = (
const view =
selectedTab === PANEL_CONFIG_TABS.DATA ? (
<SeriesEditor
colorPicker={false}
fields={this.props.fields}
model={this.props.model}
name={this.props.name}
onChange={this.props.onChange}
/>
);
} else {
view = (
) : (
<div className="tvbPanelConfig__container">
<EuiPanel>
<EuiTitle size="s">
@ -105,8 +109,8 @@ export class TopNPanelConfig extends Component {
}
>
<EuiFieldText
onChange={handleTextChange('drilldown_url')}
value={model.drilldown_url}
onChange={this.handleTextChange('drilldown_url')}
value={model.drilldown_url ?? ''}
/>
</EuiFormRow>
@ -134,12 +138,12 @@ export class TopNPanelConfig extends Component {
>
<QueryBarWrapper
query={{
language: model.filter.language
? model.filter.language
: getDefaultQueryLanguage(),
language: model.filter.language || getDefaultQueryLanguage(),
query: model.filter.query || '',
}}
onChange={(filter) => this.props.onChange({ filter })}
onChange={(filter: PanelConfigProps['model']['filter']) =>
this.props.onChange({ filter })
}
indexPatterns={[model.index_pattern || model.default_index_pattern]}
/>
</EuiFormRow>
@ -214,17 +218,23 @@ export class TopNPanelConfig extends Component {
</EuiPanel>
</div>
);
}
return (
<>
<EuiTabs size="s">
<EuiTab isSelected={selectedTab === 'data'} onClick={() => this.switchTab('data')}>
<EuiTab
isSelected={selectedTab === PANEL_CONFIG_TABS.DATA}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.DATA)}
>
<FormattedMessage
id="visTypeTimeseries.topN.dataTab.dataButtonLabel"
defaultMessage="Data"
/>
</EuiTab>
<EuiTab isSelected={selectedTab === 'options'} onClick={() => this.switchTab('options')}>
<EuiTab
isSelected={selectedTab === PANEL_CONFIG_TABS.OPTIONS}
onClick={() => this.switchTab(PANEL_CONFIG_TABS.OPTIONS)}
>
<FormattedMessage
id="visTypeTimeseries.topN.optionsTab.panelOptionsButtonLabel"
defaultMessage="Panel options"
@ -236,9 +246,3 @@ export class TopNPanelConfig extends Component {
);
}
}
TopNPanelConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
};

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Observable } from 'rxjs';
import { IUiSettingsClient } from 'kibana/public';
import { TimeseriesVisData } from '../../../../common/types';
import { TimeseriesVisParams } from '../../../types';
import { VisFields } from '../../lib/fetch_fields';
export interface PanelConfigProps {
fields: VisFields;
model: TimeseriesVisParams;
visData$: Observable<TimeseriesVisData | undefined>;
getConfig: IUiSettingsClient['get'];
onChange: (partialModel: Partial<TimeseriesVisParams>) => void;
}
export enum PANEL_CONFIG_TABS {
DATA = 'data',
OPTIONS = 'options',
ANNOTATIONS = 'annotations',
MARKDOWN = 'markdown',
}

View file

@ -7,29 +7,29 @@
*/
import React from 'react';
import { expect } from 'chai';
import { shallowWithIntl } from '@kbn/test/jest';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import { YesNo } from './yes_no';
describe('YesNo', () => {
it('call onChange={handleChange} on yes', () => {
const handleChange = sinon.spy();
const wrapper = shallowWithIntl(<YesNo name="test" onChange={handleChange} />);
const handleChange = jest.fn();
const wrapper = shallow(
<YesNo name="ignore_global_filters" onChange={handleChange} value={0} />
);
wrapper.find('EuiRadio').first().simulate('change');
expect(handleChange.calledOnce).to.equal(true);
expect(handleChange.firstCall.args[0]).to.eql({
test: 1,
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith({
ignore_global_filters: 1,
});
});
it('call onChange={handleChange} on no', () => {
const handleChange = sinon.spy();
const wrapper = shallowWithIntl(<YesNo name="test" onChange={handleChange} />);
const handleChange = jest.fn();
const wrapper = shallow(<YesNo name="show_legend" onChange={handleChange} value={1} />);
wrapper.find('EuiRadio').last().simulate('change');
expect(handleChange.calledOnce).to.equal(true);
expect(handleChange.firstCall.args[0]).to.eql({
test: 0,
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith({
show_legend: 0,
});
});
});

View file

@ -6,23 +6,35 @@
* Side Public License, v 1.
*/
import PropTypes from 'prop-types';
import React from 'react';
import _ from 'lodash';
import React, { useCallback } from 'react';
import { EuiRadio, htmlIdGenerator } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { TimeseriesVisParams } from '../../types';
export function YesNo(props) {
const { name, value, disabled, 'data-test-subj': dataTestSubj } = props;
const handleChange = (value) => {
const { name } = props;
return () => {
const parts = { [name]: value };
props.onChange(parts);
};
};
interface YesNoProps<ParamName extends keyof TimeseriesVisParams> {
name: ParamName;
value: TimeseriesVisParams[ParamName];
disabled?: boolean;
'data-test-subj'?: string;
onChange: (partialModel: Partial<TimeseriesVisParams>) => void;
}
export function YesNo<ParamName extends keyof TimeseriesVisParams>({
name,
value,
disabled,
'data-test-subj': dataTestSubj,
onChange,
}: YesNoProps<ParamName>) {
const handleChange = useCallback(
(val: number) => {
return () => onChange({ [name]: val });
},
[onChange, name]
);
const htmlId = htmlIdGenerator();
const inputName = name + _.uniqueId();
const inputName = htmlId(name);
return (
<div>
<EuiRadio
@ -63,12 +75,3 @@ export function YesNo(props) {
</div>
);
}
YesNo.propTypes = {
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
};
YesNo.defaultProps = {
disabled: false,
};