[TSVB] Index pattern select field disappear in Annotation tab (#102314)

* [TSVB] Index pattern select field disappear in Annotation tab

* Refactor AnnotationsEditor and move renderRow logic into AnnotationRow

* Remove duplicated license, add useCallback to functions in AnnotationRow, remove cross refecrence and update types

* Refactor AnnotationEditor and AnnotationRow

* refactoring

* remove extra props

* fix nits

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Alexey Antonov <alexwizp@gmail.com>
This commit is contained in:
Diana Derevyankina 2021-06-18 17:32:17 +03:00 committed by GitHub
parent d65416c60c
commit 853de830c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 398 additions and 329 deletions

View file

@ -0,0 +1,280 @@
/*
* 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 React, { useState, useEffect, useCallback, useMemo, ChangeEvent } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiCode,
EuiComboBoxOptionOption,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiSpacer,
htmlIdGenerator,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { getDataStart } from '../../services';
import { KBN_FIELD_TYPES, Query } from '../../../../../plugins/data/public';
import { AddDeleteButtons } from './add_delete_buttons';
import { ColorPicker } from './color_picker';
import { FieldSelect } from './aggs/field_select';
import { IndexPatternSelect } from './lib/index_pattern_select';
import { QueryBarWrapper } from './query_bar_wrapper';
import { YesNo } from './yes_no';
import { fetchIndexPattern } from '../../../common/index_patterns_utils';
import { getDefaultQueryLanguage } from './lib/get_default_query_language';
// @ts-expect-error not typed yet
import { IconSelect } from './icon_select/icon_select';
import type { Annotation, FetchedIndexPattern, IndexPatternValue } from '../../../common/types';
import type { VisFields } from '../lib/fetch_fields';
const RESTRICT_FIELDS = [KBN_FIELD_TYPES.DATE];
const INDEX_PATTERN_KEY = 'index_pattern';
const TIME_FIELD_KEY = 'time_field';
export interface AnnotationRowProps {
annotation: Annotation;
fields: VisFields;
onChange: (partialModel: Partial<Annotation>) => void;
handleAdd: () => void;
handleDelete: () => void;
}
const getAnnotationDefaults = () => ({
fields: '',
template: '',
index_pattern: '',
query_string: { query: '', language: getDefaultQueryLanguage() },
});
export const AnnotationRow = ({
annotation,
fields,
onChange,
handleAdd,
handleDelete,
}: AnnotationRowProps) => {
const model = useMemo(() => ({ ...getAnnotationDefaults(), ...annotation }), [annotation]);
const htmlId = htmlIdGenerator(model.id);
const [fetchedIndex, setFetchedIndex] = useState<FetchedIndexPattern | null>(null);
useEffect(() => {
const updateFetchedIndex = async (index: IndexPatternValue) => {
const { indexPatterns } = getDataStart();
setFetchedIndex(
index
? await fetchIndexPattern(index, indexPatterns)
: {
indexPattern: undefined,
indexPatternString: undefined,
}
);
};
updateFetchedIndex(model.index_pattern);
}, [model.index_pattern]);
const togglePanelActivation = useCallback(
() =>
onChange({
hidden: !model.hidden,
}),
[model.hidden, onChange]
);
const handleChange = useCallback(
(name: string) => (
event: Array<EuiComboBoxOptionOption<string>> | ChangeEvent<HTMLInputElement>
) =>
onChange({
[name]: Array.isArray(event) ? event?.[0]?.value : event.target.value,
}),
[onChange]
);
const handleQueryChange = useCallback(
(filter: Query) =>
onChange({
query_string: filter,
}),
[onChange]
);
return (
<div className="tvbAnnotationsEditor" key={model.id}>
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={false}>
<ColorPicker disableTrash={true} onChange={onChange} name="color" value={model.color} />
</EuiFlexItem>
<EuiFlexItem className="tvbAggRow__children">
<EuiFlexGroup responsive={false} wrap={true} gutterSize="m">
<EuiFlexItem>
<IndexPatternSelect
value={model[INDEX_PATTERN_KEY]}
indexPatternName={INDEX_PATTERN_KEY}
onChange={onChange}
fetchedIndex={fetchedIndex}
/>
</EuiFlexItem>
<EuiFlexItem>
<FieldSelect
type={TIME_FIELD_KEY}
label={
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.timeFieldLabel"
defaultMessage="Time field (required)"
/>
}
restrict={RESTRICT_FIELDS}
value={model.time_field}
onChange={handleChange(TIME_FIELD_KEY)}
indexPattern={model.index_pattern}
fields={fields}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexGroup responsive={false} wrap={true} gutterSize="m">
<EuiFlexItem>
<EuiFormRow
id={htmlId('queryString')}
label={
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.queryStringLabel"
defaultMessage="Query string"
/>
}
fullWidth
>
<QueryBarWrapper
query={{
language: model.query_string.language || getDefaultQueryLanguage(),
query: model.query_string.query || '',
}}
onChange={handleQueryChange}
indexPatterns={[model.index_pattern]}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow
label={i18n.translate(
'visTypeTimeseries.annotationsEditor.ignoreGlobalFiltersLabel',
{
defaultMessage: 'Ignore global filters?',
}
)}
>
<YesNo
value={model.ignore_global_filters}
name="ignore_global_filters"
onChange={onChange}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow
label={i18n.translate(
'visTypeTimeseries.annotationsEditor.ignorePanelFiltersLabel',
{
defaultMessage: 'Ignore panel filters?',
}
)}
>
<YesNo
value={model.ignore_panel_filters}
name="ignore_panel_filters"
onChange={onChange}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexGroup responsive={false} wrap={true} gutterSize="m">
<EuiFlexItem>
<EuiFormRow
id={htmlId('icon')}
label={
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.iconLabel"
defaultMessage="Icon (required)"
/>
}
>
<IconSelect value={model.icon} onChange={handleChange('icon')} />
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
id={htmlId('fields')}
label={
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.fieldsLabel"
defaultMessage="Fields (required - comma separated paths)"
/>
}
fullWidth
>
<EuiFieldText onChange={handleChange('fields')} value={model.fields} fullWidth />
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
id={htmlId('rowTemplate')}
label={
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.rowTemplateLabel"
defaultMessage="Row template (required)"
/>
}
helpText={
<span>
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.rowTemplateHelpText"
defaultMessage="eg.{rowTemplateExample}"
values={{ rowTemplateExample: <EuiCode>{'{{field}}'}</EuiCode> }}
/>
</span>
}
fullWidth
>
<EuiFieldText
onChange={handleChange('template')}
value={model.template}
fullWidth
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddDeleteButtons
onAdd={handleAdd}
onDelete={handleDelete}
togglePanelActivation={togglePanelActivation}
isPanelActive={!model.hidden}
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
};

View file

@ -1,322 +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 { i18n } from '@kbn/i18n';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import _ from 'lodash';
import { collectionActions } from './lib/collection_actions';
import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public';
import { AddDeleteButtons } from './add_delete_buttons';
import { ColorPicker } from './color_picker';
import { FieldSelect } from './aggs/field_select';
import uuid from 'uuid';
import { IconSelect } from './icon_select/icon_select';
import { YesNo } from './yes_no';
import { QueryBarWrapper } from './query_bar_wrapper';
import { getDefaultQueryLanguage } from './lib/get_default_query_language';
import {
htmlIdGenerator,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiSpacer,
EuiFieldText,
EuiTitle,
EuiButton,
EuiCode,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { IndexPatternSelect } from './lib/index_pattern_select';
function newAnnotation() {
return {
id: uuid.v1(),
color: '#F00',
index_pattern: '',
time_field: '',
icon: 'fa-tag',
ignore_global_filters: 1,
ignore_panel_filters: 1,
};
}
const RESTRICT_FIELDS = [KBN_FIELD_TYPES.DATE];
export class AnnotationsEditor extends Component {
constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
}
handleChange(item, name) {
return (e) => {
const handleChange = collectionActions.handleChange.bind(null, this.props);
const part = {};
part[name] = _.get(e, '[0].value', _.get(e, 'target.value'));
handleChange(_.assign({}, item, part));
};
}
handleQueryChange = (model, filter) => {
const part = { query_string: filter };
collectionActions.handleChange(this.props, {
...model,
...part,
});
};
renderRow(row) {
const defaults = {
fields: '',
template: '',
index_pattern: '',
query_string: { query: '', language: getDefaultQueryLanguage() },
};
const model = { ...defaults, ...row };
const handleChange = (part) => {
const fn = collectionActions.handleChange.bind(null, this.props);
fn(_.assign({}, model, part));
};
const togglePanelActivation = () => {
handleChange({
hidden: !model.hidden,
});
};
const htmlId = htmlIdGenerator(model.id);
const handleAdd = collectionActions.handleAdd.bind(null, this.props, newAnnotation);
const handleDelete = collectionActions.handleDelete.bind(null, this.props, model);
return (
<div className="tvbAnnotationsEditor" key={model.id}>
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={false}>
<ColorPicker
disableTrash={true}
onChange={handleChange}
name="color"
value={model.color}
/>
</EuiFlexItem>
<EuiFlexItem className="tvbAggRow__children">
<EuiFlexGroup responsive={false} wrap={true} gutterSize="m">
<EuiFlexItem>
<IndexPatternSelect
value={model.index_pattern}
indexPatternName={'index_pattern'}
onChange={handleChange}
/>
</EuiFlexItem>
<EuiFlexItem>
<FieldSelect
label={
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.timeFieldLabel"
defaultMessage="Time field (required)"
/>
}
restrict={RESTRICT_FIELDS}
value={model.time_field}
onChange={this.handleChange(model, 'time_field')}
indexPattern={model.index_pattern}
fields={this.props.fields}
fullWidth
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexGroup responsive={false} wrap={true} gutterSize="m">
<EuiFlexItem>
<EuiFormRow
id={htmlId('queryString')}
label={
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.queryStringLabel"
defaultMessage="Query string"
/>
}
fullWidth
>
<QueryBarWrapper
query={{
language: model.query_string.language || getDefaultQueryLanguage(),
query: model.query_string.query || '',
}}
onChange={(query) => this.handleQueryChange(model, query)}
indexPatterns={[model.index_pattern]}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow
label={i18n.translate(
'visTypeTimeseries.annotationsEditor.ignoreGlobalFiltersLabel',
{
defaultMessage: 'Ignore global filters?',
}
)}
>
<YesNo
value={model.ignore_global_filters}
name="ignore_global_filters"
onChange={handleChange}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow
label={i18n.translate(
'visTypeTimeseries.annotationsEditor.ignorePanelFiltersLabel',
{
defaultMessage: 'Ignore panel filters?',
}
)}
>
<YesNo
value={model.ignore_panel_filters}
name="ignore_panel_filters"
onChange={handleChange}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexGroup responsive={false} wrap={true} gutterSize="m">
<EuiFlexItem>
<EuiFormRow
id={htmlId('icon')}
label={
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.iconLabel"
defaultMessage="Icon (required)"
/>
}
>
<IconSelect value={model.icon} onChange={this.handleChange(model, 'icon')} />
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
id={htmlId('fields')}
label={
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.fieldsLabel"
defaultMessage="Fields (required - comma separated paths)"
/>
}
fullWidth
>
<EuiFieldText
onChange={this.handleChange(model, 'fields')}
value={model.fields}
fullWidth
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
id={htmlId('rowTemplate')}
label={
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.rowTemplateLabel"
defaultMessage="Row template (required)"
/>
}
helpText={
<span>
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.rowTemplateHelpText"
defaultMessage="eg.{rowTemplateExample}"
values={{ rowTemplateExample: <EuiCode>{'{{field}}'}</EuiCode> }}
/>
</span>
}
fullWidth
>
<EuiFieldText
onChange={this.handleChange(model, 'template')}
value={model.template}
fullWidth
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddDeleteButtons
onAdd={handleAdd}
onDelete={handleDelete}
togglePanelActivation={togglePanelActivation}
isPanelActive={!model.hidden}
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
}
render() {
const { model } = this.props;
let content;
if (!model.annotations || !model.annotations.length) {
const handleAdd = collectionActions.handleAdd.bind(null, this.props, newAnnotation);
content = (
<EuiText textAlign="center">
<p>
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.howToCreateAnnotationDataSourceDescription"
defaultMessage="Click the button below to create an annotation data source."
/>
</p>
<EuiButton fill onClick={handleAdd}>
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.addDataSourceButtonLabel"
defaultMessage="Add data source"
/>
</EuiButton>
</EuiText>
);
} else {
const annotations = model.annotations.map(this.renderRow);
content = (
<div>
<EuiTitle size="s">
<span>
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.dataSourcesLabel"
defaultMessage="Data sources"
/>
</span>
</EuiTitle>
<EuiSpacer size="m" />
{annotations}
</div>
);
}
return <div className="tvbAnnotationsEditor__container">{content}</div>;
}
}
AnnotationsEditor.defaultProps = {
name: 'annotations',
};
AnnotationsEditor.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
name: PropTypes.string,
onChange: PropTypes.func,
uiSettings: PropTypes.object,
};

View file

@ -0,0 +1,113 @@
/*
* 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 React, { useCallback } from 'react';
import uuid from 'uuid';
import { EuiSpacer, EuiTitle, EuiButton, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { AnnotationRow } from './annotation_row';
import { collectionActions, CollectionActionsProps } from './lib/collection_actions';
import type { Panel, Annotation } from '../../../common/types';
import type { VisFields } from '../lib/fetch_fields';
interface AnnotationsEditorProps {
fields: VisFields;
model: Panel;
onChange: (partialModel: Partial<Panel>) => void;
}
export const newAnnotation = () => ({
id: uuid.v1(),
color: '#F00',
index_pattern: '',
time_field: '',
icon: 'fa-tag',
ignore_global_filters: 1,
ignore_panel_filters: 1,
});
const NoContent = ({ handleAdd }: { handleAdd: () => void }) => (
<EuiText textAlign="center">
<p>
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.howToCreateAnnotationDataSourceDescription"
defaultMessage="Click the button below to create an annotation data source."
/>
</p>
<EuiButton fill onClick={handleAdd}>
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.addDataSourceButtonLabel"
defaultMessage="Add data source"
/>
</EuiButton>
</EuiText>
);
const getCollectionActionsProps = (props: AnnotationsEditorProps) =>
({
name: 'annotations',
...props,
} as CollectionActionsProps<Panel>);
export const AnnotationsEditor = (props: AnnotationsEditorProps) => {
const { annotations } = props.model;
const handleAdd = useCallback(
() => collectionActions.handleAdd(getCollectionActionsProps(props), newAnnotation),
[props]
);
const handleDelete = useCallback(
(annotation) => () =>
collectionActions.handleDelete(getCollectionActionsProps(props), annotation),
[props]
);
const onChange = useCallback(
(annotation: Annotation) => {
return (part: Partial<Annotation>) =>
collectionActions.handleChange(getCollectionActionsProps(props), {
...annotation,
...part,
});
},
[props]
);
return (
<div className="tvbAnnotationsEditor__container">
{annotations?.length ? (
<div>
<EuiTitle size="s">
<span>
<FormattedMessage
id="visTypeTimeseries.annotationsEditor.dataSourcesLabel"
defaultMessage="Data sources"
/>
</span>
</EuiTitle>
<EuiSpacer size="m" />
{annotations.map((annotation) => (
<AnnotationRow
key={annotation.id}
annotation={annotation}
fields={props.fields}
onChange={onChange(annotation)}
handleAdd={handleAdd}
handleDelete={handleDelete(annotation)}
/>
))}
</div>
) : (
<NoContent handleAdd={handleAdd} />
)}
</div>
);
};

View file

@ -28,9 +28,8 @@ import {
// @ts-expect-error not typed yet
import { SeriesEditor } from '../series_editor';
// @ts-expect-error not typed yet
import { AnnotationsEditor } from '../annotations_editor';
// @ts-expect-error not typed yet
import { IndexPattern } from '../index_pattern';
import { AnnotationsEditor } from '../annotations_editor';
import { createSelectHandler } from '../lib/create_select_handler';
import { ColorPicker } from '../color_picker';
import { YesNo } from '../yes_no';
@ -162,7 +161,6 @@ export class TimeseriesPanelConfig extends Component<
<AnnotationsEditor
fields={this.props.fields}
model={this.props.model}
name="annotations"
onChange={this.props.onChange}
/>
);

View file

@ -11,21 +11,21 @@ import { EuiRadio, htmlIdGenerator } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { TimeseriesVisParams } from '../../types';
interface YesNoProps<ParamName extends keyof TimeseriesVisParams> {
name: ParamName;
interface YesNoProps {
name: string;
value: boolean | number | undefined;
disabled?: boolean;
'data-test-subj'?: string;
onChange: (partialModel: Partial<TimeseriesVisParams>) => void;
}
export function YesNo<ParamName extends keyof TimeseriesVisParams>({
export function YesNo({
name,
value,
disabled,
'data-test-subj': dataTestSubj,
onChange,
}: YesNoProps<ParamName>) {
}: YesNoProps) {
const handleChange = useCallback(
(val: number) => {
return () => onChange({ [name]: val });