diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 616ccb65493d..60eb3eb186a8 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -83,6 +83,8 @@ interface Props { SavedObjectFinder: React.ComponentType; stateTransfer?: EmbeddableStateTransfer; hideHeader?: boolean; + hasBorder?: boolean; + hasShadow?: boolean; } interface State { @@ -234,6 +236,8 @@ export class EmbeddablePanel extends React.Component { paddingSize="none" role="figure" aria-labelledby={headerId} + hasBorder={this.props.hasBorder} + hasShadow={this.props.hasShadow} > {!this.props.hideHeader && ( { getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; } -export type EmbeddablePanelHOC = React.FC<{ embeddable: IEmbeddable; hideHeader?: boolean }>; +export type EmbeddablePanelHOC = React.FC<{ + embeddable: IEmbeddable; + hideHeader?: boolean; + hasBorder?: boolean; + hasShadow?: boolean; +}>; export class EmbeddablePublicPlugin implements Plugin { private readonly embeddableFactoryDefinitions: Map< @@ -168,12 +173,18 @@ export class EmbeddablePublicPlugin implements Plugin ({ embeddable, hideHeader, + hasBorder, + hasShadow, }: { embeddable: IEmbeddable; hideHeader?: boolean; + hasBorder?: boolean; + hasShadow?: boolean; }) => ( { + if (flyoutSession) { + flyoutSession.close(); + } + }} + savedObjectId={props.savedObjectId} + /> + ), + { + 'data-test-subj': 'viewEventsFlyout', + ownFocus: true, + } + ); +} diff --git a/src/plugins/vis_augmenter/public/actions/open_events_flyout_action.ts b/src/plugins/vis_augmenter/public/actions/open_events_flyout_action.ts new file mode 100644 index 000000000000..1f74941798e5 --- /dev/null +++ b/src/plugins/vis_augmenter/public/actions/open_events_flyout_action.ts @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { Action, IncompatibleActionError } from '../../../ui_actions/public'; +import { AugmentVisContext } from '../triggers'; +import { openViewEventsFlyout } from './open_events_flyout'; + +export const OPEN_EVENTS_FLYOUT_ACTION = 'OPEN_EVENTS_FLYOUT_ACTION'; + +/** + * This action is identical to VIEW_EVENTS_OPTION_ACTION, but with different context. + * This is because the chart doesn't persist the embeddable, which is the default + * context used by the CONTEXT_MENU_TRIGGER. Because of that, we need a separate + * one that can be persisted in the chart - in this case, the AugmentVisContext, + * which is just a saved object ID. + */ + +export class OpenEventsFlyoutAction implements Action { + public readonly type = OPEN_EVENTS_FLYOUT_ACTION; + public readonly id = OPEN_EVENTS_FLYOUT_ACTION; + public order = 1; + + constructor(private core: CoreStart) {} + + public getIconType() { + return undefined; + } + + public getDisplayName() { + return i18n.translate('visAugmenter.displayName', { + defaultMessage: 'Open View Events flyout', + }); + } + + public async isCompatible({ savedObjectId }: AugmentVisContext) { + return true; + } + + public async execute({ savedObjectId }: AugmentVisContext) { + if (!(await this.isCompatible({ savedObjectId }))) { + throw new IncompatibleActionError(); + } + openViewEventsFlyout({ + core: this.core, + savedObjectId, + }); + } +} diff --git a/src/plugins/vis_augmenter/public/actions/view_events_option_action.tsx b/src/plugins/vis_augmenter/public/actions/view_events_option_action.tsx new file mode 100644 index 000000000000..485735535244 --- /dev/null +++ b/src/plugins/vis_augmenter/public/actions/view_events_option_action.tsx @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { get } from 'lodash'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { VisualizeEmbeddable } from '../../../visualizations/public'; +import { EmbeddableContext } from '../../../embeddable/public'; +import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; +import { Action, IncompatibleActionError } from '../../../ui_actions/public'; +import { openViewEventsFlyout } from './open_events_flyout'; + +export const VIEW_EVENTS_OPTION_ACTION = 'VIEW_EVENTS_OPTION_ACTION'; + +export class ViewEventsOptionAction implements Action { + public readonly type = VIEW_EVENTS_OPTION_ACTION; + public readonly id = VIEW_EVENTS_OPTION_ACTION; + public order = 1; + + constructor(private core: CoreStart) {} + + public getIconType(): EuiIconType { + return 'apmTrace'; + } + + public getDisplayName() { + return i18n.translate('dashboard.actions.viewEvents.displayName', { + defaultMessage: 'View Events', + }); + } + + public async isCompatible({ embeddable }: EmbeddableContext) { + // TODO: add the logic for compatibility here, probably from some helper fn. + // see https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3268 + return true; + } + + public async execute({ embeddable }: EmbeddableContext) { + if (!(await this.isCompatible({ embeddable }))) { + throw new IncompatibleActionError(); + } + + const visEmbeddable = embeddable as VisualizeEmbeddable; + const savedObjectId = get(visEmbeddable.getInput(), 'savedObjectId', ''); + + openViewEventsFlyout({ + core: this.core, + savedObjectId, + }); + } +} diff --git a/src/plugins/vis_augmenter/public/components/base_vis_item.tsx b/src/plugins/vis_augmenter/public/components/base_vis_item.tsx new file mode 100644 index 000000000000..c42762390943 --- /dev/null +++ b/src/plugins/vis_augmenter/public/components/base_vis_item.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { getEmbeddable } from '../services'; +import { VisualizeEmbeddable } from '../../../visualizations/public'; +import './styles.scss'; + +interface Props { + embeddable: VisualizeEmbeddable; +} + +export function BaseVisItem(props: Props) { + const PanelComponent = getEmbeddable().getEmbeddablePanel(); + + return ( + + + + + + + ); +} diff --git a/src/plugins/vis_augmenter/public/components/date_range_item.tsx b/src/plugins/vis_augmenter/public/components/date_range_item.tsx new file mode 100644 index 000000000000..189805319208 --- /dev/null +++ b/src/plugins/vis_augmenter/public/components/date_range_item.tsx @@ -0,0 +1,64 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import moment from 'moment'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiIcon, + prettyDuration, + EuiButton, +} from '@elastic/eui'; +import { TimeRange } from '../../../data/common'; +import { DATE_RANGE_FORMAT } from './view_events_flyout'; + +interface Props { + timeRange: TimeRange; + reload: () => void; +} + +export function DateRangeItem(props: Props) { + const [lastUpdatedTime, setLastUpdatedTime] = useState( + moment(Date.now()).format(DATE_RANGE_FORMAT) + ); + + const durationText = prettyDuration( + props.timeRange.from, + props.timeRange.to, + [], + DATE_RANGE_FORMAT + ); + + return ( + + + + + + {durationText} + + + { + props.reload(); + setLastUpdatedTime(moment(Date.now()).format(DATE_RANGE_FORMAT)); + }} + > + Refresh + + + + + {`This view is not updated to load the latest events automatically. + Last updated: ${lastUpdatedTime}`} + + + + ); +} diff --git a/src/plugins/vis_augmenter/public/components/error_flyout_body.tsx b/src/plugins/vis_augmenter/public/components/error_flyout_body.tsx new file mode 100644 index 000000000000..c2016cd9ede5 --- /dev/null +++ b/src/plugins/vis_augmenter/public/components/error_flyout_body.tsx @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlyoutBody, EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; + +interface Props { + errorMessage: string; +} + +export function ErrorFlyoutBody(props: Props) { + return ( + + + + + {props.errorMessage} + + + + + ); +} diff --git a/src/plugins/vis_augmenter/public/components/event_vis_item.tsx b/src/plugins/vis_augmenter/public/components/event_vis_item.tsx new file mode 100644 index 000000000000..afa7accf43a8 --- /dev/null +++ b/src/plugins/vis_augmenter/public/components/event_vis_item.tsx @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLink, EuiNotificationBadge } from '@elastic/eui'; +import { getEmbeddable, getCore } from '../services'; +import './styles.scss'; +import { EventVisEmbeddableItem } from './'; + +interface Props { + item: EventVisEmbeddableItem; +} + +export function EventVisItem(props: Props) { + const PanelComponent = getEmbeddable().getEmbeddablePanel(); + const baseUrl = getCore().http.basePath; + const { name, urlPath } = props.item.visLayer.pluginResource; + + return ( + <> + + + + + + {name} + + + 3 + + + + + + + + + + ); +} diff --git a/src/plugins/vis_augmenter/public/components/events_panel.tsx b/src/plugins/vis_augmenter/public/components/events_panel.tsx new file mode 100644 index 000000000000..d8400affe95a --- /dev/null +++ b/src/plugins/vis_augmenter/public/components/events_panel.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import './styles.scss'; +import { EventVisEmbeddableItem, EventVisEmbeddablesMap } from './'; +import { PluginEventsPanel } from './plugin_events_panel'; + +interface Props { + eventVisEmbeddablesMap: EventVisEmbeddablesMap; +} + +export function EventsPanel(props: Props) { + return ( + <> + {Array.from(props.eventVisEmbeddablesMap.keys()).map((key, index) => { + return ( +
+ {index !== 0 ? : null} + +
+ ); + })} + + ); +} diff --git a/src/plugins/vis_augmenter/public/components/index.ts b/src/plugins/vis_augmenter/public/components/index.ts new file mode 100644 index 000000000000..ad96fd25af55 --- /dev/null +++ b/src/plugins/vis_augmenter/public/components/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { + ViewEventsFlyout, + EventVisEmbeddablesMap, + EventVisEmbeddableItem, +} from './view_events_flyout'; diff --git a/src/plugins/vis_augmenter/public/components/loading_flyout_body.tsx b/src/plugins/vis_augmenter/public/components/loading_flyout_body.tsx new file mode 100644 index 000000000000..a7c8c9b1a371 --- /dev/null +++ b/src/plugins/vis_augmenter/public/components/loading_flyout_body.tsx @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlyoutBody, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; + +export function LoadingFlyoutBody() { + return ( + + + + + + + + ); +} diff --git a/src/plugins/vis_augmenter/public/components/plugin_events_panel.tsx b/src/plugins/vis_augmenter/public/components/plugin_events_panel.tsx new file mode 100644 index 000000000000..6e5ae63f21d9 --- /dev/null +++ b/src/plugins/vis_augmenter/public/components/plugin_events_panel.tsx @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexItem, EuiText } from '@elastic/eui'; +import './styles.scss'; +import { EventVisItem } from './event_vis_item'; +import { EventVisEmbeddableItem } from './'; + +interface Props { + pluginTitle: string; + items: Array; +} + +export function PluginEventsPanel(props: Props) { + return ( + <> + + + {props.pluginTitle} + + + + {props.items.map((item, index) => ( + + ))} + + ); +} diff --git a/src/plugins/vis_augmenter/public/components/styles.scss b/src/plugins/vis_augmenter/public/components/styles.scss new file mode 100644 index 000000000000..65523f887de0 --- /dev/null +++ b/src/plugins/vis_augmenter/public/components/styles.scss @@ -0,0 +1,42 @@ +$event-vis-height: 75px; + +.view-events-flyout { + &__baseVis { + min-height: 25vh; // Visualizations require the container to have a valid width and height to render + } + &__eventVis { + height: $event-vis-height + } + &__visDescription { + width: 200px; + } + &__content { + position: absolute; + top: 110px; + right: $euiSizeM; + bottom: $euiSizeM; + left: $euiSizeM; + } + &__contentPanel { + @include euiYScroll; + overflow: auto; + overflow-x: hidden; + scrollbar-gutter: stable both-edges; + } + } + + .hide-y-scroll { + overflow-y: hidden; + } + + .show-y-scroll { + overflow-y: scroll; + } + + .date-range-panel-height { + height: 45px; + } + + .timeline-panel-height { + height: $event-vis-height + } diff --git a/src/plugins/vis_augmenter/public/components/timeline_panel.tsx b/src/plugins/vis_augmenter/public/components/timeline_panel.tsx new file mode 100644 index 000000000000..c55b64e5cc42 --- /dev/null +++ b/src/plugins/vis_augmenter/public/components/timeline_panel.tsx @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { getEmbeddable } from '../services'; +import './styles.scss'; +import { VisualizeEmbeddable } from '../../../visualizations/public'; + +interface Props { + embeddable: VisualizeEmbeddable; +} + +export function TimelinePanel(props: Props) { + const PanelComponent = getEmbeddable().getEmbeddablePanel(); + return ( + + + This will be the static timeline chart + + + + + + ); +} diff --git a/src/plugins/vis_augmenter/public/components/utils.tsx b/src/plugins/vis_augmenter/public/components/utils.tsx new file mode 100644 index 000000000000..a41d6d258a93 --- /dev/null +++ b/src/plugins/vis_augmenter/public/components/utils.tsx @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ErrorEmbeddable } from '../../../embeddable/public'; + +export const getErrorMessage = (errorEmbeddable: ErrorEmbeddable): string => { + return errorEmbeddable.error instanceof Error + ? errorEmbeddable.error.message + : errorEmbeddable.error; +}; diff --git a/src/plugins/vis_augmenter/public/components/view_events_flyout.tsx b/src/plugins/vis_augmenter/public/components/view_events_flyout.tsx new file mode 100644 index 000000000000..fde004145e40 --- /dev/null +++ b/src/plugins/vis_augmenter/public/components/view_events_flyout.tsx @@ -0,0 +1,264 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect } from 'react'; +import { get } from 'lodash'; +import { + EuiFlyoutBody, + EuiFlyoutHeader, + EuiFlyout, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { getEmbeddable, getQueryService } from '../services'; +import './styles.scss'; +import { VisualizeEmbeddable, VisualizeInput } from '../../../visualizations/public'; +import { TimeRange } from '../../../data/common'; +import { BaseVisItem } from './base_vis_item'; +import { isPointInTimeEventsVisLayer, PointInTimeEventsVisLayer, VisLayer } from '../../common'; +import { DateRangeItem } from './date_range_item'; +import { LoadingFlyoutBody } from './loading_flyout_body'; +import { ErrorFlyoutBody } from './error_flyout_body'; +import { EventsPanel } from './events_panel'; +import { TimelinePanel } from './timeline_panel'; +import { ErrorEmbeddable } from '../../../embeddable/public'; +import { getErrorMessage } from './utils'; + +interface Props { + onClose: () => void; + savedObjectId: string; +} + +export type EventVisEmbeddableItem = { + visLayer: VisLayer; + embeddable: VisualizeEmbeddable; +}; + +export type EventVisEmbeddablesMap = Map; + +export const DATE_RANGE_FORMAT = 'MM/DD/YYYY HH:mm'; + +export function ViewEventsFlyout(props: Props) { + const [visEmbeddable, setVisEmbeddable] = useState(undefined); + // This map persists a plugin resource type -> a list of vis embeddables + // for each VisLayer of that type + const [eventVisEmbeddablesMap, setEventVisEmbeddablesMap] = useState< + EventVisEmbeddablesMap | undefined + >(undefined); + const [timelineVisEmbeddable, setTimelineVisEmbeddable] = useState< + VisualizeEmbeddable | undefined + >(undefined); + const [timeRange, setTimeRange] = useState(undefined); + const [isLoading, setIsLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(undefined); + + const embeddableVisFactory = getEmbeddable().getEmbeddableFactory('visualization'); + + function reload() { + visEmbeddable?.reload(); + eventVisEmbeddablesMap?.forEach((embeddableItems) => { + embeddableItems.forEach((embeddableItem) => { + embeddableItem.embeddable.reload(); + }); + }); + } + + async function fetchVisEmbeddable() { + try { + const contextInput = { + filters: getQueryService().filterManager.getFilters(), + query: getQueryService().queryString.getQuery(), + timeRange: getQueryService().timefilter.timefilter.getTime(), + visLayerResourceIds: [ + 'detector-1-id', + 'detector-2-id', + 'monitor-1-id', + 'monitor-2-id', + 'monitor-3-id', + 'monitor-4-id', + ], + }; + setTimeRange(contextInput.timeRange); + + const embeddable = (await embeddableVisFactory?.createFromSavedObject( + props.savedObjectId, + contextInput + )) as VisualizeEmbeddable | ErrorEmbeddable; + + if (embeddable instanceof ErrorEmbeddable) { + throw getErrorMessage(embeddable); + } + + embeddable.updateInput({ + // @ts-ignore + refreshConfig: { + value: 0, + pause: true, + }, + }); + + // reload is needed so we can fetch the initial VisLayers, and so they're + // assigned to the vislayers field in the embeddable itself + embeddable.reload(); + + setVisEmbeddable(embeddable); + } catch (err: any) { + setErrorMessage(String(err)); + } + } + + // For each VisLayer in the base vis embeddable, generate a new filtered vis + // embeddable to only show datapoints for that particular VisLayer. Partition them by + // plugin resource type + async function createEventEmbeddables(visEmbeddable: VisualizeEmbeddable) { + try { + let map = new Map() as EventVisEmbeddablesMap; + // Currently only support PointInTimeEventVisLayers. Different layer types + // may require different logic in here + const visLayers = (get(visEmbeddable, 'visLayers', []) as VisLayer[]).filter((visLayer) => + isPointInTimeEventsVisLayer(visLayer) + ) as PointInTimeEventsVisLayer[]; + if (visLayers !== undefined) { + const contextInput = { + filters: visEmbeddable.getInput().filters, + query: visEmbeddable.getInput().query, + timeRange: visEmbeddable.getInput().timeRange, + }; + + await Promise.all( + visLayers.map(async (visLayer) => { + const pluginResourceType = visLayer.pluginResource.type; + const eventEmbeddable = (await embeddableVisFactory?.createFromSavedObject( + props.savedObjectId, + { + ...contextInput, + visLayerResourceIds: [visLayer.pluginResource.id as string], + } as VisualizeInput + )) as VisualizeEmbeddable | ErrorEmbeddable; + + if (eventEmbeddable instanceof ErrorEmbeddable) { + throw getErrorMessage(eventEmbeddable); + } + + eventEmbeddable.updateInput({ + // @ts-ignore + refreshConfig: { + value: 0, + pause: true, + }, + }); + + const curList = (map.get(pluginResourceType) === undefined + ? [] + : map.get(pluginResourceType)) as EventVisEmbeddableItem[]; + curList.push({ + visLayer, + embeddable: eventEmbeddable, + } as EventVisEmbeddableItem); + map.set(pluginResourceType, curList); + }) + ); + setEventVisEmbeddablesMap(map); + } + } catch (err: any) { + setErrorMessage(String(err)); + } + } + + async function createTimelineEmbeddable(visEmbeddable: VisualizeEmbeddable) { + try { + const contextInput = { + filters: visEmbeddable.getInput().filters, + query: visEmbeddable.getInput().query, + timeRange: visEmbeddable.getInput().timeRange, + // TODO: add some field in the visualize embeddable to define + // showing any data at all + }; + + const embeddable = (await embeddableVisFactory?.createFromSavedObject( + props.savedObjectId, + contextInput + )) as VisualizeEmbeddable | ErrorEmbeddable; + + if (embeddable instanceof ErrorEmbeddable) { + throw getErrorMessage(embeddable); + } + + embeddable.updateInput({ + // @ts-ignore + refreshConfig: { + value: 0, + pause: true, + }, + }); + setTimelineVisEmbeddable(embeddable); + } catch (err: any) { + setErrorMessage(String(err)); + } + } + + useEffect(() => { + fetchVisEmbeddable(); + }, [props.savedObjectId]); + + useEffect(() => { + if (visEmbeddable?.visLayers) { + createEventEmbeddables(visEmbeddable); + createTimelineEmbeddable(visEmbeddable); + } + }, [visEmbeddable?.visLayers]); + + useEffect(() => { + if ( + visEmbeddable !== undefined && + eventVisEmbeddablesMap !== undefined && + timeRange !== undefined && + timelineVisEmbeddable !== undefined + ) { + setIsLoading(false); + } + }, [visEmbeddable, eventVisEmbeddablesMap, timeRange, timelineVisEmbeddable]); + + return ( + <> + + + +

{isLoading || errorMessage ? <>  : `${visEmbeddable.getTitle()}`}

+
+
+ {errorMessage ? ( + + ) : isLoading ? ( + + ) : ( + + + + + + + + + + + + + + + + + )} +
+ + ); +} diff --git a/src/plugins/vis_augmenter/public/index.ts b/src/plugins/vis_augmenter/public/index.ts index e931e2ac2e03..2cf0d6d2c761 100644 --- a/src/plugins/vis_augmenter/public/index.ts +++ b/src/plugins/vis_augmenter/public/index.ts @@ -10,3 +10,7 @@ export function plugin(initializerContext: PluginInitializerContext) { return new VisAugmenterPlugin(initializerContext); } export { VisAugmenterSetup, VisAugmenterStart }; + +// This is done in other plugins so these items can be accessed in public files of +// dependent plugins +export * from '../common'; diff --git a/src/plugins/vis_augmenter/public/plugin.ts b/src/plugins/vis_augmenter/public/plugin.ts index d53116bdd12d..0905fefd9925 100644 --- a/src/plugins/vis_augmenter/public/plugin.ts +++ b/src/plugins/vis_augmenter/public/plugin.ts @@ -5,8 +5,19 @@ import { ExpressionsSetup } from '../../expressions/public'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; import { visLayers } from './expressions'; +import { registerTriggersAndActions } from './ui_actions_bootstrap'; +import { UiActionsStart } from '../../ui_actions/public'; +import { + setUiActions, + setEmbeddable, + setQueryService, + setVisualizations, + setCore, +} from './services'; +import { EmbeddableStart } from '../../embeddable/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { VisualizationsStart } from '../../visualizations/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface VisAugmenterSetup {} @@ -15,12 +26,14 @@ export interface VisAugmenterSetup {} export interface VisAugmenterStart {} export interface VisAugmenterSetupDeps { - data: DataPublicPluginSetup; expressions: ExpressionsSetup; } export interface VisAugmenterStartDeps { + uiActions: UiActionsStart; + embeddable: EmbeddableStart; data: DataPublicPluginStart; + visualizations: VisualizationsStart; } export class VisAugmenterPlugin @@ -30,13 +43,25 @@ export class VisAugmenterPlugin public setup( core: CoreSetup, - { data, expressions }: VisAugmenterSetupDeps + { expressions }: VisAugmenterSetupDeps ): VisAugmenterSetup { expressions.registerType(visLayers); return {}; } - public start(core: CoreStart, { data }: VisAugmenterStartDeps): VisAugmenterStart { + public start( + core: CoreStart, + { uiActions, embeddable, data, visualizations }: VisAugmenterStartDeps + ): VisAugmenterStart { + setUiActions(uiActions); + setEmbeddable(embeddable); + setQueryService(data.query); + setVisualizations(visualizations); + setCore(core); + + // registers the triggers & actions defined in this plugin + // also maps any triggers to possible actions + registerTriggersAndActions(core); return {}; } diff --git a/src/plugins/vis_augmenter/public/services.ts b/src/plugins/vis_augmenter/public/services.ts new file mode 100644 index 000000000000..dc1009d8f70e --- /dev/null +++ b/src/plugins/vis_augmenter/public/services.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EmbeddableStart } from '../../embeddable/public'; +import { createGetterSetter } from '../../opensearch_dashboards_utils/public'; +import { UiActionsStart } from '../../ui_actions/public'; +import { DataPublicPluginStart } from '../../../plugins/data/public'; +import { VisualizationsStart } from '../../visualizations/public'; +import { CoreStart } from '../../../core/public'; + +export const [getUiActions, setUiActions] = createGetterSetter('UIActions'); + +export const [getEmbeddable, setEmbeddable] = createGetterSetter('embeddable'); + +export const [getQueryService, setQueryService] = createGetterSetter< + DataPublicPluginStart['query'] +>('Query'); + +export const [getVisualizations, setVisualizations] = createGetterSetter( + 'visualizations' +); + +export const [getCore, setCore] = createGetterSetter('Core'); diff --git a/src/plugins/vis_augmenter/public/triggers/index.ts b/src/plugins/vis_augmenter/public/triggers/index.ts new file mode 100644 index 000000000000..f7a3dc2d9dce --- /dev/null +++ b/src/plugins/vis_augmenter/public/triggers/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { openEventsFlyoutTrigger, AugmentVisContext } from './open_events_flyout_trigger'; diff --git a/src/plugins/vis_augmenter/public/triggers/open_events_flyout_trigger.ts b/src/plugins/vis_augmenter/public/triggers/open_events_flyout_trigger.ts new file mode 100644 index 000000000000..d405d7364f3b --- /dev/null +++ b/src/plugins/vis_augmenter/public/triggers/open_events_flyout_trigger.ts @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { Trigger, OPEN_EVENTS_FLYOUT_TRIGGER } from '../../../ui_actions/public'; + +export interface AugmentVisContext { + savedObjectId: string; +} + +export const openEventsFlyoutTrigger: Trigger<'OPEN_EVENTS_FLYOUT_TRIGGER'> = { + id: OPEN_EVENTS_FLYOUT_TRIGGER, + title: i18n.translate('uiActions.triggers.openEventsFlyoutTrigger', { + defaultMessage: 'Open the View Events flyout', + }), + description: i18n.translate('uiActions.triggers.openEventsFlyoutDescription', { + defaultMessage: `Opening the 'View Events' flyout`, + }), +}; diff --git a/src/plugins/vis_augmenter/public/ui_actions_bootstrap.ts b/src/plugins/vis_augmenter/public/ui_actions_bootstrap.ts new file mode 100644 index 000000000000..0a68f6b42b13 --- /dev/null +++ b/src/plugins/vis_augmenter/public/ui_actions_bootstrap.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CoreStart } from 'opensearch-dashboards/public'; +import { + OpenEventsFlyoutAction, + ViewEventsOptionAction, + OPEN_EVENTS_FLYOUT_ACTION, + VIEW_EVENTS_OPTION_ACTION, +} from './actions'; +import { AugmentVisContext, openEventsFlyoutTrigger } from './triggers'; +import { OPEN_EVENTS_FLYOUT_TRIGGER } from '../../ui_actions/public'; +import { CONTEXT_MENU_TRIGGER, EmbeddableContext } from '../../embeddable/public'; +import { getUiActions } from './services'; + +// Overridding the mappings defined in UIActions plugin so that +// the new trigger and action definitions resolve +declare module '../../ui_actions/public' { + export interface TriggerContextMapping { + [OPEN_EVENTS_FLYOUT_TRIGGER]: AugmentVisContext; + } + + export interface ActionContextMapping { + [OPEN_EVENTS_FLYOUT_ACTION]: AugmentVisContext; + [VIEW_EVENTS_OPTION_ACTION]: EmbeddableContext; + } +} + +export const registerTriggersAndActions = (core: CoreStart) => { + const openEventsFlyoutAction = new OpenEventsFlyoutAction(core); + const viewEventsOptionAction = new ViewEventsOptionAction(core); + + getUiActions().registerAction(openEventsFlyoutAction); + getUiActions().registerAction(viewEventsOptionAction); + getUiActions().registerTrigger(openEventsFlyoutTrigger); + // Opening View Events flyout from the chart + getUiActions().addTriggerAction(OPEN_EVENTS_FLYOUT_TRIGGER, openEventsFlyoutAction); + // Opening View Events flyout from the context menu + getUiActions().addTriggerAction(CONTEXT_MENU_TRIGGER, viewEventsOptionAction); +}; diff --git a/src/plugins/vis_type_vega/opensearch_dashboards.json b/src/plugins/vis_type_vega/opensearch_dashboards.json index ca4d7020c2fa..210d4176e201 100644 --- a/src/plugins/vis_type_vega/opensearch_dashboards.json +++ b/src/plugins/vis_type_vega/opensearch_dashboards.json @@ -3,7 +3,14 @@ "version": "opensearchDashboards", "server": true, "ui": true, - "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions", "inspector"], - "optionalPlugins": ["home","usageCollection"], + "requiredPlugins": [ + "data", + "visualizations", + "mapsLegacy", + "expressions", + "inspector", + "uiActions" + ], + "optionalPlugins": ["home", "usageCollection"], "requiredBundles": ["opensearchDashboardsUtils", "opensearchDashboardsReact", "visDefaultEditor"] } diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index 64ab81eedfd2..d2371c4405f1 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -50,6 +50,8 @@ import './index.scss'; import { ConfigSchema } from '../config'; import { getVegaInspectorView } from './vega_inspector'; +import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import { setUiActions } from './services'; /** @internal */ export interface VegaVisualizationDependencies { @@ -72,6 +74,7 @@ export interface VegaPluginSetupDependencies { /** @internal */ export interface VegaPluginStartDependencies { data: DataPublicPluginStart; + uiActions: UiActionsStart; } /** @internal */ @@ -108,9 +111,10 @@ export class VegaPlugin implements Plugin, void> { visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies)); } - public start(core: CoreStart, { data }: VegaPluginStartDependencies) { + public start(core: CoreStart, { data, uiActions }: VegaPluginStartDependencies) { setNotifications(core.notifications); setData(data); + setUiActions(uiActions); setInjectedMetadata(core.injectedMetadata); } } diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts index d241b66d472c..3aafb1f49af5 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -33,6 +33,7 @@ import { CoreStart, NotificationsStart, IUiSettingsClient } from 'src/core/publi import { DataPublicPluginStart } from '../../data/public'; import { createGetterSetter } from '../../opensearch_dashboards_utils/public'; import { MapsLegacyConfig } from '../../maps_legacy/config'; +import { UiActionsStart } from 'src/plugins/ui_actions/public'; export const [getData, setData] = createGetterSetter('Data'); @@ -40,6 +41,8 @@ export const [getNotifications, setNotifications] = createGetterSetter('UIActions'); + export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); export const [getInjectedMetadata, setInjectedMetadata] = createGetterSetter< diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index abe2d3665ed3..c6341b7ad7d8 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -48,6 +48,7 @@ type Output = Promise>; interface Arguments { spec: string; + savedObjectId: string; } export type VisParams = Required; @@ -79,15 +80,24 @@ export const createVegaFn = ( default: '', help: '', }, + savedObjectId: { + types: ['string'], + default: '', + help: '', + }, }, async fn(input, args, context) { const vegaRequestHandler = createVegaRequestHandler(dependencies, context); + const timeRange = get(input, 'timeRange') as TimeRange; + const query = get(input, 'query') as Query; + const filters = get(input, 'filters') as any; + const response = await vegaRequestHandler({ - timeRange: get(input, 'timeRange') as TimeRange, - query: get(input, 'query') as Query, - filters: get(input, 'filters') as any, - visParams: { spec: args.spec }, + timeRange, + query, + filters, + visParams: { spec: args.spec, savedObjectId: args.savedObjectId }, }); return { @@ -98,6 +108,7 @@ export const createVegaFn = ( visType: 'vega', visConfig: { spec: args.spec, + savedObjectId: args.savedObjectId, }, }, }; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 1286495af901..5a805ecce899 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -30,6 +30,7 @@ import $ from 'jquery'; import moment from 'moment'; +import { get } from 'lodash'; import dateMath from '@elastic/datemath'; import { vega, vegaLite, vegaExpressionInterpreter } from '../lib/vega'; import { Utils } from '../data_model/utils'; @@ -38,8 +39,9 @@ import { i18n } from '@osd/i18n'; import { TooltipHandler } from './vega_tooltip'; import { opensearchFilters } from '../../../data/public'; -import { getEnableExternalUrls, getData } from '../services'; +import { getEnableExternalUrls, getData, getUiActions } from '../services'; import { extractIndexPatternsFromSpec } from '../lib/extract_index_pattern'; +import { OPEN_EVENTS_FLYOUT_TRIGGER } from '../../../ui_actions/public'; vega.scheme('elastic', euiPaletteColorBlind()); @@ -84,6 +86,7 @@ export class VegaBaseView { this._destroyHandlers = []; this._initialized = false; this._enableExternalUrls = getEnableExternalUrls(); + this._visInput = opts.visInput; } async init() { @@ -281,6 +284,14 @@ export class VegaBaseView { this._addDestroyHandler(() => tthandler.hideTooltip()); } + // trigger the open events flyout UIAction if a click happens on an annotation datapoint + view.addEventListener('click', function (event, item) { + // TODO: add filtering to determine if the item is a datapoint, and whether or not it is an + // "annotation" datapoint vs. regular datapoint + const { savedObjectId } = get(view, '_opensearchDashboardsView._visInput', {}); + getUiActions().getTrigger(OPEN_EVENTS_FLYOUT_TRIGGER).exec({ savedObjectId }); + }); + return view.runAsync(); // Allows callers to await rendering } } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js b/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js index 4cde4f8e59d7..64aefae44abf 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js @@ -65,17 +65,18 @@ export class TooltipHandler { view.tooltip(this.handler.bind(this)); } - /** - * The handler function. - */ + // test handler handler(view, event, item, value) { this.hideTooltip(); + //console.log('item: ', item); + // hide tooltip for null, undefined, or empty string values if (value == null || value === '') { return; } + // creating element & adding id & class attributes to it so it renders in the euiToolTip styling const el = document.createElement('div'); el.setAttribute('id', tooltipId); ['vgaVis__tooltip', 'euiToolTipPopover', 'euiToolTip', `euiToolTip--${this.position}`].forEach( @@ -87,8 +88,8 @@ export class TooltipHandler { // Sanitized HTML is created by the tooltip library, // with a large number of tests, hence suppressing eslint here. // eslint-disable-next-line no-unsanitized/property - el.innerHTML = createTooltipContent(value, _.escape, 2); - + // el.innerHTML = createTooltipContent(value, _.escape, 2); + el.innerHTML = this.createTooltipHtml(value); // add to DOM to calculate tooltip size document.body.appendChild(el); @@ -117,6 +118,10 @@ export class TooltipHandler { el.setAttribute('style', `top: ${pos.top}px; left: ${pos.left}px`); } + createTooltipHtml(value) { + return '

some custom tooltip

'; + } + hideTooltip() { const el = document.getElementById(tooltipId); if (el) el.remove(); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.js b/src/plugins/vis_type_vega/public/vega_visualization.js index af5c58f8a149..b7cae964ae5b 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.js @@ -29,6 +29,7 @@ */ import { i18n } from '@osd/i18n'; +import { get } from 'lodash'; import { getNotifications, getData } from './services'; export const createVegaVisualization = ({ getServiceSettings }) => @@ -83,6 +84,7 @@ export const createVegaVisualization = ({ getServiceSettings }) => const serviceSettings = await getServiceSettings(); const { filterManager } = this.dataPlugin.query; const { timefilter } = this.dataPlugin.query.timefilter; + const vegaViewParams = { parentEl: this._el, applyFilter: this._vis.API.events.applyFilter, @@ -90,6 +92,9 @@ export const createVegaVisualization = ({ getServiceSettings }) => serviceSettings, filterManager, timefilter, + visInput: { + savedObjectId: get(this._vis, 'params.savedObjectId'), + }, }; if (vegaParser.useMap) { diff --git a/src/plugins/visualizations/opensearch_dashboards.json b/src/plugins/visualizations/opensearch_dashboards.json index 6223ffce3808..b7c5e4ab9b4e 100644 --- a/src/plugins/visualizations/opensearch_dashboards.json +++ b/src/plugins/visualizations/opensearch_dashboards.json @@ -5,5 +5,5 @@ "ui": true, "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "inspector", "dashboard"], "optionalPlugins": ["usageCollection"], - "requiredBundles": ["opensearchDashboardsUtils", "discover", "savedObjects"] + "requiredBundles": ["opensearchDashboardsUtils", "discover", "savedObjects", "visAugmenter"] } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index f3808951d519..c34c02e8b4ac 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -65,6 +65,7 @@ import { SavedObjectAttributes } from '../../../../core/types'; import { AttributeService } from '../../../dashboard/public'; import { SavedVisualizationsLoader } from '../saved_visualizations'; import { VisSavedObject } from '../types'; +import { PointInTimeEventsVisLayer, VisLayer, VisLayerTypes } from '../../../vis_augmenter/public'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -83,6 +84,7 @@ export interface VisualizeInput extends EmbeddableInput { }; savedVis?: SerializedVis; table?: unknown; + visLayerResourceIds?: string[]; } export interface VisualizeOutput extends EmbeddableOutput { @@ -127,6 +129,7 @@ export class VisualizeEmbeddable VisualizeByReferenceInput >; private savedVisualizationsLoader?: SavedVisualizationsLoader; + public visLayers?: Array; constructor( timefilter: TimefilterContract, @@ -362,6 +365,7 @@ export class VisualizeEmbeddable } public destroy() { + console.log('destroying...'); super.destroy(); this.subscriptions.forEach((s) => s.unsubscribe()); this.vis.uiState.off('change', this.uiStateChangeHandler); @@ -393,6 +397,133 @@ export class VisualizeEmbeddable } this.abortController = new AbortController(); const abortController = this.abortController; + + // TODO: remove later. testing dummy vis layers that may be returned from the set + // of expressions functinos ran by the plugins + const dummyVisLayers = [ + { + originPlugin: 'Anomaly Detection', + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: 'Anomaly Detectors', + id: 'detector-1-id', + name: 'detector-1', + urlPath: 'anomaly-detection-dashboards#/detectors/anomaly-detector-1-id/configurations', + }, + events: [ + { + timestamp: 1234, + metadata: { + pluginResourceId: 'detector-1-id', + }, + }, + { + timestamp: 5678, + metadata: { + pluginResourceId: 'detector-1-id', + }, + }, + ], + }, + { + originPlugin: 'Anomaly Detection', + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: 'Anomaly Detectors', + id: 'detector-2-id', + name: 'detector-2', + urlPath: 'anomaly-detection-dashboards#/detectors/anomaly-detector-2-id/configurations', + }, + events: [ + { + timestamp: 1234, + metadata: { + pluginResourceId: 'detector-2-id', + }, + }, + ], + }, + { + originPlugin: 'Alerting', + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: 'Alerting Monitors', + id: 'monitor-1-id', + name: 'monitor-1', + urlPath: 'alerting#/monitors/monitor-1-id/details', + }, + events: [ + { + timestamp: 1234, + metadata: { + pluginResourceId: 'monitor-1-id', + }, + }, + ], + }, + { + originPlugin: 'Alerting', + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: 'Alerting Monitors', + id: 'monitor-2-id', + name: 'monitor-2', + urlPath: 'alerting#/monitors/monitor-2-id/details', + }, + events: [ + { + timestamp: 1234, + metadata: { + pluginResourceId: 'monitor-2-id', + }, + }, + ], + }, + { + originPlugin: 'Alerting', + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: 'Alerting Monitors', + id: 'monitor-3-id', + name: 'monitor-3', + urlPath: 'alerting#/monitors/monitor-3-id/details', + }, + events: [ + { + timestamp: 1234, + metadata: { + pluginResourceId: 'monitor-3-id', + }, + }, + ], + }, + { + originPlugin: 'Alerting', + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: 'Alerting Monitors', + id: 'monitor-4-id', + name: 'monitor-4', + urlPath: 'alerting#/monitors/monitor-4-id/details', + }, + events: [ + { + timestamp: 1234, + metadata: { + pluginResourceId: 'monitor-4-id', + }, + }, + ], + }, + ] as Array; + + this.visLayers = + this.input.visLayerResourceIds !== undefined + ? dummyVisLayers.filter((dummyVisLayer) => + this.input.visLayerResourceIds?.includes(dummyVisLayer.pluginResource.id) + ) + : dummyVisLayers; + this.expression = await buildPipeline(this.vis, { timefilter: this.timefilter, timeRange: this.timeRange, diff --git a/src/plugins/visualizations/public/expressions/vis.ts b/src/plugins/visualizations/public/expressions/vis.ts index acf747973dee..7a994a51e7d6 100644 --- a/src/plugins/visualizations/public/expressions/vis.ts +++ b/src/plugins/visualizations/public/expressions/vis.ts @@ -41,7 +41,6 @@ import { EventEmitter } from 'events'; import _ from 'lodash'; import { VisParams, PersistedState } from '../../../../plugins/visualizations/public'; - import { getTypes } from '../services'; import { VisType } from '../vis_types'; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 46d3b3dd7d03..2efc40bfdb84 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -41,7 +41,12 @@ export function plugin(initializerContext: PluginInitializerContext) { /** @public static code */ export { Vis } from './vis'; export { TypesService } from './vis_types/types_service'; -export { VISUALIZE_EMBEDDABLE_TYPE, VIS_EVENT_TO_TRIGGER } from './embeddable'; +export { + VISUALIZE_EMBEDDABLE_TYPE, + VIS_EVENT_TO_TRIGGER, + VisualizeEmbeddable, + DisabledLabEmbeddable, +} from './embeddable'; export { VisualizationContainer, VisualizationNoResults } from './components'; export { getSchemas as getVisSchemas, buildVislibDimensions } from './legacy/build_pipeline'; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 1cbb3bc38879..7d6a75cef9e6 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -265,8 +265,11 @@ const adjustVislibDimensionFormmaters = (vis: Vis, dimensions: { y: any[] }): vo }; export const buildPipelineVisFunction: BuildPipelineVisFunction = { - vega: (params) => { - return `vega ${prepareString('spec', params.spec)}`; + vega: (params, schemas, uiState, meta) => { + return `vega ${prepareString('spec', params.spec)} ${prepareString( + 'savedObjectId', + meta?.savedObjectId + )}`; }, input_control_vis: (params) => { return `input_control_vis ${prepareJson('visConfig', params)}`; @@ -405,11 +408,16 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { const schemas = getSchemas(vis, params); + const meta = { + savedObjectId: get(vis, 'id', ''), + }; + if (buildPipelineVisFunction[vis.type.name]) { pipeline += buildPipelineVisFunction[vis.type.name]( { title, ...vis.params }, schemas, - uiState + uiState, + meta ); } else if (vislibCharts.includes(vis.type.name)) { const visConfig = { ...vis.params }; diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 682e678ed584..1e6d01da1774 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -210,6 +210,7 @@ export class VisualizationsPlugin overlays: core.overlays, }); setSavedSearchLoader(savedSearchLoader); + return { ...types, showNewVisModal,