[Event annotations] Cache annotation group metadata (#167822)

## Summary

Fix https://github.com/elastic/kibana/issues/166855
Fix https://github.com/elastic/kibana/issues/167817

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Drew Tate 2023-10-04 08:12:29 -06:00 committed by GitHub
parent 4c5c6de84e
commit 4c233537ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 80 additions and 13 deletions

View file

@ -322,6 +322,8 @@ const AnnotationEditorControls = ({
)}
<ColorPicker
overwriteColor={currentAnnotation.color}
isClearable={false}
defaultColor={isRange ? defaultAnnotationRangeColor : defaultAnnotationColor}
showAlpha={isRange}
setConfig={update}

View file

@ -31,6 +31,7 @@ const tooltipContent = {
export const ColorPicker = ({
overwriteColor,
defaultColor,
isClearable,
setConfig,
label,
disableHelpTooltip,
@ -39,6 +40,7 @@ export const ColorPicker = ({
}: {
overwriteColor?: string | null;
defaultColor?: string | null;
isClearable?: boolean;
setConfig: (config: { color?: string }) => void;
label?: string;
disableHelpTooltip?: boolean;
@ -93,14 +95,14 @@ export const ColorPicker = ({
fullWidth
data-test-subj="indexPattern-dimension-colorPicker"
compressed
isClearable={Boolean(overwriteColor)}
isClearable={typeof isClearable !== 'undefined' ? isClearable : Boolean(overwriteColor)}
onChange={handleColor}
color={isDisabled ? '' : colorText}
disabled={isDisabled}
placeholder={' '}
onBlur={() => {
if (!colorText) {
setColorText(overwriteColor ?? defaultColor);
setColorText(validatedColor ?? defaultColor);
}
}}
aria-label={inputLabel}

View file

@ -25,6 +25,7 @@ import { IToasts } from '@kbn/core-notifications-browser';
import type { LayerAction, StateSetter } from '../../../../types';
import type { XYState, XYByReferenceAnnotationLayerConfig } from '../../types';
import { annotationLayerHasUnsavedChanges } from '../../state_helpers';
import { getAnnotationLayerTitle } from '../../visualization_helpers';
export const getRevertChangesAction = ({
state,
@ -50,7 +51,7 @@ export const getRevertChangesAction = ({
<RevertChangesConfirmModal
modalTitle={i18n.translate('xpack.lens.modalTitle.revertAnnotationGroupTitle', {
defaultMessage: 'Revert "{title}" changes?',
values: { title: layer.__lastSaved.title },
values: { title: getAnnotationLayerTitle(layer) },
})}
onCancel={() => modal.close()}
onConfirm={() => {
@ -111,7 +112,7 @@ export const revert = ({
toasts.addSuccess({
title: i18n.translate('xpack.lens.xyChart.annotations.notificationReverted', {
defaultMessage: `Reverted "{title}"`,
values: { title: layer.__lastSaved.title },
values: { title: getAnnotationLayerTitle(layer) },
}),
text: i18n.translate('xpack.lens.xyChart.annotations.notificationRevertedExplanation', {
defaultMessage: 'The most recently saved version of this annotation group has been restored.',

View file

@ -28,7 +28,11 @@ import type {
StateSetter,
} from '../../../../types';
import { XYByReferenceAnnotationLayerConfig, XYAnnotationLayerConfig, XYState } from '../../types';
import { isByReferenceAnnotationsLayer } from '../../visualization_helpers';
import {
getAnnotationLayerTitle,
getGroupMetadataFromAnnotationLayer,
isByReferenceAnnotationsLayer,
} from '../../visualization_helpers';
type ModalOnSaveProps = SavedObjectOnSaveProps & { newTags: string[]; closeModal: () => void };
@ -194,7 +198,7 @@ export const onSave = async ({
}) => {
const shouldStop = await shouldStopBecauseDuplicateTitle(
newTitle,
isByReferenceAnnotationsLayer(layer) ? layer.__lastSaved.title : '',
getAnnotationLayerTitle(layer),
newCopyOnSave,
onTitleDuplicate,
isTitleDuplicateConfirmed,
@ -326,6 +330,8 @@ export const getSaveLayerAction = ({
),
execute: async (domElement) => {
if (domElement) {
const metadata = getGroupMetadataFromAnnotationLayer(layer);
render(
<KibanaThemeProvider theme$={kibanaTheme.theme$}>
<I18nProvider>
@ -345,9 +351,7 @@ export const getSaveLayerAction = ({
goToAnnotationLibrary,
});
}}
title={neverSaved ? '' : layer.__lastSaved.title}
description={neverSaved ? '' : layer.__lastSaved.description}
tags={neverSaved ? [] : layer.__lastSaved.tags}
{...metadata}
showCopyOnSave={!neverSaved}
/>
</I18nProvider>

View file

@ -13,6 +13,7 @@ import {
XYByValueAnnotationLayerConfig,
XYState,
} from '../../types';
import { getAnnotationLayerTitle } from '../../visualization_helpers';
export const getUnlinkLayerAction = ({
state,
@ -45,7 +46,7 @@ export const getUnlinkLayerAction = ({
toasts.addSuccess(
i18n.translate('xpack.lens.xyChart.annotations.notificationUnlinked', {
defaultMessage: `Unlinked "{title}"`,
values: { title: layer.__lastSaved.title },
values: { title: getAnnotationLayerTitle(layer) },
})
);
},

View file

@ -187,6 +187,11 @@ export function getPersistableState(state: XYState) {
} else {
const persistableLayer: XYPersistedLinkedByValueAnnotationLayerConfig = {
persistanceType: 'linked',
cachedMetadata: layer.cachedMetadata || {
title: layer.__lastSaved.title,
description: layer.__lastSaved.description,
tags: layer.__lastSaved.tags,
},
layerId: layer.layerId,
layerType: layer.layerType,
annotationGroupRef: referenceName,
@ -298,6 +303,7 @@ export function injectReferences(
ignoreGlobalFilters: persistedLayer.ignoreGlobalFilters,
indexPatternId: getIndexPatternIdFromReferences(persistedLayer.layerId),
annotations: cloneDeep(persistedLayer.annotations),
cachedMetadata: persistedLayer.cachedMetadata,
};
}
}

View file

@ -119,6 +119,13 @@ export interface XYByValueAnnotationLayerConfig {
annotations: EventAnnotationConfig[];
indexPatternId: string;
ignoreGlobalFilters: boolean;
// populated only when the annotation has been forked from the
// version saved in the library (persisted as XYPersistedLinkedByValueAnnotationLayerConfig)
cachedMetadata?: {
title: string;
description: string;
tags: string[];
};
}
export type XYPersistedByValueAnnotationLayerConfig = Omit<

View file

@ -460,6 +460,11 @@ describe('xy_visualization', () => {
annotations: [], // different from the persisted group
},
{
cachedMetadata: {
title: 'Local title',
description: '',
tags: [],
},
layerId: 'annotation',
layerType: layerTypes.ANNOTATIONS,
persistanceType: 'linked',
@ -501,6 +506,7 @@ describe('xy_visualization', () => {
{
layerId: 'annotation',
layerType: layerTypes.ANNOTATIONS,
cachedMetadata: persistedAnnotationLayers[1].cachedMetadata,
annotationGroupId: annotationGroupId2,
ignoreGlobalFilters: persistedAnnotationLayers[1].ignoreGlobalFilters,
annotations: persistedAnnotationLayers[1].annotations,
@ -3718,6 +3724,12 @@ describe('xy_visualization', () => {
layerId: 'layer-id',
layerType: 'annotations',
persistanceType: 'linked',
// stores "cached" or "local" metadata
cachedMetadata: {
description: 'some description',
tags: [],
title: 'My saved object title',
},
annotations: layers[0].annotations,
ignoreGlobalFilters: layers[0].ignoreGlobalFilters,
},
@ -3726,6 +3738,11 @@ describe('xy_visualization', () => {
layerId: 'layer-id2',
layerType: 'annotations',
persistanceType: 'linked',
cachedMetadata: {
description: 'some description',
tags: [],
title: 'My saved object title',
},
annotations: layers[1].annotations,
ignoreGlobalFilters: layers[1].ignoreGlobalFilters,
},

View file

@ -85,6 +85,7 @@ import {
import {
checkXAccessorCompatibility,
defaultSeriesType,
getAnnotationLayerTitle,
getAnnotationsLayers,
getAxisName,
getDataLayers,
@ -334,7 +335,7 @@ export const getXyVisualization = ({
const layerIndex = state.layers.findIndex((l) => l.layerId === layerId);
const layer = state.layers[layerIndex];
if (layer && isByReferenceAnnotationsLayer(layer)) {
return { title: `Delete "${layer.__lastSaved.title}"` };
return { title: `Delete "${getAnnotationLayerTitle(layer)}"` };
}
},

View file

@ -178,6 +178,23 @@ export const isPersistedLinkedByValueAnnotationsLayer = (
export const getAnnotationsLayers = (layers: Array<Pick<XYLayerConfig, 'layerType'>>) =>
(layers || []).filter((layer): layer is XYAnnotationLayerConfig => isAnnotationsLayer(layer));
export const getGroupMetadataFromAnnotationLayer = (
layer: XYAnnotationLayerConfig
): { title: string; description: string; tags: string[] } => {
if (layer.cachedMetadata) {
return layer.cachedMetadata;
}
if (isByReferenceAnnotationsLayer(layer)) {
const { title, description, tags } = layer.__lastSaved;
return { title, description, tags };
}
return { title: '', description: '', tags: [] };
};
export const getAnnotationLayerTitle = (layer: XYAnnotationLayerConfig): string => {
return getGroupMetadataFromAnnotationLayer(layer).title;
};
export interface LayerTypeToLayer {
[layerTypes.DATA]: (layer: XYDataLayerConfig) => XYDataLayerConfig;
[layerTypes.REFERENCELINE]: (layer: XYReferenceLineLayerConfig) => XYReferenceLineLayerConfig;

View file

@ -71,6 +71,15 @@ describe('layer header', () => {
.text()
.trim()
).toBe(byRefGroupTitle);
const cachedMetadata = { title: 'A cached title', description: '', tags: [] };
expect(
mountWithIntl(
<LayerHeader {...props} state={getStateWithLayers([{ ...byRefLayer, cachedMetadata }])} />
)
.text()
.trim()
).toBe(cachedMetadata.title);
});
});
});

View file

@ -35,8 +35,8 @@ import {
import { ChangeIndexPattern, StaticHeader } from '../../../shared_components';
import { updateLayer } from '.';
import {
getAnnotationLayerTitle,
isAnnotationsLayer,
isByReferenceAnnotationsLayer,
isDataLayer,
isReferenceLayer,
} from '../visualization_helpers';
@ -52,7 +52,7 @@ export function LayerHeader(props: VisualizationLayerWidgetProps<State>) {
if (isAnnotationsLayer(layer)) {
return (
<AnnotationsLayerHeader
title={isByReferenceAnnotationsLayer(layer) ? layer.__lastSaved.title : undefined}
title={getAnnotationLayerTitle(layer)}
hasUnsavedChanges={annotationLayerHasUnsavedChanges(layer)}
/>
);