mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Lens] library annotation groups (#152623)
This commit is contained in:
parent
15b31c62ba
commit
f630d90697
111 changed files with 5104 additions and 923 deletions
|
@ -6,7 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { deepFreeze } from '@kbn/std';
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import type {
|
||||
CapabilitiesStart,
|
||||
|
@ -14,11 +13,11 @@ import type {
|
|||
} from '@kbn/core-capabilities-browser-internal';
|
||||
|
||||
const createStartContractMock = (): jest.Mocked<CapabilitiesStart> => ({
|
||||
capabilities: deepFreeze({
|
||||
capabilities: {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const createMock = (): jest.Mocked<PublicMethodsOf<CapabilitiesService>> => ({
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"**/*.tsx",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/std",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/core-capabilities-browser-internal"
|
||||
],
|
||||
|
|
|
@ -931,6 +931,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"event-annotation-group": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualization": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
|
|
|
@ -39,7 +39,7 @@ pageLoadAssetSize:
|
|||
embeddableEnhanced: 22107
|
||||
enterpriseSearch: 35741
|
||||
esUiShared: 326654
|
||||
eventAnnotation: 20500
|
||||
eventAnnotation: 22000
|
||||
exploratoryView: 74673
|
||||
expressionError: 22127
|
||||
expressionGauge: 25000
|
||||
|
|
|
@ -86,6 +86,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"enterprise_search_telemetry": "9ac912e1417fc8681e0cd383775382117c9e3d3d",
|
||||
"epm-packages": "2449bb565f987eff70b1b39578bb17e90c404c6e",
|
||||
"epm-packages-assets": "7a3e58efd9a14191d0d1a00b8aaed30a145fd0b1",
|
||||
"event-annotation-group": "715ba867d8c68f3c9438052210ea1c30a9362582",
|
||||
"event_loop_delays_daily": "01b967e8e043801357503de09199dfa3853bab88",
|
||||
"exception-list": "4aebc4e61fb5d608cae48eaeb0977e8db21c61a4",
|
||||
"exception-list-agnostic": "6d3262d58eee28ac381ec9654f93126a58be6f5d",
|
||||
|
|
|
@ -201,6 +201,7 @@ describe('split .kibana index into multiple system indices', () => {
|
|||
"enterprise_search_telemetry",
|
||||
"epm-packages",
|
||||
"epm-packages-assets",
|
||||
"event-annotation-group",
|
||||
"event_loop_delays_daily",
|
||||
"exception-list",
|
||||
"exception-list-agnostic",
|
||||
|
|
|
@ -43,6 +43,7 @@ const previouslyRegisteredTypes = [
|
|||
'csp-rule-template',
|
||||
'csp_rule',
|
||||
'dashboard',
|
||||
'event-annotation-group',
|
||||
'endpoint:user-artifact',
|
||||
'endpoint:user-artifact-manifest',
|
||||
'enterprise_search_telemetry',
|
||||
|
|
|
@ -12,7 +12,7 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
|||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||
import { CoreSetup, CoreStart, IUiSettingsClient } from '@kbn/core/public';
|
||||
import { EventAnnotationPluginSetup } from '@kbn/event-annotation-plugin/public';
|
||||
import type { EventAnnotationPluginStart } from '@kbn/event-annotation-plugin/public';
|
||||
import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
|
||||
import { ExpressionXyPluginSetup, ExpressionXyPluginStart, SetupDeps } from './types';
|
||||
import {
|
||||
|
@ -37,7 +37,7 @@ export interface XYPluginStartDependencies {
|
|||
data: DataPublicPluginStart;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
charts: ChartsPluginStart;
|
||||
eventAnnotation: EventAnnotationPluginSetup;
|
||||
eventAnnotation: EventAnnotationPluginStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,32 +2,13 @@
|
|||
|
||||
exports[`renders DashboardSaveModal 1`] = `
|
||||
<SavedObjectSaveModal
|
||||
description="dash description"
|
||||
initialCopyOnSave={true}
|
||||
objectType="dashboard"
|
||||
onClose={[Function]}
|
||||
onSave={[Function]}
|
||||
options={
|
||||
<React.Fragment>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Description"
|
||||
id="dashboard.topNav.saveModal.descriptionFormRowLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="dashboardDescription"
|
||||
onChange={[Function]}
|
||||
value="dash description"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
|
@ -58,7 +39,7 @@ exports[`renders DashboardSaveModal 1`] = `
|
|||
</React.Fragment>
|
||||
}
|
||||
showCopyOnSave={true}
|
||||
showDescription={false}
|
||||
showDescription={true}
|
||||
title="dash title"
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Fragment } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiFormRow, EuiTextArea, EuiSwitch } from '@elastic/eui';
|
||||
import { EuiFormRow, EuiSwitch } from '@elastic/eui';
|
||||
import { SavedObjectSaveModal } from '@kbn/saved-objects-plugin/public';
|
||||
|
||||
import type { DashboardSaveOptions } from '../../../types';
|
||||
|
@ -39,14 +39,12 @@ interface Props {
|
|||
}
|
||||
|
||||
interface State {
|
||||
description: string;
|
||||
tags: string[];
|
||||
timeRestore: boolean;
|
||||
}
|
||||
|
||||
export class DashboardSaveModal extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
description: this.props.description,
|
||||
timeRestore: this.props.timeRestore,
|
||||
tags: this.props.tags ?? [],
|
||||
};
|
||||
|
@ -57,18 +55,20 @@ export class DashboardSaveModal extends React.Component<Props, State> {
|
|||
|
||||
saveDashboard = ({
|
||||
newTitle,
|
||||
newDescription,
|
||||
newCopyOnSave,
|
||||
isTitleDuplicateConfirmed,
|
||||
onTitleDuplicate,
|
||||
}: {
|
||||
newTitle: string;
|
||||
newDescription: string;
|
||||
newCopyOnSave: boolean;
|
||||
isTitleDuplicateConfirmed: boolean;
|
||||
onTitleDuplicate: () => void;
|
||||
}) => {
|
||||
this.props.onSave({
|
||||
newTitle,
|
||||
newDescription: this.state.description,
|
||||
newDescription,
|
||||
newCopyOnSave,
|
||||
newTimeRestore: this.state.timeRestore,
|
||||
isTitleDuplicateConfirmed,
|
||||
|
@ -77,12 +77,6 @@ export class DashboardSaveModal extends React.Component<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
onDescriptionChange = (event: any) => {
|
||||
this.setState({
|
||||
description: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onTimeRestoreChange = (event: any) => {
|
||||
this.setState({
|
||||
timeRestore: event.target.checked,
|
||||
|
@ -102,26 +96,12 @@ export class DashboardSaveModal extends React.Component<Props, State> {
|
|||
tags,
|
||||
});
|
||||
}}
|
||||
markOptional
|
||||
/>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="dashboard.topNav.saveModal.descriptionFormRowLabel"
|
||||
defaultMessage="Description"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="dashboardDescription"
|
||||
value={this.state.description}
|
||||
onChange={this.onDescriptionChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{tagSelector}
|
||||
|
||||
<EuiFormRow
|
||||
|
@ -154,13 +134,14 @@ export class DashboardSaveModal extends React.Component<Props, State> {
|
|||
onSave={this.saveDashboard}
|
||||
onClose={this.props.onClose}
|
||||
title={this.props.title}
|
||||
description={this.props.description}
|
||||
showDescription
|
||||
showCopyOnSave={this.props.showCopyOnSave}
|
||||
initialCopyOnSave={this.props.showCopyOnSave}
|
||||
objectType={i18n.translate('dashboard.topNav.saveModal.objectType', {
|
||||
defaultMessage: 'dashboard',
|
||||
})}
|
||||
options={this.renderDashboardSaveOptions()}
|
||||
showDescription={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,3 +23,5 @@ export const AvailableAnnotationIcons = {
|
|||
TAG: 'tag',
|
||||
TRIANGLE: 'triangle',
|
||||
} as const;
|
||||
|
||||
export const EVENT_ANNOTATION_GROUP_TYPE = 'event-annotation-group';
|
||||
|
|
|
@ -33,4 +33,7 @@ export type {
|
|||
QueryPointEventAnnotationConfig,
|
||||
AvailableAnnotationIcon,
|
||||
EventAnnotationOutput,
|
||||
EventAnnotationGroupAttributes,
|
||||
} from './types';
|
||||
|
||||
export { EVENT_ANNOTATION_GROUP_TYPE } from './constants';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { KibanaQueryOutput } from '@kbn/data-plugin/common';
|
||||
import { DataViewSpec, KibanaQueryOutput } from '@kbn/data-plugin/common';
|
||||
import { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { $Values } from '@kbn/utility-types';
|
||||
import { AvailableAnnotationIcons } from './constants';
|
||||
|
@ -82,10 +82,23 @@ export type EventAnnotationConfig =
|
|||
| RangeEventAnnotationConfig
|
||||
| QueryPointEventAnnotationConfig;
|
||||
|
||||
export interface EventAnnotationGroupAttributes {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
ignoreGlobalFilters: boolean;
|
||||
annotations: EventAnnotationConfig[];
|
||||
dataViewSpec?: DataViewSpec;
|
||||
}
|
||||
|
||||
export interface EventAnnotationGroupConfig {
|
||||
annotations: EventAnnotationConfig[];
|
||||
indexPatternId: string;
|
||||
ignoreGlobalFilters?: boolean;
|
||||
ignoreGlobalFilters: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
dataViewSpec?: DataViewSpec;
|
||||
}
|
||||
|
||||
export type EventAnnotationArgs =
|
||||
|
|
|
@ -9,7 +9,12 @@
|
|||
"browser": true,
|
||||
"requiredPlugins": [
|
||||
"expressions",
|
||||
"data"
|
||||
"savedObjectsManagement",
|
||||
"data",
|
||||
],
|
||||
"requiredBundles": [
|
||||
"savedObjectsFinder",
|
||||
"dataViews"
|
||||
],
|
||||
"extraPublicDirs": [
|
||||
"common"
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
import type { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public';
|
||||
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { EVENT_ANNOTATION_GROUP_TYPE } from '../../common';
|
||||
|
||||
export const EventAnnotationGroupSavedObjectFinder = ({
|
||||
uiSettings,
|
||||
http,
|
||||
savedObjectsManagement,
|
||||
fixedPageSize = 10,
|
||||
checkHasAnnotationGroups,
|
||||
onChoose,
|
||||
onCreateNew,
|
||||
}: {
|
||||
uiSettings: IUiSettingsClient;
|
||||
http: CoreStart['http'];
|
||||
savedObjectsManagement: SavedObjectsManagementPluginStart;
|
||||
fixedPageSize?: number;
|
||||
checkHasAnnotationGroups: () => Promise<boolean>;
|
||||
onChoose: (value: {
|
||||
id: string;
|
||||
type: string;
|
||||
fullName: string;
|
||||
savedObject: SavedObjectCommon<unknown>;
|
||||
}) => void;
|
||||
onCreateNew: () => void;
|
||||
}) => {
|
||||
const [hasAnnotationGroups, setHasAnnotationGroups] = useState<boolean | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
checkHasAnnotationGroups().then(setHasAnnotationGroups);
|
||||
}, [checkHasAnnotationGroups]);
|
||||
|
||||
return hasAnnotationGroups === undefined ? (
|
||||
<EuiFlexGroup responsive={false} justifyContent="center">
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : hasAnnotationGroups === false ? (
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
height: 100%;
|
||||
`}
|
||||
direction="column"
|
||||
justifyContent="center"
|
||||
>
|
||||
<EuiEmptyPrompt
|
||||
titleSize="xs"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="eventAnnotation.eventAnnotationGroup.savedObjectFinder.emptyPromptTitle"
|
||||
defaultMessage="Start by adding an annotation layer"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="eventAnnotation.eventAnnotationGroup.savedObjectFinder.emptyPromptDescription"
|
||||
defaultMessage="There are currently no annotations available to select from the library. Create a new layer to add annotations."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
}
|
||||
actions={
|
||||
<EuiButton onClick={() => onCreateNew()} size="s">
|
||||
<FormattedMessage
|
||||
id="eventAnnotation.eventAnnotationGroup.savedObjectFinder.emptyCTA"
|
||||
defaultMessage="Create annotation layer"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<SavedObjectFinder
|
||||
key="searchSavedObjectFinder"
|
||||
fixedPageSize={fixedPageSize}
|
||||
onChoose={(id, type, fullName, savedObject) => {
|
||||
onChoose({ id, type, fullName, savedObject });
|
||||
}}
|
||||
showFilter={false}
|
||||
noItemsMessage={
|
||||
<FormattedMessage
|
||||
id="eventAnnotation.eventAnnotationGroup.savedObjectFinder.notFoundLabel"
|
||||
defaultMessage="No matching annotation groups found."
|
||||
/>
|
||||
}
|
||||
savedObjectMetaData={savedObjectMetaData}
|
||||
services={{
|
||||
uiSettings,
|
||||
http,
|
||||
savedObjectsManagement,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const savedObjectMetaData = [
|
||||
{
|
||||
type: EVENT_ANNOTATION_GROUP_TYPE,
|
||||
getIconForSavedObject: () => 'annotation',
|
||||
name: i18n.translate('eventAnnotation.eventAnnotationGroup.metadata.name', {
|
||||
defaultMessage: 'Annotations Groups',
|
||||
}),
|
||||
includeFields: ['*'],
|
||||
},
|
||||
];
|
13
src/plugins/event_annotation/public/event_annotation_service/__snapshots__/service.test.ts.snap
generated
Normal file
13
src/plugins/event_annotation/public/event_annotation_service/__snapshots__/service.test.ts.snap
generated
Normal file
|
@ -0,0 +1,13 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Event Annotation Service loadAnnotationGroup should properly load an annotation group with a multiple annotation 1`] = `
|
||||
Object {
|
||||
"annotations": undefined,
|
||||
"dataViewSpec": undefined,
|
||||
"description": undefined,
|
||||
"ignoreGlobalFilters": undefined,
|
||||
"indexPatternId": "ipid",
|
||||
"tags": undefined,
|
||||
"title": "groupTitle",
|
||||
}
|
||||
`;
|
|
@ -6,14 +6,28 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
|
||||
import { EventAnnotationServiceType } from './types';
|
||||
|
||||
export class EventAnnotationService {
|
||||
private eventAnnotationService?: EventAnnotationServiceType;
|
||||
|
||||
private core: CoreStart;
|
||||
private savedObjectsManagement: SavedObjectsManagementPluginStart;
|
||||
|
||||
constructor(core: CoreStart, savedObjectsManagement: SavedObjectsManagementPluginStart) {
|
||||
this.core = core;
|
||||
this.savedObjectsManagement = savedObjectsManagement;
|
||||
}
|
||||
|
||||
public async getService() {
|
||||
if (!this.eventAnnotationService) {
|
||||
const { getEventAnnotationService } = await import('./service');
|
||||
this.eventAnnotationService = getEventAnnotationService();
|
||||
this.eventAnnotationService = getEventAnnotationService(
|
||||
this.core,
|
||||
this.savedObjectsManagement
|
||||
);
|
||||
}
|
||||
return this.eventAnnotationService;
|
||||
}
|
||||
|
|
|
@ -6,14 +6,150 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { CoreStart, SimpleSavedObject } from '@kbn/core/public';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
|
||||
import { EventAnnotationConfig, EventAnnotationGroupAttributes } from '../../common';
|
||||
import { getEventAnnotationService } from './service';
|
||||
import { EventAnnotationServiceType } from './types';
|
||||
|
||||
type AnnotationGroupSavedObject = SimpleSavedObject<EventAnnotationGroupAttributes>;
|
||||
|
||||
const annotationGroupResolveMocks: Record<string, AnnotationGroupSavedObject> = {
|
||||
nonExistingGroup: {
|
||||
attributes: {} as EventAnnotationGroupAttributes,
|
||||
references: [],
|
||||
id: 'nonExistingGroup',
|
||||
error: {
|
||||
error: 'Saved object not found',
|
||||
statusCode: 404,
|
||||
message: 'Not found',
|
||||
},
|
||||
} as Partial<AnnotationGroupSavedObject> as AnnotationGroupSavedObject,
|
||||
noAnnotations: {
|
||||
attributes: {
|
||||
title: 'groupTitle',
|
||||
description: '',
|
||||
tags: [],
|
||||
ignoreGlobalFilters: false,
|
||||
annotations: [],
|
||||
},
|
||||
type: 'event-annotation-group',
|
||||
references: [
|
||||
{
|
||||
id: 'ipid',
|
||||
name: 'ipid',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
} as Partial<AnnotationGroupSavedObject> as AnnotationGroupSavedObject,
|
||||
multiAnnotations: {
|
||||
attributes: {
|
||||
title: 'groupTitle',
|
||||
},
|
||||
id: 'multiAnnotations',
|
||||
type: 'event-annotation-group',
|
||||
references: [
|
||||
{
|
||||
id: 'ipid',
|
||||
name: 'ipid',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
} as Partial<AnnotationGroupSavedObject> as AnnotationGroupSavedObject,
|
||||
withAdHocDataView: {
|
||||
attributes: {
|
||||
title: 'groupTitle',
|
||||
dataViewSpec: {
|
||||
id: 'my-id',
|
||||
},
|
||||
} as Partial<EventAnnotationGroupAttributes>,
|
||||
id: 'multiAnnotations',
|
||||
type: 'event-annotation-group',
|
||||
references: [],
|
||||
} as Partial<AnnotationGroupSavedObject> as AnnotationGroupSavedObject,
|
||||
};
|
||||
|
||||
const annotationResolveMocks = {
|
||||
nonExistingGroup: { savedObjects: [] },
|
||||
noAnnotations: { savedObjects: [] },
|
||||
multiAnnotations: {
|
||||
savedObjects: [
|
||||
{
|
||||
id: 'annotation1',
|
||||
attributes: {
|
||||
id: 'annotation1',
|
||||
type: 'manual',
|
||||
key: { type: 'point_in_time' as const, timestamp: '2022-03-18T08:25:00.000Z' },
|
||||
label: 'Event',
|
||||
icon: 'triangle' as const,
|
||||
color: 'red',
|
||||
lineStyle: 'dashed' as const,
|
||||
lineWidth: 3,
|
||||
} as EventAnnotationConfig,
|
||||
type: 'event-annotation',
|
||||
references: [
|
||||
{
|
||||
id: 'multiAnnotations',
|
||||
name: 'event_annotation_group_ref',
|
||||
type: 'event-annotation-group',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'annotation2',
|
||||
attributes: {
|
||||
id: 'ann2',
|
||||
label: 'Query based event',
|
||||
icon: 'triangle',
|
||||
color: 'red',
|
||||
type: 'query',
|
||||
timeField: 'timestamp',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
},
|
||||
lineStyle: 'dashed',
|
||||
lineWidth: 3,
|
||||
filter: { type: 'kibana_query', query: '', language: 'kuery' },
|
||||
} as EventAnnotationConfig,
|
||||
type: 'event-annotation',
|
||||
references: [
|
||||
{
|
||||
id: 'multiAnnotations',
|
||||
name: 'event_annotation_group_ref',
|
||||
type: 'event-annotation-group',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
let core: CoreStart;
|
||||
|
||||
describe('Event Annotation Service', () => {
|
||||
let eventAnnotationService: EventAnnotationServiceType;
|
||||
beforeAll(() => {
|
||||
eventAnnotationService = getEventAnnotationService();
|
||||
beforeEach(() => {
|
||||
core = coreMock.createStart();
|
||||
(core.savedObjects.client.create as jest.Mock).mockImplementation(() => {
|
||||
return annotationGroupResolveMocks.multiAnnotations;
|
||||
});
|
||||
(core.savedObjects.client.get as jest.Mock).mockImplementation((_type, id) => {
|
||||
const typedId = id as keyof typeof annotationGroupResolveMocks;
|
||||
return annotationGroupResolveMocks[typedId];
|
||||
});
|
||||
(core.savedObjects.client.bulkCreate as jest.Mock).mockImplementation(() => {
|
||||
return annotationResolveMocks.multiAnnotations;
|
||||
});
|
||||
eventAnnotationService = getEventAnnotationService(
|
||||
core,
|
||||
{} as SavedObjectsManagementPluginStart
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('toExpression', () => {
|
||||
it('should work for an empty list', () => {
|
||||
expect(eventAnnotationService.toExpression([])).toEqual([]);
|
||||
|
@ -318,4 +454,190 @@ describe('Event Annotation Service', () => {
|
|||
}
|
||||
);
|
||||
});
|
||||
describe('loadAnnotationGroup', () => {
|
||||
it('should throw error when loading group doesnt exist', async () => {
|
||||
expect(() => eventAnnotationService.loadAnnotationGroup('nonExistingGroup')).rejects
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": "Saved object not found",
|
||||
"message": "Not found",
|
||||
"statusCode": 404,
|
||||
}
|
||||
`);
|
||||
});
|
||||
it('should properly load an annotation group with no annotation', async () => {
|
||||
expect(await eventAnnotationService.loadAnnotationGroup('noAnnotations'))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"annotations": Array [],
|
||||
"dataViewSpec": undefined,
|
||||
"description": "",
|
||||
"ignoreGlobalFilters": false,
|
||||
"indexPatternId": "ipid",
|
||||
"tags": Array [],
|
||||
"title": "groupTitle",
|
||||
}
|
||||
`);
|
||||
});
|
||||
it('should properly load an annotation group with a multiple annotation', async () => {
|
||||
expect(
|
||||
await eventAnnotationService.loadAnnotationGroup('multiAnnotations')
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
it('populates id if group has ad-hoc data view', async () => {
|
||||
const group = await eventAnnotationService.loadAnnotationGroup('withAdHocDataView');
|
||||
|
||||
expect(group.indexPatternId).toBe(group.dataViewSpec?.id);
|
||||
});
|
||||
});
|
||||
// describe.skip('deleteAnnotationGroup', () => {
|
||||
// it('deletes annotation group along with annotations that reference them', async () => {
|
||||
// await eventAnnotationService.deleteAnnotationGroup('multiAnnotations');
|
||||
// expect(core.savedObjects.client.bulkDelete).toHaveBeenCalledWith([
|
||||
// { id: 'multiAnnotations', type: 'event-annotation-group' },
|
||||
// { id: 'annotation1', type: 'event-annotation' },
|
||||
// { id: 'annotation2', type: 'event-annotation' },
|
||||
// ]);
|
||||
// });
|
||||
// });
|
||||
describe('createAnnotationGroup', () => {
|
||||
it('creates annotation group along with annotations', async () => {
|
||||
const annotations = [
|
||||
annotationResolveMocks.multiAnnotations.savedObjects[0].attributes,
|
||||
annotationResolveMocks.multiAnnotations.savedObjects[1].attributes,
|
||||
];
|
||||
await eventAnnotationService.createAnnotationGroup({
|
||||
title: 'newGroupTitle',
|
||||
description: 'my description',
|
||||
tags: ['my', 'many', 'tags'],
|
||||
indexPatternId: 'ipid',
|
||||
ignoreGlobalFilters: false,
|
||||
annotations,
|
||||
});
|
||||
expect(core.savedObjects.client.create).toHaveBeenCalledWith(
|
||||
'event-annotation-group',
|
||||
{
|
||||
title: 'newGroupTitle',
|
||||
description: 'my description',
|
||||
tags: ['my', 'many', 'tags'],
|
||||
ignoreGlobalFilters: false,
|
||||
dataViewSpec: null,
|
||||
annotations,
|
||||
},
|
||||
{
|
||||
references: [
|
||||
{
|
||||
id: 'ipid',
|
||||
name: 'event-annotation-group_dataView-ref-ipid',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('updateAnnotationGroup', () => {
|
||||
it('updates annotation group attributes', async () => {
|
||||
await eventAnnotationService.updateAnnotationGroup(
|
||||
{
|
||||
title: 'newTitle',
|
||||
description: '',
|
||||
tags: [],
|
||||
indexPatternId: 'newId',
|
||||
annotations: [],
|
||||
ignoreGlobalFilters: false,
|
||||
},
|
||||
'multiAnnotations'
|
||||
);
|
||||
expect(core.savedObjects.client.update).toHaveBeenCalledWith(
|
||||
'event-annotation-group',
|
||||
'multiAnnotations',
|
||||
{
|
||||
title: 'newTitle',
|
||||
description: '',
|
||||
tags: [],
|
||||
annotations: [],
|
||||
dataViewSpec: null,
|
||||
ignoreGlobalFilters: false,
|
||||
},
|
||||
{
|
||||
references: [
|
||||
{
|
||||
id: 'newId',
|
||||
name: 'event-annotation-group_dataView-ref-newId',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
// describe.skip('updateAnnotations', () => {
|
||||
// const upsert = [
|
||||
// {
|
||||
// id: 'annotation2',
|
||||
// label: 'Query based event',
|
||||
// icon: 'triangle',
|
||||
// color: 'red',
|
||||
// type: 'query',
|
||||
// timeField: 'timestamp',
|
||||
// key: {
|
||||
// type: 'point_in_time',
|
||||
// },
|
||||
// lineStyle: 'dashed',
|
||||
// lineWidth: 3,
|
||||
// filter: { type: 'kibana_query', query: '', language: 'kuery' },
|
||||
// },
|
||||
// {
|
||||
// id: 'annotation4',
|
||||
// label: 'Query based event',
|
||||
// type: 'query',
|
||||
// timeField: 'timestamp',
|
||||
// key: {
|
||||
// type: 'point_in_time',
|
||||
// },
|
||||
// filter: { type: 'kibana_query', query: '', language: 'kuery' },
|
||||
// },
|
||||
// ] as EventAnnotationConfig[];
|
||||
// it('updates annotations - deletes annotations', async () => {
|
||||
// await eventAnnotationService.updateAnnotations('multiAnnotations', {
|
||||
// delete: ['annotation1', 'annotation2'],
|
||||
// });
|
||||
// expect(core.savedObjects.client.bulkDelete).toHaveBeenCalledWith([
|
||||
// { id: 'annotation1', type: 'event-annotation' },
|
||||
// { id: 'annotation2', type: 'event-annotation' },
|
||||
// ]);
|
||||
// });
|
||||
// it('updates annotations - inserts new annotations', async () => {
|
||||
// await eventAnnotationService.updateAnnotations('multiAnnotations', { upsert });
|
||||
// expect(core.savedObjects.client.bulkCreate).toHaveBeenCalledWith([
|
||||
// {
|
||||
// id: 'annotation2',
|
||||
// type: 'event-annotation',
|
||||
// attributes: upsert[0],
|
||||
// overwrite: true,
|
||||
// references: [
|
||||
// {
|
||||
// id: 'multiAnnotations',
|
||||
// name: 'event-annotation-group-ref-annotation2',
|
||||
// type: 'event-annotation-group',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// id: 'annotation4',
|
||||
// type: 'event-annotation',
|
||||
// attributes: upsert[1],
|
||||
// overwrite: true,
|
||||
// references: [
|
||||
// {
|
||||
// id: 'multiAnnotations',
|
||||
// name: 'event-annotation-group-ref-annotation4',
|
||||
// type: 'event-annotation-group',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ]);
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
|
|
@ -6,10 +6,19 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { partition } from 'lodash';
|
||||
import { queryToAst } from '@kbn/data-plugin/common';
|
||||
import { ExpressionAstExpression } from '@kbn/expressions-plugin/common';
|
||||
import { EventAnnotationConfig } from '../../common';
|
||||
import { CoreStart, SavedObjectReference, SavedObjectsClientContract } from '@kbn/core/public';
|
||||
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
|
||||
import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common';
|
||||
import {
|
||||
EventAnnotationConfig,
|
||||
EventAnnotationGroupAttributes,
|
||||
EventAnnotationGroupConfig,
|
||||
EVENT_ANNOTATION_GROUP_TYPE,
|
||||
} from '../../common';
|
||||
import { EventAnnotationServiceType } from './types';
|
||||
import {
|
||||
defaultAnnotationColor,
|
||||
|
@ -18,108 +27,138 @@ import {
|
|||
isRangeAnnotationConfig,
|
||||
isQueryAnnotationConfig,
|
||||
} from './helpers';
|
||||
import { EventAnnotationGroupSavedObjectFinder } from '../components/event_annotation_group_saved_object_finder';
|
||||
|
||||
export function hasIcon(icon: string | undefined): icon is string {
|
||||
return icon != null && icon !== 'empty';
|
||||
}
|
||||
|
||||
export function getEventAnnotationService(): EventAnnotationServiceType {
|
||||
const annotationsToExpression = (annotations: EventAnnotationConfig[]) => {
|
||||
const [queryBasedAnnotations, manualBasedAnnotations] = partition(
|
||||
annotations,
|
||||
isQueryAnnotationConfig
|
||||
export function getEventAnnotationService(
|
||||
core: CoreStart,
|
||||
savedObjectsManagement: SavedObjectsManagementPluginStart
|
||||
): EventAnnotationServiceType {
|
||||
const client: SavedObjectsClientContract = core.savedObjects.client;
|
||||
|
||||
const loadAnnotationGroup = async (
|
||||
savedObjectId: string
|
||||
): Promise<EventAnnotationGroupConfig> => {
|
||||
const savedObject = await client.get<EventAnnotationGroupAttributes>(
|
||||
EVENT_ANNOTATION_GROUP_TYPE,
|
||||
savedObjectId
|
||||
);
|
||||
|
||||
const expressions = [];
|
||||
|
||||
for (const annotation of manualBasedAnnotations) {
|
||||
if (isRangeAnnotationConfig(annotation)) {
|
||||
const { label, color, key, outside, id } = annotation;
|
||||
const { timestamp: time, endTimestamp: endTime } = key;
|
||||
expressions.push({
|
||||
type: 'expression' as const,
|
||||
chain: [
|
||||
{
|
||||
type: 'function' as const,
|
||||
function: 'manual_range_event_annotation',
|
||||
arguments: {
|
||||
id: [id],
|
||||
time: [time],
|
||||
endTime: [endTime],
|
||||
label: [label || defaultAnnotationLabel],
|
||||
color: [color || defaultAnnotationRangeColor],
|
||||
outside: [Boolean(outside)],
|
||||
isHidden: [Boolean(annotation.isHidden)],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
const { label, color, lineStyle, lineWidth, icon, key, textVisibility, id } = annotation;
|
||||
expressions.push({
|
||||
type: 'expression' as const,
|
||||
chain: [
|
||||
{
|
||||
type: 'function' as const,
|
||||
function: 'manual_point_event_annotation',
|
||||
arguments: {
|
||||
id: [id],
|
||||
time: [key.timestamp],
|
||||
label: [label || defaultAnnotationLabel],
|
||||
color: [color || defaultAnnotationColor],
|
||||
lineWidth: [lineWidth || 1],
|
||||
lineStyle: [lineStyle || 'solid'],
|
||||
icon: hasIcon(icon) ? [icon] : ['triangle'],
|
||||
textVisibility: [textVisibility || false],
|
||||
isHidden: [Boolean(annotation.isHidden)],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (savedObject.error) {
|
||||
throw savedObject.error;
|
||||
}
|
||||
|
||||
for (const annotation of queryBasedAnnotations) {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
color,
|
||||
lineStyle,
|
||||
lineWidth,
|
||||
icon,
|
||||
timeField,
|
||||
textVisibility,
|
||||
textField,
|
||||
filter,
|
||||
extraFields,
|
||||
} = annotation;
|
||||
expressions.push({
|
||||
type: 'expression' as const,
|
||||
chain: [
|
||||
{
|
||||
type: 'function' as const,
|
||||
function: 'query_point_event_annotation',
|
||||
arguments: {
|
||||
id: [id],
|
||||
timeField: timeField ? [timeField] : [],
|
||||
label: [label || defaultAnnotationLabel],
|
||||
color: [color || defaultAnnotationColor],
|
||||
lineWidth: [lineWidth || 1],
|
||||
lineStyle: [lineStyle || 'solid'],
|
||||
icon: hasIcon(icon) ? [icon] : ['triangle'],
|
||||
textVisibility: [textVisibility || false],
|
||||
textField: textVisibility && textField ? [textField] : [],
|
||||
filter: filter ? [queryToAst(filter)] : [],
|
||||
extraFields: extraFields || [],
|
||||
isHidden: [Boolean(annotation.isHidden)],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return expressions;
|
||||
const adHocDataViewSpec = savedObject.attributes.dataViewSpec
|
||||
? DataViewPersistableStateService.inject(
|
||||
savedObject.attributes.dataViewSpec,
|
||||
savedObject.references
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
title: savedObject.attributes.title,
|
||||
description: savedObject.attributes.description,
|
||||
tags: savedObject.attributes.tags,
|
||||
ignoreGlobalFilters: savedObject.attributes.ignoreGlobalFilters,
|
||||
indexPatternId: adHocDataViewSpec
|
||||
? adHocDataViewSpec.id!
|
||||
: savedObject.references.find((ref) => ref.type === 'index-pattern')?.id!,
|
||||
annotations: savedObject.attributes.annotations,
|
||||
dataViewSpec: adHocDataViewSpec,
|
||||
};
|
||||
};
|
||||
|
||||
const extractDataViewInformation = (group: EventAnnotationGroupConfig) => {
|
||||
let { dataViewSpec = null } = group;
|
||||
|
||||
let references: SavedObjectReference[];
|
||||
|
||||
if (dataViewSpec) {
|
||||
if (!dataViewSpec.id)
|
||||
throw new Error(
|
||||
'tried to create annotation group with a data view spec that did not include an ID!'
|
||||
);
|
||||
|
||||
const { state, references: refsFromDataView } =
|
||||
DataViewPersistableStateService.extract(dataViewSpec);
|
||||
dataViewSpec = state;
|
||||
references = refsFromDataView;
|
||||
} else {
|
||||
references = [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: group.indexPatternId,
|
||||
name: `event-annotation-group_dataView-ref-${group.indexPatternId}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return { references, dataViewSpec };
|
||||
};
|
||||
|
||||
const createAnnotationGroup = async (
|
||||
group: EventAnnotationGroupConfig
|
||||
): Promise<{ id: string }> => {
|
||||
const { references, dataViewSpec } = extractDataViewInformation(group);
|
||||
const { title, description, tags, ignoreGlobalFilters, annotations } = group;
|
||||
|
||||
const groupSavedObjectId = (
|
||||
await client.create(
|
||||
EVENT_ANNOTATION_GROUP_TYPE,
|
||||
{ title, description, tags, ignoreGlobalFilters, annotations, dataViewSpec },
|
||||
{
|
||||
references,
|
||||
}
|
||||
)
|
||||
).id;
|
||||
|
||||
return { id: groupSavedObjectId };
|
||||
};
|
||||
|
||||
const updateAnnotationGroup = async (
|
||||
group: EventAnnotationGroupConfig,
|
||||
annotationGroupId: string
|
||||
): Promise<void> => {
|
||||
const { references, dataViewSpec } = extractDataViewInformation(group);
|
||||
const { title, description, tags, ignoreGlobalFilters, annotations } = group;
|
||||
|
||||
await client.update(
|
||||
EVENT_ANNOTATION_GROUP_TYPE,
|
||||
annotationGroupId,
|
||||
{ title, description, tags, ignoreGlobalFilters, annotations, dataViewSpec },
|
||||
{
|
||||
references,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const checkHasAnnotationGroups = async (): Promise<boolean> => {
|
||||
const response = await client.find({
|
||||
type: EVENT_ANNOTATION_GROUP_TYPE,
|
||||
perPage: 0,
|
||||
});
|
||||
|
||||
return response.total > 0;
|
||||
};
|
||||
|
||||
return {
|
||||
loadAnnotationGroup,
|
||||
updateAnnotationGroup,
|
||||
createAnnotationGroup,
|
||||
renderEventAnnotationGroupSavedObjectFinder: (props) => {
|
||||
return (
|
||||
<EventAnnotationGroupSavedObjectFinder
|
||||
http={core.http}
|
||||
uiSettings={core.uiSettings}
|
||||
savedObjectsManagement={savedObjectsManagement}
|
||||
checkHasAnnotationGroups={checkHasAnnotationGroups}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
toExpression: annotationsToExpression,
|
||||
toFetchExpression: ({ interval, groups }) => {
|
||||
if (groups.length === 0) {
|
||||
|
@ -177,3 +216,99 @@ export function getEventAnnotationService(): EventAnnotationServiceType {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
const annotationsToExpression = (annotations: EventAnnotationConfig[]) => {
|
||||
const [queryBasedAnnotations, manualBasedAnnotations] = partition(
|
||||
annotations,
|
||||
isQueryAnnotationConfig
|
||||
);
|
||||
|
||||
const expressions = [];
|
||||
|
||||
for (const annotation of manualBasedAnnotations) {
|
||||
if (isRangeAnnotationConfig(annotation)) {
|
||||
const { label, color, key, outside, id } = annotation;
|
||||
const { timestamp: time, endTimestamp: endTime } = key;
|
||||
expressions.push({
|
||||
type: 'expression' as const,
|
||||
chain: [
|
||||
{
|
||||
type: 'function' as const,
|
||||
function: 'manual_range_event_annotation',
|
||||
arguments: {
|
||||
id: [id],
|
||||
time: [time],
|
||||
endTime: [endTime],
|
||||
label: [label || defaultAnnotationLabel],
|
||||
color: [color || defaultAnnotationRangeColor],
|
||||
outside: [Boolean(outside)],
|
||||
isHidden: [Boolean(annotation.isHidden)],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
const { label, color, lineStyle, lineWidth, icon, key, textVisibility, id } = annotation;
|
||||
expressions.push({
|
||||
type: 'expression' as const,
|
||||
chain: [
|
||||
{
|
||||
type: 'function' as const,
|
||||
function: 'manual_point_event_annotation',
|
||||
arguments: {
|
||||
id: [id],
|
||||
time: [key.timestamp],
|
||||
label: [label || defaultAnnotationLabel],
|
||||
color: [color || defaultAnnotationColor],
|
||||
lineWidth: [lineWidth || 1],
|
||||
lineStyle: [lineStyle || 'solid'],
|
||||
icon: hasIcon(icon) ? [icon] : ['triangle'],
|
||||
textVisibility: [textVisibility || false],
|
||||
isHidden: [Boolean(annotation.isHidden)],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const annotation of queryBasedAnnotations) {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
color,
|
||||
lineStyle,
|
||||
lineWidth,
|
||||
icon,
|
||||
timeField,
|
||||
textVisibility,
|
||||
textField,
|
||||
filter,
|
||||
extraFields,
|
||||
} = annotation;
|
||||
expressions.push({
|
||||
type: 'expression' as const,
|
||||
chain: [
|
||||
{
|
||||
type: 'function' as const,
|
||||
function: 'query_point_event_annotation',
|
||||
arguments: {
|
||||
id: [id],
|
||||
timeField: timeField ? [timeField] : [],
|
||||
label: [label || defaultAnnotationLabel],
|
||||
color: [color || defaultAnnotationColor],
|
||||
lineWidth: [lineWidth || 1],
|
||||
lineStyle: [lineStyle || 'solid'],
|
||||
icon: hasIcon(icon) ? [icon] : ['triangle'],
|
||||
textVisibility: [textVisibility || false],
|
||||
textField: textVisibility && textField ? [textField] : [],
|
||||
filter: filter ? [queryToAst(filter)] : [],
|
||||
extraFields: extraFields || [],
|
||||
isHidden: [Boolean(annotation.isHidden)],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return expressions;
|
||||
};
|
||||
|
|
|
@ -7,12 +7,31 @@
|
|||
*/
|
||||
|
||||
import { ExpressionAstExpression } from '@kbn/expressions-plugin/common/ast';
|
||||
import type { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import { EventAnnotationConfig, EventAnnotationGroupConfig } from '../../common';
|
||||
|
||||
export interface EventAnnotationServiceType {
|
||||
loadAnnotationGroup: (savedObjectId: string) => Promise<EventAnnotationGroupConfig>;
|
||||
createAnnotationGroup: (group: EventAnnotationGroupConfig) => Promise<{ id: string }>;
|
||||
updateAnnotationGroup: (
|
||||
group: EventAnnotationGroupConfig,
|
||||
savedObjectId: string
|
||||
) => Promise<void>;
|
||||
toExpression: (props: EventAnnotationConfig[]) => ExpressionAstExpression[];
|
||||
toFetchExpression: (props: {
|
||||
interval: string;
|
||||
groups: EventAnnotationGroupConfig[];
|
||||
groups: Array<
|
||||
Pick<EventAnnotationGroupConfig, 'annotations' | 'ignoreGlobalFilters' | 'indexPatternId'>
|
||||
>;
|
||||
}) => ExpressionAstExpression[];
|
||||
renderEventAnnotationGroupSavedObjectFinder: (props: {
|
||||
fixedPageSize?: number;
|
||||
onChoose: (value: {
|
||||
id: string;
|
||||
type: string;
|
||||
fullName: string;
|
||||
savedObject: SavedObjectCommon<unknown>;
|
||||
}) => void;
|
||||
onCreateNew: () => void;
|
||||
}) => JSX.Element;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
|
||||
import { getEventAnnotationService } from './event_annotation_service/service';
|
||||
|
||||
// not really mocking but avoiding async loading
|
||||
export const eventAnnotationServiceMock = getEventAnnotationService();
|
||||
export const eventAnnotationServiceMock = getEventAnnotationService(
|
||||
coreMock.createStart(),
|
||||
{} as SavedObjectsManagementPluginStart
|
||||
);
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup, CoreStart, IUiSettingsClient } from '@kbn/core/public';
|
||||
import { Plugin, CoreSetup, CoreStart } from '@kbn/core/public';
|
||||
import { ExpressionsSetup } from '@kbn/expressions-plugin/public';
|
||||
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { EventAnnotationService } from './event_annotation_service';
|
||||
import {
|
||||
|
@ -19,8 +20,8 @@ import {
|
|||
import { getFetchEventAnnotations } from './fetch_event_annotations';
|
||||
|
||||
export interface EventAnnotationStartDependencies {
|
||||
savedObjectsManagement: SavedObjectsManagementPluginStart;
|
||||
data: DataPublicPluginStart;
|
||||
uiSettings: IUiSettingsClient;
|
||||
}
|
||||
|
||||
interface SetupDependencies {
|
||||
|
@ -29,14 +30,12 @@ interface SetupDependencies {
|
|||
|
||||
/** @public */
|
||||
export type EventAnnotationPluginStart = EventAnnotationService;
|
||||
export type EventAnnotationPluginSetup = EventAnnotationService;
|
||||
export type EventAnnotationPluginSetup = void;
|
||||
|
||||
/** @public */
|
||||
export class EventAnnotationPlugin
|
||||
implements Plugin<EventAnnotationPluginSetup, EventAnnotationService>
|
||||
{
|
||||
private readonly eventAnnotationService = new EventAnnotationService();
|
||||
|
||||
public setup(
|
||||
core: CoreSetup<EventAnnotationStartDependencies, EventAnnotationService>,
|
||||
dependencies: SetupDependencies
|
||||
|
@ -48,13 +47,12 @@ export class EventAnnotationPlugin
|
|||
dependencies.expressions.registerFunction(
|
||||
getFetchEventAnnotations({ getStartServices: core.getStartServices })
|
||||
);
|
||||
return this.eventAnnotationService;
|
||||
}
|
||||
|
||||
public start(
|
||||
core: CoreStart,
|
||||
startDependencies: EventAnnotationStartDependencies
|
||||
): EventAnnotationService {
|
||||
return this.eventAnnotationService;
|
||||
return new EventAnnotationService(core, startDependencies.savedObjectsManagement);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
manualRangeEventAnnotation,
|
||||
queryPointEventAnnotation,
|
||||
} from '../common';
|
||||
import { setupSavedObjects } from './saved_objects';
|
||||
// import { getFetchEventAnnotations } from './fetch_event_annotations';
|
||||
|
||||
interface SetupDependencies {
|
||||
|
@ -33,9 +34,8 @@ export class EventAnnotationServerPlugin implements Plugin<object, object> {
|
|||
dependencies.expressions.registerFunction(manualRangeEventAnnotation);
|
||||
dependencies.expressions.registerFunction(queryPointEventAnnotation);
|
||||
dependencies.expressions.registerFunction(eventAnnotationGroup);
|
||||
// dependencies.expressions.registerFunction(
|
||||
// getFetchEventAnnotations({ getStartServices: core.getStartServices })
|
||||
// );
|
||||
|
||||
setupSavedObjects(core);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
|
50
src/plugins/event_annotation/server/saved_objects.ts
Normal file
50
src/plugins/event_annotation/server/saved_objects.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { ANALYTICS_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
|
||||
import {
|
||||
CoreSetup,
|
||||
mergeSavedObjectMigrationMaps,
|
||||
SavedObjectMigrationMap,
|
||||
} from '@kbn/core/server';
|
||||
|
||||
import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common';
|
||||
import { EVENT_ANNOTATION_GROUP_TYPE } from '../common/constants';
|
||||
import { EventAnnotationGroupAttributes } from '../common/types';
|
||||
|
||||
export function setupSavedObjects(coreSetup: CoreSetup) {
|
||||
coreSetup.savedObjects.registerType({
|
||||
name: EVENT_ANNOTATION_GROUP_TYPE,
|
||||
indexPattern: ANALYTICS_SAVED_OBJECT_INDEX,
|
||||
hidden: false,
|
||||
namespaceType: 'multiple',
|
||||
management: {
|
||||
icon: 'flag',
|
||||
defaultSearchField: 'title',
|
||||
importableAndExportable: true,
|
||||
getTitle: (obj: { attributes: EventAnnotationGroupAttributes }) => obj.attributes.title,
|
||||
},
|
||||
migrations: () => {
|
||||
const dataViewMigrations = DataViewPersistableStateService.getAllMigrations();
|
||||
return mergeSavedObjectMigrationMaps(eventAnnotationGroupMigrations, dataViewMigrations);
|
||||
},
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
properties: {
|
||||
title: {
|
||||
type: 'text',
|
||||
},
|
||||
description: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const eventAnnotationGroupMigrations: SavedObjectMigrationMap = {};
|
|
@ -19,6 +19,10 @@
|
|||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/datemath",
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/saved-objects-finder-plugin",
|
||||
"@kbn/saved-objects-management-plugin",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/core-saved-objects-server"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -62,17 +62,45 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = `
|
|||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelAppend={
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Optional"
|
||||
id="savedObjects.saveModal.optional"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="viewDescription"
|
||||
fullWidth={true}
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiModalFooter
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "fy4vru",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
align-items: center;
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={true}
|
||||
/>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="saveCancelButton"
|
||||
onClick={[Function]}
|
||||
|
@ -88,7 +116,6 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = `
|
|||
data-test-subj="confirmSaveSavedObjectButton"
|
||||
fill={true}
|
||||
form="generated-id_form"
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
size="m"
|
||||
type="submit"
|
||||
|
@ -161,17 +188,45 @@ exports[`SavedObjectSaveModal should render matching snapshot when custom isVali
|
|||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelAppend={
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Optional"
|
||||
id="savedObjects.saveModal.optional"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="viewDescription"
|
||||
fullWidth={true}
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiModalFooter
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "fy4vru",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
align-items: center;
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={true}
|
||||
/>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="saveCancelButton"
|
||||
onClick={[Function]}
|
||||
|
@ -187,7 +242,6 @@ exports[`SavedObjectSaveModal should render matching snapshot when custom isVali
|
|||
data-test-subj="confirmSaveSavedObjectButton"
|
||||
fill={true}
|
||||
form="generated-id_form"
|
||||
isDisabled={true}
|
||||
isLoading={false}
|
||||
size="m"
|
||||
type="submit"
|
||||
|
@ -260,17 +314,45 @@ exports[`SavedObjectSaveModal should render matching snapshot when custom isVali
|
|||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelAppend={
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Optional"
|
||||
id="savedObjects.saveModal.optional"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="viewDescription"
|
||||
fullWidth={true}
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiModalFooter
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "fy4vru",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
align-items: center;
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={true}
|
||||
/>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="saveCancelButton"
|
||||
onClick={[Function]}
|
||||
|
@ -286,7 +368,6 @@ exports[`SavedObjectSaveModal should render matching snapshot when custom isVali
|
|||
data-test-subj="confirmSaveSavedObjectButton"
|
||||
fill={true}
|
||||
form="generated-id_form"
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
size="m"
|
||||
type="submit"
|
||||
|
@ -363,10 +444,23 @@ exports[`SavedObjectSaveModal should render matching snapshot when given options
|
|||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelAppend={
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Optional"
|
||||
id="savedObjects.saveModal.optional"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="viewDescription"
|
||||
fullWidth={true}
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
|
@ -383,7 +477,22 @@ exports[`SavedObjectSaveModal should render matching snapshot when given options
|
|||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiModalFooter
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "fy4vru",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
align-items: center;
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={true}
|
||||
/>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="saveCancelButton"
|
||||
onClick={[Function]}
|
||||
|
@ -399,7 +508,6 @@ exports[`SavedObjectSaveModal should render matching snapshot when given options
|
|||
data-test-subj="confirmSaveSavedObjectButton"
|
||||
fill={true}
|
||||
form="generated-id_form"
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
size="m"
|
||||
type="submit"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.kbnSavedObjectSaveModal {
|
||||
width: $euiSizeXXL * 10;
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.kbnSavedObjectsSaveModal--wide {
|
||||
width: 800px;
|
||||
}
|
||||
}
|
|
@ -30,6 +30,7 @@ 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;
|
||||
|
@ -53,6 +54,7 @@ interface Props {
|
|||
description?: string;
|
||||
showDescription: boolean;
|
||||
isValid?: boolean;
|
||||
customModalTitle?: string;
|
||||
}
|
||||
|
||||
export interface SaveModalState {
|
||||
|
@ -62,6 +64,7 @@ export interface SaveModalState {
|
|||
hasTitleDuplicate: boolean;
|
||||
isLoading: boolean;
|
||||
visualizationDescription: string;
|
||||
hasAttemptedSubmit: boolean;
|
||||
}
|
||||
|
||||
const generateId = htmlIdGenerator();
|
||||
|
@ -81,10 +84,11 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
hasTitleDuplicate: false,
|
||||
isLoading: false,
|
||||
visualizationDescription: this.props.description ? this.props.description : '',
|
||||
hasAttemptedSubmit: false,
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { isTitleDuplicateConfirmed, hasTitleDuplicate, title } = this.state;
|
||||
const { isTitleDuplicateConfirmed, hasTitleDuplicate, title, hasAttemptedSubmit } = this.state;
|
||||
const duplicateWarningId = generateId();
|
||||
|
||||
const hasColumns = !!this.props.rightOptions;
|
||||
|
@ -101,7 +105,10 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
data-test-subj="savedObjectTitle"
|
||||
value={title}
|
||||
onChange={this.onTitleChange}
|
||||
isInvalid={(!isTitleDuplicateConfirmed && hasTitleDuplicate) || title.length === 0}
|
||||
isInvalid={
|
||||
hasAttemptedSubmit &&
|
||||
((!isTitleDuplicateConfirmed && hasTitleDuplicate) || title.length === 0)
|
||||
}
|
||||
aria-describedby={this.state.hasTitleDuplicate ? duplicateWarningId : undefined}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
@ -135,11 +142,15 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
id="savedObjects.saveModal.saveTitle"
|
||||
defaultMessage="Save {objectType}"
|
||||
values={{ objectType: this.props.objectType }}
|
||||
/>
|
||||
{this.props.customModalTitle ? (
|
||||
this.props.customModalTitle
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="savedObjects.saveModal.saveTitle"
|
||||
defaultMessage="Save {objectType}"
|
||||
values={{ objectType: this.props.objectType }}
|
||||
/>
|
||||
)}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
|
@ -153,11 +164,15 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
</EuiText>
|
||||
)}
|
||||
{formBody}
|
||||
{this.renderCopyOnSave()}
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiModalFooter
|
||||
css={css`
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow>{this.renderCopyOnSave()}</EuiFlexItem>
|
||||
<EuiButtonEmpty data-test-subj="saveCancelButton" onClick={this.props.onClose}>
|
||||
<FormattedMessage
|
||||
id="savedObjects.saveModal.cancelButtonLabel"
|
||||
|
@ -179,6 +194,11 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
labelAppend={
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage id="savedObjects.saveModal.optional" defaultMessage="Optional" />
|
||||
</EuiText>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="savedObjects.saveModal.descriptionLabel"
|
||||
|
@ -187,6 +207,7 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
}
|
||||
>
|
||||
<EuiTextArea
|
||||
fullWidth
|
||||
data-test-subj="viewDescription"
|
||||
value={this.state.visualizationDescription}
|
||||
onChange={this.onDescriptionChange}
|
||||
|
@ -252,11 +273,22 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
|
||||
private onFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
this.saveSavedObject();
|
||||
|
||||
const { hasAttemptedSubmit, title } = this.state;
|
||||
|
||||
if (!hasAttemptedSubmit) {
|
||||
this.setState({ hasAttemptedSubmit: true });
|
||||
}
|
||||
|
||||
const isValid = this.props.isValid !== undefined ? this.props.isValid : true;
|
||||
|
||||
if (title.length !== 0 && isValid) {
|
||||
this.saveSavedObject();
|
||||
}
|
||||
};
|
||||
|
||||
private renderConfirmButton = () => {
|
||||
const { isLoading, title } = this.state;
|
||||
const { isLoading } = this.state;
|
||||
|
||||
let confirmLabel: string | React.ReactNode = i18n.translate(
|
||||
'savedObjects.saveModal.saveButtonLabel',
|
||||
|
@ -269,14 +301,11 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
confirmLabel = this.props.confirmButtonLabel;
|
||||
}
|
||||
|
||||
const isValid = this.props.isValid !== undefined ? this.props.isValid : true;
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
fill
|
||||
data-test-subj="confirmSaveSavedObjectButton"
|
||||
isLoading={isLoading}
|
||||
isDisabled={title.length === 0 || !isValid}
|
||||
type="submit"
|
||||
form={this.formId}
|
||||
>
|
||||
|
@ -327,21 +356,18 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiSwitch
|
||||
data-test-subj="saveAsNewCheckbox"
|
||||
checked={this.state.copyOnSave}
|
||||
onChange={this.onCopyOnSaveChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="savedObjects.saveModal.saveAsNewLabel"
|
||||
defaultMessage="Save as new {objectType}"
|
||||
values={{ objectType: this.props.objectType }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
<EuiSwitch
|
||||
data-test-subj="saveAsNewCheckbox"
|
||||
checked={this.state.copyOnSave}
|
||||
onChange={this.onCopyOnSaveChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="savedObjects.saveModal.saveAsNewLabel"
|
||||
defaultMessage="Save as new {objectType}"
|
||||
values={{ objectType: this.props.objectType }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -268,6 +268,11 @@ export type SavedObjectSaveModalTagSelectorComponentProps = EuiComboBoxProps<
|
|||
* tags selection callback
|
||||
*/
|
||||
onTagsSelected: (ids: string[]) => void;
|
||||
|
||||
/**
|
||||
* Add "Optional" to the label
|
||||
*/
|
||||
markOptional?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -544,6 +544,7 @@ export const getTopNavConfig = (
|
|||
onTagsSelected={(newSelection) => {
|
||||
selectedTags = newSelection;
|
||||
}}
|
||||
markOptional
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ interface VisualizeSaveModalArgs {
|
|||
redirectToOrigin?: boolean;
|
||||
addToDashboard?: boolean;
|
||||
dashboardId?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
type DashboardPickerOption =
|
||||
|
@ -393,10 +394,20 @@ export class VisualizePageObject extends FtrService {
|
|||
|
||||
public async setSaveModalValues(
|
||||
vizName: string,
|
||||
{ saveAsNew, redirectToOrigin, addToDashboard, dashboardId }: VisualizeSaveModalArgs = {}
|
||||
{
|
||||
saveAsNew,
|
||||
redirectToOrigin,
|
||||
addToDashboard,
|
||||
dashboardId,
|
||||
description,
|
||||
}: VisualizeSaveModalArgs = {}
|
||||
) {
|
||||
await this.testSubjects.setValue('savedObjectTitle', vizName);
|
||||
|
||||
if (description) {
|
||||
await this.testSubjects.setValue('viewDescription', description);
|
||||
}
|
||||
|
||||
const saveAsNewCheckboxExists = await this.testSubjects.exists('saveAsNewCheckbox');
|
||||
if (saveAsNewCheckboxExists) {
|
||||
const state = saveAsNew ? 'check' : 'uncheck';
|
||||
|
|
|
@ -113,6 +113,7 @@ export function App({
|
|||
isLoading,
|
||||
isSaveable,
|
||||
visualization,
|
||||
annotationGroups,
|
||||
} = useLensSelector((state) => state.lens);
|
||||
|
||||
const selectorDependencies = useMemo(
|
||||
|
@ -180,7 +181,9 @@ export function App({
|
|||
persistedDoc,
|
||||
lastKnownDoc,
|
||||
data.query.filterManager.inject.bind(data.query.filterManager),
|
||||
datasourceMap
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
annotationGroups
|
||||
) &&
|
||||
(isSaveable || persistedDoc)
|
||||
) {
|
||||
|
@ -209,6 +212,8 @@ export function App({
|
|||
application.capabilities.visualize.save,
|
||||
data.query.filterManager,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
annotationGroups,
|
||||
]);
|
||||
|
||||
const getLegacyUrlConflictCallout = useCallback(() => {
|
||||
|
@ -374,7 +379,14 @@ export function App({
|
|||
initialDocFromContext,
|
||||
persistedDoc,
|
||||
].map((refDoc) =>
|
||||
isLensEqual(refDoc, lastKnownDoc, data.query.filterManager.inject, datasourceMap)
|
||||
isLensEqual(
|
||||
refDoc,
|
||||
lastKnownDoc,
|
||||
data.query.filterManager.inject,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
annotationGroups
|
||||
)
|
||||
);
|
||||
if (initialDocFromContextUnchanged || currentDocHasBeenSavedInLens) {
|
||||
onAppLeave((actions) => {
|
||||
|
@ -386,6 +398,7 @@ export function App({
|
|||
}
|
||||
}
|
||||
}, [
|
||||
annotationGroups,
|
||||
application,
|
||||
data.query.filterManager.inject,
|
||||
datasourceMap,
|
||||
|
@ -394,6 +407,7 @@ export function App({
|
|||
lastKnownDoc,
|
||||
onAppLeave,
|
||||
persistedDoc,
|
||||
visualizationMap,
|
||||
]);
|
||||
|
||||
const navigateToVizEditor = useCallback(() => {
|
||||
|
@ -422,7 +436,6 @@ export function App({
|
|||
dataViews,
|
||||
uiActions,
|
||||
core: { http, notifications, uiSettings },
|
||||
data,
|
||||
contextDataViewSpec: (initialContext as VisualizeFieldContext | undefined)?.dataViewSpec,
|
||||
updateIndexPatterns: (newIndexPatternsState, options) => {
|
||||
dispatch(updateIndexPatterns(newIndexPatternsState));
|
||||
|
@ -437,7 +450,7 @@ export function App({
|
|||
}
|
||||
},
|
||||
}),
|
||||
[dataViews, uiActions, http, notifications, uiSettings, data, initialContext, dispatch]
|
||||
[dataViews, uiActions, http, notifications, uiSettings, initialContext, dispatch]
|
||||
);
|
||||
|
||||
const onTextBasedSavedAndExit = useCallback(async ({ onSave, onCancel }) => {
|
||||
|
@ -597,7 +610,9 @@ export function App({
|
|||
persistedDoc,
|
||||
lastKnownDoc,
|
||||
data.query.filterManager.inject.bind(data.query.filterManager),
|
||||
datasourceMap
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
annotationGroups
|
||||
)
|
||||
}
|
||||
goBackToOriginatingApp={goBackToOriginatingApp}
|
||||
|
|
|
@ -8,11 +8,19 @@
|
|||
import { Filter, FilterStateStore } from '@kbn/es-query';
|
||||
import { isLensEqual } from './lens_document_equality';
|
||||
import { Document } from '../persistence/saved_object_store';
|
||||
import { Datasource, DatasourceMap } from '../types';
|
||||
import {
|
||||
AnnotationGroups,
|
||||
Datasource,
|
||||
DatasourceMap,
|
||||
Visualization,
|
||||
VisualizationMap,
|
||||
} from '../types';
|
||||
|
||||
const visualizationType = 'lnsSomeVis';
|
||||
|
||||
const defaultDoc: Document = {
|
||||
title: 'some-title',
|
||||
visualizationType: 'lnsXY',
|
||||
visualizationType,
|
||||
state: {
|
||||
query: {
|
||||
query: '',
|
||||
|
@ -53,23 +61,67 @@ describe('lens document equality', () => {
|
|||
);
|
||||
|
||||
let mockDatasourceMap: DatasourceMap;
|
||||
let mockVisualizationMap: VisualizationMap;
|
||||
let mockAnnotationGroups: AnnotationGroups;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDatasourceMap = {
|
||||
indexpattern: { isEqual: jest.fn(() => true) },
|
||||
} as unknown as DatasourceMap;
|
||||
indexpattern: { isEqual: jest.fn(() => true) } as Partial<Datasource> as Datasource,
|
||||
};
|
||||
|
||||
mockVisualizationMap = {
|
||||
[visualizationType]: {
|
||||
isEqual: jest.fn(() => true),
|
||||
} as Partial<Visualization> as Visualization,
|
||||
};
|
||||
|
||||
mockAnnotationGroups = {};
|
||||
});
|
||||
|
||||
it('returns true when documents are equal', () => {
|
||||
expect(
|
||||
isLensEqual(defaultDoc, defaultDoc, mockInjectFilterReferences, mockDatasourceMap)
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
defaultDoc,
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap,
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('handles undefined documents', () => {
|
||||
expect(isLensEqual(undefined, undefined, mockInjectFilterReferences, {})).toBeTruthy();
|
||||
expect(isLensEqual(undefined, {} as Document, mockInjectFilterReferences, {})).toBeFalsy();
|
||||
expect(isLensEqual({} as Document, undefined, mockInjectFilterReferences, {})).toBeFalsy();
|
||||
expect(
|
||||
isLensEqual(
|
||||
undefined,
|
||||
undefined,
|
||||
mockInjectFilterReferences,
|
||||
{},
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
isLensEqual(
|
||||
undefined,
|
||||
{} as Document,
|
||||
mockInjectFilterReferences,
|
||||
{},
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
isLensEqual(
|
||||
{} as Document,
|
||||
undefined,
|
||||
mockInjectFilterReferences,
|
||||
{},
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should compare visualization type', () => {
|
||||
|
@ -78,7 +130,9 @@ describe('lens document equality', () => {
|
|||
defaultDoc,
|
||||
{ ...defaultDoc, visualizationType: 'other-type' },
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap
|
||||
mockDatasourceMap,
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
@ -98,26 +152,9 @@ describe('lens document equality', () => {
|
|||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should compare the visualization state', () => {
|
||||
expect(
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
{
|
||||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
visualization: {
|
||||
some: 'other-props',
|
||||
},
|
||||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap
|
||||
mockDatasourceMap,
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
@ -139,7 +176,9 @@ describe('lens document equality', () => {
|
|||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
{ ...mockDatasourceMap, foodatasource: { isEqual: () => true } as unknown as Datasource }
|
||||
{ ...mockDatasourceMap, foodatasource: { isEqual: () => true } as unknown as Datasource },
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeFalsy();
|
||||
|
||||
|
@ -167,7 +206,9 @@ describe('lens document equality', () => {
|
|||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
{ ...mockDatasourceMap, foodatasource: { isEqual: () => true } as unknown as Datasource }
|
||||
{ ...mockDatasourceMap, foodatasource: { isEqual: () => true } as unknown as Datasource },
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
@ -176,7 +217,67 @@ describe('lens document equality', () => {
|
|||
// datasource's isEqual returns false
|
||||
(mockDatasourceMap.indexpattern.isEqual as jest.Mock).mockReturnValue(false);
|
||||
expect(
|
||||
isLensEqual(defaultDoc, defaultDoc, mockInjectFilterReferences, mockDatasourceMap)
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
defaultDoc,
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap,
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('comparing the visualizations', () => {
|
||||
it('delegates to visualization class if visualization.isEqual available', () => {
|
||||
expect(
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
defaultDoc,
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap,
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(mockVisualizationMap[visualizationType].isEqual).toHaveBeenCalled();
|
||||
|
||||
(mockVisualizationMap[visualizationType].isEqual as jest.Mock).mockReturnValue(false);
|
||||
|
||||
expect(
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
defaultDoc,
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap,
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('direct comparison if no isEqual implementation', () => {
|
||||
delete mockVisualizationMap[visualizationType].isEqual;
|
||||
|
||||
expect(
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
{
|
||||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
visualization: {
|
||||
some: 'other-props',
|
||||
},
|
||||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap,
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
@ -197,7 +298,9 @@ describe('lens document equality', () => {
|
|||
defaultDoc,
|
||||
{ ...defaultDoc, state: { ...defaultDoc.state, filters: filtersWithPinned } },
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap
|
||||
mockDatasourceMap,
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
@ -221,7 +324,9 @@ describe('lens document equality', () => {
|
|||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap
|
||||
mockDatasourceMap,
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
@ -241,7 +346,9 @@ describe('lens document equality', () => {
|
|||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap
|
||||
mockDatasourceMap,
|
||||
mockVisualizationMap,
|
||||
mockAnnotationGroups
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { isEqual, intersection, union } from 'lodash';
|
||||
import { FilterManager } from '@kbn/data-plugin/public';
|
||||
import { Document } from '../persistence/saved_object_store';
|
||||
import { DatasourceMap } from '../types';
|
||||
import { AnnotationGroups, DatasourceMap, VisualizationMap } from '../types';
|
||||
import { removePinnedFilters } from './save_modal_container';
|
||||
|
||||
const removeNonSerializable = (obj: Parameters<JSON['stringify']>[0]) =>
|
||||
|
@ -18,7 +18,9 @@ export const isLensEqual = (
|
|||
doc1In: Document | undefined,
|
||||
doc2In: Document | undefined,
|
||||
injectFilterReferences: FilterManager['inject'],
|
||||
datasourceMap: DatasourceMap
|
||||
datasourceMap: DatasourceMap,
|
||||
visualizationMap: VisualizationMap,
|
||||
annotationGroups: AnnotationGroups
|
||||
) => {
|
||||
if (doc1In === undefined || doc2In === undefined) {
|
||||
return doc1In === doc2In;
|
||||
|
@ -36,7 +38,23 @@ export const isLensEqual = (
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!isEqual(doc1.state.visualization, doc2.state.visualization)) {
|
||||
const isEqualFromVis = visualizationMap[doc1.visualizationType]?.isEqual;
|
||||
const visualizationStateIsEqual = isEqualFromVis
|
||||
? (() => {
|
||||
try {
|
||||
return isEqualFromVis(
|
||||
doc1.state.visualization,
|
||||
doc1.references,
|
||||
doc2.state.visualization,
|
||||
doc2.references,
|
||||
annotationGroups
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
})()
|
||||
: isEqual(doc1.state.visualization, doc2.state.visualization);
|
||||
if (!visualizationStateIsEqual) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -96,6 +96,7 @@ export async function getLensServices(
|
|||
inspector,
|
||||
navigation,
|
||||
embeddable,
|
||||
eventAnnotation,
|
||||
savedObjectsTagging,
|
||||
usageCollection,
|
||||
fieldFormats,
|
||||
|
@ -107,6 +108,7 @@ export async function getLensServices(
|
|||
const storage = new Storage(localStorage);
|
||||
const stateTransfer = embeddable?.getStateTransfer();
|
||||
const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID);
|
||||
const eventAnnotationService = await eventAnnotation.getService();
|
||||
|
||||
return {
|
||||
data,
|
||||
|
@ -118,6 +120,7 @@ export async function getLensServices(
|
|||
usageCollection,
|
||||
savedObjectsTagging,
|
||||
attributeService,
|
||||
eventAnnotationService,
|
||||
executionContext: coreStart.executionContext,
|
||||
http: coreStart.http,
|
||||
uiActions: startDependencies.uiActions,
|
||||
|
|
|
@ -25,7 +25,10 @@ import { showMemoizedErrorNotification } from '../lens_ui_errors';
|
|||
import { TableInspectorAdapter } from '../editor_frame_service/types';
|
||||
import { Datasource, DatasourcePublicAPI, IndexPatternMap } from '../types';
|
||||
import { Visualization } from '..';
|
||||
import { getLayerType } from '../editor_frame_service/editor_frame/config_panel/add_layer';
|
||||
|
||||
function getLayerType(visualization: Visualization, state: unknown, layerId: string) {
|
||||
return visualization.getLayerType(layerId, state) || LayerTypes.DATA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a series of queries.
|
||||
|
|
|
@ -43,6 +43,7 @@ export const TagEnhancedSavedObjectSaveModalDashboard: FC<
|
|||
<savedObjectsTagging.ui.components.SavedObjectSaveModalTagSelector
|
||||
initialSelection={initialTags}
|
||||
onTagsSelected={setSelectedTags}
|
||||
markOptional
|
||||
/>
|
||||
) : undefined,
|
||||
[savedObjectsTagging, initialTags]
|
||||
|
|
|
@ -45,6 +45,7 @@ import type { ChartsPluginSetup } from '@kbn/charts-plugin/public';
|
|||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
|
||||
import type {
|
||||
DatasourceMap,
|
||||
|
@ -148,6 +149,7 @@ export interface LensAppServices {
|
|||
dataViews: DataViewsPublicPluginStart;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
data: DataPublicPluginStart;
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
inspector: LensInspector;
|
||||
uiSettings: IUiSettingsClient;
|
||||
settings: SettingsStart;
|
||||
|
|
|
@ -8,20 +8,18 @@
|
|||
import type { DataViewsContract, DataView, DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { ActionExecutionContext, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import {
|
||||
UPDATE_FILTER_REFERENCES_ACTION,
|
||||
UPDATE_FILTER_REFERENCES_TRIGGER,
|
||||
} from '@kbn/unified-search-plugin/public';
|
||||
import type { IndexPattern, IndexPatternMap, IndexPatternRef } from '../types';
|
||||
import { ensureIndexPattern, loadIndexPatternRefs, loadIndexPatterns } from './loader';
|
||||
import type { IndexPattern, IndexPatternMap } from '../types';
|
||||
import { ensureIndexPattern, loadIndexPatterns } from './loader';
|
||||
import type { DataViewsState } from '../state_management';
|
||||
import { generateId } from '../id_generator';
|
||||
|
||||
export interface IndexPatternServiceProps {
|
||||
core: Pick<CoreStart, 'http' | 'notifications' | 'uiSettings'>;
|
||||
data: DataPublicPluginStart;
|
||||
dataViews: DataViewsContract;
|
||||
uiActions: UiActionsStart;
|
||||
contextDataViewSpec?: DataViewSpec;
|
||||
|
@ -54,10 +52,7 @@ export interface IndexPatternServiceAPI {
|
|||
cache: IndexPatternMap;
|
||||
onIndexPatternRefresh?: () => void;
|
||||
}) => Promise<IndexPatternMap>;
|
||||
/**
|
||||
* Load indexPatternRefs with title and ids
|
||||
*/
|
||||
loadIndexPatternRefs: (options: { isFullEditor: boolean }) => Promise<IndexPatternRef[]>;
|
||||
|
||||
/**
|
||||
* Ensure an indexPattern is loaded in the cache, usually used in conjuction with a indexPattern change action.
|
||||
*/
|
||||
|
@ -81,16 +76,15 @@ export interface IndexPatternServiceAPI {
|
|||
) => void;
|
||||
}
|
||||
|
||||
export function createIndexPatternService({
|
||||
export const createIndexPatternService = ({
|
||||
core,
|
||||
dataViews,
|
||||
data,
|
||||
updateIndexPatterns,
|
||||
replaceIndexPattern,
|
||||
uiActions,
|
||||
contextDataViewSpec,
|
||||
}: IndexPatternServiceProps): IndexPatternServiceAPI {
|
||||
const onChangeError = (err: Error) =>
|
||||
}: IndexPatternServiceProps): IndexPatternServiceAPI => {
|
||||
const showLoadingDataViewError = (err: Error) =>
|
||||
core.notifications.toasts.addError(err, {
|
||||
title: i18n.translate('xpack.lens.indexPattern.dataViewLoadError', {
|
||||
defaultMessage: 'Error loading data view',
|
||||
|
@ -131,9 +125,7 @@ export function createIndexPatternService({
|
|||
} as ActionExecutionContext);
|
||||
},
|
||||
ensureIndexPattern: (args) =>
|
||||
ensureIndexPattern({ onError: onChangeError, dataViews, ...args }),
|
||||
loadIndexPatternRefs: async ({ isFullEditor }) =>
|
||||
isFullEditor ? loadIndexPatternRefs(dataViews) : [],
|
||||
ensureIndexPattern({ onError: showLoadingDataViewError, dataViews, ...args }),
|
||||
getDefaultIndex: () => core.uiSettings.get('defaultIndex'),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import {
|
||||
EuiToolTip,
|
||||
EuiButton,
|
||||
EuiPopover,
|
||||
EuiIcon,
|
||||
EuiContextMenu,
|
||||
EuiBadge,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import type { LayerType } from '../../../../common/types';
|
||||
import type { FramePublicAPI, Visualization } from '../../../types';
|
||||
|
||||
interface AddLayerButtonProps {
|
||||
visualization: Visualization;
|
||||
visualizationState: unknown;
|
||||
onAddLayerClick: (layerType: LayerType) => void;
|
||||
layersMeta: Pick<FramePublicAPI, 'datasourceLayers' | 'activeData'>;
|
||||
}
|
||||
|
||||
export function getLayerType(visualization: Visualization, state: unknown, layerId: string) {
|
||||
return visualization.getLayerType(layerId, state) || LayerTypes.DATA;
|
||||
}
|
||||
|
||||
export function AddLayerButton({
|
||||
visualization,
|
||||
visualizationState,
|
||||
onAddLayerClick,
|
||||
layersMeta,
|
||||
}: AddLayerButtonProps) {
|
||||
const [showLayersChoice, toggleLayersChoice] = useState(false);
|
||||
|
||||
const supportedLayers = useMemo(() => {
|
||||
if (!visualization.appendLayer || !visualizationState) {
|
||||
return null;
|
||||
}
|
||||
return visualization
|
||||
.getSupportedLayers?.(visualizationState, layersMeta)
|
||||
?.filter(({ canAddViaMenu: hideFromMenu }) => !hideFromMenu);
|
||||
}, [visualization, visualizationState, layersMeta]);
|
||||
|
||||
if (supportedLayers == null || !supportedLayers.length) {
|
||||
return null;
|
||||
}
|
||||
if (supportedLayers.length === 1) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
display="block"
|
||||
title={i18n.translate('xpack.lens.xyChart.addLayer', {
|
||||
defaultMessage: 'Add a layer',
|
||||
})}
|
||||
content={i18n.translate('xpack.lens.xyChart.addLayerTooltip', {
|
||||
defaultMessage:
|
||||
'Use multiple layers to combine visualization types or visualize different data views.',
|
||||
})}
|
||||
position="bottom"
|
||||
>
|
||||
<EuiButton
|
||||
size="s"
|
||||
fullWidth
|
||||
data-test-subj="lnsLayerAddButton"
|
||||
aria-label={i18n.translate('xpack.lens.configPanel.addLayerButton', {
|
||||
defaultMessage: 'Add layer',
|
||||
})}
|
||||
onClick={() => onAddLayerClick(supportedLayers[0].type)}
|
||||
iconType="layers"
|
||||
>
|
||||
{i18n.translate('xpack.lens.configPanel.addLayerButton', {
|
||||
defaultMessage: 'Add layer',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiPopover
|
||||
display="block"
|
||||
data-test-subj="lnsConfigPanel__addLayerPopover"
|
||||
button={
|
||||
<EuiButton
|
||||
size="s"
|
||||
fullWidth
|
||||
data-test-subj="lnsLayerAddButton"
|
||||
aria-label={i18n.translate('xpack.lens.configPanel.addLayerButton', {
|
||||
defaultMessage: 'Add layer',
|
||||
})}
|
||||
onClick={() => toggleLayersChoice(!showLayersChoice)}
|
||||
iconType="layers"
|
||||
>
|
||||
{i18n.translate('xpack.lens.configPanel.addLayerButton', {
|
||||
defaultMessage: 'Add layer',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
isOpen={showLayersChoice}
|
||||
closePopover={() => toggleLayersChoice(false)}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={[
|
||||
{
|
||||
id: 0,
|
||||
title: i18n.translate('xpack.lens.configPanel.selectLayerType', {
|
||||
defaultMessage: 'Select layer type',
|
||||
}),
|
||||
width: 300,
|
||||
items: supportedLayers.map(({ type, label, icon, disabled, toolTipContent }) => {
|
||||
return {
|
||||
toolTipContent,
|
||||
disabled,
|
||||
name:
|
||||
type === LayerTypes.ANNOTATIONS ? (
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<span className="lnsLayerAddButton__label">{label}</span>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge
|
||||
className="lnsLayerAddButton__techBadge"
|
||||
color="hollow"
|
||||
isDisabled={disabled}
|
||||
>
|
||||
{i18n.translate('xpack.lens.configPanel.experimentalLabel', {
|
||||
defaultMessage: 'Technical preview',
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<span className="lnsLayerAddButtonLabel">{label}</span>
|
||||
),
|
||||
className: 'lnsLayerAddButton',
|
||||
icon: icon && <EuiIcon size="m" type={icon} />,
|
||||
['data-test-subj']: `lnsLayerAddButton-${type}`,
|
||||
onClick: () => {
|
||||
onAddLayerClick(type);
|
||||
toggleLayersChoice(false);
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
|
@ -35,9 +35,9 @@ export function shouldRemoveSource(source: DragDropIdentifier, dropType: DropTyp
|
|||
);
|
||||
}
|
||||
|
||||
export function onDropForVisualization<T, P = unknown>(
|
||||
export function onDropForVisualization<T, P = unknown, E = unknown>(
|
||||
props: OnVisDropProps<T>,
|
||||
activeVisualization: Visualization<T, P>
|
||||
activeVisualization: Visualization<T, P, E>
|
||||
) {
|
||||
const { prevState, target, frame, source, group } = props;
|
||||
const { layerId, columnId, groupId } = target;
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
mockStoreDeps,
|
||||
MountStoreProps,
|
||||
} from '../../../mocks';
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { Visualization } from '../../../types';
|
||||
import { LayerPanels } from './config_panel';
|
||||
import { LayerPanel } from './layer_panel';
|
||||
|
@ -25,11 +26,11 @@ import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
|||
import { generateId } from '../../../id_generator';
|
||||
import { mountWithProvider } from '../../../mocks';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import type { LayerType } from '../../../../common/types';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { addLayer } from '../../../state_management';
|
||||
import { AddLayerButton } from './add_layer';
|
||||
import { createIndexPatternServiceMock } from '../../../mocks/data_views_service_mock';
|
||||
import { AddLayerButton } from '../../../visualizations/xy/add_layer';
|
||||
import { LayerType } from '@kbn/visualizations-plugin/common';
|
||||
|
||||
jest.mock('../../../id_generator');
|
||||
|
||||
|
@ -43,6 +44,11 @@ jest.mock('@kbn/kibana-utils-plugin/public', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const addNewLayer = (instance: ReactWrapper, type: LayerType = LayerTypes.REFERENCELINE) =>
|
||||
act(() => {
|
||||
instance.find(`button[data-test-subj="${type}"]`).first().simulate('click');
|
||||
});
|
||||
|
||||
const waitMs = (time: number) => new Promise((r) => setTimeout(r, time));
|
||||
|
||||
let container: HTMLDivElement | undefined;
|
||||
|
@ -117,7 +123,21 @@ describe('ConfigPanel', () => {
|
|||
activeVisualization: {
|
||||
...visualizationMap.testVis,
|
||||
getLayerIds: () => Object.keys(frame.datasourceLayers),
|
||||
} as unknown as Visualization,
|
||||
getAddLayerButtonComponent: (props) => {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
data-test-subj={LayerTypes.REFERENCELINE}
|
||||
onClick={() => props.addLayer(LayerTypes.REFERENCELINE)}
|
||||
/>
|
||||
<button
|
||||
data-test-subj={LayerTypes.ANNOTATIONS}
|
||||
onClick={() => props.addLayer(LayerTypes.ANNOTATIONS)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
} as Visualization,
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
isLoading: false,
|
||||
|
@ -135,6 +155,7 @@ describe('ConfigPanel', () => {
|
|||
isFullscreen: false,
|
||||
toggleFullscreen: jest.fn(),
|
||||
uiActions,
|
||||
dataViews: {} as DataViewsPublicPluginStart,
|
||||
getUserMessages: () => [],
|
||||
};
|
||||
}
|
||||
|
@ -257,34 +278,13 @@ describe('ConfigPanel', () => {
|
|||
}),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
instance.find('button[data-test-subj="lnsLayerAddButton"]').first().simulate('click');
|
||||
});
|
||||
addNewLayer(instance);
|
||||
const focusedEl = document.activeElement;
|
||||
expect(focusedEl?.children[0].getAttribute('data-test-subj')).toEqual('lns-layerPanel-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('initial default value', () => {
|
||||
function clickToAddLayer(
|
||||
instance: ReactWrapper,
|
||||
layerType: LayerType = LayerTypes.REFERENCELINE
|
||||
) {
|
||||
act(() => {
|
||||
instance.find('button[data-test-subj="lnsLayerAddButton"]').first().simulate('click');
|
||||
});
|
||||
instance.update();
|
||||
act(() => {
|
||||
instance
|
||||
.find(`[data-test-subj="lnsLayerAddButton-${layerType}"]`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
});
|
||||
instance.update();
|
||||
|
||||
return waitMs(0);
|
||||
}
|
||||
|
||||
function clickToAddDimension(instance: ReactWrapper) {
|
||||
act(() => {
|
||||
instance.find('[data-test-subj="lns-empty-dimension"]').last().simulate('click');
|
||||
|
@ -307,7 +307,7 @@ describe('ConfigPanel', () => {
|
|||
const props = getDefaultProps({ datasourceMap, visualizationMap });
|
||||
|
||||
const { instance, lensStore } = await prepareAndMountComponent(props);
|
||||
await clickToAddLayer(instance);
|
||||
addNewLayer(instance);
|
||||
|
||||
expect(lensStore.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(datasourceMap.testDatasource.initializeDimension).not.toHaveBeenCalled();
|
||||
|
@ -337,8 +337,8 @@ describe('ConfigPanel', () => {
|
|||
]);
|
||||
const props = getDefaultProps({ datasourceMap, visualizationMap });
|
||||
const { instance, lensStore } = await prepareAndMountComponent(props);
|
||||
await clickToAddLayer(instance);
|
||||
|
||||
addNewLayer(instance);
|
||||
expect(lensStore.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(datasourceMap.testDatasource.initializeDimension).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -364,7 +364,7 @@ describe('ConfigPanel', () => {
|
|||
const props = getDefaultProps({ datasourceMap, visualizationMap });
|
||||
|
||||
const { instance, lensStore } = await prepareAndMountComponent(props);
|
||||
await clickToAddLayer(instance);
|
||||
addNewLayer(instance);
|
||||
|
||||
expect(lensStore.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(datasourceMap.testDatasource.initializeDimension).toHaveBeenCalledWith(
|
||||
|
@ -473,7 +473,9 @@ describe('ConfigPanel', () => {
|
|||
datasourceMap.testDatasource.initializeDimension = jest.fn();
|
||||
const props = getDefaultProps({ visualizationMap, datasourceMap });
|
||||
const { instance, lensStore } = await prepareAndMountComponent(props);
|
||||
await clickToAddLayer(instance, LayerTypes.ANNOTATIONS);
|
||||
|
||||
addNewLayer(instance, LayerTypes.ANNOTATIONS);
|
||||
|
||||
expect(lensStore.dispatch).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(visualizationMap.testVis.setDimension).toHaveBeenCalledWith({
|
||||
|
|
|
@ -13,9 +13,8 @@ import {
|
|||
UPDATE_FILTER_REFERENCES_ACTION,
|
||||
UPDATE_FILTER_REFERENCES_TRIGGER,
|
||||
} from '@kbn/unified-search-plugin/public';
|
||||
import { LayerType } from '../../../../common/types';
|
||||
import { changeIndexPattern, removeDimension } from '../../../state_management/lens_slice';
|
||||
import { Visualization } from '../../../types';
|
||||
import { AddLayerFunction, Visualization } from '../../../types';
|
||||
import { LayerPanel } from './layer_panel';
|
||||
import { generateId } from '../../../id_generator';
|
||||
import { ConfigPanelWrapperProps } from './types';
|
||||
|
@ -32,8 +31,8 @@ import {
|
|||
setToggleFullscreen,
|
||||
useLensSelector,
|
||||
selectVisualization,
|
||||
registerLibraryAnnotationGroup,
|
||||
} from '../../../state_management';
|
||||
import { AddLayerButton } from './add_layer';
|
||||
import { getRemoveOperation } from '../../../utils';
|
||||
|
||||
export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
|
||||
|
@ -218,6 +217,7 @@ export function LayerPanels(
|
|||
id: indexPatternId,
|
||||
cache: props.framePublicAPI.dataViews.indexPatterns,
|
||||
});
|
||||
|
||||
dispatchLens(
|
||||
changeIndexPattern({
|
||||
indexPatternId,
|
||||
|
@ -231,9 +231,9 @@ export function LayerPanels(
|
|||
[dispatchLens, props.framePublicAPI.dataViews.indexPatterns, props.indexPatternService]
|
||||
);
|
||||
|
||||
const addLayer = (layerType: LayerType) => {
|
||||
const addLayer: AddLayerFunction = (layerType, extraArg, ignoreInitialValues) => {
|
||||
const layerId = generateId();
|
||||
dispatchLens(addLayerAction({ layerId, layerType }));
|
||||
dispatchLens(addLayerAction({ layerId, layerType, extraArg, ignoreInitialValues }));
|
||||
setNextFocusedLayerId(layerId);
|
||||
};
|
||||
|
||||
|
@ -314,14 +314,45 @@ export function LayerPanels(
|
|||
)
|
||||
);
|
||||
})}
|
||||
{!hideAddLayerButton && (
|
||||
<AddLayerButton
|
||||
visualization={activeVisualization}
|
||||
visualizationState={visualization.state}
|
||||
layersMeta={props.framePublicAPI}
|
||||
onAddLayerClick={(layerType) => addLayer(layerType)}
|
||||
/>
|
||||
)}
|
||||
{!hideAddLayerButton &&
|
||||
activeVisualization?.getAddLayerButtonComponent?.({
|
||||
supportedLayers: activeVisualization.getSupportedLayers(
|
||||
visualization.state,
|
||||
props.framePublicAPI
|
||||
),
|
||||
addLayer,
|
||||
ensureIndexPattern: async (specOrId) => {
|
||||
let indexPatternId;
|
||||
|
||||
if (typeof specOrId === 'string') {
|
||||
indexPatternId = specOrId;
|
||||
} else {
|
||||
const dataView = await props.dataViews.create(specOrId);
|
||||
|
||||
if (!dataView.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
indexPatternId = dataView.id;
|
||||
}
|
||||
|
||||
const newIndexPatterns = await indexPatternService.ensureIndexPattern({
|
||||
id: indexPatternId,
|
||||
cache: props.framePublicAPI.dataViews.indexPatterns,
|
||||
});
|
||||
|
||||
dispatchLens(
|
||||
changeIndexPattern({
|
||||
dataViews: { indexPatterns: newIndexPatterns },
|
||||
datasourceIds: Object.keys(datasourceStates),
|
||||
visualizationIds: visualization.activeId ? [visualization.activeId] : [],
|
||||
indexPatternId,
|
||||
})
|
||||
);
|
||||
},
|
||||
registerLibraryAnnotationGroup: (groupInfo) =>
|
||||
dispatchLens(registerLibraryAnnotationGroup(groupInfo)),
|
||||
})}
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import './dimension_container.scss';
|
||||
|
||||
import React from 'react';
|
||||
import { FlyoutContainer } from './flyout_container';
|
||||
import { FlyoutContainer } from '../../../shared_components/flyout_container';
|
||||
|
||||
export function DimensionContainer({
|
||||
panel,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { LayerAction } from '../../../../types';
|
||||
import type { Visualization } from '../../../..';
|
||||
import { FIRST_ACTION_ORDER } from './order_bounds';
|
||||
|
||||
interface CloneLayerAction {
|
||||
execute: () => void;
|
||||
|
@ -22,11 +23,11 @@ export const getCloneLayerAction = (props: CloneLayerAction): LayerAction => {
|
|||
});
|
||||
|
||||
return {
|
||||
id: 'cloneLayerAction',
|
||||
execute: props.execute,
|
||||
displayName,
|
||||
isCompatible: Boolean(props.activeVisualization.cloneLayer && !props.isTextBasedLanguage),
|
||||
icon: 'copy',
|
||||
'data-test-subj': `lnsLayerClone--${props.layerIndex}`,
|
||||
order: FIRST_ACTION_ORDER + 1,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,6 +17,9 @@ import {
|
|||
EuiText,
|
||||
EuiOutsideClickDetector,
|
||||
useEuiTheme,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { css } from '@emotion/react';
|
||||
|
@ -29,6 +32,7 @@ import { getOpenLayerSettingsAction } from './open_layer_settings';
|
|||
export interface LayerActionsProps {
|
||||
layerIndex: number;
|
||||
actions: LayerAction[];
|
||||
mountingPoint?: HTMLDivElement | null | undefined;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
|
@ -129,9 +133,10 @@ const InContextMenuActions = (props: LayerActionsProps) => {
|
|||
data-test-subj={i['data-test-subj']}
|
||||
aria-label={i.displayName}
|
||||
title={i.displayName}
|
||||
disabled={i.disabled}
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
i.execute();
|
||||
i.execute(props.mountingPoint);
|
||||
}}
|
||||
{...(i.color
|
||||
? {
|
||||
|
@ -158,20 +163,46 @@ export const LayerActions = (props: LayerActionsProps) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (props.actions.length > 1) {
|
||||
return <InContextMenuActions {...props} />;
|
||||
}
|
||||
const [{ displayName, execute, icon, color, 'data-test-subj': dataTestSubj }] = props.actions;
|
||||
const sortedActions = [...props.actions].sort(
|
||||
({ order: order1 }, { order: order2 }) => order1 - order2
|
||||
);
|
||||
|
||||
const outsideListAction =
|
||||
sortedActions.length === 1
|
||||
? sortedActions[0]
|
||||
: sortedActions.find((action) => action.showOutsideList);
|
||||
|
||||
const listActions = sortedActions.filter((action) => action !== outsideListAction);
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
iconType={icon}
|
||||
color={color}
|
||||
data-test-subj={dataTestSubj}
|
||||
aria-label={displayName}
|
||||
title={displayName}
|
||||
onClick={execute}
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
gap: 0;
|
||||
`}
|
||||
responsive={false}
|
||||
alignItems="center"
|
||||
direction="row"
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
{outsideListAction && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={outsideListAction.displayName}>
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
iconType={outsideListAction.icon}
|
||||
color={outsideListAction.color ?? 'text'}
|
||||
data-test-subj={outsideListAction['data-test-subj']}
|
||||
aria-label={outsideListAction.displayName}
|
||||
title={outsideListAction.displayName}
|
||||
disabled={outsideListAction.disabled}
|
||||
onClick={() => outsideListAction.execute?.(props.mountingPoint)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<InContextMenuActions {...props} actions={listActions} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { LayerAction } from '../../../../types';
|
||||
import { FIRST_ACTION_ORDER } from './order_bounds';
|
||||
|
||||
export const getOpenLayerSettingsAction = (props: {
|
||||
openLayerSettings: () => void;
|
||||
|
@ -17,11 +18,11 @@ export const getOpenLayerSettingsAction = (props: {
|
|||
});
|
||||
|
||||
return {
|
||||
id: 'openLayerSettings',
|
||||
displayName,
|
||||
execute: props.openLayerSettings,
|
||||
icon: 'gear',
|
||||
isCompatible: props.hasLayerSettings,
|
||||
'data-test-subj': 'lnsLayerSettings',
|
||||
order: FIRST_ACTION_ORDER,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const LAST_ACTION_ORDER = Number.MAX_SAFE_INTEGER;
|
||||
export const FIRST_ACTION_ORDER = -Number.MAX_SAFE_INTEGER;
|
|
@ -26,6 +26,7 @@ import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
|||
import type { LayerAction } from '../../../../types';
|
||||
import { LOCAL_STORAGE_LENS_KEY } from '../../../../settings_storage';
|
||||
import type { LayerType } from '../../../../../common/types';
|
||||
import { LAST_ACTION_ORDER } from './order_bounds';
|
||||
|
||||
interface RemoveLayerAction {
|
||||
execute: () => void;
|
||||
|
@ -196,7 +197,6 @@ export const getRemoveLayerAction = (props: RemoveLayerAction): LayerAction => {
|
|||
);
|
||||
|
||||
return {
|
||||
id: 'removeLayerAction',
|
||||
execute: async () => {
|
||||
const storage = new Storage(localStorage);
|
||||
const lensLocalStorage = storage.get(LOCAL_STORAGE_LENS_KEY) ?? {};
|
||||
|
@ -224,6 +224,7 @@ export const getRemoveLayerAction = (props: RemoveLayerAction): LayerAction => {
|
|||
),
|
||||
{
|
||||
'data-test-subj': 'lnsLayerRemoveModal',
|
||||
maxWidth: 600,
|
||||
}
|
||||
);
|
||||
await modal.onClose;
|
||||
|
@ -236,5 +237,6 @@ export const getRemoveLayerAction = (props: RemoveLayerAction): LayerAction => {
|
|||
icon: props.isOnlyLayer ? 'eraser' : 'trash',
|
||||
color: 'danger',
|
||||
'data-test-subj': `lnsLayerRemove--${props.layerIndex}`,
|
||||
order: LAST_ACTION_ORDER,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -22,7 +22,6 @@ import { css } from '@emotion/react';
|
|||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { DragDropIdentifier, ReorderProvider, DropType } from '@kbn/dom-drag-drop';
|
||||
import { DimensionButton } from '@kbn/visualization-ui-components/public';
|
||||
import { LayerType } from '../../../../common/types';
|
||||
import { LayerActions } from './layer_actions';
|
||||
import { IndexPatternServiceAPI } from '../../../data_views_service/service';
|
||||
import { NativeRenderer } from '../../../native_renderer';
|
||||
|
@ -34,6 +33,7 @@ import {
|
|||
LayerAction,
|
||||
VisualizationDimensionGroupConfig,
|
||||
UserMessagesGetter,
|
||||
AddLayerFunction,
|
||||
} from '../../../types';
|
||||
import { LayerSettings } from './layer_settings';
|
||||
import { LayerPanelProps, ActiveDimensionState } from './types';
|
||||
|
@ -49,7 +49,7 @@ import {
|
|||
} from '../../../state_management';
|
||||
import { onDropForVisualization, shouldRemoveSource } from './buttons/drop_targets_utils';
|
||||
import { getSharedActions } from './layer_actions/layer_actions';
|
||||
import { FlyoutContainer } from './flyout_container';
|
||||
import { FlyoutContainer } from '../../../shared_components/flyout_container';
|
||||
|
||||
const initialActiveDimensionState = {
|
||||
isNew: false,
|
||||
|
@ -62,7 +62,7 @@ export function LayerPanel(
|
|||
layerId: string;
|
||||
layerIndex: number;
|
||||
isOnlyLayer: boolean;
|
||||
addLayer: (layerType: LayerType) => void;
|
||||
addLayer: AddLayerFunction;
|
||||
updateVisualization: StateSetter<unknown>;
|
||||
updateDatasource: (
|
||||
datasourceId: string | undefined,
|
||||
|
@ -118,6 +118,8 @@ export function LayerPanel(
|
|||
core,
|
||||
} = props;
|
||||
|
||||
const isSaveable = useLensSelector((state) => state.lens.isSaveable);
|
||||
|
||||
const datasourceStates = useLensSelector(selectDatasourceStates);
|
||||
const isFullscreen = useLensSelector(selectIsFullscreenDatasource);
|
||||
const dateRange = useLensSelector(selectResolvedDateRange);
|
||||
|
@ -128,6 +130,7 @@ export function LayerPanel(
|
|||
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
const settingsPanelRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const registerLayerRef = useCallback(
|
||||
(el) => registerNewLayerRef(layerId, el),
|
||||
[layerId, registerNewLayerRef]
|
||||
|
@ -332,15 +335,19 @@ export function LayerPanel(
|
|||
() =>
|
||||
[
|
||||
...(activeVisualization
|
||||
.getSupportedActionsForLayer?.(layerId, visualizationState)
|
||||
.getSupportedActionsForLayer?.(
|
||||
layerId,
|
||||
visualizationState,
|
||||
updateVisualization,
|
||||
isSaveable
|
||||
)
|
||||
.map((action) => ({
|
||||
...action,
|
||||
execute: () => {
|
||||
updateVisualization(
|
||||
activeVisualization.onLayerAction?.(layerId, action.id, visualizationState)
|
||||
);
|
||||
action.execute(layerActionsFlyoutRef.current);
|
||||
},
|
||||
})) || []),
|
||||
|
||||
...getSharedActions({
|
||||
layerId,
|
||||
activeVisualization,
|
||||
|
@ -373,8 +380,10 @@ export function LayerPanel(
|
|||
updateVisualization,
|
||||
visualizationLayerSettings,
|
||||
visualizationState,
|
||||
isSaveable,
|
||||
]
|
||||
);
|
||||
const layerActionsFlyoutRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -398,7 +407,12 @@ export function LayerPanel(
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LayerActions actions={compatibleActions} layerIndex={layerIndex} />
|
||||
<LayerActions
|
||||
actions={compatibleActions}
|
||||
layerIndex={layerIndex}
|
||||
mountingPoint={layerActionsFlyoutRef.current}
|
||||
/>
|
||||
<div ref={layerActionsFlyoutRef} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{(layerDatasource || activeVisualization.renderLayerPanel) && <EuiSpacer size="s" />}
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { IndexPatternServiceAPI } from '../../../data_views_service/service';
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import type { IndexPatternServiceAPI } from '../../../data_views_service/service';
|
||||
|
||||
import {
|
||||
Visualization,
|
||||
|
@ -23,6 +24,7 @@ export interface ConfigPanelWrapperProps {
|
|||
datasourceMap: DatasourceMap;
|
||||
visualizationMap: VisualizationMap;
|
||||
core: DatasourceDimensionEditorProps['core'];
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
indexPatternService: IndexPatternServiceAPI;
|
||||
uiActions: UiActionsStart;
|
||||
getUserMessages: UserMessagesGetter;
|
||||
|
|
|
@ -15,6 +15,7 @@ import { disableAutoApply } from '../../state_management/lens_slice';
|
|||
import { selectTriggerApplyChanges } from '../../state_management';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { createIndexPatternServiceMock } from '../../mocks/data_views_service_mock';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
|
||||
describe('Data Panel Wrapper', () => {
|
||||
describe('Datasource data panel properties', () => {
|
||||
|
@ -39,7 +40,11 @@ describe('Data Panel Wrapper', () => {
|
|||
core={{} as DatasourceDataPanelProps['core']}
|
||||
dropOntoWorkspace={(field: DragDropIdentifier) => {}}
|
||||
hasSuggestionForField={(field: DragDropIdentifier) => true}
|
||||
plugins={{ uiActions: {} as UiActionsStart, dataViews: {} as DataViewsPublicPluginStart }}
|
||||
plugins={{
|
||||
uiActions: {} as UiActionsStart,
|
||||
dataViews: {} as DataViewsPublicPluginStart,
|
||||
eventAnnotationService: {} as EventAnnotationServiceType,
|
||||
}}
|
||||
indexPatternService={createIndexPatternServiceMock()}
|
||||
frame={createMockFramePublicAPI()}
|
||||
/>,
|
||||
|
|
|
@ -11,6 +11,7 @@ import React, { useMemo, memo, useContext, useEffect, useCallback } from 'react'
|
|||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import { DragContext, DragDropIdentifier } from '@kbn/dom-drag-drop';
|
||||
import { Easteregg } from './easteregg';
|
||||
import { NativeRenderer } from '../../native_renderer';
|
||||
|
@ -44,7 +45,11 @@ interface DataPanelWrapperProps {
|
|||
core: DatasourceDataPanelProps['core'];
|
||||
dropOntoWorkspace: (field: DragDropIdentifier) => void;
|
||||
hasSuggestionForField: (field: DragDropIdentifier) => boolean;
|
||||
plugins: { uiActions: UiActionsStart; dataViews: DataViewsPublicPluginStart };
|
||||
plugins: {
|
||||
uiActions: UiActionsStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
};
|
||||
indexPatternService: IndexPatternServiceAPI;
|
||||
frame: FramePublicAPI;
|
||||
}
|
||||
|
@ -80,6 +85,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
|||
initializeSources(
|
||||
{
|
||||
datasourceMap: props.datasourceMap,
|
||||
eventAnnotationService: props.plugins.eventAnnotationService,
|
||||
visualizationMap: props.visualizationMap,
|
||||
visualizationState,
|
||||
datasourceStates,
|
||||
|
@ -127,6 +133,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
|||
dispatchLens,
|
||||
props.plugins.dataViews,
|
||||
props.core.uiSettings,
|
||||
props.plugins.eventAnnotationService,
|
||||
]);
|
||||
|
||||
const onChangeIndexPattern = useCallback(
|
||||
|
|
|
@ -51,6 +51,7 @@ import { getLensInspectorService } from '../../lens_inspector_service';
|
|||
import { toExpression } from '@kbn/interpreter';
|
||||
import { createIndexPatternServiceMock } from '../../mocks/data_views_service_mock';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
|
||||
function generateSuggestion(state = {}): DatasourceSuggestion {
|
||||
return {
|
||||
|
@ -94,6 +95,7 @@ function getDefaultProps() {
|
|||
expressions: expressionsPluginMock.createStartContract(),
|
||||
charts: chartPluginMock.createStartContract(),
|
||||
dataViews: wrapDataViewsContract(),
|
||||
eventAnnotationService: {} as EventAnnotationServiceType,
|
||||
},
|
||||
palettes: chartPluginMock.createPaletteRegistry(),
|
||||
lensInspector: getLensInspectorService(inspectorPluginMock.createStartContract()),
|
||||
|
|
|
@ -142,6 +142,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
visualizationMap={visualizationMap}
|
||||
framePublicAPI={framePublicAPI}
|
||||
uiActions={props.plugins.uiActions}
|
||||
dataViews={props.plugins.dataViews}
|
||||
indexPatternService={props.indexPatternService}
|
||||
getUserMessages={props.getUserMessages}
|
||||
/>
|
||||
|
|
|
@ -13,6 +13,11 @@ import type { DataViewsContract, DataViewSpec } from '@kbn/data-views-plugin/pub
|
|||
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common';
|
||||
import type { TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import {
|
||||
EventAnnotationGroupConfig,
|
||||
EVENT_ANNOTATION_GROUP_TYPE,
|
||||
} from '@kbn/event-annotation-plugin/common';
|
||||
import type {
|
||||
Datasource,
|
||||
DatasourceMap,
|
||||
|
@ -32,12 +37,13 @@ import { loadIndexPatternRefs, loadIndexPatterns } from '../../data_views_servic
|
|||
import { getDatasourceLayers } from '../../state_management/utils';
|
||||
|
||||
function getIndexPatterns(
|
||||
annotationGroupDataviewIds: string[],
|
||||
references?: SavedObjectReference[],
|
||||
initialContext?: VisualizeFieldContext | VisualizeEditorContext,
|
||||
initialId?: string,
|
||||
adHocDataviews?: string[]
|
||||
) {
|
||||
const indexPatternIds = [];
|
||||
const indexPatternIds = [...annotationGroupDataviewIds];
|
||||
|
||||
// use the initialId only when no context is passed over
|
||||
if (!initialContext && initialId) {
|
||||
|
@ -96,6 +102,7 @@ export async function initializeDataViews(
|
|||
references,
|
||||
initialContext,
|
||||
adHocDataViews: persistedAdHocDataViews,
|
||||
annotationGroups,
|
||||
}: {
|
||||
dataViews: DataViewsContract;
|
||||
datasourceMap: DatasourceMap;
|
||||
|
@ -105,6 +112,7 @@ export async function initializeDataViews(
|
|||
references?: SavedObjectReference[];
|
||||
initialContext?: VisualizeFieldContext | VisualizeEditorContext;
|
||||
adHocDataViews?: Record<string, DataViewSpec>;
|
||||
annotationGroups: Record<string, EventAnnotationGroupConfig>;
|
||||
},
|
||||
options?: InitializationOptions
|
||||
) {
|
||||
|
@ -114,6 +122,14 @@ export async function initializeDataViews(
|
|||
return [id, spec];
|
||||
})
|
||||
);
|
||||
|
||||
const annotationGroupValues = Object.values(annotationGroups);
|
||||
for (const group of annotationGroupValues) {
|
||||
if (group.dataViewSpec?.id) {
|
||||
adHocDataViews[group.dataViewSpec.id] = group.dataViewSpec;
|
||||
}
|
||||
}
|
||||
|
||||
const { isFullEditor } = options ?? {};
|
||||
|
||||
// make it explicit or TS will infer never[] and break few lines down
|
||||
|
@ -133,6 +149,7 @@ export async function initializeDataViews(
|
|||
const adHocDataviewsIds: string[] = Object.keys(adHocDataViews || {});
|
||||
|
||||
const usedIndexPatternsIds = getIndexPatterns(
|
||||
annotationGroupValues.map((group) => group.indexPatternId),
|
||||
references,
|
||||
initialContext,
|
||||
initialId,
|
||||
|
@ -163,12 +180,32 @@ export async function initializeDataViews(
|
|||
};
|
||||
}
|
||||
|
||||
const initializeEventAnnotationGroups = async (
|
||||
eventAnnotationService: EventAnnotationServiceType,
|
||||
references?: SavedObjectReference[]
|
||||
) => {
|
||||
const annotationGroups: Record<string, EventAnnotationGroupConfig> = {};
|
||||
|
||||
await Promise.allSettled(
|
||||
(references || [])
|
||||
.filter((ref) => ref.type === EVENT_ANNOTATION_GROUP_TYPE)
|
||||
.map(({ id }) =>
|
||||
eventAnnotationService.loadAnnotationGroup(id).then((group) => {
|
||||
annotationGroups[id] = group;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return annotationGroups;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function composes both initializeDataViews & initializeDatasources into a single call
|
||||
*/
|
||||
export async function initializeSources(
|
||||
{
|
||||
dataViews,
|
||||
eventAnnotationService,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
visualizationState,
|
||||
|
@ -180,6 +217,7 @@ export async function initializeSources(
|
|||
adHocDataViews,
|
||||
}: {
|
||||
dataViews: DataViewsContract;
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
datasourceMap: DatasourceMap;
|
||||
visualizationMap: VisualizationMap;
|
||||
visualizationState: VisualizationState;
|
||||
|
@ -192,6 +230,11 @@ export async function initializeSources(
|
|||
},
|
||||
options?: InitializationOptions
|
||||
) {
|
||||
const annotationGroups = await initializeEventAnnotationGroups(
|
||||
eventAnnotationService,
|
||||
references
|
||||
);
|
||||
|
||||
const { indexPatternRefs, indexPatterns } = await initializeDataViews(
|
||||
{
|
||||
datasourceMap,
|
||||
|
@ -202,12 +245,15 @@ export async function initializeSources(
|
|||
defaultIndexPatternId,
|
||||
references,
|
||||
adHocDataViews,
|
||||
annotationGroups,
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
return {
|
||||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
annotationGroups,
|
||||
datasourceStates: initializeDatasources({
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
|
@ -221,6 +267,7 @@ export async function initializeSources(
|
|||
visualizationState,
|
||||
references,
|
||||
initialContext,
|
||||
annotationGroups,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -229,12 +276,13 @@ export function initializeVisualization({
|
|||
visualizationMap,
|
||||
visualizationState,
|
||||
references,
|
||||
initialContext,
|
||||
annotationGroups,
|
||||
}: {
|
||||
visualizationState: VisualizationState;
|
||||
visualizationMap: VisualizationMap;
|
||||
references?: SavedObjectReference[];
|
||||
initialContext?: VisualizeFieldContext | VisualizeEditorContext;
|
||||
annotationGroups: Record<string, EventAnnotationGroupConfig>;
|
||||
}) {
|
||||
if (visualizationState?.activeId) {
|
||||
return (
|
||||
|
@ -242,8 +290,8 @@ export function initializeVisualization({
|
|||
() => '',
|
||||
visualizationState.state,
|
||||
undefined,
|
||||
references,
|
||||
initialContext
|
||||
annotationGroups,
|
||||
references
|
||||
) ?? visualizationState.state
|
||||
);
|
||||
}
|
||||
|
@ -282,6 +330,13 @@ export function initializeDatasources({
|
|||
return states;
|
||||
}
|
||||
|
||||
export interface DocumentToExpressionReturnType {
|
||||
ast: Ast | null;
|
||||
indexPatterns: IndexPatternMap;
|
||||
indexPatternRefs: IndexPatternRef[];
|
||||
activeVisualizationState: unknown;
|
||||
}
|
||||
|
||||
export async function persistedStateToExpression(
|
||||
datasourceMap: DatasourceMap,
|
||||
visualizations: VisualizationMap,
|
||||
|
@ -291,12 +346,9 @@ export async function persistedStateToExpression(
|
|||
storage: IStorageWrapper;
|
||||
dataViews: DataViewsContract;
|
||||
timefilter: TimefilterContract;
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
}
|
||||
): Promise<{
|
||||
ast: Ast | null;
|
||||
indexPatterns: IndexPatternMap;
|
||||
indexPatternRefs: IndexPatternRef[];
|
||||
}> {
|
||||
): Promise<DocumentToExpressionReturnType> {
|
||||
const {
|
||||
state: {
|
||||
visualization: persistedVisualizationState,
|
||||
|
@ -310,15 +362,22 @@ export async function persistedStateToExpression(
|
|||
description,
|
||||
} = doc;
|
||||
if (!visualizationType) {
|
||||
return { ast: null, indexPatterns: {}, indexPatternRefs: [] };
|
||||
return { ast: null, indexPatterns: {}, indexPatternRefs: [], activeVisualizationState: null };
|
||||
}
|
||||
|
||||
const annotationGroups = await initializeEventAnnotationGroups(
|
||||
services.eventAnnotationService,
|
||||
references
|
||||
);
|
||||
|
||||
const visualization = visualizations[visualizationType!];
|
||||
const visualizationState = initializeVisualization({
|
||||
const activeVisualizationState = initializeVisualization({
|
||||
visualizationMap: visualizations,
|
||||
visualizationState: {
|
||||
state: persistedVisualizationState,
|
||||
activeId: visualizationType,
|
||||
},
|
||||
annotationGroups,
|
||||
references: [...references, ...(internalReferences || [])],
|
||||
});
|
||||
const datasourceStatesFromSO = Object.fromEntries(
|
||||
|
@ -336,6 +395,7 @@ export async function persistedStateToExpression(
|
|||
storage: services.storage,
|
||||
defaultIndexPatternId: services.uiSettings.get('defaultIndex'),
|
||||
adHocDataViews,
|
||||
annotationGroups,
|
||||
},
|
||||
{ isFullEditor: false }
|
||||
);
|
||||
|
@ -355,6 +415,7 @@ export async function persistedStateToExpression(
|
|||
ast: null,
|
||||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
activeVisualizationState,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -365,13 +426,14 @@ export async function persistedStateToExpression(
|
|||
title,
|
||||
description,
|
||||
visualization,
|
||||
visualizationState,
|
||||
visualizationState: activeVisualizationState,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
datasourceLayers,
|
||||
indexPatterns,
|
||||
dateRange: { fromDate: currentTimeRange.from, toDate: currentTimeRange.to },
|
||||
}),
|
||||
activeVisualizationState,
|
||||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
};
|
||||
|
|
|
@ -23,7 +23,6 @@ import type {
|
|||
DatasourceLayers,
|
||||
} from '../../types';
|
||||
import type { LayerType } from '../../../common/types';
|
||||
import { getLayerType } from './config_panel/add_layer';
|
||||
import {
|
||||
LensDispatch,
|
||||
switchVisualization,
|
||||
|
@ -79,7 +78,7 @@ export function getSuggestions({
|
|||
}
|
||||
const layers = datasource.getLayers(datasourceState);
|
||||
for (const layerId of layers) {
|
||||
const type = getLayerType(activeVisualization, visualizationState, layerId);
|
||||
const type = activeVisualization.getLayerType(layerId, visualizationState) || LayerTypes.DATA;
|
||||
memo[layerId] = type;
|
||||
}
|
||||
return memo;
|
||||
|
|
|
@ -47,7 +47,7 @@ describe('chart_switch', () => {
|
|||
groupLabel: `${id}Group`,
|
||||
},
|
||||
],
|
||||
initialize: jest.fn((_frame, state?: unknown) => {
|
||||
initialize: jest.fn((_addNewLayer, state) => {
|
||||
return state || `${id} initial state`;
|
||||
}),
|
||||
getSuggestions: jest.fn((options) => {
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
DataViewsPublicPluginSetup,
|
||||
DataViewsPublicPluginStart,
|
||||
} from '@kbn/data-views-plugin/public';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import { Document } from '../persistence/saved_object_store';
|
||||
import {
|
||||
Datasource,
|
||||
|
@ -50,6 +51,7 @@ export interface EditorFrameStartPlugins {
|
|||
expressions: ExpressionsStart;
|
||||
charts: ChartsPluginSetup;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
}
|
||||
|
||||
export interface EditorFramePlugins {
|
||||
|
@ -57,6 +59,7 @@ export interface EditorFramePlugins {
|
|||
uiSettings: IUiSettingsClient;
|
||||
storage: IStorageWrapper;
|
||||
timefilter: TimefilterContract;
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
}
|
||||
|
||||
async function collectAsyncDefinitions<T extends { id: string }>(
|
||||
|
|
|
@ -186,6 +186,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
indexPatterns: {},
|
||||
indexPatternRefs: [],
|
||||
activeVisualizationState: null,
|
||||
}),
|
||||
...props,
|
||||
};
|
||||
|
@ -382,6 +383,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
indexPatterns: {},
|
||||
indexPatternRefs: [],
|
||||
activeVisualizationState: null,
|
||||
}),
|
||||
},
|
||||
{ id: '123' } as LensEmbeddableInput
|
||||
|
@ -435,6 +437,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
indexPatterns: {},
|
||||
indexPatternRefs: [],
|
||||
activeVisualizationState: null,
|
||||
}),
|
||||
},
|
||||
{ id: '123', searchSessionId: 'firstSession' } as LensEmbeddableInput
|
||||
|
@ -969,6 +972,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
indexPatterns: {},
|
||||
indexPatternRefs: [],
|
||||
activeVisualizationState: null,
|
||||
}),
|
||||
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
|
||||
},
|
||||
|
@ -1063,6 +1067,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
indexPatterns: {},
|
||||
indexPatternRefs: [],
|
||||
activeVisualizationState: null,
|
||||
}),
|
||||
},
|
||||
{ id: '123', timeRange, query, filters } as LensEmbeddableInput
|
||||
|
@ -1161,6 +1166,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
indexPatterns: {},
|
||||
indexPatternRefs: [],
|
||||
activeVisualizationState: null,
|
||||
}),
|
||||
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
|
||||
},
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
import type { Start as InspectorStart } from '@kbn/inspector-plugin/public';
|
||||
|
||||
import { merge, Subscription } from 'rxjs';
|
||||
import { toExpression, Ast } from '@kbn/interpreter';
|
||||
import { toExpression } from '@kbn/interpreter';
|
||||
import { DefaultInspectorAdapters, ErrorLike, RenderMode } from '@kbn/expressions-plugin/common';
|
||||
import { map, distinctUntilChanged, skip, debounceTime } from 'rxjs/operators';
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
|
@ -125,6 +125,7 @@ import {
|
|||
getApplicationUserMessages,
|
||||
} from '../app_plugin/get_application_user_messages';
|
||||
import { MessageList } from '../editor_frame_service/editor_frame/workspace_panel/message_list';
|
||||
import type { DocumentToExpressionReturnType } from '../editor_frame_service/editor_frame';
|
||||
import { EmbeddableFeatureBadge } from './embeddable_info_badges';
|
||||
import { getDatasourceLayers } from '../state_management/utils';
|
||||
|
||||
|
@ -191,11 +192,7 @@ export interface LensEmbeddableOutput extends EmbeddableOutput {
|
|||
export interface LensEmbeddableDeps {
|
||||
attributeService: LensAttributeService;
|
||||
data: DataPublicPluginStart;
|
||||
documentToExpression: (doc: Document) => Promise<{
|
||||
ast: Ast | null;
|
||||
indexPatterns: IndexPatternMap;
|
||||
indexPatternRefs: IndexPatternRef[];
|
||||
}>;
|
||||
documentToExpression: (doc: Document) => Promise<DocumentToExpressionReturnType>;
|
||||
injectFilterReferences: FilterManager['inject'];
|
||||
visualizationMap: VisualizationMap;
|
||||
datasourceMap: DatasourceMap;
|
||||
|
@ -278,8 +275,14 @@ const getExpressionFromDocument = async (
|
|||
document: Document,
|
||||
documentToExpression: LensEmbeddableDeps['documentToExpression']
|
||||
) => {
|
||||
const { ast, indexPatterns, indexPatternRefs } = await documentToExpression(document);
|
||||
return { ast: ast ? toExpression(ast) : null, indexPatterns, indexPatternRefs };
|
||||
const { ast, indexPatterns, indexPatternRefs, activeVisualizationState } =
|
||||
await documentToExpression(document);
|
||||
return {
|
||||
ast: ast ? toExpression(ast) : null,
|
||||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
activeVisualizationState,
|
||||
};
|
||||
};
|
||||
|
||||
function getViewUnderlyingDataArgs({
|
||||
|
@ -432,6 +435,8 @@ export class Embeddable
|
|||
|
||||
private viewUnderlyingDataArgs?: ViewUnderlyingDataArgs;
|
||||
|
||||
private activeVisualizationState?: unknown;
|
||||
|
||||
constructor(
|
||||
private deps: LensEmbeddableDeps,
|
||||
initialInput: LensEmbeddableInput,
|
||||
|
@ -560,20 +565,12 @@ export class Embeddable
|
|||
return this.deps.visualizationMap[this.activeVisualizationId];
|
||||
}
|
||||
|
||||
private get activeVisualizationState() {
|
||||
if (!this.activeVisualization) return;
|
||||
return this.activeVisualization.initialize(
|
||||
() => '',
|
||||
this.savedVis?.state.visualization,
|
||||
undefined,
|
||||
this.savedVis?.references
|
||||
);
|
||||
}
|
||||
|
||||
private indexPatterns: IndexPatternMap = {};
|
||||
|
||||
private indexPatternRefs: IndexPatternRef[] = [];
|
||||
|
||||
// TODO - consider getting this from the persistedStateToExpression function
|
||||
// where it is already computed
|
||||
private get activeDatasourceState(): undefined | unknown {
|
||||
if (!this.activeDatasourceId || !this.activeDatasource) return;
|
||||
|
||||
|
@ -733,14 +730,13 @@ export class Embeddable
|
|||
};
|
||||
|
||||
try {
|
||||
const { ast, indexPatterns, indexPatternRefs } = await getExpressionFromDocument(
|
||||
this.savedVis,
|
||||
this.deps.documentToExpression
|
||||
);
|
||||
const { ast, indexPatterns, indexPatternRefs, activeVisualizationState } =
|
||||
await getExpressionFromDocument(this.savedVis, this.deps.documentToExpression);
|
||||
|
||||
this.expression = ast;
|
||||
this.indexPatterns = indexPatterns;
|
||||
this.indexPatternRefs = indexPatternRefs;
|
||||
this.activeVisualizationState = activeVisualizationState;
|
||||
} catch {
|
||||
// nothing, errors should be reported via getUserMessages
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import type {
|
|||
} from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RecursiveReadonly } from '@kbn/utility-types';
|
||||
import { Ast } from '@kbn/interpreter';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import { DataPublicPluginStart, FilterManager, TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
|
@ -32,7 +31,8 @@ import type { Document } from '../persistence/saved_object_store';
|
|||
import type { LensAttributeService } from '../lens_attribute_service';
|
||||
import { DOC_TYPE } from '../../common/constants';
|
||||
import { extract, inject } from '../../common/embeddable_factory';
|
||||
import type { DatasourceMap, IndexPatternMap, IndexPatternRef, VisualizationMap } from '../types';
|
||||
import type { DatasourceMap, VisualizationMap } from '../types';
|
||||
import type { DocumentToExpressionReturnType } from '../editor_frame_service/editor_frame';
|
||||
|
||||
export interface LensEmbeddableStartServices {
|
||||
data: DataPublicPluginStart;
|
||||
|
@ -46,11 +46,7 @@ export interface LensEmbeddableStartServices {
|
|||
dataViews: DataViewsContract;
|
||||
uiActions?: UiActionsStart;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
documentToExpression: (doc: Document) => Promise<{
|
||||
ast: Ast | null;
|
||||
indexPatterns: IndexPatternMap;
|
||||
indexPatternRefs: IndexPatternRef[];
|
||||
}>;
|
||||
documentToExpression: (doc: Document) => Promise<DocumentToExpressionReturnType>;
|
||||
injectFilterReferences: FilterManager['inject'];
|
||||
visualizationMap: VisualizationMap;
|
||||
datasourceMap: DatasourceMap;
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import {
|
||||
|
@ -19,14 +18,12 @@ export function createIndexPatternServiceMock({
|
|||
core = coreMock.createStart(),
|
||||
dataViews = dataViewPluginMocks.createStartContract(),
|
||||
uiActions = uiActionsPluginMock.createStartContract(),
|
||||
data = dataPluginMock.createStartContract(),
|
||||
updateIndexPatterns = jest.fn(),
|
||||
replaceIndexPattern = jest.fn(),
|
||||
}: Partial<IndexPatternServiceProps> = {}): IndexPatternServiceAPI {
|
||||
return createIndexPatternService({
|
||||
core,
|
||||
dataViews,
|
||||
data,
|
||||
updateIndexPatterns,
|
||||
replaceIndexPattern,
|
||||
uiActions,
|
||||
|
|
|
@ -29,6 +29,7 @@ import type { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public';
|
|||
|
||||
import { presentationUtilPluginMock } from '@kbn/presentation-util-plugin/public/mocks';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import type { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import { settingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
|
||||
import type { LensAttributeService } from '../lens_attribute_service';
|
||||
import type {
|
||||
|
@ -184,5 +185,6 @@ export function makeDefaultServices(
|
|||
dataViewEditor: indexPatternEditorPluginMock.createStartContract(),
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
docLinks: startMock.docLinks,
|
||||
eventAnnotationService: {} as EventAnnotationServiceType,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ export function createMockVisualization(id = 'testVis'): jest.Mocked<Visualizati
|
|||
switchVisualizationType: jest.fn((_, x) => x),
|
||||
getSuggestions: jest.fn((_options) => []),
|
||||
getRenderEventCounters: jest.fn((_state) => []),
|
||||
initialize: jest.fn((_frame, _state?) => ({ newState: 'newState' })),
|
||||
initialize: jest.fn((_addNewLayer, _state) => ({ newState: 'newState' })),
|
||||
getConfiguration: jest.fn((props) => ({
|
||||
groups: [
|
||||
{
|
||||
|
|
|
@ -33,7 +33,10 @@ import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'
|
|||
import type { UrlForwardingSetup } from '@kbn/url-forwarding-plugin/public';
|
||||
import type { GlobalSearchPluginSetup } from '@kbn/global-search-plugin/public';
|
||||
import type { ChartsPluginSetup, ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||
import type { EventAnnotationPluginSetup } from '@kbn/event-annotation-plugin/public';
|
||||
import type {
|
||||
EventAnnotationPluginStart,
|
||||
EventAnnotationServiceType,
|
||||
} from '@kbn/event-annotation-plugin/public';
|
||||
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
|
||||
import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public';
|
||||
import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
|
||||
|
@ -131,7 +134,6 @@ export interface LensPluginSetupDependencies {
|
|||
embeddable?: EmbeddableSetup;
|
||||
visualizations: VisualizationsSetup;
|
||||
charts: ChartsPluginSetup;
|
||||
eventAnnotation: EventAnnotationPluginSetup;
|
||||
globalSearch?: GlobalSearchPluginSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
uiActionsEnhanced: AdvancedUiActionsSetup;
|
||||
|
@ -151,7 +153,7 @@ export interface LensPluginStartDependencies {
|
|||
visualizations: VisualizationsStart;
|
||||
embeddable: EmbeddableStart;
|
||||
charts: ChartsPluginStart;
|
||||
eventAnnotation: EventAnnotationPluginSetup;
|
||||
eventAnnotation: EventAnnotationPluginStart;
|
||||
savedObjectsTagging?: SavedObjectTaggingPluginStart;
|
||||
presentationUtil: PresentationUtilPluginStart;
|
||||
dataViewFieldEditor: IndexPatternFieldEditorStart;
|
||||
|
@ -161,6 +163,7 @@ export interface LensPluginStartDependencies {
|
|||
usageCollection?: UsageCollectionStart;
|
||||
docLinks: DocLinksStart;
|
||||
share?: SharePluginStart;
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
contentManagement: ContentManagementPublicStart;
|
||||
}
|
||||
|
||||
|
@ -283,7 +286,6 @@ export class LensPlugin {
|
|||
embeddable,
|
||||
visualizations,
|
||||
charts,
|
||||
eventAnnotation,
|
||||
globalSearch,
|
||||
usageCollection,
|
||||
uiActionsEnhanced,
|
||||
|
@ -293,7 +295,7 @@ export class LensPlugin {
|
|||
) {
|
||||
const startServices = createStartServicesGetter(core.getStartServices);
|
||||
|
||||
const getStartServices = async (): Promise<LensEmbeddableStartServices> => {
|
||||
const getStartServicesForEmbeddable = async (): Promise<LensEmbeddableStartServices> => {
|
||||
const { getLensAttributeService, setUsageCollectionStart, initMemoizedErrorNotification } =
|
||||
await import('./async_services');
|
||||
const { core: coreStart, plugins } = startServices();
|
||||
|
@ -304,11 +306,11 @@ export class LensPlugin {
|
|||
charts,
|
||||
expressions,
|
||||
fieldFormats,
|
||||
plugins.fieldFormats.deserialize,
|
||||
eventAnnotation
|
||||
plugins.fieldFormats.deserialize
|
||||
);
|
||||
const visualizationMap = await this.editorFrameService!.loadVisualizations();
|
||||
const datasourceMap = await this.editorFrameService!.loadDatasources();
|
||||
const eventAnnotationService = await plugins.eventAnnotation.getService();
|
||||
|
||||
if (plugins.usageCollection) {
|
||||
setUsageCollectionStart(plugins.usageCollection);
|
||||
|
@ -330,6 +332,7 @@ export class LensPlugin {
|
|||
storage: new Storage(localStorage),
|
||||
uiSettings: core.uiSettings,
|
||||
timefilter: plugins.data.query.timefilter.timefilter,
|
||||
eventAnnotationService,
|
||||
}),
|
||||
injectFilterReferences: data.query.filterManager.inject.bind(data.query.filterManager),
|
||||
visualizationMap,
|
||||
|
@ -345,7 +348,10 @@ export class LensPlugin {
|
|||
};
|
||||
|
||||
if (embeddable) {
|
||||
embeddable.registerEmbeddableFactory('lens', new EmbeddableFactory(getStartServices));
|
||||
embeddable.registerEmbeddableFactory(
|
||||
'lens',
|
||||
new EmbeddableFactory(getStartServicesForEmbeddable)
|
||||
);
|
||||
}
|
||||
|
||||
if (share) {
|
||||
|
@ -407,8 +413,7 @@ export class LensPlugin {
|
|||
charts,
|
||||
expressions,
|
||||
fieldFormats,
|
||||
deps.fieldFormats.deserialize,
|
||||
eventAnnotation
|
||||
deps.fieldFormats.deserialize
|
||||
);
|
||||
|
||||
const {
|
||||
|
@ -458,8 +463,7 @@ export class LensPlugin {
|
|||
charts,
|
||||
expressions,
|
||||
fieldFormats,
|
||||
plugins.fieldFormats.deserialize,
|
||||
eventAnnotation
|
||||
plugins.fieldFormats.deserialize
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -484,8 +488,7 @@ export class LensPlugin {
|
|||
charts: ChartsPluginSetup,
|
||||
expressions: ExpressionsServiceSetup,
|
||||
fieldFormats: FieldFormatsSetup,
|
||||
formatFactory: FormatFactory,
|
||||
eventAnnotation: EventAnnotationPluginSetup
|
||||
formatFactory: FormatFactory
|
||||
) {
|
||||
const {
|
||||
DatatableVisualization,
|
||||
|
@ -523,7 +526,6 @@ export class LensPlugin {
|
|||
charts,
|
||||
editorFrame: editorFrameSetupInterface,
|
||||
formatFactory,
|
||||
eventAnnotation,
|
||||
};
|
||||
this.FormBasedDatasource.setup(core, dependencies);
|
||||
this.TextBasedDatasource.setup(core, dependencies);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import '../../../mixins';
|
||||
@import '../mixins';
|
||||
|
||||
.lnsDimensionContainer {
|
||||
// Use the EuiFlyout style
|
|
@ -19,7 +19,7 @@ import {
|
|||
EuiFocusTrap,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from '../../../utils';
|
||||
import { DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from '../utils';
|
||||
|
||||
function fromExcludedClickTarget(event: Event) {
|
||||
for (
|
||||
|
@ -44,14 +44,18 @@ export function FlyoutContainer({
|
|||
handleClose,
|
||||
isFullscreen,
|
||||
panelRef,
|
||||
panelContainerRef,
|
||||
children,
|
||||
customFooter,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
handleClose: () => boolean;
|
||||
children: React.ReactElement | null;
|
||||
groupLabel: string;
|
||||
isFullscreen: boolean;
|
||||
panelRef: (el: HTMLDivElement) => void;
|
||||
isFullscreen?: boolean;
|
||||
panelRef?: (el: HTMLDivElement) => void;
|
||||
panelContainerRef?: (el: HTMLDivElement) => void;
|
||||
customFooter?: React.ReactElement;
|
||||
}) {
|
||||
const [focusTrapIsEnabled, setFocusTrapIsEnabled] = useState(false);
|
||||
|
||||
|
@ -91,6 +95,7 @@ export function FlyoutContainer({
|
|||
onEscapeKey={closeFlyout}
|
||||
>
|
||||
<div
|
||||
ref={panelContainerRef}
|
||||
role="dialog"
|
||||
aria-labelledby="lnsDimensionContainerTitle"
|
||||
className="lnsDimensionContainer euiFlyout"
|
||||
|
@ -137,19 +142,21 @@ export function FlyoutContainer({
|
|||
|
||||
<div className="lnsDimensionContainer__content">{children}</div>
|
||||
|
||||
<EuiFlyoutFooter className="lnsDimensionContainer__footer">
|
||||
<EuiButtonEmpty
|
||||
flush="left"
|
||||
size="s"
|
||||
iconType="cross"
|
||||
onClick={closeFlyout}
|
||||
data-test-subj="lns-indexPattern-dimensionContainerClose"
|
||||
>
|
||||
{i18n.translate('xpack.lens.dimensionContainer.close', {
|
||||
defaultMessage: 'Close',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlyoutFooter>
|
||||
{customFooter || (
|
||||
<EuiFlyoutFooter className="lnsDimensionContainer__footer">
|
||||
<EuiButtonEmpty
|
||||
flush="left"
|
||||
size="s"
|
||||
iconType="cross"
|
||||
onClick={closeFlyout}
|
||||
data-test-subj="lns-indexPattern-dimensionContainerClose"
|
||||
>
|
||||
{i18n.translate('xpack.lens.dimensionContainer.close', {
|
||||
defaultMessage: 'Close',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlyoutFooter>
|
||||
)}
|
||||
</div>
|
||||
</EuiFocusTrap>
|
||||
</div>
|
|
@ -7,8 +7,17 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle, IconType } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
export const StaticHeader = ({ label, icon }: { label: string; icon?: IconType }) => {
|
||||
export const StaticHeader = ({
|
||||
label,
|
||||
icon,
|
||||
indicator,
|
||||
}: {
|
||||
label: string;
|
||||
icon?: IconType;
|
||||
indicator?: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
|
@ -21,10 +30,25 @@ export const StaticHeader = ({ label, icon }: { label: string; icon?: IconType }
|
|||
<EuiIcon type={icon} />{' '}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow>
|
||||
<EuiFlexItem
|
||||
grow
|
||||
css={css`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<EuiTitle size="xxs">
|
||||
<h5>{label}</h5>
|
||||
</EuiTitle>
|
||||
{indicator && (
|
||||
<div
|
||||
css={css`
|
||||
padding-bottom: 3px;
|
||||
`}
|
||||
>
|
||||
{indicator}
|
||||
</div>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -4,6 +4,7 @@ exports[`Initializing the store should initialize all datasources with state fro
|
|||
Object {
|
||||
"lens": Object {
|
||||
"activeDatasourceId": "testDatasource",
|
||||
"annotationGroups": Object {},
|
||||
"dataViews": Object {
|
||||
"indexPatternRefs": Array [],
|
||||
"indexPatterns": Object {},
|
||||
|
|
|
@ -45,6 +45,8 @@ export const {
|
|||
addLayer,
|
||||
setLayerDefaultDimension,
|
||||
removeDimension,
|
||||
setIsLoadLibraryVisible,
|
||||
registerLibraryAnnotationGroup,
|
||||
} = lensActions;
|
||||
|
||||
export const makeConfigureStore = (
|
||||
|
|
|
@ -114,6 +114,7 @@ export function loadInitial(
|
|||
const loaderSharedArgs = {
|
||||
dataViews: lensServices.dataViews,
|
||||
storage: lensServices.storage,
|
||||
eventAnnotationService: lensServices.eventAnnotationService,
|
||||
defaultIndexPatternId: lensServices.uiSettings.get('defaultIndex'),
|
||||
};
|
||||
|
||||
|
@ -159,39 +160,48 @@ export function loadInitial(
|
|||
isFullEditor: true,
|
||||
}
|
||||
)
|
||||
.then(({ datasourceStates, visualizationState, indexPatterns, indexPatternRefs }) => {
|
||||
const currentSessionId =
|
||||
initialStateFromLocator?.searchSessionId || data.search.session.getSessionId();
|
||||
store.dispatch(
|
||||
setState({
|
||||
isSaveable: true,
|
||||
filters: initialStateFromLocator.filters || data.query.filterManager.getFilters(),
|
||||
query: initialStateFromLocator.query || emptyState.query,
|
||||
searchSessionId: currentSessionId,
|
||||
activeDatasourceId: emptyState.activeDatasourceId,
|
||||
visualization: {
|
||||
activeId: emptyState.visualization.activeId,
|
||||
state: visualizationState,
|
||||
},
|
||||
dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs),
|
||||
datasourceStates: Object.entries(datasourceStates).reduce(
|
||||
(state, [datasourceId, datasourceState]) => ({
|
||||
...state,
|
||||
[datasourceId]: {
|
||||
...datasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
),
|
||||
isLoading: false,
|
||||
})
|
||||
);
|
||||
.then(
|
||||
({
|
||||
datasourceStates,
|
||||
visualizationState,
|
||||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
annotationGroups,
|
||||
}) => {
|
||||
const currentSessionId =
|
||||
initialStateFromLocator?.searchSessionId || data.search.session.getSessionId();
|
||||
store.dispatch(
|
||||
setState({
|
||||
isSaveable: true,
|
||||
filters: initialStateFromLocator.filters || data.query.filterManager.getFilters(),
|
||||
query: initialStateFromLocator.query || emptyState.query,
|
||||
searchSessionId: currentSessionId,
|
||||
activeDatasourceId: emptyState.activeDatasourceId,
|
||||
visualization: {
|
||||
activeId: emptyState.visualization.activeId,
|
||||
state: visualizationState,
|
||||
},
|
||||
dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs),
|
||||
datasourceStates: Object.entries(datasourceStates).reduce(
|
||||
(state, [datasourceId, datasourceState]) => ({
|
||||
...state,
|
||||
[datasourceId]: {
|
||||
...datasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
),
|
||||
isLoading: false,
|
||||
annotationGroups,
|
||||
})
|
||||
);
|
||||
|
||||
if (autoApplyDisabled) {
|
||||
store.dispatch(disableAutoApply());
|
||||
if (autoApplyDisabled) {
|
||||
store.dispatch(disableAutoApply());
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((e: { message: string }) => {
|
||||
notifications.toasts.addDanger({
|
||||
title: e.message,
|
||||
|
@ -304,52 +314,62 @@ export function loadInitial(
|
|||
references: [...doc.references, ...(doc.state.internalReferences || [])],
|
||||
initialContext,
|
||||
dataViews: lensServices.dataViews,
|
||||
eventAnnotationService: lensServices.eventAnnotationService,
|
||||
storage: lensServices.storage,
|
||||
adHocDataViews: doc.state.adHocDataViews,
|
||||
defaultIndexPatternId: lensServices.uiSettings.get('defaultIndex'),
|
||||
},
|
||||
{ isFullEditor: true }
|
||||
)
|
||||
.then(({ datasourceStates, visualizationState, indexPatterns, indexPatternRefs }) => {
|
||||
const currentSessionId = data.search.session.getSessionId();
|
||||
store.dispatch(
|
||||
setState({
|
||||
isSaveable: true,
|
||||
sharingSavedObjectProps,
|
||||
filters: data.query.filterManager.getFilters(),
|
||||
query: doc.state.query,
|
||||
searchSessionId:
|
||||
dashboardFeatureFlag.allowByValueEmbeddables &&
|
||||
Boolean(embeddableEditorIncomingState?.originatingApp) &&
|
||||
!(initialInput as LensByReferenceInput)?.savedObjectId &&
|
||||
currentSessionId
|
||||
? currentSessionId
|
||||
: data.search.session.start(),
|
||||
persistedDoc: doc,
|
||||
activeDatasourceId: getInitialDatasourceId(datasourceMap, doc),
|
||||
visualization: {
|
||||
activeId: doc.visualizationType,
|
||||
state: visualizationState,
|
||||
},
|
||||
dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs),
|
||||
datasourceStates: Object.entries(datasourceStates).reduce(
|
||||
(state, [datasourceId, datasourceState]) => ({
|
||||
...state,
|
||||
[datasourceId]: {
|
||||
...datasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
),
|
||||
isLoading: false,
|
||||
})
|
||||
);
|
||||
.then(
|
||||
({
|
||||
datasourceStates,
|
||||
visualizationState,
|
||||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
annotationGroups,
|
||||
}) => {
|
||||
const currentSessionId = data.search.session.getSessionId();
|
||||
store.dispatch(
|
||||
setState({
|
||||
isSaveable: true,
|
||||
sharingSavedObjectProps,
|
||||
filters: data.query.filterManager.getFilters(),
|
||||
query: doc.state.query,
|
||||
searchSessionId:
|
||||
dashboardFeatureFlag.allowByValueEmbeddables &&
|
||||
Boolean(embeddableEditorIncomingState?.originatingApp) &&
|
||||
!(initialInput as LensByReferenceInput)?.savedObjectId &&
|
||||
currentSessionId
|
||||
? currentSessionId
|
||||
: data.search.session.start(),
|
||||
persistedDoc: doc,
|
||||
activeDatasourceId: getInitialDatasourceId(datasourceMap, doc),
|
||||
visualization: {
|
||||
activeId: doc.visualizationType,
|
||||
state: visualizationState,
|
||||
},
|
||||
dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs),
|
||||
datasourceStates: Object.entries(datasourceStates).reduce(
|
||||
(state, [datasourceId, datasourceState]) => ({
|
||||
...state,
|
||||
[datasourceId]: {
|
||||
...datasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
),
|
||||
isLoading: false,
|
||||
annotationGroups,
|
||||
})
|
||||
);
|
||||
|
||||
if (autoApplyDisabled) {
|
||||
store.dispatch(disableAutoApply());
|
||||
if (autoApplyDisabled) {
|
||||
store.dispatch(disableAutoApply());
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((e: { message: string }) =>
|
||||
notifications.toasts.addDanger({
|
||||
title: e.message,
|
||||
|
|
|
@ -362,6 +362,7 @@ describe('lensSlice', () => {
|
|||
addLayer({
|
||||
layerId: 'foo',
|
||||
layerType: LayerTypes.DATA,
|
||||
extraArg: 'some arg',
|
||||
})
|
||||
);
|
||||
const state = customStore.getState().lens;
|
||||
|
@ -405,6 +406,7 @@ describe('lensSlice', () => {
|
|||
addLayer({
|
||||
layerId: 'foo',
|
||||
layerType: layerTypes.DATA,
|
||||
extraArg: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
|||
import { mapValues, uniq } from 'lodash';
|
||||
import { Query } from '@kbn/es-query';
|
||||
import { History } from 'history';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { EventAnnotationGroupConfig } from '@kbn/event-annotation-plugin/common';
|
||||
import { LensEmbeddableInput } from '..';
|
||||
import { TableInspectorAdapter } from '../editor_frame_service/types';
|
||||
import type {
|
||||
|
@ -24,7 +26,6 @@ import type { DataViewsState, LensAppState, LensStoreDeps, VisualizationState }
|
|||
import type { Datasource, Visualization } from '../types';
|
||||
import { generateId } from '../id_generator';
|
||||
import type { LayerType } from '../../common/types';
|
||||
import { getLayerType } from '../editor_frame_service/editor_frame/config_panel/add_layer';
|
||||
import { getVisualizeFieldSuggestions } from '../editor_frame_service/editor_frame/suggestion_helpers';
|
||||
import type { FramePublicAPI, LensEditContextMapping, LensEditEvent } from '../types';
|
||||
import { selectDataViews, selectFramePublicAPI } from './selectors';
|
||||
|
@ -50,6 +51,7 @@ export const initialState: LensAppState = {
|
|||
indexPatternRefs: [],
|
||||
indexPatterns: {},
|
||||
},
|
||||
annotationGroups: {},
|
||||
};
|
||||
|
||||
export const getPreloadedState = ({
|
||||
|
@ -161,6 +163,7 @@ export const switchVisualization = createAction<{
|
|||
}>('lens/switchVisualization');
|
||||
export const rollbackSuggestion = createAction<void>('lens/rollbackSuggestion');
|
||||
export const setToggleFullscreen = createAction<void>('lens/setToggleFullscreen');
|
||||
export const setIsLoadLibraryVisible = createAction<boolean>('lens/setIsLoadLibraryVisible');
|
||||
export const submitSuggestion = createAction<void>('lens/submitSuggestion');
|
||||
export const switchDatasource = createAction<{
|
||||
newDatasourceId: string;
|
||||
|
@ -212,6 +215,8 @@ export const cloneLayer = createAction(
|
|||
export const addLayer = createAction<{
|
||||
layerId: string;
|
||||
layerType: LayerType;
|
||||
extraArg: unknown;
|
||||
ignoreInitialValues?: boolean;
|
||||
}>('lens/addLayer');
|
||||
|
||||
export const setLayerDefaultDimension = createAction<{
|
||||
|
@ -238,6 +243,10 @@ export const removeDimension = createAction<{
|
|||
columnId: string;
|
||||
datasourceId?: string;
|
||||
}>('lens/removeDimension');
|
||||
export const registerLibraryAnnotationGroup = createAction<{
|
||||
group: EventAnnotationGroupConfig;
|
||||
id: string;
|
||||
}>('lens/registerLibraryAnnotationGroup');
|
||||
|
||||
export const lensActions = {
|
||||
setState,
|
||||
|
@ -254,6 +263,7 @@ export const lensActions = {
|
|||
switchVisualization,
|
||||
rollbackSuggestion,
|
||||
setToggleFullscreen,
|
||||
setIsLoadLibraryVisible,
|
||||
submitSuggestion,
|
||||
switchDatasource,
|
||||
switchAndCleanDatasource,
|
||||
|
@ -271,6 +281,7 @@ export const lensActions = {
|
|||
changeIndexPattern,
|
||||
removeDimension,
|
||||
syncLinkedDimensions,
|
||||
registerLibraryAnnotationGroup,
|
||||
};
|
||||
|
||||
export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
||||
|
@ -1031,11 +1042,13 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
[addLayer.type]: (
|
||||
state,
|
||||
{
|
||||
payload: { layerId, layerType },
|
||||
payload: { layerId, layerType, extraArg, ignoreInitialValues },
|
||||
}: {
|
||||
payload: {
|
||||
layerId: string;
|
||||
layerType: LayerType;
|
||||
extraArg: unknown;
|
||||
ignoreInitialValues: boolean;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
|
@ -1053,7 +1066,8 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
state.visualization.state,
|
||||
layerId,
|
||||
layerType,
|
||||
currentDataViewsId
|
||||
currentDataViewsId,
|
||||
extraArg
|
||||
);
|
||||
|
||||
const framePublicAPI = selectFramePublicAPI({ lens: current(state) }, datasourceMap);
|
||||
|
@ -1075,15 +1089,17 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
)
|
||||
: state.datasourceStates[state.activeDatasourceId].state;
|
||||
|
||||
const { activeDatasourceState, activeVisualizationState } = addInitialValueIfAvailable({
|
||||
datasourceState,
|
||||
visualizationState,
|
||||
framePublicAPI,
|
||||
activeVisualization,
|
||||
activeDatasource,
|
||||
layerId,
|
||||
layerType,
|
||||
});
|
||||
const { activeDatasourceState, activeVisualizationState } = ignoreInitialValues
|
||||
? { activeDatasourceState: datasourceState, activeVisualizationState: visualizationState }
|
||||
: addInitialValueIfAvailable({
|
||||
datasourceState,
|
||||
visualizationState,
|
||||
framePublicAPI,
|
||||
activeVisualization,
|
||||
activeDatasource,
|
||||
layerId,
|
||||
layerType,
|
||||
});
|
||||
|
||||
state.visualization.state = activeVisualizationState;
|
||||
state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState;
|
||||
|
@ -1115,7 +1131,8 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
|
||||
const activeDatasource = datasourceMap[state.activeDatasourceId];
|
||||
const activeVisualization = visualizationMap[state.visualization.activeId];
|
||||
const layerType = getLayerType(activeVisualization, state.visualization.state, layerId);
|
||||
const layerType =
|
||||
activeVisualization.getLayerType(layerId, state.visualization.state) || LayerTypes.DATA;
|
||||
const { activeDatasourceState, activeVisualizationState } = addInitialValueIfAvailable({
|
||||
datasourceState: state.datasourceStates[state.activeDatasourceId].state,
|
||||
visualizationState: state.visualization.state,
|
||||
|
@ -1197,6 +1214,16 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
remove({ columnId: linkedDimension.columnId, layerId: linkedDimension.layerId })
|
||||
);
|
||||
},
|
||||
[registerLibraryAnnotationGroup.type]: (
|
||||
state,
|
||||
{
|
||||
payload: { group, id },
|
||||
}: {
|
||||
payload: { group: EventAnnotationGroupConfig; id: string };
|
||||
}
|
||||
) => {
|
||||
state.annotationGroups[id] = group;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import type {
|
|||
VisualizeEditorContext,
|
||||
IndexPattern,
|
||||
IndexPatternRef,
|
||||
AnnotationGroups,
|
||||
} from '../types';
|
||||
export interface VisualizationState {
|
||||
activeId: string | null;
|
||||
|
@ -63,6 +64,7 @@ export interface LensAppState extends EditorFrameState {
|
|||
sharingSavedObjectProps?: Omit<SharingSavedObjectProps, 'sourceId'>;
|
||||
// Dataview/Indexpattern management has moved in here from datasource
|
||||
dataViews: DataViewsState;
|
||||
annotationGroups: AnnotationGroups;
|
||||
}
|
||||
|
||||
export type DispatchSetState = (state: Partial<LensAppState>) => {
|
||||
|
|
|
@ -39,6 +39,7 @@ import { SearchRequest } from '@kbn/data-plugin/public';
|
|||
import { estypes } from '@elastic/elasticsearch';
|
||||
import React from 'react';
|
||||
import { CellValueContext } from '@kbn/embeddable-plugin/public';
|
||||
import { EventAnnotationGroupConfig } from '@kbn/event-annotation-plugin/common';
|
||||
import type {
|
||||
DraggingIdentifier,
|
||||
DragDropIdentifier,
|
||||
|
@ -133,8 +134,10 @@ export interface EditorFrameSetup {
|
|||
registerDatasource: <T, P>(
|
||||
datasource: Datasource<T, P> | (() => Promise<Datasource<T, P>>)
|
||||
) => void;
|
||||
registerVisualization: <T, P>(
|
||||
visualization: Visualization<T, P> | (() => Promise<Visualization<T, P>>)
|
||||
registerVisualization: <T, P, ExtraAppendLayerArg>(
|
||||
visualization:
|
||||
| Visualization<T, P, ExtraAppendLayerArg>
|
||||
| (() => Promise<Visualization<T, P, ExtraAppendLayerArg>>)
|
||||
) => void;
|
||||
}
|
||||
|
||||
|
@ -604,18 +607,18 @@ export interface DatasourceDataPanelProps<T = unknown> {
|
|||
|
||||
/** @internal **/
|
||||
export interface LayerAction {
|
||||
id: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
execute: () => void | Promise<void>;
|
||||
execute: (mountingPoint: HTMLDivElement | null | undefined) => void | Promise<void>;
|
||||
icon: IconType;
|
||||
color?: EuiButtonIconProps['color'];
|
||||
isCompatible: boolean;
|
||||
disabled?: boolean;
|
||||
'data-test-subj'?: string;
|
||||
order: number;
|
||||
showOutsideList?: boolean;
|
||||
}
|
||||
|
||||
export type LayerActionFromVisualization = Omit<LayerAction, 'execute'>;
|
||||
|
||||
interface SharedDimensionProps {
|
||||
/** Visualizations can restrict operations based on their own rules.
|
||||
* For example, limiting to only bucketed or only numeric operations.
|
||||
|
@ -1002,7 +1005,29 @@ interface VisualizationStateFromContextChangeProps {
|
|||
context: VisualizeEditorContext;
|
||||
}
|
||||
|
||||
export interface Visualization<T = unknown, P = T> {
|
||||
export type AddLayerFunction<T = unknown> = (
|
||||
layerType: LayerType,
|
||||
extraArg?: T,
|
||||
ignoreInitialValues?: boolean
|
||||
) => void;
|
||||
|
||||
export type AnnotationGroups = Record<string, EventAnnotationGroupConfig>;
|
||||
|
||||
export interface VisualizationLayerDescription {
|
||||
type: LayerType;
|
||||
label: string;
|
||||
icon?: IconType;
|
||||
noDatasource?: boolean;
|
||||
disabled?: boolean;
|
||||
toolTipContent?: string;
|
||||
initialDimensions?: Array<{
|
||||
columnId: string;
|
||||
groupId: string;
|
||||
staticValue?: unknown;
|
||||
autoTimeField?: boolean;
|
||||
}>;
|
||||
}
|
||||
export interface Visualization<T = unknown, P = T, ExtraAppendLayerArg = unknown> {
|
||||
/** Plugin ID, such as "lnsXY" */
|
||||
id: string;
|
||||
|
||||
|
@ -1012,13 +1037,16 @@ export interface Visualization<T = unknown, P = T> {
|
|||
* - Loading from a saved visualization
|
||||
* - When using suggestions, the suggested state is passed in
|
||||
*/
|
||||
initialize: (
|
||||
addNewLayer: () => string,
|
||||
state?: T | P,
|
||||
mainPalette?: PaletteOutput,
|
||||
references?: SavedObjectReference[],
|
||||
initialContext?: VisualizeFieldContext | VisualizeEditorContext
|
||||
) => T;
|
||||
initialize: {
|
||||
(addNewLayer: () => string, nonPersistedState?: T, mainPalette?: PaletteOutput): T;
|
||||
(
|
||||
addNewLayer: () => string,
|
||||
persistedState: P,
|
||||
mainPalette?: PaletteOutput,
|
||||
annotationGroups?: AnnotationGroups,
|
||||
references?: SavedObjectReference[]
|
||||
): T;
|
||||
};
|
||||
|
||||
getUsedDataView?: (state: T, layerId: string) => string | undefined;
|
||||
/**
|
||||
|
@ -1065,37 +1093,29 @@ export interface Visualization<T = unknown, P = T> {
|
|||
/** Optional, if the visualization supports multiple layers */
|
||||
removeLayer?: (state: T, layerId: string) => T;
|
||||
/** Track added layers in internal state */
|
||||
appendLayer?: (state: T, layerId: string, type: LayerType, indexPatternId: string) => T;
|
||||
appendLayer?: (
|
||||
state: T,
|
||||
layerId: string,
|
||||
type: LayerType,
|
||||
indexPatternId: string,
|
||||
extraArg?: ExtraAppendLayerArg
|
||||
) => T;
|
||||
|
||||
/** Retrieve a list of supported layer types with initialization data */
|
||||
getSupportedLayers: (
|
||||
state?: T,
|
||||
frame?: Pick<FramePublicAPI, 'datasourceLayers' | 'activeData'>
|
||||
) => Array<{
|
||||
type: LayerType;
|
||||
label: string;
|
||||
icon?: IconType;
|
||||
noDatasource?: boolean;
|
||||
disabled?: boolean;
|
||||
toolTipContent?: string;
|
||||
initialDimensions?: Array<{
|
||||
columnId: string;
|
||||
groupId: string;
|
||||
staticValue?: unknown;
|
||||
autoTimeField?: boolean;
|
||||
}>;
|
||||
canAddViaMenu?: boolean;
|
||||
}>;
|
||||
) => VisualizationLayerDescription[];
|
||||
/**
|
||||
* returns a list of custom actions supported by the visualization layer.
|
||||
* Default actions like delete/clear are not included in this list and are managed by the editor frame
|
||||
* */
|
||||
getSupportedActionsForLayer?: (layerId: string, state: T) => LayerActionFromVisualization[];
|
||||
|
||||
/**
|
||||
* Perform state mutations in response to a layer action
|
||||
*/
|
||||
onLayerAction?: (layerId: string, actionId: string, state: T) => T;
|
||||
getSupportedActionsForLayer?: (
|
||||
layerId: string,
|
||||
state: T,
|
||||
setState: StateSetter<T>,
|
||||
isSaveable?: boolean
|
||||
) => LayerAction[];
|
||||
|
||||
/** returns the type string of the given layer */
|
||||
getLayerType: (layerId: string, state?: T) => LayerType | undefined;
|
||||
|
@ -1217,6 +1237,15 @@ export interface Visualization<T = unknown, P = T> {
|
|||
label: string;
|
||||
hideTooltip?: boolean;
|
||||
}) => JSX.Element | null;
|
||||
getAddLayerButtonComponent?: (props: {
|
||||
supportedLayers: VisualizationLayerDescription[];
|
||||
addLayer: AddLayerFunction;
|
||||
ensureIndexPattern: (specOrId: DataViewSpec | string) => Promise<void>;
|
||||
registerLibraryAnnotationGroup: (groupInfo: {
|
||||
id: string;
|
||||
group: EventAnnotationGroupConfig;
|
||||
}) => void;
|
||||
}) => JSX.Element | null;
|
||||
/**
|
||||
* Creates map of columns ids and unique lables. Used only for noDatasource layers
|
||||
*/
|
||||
|
@ -1280,6 +1309,14 @@ export interface Visualization<T = unknown, P = T> {
|
|||
props: VisualizationStateFromContextChangeProps
|
||||
) => Suggestion<T> | undefined;
|
||||
|
||||
isEqual?: (
|
||||
state1: P,
|
||||
references1: SavedObjectReference[],
|
||||
state2: P,
|
||||
references2: SavedObjectReference[],
|
||||
annotationGroups: AnnotationGroups
|
||||
) => boolean;
|
||||
|
||||
getVisualizationInfo?: (state: T, frame?: FramePublicAPI) => VisualizationInfo;
|
||||
/**
|
||||
* A visualization can return custom dimensions for the reporting tool
|
||||
|
|
|
@ -175,6 +175,7 @@ export function getIndexPatternsIds({
|
|||
}
|
||||
return currentId;
|
||||
}, undefined);
|
||||
|
||||
const referencesIds = references
|
||||
.filter(({ type }) => type === 'index-pattern')
|
||||
.map(({ id }) => id);
|
||||
|
|
|
@ -923,7 +923,6 @@ describe('metric visualization', () => {
|
|||
expect(supportedLayers[0].initialDimensions).toBeUndefined();
|
||||
expect(supportedLayers[0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"canAddViaMenu": true,
|
||||
"disabled": true,
|
||||
"initialDimensions": undefined,
|
||||
"label": "Visualization",
|
||||
|
@ -933,7 +932,6 @@ describe('metric visualization', () => {
|
|||
|
||||
expect({ ...supportedLayers[1], initialDimensions: undefined }).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"canAddViaMenu": true,
|
||||
"disabled": false,
|
||||
"initialDimensions": undefined,
|
||||
"label": "Trendline",
|
||||
|
|
|
@ -420,7 +420,6 @@ export const getMetricVisualization = ({
|
|||
]
|
||||
: undefined,
|
||||
disabled: true,
|
||||
canAddViaMenu: true,
|
||||
},
|
||||
{
|
||||
type: layerTypes.METRIC_TRENDLINE,
|
||||
|
@ -431,7 +430,6 @@ export const getMetricVisualization = ({
|
|||
{ groupId: GROUP_ID.TREND_TIME, columnId: generateId(), autoTimeField: true },
|
||||
],
|
||||
disabled: Boolean(state?.trendlineLayerId),
|
||||
canAddViaMenu: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
|
133
x-pack/plugins/lens/public/visualizations/xy/__snapshots__/visualization.test.tsx.snap
generated
Normal file
133
x-pack/plugins/lens/public/visualizations/xy/__snapshots__/visualization.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,133 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`xy_visualization #appendLayer adding an annotation layer adds a by-reference annotation layer 1`] = `
|
||||
Object {
|
||||
"__lastSaved": Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"icon": "circle",
|
||||
"id": "an1",
|
||||
"key": Object {
|
||||
"timestamp": "2022-03-18T08:25:17.140Z",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"label": "Event 1",
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"description": "Some description",
|
||||
"ignoreGlobalFilters": false,
|
||||
"indexPatternId": "indexPattern1",
|
||||
"tags": Array [],
|
||||
"title": "Title",
|
||||
},
|
||||
"annotationGroupId": "some-annotation-group-id",
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"icon": "circle",
|
||||
"id": "an1",
|
||||
"key": Object {
|
||||
"timestamp": "2022-03-18T08:25:17.140Z",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"label": "Event 1",
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"ignoreGlobalFilters": false,
|
||||
"indexPatternId": "indexPattern1",
|
||||
"layerId": "",
|
||||
"layerType": "annotations",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`xy_visualization #appendLayer adding an annotation layer adds a by-value annotation layer 1`] = `
|
||||
Object {
|
||||
"annotations": Array [],
|
||||
"ignoreGlobalFilters": true,
|
||||
"indexPatternId": "indexPattern1",
|
||||
"layerId": "",
|
||||
"layerType": "annotations",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`xy_visualization #initialize should hydrate by-reference annotation groups 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"__lastSaved": Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"icon": "circle",
|
||||
"id": "an1",
|
||||
"key": Object {
|
||||
"timestamp": "2022-03-18T08:25:17.140Z",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"label": "Event 1",
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"description": "",
|
||||
"ignoreGlobalFilters": true,
|
||||
"indexPatternId": "data-view-123",
|
||||
"tags": Array [],
|
||||
"title": "my title!",
|
||||
},
|
||||
"annotationGroupId": "my-annotation-group-id1",
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"icon": "circle",
|
||||
"id": "an1",
|
||||
"key": Object {
|
||||
"timestamp": "2022-03-18T08:25:17.140Z",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"label": "Event 1",
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"ignoreGlobalFilters": true,
|
||||
"indexPatternId": "data-view-123",
|
||||
"layerId": "annotation",
|
||||
"layerType": "annotations",
|
||||
},
|
||||
Object {
|
||||
"__lastSaved": Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"icon": "circle",
|
||||
"id": "an2",
|
||||
"key": Object {
|
||||
"timestamp": "2022-04-18T11:01:59.135Z",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"label": "Annotation2",
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"description": "",
|
||||
"ignoreGlobalFilters": true,
|
||||
"indexPatternId": "data-view-773203",
|
||||
"tags": Array [],
|
||||
"title": "my other title!",
|
||||
},
|
||||
"annotationGroupId": "my-annotation-group-id2",
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"icon": "circle",
|
||||
"id": "an2",
|
||||
"key": Object {
|
||||
"timestamp": "2022-04-18T11:01:59.135Z",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"label": "Annotation2",
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"ignoreGlobalFilters": true,
|
||||
"indexPatternId": "data-view-773203",
|
||||
"layerId": "annotation",
|
||||
"layerType": "annotations",
|
||||
},
|
||||
]
|
||||
`;
|
172
x-pack/plugins/lens/public/visualizations/xy/add_layer.tsx
Normal file
172
x-pack/plugins/lens/public/visualizations/xy/add_layer.tsx
Normal file
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiPopover,
|
||||
EuiIcon,
|
||||
EuiContextMenu,
|
||||
EuiBadge,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import { AddLayerFunction, VisualizationLayerDescription } from '../../types';
|
||||
import { LoadAnnotationLibraryFlyout } from './load_annotation_library_flyout';
|
||||
import type { ExtraAppendLayerArg } from './visualization';
|
||||
|
||||
interface AddLayerButtonProps {
|
||||
supportedLayers: VisualizationLayerDescription[];
|
||||
addLayer: AddLayerFunction<ExtraAppendLayerArg>;
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
}
|
||||
|
||||
export function AddLayerButton({
|
||||
supportedLayers,
|
||||
addLayer,
|
||||
eventAnnotationService,
|
||||
}: AddLayerButtonProps) {
|
||||
const [showLayersChoice, toggleLayersChoice] = useState(false);
|
||||
|
||||
const [isLoadLibraryVisible, setLoadLibraryFlyoutVisible] = useState(false);
|
||||
|
||||
const annotationPanel = ({
|
||||
type,
|
||||
label,
|
||||
icon,
|
||||
disabled,
|
||||
toolTipContent,
|
||||
}: typeof supportedLayers[0]) => {
|
||||
return {
|
||||
panel: 1,
|
||||
toolTipContent,
|
||||
disabled,
|
||||
name: (
|
||||
<EuiFlexGroup gutterSize="m" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<span className="lnsLayerAddButton__label">{label}</span>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge className="lnsLayerAddButton__techBadge" color="hollow" isDisabled={disabled}>
|
||||
{i18n.translate('xpack.lens.configPanel.experimentalLabel', {
|
||||
defaultMessage: 'Technical preview',
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
className: 'lnsLayerAddButton',
|
||||
icon: icon && <EuiIcon size="m" type={icon} />,
|
||||
['data-test-subj']: `lnsLayerAddButton-${type}`,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopover
|
||||
display="block"
|
||||
data-test-subj="lnsConfigPanel__addLayerPopover"
|
||||
button={
|
||||
<EuiButton
|
||||
fullWidth
|
||||
data-test-subj="lnsLayerAddButton"
|
||||
aria-label={i18n.translate('xpack.lens.configPanel.addLayerButton', {
|
||||
defaultMessage: 'Add layer',
|
||||
})}
|
||||
fill
|
||||
color="text"
|
||||
onClick={() => toggleLayersChoice(!showLayersChoice)}
|
||||
iconType="layers"
|
||||
>
|
||||
{i18n.translate('xpack.lens.configPanel.addLayerButton', {
|
||||
defaultMessage: 'Add layer',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
isOpen={showLayersChoice}
|
||||
closePopover={() => toggleLayersChoice(false)}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={[
|
||||
{
|
||||
id: 0,
|
||||
title: i18n.translate('xpack.lens.configPanel.selectLayerType', {
|
||||
defaultMessage: 'Select layer type',
|
||||
}),
|
||||
width: 300,
|
||||
items: supportedLayers.map((props) => {
|
||||
const { type, label, icon, disabled, toolTipContent } = props;
|
||||
if (type === LayerTypes.ANNOTATIONS) {
|
||||
return annotationPanel(props);
|
||||
}
|
||||
return {
|
||||
toolTipContent,
|
||||
disabled,
|
||||
name: <span className="lnsLayerAddButtonLabel">{label}</span>,
|
||||
className: 'lnsLayerAddButton',
|
||||
icon: icon && <EuiIcon size="m" type={icon} />,
|
||||
['data-test-subj']: `lnsLayerAddButton-${type}`,
|
||||
onClick: () => {
|
||||
addLayer(type);
|
||||
toggleLayersChoice(false);
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
initialFocusedItemIndex: 0,
|
||||
title: i18n.translate('xpack.lens.configPanel.selectAnnotationMethod', {
|
||||
defaultMessage: 'Select annotation method',
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.configPanel.newAnnotation', {
|
||||
defaultMessage: 'New annotation',
|
||||
}),
|
||||
icon: 'plusInCircle',
|
||||
onClick: () => {
|
||||
addLayer(LayerTypes.ANNOTATIONS);
|
||||
toggleLayersChoice(false);
|
||||
},
|
||||
'data-test-subj': 'lnsAnnotationLayer_new',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.lens.configPanel.loadFromLibrary', {
|
||||
defaultMessage: 'Load from library',
|
||||
}),
|
||||
icon: 'folderOpen',
|
||||
onClick: () => {
|
||||
setLoadLibraryFlyoutVisible(true);
|
||||
toggleLayersChoice(false);
|
||||
},
|
||||
'data-test-subj': 'lnsAnnotationLayer_addFromLibrary',
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
{isLoadLibraryVisible && (
|
||||
<LoadAnnotationLibraryFlyout
|
||||
isLoadLibraryVisible={isLoadLibraryVisible}
|
||||
setLoadLibraryFlyoutVisible={setLoadLibraryFlyoutVisible}
|
||||
eventAnnotationService={eventAnnotationService}
|
||||
addLayer={(extraArg) => {
|
||||
addLayer(LayerTypes.ANNOTATIONS, extraArg);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { LayerActionFromVisualization } from '../../../types';
|
||||
import type { XYState, XYAnnotationLayerConfig } from '../types';
|
||||
|
||||
// Leaving the stub for annotation groups
|
||||
export const createAnnotationActions = ({
|
||||
state,
|
||||
layer,
|
||||
layerIndex,
|
||||
}: {
|
||||
state: XYState;
|
||||
layer: XYAnnotationLayerConfig;
|
||||
layerIndex: number;
|
||||
}): LayerActionFromVisualization[] => {
|
||||
return [];
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`revert changes routine reverts changes 2`] = `
|
||||
Object {
|
||||
"layers": Array [
|
||||
Object {
|
||||
"__lastSaved": Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"id": "some-different-annotation-id",
|
||||
"key": Object {
|
||||
"timestamp": "timestamp2",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"description": "",
|
||||
"ignoreGlobalFilters": true,
|
||||
"indexPatternId": "other index pattern",
|
||||
"layerId": "some-id",
|
||||
"layerType": "annotations",
|
||||
"tags": Array [],
|
||||
"title": "My library group",
|
||||
},
|
||||
"annotationGroupId": "shouldnt show up",
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"id": "some-different-annotation-id",
|
||||
"key": Object {
|
||||
"timestamp": "timestamp2",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"ignoreGlobalFilters": true,
|
||||
"indexPatternId": "other index pattern",
|
||||
"layerId": "some-id",
|
||||
"layerType": "annotations",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
215
x-pack/plugins/lens/public/visualizations/xy/annotations/actions/__snapshots__/save_action.test.tsx.snap
generated
Normal file
215
x-pack/plugins/lens/public/visualizations/xy/annotations/actions/__snapshots__/save_action.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,215 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`annotation group save action save routine saving an existing group as new 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"layers": Array [
|
||||
Object {
|
||||
"__lastSaved": Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"id": "some-annotation-id",
|
||||
"key": Object {
|
||||
"timestamp": "timestamp",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"dataViewSpec": undefined,
|
||||
"description": "my description",
|
||||
"ignoreGlobalFilters": false,
|
||||
"indexPatternId": "some-index-pattern",
|
||||
"tags": Array [
|
||||
"my-tag",
|
||||
],
|
||||
"title": "my title",
|
||||
},
|
||||
"annotationGroupId": "saved-id-123",
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"id": "some-annotation-id",
|
||||
"key": Object {
|
||||
"timestamp": "timestamp",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"ignoreGlobalFilters": false,
|
||||
"indexPatternId": "some-index-pattern",
|
||||
"layerId": "mylayerid",
|
||||
"layerType": "annotations",
|
||||
},
|
||||
],
|
||||
"legend": Object {
|
||||
"isVisible": true,
|
||||
"position": "bottom",
|
||||
},
|
||||
"preferredSeriesType": "area",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`annotation group save action save routine successful initial save 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"layers": Array [
|
||||
Object {
|
||||
"__lastSaved": Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"id": "some-annotation-id",
|
||||
"key": Object {
|
||||
"timestamp": "timestamp",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"dataViewSpec": undefined,
|
||||
"description": "my description",
|
||||
"ignoreGlobalFilters": false,
|
||||
"indexPatternId": "some-index-pattern",
|
||||
"tags": Array [
|
||||
"my-tag",
|
||||
],
|
||||
"title": "my title",
|
||||
},
|
||||
"annotationGroupId": "saved-id-123",
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"id": "some-annotation-id",
|
||||
"key": Object {
|
||||
"timestamp": "timestamp",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"ignoreGlobalFilters": false,
|
||||
"indexPatternId": "some-index-pattern",
|
||||
"layerId": "mylayerid",
|
||||
"layerType": "annotations",
|
||||
},
|
||||
],
|
||||
"legend": Object {
|
||||
"isVisible": true,
|
||||
"position": "bottom",
|
||||
},
|
||||
"preferredSeriesType": "area",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`annotation group save action save routine successful initial save with ad-hoc data view 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"layers": Array [
|
||||
Object {
|
||||
"__lastSaved": Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"id": "some-annotation-id",
|
||||
"key": Object {
|
||||
"timestamp": "timestamp",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"dataViewSpec": Object {
|
||||
"id": "some-adhoc-data-view-id",
|
||||
},
|
||||
"description": "my description",
|
||||
"ignoreGlobalFilters": false,
|
||||
"indexPatternId": "some-index-pattern",
|
||||
"tags": Array [
|
||||
"my-tag",
|
||||
],
|
||||
"title": "my title",
|
||||
},
|
||||
"annotationGroupId": "saved-id-123",
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"id": "some-annotation-id",
|
||||
"key": Object {
|
||||
"timestamp": "timestamp",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"ignoreGlobalFilters": false,
|
||||
"indexPatternId": "some-index-pattern",
|
||||
"layerId": "mylayerid",
|
||||
"layerType": "annotations",
|
||||
},
|
||||
],
|
||||
"legend": Object {
|
||||
"isVisible": true,
|
||||
"position": "bottom",
|
||||
},
|
||||
"preferredSeriesType": "area",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`annotation group save action save routine updating an existing group 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"layers": Array [
|
||||
Object {
|
||||
"__lastSaved": Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"id": "some-annotation-id",
|
||||
"key": Object {
|
||||
"timestamp": "timestamp",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"dataViewSpec": undefined,
|
||||
"description": "my description",
|
||||
"ignoreGlobalFilters": false,
|
||||
"indexPatternId": "some-index-pattern",
|
||||
"tags": Array [
|
||||
"my-tag",
|
||||
],
|
||||
"title": "my title",
|
||||
},
|
||||
"annotationGroupId": "my-group-id",
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"id": "some-annotation-id",
|
||||
"key": Object {
|
||||
"timestamp": "timestamp",
|
||||
"type": "point_in_time",
|
||||
},
|
||||
"type": "manual",
|
||||
},
|
||||
],
|
||||
"ignoreGlobalFilters": false,
|
||||
"indexPatternId": "some-index-pattern",
|
||||
"layerId": "mylayerid",
|
||||
"layerType": "annotations",
|
||||
},
|
||||
],
|
||||
"legend": Object {
|
||||
"isVisible": true,
|
||||
"position": "bottom",
|
||||
},
|
||||
"preferredSeriesType": "area",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public';
|
||||
import { DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import type { LayerAction, StateSetter } from '../../../../types';
|
||||
import { XYState, XYAnnotationLayerConfig } from '../../types';
|
||||
import { getUnlinkLayerAction } from './unlink_action';
|
||||
import { getSaveLayerAction } from './save_action';
|
||||
import { isByReferenceAnnotationsLayer } from '../../visualization_helpers';
|
||||
import { getRevertChangesAction } from './revert_changes_action';
|
||||
|
||||
export const createAnnotationActions = ({
|
||||
state,
|
||||
layer,
|
||||
setState,
|
||||
core,
|
||||
isSaveable,
|
||||
eventAnnotationService,
|
||||
savedObjectsTagging,
|
||||
dataViews,
|
||||
}: {
|
||||
state: XYState;
|
||||
layer: XYAnnotationLayerConfig;
|
||||
setState: StateSetter<XYState, unknown>;
|
||||
core: CoreStart;
|
||||
isSaveable?: boolean;
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
savedObjectsTagging?: SavedObjectTaggingPluginStart;
|
||||
dataViews: DataViewsContract;
|
||||
}): LayerAction[] => {
|
||||
const actions = [];
|
||||
|
||||
const savingToLibraryPermitted = Boolean(
|
||||
core.application.capabilities.visualize.save && isSaveable
|
||||
);
|
||||
|
||||
if (savingToLibraryPermitted) {
|
||||
actions.push(
|
||||
getSaveLayerAction({
|
||||
state,
|
||||
layer,
|
||||
setState,
|
||||
eventAnnotationService,
|
||||
toasts: core.notifications.toasts,
|
||||
savedObjectsTagging,
|
||||
dataViews,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isByReferenceAnnotationsLayer(layer)) {
|
||||
actions.push(
|
||||
getUnlinkLayerAction({
|
||||
state,
|
||||
layer,
|
||||
setState,
|
||||
toasts: core.notifications.toasts,
|
||||
})
|
||||
);
|
||||
|
||||
actions.push(getRevertChangesAction({ state, layer, setState, core }));
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { OverlayRef } from '@kbn/core-mount-utils-browser';
|
||||
import { IToasts } from '@kbn/core-notifications-browser';
|
||||
import { PointInTimeEventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import {
|
||||
XYByReferenceAnnotationLayerConfig,
|
||||
XYByValueAnnotationLayerConfig,
|
||||
XYState,
|
||||
} from '../../types';
|
||||
import { revert } from './revert_changes_action';
|
||||
|
||||
describe('revert changes routine', () => {
|
||||
const byValueLayer: XYByValueAnnotationLayerConfig = {
|
||||
layerId: 'some-id',
|
||||
layerType: 'annotations',
|
||||
indexPatternId: 'some-index-pattern',
|
||||
ignoreGlobalFilters: false,
|
||||
annotations: [
|
||||
{
|
||||
id: 'some-annotation-id',
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp: 'timestamp',
|
||||
},
|
||||
} as PointInTimeEventAnnotationConfig,
|
||||
],
|
||||
};
|
||||
|
||||
const byRefLayer: XYByReferenceAnnotationLayerConfig = {
|
||||
...byValueLayer,
|
||||
annotationGroupId: 'shouldnt show up',
|
||||
__lastSaved: {
|
||||
...cloneDeep(byValueLayer),
|
||||
|
||||
// some differences
|
||||
annotations: [
|
||||
{
|
||||
id: 'some-different-annotation-id',
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp: 'timestamp2',
|
||||
},
|
||||
} as PointInTimeEventAnnotationConfig,
|
||||
],
|
||||
ignoreGlobalFilters: true,
|
||||
indexPatternId: 'other index pattern',
|
||||
|
||||
title: 'My library group',
|
||||
description: '',
|
||||
tags: [],
|
||||
},
|
||||
};
|
||||
|
||||
it('reverts changes', async () => {
|
||||
const setState = jest.fn();
|
||||
const modal = {
|
||||
close: jest.fn(() => Promise.resolve()),
|
||||
} as Partial<OverlayRef> as OverlayRef;
|
||||
|
||||
const toasts = { addSuccess: jest.fn() } as Partial<IToasts> as IToasts;
|
||||
|
||||
revert({
|
||||
setState,
|
||||
layer: byRefLayer,
|
||||
state: { layers: [byRefLayer] } as XYState,
|
||||
modal,
|
||||
toasts,
|
||||
});
|
||||
|
||||
expect(setState).toHaveBeenCalled();
|
||||
expect((toasts.addSuccess as jest.Mock).mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"text": "The most recently saved version of this annotation group has been restored.",
|
||||
"title": "Reverted \\"My library group\\"",
|
||||
}
|
||||
`);
|
||||
expect(modal.close).toHaveBeenCalled();
|
||||
|
||||
const newState = setState.mock.calls[0][0] as XYState;
|
||||
|
||||
expect(
|
||||
(newState.layers[0] as XYByReferenceAnnotationLayerConfig).ignoreGlobalFilters
|
||||
).toBeTruthy();
|
||||
expect(newState).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { OverlayRef } from '@kbn/core-mount-utils-browser';
|
||||
import { IToasts } from '@kbn/core-notifications-browser';
|
||||
import type { LayerAction, StateSetter } from '../../../../types';
|
||||
import type { XYState, XYByReferenceAnnotationLayerConfig } from '../../types';
|
||||
import { annotationLayerHasUnsavedChanges } from '../../state_helpers';
|
||||
|
||||
export const getRevertChangesAction = ({
|
||||
state,
|
||||
layer,
|
||||
setState,
|
||||
core,
|
||||
}: {
|
||||
state: XYState;
|
||||
layer: XYByReferenceAnnotationLayerConfig;
|
||||
setState: StateSetter<XYState, unknown>;
|
||||
core: Pick<CoreStart, 'overlays' | 'theme' | 'notifications'>;
|
||||
}): LayerAction => {
|
||||
return {
|
||||
displayName: i18n.translate('xpack.lens.xyChart.annotations.revertChanges', {
|
||||
defaultMessage: 'Revert changes',
|
||||
}),
|
||||
description: i18n.translate('xpack.lens.xyChart.annotations.revertChangesDescription', {
|
||||
defaultMessage: 'Restores annotation group to the last saved state.',
|
||||
}),
|
||||
execute: async () => {
|
||||
const modal = core.overlays.openModal(
|
||||
toMountPoint(
|
||||
<RevertChangesConfirmModal
|
||||
modalTitle={i18n.translate('xpack.lens.modalTitle.revertAnnotationGroupTitle', {
|
||||
defaultMessage: 'Revert "{title}" changes?',
|
||||
values: { title: layer.__lastSaved.title },
|
||||
})}
|
||||
onCancel={() => modal.close()}
|
||||
onConfirm={() => {
|
||||
revert({ setState, layer, state, modal, toasts: core.notifications.toasts });
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
theme$: core.theme.theme$,
|
||||
}
|
||||
),
|
||||
{
|
||||
'data-test-subj': 'lnsAnnotationLayerRevertModal',
|
||||
maxWidth: 600,
|
||||
}
|
||||
);
|
||||
await modal.onClose;
|
||||
},
|
||||
icon: 'editorUndo',
|
||||
isCompatible: true,
|
||||
disabled: !annotationLayerHasUnsavedChanges(layer),
|
||||
'data-test-subj': 'lnsXY_annotationLayer_revertChanges',
|
||||
order: 200,
|
||||
};
|
||||
};
|
||||
|
||||
export const revert = ({
|
||||
setState,
|
||||
layer,
|
||||
state,
|
||||
modal,
|
||||
toasts,
|
||||
}: {
|
||||
setState: StateSetter<XYState>;
|
||||
layer: XYByReferenceAnnotationLayerConfig;
|
||||
state: XYState;
|
||||
modal: OverlayRef;
|
||||
toasts: IToasts;
|
||||
}) => {
|
||||
const newLayer: XYByReferenceAnnotationLayerConfig = {
|
||||
layerId: layer.layerId,
|
||||
layerType: layer.layerType,
|
||||
annotationGroupId: layer.annotationGroupId,
|
||||
|
||||
indexPatternId: layer.__lastSaved.indexPatternId,
|
||||
ignoreGlobalFilters: layer.__lastSaved.ignoreGlobalFilters,
|
||||
annotations: cloneDeep(layer.__lastSaved.annotations),
|
||||
__lastSaved: layer.__lastSaved,
|
||||
};
|
||||
setState({
|
||||
...state,
|
||||
layers: state.layers.map((layerToCheck) =>
|
||||
layerToCheck.layerId === layer.layerId ? newLayer : layerToCheck
|
||||
),
|
||||
});
|
||||
|
||||
modal.close();
|
||||
|
||||
toasts.addSuccess({
|
||||
title: i18n.translate('xpack.lens.xyChart.annotations.notificationReverted', {
|
||||
defaultMessage: `Reverted "{title}"`,
|
||||
values: { title: layer.__lastSaved.title },
|
||||
}),
|
||||
text: i18n.translate('xpack.lens.xyChart.annotations.notificationRevertedExplanation', {
|
||||
defaultMessage: 'The most recently saved version of this annotation group has been restored.',
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const RevertChangesConfirmModal = ({
|
||||
modalTitle,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: {
|
||||
modalTitle: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{modalTitle}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<p>
|
||||
{i18n.translate('xpack.lens.layer.revertModal.revertAnnotationGroupDescription', {
|
||||
defaultMessage: `This action will remove all unsaved changes that you've made and restore the most recent saved version of this annotation group to you visualization. Any lost unsaved changes cannot be restored.`,
|
||||
})}
|
||||
</p>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onCancel}>
|
||||
{i18n.translate('xpack.lens.layer.cancelDelete', {
|
||||
defaultMessage: `Cancel`,
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="lnsLayerRevertChangesButton"
|
||||
onClick={onConfirm}
|
||||
color="warning"
|
||||
iconType="returnKey"
|
||||
fill
|
||||
>
|
||||
{i18n.translate('xpack.lens.layer.unlinkConfirm', {
|
||||
defaultMessage: 'Revert changes',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalFooter>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,324 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { toastsServiceMock } from '@kbn/core-notifications-browser-mocks/src/toasts_service.mock';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import {
|
||||
XYByValueAnnotationLayerConfig,
|
||||
XYAnnotationLayerConfig,
|
||||
XYState,
|
||||
XYByReferenceAnnotationLayerConfig,
|
||||
} from '../../types';
|
||||
import { onSave, SaveModal } from './save_action';
|
||||
import { shallowWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { PointInTimeEventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
|
||||
import { SavedObjectSaveModal } from '@kbn/saved-objects-plugin/public';
|
||||
import { taggingApiMock } from '@kbn/saved-objects-tagging-plugin/public/mocks';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
|
||||
describe('annotation group save action', () => {
|
||||
describe('save modal', () => {
|
||||
const modalSaveArgs = {
|
||||
newCopyOnSave: false,
|
||||
isTitleDuplicateConfirmed: false,
|
||||
onTitleDuplicate: () => {},
|
||||
};
|
||||
|
||||
it('reports new saved object attributes', async () => {
|
||||
const onSaveMock = jest.fn();
|
||||
const savedObjectsTagging = taggingApiMock.create();
|
||||
const wrapper = shallowWithIntl(
|
||||
<SaveModal
|
||||
domElement={document.createElement('div')}
|
||||
onSave={onSaveMock}
|
||||
savedObjectsTagging={savedObjectsTagging}
|
||||
title=""
|
||||
description=""
|
||||
tags={[]}
|
||||
showCopyOnSave={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const newTitle = 'title';
|
||||
const newDescription = 'description';
|
||||
const myTags = ['my', 'many', 'tags'];
|
||||
|
||||
(wrapper
|
||||
.find(SavedObjectSaveModal)
|
||||
.prop('options') as React.ReactElement)!.props.onTagsSelected(myTags);
|
||||
|
||||
// ignore the linter, you need this await statement
|
||||
await wrapper.find(SavedObjectSaveModal).prop('onSave')({
|
||||
newTitle,
|
||||
newDescription,
|
||||
...modalSaveArgs,
|
||||
});
|
||||
|
||||
expect(onSaveMock).toHaveBeenCalledTimes(1);
|
||||
expect(onSaveMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"closeModal": [Function],
|
||||
"isTitleDuplicateConfirmed": false,
|
||||
"newCopyOnSave": false,
|
||||
"newDescription": "description",
|
||||
"newTags": Array [
|
||||
"my",
|
||||
"many",
|
||||
"tags",
|
||||
],
|
||||
"newTitle": "title",
|
||||
"onTitleDuplicate": [Function],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('shows existing saved object attributes', async () => {
|
||||
const savedObjectsTagging = taggingApiMock.create();
|
||||
|
||||
const title = 'my title';
|
||||
const description = 'my description';
|
||||
const tags = ['my', 'tags'];
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<SaveModal
|
||||
domElement={document.createElement('div')}
|
||||
onSave={() => {}}
|
||||
savedObjectsTagging={savedObjectsTagging}
|
||||
title={title}
|
||||
description={description}
|
||||
tags={tags}
|
||||
showCopyOnSave={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const saveModal = wrapper.find(SavedObjectSaveModal);
|
||||
|
||||
expect(saveModal.prop('title')).toBe(title);
|
||||
expect(saveModal.prop('description')).toBe(description);
|
||||
expect(saveModal.prop('showCopyOnSave')).toBe(true);
|
||||
expect((saveModal.prop('options') as React.ReactElement).props.initialSelection).toEqual(
|
||||
tags
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('save routine', () => {
|
||||
const layerId = 'mylayerid';
|
||||
|
||||
const layer: XYByValueAnnotationLayerConfig = {
|
||||
layerId,
|
||||
layerType: 'annotations',
|
||||
indexPatternId: 'some-index-pattern',
|
||||
ignoreGlobalFilters: false,
|
||||
annotations: [
|
||||
{
|
||||
id: 'some-annotation-id',
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp: 'timestamp',
|
||||
},
|
||||
} as PointInTimeEventAnnotationConfig,
|
||||
],
|
||||
};
|
||||
|
||||
const savedId = 'saved-id-123';
|
||||
|
||||
const getProps: () => Parameters<typeof onSave>[0] = () => {
|
||||
const dataViews = dataViewPluginMocks.createStartContract();
|
||||
dataViews.get.mockResolvedValue({
|
||||
isPersisted: () => true,
|
||||
} as Partial<DataView> as DataView);
|
||||
|
||||
return {
|
||||
state: {
|
||||
preferredSeriesType: 'area',
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
layers: [{ layerId } as XYAnnotationLayerConfig],
|
||||
} as XYState,
|
||||
layer,
|
||||
setState: jest.fn(),
|
||||
eventAnnotationService: {
|
||||
createAnnotationGroup: jest.fn(() => Promise.resolve({ id: savedId })),
|
||||
updateAnnotationGroup: jest.fn(),
|
||||
loadAnnotationGroup: jest.fn(),
|
||||
toExpression: jest.fn(),
|
||||
toFetchExpression: jest.fn(),
|
||||
renderEventAnnotationGroupSavedObjectFinder: jest.fn(),
|
||||
} as EventAnnotationServiceType,
|
||||
toasts: toastsServiceMock.createStartContract(),
|
||||
modalOnSaveProps: {
|
||||
newTitle: 'my title',
|
||||
newDescription: 'my description',
|
||||
closeModal: jest.fn(),
|
||||
newTags: ['my-tag'],
|
||||
newCopyOnSave: false,
|
||||
isTitleDuplicateConfirmed: false,
|
||||
onTitleDuplicate: () => {},
|
||||
},
|
||||
dataViews,
|
||||
};
|
||||
};
|
||||
|
||||
let props: ReturnType<typeof getProps>;
|
||||
beforeEach(() => {
|
||||
props = getProps();
|
||||
});
|
||||
|
||||
test('successful initial save', async () => {
|
||||
await onSave(props);
|
||||
|
||||
expect(props.eventAnnotationService.createAnnotationGroup).toHaveBeenCalledWith({
|
||||
annotations: props.layer.annotations,
|
||||
indexPatternId: props.layer.indexPatternId,
|
||||
ignoreGlobalFilters: props.layer.ignoreGlobalFilters,
|
||||
title: props.modalOnSaveProps.newTitle,
|
||||
description: props.modalOnSaveProps.newDescription,
|
||||
tags: props.modalOnSaveProps.newTags,
|
||||
});
|
||||
|
||||
expect(props.modalOnSaveProps.closeModal).toHaveBeenCalled();
|
||||
|
||||
expect((props.setState as jest.Mock).mock.calls).toMatchSnapshot();
|
||||
|
||||
expect(props.toasts.addSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('successful initial save with ad-hoc data view', async () => {
|
||||
const dataViewSpec = {
|
||||
id: 'some-adhoc-data-view-id',
|
||||
} as DataViewSpec;
|
||||
|
||||
(props.dataViews.get as jest.Mock).mockResolvedValueOnce({
|
||||
isPersisted: () => false, // ad-hoc
|
||||
toSpec: () => dataViewSpec,
|
||||
} as Partial<DataView>);
|
||||
|
||||
await onSave(props);
|
||||
|
||||
expect(props.eventAnnotationService.createAnnotationGroup).toHaveBeenCalledWith({
|
||||
annotations: props.layer.annotations,
|
||||
indexPatternId: props.layer.indexPatternId,
|
||||
ignoreGlobalFilters: props.layer.ignoreGlobalFilters,
|
||||
title: props.modalOnSaveProps.newTitle,
|
||||
description: props.modalOnSaveProps.newDescription,
|
||||
tags: props.modalOnSaveProps.newTags,
|
||||
dataViewSpec,
|
||||
});
|
||||
|
||||
expect(props.modalOnSaveProps.closeModal).toHaveBeenCalled();
|
||||
|
||||
expect((props.setState as jest.Mock).mock.calls).toMatchSnapshot();
|
||||
|
||||
expect(props.toasts.addSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('failed initial save', async () => {
|
||||
(props.eventAnnotationService.createAnnotationGroup as jest.Mock).mockRejectedValue(
|
||||
new Error('oh noooooo')
|
||||
);
|
||||
|
||||
await onSave(props);
|
||||
|
||||
expect(props.eventAnnotationService.createAnnotationGroup).toHaveBeenCalledWith({
|
||||
annotations: props.layer.annotations,
|
||||
indexPatternId: props.layer.indexPatternId,
|
||||
ignoreGlobalFilters: props.layer.ignoreGlobalFilters,
|
||||
title: props.modalOnSaveProps.newTitle,
|
||||
description: props.modalOnSaveProps.newDescription,
|
||||
tags: props.modalOnSaveProps.newTags,
|
||||
});
|
||||
|
||||
expect(props.toasts.addError).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(props.modalOnSaveProps.closeModal).not.toHaveBeenCalled();
|
||||
|
||||
expect(props.setState).not.toHaveBeenCalled();
|
||||
|
||||
expect(props.toasts.addSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('updating an existing group', async () => {
|
||||
const annotationGroupId = 'my-group-id';
|
||||
|
||||
const byReferenceLayer: XYByReferenceAnnotationLayerConfig = {
|
||||
...props.layer,
|
||||
annotationGroupId,
|
||||
__lastSaved: {
|
||||
...props.layer,
|
||||
title: 'old title',
|
||||
description: 'old description',
|
||||
tags: [],
|
||||
},
|
||||
};
|
||||
|
||||
await onSave({ ...props, layer: byReferenceLayer });
|
||||
|
||||
expect(props.eventAnnotationService.createAnnotationGroup).not.toHaveBeenCalled();
|
||||
|
||||
expect(props.eventAnnotationService.updateAnnotationGroup).toHaveBeenCalledWith(
|
||||
{
|
||||
annotations: props.layer.annotations,
|
||||
indexPatternId: props.layer.indexPatternId,
|
||||
ignoreGlobalFilters: props.layer.ignoreGlobalFilters,
|
||||
title: props.modalOnSaveProps.newTitle,
|
||||
description: props.modalOnSaveProps.newDescription,
|
||||
tags: props.modalOnSaveProps.newTags,
|
||||
},
|
||||
annotationGroupId
|
||||
);
|
||||
|
||||
expect(props.modalOnSaveProps.closeModal).toHaveBeenCalled();
|
||||
|
||||
expect((props.setState as jest.Mock).mock.calls).toMatchSnapshot();
|
||||
|
||||
expect(props.toasts.addSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('saving an existing group as new', async () => {
|
||||
const annotationGroupId = 'my-group-id';
|
||||
|
||||
const byReferenceLayer: XYByReferenceAnnotationLayerConfig = {
|
||||
...props.layer,
|
||||
annotationGroupId,
|
||||
__lastSaved: {
|
||||
...props.layer,
|
||||
title: 'old title',
|
||||
description: 'old description',
|
||||
tags: [],
|
||||
},
|
||||
};
|
||||
|
||||
await onSave({
|
||||
...props,
|
||||
layer: byReferenceLayer,
|
||||
modalOnSaveProps: { ...props.modalOnSaveProps, newCopyOnSave: true },
|
||||
});
|
||||
|
||||
expect(props.eventAnnotationService.updateAnnotationGroup).not.toHaveBeenCalled();
|
||||
|
||||
expect(props.eventAnnotationService.createAnnotationGroup).toHaveBeenCalledWith({
|
||||
annotations: props.layer.annotations,
|
||||
indexPatternId: props.layer.indexPatternId,
|
||||
ignoreGlobalFilters: props.layer.ignoreGlobalFilters,
|
||||
title: props.modalOnSaveProps.newTitle,
|
||||
description: props.modalOnSaveProps.newDescription,
|
||||
tags: props.modalOnSaveProps.newTags,
|
||||
});
|
||||
|
||||
expect(props.modalOnSaveProps.closeModal).toHaveBeenCalled();
|
||||
|
||||
expect((props.setState as jest.Mock).mock.calls).toMatchSnapshot();
|
||||
|
||||
expect(props.toasts.addSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,281 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import { ToastsStart } from '@kbn/core-notifications-browser';
|
||||
import { MountPoint } from '@kbn/core-mount-utils-browser';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
OnSaveProps as SavedObjectOnSaveProps,
|
||||
SavedObjectSaveModal,
|
||||
} from '@kbn/saved-objects-plugin/public';
|
||||
import { EventAnnotationGroupConfig } from '@kbn/event-annotation-plugin/common';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import { type SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public';
|
||||
import { DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import type { LayerAction, StateSetter } from '../../../../types';
|
||||
import { XYByReferenceAnnotationLayerConfig, XYAnnotationLayerConfig, XYState } from '../../types';
|
||||
import { isByReferenceAnnotationsLayer } from '../../visualization_helpers';
|
||||
|
||||
type ModalOnSaveProps = SavedObjectOnSaveProps & { newTags: string[]; closeModal: () => void };
|
||||
|
||||
/** @internal exported for testing only */
|
||||
export const SaveModal = ({
|
||||
domElement,
|
||||
savedObjectsTagging,
|
||||
onSave,
|
||||
title,
|
||||
description,
|
||||
tags,
|
||||
showCopyOnSave,
|
||||
}: {
|
||||
domElement: HTMLDivElement;
|
||||
savedObjectsTagging: SavedObjectTaggingPluginStart | undefined;
|
||||
onSave: (props: ModalOnSaveProps) => void;
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
showCopyOnSave: boolean;
|
||||
}) => {
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>(tags);
|
||||
|
||||
const closeModal = () => unmountComponentAtNode(domElement);
|
||||
|
||||
return (
|
||||
<SavedObjectSaveModal
|
||||
onSave={async (props) => onSave({ ...props, closeModal, newTags: selectedTags })}
|
||||
onClose={closeModal}
|
||||
title={title}
|
||||
description={description}
|
||||
showCopyOnSave={showCopyOnSave}
|
||||
objectType={i18n.translate(
|
||||
'xpack.lens.xyChart.annotations.saveAnnotationGroupToLibrary.objectType',
|
||||
{ defaultMessage: 'group' }
|
||||
)}
|
||||
customModalTitle={i18n.translate(
|
||||
'xpack.lens.xyChart.annotations.saveAnnotationGroupToLibrary.modalTitle',
|
||||
{
|
||||
defaultMessage: 'Save annotation group to library',
|
||||
}
|
||||
)}
|
||||
showDescription={true}
|
||||
confirmButtonLabel={
|
||||
<>
|
||||
<div>
|
||||
<EuiIcon type="save" />
|
||||
</div>
|
||||
<div>
|
||||
{i18n.translate(
|
||||
'xpack.lens.xyChart.annotations.saveAnnotationGroupToLibrary.confirmButton',
|
||||
{ defaultMessage: 'Save group' }
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
options={
|
||||
savedObjectsTagging ? (
|
||||
<savedObjectsTagging.ui.components.SavedObjectSaveModalTagSelector
|
||||
initialSelection={selectedTags}
|
||||
onTagsSelected={setSelectedTags}
|
||||
markOptional
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const saveAnnotationGroupToLibrary = async (
|
||||
layer: XYAnnotationLayerConfig,
|
||||
{
|
||||
newTitle,
|
||||
newDescription,
|
||||
newTags,
|
||||
newCopyOnSave,
|
||||
}: Pick<ModalOnSaveProps, 'newTitle' | 'newDescription' | 'newTags' | 'newCopyOnSave'>,
|
||||
eventAnnotationService: EventAnnotationServiceType,
|
||||
dataViews: DataViewsContract
|
||||
): Promise<{ id: string; config: EventAnnotationGroupConfig }> => {
|
||||
let savedId: string;
|
||||
|
||||
const dataView = await dataViews.get(layer.indexPatternId);
|
||||
|
||||
const saveAsNew = !isByReferenceAnnotationsLayer(layer) || newCopyOnSave;
|
||||
|
||||
const groupConfig: EventAnnotationGroupConfig = {
|
||||
annotations: layer.annotations,
|
||||
indexPatternId: layer.indexPatternId,
|
||||
ignoreGlobalFilters: layer.ignoreGlobalFilters,
|
||||
title: newTitle,
|
||||
description: newDescription,
|
||||
tags: newTags,
|
||||
dataViewSpec: dataView.isPersisted() ? undefined : dataView.toSpec(),
|
||||
};
|
||||
|
||||
if (saveAsNew) {
|
||||
const { id } = await eventAnnotationService.createAnnotationGroup(groupConfig);
|
||||
savedId = id;
|
||||
} else {
|
||||
await eventAnnotationService.updateAnnotationGroup(groupConfig, layer.annotationGroupId);
|
||||
|
||||
savedId = layer.annotationGroupId;
|
||||
}
|
||||
|
||||
return { id: savedId, config: groupConfig };
|
||||
};
|
||||
|
||||
/** @internal exported for testing only */
|
||||
export const onSave = async ({
|
||||
state,
|
||||
layer,
|
||||
setState,
|
||||
eventAnnotationService,
|
||||
toasts,
|
||||
modalOnSaveProps: { newTitle, newDescription, newTags, closeModal, newCopyOnSave },
|
||||
dataViews,
|
||||
}: {
|
||||
state: XYState;
|
||||
layer: XYAnnotationLayerConfig;
|
||||
setState: StateSetter<XYState, unknown>;
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
toasts: ToastsStart;
|
||||
modalOnSaveProps: ModalOnSaveProps;
|
||||
dataViews: DataViewsContract;
|
||||
}) => {
|
||||
let savedInfo: Awaited<ReturnType<typeof saveAnnotationGroupToLibrary>>;
|
||||
try {
|
||||
savedInfo = await saveAnnotationGroupToLibrary(
|
||||
layer,
|
||||
{ newTitle, newDescription, newTags, newCopyOnSave },
|
||||
eventAnnotationService,
|
||||
dataViews
|
||||
);
|
||||
} catch (err) {
|
||||
toasts.addError(err, {
|
||||
title: i18n.translate(
|
||||
'xpack.lens.xyChart.annotations.saveAnnotationGroupToLibrary.errorToastTitle',
|
||||
{
|
||||
defaultMessage: 'Failed to save "{title}"',
|
||||
values: {
|
||||
title: newTitle,
|
||||
},
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newLayer: XYByReferenceAnnotationLayerConfig = {
|
||||
...layer,
|
||||
annotationGroupId: savedInfo.id,
|
||||
__lastSaved: savedInfo.config,
|
||||
};
|
||||
|
||||
setState({
|
||||
...state,
|
||||
layers: state.layers.map((existingLayer) =>
|
||||
existingLayer.layerId === newLayer.layerId ? newLayer : existingLayer
|
||||
),
|
||||
});
|
||||
|
||||
closeModal();
|
||||
|
||||
toasts.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.lens.xyChart.annotations.saveAnnotationGroupToLibrary.successToastTitle',
|
||||
{
|
||||
defaultMessage: 'Saved "{title}"',
|
||||
values: {
|
||||
title: newTitle,
|
||||
},
|
||||
}
|
||||
),
|
||||
text: ((element) =>
|
||||
render(
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.xyChart.annotations.saveAnnotationGroupToLibrary.successToastBody"
|
||||
defaultMessage="View or manage in the {link}"
|
||||
values={{
|
||||
link: <a href="#">annotation library</a>,
|
||||
}}
|
||||
/>
|
||||
</p>,
|
||||
element
|
||||
)) as MountPoint,
|
||||
});
|
||||
};
|
||||
|
||||
export const getSaveLayerAction = ({
|
||||
state,
|
||||
layer,
|
||||
setState,
|
||||
eventAnnotationService,
|
||||
toasts,
|
||||
savedObjectsTagging,
|
||||
dataViews,
|
||||
}: {
|
||||
state: XYState;
|
||||
layer: XYAnnotationLayerConfig;
|
||||
setState: StateSetter<XYState, unknown>;
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
toasts: ToastsStart;
|
||||
savedObjectsTagging?: SavedObjectTaggingPluginStart;
|
||||
dataViews: DataViewsContract;
|
||||
}): LayerAction => {
|
||||
const neverSaved = !isByReferenceAnnotationsLayer(layer);
|
||||
|
||||
const displayName = i18n.translate(
|
||||
'xpack.lens.xyChart.annotations.saveAnnotationGroupToLibrary',
|
||||
{
|
||||
defaultMessage: 'Save annotation group',
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
displayName,
|
||||
description: i18n.translate(
|
||||
'xpack.lens.xyChart.annotations.addAnnotationGroupToLibraryDescription',
|
||||
{ defaultMessage: 'Saves annotation group as separate saved object' }
|
||||
),
|
||||
execute: async (domElement) => {
|
||||
if (domElement) {
|
||||
render(
|
||||
<SaveModal
|
||||
domElement={domElement}
|
||||
savedObjectsTagging={savedObjectsTagging}
|
||||
onSave={async (props) => {
|
||||
await onSave({
|
||||
state,
|
||||
layer,
|
||||
setState,
|
||||
eventAnnotationService,
|
||||
toasts,
|
||||
modalOnSaveProps: props,
|
||||
dataViews,
|
||||
});
|
||||
}}
|
||||
title={neverSaved ? '' : layer.__lastSaved.title}
|
||||
description={neverSaved ? '' : layer.__lastSaved.description}
|
||||
tags={neverSaved ? [] : layer.__lastSaved.tags}
|
||||
showCopyOnSave={!neverSaved}
|
||||
/>,
|
||||
domElement
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: 'save',
|
||||
isCompatible: true,
|
||||
'data-test-subj': 'lnsXY_annotationLayer_saveToLibrary',
|
||||
order: 100,
|
||||
showOutsideList: true,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
XYByValueAnnotationLayerConfig,
|
||||
XYByReferenceAnnotationLayerConfig,
|
||||
XYState,
|
||||
} from '../../types';
|
||||
import { toastsServiceMock } from '@kbn/core-notifications-browser-mocks/src/toasts_service.mock';
|
||||
import { PointInTimeEventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { getUnlinkLayerAction } from './unlink_action';
|
||||
|
||||
describe('annotation group unlink actions', () => {
|
||||
const layerId = 'mylayerid';
|
||||
|
||||
const byValueLayer: XYByValueAnnotationLayerConfig = {
|
||||
layerId,
|
||||
layerType: 'annotations',
|
||||
indexPatternId: 'some-index-pattern',
|
||||
ignoreGlobalFilters: false,
|
||||
annotations: [
|
||||
{
|
||||
id: 'some-annotation-id',
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp: 'timestamp',
|
||||
},
|
||||
} as PointInTimeEventAnnotationConfig,
|
||||
],
|
||||
};
|
||||
|
||||
const byRefLayer: XYByReferenceAnnotationLayerConfig = {
|
||||
...byValueLayer,
|
||||
annotationGroupId: 'shouldnt show up',
|
||||
__lastSaved: {
|
||||
...cloneDeep(byValueLayer),
|
||||
title: 'My library group',
|
||||
description: '',
|
||||
tags: [],
|
||||
},
|
||||
};
|
||||
|
||||
const state: XYState = {
|
||||
layers: [byRefLayer],
|
||||
legend: { isVisible: false, position: 'bottom' },
|
||||
preferredSeriesType: 'area',
|
||||
};
|
||||
|
||||
it('should unlink layer from library annotation group', () => {
|
||||
const toasts = toastsServiceMock.createStartContract();
|
||||
|
||||
const setState = jest.fn();
|
||||
const action = getUnlinkLayerAction({
|
||||
state,
|
||||
layer: byRefLayer,
|
||||
setState,
|
||||
toasts,
|
||||
});
|
||||
|
||||
action.execute(undefined);
|
||||
|
||||
expect(setState).toHaveBeenCalledWith({ ...state, layers: [byValueLayer] });
|
||||
|
||||
expect(toasts.addSuccess).toHaveBeenCalledWith(`Unlinked "${byRefLayer.__lastSaved.title}"`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ToastsStart } from '@kbn/core-notifications-browser';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { LayerAction, StateSetter } from '../../../../types';
|
||||
import {
|
||||
XYByReferenceAnnotationLayerConfig,
|
||||
XYByValueAnnotationLayerConfig,
|
||||
XYState,
|
||||
} from '../../types';
|
||||
|
||||
export const getUnlinkLayerAction = ({
|
||||
state,
|
||||
layer,
|
||||
setState,
|
||||
toasts,
|
||||
}: {
|
||||
state: XYState;
|
||||
layer: XYByReferenceAnnotationLayerConfig;
|
||||
setState: StateSetter<XYState, unknown>;
|
||||
toasts: ToastsStart;
|
||||
}): LayerAction => {
|
||||
return {
|
||||
execute: () => {
|
||||
const unlinkedLayer: XYByValueAnnotationLayerConfig = {
|
||||
layerId: layer.layerId,
|
||||
layerType: layer.layerType,
|
||||
indexPatternId: layer.indexPatternId,
|
||||
ignoreGlobalFilters: layer.ignoreGlobalFilters,
|
||||
annotations: layer.annotations,
|
||||
};
|
||||
|
||||
setState({
|
||||
...state,
|
||||
layers: state.layers.map((layerToCheck) =>
|
||||
layerToCheck.layerId === layer.layerId ? unlinkedLayer : layerToCheck
|
||||
),
|
||||
});
|
||||
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.lens.xyChart.annotations.notificationUnlinked', {
|
||||
defaultMessage: `Unlinked "{title}"`,
|
||||
values: { title: layer.__lastSaved.title },
|
||||
})
|
||||
);
|
||||
},
|
||||
description: i18n.translate('xpack.lens.xyChart.annotations.unlinksFromLibrary', {
|
||||
defaultMessage: 'Saves the annotation group as a part of the Lens Saved Object',
|
||||
}),
|
||||
displayName: i18n.translate('xpack.lens.xyChart.annotations.unlinkFromLibrary', {
|
||||
defaultMessage: 'Unlink from library',
|
||||
}),
|
||||
isCompatible: true,
|
||||
icon: 'unlink',
|
||||
'data-test-subj': 'lnsXY_annotationLayer_unlinkFromLibrary',
|
||||
order: 300,
|
||||
};
|
||||
};
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import type { CoreSetup } from '@kbn/core/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { EventAnnotationPluginSetup } from '@kbn/event-annotation-plugin/public';
|
||||
import type { ExpressionsSetup } from '@kbn/expressions-plugin/public';
|
||||
import type { ChartsPluginSetup } from '@kbn/charts-plugin/public';
|
||||
import { LEGACY_TIME_AXIS } from '@kbn/charts-plugin/common';
|
||||
|
@ -20,7 +19,6 @@ export interface XyVisualizationPluginSetupPlugins {
|
|||
formatFactory: FormatFactory;
|
||||
editorFrame: EditorFrameSetup;
|
||||
charts: ChartsPluginSetup;
|
||||
eventAnnotation: EventAnnotationPluginSetup;
|
||||
}
|
||||
|
||||
export class XyVisualization {
|
||||
|
@ -30,8 +28,10 @@ export class XyVisualization {
|
|||
) {
|
||||
editorFrame.registerVisualization(async () => {
|
||||
const { getXyVisualization } = await import('../../async_services');
|
||||
const [coreStart, { charts, data, fieldFormats, eventAnnotation, unifiedSearch }] =
|
||||
await core.getStartServices();
|
||||
const [
|
||||
coreStart,
|
||||
{ charts, data, fieldFormats, eventAnnotation, unifiedSearch, savedObjectsTagging },
|
||||
] = await core.getStartServices();
|
||||
const [palettes, eventAnnotationService] = await Promise.all([
|
||||
charts.palettes.getPalettes(),
|
||||
eventAnnotation.getService(),
|
||||
|
@ -47,6 +47,7 @@ export class XyVisualization {
|
|||
useLegacyTimeAxis,
|
||||
kibanaTheme: core.theme,
|
||||
unifiedSearch,
|
||||
savedObjectsTagging,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFlyoutFooter } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { css } from '@emotion/react';
|
||||
import { FlyoutContainer } from '../../shared_components/flyout_container';
|
||||
import type { ExtraAppendLayerArg } from './visualization';
|
||||
|
||||
export function LoadAnnotationLibraryFlyout({
|
||||
eventAnnotationService,
|
||||
isLoadLibraryVisible,
|
||||
setLoadLibraryFlyoutVisible,
|
||||
addLayer,
|
||||
}: {
|
||||
isLoadLibraryVisible: boolean;
|
||||
setLoadLibraryFlyoutVisible: (visible: boolean) => void;
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
addLayer: (argument?: ExtraAppendLayerArg) => void;
|
||||
}) {
|
||||
const {
|
||||
renderEventAnnotationGroupSavedObjectFinder: EventAnnotationGroupSavedObjectFinder,
|
||||
loadAnnotationGroup,
|
||||
} = eventAnnotationService || {};
|
||||
|
||||
return (
|
||||
<FlyoutContainer
|
||||
customFooter={
|
||||
<EuiFlyoutFooter className="lnsDimensionContainer__footer">
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
flush="left"
|
||||
size="s"
|
||||
iconType="cross"
|
||||
onClick={() => {
|
||||
setLoadLibraryFlyoutVisible(false);
|
||||
}}
|
||||
data-test-subj="lns-indexPattern-loadLibraryCancel"
|
||||
>
|
||||
{i18n.translate('xpack.lens.loadAnnotationsLibrary.cancel', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
}
|
||||
isOpen={Boolean(isLoadLibraryVisible)}
|
||||
groupLabel={i18n.translate('xpack.lens.editorFrame.loadFromLibrary', {
|
||||
defaultMessage: 'Select annotations from library',
|
||||
})}
|
||||
handleClose={() => {
|
||||
setLoadLibraryFlyoutVisible(false);
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
padding: ${euiThemeVars.euiSize};
|
||||
`}
|
||||
>
|
||||
<EventAnnotationGroupSavedObjectFinder
|
||||
onChoose={({ id }) => {
|
||||
loadAnnotationGroup(id).then((loadedGroup) => {
|
||||
addLayer({ ...loadedGroup, annotationGroupId: id });
|
||||
setLoadLibraryFlyoutVisible(false);
|
||||
});
|
||||
}}
|
||||
onCreateNew={() => {
|
||||
addLayer();
|
||||
setLoadLibraryFlyoutVisible(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FlyoutContainer>
|
||||
);
|
||||
}
|
|
@ -5,15 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DistributiveOmit } from '@elastic/eui';
|
||||
import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
|
||||
import type { SavedObjectReference } from '@kbn/core/public';
|
||||
import {
|
||||
EventAnnotationGroupConfig,
|
||||
EVENT_ANNOTATION_GROUP_TYPE,
|
||||
} from '@kbn/event-annotation-plugin/common';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { isQueryAnnotationConfig } from '@kbn/event-annotation-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { validateQuery } from '@kbn/visualization-ui-components/public';
|
||||
import { DataViewsState } from '../../state_management';
|
||||
import type { FramePublicAPI, DatasourcePublicAPI, VisualizeEditorContext } from '../../types';
|
||||
import { FramePublicAPI, DatasourcePublicAPI, AnnotationGroups } from '../../types';
|
||||
import {
|
||||
visualizationTypes,
|
||||
XYLayerConfig,
|
||||
|
@ -24,8 +29,21 @@ import {
|
|||
XYState,
|
||||
XYPersistedState,
|
||||
XYAnnotationLayerConfig,
|
||||
XYPersistedLayerConfig,
|
||||
XYByReferenceAnnotationLayerConfig,
|
||||
XYPersistedByReferenceAnnotationLayerConfig,
|
||||
XYPersistedLinkedByValueAnnotationLayerConfig,
|
||||
} from './types';
|
||||
import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization_helpers';
|
||||
import {
|
||||
getDataLayers,
|
||||
isAnnotationsLayer,
|
||||
isDataLayer,
|
||||
isPersistedByReferenceAnnotationsLayer,
|
||||
isByReferenceAnnotationsLayer,
|
||||
isPersistedByValueAnnotationsLayer,
|
||||
isPersistedAnnotationsLayer,
|
||||
} from './visualization_helpers';
|
||||
import { nonNullable } from '../../utils';
|
||||
|
||||
export function isHorizontalSeries(seriesType: SeriesType) {
|
||||
return (
|
||||
|
@ -117,64 +135,177 @@ function getLayerReferenceName(layerId: string) {
|
|||
return `xy-visualization-layer-${layerId}`;
|
||||
}
|
||||
|
||||
export function extractReferences(state: XYState) {
|
||||
export const annotationLayerHasUnsavedChanges = (layer: XYAnnotationLayerConfig) => {
|
||||
if (!isByReferenceAnnotationsLayer(layer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
type PropsToCompare = Pick<
|
||||
EventAnnotationGroupConfig,
|
||||
'annotations' | 'ignoreGlobalFilters' | 'indexPatternId'
|
||||
>;
|
||||
|
||||
const currentConfig: PropsToCompare = {
|
||||
annotations: layer.annotations,
|
||||
ignoreGlobalFilters: layer.ignoreGlobalFilters,
|
||||
indexPatternId: layer.indexPatternId,
|
||||
};
|
||||
|
||||
const savedConfig: PropsToCompare = {
|
||||
annotations: layer.__lastSaved.annotations,
|
||||
ignoreGlobalFilters: layer.__lastSaved.ignoreGlobalFilters,
|
||||
indexPatternId: layer.__lastSaved.indexPatternId,
|
||||
};
|
||||
|
||||
return !fastIsEqual(currentConfig, savedConfig);
|
||||
};
|
||||
|
||||
export function getPersistableState(state: XYState) {
|
||||
const savedObjectReferences: SavedObjectReference[] = [];
|
||||
const persistableLayers: Array<DistributiveOmit<XYLayerConfig, 'indexPatternId'>> = [];
|
||||
const persistableLayers: XYPersistedLayerConfig[] = [];
|
||||
state.layers.forEach((layer) => {
|
||||
if (isAnnotationsLayer(layer)) {
|
||||
const { indexPatternId, ...persistableLayer } = layer;
|
||||
savedObjectReferences.push({
|
||||
type: 'index-pattern',
|
||||
id: indexPatternId,
|
||||
name: getLayerReferenceName(layer.layerId),
|
||||
});
|
||||
persistableLayers.push(persistableLayer);
|
||||
} else {
|
||||
if (!isAnnotationsLayer(layer)) {
|
||||
persistableLayers.push(layer);
|
||||
} else {
|
||||
if (isByReferenceAnnotationsLayer(layer)) {
|
||||
const referenceName = `ref-${uuidv4()}`;
|
||||
savedObjectReferences.push({
|
||||
type: EVENT_ANNOTATION_GROUP_TYPE,
|
||||
id: layer.annotationGroupId,
|
||||
name: referenceName,
|
||||
});
|
||||
|
||||
if (!annotationLayerHasUnsavedChanges(layer)) {
|
||||
const persistableLayer: XYPersistedByReferenceAnnotationLayerConfig = {
|
||||
persistanceType: 'byReference',
|
||||
layerId: layer.layerId,
|
||||
layerType: layer.layerType,
|
||||
annotationGroupRef: referenceName,
|
||||
};
|
||||
|
||||
persistableLayers.push(persistableLayer);
|
||||
} else {
|
||||
const persistableLayer: XYPersistedLinkedByValueAnnotationLayerConfig = {
|
||||
persistanceType: 'linked',
|
||||
layerId: layer.layerId,
|
||||
layerType: layer.layerType,
|
||||
annotationGroupRef: referenceName,
|
||||
annotations: layer.annotations,
|
||||
ignoreGlobalFilters: layer.ignoreGlobalFilters,
|
||||
};
|
||||
persistableLayers.push(persistableLayer);
|
||||
|
||||
savedObjectReferences.push({
|
||||
type: 'index-pattern',
|
||||
id: layer.indexPatternId,
|
||||
name: getLayerReferenceName(layer.layerId),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const { indexPatternId, ...persistableLayer } = layer;
|
||||
savedObjectReferences.push({
|
||||
type: 'index-pattern',
|
||||
id: indexPatternId,
|
||||
name: getLayerReferenceName(layer.layerId),
|
||||
});
|
||||
persistableLayers.push({ ...persistableLayer, persistanceType: 'byValue' });
|
||||
}
|
||||
}
|
||||
});
|
||||
return { savedObjectReferences, state: { ...state, layers: persistableLayers } };
|
||||
}
|
||||
|
||||
export function isPersistedState(state: XYPersistedState | XYState): state is XYPersistedState {
|
||||
const annotationLayers = state.layers.filter((l) => isAnnotationsLayer(l));
|
||||
return annotationLayers.some((l) => !('indexPatternId' in l));
|
||||
return state.layers.some(isPersistedAnnotationsLayer);
|
||||
}
|
||||
|
||||
export function injectReferences(
|
||||
state: XYPersistedState,
|
||||
references?: SavedObjectReference[],
|
||||
initialContext?: VisualizeFieldContext | VisualizeEditorContext
|
||||
annotationGroups?: AnnotationGroups,
|
||||
references?: SavedObjectReference[]
|
||||
): XYState {
|
||||
if (!references || !references.length) {
|
||||
return state as XYState;
|
||||
}
|
||||
|
||||
const fallbackIndexPatternId = references.find(({ type }) => type === 'index-pattern')!.id;
|
||||
if (!annotationGroups) {
|
||||
throw new Error(
|
||||
'xy visualization: injecting references relies on annotation groups but they were not provided'
|
||||
);
|
||||
}
|
||||
|
||||
// called on-demand since indexPattern reference won't be here on the vis if its a by-reference group
|
||||
const getIndexPatternIdFromReferences = (annotationLayerId: string) => {
|
||||
const fallbackIndexPatternId = references.find(({ type }) => type === 'index-pattern')!.id;
|
||||
return (
|
||||
references.find(({ name }) => name === getLayerReferenceName(annotationLayerId))?.id ||
|
||||
fallbackIndexPatternId
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
layers: state.layers.map((layer) => {
|
||||
if (!isAnnotationsLayer(layer)) {
|
||||
return layer as XYLayerConfig;
|
||||
}
|
||||
return {
|
||||
...layer,
|
||||
indexPatternId:
|
||||
getIndexPatternIdFromInitialContext(layer, initialContext) ||
|
||||
references.find(({ name }) => name === getLayerReferenceName(layer.layerId))?.id ||
|
||||
fallbackIndexPatternId,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
layers: state.layers
|
||||
.map((persistedLayer) => {
|
||||
if (!isPersistedAnnotationsLayer(persistedLayer)) {
|
||||
return persistedLayer as XYLayerConfig;
|
||||
}
|
||||
|
||||
function getIndexPatternIdFromInitialContext(
|
||||
layer: XYAnnotationLayerConfig,
|
||||
initialContext?: VisualizeFieldContext | VisualizeEditorContext
|
||||
) {
|
||||
if (initialContext && 'isVisualizeAction' in initialContext) {
|
||||
return layer && 'indexPatternId' in layer ? layer.indexPatternId : undefined;
|
||||
}
|
||||
let injectedLayer: XYAnnotationLayerConfig;
|
||||
|
||||
if (isPersistedByValueAnnotationsLayer(persistedLayer)) {
|
||||
injectedLayer = {
|
||||
...persistedLayer,
|
||||
indexPatternId: getIndexPatternIdFromReferences(persistedLayer.layerId),
|
||||
};
|
||||
} else {
|
||||
const annotationGroupId = references?.find(
|
||||
({ name }) => name === persistedLayer.annotationGroupRef
|
||||
)?.id;
|
||||
|
||||
const annotationGroup = annotationGroupId
|
||||
? annotationGroups[annotationGroupId]
|
||||
: undefined;
|
||||
|
||||
if (!annotationGroupId || !annotationGroup) {
|
||||
return undefined; // the annotation group this layer was referencing is gone, so remove the layer
|
||||
}
|
||||
|
||||
// declared as a separate variable for type checking
|
||||
const commonProps: Pick<
|
||||
XYByReferenceAnnotationLayerConfig,
|
||||
'layerId' | 'layerType' | 'annotationGroupId' | '__lastSaved'
|
||||
> = {
|
||||
layerId: persistedLayer.layerId,
|
||||
layerType: persistedLayer.layerType,
|
||||
annotationGroupId,
|
||||
__lastSaved: annotationGroup,
|
||||
};
|
||||
|
||||
if (isPersistedByReferenceAnnotationsLayer(persistedLayer)) {
|
||||
// a clean by-reference layer inherits everything from the library annotation group
|
||||
injectedLayer = {
|
||||
...commonProps,
|
||||
ignoreGlobalFilters: annotationGroup.ignoreGlobalFilters,
|
||||
indexPatternId: annotationGroup.indexPatternId,
|
||||
annotations: cloneDeep(annotationGroup.annotations),
|
||||
};
|
||||
} else {
|
||||
// a linked by-value layer gets settings from visualization state while
|
||||
// still maintaining the reference to the library annotation group
|
||||
injectedLayer = {
|
||||
...commonProps,
|
||||
ignoreGlobalFilters: persistedLayer.ignoreGlobalFilters,
|
||||
indexPatternId: getIndexPatternIdFromReferences(persistedLayer.layerId),
|
||||
annotations: cloneDeep(persistedLayer.annotations),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return injectedLayer;
|
||||
})
|
||||
.filter(nonNullable),
|
||||
};
|
||||
}
|
||||
|
||||
export function getAnnotationLayerErrors(
|
||||
|
|
|
@ -39,13 +39,14 @@ import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
|
|||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { SystemPaletteExpressionFunctionDefinition } from '@kbn/charts-plugin/common';
|
||||
import type {
|
||||
State,
|
||||
State as XYState,
|
||||
YConfig,
|
||||
XYDataLayerConfig,
|
||||
XYReferenceLineLayerConfig,
|
||||
XYAnnotationLayerConfig,
|
||||
AxisConfig,
|
||||
ValidXYDataLayerConfig,
|
||||
XYLayerConfig,
|
||||
} from './types';
|
||||
import type { OperationMetadata, DatasourcePublicAPI, DatasourceLayers } from '../../types';
|
||||
import { getColumnToLabelMap } from './state_helpers';
|
||||
|
@ -65,6 +66,10 @@ import {
|
|||
import type { CollapseExpressionFunction } from '../../../common/expressions';
|
||||
import { hasIcon } from './xy_config_panel/shared/marker_decoration_settings';
|
||||
|
||||
type XYLayerConfigWithSimpleView = XYLayerConfig & { simpleView?: boolean };
|
||||
type XYAnnotationLayerConfigWithSimpleView = XYAnnotationLayerConfig & { simpleView?: boolean };
|
||||
type State = Omit<XYState, 'layers'> & { layers: XYLayerConfigWithSimpleView[] };
|
||||
|
||||
export const getSortedAccessors = (
|
||||
datasource: DatasourcePublicAPI | undefined,
|
||||
layer: XYDataLayerConfig | XYReferenceLineLayerConfig
|
||||
|
@ -83,7 +88,6 @@ export const toExpression = (
|
|||
state: State,
|
||||
datasourceLayers: DatasourceLayers,
|
||||
paletteService: PaletteRegistry,
|
||||
attributes: Partial<{ title: string; description: string }> = {},
|
||||
datasourceExpressionsByLayers: Record<string, Ast>,
|
||||
eventAnnotationService: EventAnnotationServiceType
|
||||
): Ast | null => {
|
||||
|
@ -152,7 +156,6 @@ export function toPreviewExpression(
|
|||
},
|
||||
datasourceLayers,
|
||||
paletteService,
|
||||
{},
|
||||
datasourceExpressionsByLayers,
|
||||
eventAnnotationService
|
||||
);
|
||||
|
@ -429,7 +432,7 @@ const referenceLineLayerToExpression = (
|
|||
};
|
||||
|
||||
const annotationLayerToExpression = (
|
||||
layer: XYAnnotationLayerConfig,
|
||||
layer: XYAnnotationLayerConfigWithSimpleView,
|
||||
eventAnnotationService: EventAnnotationServiceType
|
||||
): Ast => {
|
||||
const extendedAnnotationLayerFn = buildExpressionFunction<ExtendedAnnotationLayerFn>(
|
||||
|
|
|
@ -21,7 +21,10 @@ import type {
|
|||
FillStyle,
|
||||
YAxisConfig,
|
||||
} from '@kbn/expression-xy-plugin/common';
|
||||
import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
|
||||
import {
|
||||
EventAnnotationConfig,
|
||||
EventAnnotationGroupConfig,
|
||||
} from '@kbn/event-annotation-plugin/common';
|
||||
import {
|
||||
IconChartArea,
|
||||
IconChartLine,
|
||||
|
@ -35,7 +38,6 @@ import {
|
|||
IconChartBarHorizontal,
|
||||
} from '@kbn/chart-icons';
|
||||
|
||||
import { DistributiveOmit } from '@elastic/eui';
|
||||
import { CollapseFunction } from '../../../common/expressions';
|
||||
import type { VisualizationType } from '../../types';
|
||||
import type { ValueLabelConfig } from '../../../common/types';
|
||||
|
@ -113,16 +115,66 @@ export interface XYReferenceLineLayerConfig {
|
|||
layerType: 'referenceLine';
|
||||
}
|
||||
|
||||
export interface XYAnnotationLayerConfig {
|
||||
export interface XYByValueAnnotationLayerConfig {
|
||||
layerId: string;
|
||||
layerType: 'annotations';
|
||||
annotations: EventAnnotationConfig[];
|
||||
hide?: boolean;
|
||||
indexPatternId: string;
|
||||
simpleView?: boolean;
|
||||
ignoreGlobalFilters: boolean;
|
||||
}
|
||||
|
||||
export type XYPersistedByValueAnnotationLayerConfig = Omit<
|
||||
XYByValueAnnotationLayerConfig,
|
||||
'indexPatternId' | 'hide' | 'simpleView'
|
||||
> & { persistanceType?: 'byValue'; hide?: boolean; simpleView?: boolean }; // props made optional for backwards compatibility since this is how the existing saved objects are
|
||||
|
||||
export type XYByReferenceAnnotationLayerConfig = XYByValueAnnotationLayerConfig & {
|
||||
annotationGroupId: string;
|
||||
__lastSaved: EventAnnotationGroupConfig;
|
||||
};
|
||||
|
||||
export type XYPersistedByReferenceAnnotationLayerConfig = Pick<
|
||||
XYByValueAnnotationLayerConfig,
|
||||
'layerId' | 'layerType'
|
||||
> & {
|
||||
persistanceType: 'byReference';
|
||||
annotationGroupRef: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the type of hybrid layer we get after the user has made a change to
|
||||
* a by-reference annotation layer and saved the visualization without
|
||||
* first saving the changes to the library annotation layer.
|
||||
*
|
||||
* We maintain the link to the library annotation group, but allow the users
|
||||
* changes (persisted in the visualization state) to override the attributes in
|
||||
* the library version until the user
|
||||
* - saves the changes to the library annotation group
|
||||
* - reverts the changes
|
||||
* - unlinks the layer from the library annotation group
|
||||
*/
|
||||
export type XYPersistedLinkedByValueAnnotationLayerConfig = Omit<
|
||||
XYPersistedByValueAnnotationLayerConfig,
|
||||
'persistanceType'
|
||||
> &
|
||||
Omit<XYPersistedByReferenceAnnotationLayerConfig, 'persistanceType'> & {
|
||||
persistanceType: 'linked';
|
||||
};
|
||||
|
||||
export type XYAnnotationLayerConfig =
|
||||
| XYByReferenceAnnotationLayerConfig
|
||||
| XYByValueAnnotationLayerConfig;
|
||||
|
||||
export type XYPersistedAnnotationLayerConfig =
|
||||
| XYPersistedByReferenceAnnotationLayerConfig
|
||||
| XYPersistedByValueAnnotationLayerConfig
|
||||
| XYPersistedLinkedByValueAnnotationLayerConfig;
|
||||
|
||||
export type XYPersistedLayerConfig =
|
||||
| XYDataLayerConfig
|
||||
| XYReferenceLineLayerConfig
|
||||
| XYPersistedAnnotationLayerConfig;
|
||||
|
||||
export type XYLayerConfig =
|
||||
| XYDataLayerConfig
|
||||
| XYReferenceLineLayerConfig
|
||||
|
@ -166,7 +218,7 @@ export interface XYState {
|
|||
export type State = XYState;
|
||||
|
||||
export type XYPersistedState = Omit<XYState, 'layers'> & {
|
||||
layers: Array<DistributiveOmit<XYLayerConfig, 'indexPatternId'>>;
|
||||
layers: XYPersistedLayerConfig[];
|
||||
};
|
||||
|
||||
export type PersistedState = XYPersistedState;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
import { IconChartBarReferenceLine, IconChartBarAnnotations } from '@kbn/chart-icons';
|
||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { CoreStart, ThemeServiceStart } from '@kbn/core/public';
|
||||
import { CoreStart, SavedObjectReference, ThemeServiceStart } from '@kbn/core/public';
|
||||
import type { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public';
|
||||
|
@ -21,6 +21,9 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
|||
import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public';
|
||||
import { EventAnnotationGroupConfig } from '@kbn/event-annotation-plugin/common';
|
||||
import { isEqual } from 'lodash';
|
||||
import type { AccessorConfig } from '@kbn/visualization-ui-components/public';
|
||||
import { generateId } from '../../id_generator';
|
||||
import {
|
||||
|
@ -37,7 +40,13 @@ import {
|
|||
DimensionEditor,
|
||||
} from './xy_config_panel/dimension_editor';
|
||||
import { LayerHeader, LayerHeaderContent } from './xy_config_panel/layer_header';
|
||||
import type { Visualization, FramePublicAPI, Suggestion, UserMessage } from '../../types';
|
||||
import type {
|
||||
Visualization,
|
||||
FramePublicAPI,
|
||||
Suggestion,
|
||||
UserMessage,
|
||||
AnnotationGroups,
|
||||
} from '../../types';
|
||||
import type { FormBasedPersistedState } from '../../datasources/form_based/types';
|
||||
import {
|
||||
type State,
|
||||
|
@ -48,7 +57,7 @@ import {
|
|||
visualizationTypes,
|
||||
} from './types';
|
||||
import {
|
||||
extractReferences,
|
||||
getPersistableState,
|
||||
getAnnotationLayerErrors,
|
||||
injectReferences,
|
||||
isHorizontalChart,
|
||||
|
@ -99,10 +108,15 @@ import { AnnotationsPanel } from './xy_config_panel/annotations_config_panel';
|
|||
import { DimensionTrigger } from '../../shared_components/dimension_trigger';
|
||||
import { defaultAnnotationLabel } from './annotations/helpers';
|
||||
import { onDropForVisualization } from '../../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils';
|
||||
import { createAnnotationActions } from './annotations/actions';
|
||||
import { AddLayerButton } from './add_layer';
|
||||
import { IgnoredGlobalFiltersEntries } from './info_badges';
|
||||
import { LayerSettings } from './layer_settings';
|
||||
|
||||
const XY_ID = 'lnsXY';
|
||||
|
||||
export type ExtraAppendLayerArg = EventAnnotationGroupConfig & { annotationGroupId: string };
|
||||
|
||||
export const getXyVisualization = ({
|
||||
core,
|
||||
storage,
|
||||
|
@ -113,6 +127,7 @@ export const getXyVisualization = ({
|
|||
kibanaTheme,
|
||||
eventAnnotationService,
|
||||
unifiedSearch,
|
||||
savedObjectsTagging,
|
||||
}: {
|
||||
core: CoreStart;
|
||||
storage: IStorageWrapper;
|
||||
|
@ -123,7 +138,8 @@ export const getXyVisualization = ({
|
|||
useLegacyTimeAxis: boolean;
|
||||
kibanaTheme: ThemeServiceStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
}): Visualization<State, PersistedState> => ({
|
||||
savedObjectsTagging?: SavedObjectTaggingPluginStart;
|
||||
}): Visualization<State, PersistedState, ExtraAppendLayerArg> => ({
|
||||
id: XY_ID,
|
||||
visualizationTypes,
|
||||
getVisualizationTypeId(state) {
|
||||
|
@ -165,7 +181,7 @@ export const getXyVisualization = ({
|
|||
return state;
|
||||
},
|
||||
|
||||
appendLayer(state, layerId, layerType, indexPatternId) {
|
||||
appendLayer(state, layerId, layerType, indexPatternId, extraArg) {
|
||||
if (layerType === 'metricTrendline') {
|
||||
return state;
|
||||
}
|
||||
|
@ -180,6 +196,7 @@ export const getXyVisualization = ({
|
|||
layerId,
|
||||
layerType,
|
||||
indexPatternId,
|
||||
extraArg,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
@ -201,7 +218,7 @@ export const getXyVisualization = ({
|
|||
},
|
||||
|
||||
getPersistableState(state) {
|
||||
return extractReferences(state);
|
||||
return getPersistableState(state);
|
||||
},
|
||||
|
||||
getDescription,
|
||||
|
@ -218,10 +235,16 @@ export const getXyVisualization = ({
|
|||
|
||||
triggers: [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush],
|
||||
|
||||
initialize(addNewLayer, state, _, references, initialContext) {
|
||||
initialize(
|
||||
addNewLayer,
|
||||
state,
|
||||
_mainPalette?,
|
||||
annotationGroups?: AnnotationGroups,
|
||||
references?: SavedObjectReference[]
|
||||
) {
|
||||
const finalState =
|
||||
state && isPersistedState(state)
|
||||
? injectReferences(state, references, initialContext)
|
||||
? injectReferences(state, annotationGroups!, references)
|
||||
: state;
|
||||
return (
|
||||
finalState || {
|
||||
|
@ -255,8 +278,25 @@ export const getXyVisualization = ({
|
|||
];
|
||||
},
|
||||
|
||||
getSupportedActionsForLayer() {
|
||||
return [];
|
||||
getSupportedActionsForLayer(layerId, state, setState, isSaveable) {
|
||||
const layerIndex = state.layers.findIndex((l) => l.layerId === layerId);
|
||||
const layer = state.layers[layerIndex];
|
||||
const actions = [];
|
||||
if (isAnnotationsLayer(layer)) {
|
||||
actions.push(
|
||||
...createAnnotationActions({
|
||||
state,
|
||||
layer,
|
||||
setState,
|
||||
core,
|
||||
isSaveable,
|
||||
eventAnnotationService,
|
||||
savedObjectsTagging,
|
||||
dataViews: data.dataViews,
|
||||
})
|
||||
);
|
||||
}
|
||||
return actions;
|
||||
},
|
||||
|
||||
hasLayerSettings({ state, layerId: currentLayerId }) {
|
||||
|
@ -274,11 +314,6 @@ export const getXyVisualization = ({
|
|||
domElement
|
||||
);
|
||||
},
|
||||
|
||||
onLayerAction(layerId, actionId, state) {
|
||||
return state;
|
||||
},
|
||||
|
||||
onIndexPatternChange(state, indexPatternId, layerId) {
|
||||
const layerIndex = state.layers.findIndex((l) => l.layerId === layerId);
|
||||
const layer = state.layers[layerIndex];
|
||||
|
@ -683,13 +718,33 @@ export const getXyVisualization = ({
|
|||
domElement
|
||||
);
|
||||
},
|
||||
getAddLayerButtonComponent: (props) => {
|
||||
return (
|
||||
<AddLayerButton
|
||||
{...props}
|
||||
eventAnnotationService={eventAnnotationService}
|
||||
addLayer={async (type, loadedGroupInfo) => {
|
||||
if (type === LayerTypes.ANNOTATIONS && loadedGroupInfo) {
|
||||
await props.ensureIndexPattern(
|
||||
loadedGroupInfo.dataViewSpec ?? loadedGroupInfo.indexPatternId
|
||||
);
|
||||
|
||||
props.registerLibraryAnnotationGroup({
|
||||
id: loadedGroupInfo.annotationGroupId,
|
||||
group: loadedGroupInfo,
|
||||
});
|
||||
}
|
||||
|
||||
props.addLayer(type, loadedGroupInfo, !!loadedGroupInfo);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
toExpression: (state, layers, attributes, datasourceExpressionsByLayers = {}) =>
|
||||
toExpression(
|
||||
state,
|
||||
layers,
|
||||
paletteService,
|
||||
attributes,
|
||||
datasourceExpressionsByLayers,
|
||||
eventAnnotationService
|
||||
),
|
||||
|
@ -924,6 +979,12 @@ export const getXyVisualization = ({
|
|||
return suggestion;
|
||||
},
|
||||
|
||||
isEqual(state1, references1, state2, references2, annotationGroups) {
|
||||
const injected1 = injectReferences(state1, annotationGroups, references1);
|
||||
const injected2 = injectReferences(state2, annotationGroups, references2);
|
||||
return isEqual(injected1, injected2);
|
||||
},
|
||||
|
||||
getVisualizationInfo(state, frame) {
|
||||
return getVisualizationInfo(state, frame, paletteService, fieldFormats);
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { uniq } from 'lodash';
|
||||
import { cloneDeep, uniq } from 'lodash';
|
||||
import { IconChartBarHorizontal, IconChartBarStacked, IconChartMixedXy } from '@kbn/chart-icons';
|
||||
import type { LayerType as XYLayerType } from '@kbn/expression-xy-plugin/common';
|
||||
import { DatasourceLayers, OperationMetadata, VisualizationType } from '../../types';
|
||||
|
@ -19,9 +19,17 @@ import {
|
|||
XYDataLayerConfig,
|
||||
XYReferenceLineLayerConfig,
|
||||
SeriesType,
|
||||
XYByReferenceAnnotationLayerConfig,
|
||||
XYPersistedAnnotationLayerConfig,
|
||||
XYPersistedByReferenceAnnotationLayerConfig,
|
||||
XYPersistedLinkedByValueAnnotationLayerConfig,
|
||||
XYPersistedLayerConfig,
|
||||
XYPersistedByValueAnnotationLayerConfig,
|
||||
XYByValueAnnotationLayerConfig,
|
||||
} from './types';
|
||||
import { isHorizontalChart } from './state_helpers';
|
||||
import { layerTypes } from '../..';
|
||||
import type { ExtraAppendLayerArg } from './visualization';
|
||||
|
||||
export function getAxisName(
|
||||
axis: 'x' | 'y' | 'yLeft' | 'yRight',
|
||||
|
@ -138,7 +146,34 @@ export const getReferenceLayers = (layers: Array<Pick<XYLayerConfig, 'layerType'
|
|||
|
||||
export const isAnnotationsLayer = (
|
||||
layer: Pick<XYLayerConfig, 'layerType'>
|
||||
): layer is XYAnnotationLayerConfig => layer.layerType === layerTypes.ANNOTATIONS;
|
||||
): layer is XYAnnotationLayerConfig =>
|
||||
layer.layerType === layerTypes.ANNOTATIONS && 'indexPatternId' in layer;
|
||||
|
||||
export const isPersistedAnnotationsLayer = (
|
||||
layer: XYPersistedLayerConfig
|
||||
): layer is XYPersistedAnnotationLayerConfig =>
|
||||
layer.layerType === layerTypes.ANNOTATIONS && !('indexPatternId' in layer);
|
||||
|
||||
export const isPersistedByValueAnnotationsLayer = (
|
||||
layer: XYPersistedLayerConfig
|
||||
): layer is XYPersistedByValueAnnotationLayerConfig =>
|
||||
isPersistedAnnotationsLayer(layer) &&
|
||||
(layer.persistanceType === 'byValue' || !layer.persistanceType);
|
||||
|
||||
export const isByReferenceAnnotationsLayer = (
|
||||
layer: XYAnnotationLayerConfig
|
||||
): layer is XYByReferenceAnnotationLayerConfig =>
|
||||
'annotationGroupId' in layer && '__lastSaved' in layer;
|
||||
|
||||
export const isPersistedByReferenceAnnotationsLayer = (
|
||||
layer: XYPersistedAnnotationLayerConfig
|
||||
): layer is XYPersistedByReferenceAnnotationLayerConfig =>
|
||||
isPersistedAnnotationsLayer(layer) && layer.persistanceType === 'byReference';
|
||||
|
||||
export const isPersistedLinkedByValueAnnotationsLayer = (
|
||||
layer: XYPersistedAnnotationLayerConfig
|
||||
): layer is XYPersistedLinkedByValueAnnotationLayerConfig =>
|
||||
isPersistedAnnotationsLayer(layer) && layer.persistanceType === 'linked';
|
||||
|
||||
export const getAnnotationsLayers = (layers: Array<Pick<XYLayerConfig, 'layerType'>>) =>
|
||||
(layers || []).filter((layer): layer is XYAnnotationLayerConfig => isAnnotationsLayer(layer));
|
||||
|
@ -273,16 +308,39 @@ const newLayerFn = {
|
|||
[layerTypes.ANNOTATIONS]: ({
|
||||
layerId,
|
||||
indexPatternId,
|
||||
extraArg,
|
||||
}: {
|
||||
layerId: string;
|
||||
indexPatternId: string;
|
||||
}): XYAnnotationLayerConfig => ({
|
||||
layerId,
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
annotations: [],
|
||||
indexPatternId,
|
||||
ignoreGlobalFilters: true,
|
||||
}),
|
||||
extraArg: ExtraAppendLayerArg | undefined;
|
||||
}): XYAnnotationLayerConfig => {
|
||||
if (extraArg) {
|
||||
const { annotationGroupId, ...libraryGroupConfig } = extraArg;
|
||||
|
||||
const newLayer: XYByReferenceAnnotationLayerConfig = {
|
||||
layerId,
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
annotationGroupId,
|
||||
|
||||
annotations: cloneDeep(libraryGroupConfig.annotations),
|
||||
indexPatternId: libraryGroupConfig.indexPatternId,
|
||||
ignoreGlobalFilters: libraryGroupConfig.ignoreGlobalFilters,
|
||||
__lastSaved: libraryGroupConfig,
|
||||
};
|
||||
|
||||
return newLayer;
|
||||
}
|
||||
|
||||
const newLayer: XYByValueAnnotationLayerConfig = {
|
||||
layerId,
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
annotations: [],
|
||||
indexPatternId,
|
||||
ignoreGlobalFilters: true,
|
||||
};
|
||||
|
||||
return newLayer;
|
||||
},
|
||||
};
|
||||
|
||||
export function newLayerState({
|
||||
|
@ -290,13 +348,15 @@ export function newLayerState({
|
|||
layerType = layerTypes.DATA,
|
||||
seriesType,
|
||||
indexPatternId,
|
||||
extraArg,
|
||||
}: {
|
||||
layerId: string;
|
||||
layerType?: XYLayerType;
|
||||
seriesType: SeriesType;
|
||||
indexPatternId: string;
|
||||
extraArg?: ExtraAppendLayerArg;
|
||||
}) {
|
||||
return newLayerFn[layerType]({ layerId, seriesType, indexPatternId });
|
||||
return newLayerFn[layerType]({ layerId, seriesType, indexPatternId, extraArg });
|
||||
}
|
||||
|
||||
export function getLayersByType(state: State, byType?: string) {
|
||||
|
|
|
@ -13,7 +13,7 @@ import { AnnotationsPanel } from '.';
|
|||
import { FramePublicAPI } from '../../../../types';
|
||||
import { DatasourcePublicAPI } from '../../../..';
|
||||
import { createMockFramePublicAPI } from '../../../../mocks';
|
||||
import { State } from '../../types';
|
||||
import { State, XYState } from '../../types';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import moment from 'moment';
|
||||
|
@ -206,7 +206,7 @@ describe('AnnotationsPanel', () => {
|
|||
|
||||
component.find('button[data-test-subj="lns-xyAnnotation-rangeSwitch"]').simulate('click');
|
||||
|
||||
expect(setState).toBeCalledWith({
|
||||
expect(setState).toBeCalledWith<XYState[]>({
|
||||
...state,
|
||||
layers: [
|
||||
{
|
||||
|
@ -232,7 +232,7 @@ describe('AnnotationsPanel', () => {
|
|||
],
|
||||
});
|
||||
component.find('button[data-test-subj="lns-xyAnnotation-rangeSwitch"]').simulate('click');
|
||||
expect(setState).toBeCalledWith({
|
||||
expect(setState).toBeCalledWith<XYState[]>({
|
||||
...state,
|
||||
layers: [
|
||||
{
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue