mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[DataViewField] Allow to add a custom description for data view fields (#168577)
- Closes https://github.com/elastic/kibana/issues/89726 ## Summary This PR extends Data View Field flyout with a new textarea to enter and save a custom field description. This description will be shown in a field popover for Discover sidebar, in Doc Viewer and also on Data View management page. Current limit for the custom description is 300. When creating/editing a field: <img width="600" alt="Screenshot 2024-03-07 at 18 59 24" src="433e66d1
-9366-4906-8aea-33b77ae81c16"> In the field popover(truncated): <img width="500" alt="Screenshot 2024-03-07 at 18 56 52" src="8753a11d
-6b27-40c1-adaa-de35addb50df"> In the field popover(expanded): <img width="500" alt="Screenshot 2024-03-07 at 18 57 00" src="b593169e
-305e-4d4b-853a-4937324e2470"> In Doc Viewer popover(always expanded): <img width="500" alt="Screenshot 2024-03-07 at 18 57 21" src="106562a2
-baad-4952-a9cc-fa779f96c1e1"> On Data View Management page(truncated): <img width="500" alt="Screenshot 2024-03-07 at 18 57 42" src="031ed482
-5c84-484f-ae9e-6b1e7622c17c"> <details> <summary>Initial implementation examples</summary>  <img width="600" alt="Screenshot 2023-10-11 at 11 52 22" src="8e40da6f
-fcfc-4e36-9314-d3fc34e6ecab"> <img width="600" alt="Screenshot 2023-10-11 at 11 46 44" src="d5ee22a7
-0314-4742-b75f-6534e1b4024d"> <img width="600" alt="Screenshot 2023-10-11 at 11 46 29" src="dcd809df
-7942-4165-8b83-4a83267cea00"> <img width="600" alt="Screenshot 2023-10-11 at 11 47 15" src="197add4d
-b185-4631-a2b9-eaf013aad8ba"> <img width="600" alt="Screenshot 2023-10-11 at 12 05 29" src="9b619e20
-c3d1-4c20-ac65-8b922ad1da72"> </details> ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Matthew Kime <matt@mattki.me> Co-authored-by: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com>
This commit is contained in:
parent
64ea83912a
commit
fc2c389cce
45 changed files with 1003 additions and 176 deletions
|
@ -24,3 +24,8 @@ export {
|
|||
} from './src/utils/field_name_wildcard_matcher';
|
||||
|
||||
export { FieldIcon, type FieldIconProps, getFieldIconProps } from './src/components/field_icon';
|
||||
export { FieldDescription, type FieldDescriptionProps } from './src/components/field_description';
|
||||
export {
|
||||
FieldDescriptionIconButton,
|
||||
type FieldDescriptionIconButtonProps,
|
||||
} from './src/components/field_description_icon_button';
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { FieldDescription } from './field_description';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
describe('FieldDescription', () => {
|
||||
it('should render correctly when no custom description', async () => {
|
||||
render(<FieldDescription field={{ name: 'bytes' }} />);
|
||||
const desc = screen.queryByTestId('fieldDescription-bytes');
|
||||
expect(desc).toBeNull();
|
||||
});
|
||||
|
||||
it('should render correctly with a short custom description', async () => {
|
||||
const customDescription = 'test this desc';
|
||||
render(<FieldDescription field={{ name: 'bytes', customDescription }} />);
|
||||
const desc = screen.queryByTestId('fieldDescription-bytes');
|
||||
expect(desc).toHaveTextContent(customDescription);
|
||||
const button = screen.queryByTestId('toggleFieldDescription-bytes');
|
||||
expect(button).toBeNull();
|
||||
});
|
||||
|
||||
it('should render correctly with a long custom description', async () => {
|
||||
const customDescription = 'test this long desc '.repeat(8).trim();
|
||||
render(<FieldDescription field={{ name: 'bytes', customDescription }} />);
|
||||
expect(screen.queryByTestId('fieldDescription-bytes')).toHaveTextContent(customDescription);
|
||||
screen.queryByTestId('toggleFieldDescription-bytes')?.click();
|
||||
expect(screen.queryByTestId('fieldDescription-bytes')).toHaveTextContent(
|
||||
`${customDescription}View less`
|
||||
);
|
||||
screen.queryByTestId('toggleFieldDescription-bytes')?.click();
|
||||
expect(screen.queryByTestId('fieldDescription-bytes')).toHaveTextContent(customDescription);
|
||||
});
|
||||
|
||||
it('should render a long custom description without truncation', async () => {
|
||||
const customDescription = 'test this long desc '.repeat(8).trim();
|
||||
render(<FieldDescription field={{ name: 'bytes', customDescription }} truncate={false} />);
|
||||
expect(screen.queryByTestId('fieldDescription-bytes')).toHaveTextContent(customDescription);
|
||||
const button = screen.queryByTestId('toggleFieldDescription-bytes');
|
||||
expect(button).toBeNull();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiText, EuiButtonEmpty, EuiTextBlockTruncate, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
const MAX_VISIBLE_LENGTH = 110;
|
||||
|
||||
export interface FieldDescriptionProps {
|
||||
field: {
|
||||
name: string;
|
||||
customDescription?: string;
|
||||
};
|
||||
color?: 'subdued';
|
||||
truncate?: boolean;
|
||||
}
|
||||
|
||||
export const FieldDescription: React.FC<FieldDescriptionProps> = ({
|
||||
field,
|
||||
color,
|
||||
truncate = true,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const customDescription = (field?.customDescription || '').trim();
|
||||
const isTooLong = Boolean(truncate && customDescription.length > MAX_VISIBLE_LENGTH);
|
||||
const [isTruncated, setIsTruncated] = useState<boolean>(isTooLong);
|
||||
|
||||
if (!customDescription) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-test-subj={`fieldDescription-${field.name}`}>
|
||||
{isTruncated ? (
|
||||
<EuiText color={color} size="xs" className="eui-textBreakWord eui-textLeft">
|
||||
<button
|
||||
data-test-subj={`toggleFieldDescription-${field.name}`}
|
||||
title={i18n.translate('fieldUtils.fieldDescription.viewMoreButton', {
|
||||
defaultMessage: 'View full field description',
|
||||
})}
|
||||
className="eui-textBreakWord eui-textLeft"
|
||||
onClick={() => setIsTruncated(false)}
|
||||
css={css`
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: ${color === 'subdued' ? euiTheme.colors.subduedText : euiTheme.colors.text};
|
||||
line-height: inherit;
|
||||
font-size: inherit;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: ${euiTheme.colors.link};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<EuiTextBlockTruncate lines={2}>{customDescription}</EuiTextBlockTruncate>
|
||||
</button>
|
||||
</EuiText>
|
||||
) : (
|
||||
<>
|
||||
<EuiText color={color} size="xs" className="eui-textBreakWord eui-textLeft">
|
||||
{customDescription}
|
||||
</EuiText>
|
||||
{isTooLong && (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
flush="both"
|
||||
data-test-subj={`toggleFieldDescription-${field.name}`}
|
||||
onClick={() => setIsTruncated(true)}
|
||||
>
|
||||
{i18n.translate('fieldUtils.fieldDescription.viewLessButton', {
|
||||
defaultMessage: 'View less',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { FieldDescription, type FieldDescriptionProps } from './field_description';
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { FieldDescriptionIconButton } from './field_description_icon_button';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
describe('FieldDescriptionIconButton', () => {
|
||||
it('should render correctly when no custom description', async () => {
|
||||
const { container } = render(<FieldDescriptionIconButton field={{ name: 'bytes' }} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render correctly with a short custom description', async () => {
|
||||
const customDescription = 'test this desc';
|
||||
render(<FieldDescriptionIconButton field={{ name: 'bytes', customDescription }} />);
|
||||
expect(screen.queryByTestId('fieldDescription-bytes')).toBeNull();
|
||||
screen.queryByTestId('fieldDescriptionPopoverButton-bytes')?.click();
|
||||
expect(screen.queryByTestId('fieldDescription-bytes')).toHaveTextContent(customDescription);
|
||||
});
|
||||
|
||||
it('should render correctly with a long custom description', async () => {
|
||||
const customDescription = 'test this long desc '.repeat(8).trim();
|
||||
render(<FieldDescriptionIconButton field={{ name: 'bytes', customDescription }} />);
|
||||
expect(screen.queryByTestId('fieldDescription-bytes')).toBeNull();
|
||||
screen.queryByTestId('fieldDescriptionPopoverButton-bytes')?.click();
|
||||
expect(screen.queryByTestId('fieldDescription-bytes')).toHaveTextContent(customDescription);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { EuiButtonIcon, EuiPopover, EuiPopoverProps, useEuiTheme } from '@elastic/eui';
|
||||
import { FieldDescription, FieldDescriptionProps } from '../field_description';
|
||||
|
||||
export type FieldDescriptionIconButtonProps = Pick<EuiPopoverProps, 'css'> & {
|
||||
field: FieldDescriptionProps['field'];
|
||||
};
|
||||
|
||||
export const FieldDescriptionIconButton: React.FC<FieldDescriptionIconButtonProps> = ({
|
||||
field,
|
||||
...otherProps
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
if (!field?.customDescription) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const buttonTitle = i18n.translate('fieldUtils.fieldDescriptionIconButtonTitle', {
|
||||
defaultMessage: 'View field description',
|
||||
});
|
||||
|
||||
return (
|
||||
<span>
|
||||
<EuiPopover
|
||||
{...otherProps}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
panelProps={{
|
||||
css: css`
|
||||
max-width: ${euiTheme.base * 20}px;
|
||||
`,
|
||||
}}
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
iconType="iInCircle"
|
||||
title={buttonTitle}
|
||||
aria-label={buttonTitle}
|
||||
size="xs"
|
||||
data-test-subj={`fieldDescriptionPopoverButton-${field.name}`}
|
||||
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FieldDescription field={field} truncate={false} />
|
||||
</EuiPopover>
|
||||
</span>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 {
|
||||
FieldDescriptionIconButton,
|
||||
type FieldDescriptionIconButtonProps,
|
||||
} from './field_description_icon_button';
|
|
@ -17,6 +17,7 @@ export interface FieldBase {
|
|||
name: DataViewField['name'];
|
||||
type?: DataViewField['type'];
|
||||
displayName?: DataViewField['displayName'];
|
||||
customDescription?: DataViewField['customDescription'];
|
||||
count?: DataViewField['count'];
|
||||
timeSeriesMetric?: DataViewField['timeSeriesMetric'];
|
||||
esTypes?: DataViewField['esTypes'];
|
||||
|
|
|
@ -33,7 +33,7 @@ pageLoadAssetSize:
|
|||
dataViewEditor: 28082
|
||||
dataViewFieldEditor: 27000
|
||||
dataViewManagement: 5176
|
||||
dataViews: 51000
|
||||
dataViews: 55000
|
||||
dataVisualizer: 27530
|
||||
devTools: 38637
|
||||
discover: 99999
|
||||
|
|
|
@ -1,5 +1,63 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FieldName renders a custom description icon 1`] = `
|
||||
Array [
|
||||
<div
|
||||
class="euiFlexItem kbnDocViewer__fieldIcon emotion-euiFlexItem-growZero"
|
||||
>
|
||||
<span
|
||||
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis1"
|
||||
>
|
||||
<span
|
||||
data-euiicon-type="tokenString"
|
||||
title="String"
|
||||
>
|
||||
String
|
||||
</span>
|
||||
</span>
|
||||
</div>,
|
||||
<div
|
||||
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
|
||||
>
|
||||
<span>
|
||||
test
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
||||
>
|
||||
<span>
|
||||
<div
|
||||
class="euiPopover emotion-euiPopover-inline-block"
|
||||
>
|
||||
<button
|
||||
aria-label="View field description"
|
||||
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
|
||||
data-test-subj="fieldDescriptionPopoverButton-bytes"
|
||||
title="View field description"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="iInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`FieldName renders a geo field 1`] = `
|
||||
Array [
|
||||
<div
|
||||
|
@ -17,7 +75,7 @@ Array [
|
|||
</span>
|
||||
</div>,
|
||||
<div
|
||||
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-flexStart-row"
|
||||
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
|
||||
|
@ -51,7 +109,7 @@ Array [
|
|||
</span>
|
||||
</div>,
|
||||
<div
|
||||
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-flexStart-row"
|
||||
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
|
||||
|
@ -85,7 +143,7 @@ Array [
|
|||
</span>
|
||||
</div>,
|
||||
<div
|
||||
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-flexStart-row"
|
||||
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
|
||||
|
@ -119,7 +177,7 @@ Array [
|
|||
</span>
|
||||
</div>,
|
||||
<div
|
||||
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-flexStart-row"
|
||||
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
|
||||
|
@ -153,7 +211,7 @@ Array [
|
|||
</span>
|
||||
</div>,
|
||||
<div
|
||||
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-flexStart-row"
|
||||
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
|
||||
|
@ -187,7 +245,7 @@ Array [
|
|||
</span>
|
||||
</div>,
|
||||
<div
|
||||
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-flexStart-row"
|
||||
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render } from 'enzyme';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { stubLogstashDataView as dataView } from '@kbn/data-plugin/common/stubs';
|
||||
import { FieldName } from './field_name';
|
||||
|
||||
|
@ -49,4 +50,20 @@ describe('FieldName', function () {
|
|||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders a custom description icon', () => {
|
||||
const component = render(
|
||||
<FieldName
|
||||
fieldType="string"
|
||||
fieldName="test"
|
||||
fieldMapping={
|
||||
{
|
||||
...dataView.getFieldByName('bytes')!.spec,
|
||||
customDescription: 'test description',
|
||||
} as DataViewField
|
||||
}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FieldIcon, FieldIconProps } from '@kbn/react-field';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { getDataViewFieldSubtypeMulti } from '@kbn/es-query';
|
||||
import { getFieldTypeName } from '@kbn/field-utils';
|
||||
import { FieldDescriptionIconButton, getFieldTypeName } from '@kbn/field-utils';
|
||||
|
||||
interface Props {
|
||||
fieldName: string;
|
||||
|
@ -46,7 +46,7 @@ export function FieldName({
|
|||
<FieldIcon type={fieldType!} label={typeName} scripted={scripted} {...fieldIconProps} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexGroup wrap={true} gutterSize="none" responsive={false} alignItems="flexStart">
|
||||
<EuiFlexGroup gutterSize="none" responsive={false} alignItems="flexStart" direction="row">
|
||||
<EuiFlexItem className="kbnDocViewer__fieldName eui-textBreakAll" grow={false}>
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
|
@ -58,6 +58,12 @@ export function FieldName({
|
|||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
|
||||
{fieldMapping?.customDescription ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldDescriptionIconButton field={fieldMapping} />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
|
||||
{isMultiField && (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
|
|
|
@ -15,8 +15,10 @@ import {
|
|||
EuiPopoverProps,
|
||||
EuiToolTip,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FieldDescription } from '@kbn/field-utils';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import type { AddFieldFilterHandler } from '../../types';
|
||||
|
||||
|
@ -75,80 +77,88 @@ export const FieldPopoverHeader: React.FC<FieldPopoverHeaderProps> = ({
|
|||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiTitle size="xxs">
|
||||
<h5 className="eui-textBreakWord">{field.displayName}</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{onAddFieldToWorkspace && (
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_addField">
|
||||
<EuiToolTip
|
||||
content={buttonAddFieldToWorkspaceProps?.['aria-label'] ?? addFieldToWorkspaceTooltip}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`fieldPopoverHeader_addField-${field.name}`}
|
||||
aria-label={addFieldToWorkspaceTooltip}
|
||||
{...(buttonAddFieldToWorkspaceProps || {})}
|
||||
iconType="plusInCircle"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
onAddFieldToWorkspace(field);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
<>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiTitle size="xxs">
|
||||
<h5 className="eui-textBreakWord">{field.displayName}</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{onAddFilter && field.filterable && !field.scripted && (
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_addExistsFilter">
|
||||
<EuiToolTip content={buttonAddFilterProps?.['aria-label'] ?? addExistsFilterTooltip}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`fieldPopoverHeader_addExistsFilter-${field.name}`}
|
||||
aria-label={addExistsFilterTooltip}
|
||||
{...(buttonAddFilterProps || {})}
|
||||
iconType="filter"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
onAddFilter('_exists_', field.name, '+');
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{onEditField &&
|
||||
(field.isRuntimeField || !['unknown', 'unknown_selected'].includes(field.type)) && (
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_editField">
|
||||
<EuiToolTip content={buttonEditFieldProps?.['aria-label'] ?? editFieldTooltip}>
|
||||
{onAddFieldToWorkspace && (
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_addField">
|
||||
<EuiToolTip
|
||||
content={buttonAddFieldToWorkspaceProps?.['aria-label'] ?? addFieldToWorkspaceTooltip}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`fieldPopoverHeader_editField-${field.name}`}
|
||||
aria-label={editFieldTooltip}
|
||||
{...(buttonEditFieldProps || {})}
|
||||
iconType="pencil"
|
||||
data-test-subj={`fieldPopoverHeader_addField-${field.name}`}
|
||||
aria-label={addFieldToWorkspaceTooltip}
|
||||
{...(buttonAddFieldToWorkspaceProps || {})}
|
||||
iconType="plusInCircle"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
onEditField(field.name);
|
||||
onAddFieldToWorkspace(field);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{onDeleteField && field.isRuntimeField && (
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_deleteField">
|
||||
<EuiToolTip content={buttonDeleteFieldProps?.['aria-label'] ?? deleteFieldTooltip}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`fieldPopoverHeader_deleteField-${field.name}`}
|
||||
aria-label={deleteFieldTooltip}
|
||||
{...(buttonDeleteFieldProps || {})}
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
onDeleteField(field.name);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
{onAddFilter && field.filterable && !field.scripted && (
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_addExistsFilter">
|
||||
<EuiToolTip content={buttonAddFilterProps?.['aria-label'] ?? addExistsFilterTooltip}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`fieldPopoverHeader_addExistsFilter-${field.name}`}
|
||||
aria-label={addExistsFilterTooltip}
|
||||
{...(buttonAddFilterProps || {})}
|
||||
iconType="filter"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
onAddFilter('_exists_', field.name, '+');
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{onEditField &&
|
||||
(field.isRuntimeField || !['unknown', 'unknown_selected'].includes(field.type)) && (
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_editField">
|
||||
<EuiToolTip content={buttonEditFieldProps?.['aria-label'] ?? editFieldTooltip}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`fieldPopoverHeader_editField-${field.name}`}
|
||||
aria-label={editFieldTooltip}
|
||||
{...(buttonEditFieldProps || {})}
|
||||
iconType="pencil"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
onEditField(field.name);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{onDeleteField && field.isRuntimeField && (
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_deleteField">
|
||||
<EuiToolTip content={buttonDeleteFieldProps?.['aria-label'] ?? deleteFieldTooltip}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`fieldPopoverHeader_deleteField-${field.name}`}
|
||||
aria-label={deleteFieldTooltip}
|
||||
{...(buttonDeleteFieldProps || {})}
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
onDeleteField(field.name);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
{field.customDescription ? (
|
||||
<>
|
||||
<EuiSpacer size="xs" />
|
||||
<FieldDescription field={field} />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -81,7 +81,7 @@ describe('<FieldEditor />', () => {
|
|||
test('initial state should have "set custom label", "set value" and "set format" turned off', async () => {
|
||||
testBed = await setup();
|
||||
|
||||
['customLabel', 'value', 'format'].forEach((row) => {
|
||||
['customLabel', 'customDescription', 'value', 'format'].forEach((row) => {
|
||||
const testSubj = `${row}Row.toggle`;
|
||||
const toggle = testBed.find(testSubj);
|
||||
const isOn = toggle.props()['aria-checked'];
|
||||
|
|
|
@ -40,7 +40,7 @@ export const waitForUpdates = async (testBed?: TestBed) => {
|
|||
|
||||
export const getCommonActions = (testBed: TestBed) => {
|
||||
const toggleFormRow = async (
|
||||
row: 'customLabel' | 'value' | 'format',
|
||||
row: 'customLabel' | 'customDescription' | 'value' | 'format',
|
||||
value: 'on' | 'off' = 'on'
|
||||
) => {
|
||||
const testSubj = `${row}Row.toggle`;
|
||||
|
|
|
@ -12,7 +12,13 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { EuiCode } from '@elastic/eui';
|
||||
import { AdvancedParametersSection } from './advanced_parameters_section';
|
||||
import { FormRow } from './form_row';
|
||||
import { PopularityField, FormatField, ScriptField, CustomLabelField } from './form_fields';
|
||||
import {
|
||||
PopularityField,
|
||||
FormatField,
|
||||
ScriptField,
|
||||
CustomLabelField,
|
||||
CustomDescriptionField,
|
||||
} from './form_fields';
|
||||
import { useFieldEditorContext } from '../field_editor_context';
|
||||
|
||||
const geti18nTexts = (): {
|
||||
|
@ -26,6 +32,18 @@ const geti18nTexts = (): {
|
|||
defaultMessage: `Create a label to display in place of the field name in Discover, Maps, Lens, Visualize, and TSVB. Useful for shortening a long field name. Queries and filters use the original field name.`,
|
||||
}),
|
||||
},
|
||||
customDescription: {
|
||||
title: i18n.translate('indexPatternFieldEditor.editor.form.customDescriptionTitle', {
|
||||
defaultMessage: 'Set custom description',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.customDescriptionDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
"Add a description to the field. It's displayed next to the field on the Discover, Lens, and Data View Management pages.",
|
||||
}
|
||||
),
|
||||
},
|
||||
value: {
|
||||
title: i18n.translate('indexPatternFieldEditor.editor.form.valueTitle', {
|
||||
defaultMessage: 'Set value',
|
||||
|
@ -76,6 +94,17 @@ export const FieldDetail = ({}) => {
|
|||
<CustomLabelField />
|
||||
</FormRow>
|
||||
|
||||
{/* Set custom description */}
|
||||
<FormRow
|
||||
title={i18nTexts.customDescription.title}
|
||||
description={i18nTexts.customDescription.description}
|
||||
formFieldPath="__meta__.isCustomDescriptionVisible"
|
||||
data-test-subj="customDescriptionRow"
|
||||
withDividerRule
|
||||
>
|
||||
<CustomDescriptionField />
|
||||
</FormRow>
|
||||
|
||||
{/* Set value */}
|
||||
{fieldTypeToProcess === 'runtime' && (
|
||||
<FormRow
|
||||
|
@ -102,6 +131,7 @@ export const FieldDetail = ({}) => {
|
|||
|
||||
{/* Advanced settings */}
|
||||
<AdvancedParametersSection>
|
||||
{/* Popularity score (higher value means it will be positioned higher in the fields list) */}
|
||||
<FormRow
|
||||
title={i18nTexts.popularity.title}
|
||||
description={i18nTexts.popularity.description}
|
||||
|
|
|
@ -47,6 +47,7 @@ export interface FieldFormInternal extends Omit<Field, 'type' | 'internalType' |
|
|||
type: TypeSelection;
|
||||
__meta__: {
|
||||
isCustomLabelVisible: boolean;
|
||||
isCustomDescriptionVisible: boolean;
|
||||
isValueVisible: boolean;
|
||||
isFormatVisible: boolean;
|
||||
isPopularityVisible: boolean;
|
||||
|
@ -86,6 +87,7 @@ const formDeserializer = (field: Field): FieldFormInternal => {
|
|||
format,
|
||||
__meta__: {
|
||||
isCustomLabelVisible: field.customLabel !== undefined,
|
||||
isCustomDescriptionVisible: field.customDescription !== undefined,
|
||||
isValueVisible: field.script !== undefined,
|
||||
isFormatVisible: field.format !== undefined,
|
||||
isPopularityVisible: field.popularity !== undefined,
|
||||
|
@ -126,6 +128,7 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props)
|
|||
form,
|
||||
discard: [
|
||||
'__meta__.isCustomLabelVisible',
|
||||
'__meta__.isCustomDescriptionVisible',
|
||||
'__meta__.isValueVisible',
|
||||
'__meta__.isFormatVisible',
|
||||
'__meta__.isPopularityVisible',
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { UseField, TextAreaField } from '../../../shared_imports';
|
||||
|
||||
export const CustomDescriptionField = () => {
|
||||
return <UseField path="customDescription" component={TextAreaField} />;
|
||||
};
|
|
@ -10,6 +10,8 @@ export { TypeField } from './type_field';
|
|||
|
||||
export { CustomLabelField } from './custom_label_field';
|
||||
|
||||
export { CustomDescriptionField } from './custom_description_field';
|
||||
|
||||
export { PopularityField } from './popularity_field';
|
||||
|
||||
export { ScriptField } from './script_field';
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH } from '@kbn/data-views-plugin/common';
|
||||
import { fieldValidators, FieldConfig, RuntimeType, ValidationFunc } from '../../shared_imports';
|
||||
import { RUNTIME_FIELD_OPTIONS } from './constants';
|
||||
import type { PreviewState } from '../preview/types';
|
||||
|
||||
const { containsCharsField, emptyField, numberGreaterThanField } = fieldValidators;
|
||||
const { containsCharsField, emptyField, numberGreaterThanField, maxLengthField } = fieldValidators;
|
||||
const i18nTexts = {
|
||||
invalidScriptErrorMessage: i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.scriptEditorPainlessValidationMessage',
|
||||
|
@ -110,6 +110,36 @@ export const schema = {
|
|||
},
|
||||
],
|
||||
},
|
||||
customDescription: {
|
||||
label: i18n.translate('indexPatternFieldEditor.editor.form.customDescriptionLabel', {
|
||||
defaultMessage: 'Custom description',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.validations.customDescriptionIsRequiredErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Give a description to the field.',
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
validator: maxLengthField({
|
||||
length: MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH,
|
||||
message: i18n.translate(
|
||||
'indexPatternFieldEditor.editor.form.validations.customDescriptionMaxLengthErrorMessage',
|
||||
{
|
||||
values: { length: MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH },
|
||||
defaultMessage:
|
||||
'The length of the description is too long. The maximum length is {length} characters.',
|
||||
}
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
popularity: {
|
||||
label: i18n.translate('indexPatternFieldEditor.editor.form.popularityLabel', {
|
||||
defaultMessage: 'Popularity',
|
||||
|
@ -146,6 +176,9 @@ export const schema = {
|
|||
isCustomLabelVisible: {
|
||||
defaultValue: false,
|
||||
},
|
||||
isCustomDescriptionVisible: {
|
||||
defaultValue: false,
|
||||
},
|
||||
isValueVisible: {
|
||||
defaultValue: false,
|
||||
},
|
||||
|
|
|
@ -178,6 +178,7 @@ export const FieldEditorFlyoutContentContainer = ({
|
|||
|
||||
// Update custom label, popularity and format
|
||||
dataView.setFieldCustomLabel(updatedField.name, updatedField.customLabel);
|
||||
dataView.setFieldCustomDescription(updatedField.name, updatedField.customDescription);
|
||||
|
||||
editedField.count = updatedField.popularity || 0;
|
||||
if (updatedField.format) {
|
||||
|
|
|
@ -22,6 +22,7 @@ export const deserializeField = (dataView: DataView, field?: DataViewField): Fie
|
|||
type: editType,
|
||||
script: field.runtimeField ? field.runtimeField.script : undefined,
|
||||
customLabel: field.customLabel,
|
||||
customDescription: field.customDescription,
|
||||
popularity: field.count,
|
||||
format: dataView.getFormatterForFieldNoDefault(field.name)?.toJSON(),
|
||||
fields: field.runtimeField?.fields,
|
||||
|
|
|
@ -121,6 +121,7 @@ export const getFieldEditorOpener =
|
|||
esTypes: [],
|
||||
type: undefined,
|
||||
customLabel: undefined,
|
||||
customDescription: undefined,
|
||||
count: undefined,
|
||||
spec: {
|
||||
parentName: undefined,
|
||||
|
@ -159,6 +160,7 @@ export const getFieldEditorOpener =
|
|||
field = {
|
||||
name: fieldNameToEdit!,
|
||||
customLabel: dataViewField.customLabel,
|
||||
customDescription: dataViewField.customDescription,
|
||||
popularity: dataViewField.count,
|
||||
format: dataView.getFormatterForFieldNoDefault(fieldNameToEdit!)?.toJSON(),
|
||||
...dataView.getRuntimeField(fieldNameToEdit!)!,
|
||||
|
@ -169,6 +171,7 @@ export const getFieldEditorOpener =
|
|||
name: fieldNameToEdit!,
|
||||
type: (dataViewField?.esTypes ? dataViewField.esTypes[0] : 'keyword') as RuntimeType,
|
||||
customLabel: dataViewField.customLabel,
|
||||
customDescription: dataViewField.customDescription,
|
||||
popularity: dataViewField.count,
|
||||
format: dataView.getFormatterForFieldNoDefault(fieldNameToEdit!)?.toJSON(),
|
||||
parentName: dataViewField.spec.parentName,
|
||||
|
|
|
@ -56,6 +56,7 @@ export { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
|||
|
||||
export {
|
||||
TextField,
|
||||
TextAreaField,
|
||||
ToggleField,
|
||||
NumericField,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import React, { PureComponent } from 'react';
|
||||
import { OverlayModalStart, ThemeServiceStart } from '@kbn/core/public';
|
||||
|
||||
import { FieldDescription } from '@kbn/field-utils';
|
||||
import {
|
||||
EuiIcon,
|
||||
EuiInMemoryTable,
|
||||
|
@ -24,6 +24,7 @@ import {
|
|||
EuiText,
|
||||
EuiBasicTable,
|
||||
EuiCode,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -265,6 +266,12 @@ export const renderFieldName = (field: IndexedFieldItem, timeFieldName?: string)
|
|||
</EuiToolTip>
|
||||
</div>
|
||||
) : null}
|
||||
{field.customDescription ? (
|
||||
<div>
|
||||
<EuiSpacer size="xs" />
|
||||
<FieldDescription field={field} color="subdued" />
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
"@kbn/shared-ux-router",
|
||||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/field-utils",
|
||||
"@kbn/no-data-page-plugin",
|
||||
"@kbn/core-application-browser",
|
||||
"@kbn/core-doc-links-browser",
|
||||
|
|
|
@ -52,6 +52,11 @@ export const DATA_VIEW_SAVED_OBJECT_TYPE = 'index-pattern';
|
|||
*/
|
||||
export const PLUGIN_NAME = 'DataViews';
|
||||
|
||||
/**
|
||||
* Max length for the custom field description
|
||||
*/
|
||||
export const MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH = 300;
|
||||
|
||||
/**
|
||||
* Fields for wildcard path.
|
||||
* @public
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
searchOptionsSchemas,
|
||||
} from '@kbn/content-management-utils';
|
||||
import { DataViewType } from '../..';
|
||||
import { MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH } from '../../constants';
|
||||
import { serializedFieldFormatSchema, fieldSpecSchema } from '../../schemas';
|
||||
|
||||
const dataViewAttributesSchema = schema.object(
|
||||
|
@ -40,6 +41,11 @@ const dataViewAttributesSchema = schema.object(
|
|||
schema.string(),
|
||||
schema.object({
|
||||
customLabel: schema.maybe(schema.string()),
|
||||
customDescription: schema.maybe(
|
||||
schema.string({
|
||||
maxLength: MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH,
|
||||
})
|
||||
),
|
||||
count: schema.maybe(schema.number()),
|
||||
})
|
||||
)
|
||||
|
|
|
@ -6,6 +6,7 @@ FldList [
|
|||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 5,
|
||||
"customDescription": undefined,
|
||||
"customLabel": "A Runtime Field",
|
||||
"defaultFormatter": undefined,
|
||||
"esTypes": Array [
|
||||
|
|
|
@ -302,6 +302,23 @@ export abstract class AbstractDataView {
|
|||
this.setFieldAttrs(fieldName, 'customLabel', customLabel === null ? undefined : customLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set field custom description
|
||||
* @param fieldName name of field to set custom description on
|
||||
* @param customDescription custom description value. If undefined, custom description is removed
|
||||
*/
|
||||
|
||||
protected setFieldCustomDescriptionInternal(
|
||||
fieldName: string,
|
||||
customDescription: string | undefined | null
|
||||
) {
|
||||
this.setFieldAttrs(
|
||||
fieldName,
|
||||
'customDescription',
|
||||
customDescription === null ? undefined : customDescription
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set field formatter
|
||||
* @param fieldName name of field to set format on
|
||||
|
|
|
@ -278,7 +278,7 @@ export class DataView extends AbstractDataView implements DataViewBase {
|
|||
throw new CharacterNotAllowedInField('*', name);
|
||||
}
|
||||
|
||||
const { type, script, customLabel, format, popularity } = runtimeField;
|
||||
const { type, script, customLabel, customDescription, format, popularity } = runtimeField;
|
||||
|
||||
if (type === 'composite') {
|
||||
return this.addCompositeRuntimeField(name, runtimeField);
|
||||
|
@ -291,6 +291,7 @@ export class DataView extends AbstractDataView implements DataViewBase {
|
|||
{ type, script },
|
||||
{
|
||||
customLabel,
|
||||
customDescription,
|
||||
format,
|
||||
popularity,
|
||||
}
|
||||
|
@ -404,6 +405,27 @@ export class DataView extends AbstractDataView implements DataViewBase {
|
|||
this.setFieldCustomLabelInternal(fieldName, customLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set field custom description
|
||||
* @param fieldName name of field to set custom label on
|
||||
* @param customDescription custom description value. If undefined, custom description is removed
|
||||
*/
|
||||
|
||||
public setFieldCustomDescription(
|
||||
fieldName: string,
|
||||
customDescription: string | undefined | null
|
||||
) {
|
||||
const fieldObject = this.fields.getByName(fieldName);
|
||||
const newCustomDescription: string | undefined =
|
||||
customDescription === null ? undefined : customDescription;
|
||||
|
||||
if (fieldObject) {
|
||||
fieldObject.customDescription = newCustomDescription;
|
||||
}
|
||||
|
||||
this.setFieldCustomDescriptionInternal(fieldName, customDescription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set field count
|
||||
* @param fieldName name of field to set count on
|
||||
|
@ -465,6 +487,7 @@ export class DataView extends AbstractDataView implements DataViewBase {
|
|||
// Every child field gets the complete runtime field script for consumption by searchSource
|
||||
this.updateOrAddRuntimeField(`${name}.${subFieldName}`, subField.type, runtimeFieldSpec, {
|
||||
customLabel: subField.customLabel,
|
||||
customDescription: subField.customDescription,
|
||||
format: subField.format,
|
||||
popularity: subField.popularity,
|
||||
})
|
||||
|
@ -507,6 +530,7 @@ export class DataView extends AbstractDataView implements DataViewBase {
|
|||
|
||||
// Apply configuration to the field
|
||||
this.setFieldCustomLabel(fieldName, config.customLabel);
|
||||
this.setFieldCustomDescription(fieldName, config.customDescription);
|
||||
|
||||
if (config.popularity || config.popularity === null) {
|
||||
this.setFieldCount(fieldName, config.popularity);
|
||||
|
|
|
@ -714,6 +714,7 @@ export class DataViewsService {
|
|||
collector[field.name] = {
|
||||
...field,
|
||||
customLabel: fieldAttrs?.[field.name]?.customLabel,
|
||||
customDescription: fieldAttrs?.[field.name]?.customDescription,
|
||||
count: fieldAttrs?.[field.name]?.count,
|
||||
};
|
||||
return collector;
|
||||
|
@ -897,6 +898,7 @@ export class DataViewsService {
|
|||
searchable: true,
|
||||
readFromDocValues: false,
|
||||
customLabel: fieldAttrs?.[name]?.customLabel,
|
||||
customDescription: fieldAttrs?.[name]?.customDescription,
|
||||
count: fieldAttrs?.[name]?.count,
|
||||
};
|
||||
|
||||
|
|
|
@ -135,6 +135,22 @@ export class DataViewField implements DataViewFieldBase {
|
|||
this.spec.customLabel = customLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns custom description if set, otherwise undefined.
|
||||
*/
|
||||
|
||||
public get customDescription() {
|
||||
return this.spec.customDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets custom description for field, or unsets if passed undefined.
|
||||
* @param customDescription custom label value
|
||||
*/
|
||||
public set customDescription(customDescription) {
|
||||
this.spec.customDescription = customDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of field type conflicts across different indices in the same index pattern.
|
||||
*/
|
||||
|
@ -374,6 +390,7 @@ export class DataViewField implements DataViewFieldBase {
|
|||
readFromDocValues: this.readFromDocValues,
|
||||
subType: this.subType,
|
||||
customLabel: this.customLabel,
|
||||
customDescription: this.customDescription,
|
||||
defaultFormatter: this.defaultFormatter,
|
||||
};
|
||||
}
|
||||
|
@ -401,6 +418,7 @@ export class DataViewField implements DataViewFieldBase {
|
|||
subType: this.subType,
|
||||
format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined,
|
||||
customLabel: this.customLabel,
|
||||
customDescription: this.customDescription,
|
||||
shortDotsEnable: this.spec.shortDotsEnable,
|
||||
runtimeField: this.runtimeField,
|
||||
isMapped: this.isMapped,
|
||||
|
|
|
@ -11,6 +11,7 @@ export {
|
|||
DEFAULT_ASSETS_TO_IGNORE,
|
||||
META_FIELDS,
|
||||
DATA_VIEW_SAVED_OBJECT_TYPE,
|
||||
MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH,
|
||||
} from './constants';
|
||||
|
||||
export { LATEST_VERSION } from './content_management/v1/constants';
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import { schema, Type } from '@kbn/config-schema';
|
||||
import { MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH } from './constants';
|
||||
import { RuntimeType } from '.';
|
||||
|
||||
export const serializedFieldFormatSchema = schema.object({
|
||||
|
@ -38,6 +39,11 @@ const primitiveRuntimeFieldSchemaShared = {
|
|||
),
|
||||
format: schema.maybe(serializedFieldFormatSchema),
|
||||
customLabel: schema.maybe(schema.string()),
|
||||
customDescription: schema.maybe(
|
||||
schema.string({
|
||||
maxLength: MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH,
|
||||
})
|
||||
),
|
||||
popularity: schema.maybe(
|
||||
schema.number({
|
||||
min: 0,
|
||||
|
@ -68,6 +74,11 @@ const compositeRuntimeFieldSchemaShared = {
|
|||
type: runtimeFieldNonCompositeFieldsSpecTypeSchema,
|
||||
format: schema.maybe(serializedFieldFormatSchema),
|
||||
customLabel: schema.maybe(schema.string()),
|
||||
customDescription: schema.maybe(
|
||||
schema.string({
|
||||
maxLength: MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH,
|
||||
})
|
||||
),
|
||||
popularity: schema.maybe(
|
||||
schema.number({
|
||||
min: 0,
|
||||
|
@ -134,6 +145,11 @@ export const fieldSpecSchemaFields = {
|
|||
})
|
||||
),
|
||||
customLabel: schema.maybe(schema.string()),
|
||||
customDescription: schema.maybe(
|
||||
schema.string({
|
||||
maxLength: MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH,
|
||||
})
|
||||
),
|
||||
shortDotsEnable: schema.maybe(schema.boolean()),
|
||||
searchable: schema.maybe(schema.boolean()),
|
||||
aggregatable: schema.maybe(schema.boolean()),
|
||||
|
|
|
@ -79,6 +79,10 @@ export interface FieldConfiguration {
|
|||
* Custom label
|
||||
*/
|
||||
customLabel?: string;
|
||||
/**
|
||||
* Custom description
|
||||
*/
|
||||
customDescription?: string;
|
||||
/**
|
||||
* Popularity - used for discover
|
||||
*/
|
||||
|
@ -183,6 +187,10 @@ export type FieldAttrSet = {
|
|||
* Custom field label
|
||||
*/
|
||||
customLabel?: string;
|
||||
/**
|
||||
* Custom field description
|
||||
*/
|
||||
customDescription?: string;
|
||||
/**
|
||||
* Popularity count - used for discover
|
||||
*/
|
||||
|
@ -425,6 +433,10 @@ export type FieldSpec = DataViewFieldBase & {
|
|||
* Custom label for field, used for display in kibana
|
||||
*/
|
||||
customLabel?: string;
|
||||
/**
|
||||
* Custom description for field, used for display in kibana
|
||||
*/
|
||||
customDescription?: string;
|
||||
/**
|
||||
* Runtime field definition
|
||||
*/
|
||||
|
|
|
@ -13,6 +13,7 @@ import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
|
|||
import { DataViewsService } from '../../../../common';
|
||||
import { handleErrors } from '../util/handle_errors';
|
||||
import { serializedFieldFormatSchema } from '../../../../common/schemas';
|
||||
import { MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH } from '../../../../common/constants';
|
||||
import { dataViewSpecSchema } from '../../schema';
|
||||
import { DataViewSpecRestResponse } from '../../route_types';
|
||||
import type {
|
||||
|
@ -60,6 +61,11 @@ export const updateFields = async ({
|
|||
dataView.setFieldCustomLabel(fieldName, field.customLabel);
|
||||
}
|
||||
|
||||
if (field.customDescription !== undefined) {
|
||||
changeCount++;
|
||||
dataView.setFieldCustomDescription(fieldName, field.customDescription);
|
||||
}
|
||||
|
||||
if (field.count !== undefined) {
|
||||
changeCount++;
|
||||
dataView.setFieldCount(fieldName, field.count);
|
||||
|
@ -85,6 +91,7 @@ export const updateFields = async ({
|
|||
|
||||
interface FieldUpdateType {
|
||||
customLabel?: string | null;
|
||||
customDescription?: string | null;
|
||||
count?: number | null;
|
||||
format?: SerializedFieldFormat | null;
|
||||
}
|
||||
|
@ -98,6 +105,14 @@ const fieldUpdateSchema = schema.object({
|
|||
})
|
||||
)
|
||||
),
|
||||
customDescription: schema.maybe(
|
||||
schema.nullable(
|
||||
schema.string({
|
||||
minLength: 1,
|
||||
maxLength: MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH,
|
||||
})
|
||||
)
|
||||
),
|
||||
count: schema.maybe(schema.nullable(schema.number())),
|
||||
format: schema.maybe(schema.nullable(serializedFieldFormatSchema)),
|
||||
});
|
||||
|
|
|
@ -41,6 +41,7 @@ export type TypeMetaRestResponse = {
|
|||
|
||||
export type FieldAttrSetRestResponse = {
|
||||
customLabel?: string;
|
||||
customDescription?: string;
|
||||
count?: number;
|
||||
};
|
||||
|
||||
|
@ -94,6 +95,7 @@ export type FieldSpecRestResponse = DataViewFieldBaseRestResponse & {
|
|||
readFromDocValues?: boolean;
|
||||
indexed?: boolean;
|
||||
customLabel?: string;
|
||||
customDescription?: string;
|
||||
runtimeField?: RuntimeFieldSpecRestResponse;
|
||||
fixedInterval?: string[];
|
||||
timeZone?: string[];
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
serializedFieldFormatSchema,
|
||||
fieldSpecSchemaFields,
|
||||
} from '../../common/schemas';
|
||||
import { MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH } from '../../common/constants';
|
||||
|
||||
export const dataViewSpecSchema = schema.object({
|
||||
title: schema.string(),
|
||||
|
@ -38,6 +39,11 @@ export const dataViewSpecSchema = schema.object({
|
|||
schema.string(),
|
||||
schema.object({
|
||||
customLabel: schema.maybe(schema.string()),
|
||||
customDescription: schema.maybe(
|
||||
schema.string({
|
||||
maxLength: MAX_DATA_VIEW_FIELD_DESCRIPTION_LENGTH,
|
||||
})
|
||||
),
|
||||
count: schema.maybe(schema.number()),
|
||||
})
|
||||
)
|
||||
|
|
|
@ -69,6 +69,29 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(response2.body.statusCode).to.be(400);
|
||||
expect(response2.body.message).to.be('Change set is empty.');
|
||||
});
|
||||
|
||||
it('returns validation error', async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}*`;
|
||||
const response1 = await supertest.post(config.path).send({
|
||||
[config.serviceKey]: {
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
const response2 = await supertest
|
||||
.post(`${config.path}/${response1.body[config.serviceKey].id}/fields`)
|
||||
.send({
|
||||
fields: {
|
||||
foo: {
|
||||
customDescription: 'too long value'.repeat(50),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response2.status).to.be(400);
|
||||
expect(response2.body.statusCode).to.be(400);
|
||||
expect(response2.body.message).to.contain('it must have a maximum length');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -67,6 +67,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
bar: {
|
||||
count: 456,
|
||||
customDescription: 'desc',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -75,6 +76,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(response2.body[config.serviceKey].fieldAttrs.foo.count).to.be(123);
|
||||
expect(response2.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('test');
|
||||
expect(response2.body[config.serviceKey].fieldAttrs.bar.count).to.be(456);
|
||||
expect(response2.body[config.serviceKey].fieldAttrs.bar.customDescription).to.be('desc');
|
||||
|
||||
const response3 = await supertest.get(
|
||||
`${config.path}/${response1.body[config.serviceKey].id}`
|
||||
|
@ -84,6 +86,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(response3.body[config.serviceKey].fieldAttrs.foo.count).to.be(123);
|
||||
expect(response3.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('test');
|
||||
expect(response3.body[config.serviceKey].fieldAttrs.bar.count).to.be(456);
|
||||
expect(response3.body[config.serviceKey].fieldAttrs.bar.customDescription).to.be('desc');
|
||||
});
|
||||
|
||||
describe('count', () => {
|
||||
|
@ -194,126 +197,146 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('customLabel', () => {
|
||||
it('can set field "customLabel" attribute on non-existing field', async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}*`;
|
||||
const response1 = await supertest.post(config.path).send({
|
||||
[config.serviceKey]: {
|
||||
title,
|
||||
},
|
||||
['customLabel', 'customDescription'].forEach((customStringAttribute) => {
|
||||
describe(`can set optional ${customStringAttribute}`, () => {
|
||||
it(`can set field "${customStringAttribute}" attribute on non-existing field`, async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}*`;
|
||||
const response1 = await supertest.post(config.path).send({
|
||||
[config.serviceKey]: {
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response1.status).to.be(200);
|
||||
expect(response1.body[config.serviceKey].fieldAttrs.foo).to.be(undefined);
|
||||
|
||||
const response2 = await supertest
|
||||
.post(`${config.path}/${response1.body[config.serviceKey].id}/fields`)
|
||||
.send({
|
||||
fields: {
|
||||
foo: {
|
||||
[customStringAttribute]: 'foo',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response2.status).to.be(200);
|
||||
expect(response2.body[config.serviceKey].fieldAttrs.foo[customStringAttribute]).to.be(
|
||||
'foo'
|
||||
);
|
||||
|
||||
const response3 = await supertest.get(
|
||||
`${config.path}/${response1.body[config.serviceKey].id}`
|
||||
);
|
||||
|
||||
expect(response3.status).to.be(200);
|
||||
expect(response3.body[config.serviceKey].fieldAttrs.foo[customStringAttribute]).to.be(
|
||||
'foo'
|
||||
);
|
||||
});
|
||||
|
||||
expect(response1.status).to.be(200);
|
||||
expect(response1.body[config.serviceKey].fieldAttrs.foo).to.be(undefined);
|
||||
|
||||
const response2 = await supertest
|
||||
.post(`${config.path}/${response1.body[config.serviceKey].id}/fields`)
|
||||
.send({
|
||||
fields: {
|
||||
foo: {
|
||||
customLabel: 'foo',
|
||||
it(`can update "${customStringAttribute}" attribute in index_pattern attribute map`, async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}*`;
|
||||
const response1 = await supertest.post(config.path).send({
|
||||
[config.serviceKey]: {
|
||||
title,
|
||||
fieldAttrs: {
|
||||
foo: {
|
||||
[customStringAttribute]: 'foo',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response2.status).to.be(200);
|
||||
expect(response2.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('foo');
|
||||
expect(response1.status).to.be(200);
|
||||
expect(response1.body[config.serviceKey].fieldAttrs.foo[customStringAttribute]).to.be(
|
||||
'foo'
|
||||
);
|
||||
|
||||
const response3 = await supertest.get(
|
||||
`${config.path}/${response1.body[config.serviceKey].id}`
|
||||
);
|
||||
|
||||
expect(response3.status).to.be(200);
|
||||
expect(response3.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('foo');
|
||||
});
|
||||
|
||||
it('can update "customLabel" attribute in index_pattern attribute map', async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}*`;
|
||||
const response1 = await supertest.post(config.path).send({
|
||||
[config.serviceKey]: {
|
||||
title,
|
||||
fieldAttrs: {
|
||||
foo: {
|
||||
customLabel: 'foo',
|
||||
const response2 = await supertest
|
||||
.post(`${config.path}/${response1.body[config.serviceKey].id}/fields`)
|
||||
.send({
|
||||
fields: {
|
||||
foo: {
|
||||
[customStringAttribute]: 'bar',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response2.status).to.be(200);
|
||||
expect(response2.body[config.serviceKey].fieldAttrs.foo[customStringAttribute]).to.be(
|
||||
'bar'
|
||||
);
|
||||
|
||||
const response3 = await supertest.get(
|
||||
`${config.path}/${response1.body[config.serviceKey].id}`
|
||||
);
|
||||
|
||||
expect(response3.status).to.be(200);
|
||||
expect(response3.body[config.serviceKey].fieldAttrs.foo[customStringAttribute]).to.be(
|
||||
'bar'
|
||||
);
|
||||
});
|
||||
|
||||
expect(response1.status).to.be(200);
|
||||
expect(response1.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('foo');
|
||||
|
||||
const response2 = await supertest
|
||||
.post(`${config.path}/${response1.body[config.serviceKey].id}/fields`)
|
||||
.send({
|
||||
fields: {
|
||||
foo: {
|
||||
customLabel: 'bar',
|
||||
it(`can delete "${customStringAttribute}" attribute from index_pattern attribute map`, async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}*`;
|
||||
const response1 = await supertest.post(config.path).send({
|
||||
[config.serviceKey]: {
|
||||
title,
|
||||
fieldAttrs: {
|
||||
foo: {
|
||||
[customStringAttribute]: 'foo',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response2.status).to.be(200);
|
||||
expect(response2.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('bar');
|
||||
expect(response1.status).to.be(200);
|
||||
expect(response1.body[config.serviceKey].fieldAttrs.foo[customStringAttribute]).to.be(
|
||||
'foo'
|
||||
);
|
||||
|
||||
const response3 = await supertest.get(
|
||||
`${config.path}/${response1.body[config.serviceKey].id}`
|
||||
);
|
||||
|
||||
expect(response3.status).to.be(200);
|
||||
expect(response3.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('bar');
|
||||
});
|
||||
|
||||
it('can delete "customLabel" attribute from index_pattern attribute map', async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}*`;
|
||||
const response1 = await supertest.post(config.path).send({
|
||||
[config.serviceKey]: {
|
||||
title,
|
||||
fieldAttrs: {
|
||||
foo: {
|
||||
customLabel: 'foo',
|
||||
const response2 = await supertest
|
||||
.post(`${config.path}/${response1.body[config.serviceKey].id}/fields`)
|
||||
.send({
|
||||
fields: {
|
||||
foo: {
|
||||
[customStringAttribute]: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response2.status).to.be(200);
|
||||
expect(response2.body[config.serviceKey].fieldAttrs.foo[customStringAttribute]).to.be(
|
||||
undefined
|
||||
);
|
||||
|
||||
const response3 = await supertest.get(
|
||||
`${config.path}/${response1.body[config.serviceKey].id}`
|
||||
);
|
||||
|
||||
expect(response3.status).to.be(200);
|
||||
expect(response3.body[config.serviceKey].fieldAttrs.foo[customStringAttribute]).to.be(
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
expect(response1.status).to.be(200);
|
||||
expect(response1.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('foo');
|
||||
|
||||
const response2 = await supertest
|
||||
.post(`${config.path}/${response1.body[config.serviceKey].id}/fields`)
|
||||
.send({
|
||||
it(`can set field "${customStringAttribute}" attribute on an existing field`, async () => {
|
||||
await supertest.post(`${config.path}/${indexPattern.id}/fields`).send({
|
||||
fields: {
|
||||
foo: {
|
||||
customLabel: null,
|
||||
[customStringAttribute]: 'baz',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response2.status).to.be(200);
|
||||
expect(response2.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be(undefined);
|
||||
const response1 = await supertest.get(`${config.path}/${indexPattern.id}`);
|
||||
|
||||
const response3 = await supertest.get(
|
||||
`${config.path}/${response1.body[config.serviceKey].id}`
|
||||
);
|
||||
|
||||
expect(response3.status).to.be(200);
|
||||
expect(response3.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be(undefined);
|
||||
});
|
||||
|
||||
it('can set field "customLabel" attribute on an existing field', async () => {
|
||||
await supertest.post(`${config.path}/${indexPattern.id}/fields`).send({
|
||||
fields: {
|
||||
foo: {
|
||||
customLabel: 'baz',
|
||||
},
|
||||
},
|
||||
expect(response1.status).to.be(200);
|
||||
expect(response1.body[config.serviceKey].fields.foo[customStringAttribute]).to.be(
|
||||
'baz'
|
||||
);
|
||||
});
|
||||
|
||||
const response1 = await supertest.get(`${config.path}/${indexPattern.id}`);
|
||||
|
||||
expect(response1.status).to.be(200);
|
||||
expect(response1.body[config.serviceKey].fields.foo.customLabel).to.be('baz');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const esArchiver = getService('esArchiver');
|
||||
const fieldEditor = getService('fieldEditor');
|
||||
const security = getService('security');
|
||||
const dataGrid = getService('dataGrid');
|
||||
const PageObjects = getPageObjects([
|
||||
'common',
|
||||
'discover',
|
||||
|
@ -68,6 +69,57 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await PageObjects.discover.getDocHeader()).to.have.string(customLabel);
|
||||
});
|
||||
|
||||
it('allows adding custom description to existing fields', async function () {
|
||||
const customDescription = 'custom bytes description here';
|
||||
const customDescription2 = `${customDescription} updated`;
|
||||
// set a custom description
|
||||
await PageObjects.discover.editField('bytes');
|
||||
await fieldEditor.enableCustomDescription();
|
||||
await fieldEditor.setCustomDescription(customDescription);
|
||||
await fieldEditor.save();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.unifiedFieldList.clickFieldListItem('bytes');
|
||||
await retry.waitFor('field popover text', async () => {
|
||||
return (await testSubjects.getVisibleText('fieldDescription-bytes')) === customDescription;
|
||||
});
|
||||
await PageObjects.unifiedFieldList.clickFieldListItemToggle('bytes');
|
||||
|
||||
// edit the custom description
|
||||
await PageObjects.discover.editField('bytes');
|
||||
await fieldEditor.enableCustomDescription();
|
||||
await fieldEditor.setCustomDescription(customDescription2);
|
||||
await fieldEditor.save();
|
||||
await fieldEditor.waitUntilClosed();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.unifiedFieldList.clickFieldListItem('bytes');
|
||||
await retry.waitFor('field popover text', async () => {
|
||||
return (await testSubjects.getVisibleText('fieldDescription-bytes')) === customDescription2;
|
||||
});
|
||||
await PageObjects.unifiedFieldList.clickFieldListItemToggle('bytes');
|
||||
|
||||
// check it in the doc viewer too
|
||||
await dataGrid.clickRowToggle({ rowIndex: 0 });
|
||||
await testSubjects.click('fieldDescriptionPopoverButton-bytes');
|
||||
await retry.waitFor('doc viewer popover text', async () => {
|
||||
return (await testSubjects.getVisibleText('fieldDescription-bytes')) === customDescription2;
|
||||
});
|
||||
|
||||
await dataGrid.closeFlyout();
|
||||
});
|
||||
|
||||
it('should show a validation error when adding a too long custom description to existing fields', async function () {
|
||||
const customDescription = 'custom bytes long description here'.repeat(10);
|
||||
// set a custom description
|
||||
await PageObjects.discover.editField('bytes');
|
||||
await fieldEditor.enableCustomDescription();
|
||||
await fieldEditor.setCustomDescription(customDescription);
|
||||
await fieldEditor.save();
|
||||
expect(await fieldEditor.getFormError()).to.contain(
|
||||
'The length of the description is too long. The maximum length is 300 characters.'
|
||||
);
|
||||
await fieldEditor.closeFlyoutAndDiscardChanges();
|
||||
});
|
||||
|
||||
it('allows creation of a new field', async function () {
|
||||
const field = '_runtimefield';
|
||||
await createRuntimeField(field);
|
||||
|
|
|
@ -27,6 +27,18 @@ export class FieldEditorService extends FtrService {
|
|||
public async setCustomLabel(name: string) {
|
||||
await this.testSubjects.setValue('customLabelRow > input', name);
|
||||
}
|
||||
public async enableCustomDescription() {
|
||||
await this.testSubjects.setEuiSwitch('customDescriptionRow > toggle', 'check');
|
||||
}
|
||||
public async setCustomDescription(description: string) {
|
||||
await this.testSubjects.setValue('customDescriptionRow > input', description);
|
||||
}
|
||||
public async getFormError() {
|
||||
const alert = await this.find.byCssSelector(
|
||||
'[data-test-subj=indexPatternFieldEditorForm] > [role="alert"]'
|
||||
);
|
||||
return await alert.getVisibleText();
|
||||
}
|
||||
public async enableValue() {
|
||||
await this.testSubjects.setEuiSwitch('valueRow > toggle', 'check');
|
||||
}
|
||||
|
@ -124,7 +136,21 @@ export class FieldEditorService extends FtrService {
|
|||
});
|
||||
}
|
||||
|
||||
public async confirmDiscardChanges() {
|
||||
await this.retry.try(async () => {
|
||||
await this.testSubjects.clickWhenNotDisabledWithoutRetry('confirmModalConfirmButton', {
|
||||
timeout: 1000,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async waitUntilClosed() {
|
||||
await this.testSubjects.waitForDeleted('fieldEditor');
|
||||
}
|
||||
|
||||
public async closeFlyoutAndDiscardChanges() {
|
||||
await this.testSubjects.click('fieldEditor > closeFlyoutButton');
|
||||
await this.confirmDiscardChanges();
|
||||
await this.waitUntilClosed();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3246,6 +3246,28 @@ Object {
|
|||
},
|
||||
"type": "number",
|
||||
},
|
||||
"customDescription": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"customLabel": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
|
@ -3402,6 +3424,28 @@ Object {
|
|||
],
|
||||
"type": "number",
|
||||
},
|
||||
"customDescription": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"customLabel": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
|
@ -3527,6 +3571,28 @@ Object {
|
|||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"customDescription": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"customLabel": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
|
@ -3770,6 +3836,28 @@ Object {
|
|||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"customDescription": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"customLabel": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
|
@ -4248,6 +4336,28 @@ Object {
|
|||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"customDescription": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"customLabel": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
|
@ -4491,6 +4601,28 @@ Object {
|
|||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"customDescription": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"customLabel": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
|
|
|
@ -97,6 +97,7 @@ export function buildIndexPatternField(
|
|||
scripted: field.scripted,
|
||||
isMapped: field.isMapped,
|
||||
customLabel: field.customLabel,
|
||||
customDescription: field.customDescription,
|
||||
runtimeField: field.runtimeField,
|
||||
runtime: Boolean(field.runtimeField),
|
||||
timeSeriesDimension: field.timeSeriesDimension,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue