diff --git a/src/plugins/event_annotation/common/content_management/v1/types.ts b/src/plugins/event_annotation/common/content_management/v1/types.ts index a5c23061008215..d3370b9ff28bf9 100644 --- a/src/plugins/event_annotation/common/content_management/v1/types.ts +++ b/src/plugins/event_annotation/common/content_management/v1/types.ts @@ -120,6 +120,9 @@ export interface EventAnnotationGroupSearchQuery { searchFields?: string[]; } -export type EventAnnotationGroupSearchIn = SearchIn; +export type EventAnnotationGroupSearchIn = SearchIn< + EventAnnotationGroupContentType, + EventAnnotationGroupSearchQuery +>; export type EventAnnotationGroupSearchOut = SearchResult; diff --git a/src/plugins/event_annotation/public/components/event_annotation_group_saved_object_finder.tsx b/src/plugins/event_annotation/public/components/event_annotation_group_saved_object_finder.tsx index 34b1ce7eb06945..72f931797d3c4f 100644 --- a/src/plugins/event_annotation/public/components/event_annotation_group_saved_object_finder.tsx +++ b/src/plugins/event_annotation/public/components/event_annotation_group_saved_object_finder.tsx @@ -67,35 +67,37 @@ export const EventAnnotationGroupSavedObjectFinder = ({ direction="column" justifyContent="center" > - - - - } - body={ - -

+ + -

-
- } - actions={ - onCreateNew()} size="s"> - - - } - /> + + } + body={ + +

+ +

+
+ } + actions={ + onCreateNew()} size="s"> + + + } + /> + ) : ( => { const savedObject = await client.get({ - contentTypeId: EVENT_ANNOTATION_GROUP_TYPE, + contentTypeId: CONTENT_ID, id: savedObjectId, }); @@ -117,6 +118,29 @@ export function getEventAnnotationService( return mapSavedObjectToGroupConfig(savedObject.item); }; + const groupExistsWithTitle = async (title: string): Promise => { + const { hits } = await client.search< + EventAnnotationGroupSearchIn, + EventAnnotationGroupSearchOut + >({ + contentTypeId: CONTENT_ID, + query: { + text: title, + }, + options: { + searchFields: ['title'], + }, + }); + + for (const hit of hits) { + if (hit.attributes.title.toLowerCase() === title.toLowerCase()) { + return true; + } + } + + return false; + }; + const findAnnotationGroupContent = async ( searchTerm: string, pageSize: number, @@ -138,7 +162,7 @@ export function getEventAnnotationService( EventAnnotationGroupSearchIn, EventAnnotationGroupSearchOut >({ - contentTypeId: EVENT_ANNOTATION_GROUP_TYPE, + contentTypeId: CONTENT_ID, query: { text: searchOptions.search, }, @@ -153,7 +177,7 @@ export function getEventAnnotationService( const deleteAnnotationGroups = async (ids: string[]): Promise => { for (const id of ids) { await client.delete({ - contentTypeId: EVENT_ANNOTATION_GROUP_TYPE, + contentTypeId: CONTENT_ID, id, }); } @@ -223,7 +247,7 @@ export function getEventAnnotationService( const groupSavedObjectId = ( await client.create({ - contentTypeId: EVENT_ANNOTATION_GROUP_TYPE, + contentTypeId: CONTENT_ID, data: { ...attributes, }, @@ -243,7 +267,7 @@ export function getEventAnnotationService( const { attributes, references } = getAnnotationGroupAttributesAndReferences(group); await client.update({ - contentTypeId: EVENT_ANNOTATION_GROUP_TYPE, + contentTypeId: CONTENT_ID, id: annotationGroupId, data: { ...attributes, @@ -259,7 +283,7 @@ export function getEventAnnotationService( EventAnnotationGroupSearchIn, EventAnnotationGroupSearchOut >({ - contentTypeId: EVENT_ANNOTATION_GROUP_TYPE, + contentTypeId: CONTENT_ID, query: { text: '*', }, @@ -270,6 +294,7 @@ export function getEventAnnotationService( return { loadAnnotationGroup, + groupExistsWithTitle, updateAnnotationGroup, createAnnotationGroup, deleteAnnotationGroups, diff --git a/src/plugins/event_annotation/public/event_annotation_service/types.ts b/src/plugins/event_annotation/public/event_annotation_service/types.ts index 8742ea86afff8c..8837792954b512 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/types.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/types.ts @@ -14,6 +14,7 @@ import { EventAnnotationConfig, EventAnnotationGroupConfig } from '../../common' export interface EventAnnotationServiceType { loadAnnotationGroup: (savedObjectId: string) => Promise; + groupExistsWithTitle: (title: string) => Promise; findAnnotationGroupContent: ( searchTerm: string, pageSize: number, diff --git a/src/plugins/event_annotation/server/content_management/event_annotation_group_storage.ts b/src/plugins/event_annotation/server/content_management/event_annotation_group_storage.ts index ce032374e7b535..5755a7efe998f6 100644 --- a/src/plugins/event_annotation/server/content_management/event_annotation_group_storage.ts +++ b/src/plugins/event_annotation/server/content_management/event_annotation_group_storage.ts @@ -268,9 +268,11 @@ export class EventAnnotationGroupStorage EventAnnotationGroupSearchQuery, EventAnnotationGroupSearchQuery >(options); + if (optionsError) { throw Boom.badRequest(`Invalid payload. ${optionsError.message}`); } + const { searchFields = ['title^3', 'description'], types = [SO_TYPE] } = optionsToLatest; const { included, excluded } = query.tags ?? {}; diff --git a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap index 70ba2d5619d032..e42ac60f9ba895 100644 --- a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -28,9 +28,11 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` - - - + - - - - Save - + + + + + + + + Save + + + `; @@ -154,9 +154,11 @@ exports[`SavedObjectSaveModal should render matching snapshot when custom isVali - - - + - - - - Save - + + + + + + + + Save + + + `; @@ -280,9 +280,11 @@ exports[`SavedObjectSaveModal should render matching snapshot when custom isVali - - - + - - - - Save - + + + + + + + + Save + + + `; @@ -410,9 +410,11 @@ exports[`SavedObjectSaveModal should render matching snapshot when given options - - - + - - - - Save - + + + + + + + + Save + + + `; diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx index c471271ead7edc..b9bd5e5d7fe362 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx @@ -30,7 +30,6 @@ import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { css } from '@emotion/react'; export interface OnSaveProps { newTitle: string; @@ -93,11 +92,19 @@ export class SavedObjectSaveModal extends React.Component const hasColumns = !!this.props.rightOptions; + const titleInputValid = + hasAttemptedSubmit && + ((!isTitleDuplicateConfirmed && hasTitleDuplicate) || title.length === 0); + const formBodyContent = ( <> } + isInvalid={titleInputValid} + error={i18n.translate('savedObjects.saveModal.titleRequired', { + defaultMessage: 'A title is required', + })} > data-test-subj="savedObjectTitle" value={title} onChange={this.onTitleChange} - isInvalid={ - hasAttemptedSubmit && - ((!isTitleDuplicateConfirmed && hasTitleDuplicate) || title.length === 0) - } + isInvalid={titleInputValid} aria-describedby={this.state.hasTitleDuplicate ? duplicateWarningId : undefined} /> @@ -167,20 +171,19 @@ export class SavedObjectSaveModal extends React.Component - - {this.renderCopyOnSave()} - - - - - {this.renderConfirmButton()} + + + {this.props.showCopyOnSave && {this.renderCopyOnSave()}} + + + + + + {this.renderConfirmButton()} + ); @@ -351,10 +354,6 @@ export class SavedObjectSaveModal extends React.Component }; private renderCopyOnSave = () => { - if (!this.props.showCopyOnSave) { - return; - } - return ( void; onCloneLayer: () => void; @@ -54,12 +55,12 @@ export const getSharedActions = ({ layerId: string; isOnlyLayer: boolean; activeVisualization: Visualization; - visualizationState: unknown; layerType?: LayerType; isTextBasedLanguage?: boolean; hasLayerSettings: boolean; openLayerSettings: () => void; core: Pick; + customRemoveModalText?: { title?: string; description?: string }; }) => [ getOpenLayerSettingsAction({ hasLayerSettings, @@ -77,6 +78,7 @@ export const getSharedActions = ({ layerType, isOnlyLayer, core, + customModalText: customRemoveModalText, }), ]; @@ -189,7 +191,7 @@ export const LayerActions = (props: LayerActionsProps) => { ; + customModalText?: { title?: string; description?: string }; } const SKIP_DELETE_MODAL_KEY = 'skipDeleteModal'; const getCopy = ( layerType: LayerType, - isOnlyLayer?: boolean + isOnlyLayer?: boolean, + customModalText: { title?: string; description?: string } | undefined = undefined ): { buttonLabel: string; modalTitle: string; modalBody: string } => { if (isOnlyLayer && layerType === 'data') { return { @@ -64,34 +66,46 @@ const getCopy = ( case 'data': return { buttonLabel, - modalTitle: i18n.translate('xpack.lens.modalTitle.title.deleteVis', { - defaultMessage: 'Delete visualization layer?', - }), - modalBody: i18n.translate('xpack.lens.layer.confirmModal.deleteVis', { - defaultMessage: `Deleting this layer removes the visualization and its configurations. `, - }), + modalTitle: + customModalText?.title ?? + i18n.translate('xpack.lens.modalTitle.title.deleteVis', { + defaultMessage: 'Delete visualization layer?', + }), + modalBody: + customModalText?.description ?? + i18n.translate('xpack.lens.layer.confirmModal.deleteVis', { + defaultMessage: `Deleting this layer removes the visualization and its configurations. `, + }), }; case 'annotations': return { buttonLabel, - modalTitle: i18n.translate('xpack.lens.modalTitle.title.deleteAnnotations', { - defaultMessage: 'Delete annotations layer?', - }), - modalBody: i18n.translate('xpack.lens.layer.confirmModal.deleteAnnotation', { - defaultMessage: `Deleting this layer removes the annotations and their configurations. `, - }), + modalTitle: + customModalText?.title ?? + i18n.translate('xpack.lens.modalTitle.title.deleteAnnotations', { + defaultMessage: 'Delete annotation group?', + }), + modalBody: + customModalText?.description ?? + i18n.translate('xpack.lens.layer.confirmModal.deleteAnnotation', { + defaultMessage: `Deleting this layer removes the annotations and their configurations. `, + }), }; case 'referenceLine': return { buttonLabel, - modalTitle: i18n.translate('xpack.lens.modalTitle.title.deleteReferenceLines', { - defaultMessage: 'Delete reference lines layer?', - }), - modalBody: i18n.translate('xpack.lens.layer.confirmModal.deleteRefLine', { - defaultMessage: `Deleting this layer removes the reference lines and their configurations. `, - }), + modalTitle: + customModalText?.title ?? + i18n.translate('xpack.lens.modalTitle.title.deleteReferenceLines', { + defaultMessage: 'Delete reference lines layer?', + }), + modalBody: + customModalText?.description ?? + i18n.translate('xpack.lens.layer.confirmModal.deleteRefLine', { + defaultMessage: `Deleting this layer removes the reference lines and their configurations. `, + }), }; default: @@ -193,7 +207,8 @@ const RemoveConfirmModal = ({ export const getRemoveLayerAction = (props: RemoveLayerAction): LayerAction => { const { buttonLabel, modalTitle, modalBody } = getCopy( props.layerType || LayerTypes.DATA, - props.isOnlyLayer + props.isOnlyLayer, + props.customModalText ); return { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index bb90c82b235e6b..96c460bcd04fe1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -363,7 +363,6 @@ export function LayerPanel( ...getSharedActions({ layerId, activeVisualization, - visualizationState, core, layerIndex, layerType: activeVisualization.getLayerType(layerId, visualizationState), @@ -377,6 +376,10 @@ export function LayerPanel( openLayerSettings: () => setPanelSettingsOpen(true), onCloneLayer, onRemoveLayer: () => onRemoveLayer(layerId), + customRemoveModalText: activeVisualization.getCustomRemoveLayerText?.( + layerId, + visualizationState + ), }), ].filter((i) => i.isCompatible), [ diff --git a/x-pack/plugins/lens/public/shared_components/static_header.tsx b/x-pack/plugins/lens/public/shared_components/static_header.tsx index 8069a2d2c98498..b4c5d8931065e9 100644 --- a/x-pack/plugins/lens/public/shared_components/static_header.tsx +++ b/x-pack/plugins/lens/public/shared_components/static_header.tsx @@ -40,15 +40,7 @@ export const StaticHeader = ({
{label}
- {indicator && ( -
- {indicator} -
- )} + {indicator}
); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 005c016d0580e8..99cf847508133e 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -1118,6 +1118,15 @@ export interface Visualization LayerAction[]; + /** + * This method is a clunky solution to the problem, but I'm banking on the confirm modal being removed + * with undo/redo anyways + */ + getCustomRemoveLayerText?: ( + layerId: string, + state: T + ) => { title?: string; description?: string } | undefined; + /** returns the type string of the given layer */ getLayerType: (layerId: string, state?: T) => LayerType | undefined; diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.test.tsx index 8505f9811749a1..cf389614a50b44 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.test.tsx @@ -113,7 +113,7 @@ describe('annotation group save action', () => { describe('save routine', () => { const layerId = 'mylayerid'; - const layer: XYByValueAnnotationLayerConfig = { + const byValueLayer: XYByValueAnnotationLayerConfig = { layerId, layerType: 'annotations', indexPatternId: 'some-index-pattern', @@ -144,10 +144,11 @@ describe('annotation group save action', () => { legend: { isVisible: true, position: 'bottom' }, layers: [{ layerId } as XYAnnotationLayerConfig], } as XYState, - layer, + layer: byValueLayer, setState: jest.fn(), eventAnnotationService: { createAnnotationGroup: jest.fn(() => Promise.resolve({ id: savedId })), + groupExistsWithTitle: jest.fn(() => Promise.resolve(false)), updateAnnotationGroup: jest.fn(), loadAnnotationGroup: jest.fn(), toExpression: jest.fn(), @@ -162,7 +163,7 @@ describe('annotation group save action', () => { newTags: ['my-tag'], newCopyOnSave: false, isTitleDuplicateConfirmed: false, - onTitleDuplicate: () => {}, + onTitleDuplicate: jest.fn(), }, dataViews, goToAnnotationLibrary: () => Promise.resolve(), @@ -321,5 +322,78 @@ describe('annotation group save action', () => { expect(props.toasts.addSuccess).toHaveBeenCalledTimes(1); }); + + it.each` + existingGroup | newCopyOnSave | titleChanged | isTitleDuplicateConfirmed | expectPreventSave + ${false} | ${false} | ${false} | ${false} | ${true} + ${false} | ${false} | ${false} | ${true} | ${false} + ${true} | ${false} | ${false} | ${false} | ${false} + ${true} | ${true} | ${false} | ${false} | ${true} + ${true} | ${true} | ${false} | ${true} | ${false} + `( + 'checks duplicate title when saving group', + async ({ + existingGroup, + newCopyOnSave, + titleChanged, + isTitleDuplicateConfirmed, + expectPreventSave, + }) => { + (props.eventAnnotationService.groupExistsWithTitle as jest.Mock).mockResolvedValueOnce( + true + ); + + const oldTitle = 'old title'; + let layer: XYAnnotationLayerConfig = byValueLayer; + if (existingGroup) { + const byReferenceLayer: XYByReferenceAnnotationLayerConfig = { + ...props.layer, + annotationGroupId: 'my-group-id', + __lastSaved: { + ...props.layer, + title: oldTitle, + description: 'description', + tags: [], + }, + }; + layer = byReferenceLayer; + } + + const newTitle = titleChanged ? 'my changed title' : oldTitle; + + await onSave({ + ...props, + layer, + modalOnSaveProps: { + ...props.modalOnSaveProps, + newTitle, + isTitleDuplicateConfirmed, + newCopyOnSave, + }, + }); + + if (expectPreventSave) { + expect(props.eventAnnotationService.updateAnnotationGroup).not.toHaveBeenCalled(); + + expect(props.eventAnnotationService.createAnnotationGroup).not.toHaveBeenCalled(); + + expect(props.modalOnSaveProps.closeModal).not.toHaveBeenCalled(); + + expect(props.setState).not.toHaveBeenCalled(); + + expect(props.toasts.addSuccess).not.toHaveBeenCalled(); + + expect(props.modalOnSaveProps.onTitleDuplicate).toHaveBeenCalled(); + } else { + expect(props.modalOnSaveProps.onTitleDuplicate).not.toHaveBeenCalled(); + + expect(props.modalOnSaveProps.closeModal).toHaveBeenCalled(); + + expect(props.setState).toHaveBeenCalled(); + + expect(props.toasts.addSuccess).toHaveBeenCalledTimes(1); + } + } + ); }); }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.tsx b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.tsx index d0888189b85927..803dc3e41e87d1 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.tsx @@ -133,6 +133,28 @@ const saveAnnotationGroupToLibrary = async ( return { id: savedId, config: groupConfig }; }; +const shouldStopBecauseDuplicateTitle = async ( + newTitle: string, + existingTitle: string, + newCopyOnSave: ModalOnSaveProps['newCopyOnSave'], + onTitleDuplicate: ModalOnSaveProps['onTitleDuplicate'], + isTitleDuplicateConfirmed: ModalOnSaveProps['isTitleDuplicateConfirmed'], + eventAnnotationService: EventAnnotationServiceType +) => { + if (isTitleDuplicateConfirmed || (newTitle === existingTitle && !newCopyOnSave)) { + return false; + } + + const duplicateExists = await eventAnnotationService.groupExistsWithTitle(newTitle); + + if (duplicateExists) { + onTitleDuplicate(); + return true; + } else { + return false; + } +}; + /** @internal exported for testing only */ export const onSave = async ({ state, @@ -140,7 +162,15 @@ export const onSave = async ({ setState, eventAnnotationService, toasts, - modalOnSaveProps: { newTitle, newDescription, newTags, closeModal, newCopyOnSave }, + modalOnSaveProps: { + newTitle, + newDescription, + newTags, + closeModal, + newCopyOnSave, + onTitleDuplicate, + isTitleDuplicateConfirmed, + }, dataViews, goToAnnotationLibrary, }: { @@ -153,6 +183,17 @@ export const onSave = async ({ dataViews: DataViewsContract; goToAnnotationLibrary: () => Promise; }) => { + const shouldStop = await shouldStopBecauseDuplicateTitle( + newTitle, + isByReferenceAnnotationsLayer(layer) ? layer.__lastSaved.title : '', + newCopyOnSave, + onTitleDuplicate, + isTitleDuplicateConfirmed, + eventAnnotationService + ); + + if (shouldStop) return; + let savedInfo: Awaited>; try { savedInfo = await saveAnnotationGroupToLibrary( @@ -205,27 +246,25 @@ export const onSave = async ({ text: ((element) => render( -

- goToAnnotationLibrary()} - > - {i18n.translate( - 'xpack.lens.xyChart.annotations.saveAnnotationGroupToLibrary.annotationLibrary', - { - defaultMessage: 'annotation library', - } - )} - - ), - }} - /> -

+ goToAnnotationLibrary()} + > + {i18n.translate( + 'xpack.lens.xyChart.annotations.saveAnnotationGroupToLibrary.annotationLibrary', + { + defaultMessage: 'annotation library', + } + )} + + ), + }} + />
, element )) as MountPoint, @@ -258,7 +297,7 @@ export const getSaveLayerAction = ({ const displayName = i18n.translate( 'xpack.lens.xyChart.annotations.saveAnnotationGroupToLibrary', { - defaultMessage: 'Save annotation group', + defaultMessage: 'Save to library', } ); diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx index ec89021686c6d9..b9b29a981c2daa 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx @@ -416,16 +416,6 @@ export const getAnnotationsConfiguration = ({ }) => { const groupLabel = getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }); - const emptyButtonLabels = { - buttonAriaLabel: i18n.translate('xpack.lens.indexPattern.addColumnAriaLabelClick', { - defaultMessage: 'Add an annotation to {groupLabel}', - values: { groupLabel }, - }), - buttonLabel: i18n.translate('xpack.lens.configure.emptyConfigClick', { - defaultMessage: 'Add an annotation', - }), - }; - return { groups: [ { @@ -445,7 +435,6 @@ export const getAnnotationsConfiguration = ({ supportFieldFormat: false, enableDimensionEditor: true, filterOperations: () => false, - labels: emptyButtonLabels, }, ], }; diff --git a/x-pack/plugins/lens/public/visualizations/xy/load_annotation_library_flyout.tsx b/x-pack/plugins/lens/public/visualizations/xy/load_annotation_library_flyout.tsx index ba22b00ccd3626..cb9521b42e0a64 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/load_annotation_library_flyout.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/load_annotation_library_flyout.tsx @@ -70,6 +70,7 @@ export function LoadAnnotationLibraryFlyout({
{ Object { "data-test-subj": "lnsXY_annotationLayer_saveToLibrary", "description": "Saves annotation group as separate saved object", - "displayName": "Save annotation group", + "displayName": "Save to library", "execute": [Function], "icon": "save", "isCompatible": true, @@ -3899,4 +3899,52 @@ describe('xy_visualization', () => { ).not.toThrowError(); }); }); + + describe('#getCustomRemoveLayerText', () => { + it('should NOT return custom text for the remove layer button if not by-reference', () => { + expect(xyVisualization.getCustomRemoveLayerText!('first', exampleState())).toBeUndefined(); + }); + + it('should return custom text for the remove layer button if by-reference', () => { + const layerId = 'layer-id'; + + const commonProps = { + layerId, + layerType: 'annotations' as const, + indexPatternId: 'some-index-pattern', + ignoreGlobalFilters: false, + annotations: [ + { + id: 'some-annotation-id', + type: 'manual', + key: { + type: 'point_in_time', + timestamp: 'timestamp', + }, + } as PointInTimeEventAnnotationConfig, + ], + }; + + const layer: XYByReferenceAnnotationLayerConfig = { + ...commonProps, + annotationGroupId: 'some-group-id', + __lastSaved: { + ...commonProps, + title: 'My saved object title', + description: 'some description', + tags: [], + }, + }; + expect( + xyVisualization.getCustomRemoveLayerText!(layerId, { + ...exampleState(), + layers: [layer], + }) + ).toMatchInlineSnapshot(` + Object { + "title": "Delete \\"My saved object title\\"", + } + `); + }); + }); }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 1a3d002d3baea8..7f236b69f44714 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -97,6 +97,7 @@ import { getVisualizationType, isAnnotationsLayer, isBucketed, + isByReferenceAnnotationsLayer, isDataLayer, isNumericDynamicMetric, isReferenceLayer, @@ -304,6 +305,14 @@ export const getXyVisualization = ({ return actions; }, + getCustomRemoveLayerText(layerId, state) { + const layerIndex = state.layers.findIndex((l) => l.layerId === layerId); + const layer = state.layers[layerIndex]; + if (layer && isByReferenceAnnotationsLayer(layer)) { + return { title: `Delete "${layer.__lastSaved.title}"` }; + } + }, + hasLayerSettings({ state, layerId: currentLayerId }) { const layer = state.layers?.find(({ layerId }) => layerId === currentLayerId); return { data: Boolean(layer && isAnnotationsLayer(layer)), appearance: false }; diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx index a549ac42d94485..c9bd4423015961 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx @@ -161,7 +161,7 @@ export const isPersistedByValueAnnotationsLayer = ( (layer.persistanceType === 'byValue' || !layer.persistanceType); export const isByReferenceAnnotationsLayer = ( - layer: XYAnnotationLayerConfig + layer: XYLayerConfig ): layer is XYByReferenceAnnotationLayerConfig => 'annotationGroupId' in layer && '__lastSaved' in layer; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx index eab7da089e07d6..cb37fcbf5edfcf 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx @@ -19,6 +19,7 @@ import { import { ToolbarButton } from '@kbn/kibana-react-plugin/public'; import { IconChartBarReferenceLine, IconChartBarAnnotations } from '@kbn/chart-icons'; import { euiThemeVars } from '@kbn/ui-theme'; +import { css } from '@emotion/react'; import { getIgnoreGlobalFilterIcon } from '../../../shared_components/ignore_global_filter/data_view_picker_icon'; import type { VisualizationLayerHeaderContentProps, @@ -96,13 +97,20 @@ function AnnotationsLayerHeader({ } indicator={ hasUnsavedChanges && ( - +
+ +
) } /> diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index a3a5e56ea9890c..3cd106c62778f3 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -20081,7 +20081,6 @@ "xpack.lens.functions.timeScale.dateColumnMissingMessage": "L'ID de colonne de date spécifié {columnId} n'existe pas.", "xpack.lens.heatmapVisualization.arrayValuesWarningMessage": "{label} contient des valeurs de tableau. Le rendu de votre visualisation peut ne pas se présenter comme attendu.", "xpack.lens.indexPattern.addColumnAriaLabel": "Ajouter ou faire glisser un champ vers {groupLabel}", - "xpack.lens.indexPattern.addColumnAriaLabelClick": "Ajouter une annotation à {groupLabel}", "xpack.lens.indexPattern.annotationsDimensionEditorLabel": "Annotation {groupLabel}", "xpack.lens.indexPattern.ascendingCountPrecisionErrorWarning": "{name} pour cette visualisation peut être approximatif en raison de la manière dont les données sont indexées. Essayez de trier par rareté plutôt que par nombre ascendant d’enregistrements. Pour en savoir plus sur cette limitation, {link}.", "xpack.lens.indexPattern.autoIntervalLabel": "Auto ({interval})", @@ -20316,7 +20315,6 @@ "xpack.lens.configPanel.selectVisualization": "Sélectionner une visualisation", "xpack.lens.configPanel.visualizationType": "Type de visualisation", "xpack.lens.configure.emptyConfig": "Ajouter ou glisser-déposer un champ", - "xpack.lens.configure.emptyConfigClick": "Ajouter une annotation", "xpack.lens.configure.invalidBottomReferenceLineDimension": "La ligne de référence est affectée à un axe qui n’existe plus ou qui n’est plus valide. Vous pouvez déplacer cette ligne de référence vers un autre axe disponible ou la supprimer.", "xpack.lens.configure.invalidConfigTooltip": "Configuration non valide.", "xpack.lens.configure.invalidConfigTooltipClick": "Cliquez pour en savoir plus.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4f5ca7c630f7e1..f9f7533a0a6d6e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20080,7 +20080,6 @@ "xpack.lens.functions.timeScale.dateColumnMissingMessage": "指定したdateColumnId {columnId}は存在しません。", "xpack.lens.heatmapVisualization.arrayValuesWarningMessage": "{label}には配列値が含まれます。可視化が想定通りに表示されない場合があります。", "xpack.lens.indexPattern.addColumnAriaLabel": "フィールドを追加するか、{groupLabel}にドラッグアンドドロップします", - "xpack.lens.indexPattern.addColumnAriaLabelClick": "注釈を{groupLabel}に追加", "xpack.lens.indexPattern.annotationsDimensionEditorLabel": "{groupLabel}注釈", "xpack.lens.indexPattern.ascendingCountPrecisionErrorWarning": "データのインデックス方法のため、このビジュアライゼーションの{name}は近似される場合があります。レコード数の昇順ではなく希少性で並べ替えてください。この制限の詳細については、{link}。", "xpack.lens.indexPattern.autoIntervalLabel": "自動({interval})", @@ -20316,7 +20315,6 @@ "xpack.lens.configPanel.selectVisualization": "ビジュアライゼーションを選択してください", "xpack.lens.configPanel.visualizationType": "ビジュアライゼーションタイプ", "xpack.lens.configure.emptyConfig": "フィールドを追加するか、ドラッグアンドドロップします", - "xpack.lens.configure.emptyConfigClick": "注釈の追加", "xpack.lens.configure.invalidBottomReferenceLineDimension": "この基準線は存在しないか有効ではない軸に割り当てられています。この基準線を別の使用可能な軸に移動するか、削除することができます。", "xpack.lens.configure.invalidConfigTooltip": "無効な構成です。", "xpack.lens.configure.invalidConfigTooltipClick": "詳細はクリックしてください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 811f7d6cc771fc..340ddab39caef3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20080,7 +20080,6 @@ "xpack.lens.functions.timeScale.dateColumnMissingMessage": "指定的 dateColumnId {columnId} 不存在。", "xpack.lens.heatmapVisualization.arrayValuesWarningMessage": "{label} 包含数组值。您的可视化可能无法正常渲染。", "xpack.lens.indexPattern.addColumnAriaLabel": "将字段添加或拖放到 {groupLabel}", - "xpack.lens.indexPattern.addColumnAriaLabelClick": "添加标注到 {groupLabel}", "xpack.lens.indexPattern.annotationsDimensionEditorLabel": "{groupLabel} 标注", "xpack.lens.indexPattern.ascendingCountPrecisionErrorWarning": "由于数据的索引方式,此可视化的 {name} 可能为近似值。尝试按稀有度排序,而不是采用升序记录计数。有关此限制的详情,{link}。", "xpack.lens.indexPattern.autoIntervalLabel": "自动 ({interval})", @@ -20316,7 +20315,6 @@ "xpack.lens.configPanel.selectVisualization": "选择可视化", "xpack.lens.configPanel.visualizationType": "可视化类型", "xpack.lens.configure.emptyConfig": "添加或拖放字段", - "xpack.lens.configure.emptyConfigClick": "添加标注", "xpack.lens.configure.invalidBottomReferenceLineDimension": "此参考线分配给了不再存在或不再有效的轴。您可以将此参考线移到其他可用的轴,或将其移除。", "xpack.lens.configure.invalidConfigTooltip": "配置无效。", "xpack.lens.configure.invalidConfigTooltipClick": "单击了解更多详情。",