From 184eeecf44b3b527479ae2516cb1b2b7dd72ac70 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Fri, 6 Jan 2023 16:57:26 -0800 Subject: [PATCH] Add view events page Signed-off-by: Tyler Ohlsen --- .../public/lib/panel/embeddable_panel.tsx | 4 + src/plugins/embeddable/public/plugin.tsx | 13 +- src/plugins/ui_actions/public/index.ts | 1 + .../ui_actions/public/triggers/index.ts | 1 + .../triggers/open_events_flyout_trigger.ts | 6 + .../vis_augmenter/opensearch_dashboards.json | 11 +- src/plugins/vis_augmenter/public/constants.ts | 4 +- src/plugins/vis_augmenter/public/index.ts | 1 + src/plugins/vis_augmenter/public/mocks.ts | 116 ++++++++ src/plugins/vis_augmenter/public/plugin.ts | 32 +- src/plugins/vis_augmenter/public/services.ts | 21 +- src/plugins/vis_augmenter/public/types.ts | 13 + .../public/ui_actions_bootstrap.ts | 43 +++ .../vis_augmenter/public/vega/helpers.ts | 107 ++++++- .../view_events_flyout/actions/index.ts | 7 + .../actions/open_events_flyout.tsx | 32 ++ .../actions/open_events_flyout_action.test.ts | 62 ++++ .../actions/open_events_flyout_action.ts | 53 ++++ .../actions/view_events_option_action.test.ts | 56 ++++ .../actions/view_events_option_action.tsx | 54 ++++ .../error_flyout_body.test.tsx.snap | 41 +++ .../loading_flyout_body.test.tsx.snap | 31 ++ .../components/base_vis_item.test.tsx | 33 +++ .../components/base_vis_item.tsx | 32 ++ .../components/date_range_item.test.tsx | 54 ++++ .../components/date_range_item.tsx | 65 +++++ .../components/error_flyout_body.test.tsx | 21 ++ .../components/error_flyout_body.tsx | 25 ++ .../components/event_vis_item.test.tsx | 63 ++++ .../components/event_vis_item.tsx | 62 ++++ .../components/events_panel.tsx | 32 ++ .../view_events_flyout/components/index.ts | 10 + .../components/loading_flyout_body.test.tsx | 16 + .../components/loading_flyout_body.tsx | 19 ++ .../components/plugin_events_panel.tsx | 31 ++ .../view_events_flyout/components/styles.scss | 54 ++++ .../components/timeline_panel.test.tsx | 33 +++ .../components/timeline_panel.tsx | 35 +++ .../components/utils.test.ts | 23 ++ .../view_events_flyout/components/utils.tsx | 12 + .../components/view_events_flyout.tsx | 275 ++++++++++++++++++ .../public/view_events_flyout/index.ts | 8 + .../view_events_flyout/triggers/index.ts | 6 + .../triggers/open_events_flyout_trigger.ts | 21 ++ .../vis_type_vega/opensearch_dashboards.json | 9 +- .../vis_type_vega/public/data_model/types.ts | 3 +- .../public/data_model/vega_parser.ts | 21 +- .../public/expressions/helpers.ts | 27 +- .../vis_type_vega/public/expressions/index.ts | 1 + .../public/expressions/line_vega_spec_fn.ts | 20 +- .../public/expressions/vega_fn.ts | 11 +- src/plugins/vis_type_vega/public/plugin.ts | 6 +- src/plugins/vis_type_vega/public/services.ts | 3 + .../public/vega_view/vega_base_view.js | 20 +- .../public/vega_view/vega_tooltip.js | 14 +- .../public/vega_view/vega_view.js | 31 +- .../public/vega_visualization.js | 4 + .../public/line_to_expression.ts | 10 + .../public/embeddable/visualize_embeddable.ts | 148 +++++++++- src/plugins/visualizations/public/index.ts | 7 +- .../public/legacy/build_pipeline.ts | 3 +- src/plugins/visualizations/public/plugin.ts | 1 + 62 files changed, 1932 insertions(+), 46 deletions(-) create mode 100644 src/plugins/ui_actions/public/triggers/open_events_flyout_trigger.ts create mode 100644 src/plugins/vis_augmenter/public/mocks.ts create mode 100644 src/plugins/vis_augmenter/public/ui_actions_bootstrap.ts create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/actions/index.ts create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.test.ts create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.ts create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.test.ts create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/__snapshots__/error_flyout_body.test.tsx.snap create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/__snapshots__/loading_flyout_body.test.tsx.snap create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/base_vis_item.test.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/base_vis_item.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/date_range_item.test.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/date_range_item.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/error_flyout_body.test.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/error_flyout_body.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/event_vis_item.test.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/event_vis_item.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/events_panel.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/index.ts create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/loading_flyout_body.test.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/loading_flyout_body.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/plugin_events_panel.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/styles.scss create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/timeline_panel.test.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/timeline_panel.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/utils.test.ts create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/utils.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/components/view_events_flyout.tsx create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/index.ts create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/triggers/index.ts create mode 100644 src/plugins/vis_augmenter/public/view_events_flyout/triggers/open_events_flyout_trigger.ts 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; }) => ( { + return { + type, + id, + name, + urlPath, + }; +}; + +export const createMockErrorEmbeddable = (): ErrorEmbeddable => { + return new ErrorEmbeddable('Oh no something has gone wrong', { id: ' 404' }); +}; + +export const createMockVisEmbeddable = ( + savedObjectId: string = SAVED_OBJ_ID, + title: string = VIS_TITLE +): VisualizeEmbeddable => { + const mockTimeFilterService = timefilterServiceMock.createStartContract(); + const mockTimeFilter = mockTimeFilterService.timefilter; + const mockVis = ({ + type: {}, + data: {}, + uiState: { + on: jest.fn(), + }, + } as unknown) as Vis; + const mockDeps = { + start: jest.fn(), + }; + const mockConfiguration = { + vis: mockVis, + editPath: 'test-edit-path', + editUrl: 'test-edit-url', + editable: true, + deps: mockDeps, + }; + const mockVisualizeInput = { id: 'test-id', savedObjectId }; + + const mockVisEmbeddable = new VisualizeEmbeddable( + mockTimeFilter, + mockConfiguration, + mockVisualizeInput + ); + mockVisEmbeddable.getTitle = () => title; + return mockVisEmbeddable; +}; + +export const createPointInTimeEventsVisLayer = ( + originPlugin: string = ORIGIN_PLUGIN, + pluginResource: PluginResource = PLUGIN_RESOURCE, + eventCount: number = EVENT_COUNT +): PointInTimeEventsVisLayer => { + const events = [] as PointInTimeEvent[]; + for (let i = 0; i < eventCount; i++) { + events.push({ + timestamp: i, + metadata: { + pluginResourceId: pluginResource.id, + }, + } as PointInTimeEvent); + } + return { + originPlugin, + type: VisLayerTypes.PointInTimeEvents, + pluginResource, + events, + }; +}; + +export const createMockEventVisEmbeddableItem = ( + savedObjectId: string = SAVED_OBJ_ID, + title: string = VIS_TITLE, + originPlugin: string = ORIGIN_PLUGIN, + pluginResource: PluginResource = PLUGIN_RESOURCE, + eventCount: number = EVENT_COUNT +): EventVisEmbeddableItem => { + const visLayer = createPointInTimeEventsVisLayer(originPlugin, pluginResource, eventCount); + const embeddable = createMockVisEmbeddable(savedObjectId, title); + return { + visLayer, + embeddable, + }; +}; diff --git a/src/plugins/vis_augmenter/public/plugin.ts b/src/plugins/vis_augmenter/public/plugin.ts index 1c064a1cee10..5e73c2b58744 100644 --- a/src/plugins/vis_augmenter/public/plugin.ts +++ b/src/plugins/vis_augmenter/public/plugin.ts @@ -5,10 +5,21 @@ import { ExpressionsSetup } from '../../expressions/public'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; import { visLayers } from './expressions'; import { setSavedAugmentVisLoader } from './services'; import { createSavedAugmentVisLoader, SavedAugmentVisLoader } from './saved_augment_vis'; +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 {} @@ -18,12 +29,14 @@ export interface VisAugmenterStart { } export interface VisAugmenterSetupDeps { - data: DataPublicPluginSetup; expressions: ExpressionsSetup; } export interface VisAugmenterStartDeps { + uiActions: UiActionsStart; + embeddable: EmbeddableStart; data: DataPublicPluginStart; + visualizations: VisualizationsStart; } export class VisAugmenterPlugin @@ -33,13 +46,24 @@ 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); + + registerTriggersAndActions(core); + const savedAugmentVisLoader = createSavedAugmentVisLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, diff --git a/src/plugins/vis_augmenter/public/services.ts b/src/plugins/vis_augmenter/public/services.ts index 00fa45374980..bed1f8182e05 100644 --- a/src/plugins/vis_augmenter/public/services.ts +++ b/src/plugins/vis_augmenter/public/services.ts @@ -3,9 +3,28 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { createGetterSetter } from '../../opensearch_dashboards_utils/common'; import { SavedObjectLoader } from '../../saved_objects/public'; +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 [getSavedAugmentVisLoader, setSavedAugmentVisLoader] = createGetterSetter< SavedObjectLoader >('savedAugmentVisLoader'); + +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/types.ts b/src/plugins/vis_augmenter/public/types.ts index a45ce4f20df3..3ec169a6882f 100644 --- a/src/plugins/vis_augmenter/public/types.ts +++ b/src/plugins/vis_augmenter/public/types.ts @@ -56,3 +56,16 @@ export const isPointInTimeEventsVisLayer = (obj: any) => { export const isValidVisLayer = (obj: any) => { return obj?.type in VisLayerTypes; }; + +// TODO: clean these up to more clearly show what is happening or why these are needed. +// mostly all pertain to the view events flyout. If they apply multiple places, can leave as +// generic flags +export interface VisAugmenterEmbeddableConfig { + visLayerResourceIds?: string[]; + showVisData?: boolean; + showEventData?: boolean; + showXAxis?: boolean; + fromDashboard?: boolean; + legendPosition?: string; + eventMarksFilled?: boolean; +} 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..bfdb64735195 --- /dev/null +++ b/src/plugins/vis_augmenter/public/ui_actions_bootstrap.ts @@ -0,0 +1,43 @@ +/* + * 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 './view_events_flyout'; +import { AugmentVisContext, openEventsFlyoutTrigger } from './view_events_flyout/triggers'; +import { OPEN_EVENTS_FLYOUT_TRIGGER } from '../../ui_actions/public'; +import { CONTEXT_MENU_TRIGGER, EmbeddableContext } from '../../embeddable/public'; +import { getUiActions } from './services'; + +// Overriding the mappings defined in UIActions plugin so that +// the new trigger and action definitions resolve. +// This is a common pattern among internal Dashboards plugins. +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_augmenter/public/vega/helpers.ts b/src/plugins/vis_augmenter/public/vega/helpers.ts index 2132e170bb4d..a07dc0ffbf69 100644 --- a/src/plugins/vis_augmenter/public/vega/helpers.ts +++ b/src/plugins/vis_augmenter/public/vega/helpers.ts @@ -5,6 +5,7 @@ import moment from 'moment'; import { cloneDeep, isEmpty, get } from 'lodash'; +import { YAxisConfig } from 'src/plugins/vis_type_vega/public'; import { OpenSearchDashboardsDatatable, OpenSearchDashboardsDatatableColumn, @@ -23,6 +24,7 @@ import { VisLayer, VisLayers, VisLayerTypes, + VisAugmenterEmbeddableConfig, } from '../'; // Given any visLayers, create a map to indicate which VisLayer types are present. @@ -310,9 +312,10 @@ export const addPointInTimeEventsLayersToSpec = ( mark: { type: 'point', shape: EVENT_MARK_SHAPE, - color: EVENT_COLOR, - filled: true, - opacity: 1, + fill: EVENT_COLOR, + stroke: EVENT_COLOR, + strokeOpacity: 1, + fillOpacity: 1, }, transform: [{ filter: generateVisLayerFilterString(visLayerColumnIds) }], params: [{ name: HOVER_PARAM, select: { type: 'point', on: 'mouseover' } }], @@ -340,3 +343,101 @@ export const addPointInTimeEventsLayersToSpec = ( return newSpec; }; + +// This is the total y-axis padding such that if this is added to the "padding" value of the view, if there is no axis, +// it will align values on the x-axis +export const calculateYAxisPadding = (config: YAxisConfig): number => { + return ( + get(config, 'minExtent', 0) + + get(config, 'offset', 0) + + get(config, 'translate', 0) + + get(config, 'domainWidth', 0) + + get(config, 'labelPadding', 0) + + get(config, 'titlePadding', 0) + + get(config, 'tickOffset', 0) + + get(config, 'tickSize', 0) + + // TODO: figure out where this value is coming from + 3 + ); +}; + +// Parse the vis augmenter config to apply different visual changes to the event chart spec. +// This includes potentially removing the original vis data, hiding axes, moving the legend, etc. +// Primarily used within the view events flyout to render the charts in different ways, and to +// ensure the stacked event charts are aligned with the base vis chart. +export const augmentEventChartSpec = ( + config: VisAugmenterEmbeddableConfig, + origSpec: object +): {} => { + const showVisData = get(config, 'showVisData', true) as boolean; + const showEventData = get(config, 'showEventData', true) as boolean; + const legendPosition = get(config, 'legendPosition', undefined) as string | undefined; + const showXAxis = get(config, 'showXAxis', true) as boolean; + const eventMarksFilled = get(config, 'eventMarksFilled', undefined) as boolean | undefined; + const fromDashboard = get(config, 'fromDashboard', true) as boolean; + const newVconcat = [] as Array<{}>; + // @ts-ignore + const newConfig = origSpec.config; + const visChart = get(origSpec, 'vconcat[0]', {}); + const eventChart = get(origSpec, 'vconcat[1]', {}); + + if (legendPosition !== undefined) { + newConfig.legend = { + ...newConfig.legend, + orient: legendPosition, + // 18 is the default offset as of vega lite 5 + offset: legendPosition === 'top' || legendPosition === 'bottom' ? 0 : 18, + }; + } + + if (!showXAxis) { + eventChart.encoding.x.axis = { + domain: true, + grid: false, + ticks: false, + labels: false, + title: null, + }; + } + if (eventMarksFilled !== undefined) { + eventChart.mark.fillOpacity = eventMarksFilled ? 1 : 0; + } + + // This will filter out all event data so it shows as empty + if (!showEventData) { + eventChart.transform = [ + { + filter: 'false', + }, + ]; + } + + // if coming from view events page, need to standardize the y axis padding values so we can + // align all of the charts correctly + if (!fromDashboard) { + newConfig.axisY = { + // We need minExtent and maxExtent to be the same. We cannot calculate these on-the-fly + // so we need to force a static value. + minExtent: 40, + maxExtent: 40, + offset: 0, + translate: 0, + domainWidth: 1, + labelPadding: 2, + titlePadding: 2, + tickOffset: 0, + tickSize: 5, + } as YAxisConfig; + } + + if (showVisData) { + newVconcat.push(visChart); + } + newVconcat.push(eventChart); + + return { + ...cloneDeep(origSpec), + config: newConfig, + vconcat: newVconcat, + }; +}; diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/index.ts b/src/plugins/vis_augmenter/public/view_events_flyout/actions/index.ts new file mode 100644 index 000000000000..cd333ed9451d --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { OPEN_EVENTS_FLYOUT_ACTION, OpenEventsFlyoutAction } from './open_events_flyout_action'; +export { VIEW_EVENTS_OPTION_ACTION, ViewEventsOptionAction } from './view_events_option_action'; diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout.tsx new file mode 100644 index 000000000000..35caa41ef837 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React from 'react'; +import { CoreStart } from 'src/core/public'; +import { toMountPoint } from '../../../../opensearch_dashboards_react/public'; +import { ViewEventsFlyout } from '../components'; + +interface Props { + core: CoreStart; + savedObjectId: string; +} + +export async function openViewEventsFlyout(props: Props) { + const flyoutSession = props.core.overlays.openFlyout( + toMountPoint( + { + if (flyoutSession) { + flyoutSession.close(); + } + }} + savedObjectId={props.savedObjectId} + /> + ), + { + 'data-test-subj': 'viewEventsFlyout', + ownFocus: true, + } + ); +} diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.test.ts b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.test.ts new file mode 100644 index 000000000000..fe52c6ff2d36 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { coreMock } from '../../../../../core/public/mocks'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { OpenEventsFlyoutAction } from './open_events_flyout_action'; + +let coreStart: CoreStart; +beforeEach(async () => { + coreStart = coreMock.createStart(); +}); + +describe('OpenEventsFlyoutAction', () => { + it('is incompatible with null saved obj id', async () => { + const action = new OpenEventsFlyoutAction(coreStart); + const savedObjectId = null; + // @ts-ignore + expect(await action.isCompatible({ savedObjectId })).toBe(false); + }); + + it('is incompatible with undefined saved obj id', async () => { + const action = new OpenEventsFlyoutAction(coreStart); + const savedObjectId = undefined; + // @ts-ignore + expect(await action.isCompatible({ savedObjectId })).toBe(false); + }); + + it('is incompatible with empty saved obj id', async () => { + const action = new OpenEventsFlyoutAction(coreStart); + const savedObjectId = ''; + expect(await action.isCompatible({ savedObjectId })).toBe(false); + }); + + it('execute throws error if incompatible saved obj id', async () => { + const action = new OpenEventsFlyoutAction(coreStart); + async function check(id: any) { + await action.execute({ savedObjectId: id }); + } + await expect(check(null)).rejects.toThrow(Error); + await expect(check(undefined)).rejects.toThrow(Error); + await expect(check('')).rejects.toThrow(Error); + }); + + it('execute calls openFlyout if compatible saved obj id', async () => { + const savedObjectId = 'test-id'; + const action = new OpenEventsFlyoutAction(coreStart); + await action.execute({ savedObjectId }); + expect(coreStart.overlays.openFlyout).toHaveBeenCalledTimes(1); + }); + + it('Returns display name', async () => { + const action = new OpenEventsFlyoutAction(coreStart); + expect(action.getDisplayName()).toBeDefined(); + }); + + it('Returns undefined icon type', async () => { + const action = new OpenEventsFlyoutAction(coreStart); + expect(action.getIconType()).toBeUndefined(); + }); +}); diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.ts b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.ts new file mode 100644 index 000000000000..fdf8596f1224 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.ts @@ -0,0 +1,53 @@ +/* + * 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('dashboard.actions.viewEvents.displayName', { + defaultMessage: 'View Events', + }); + } + + public async isCompatible({ savedObjectId }: AugmentVisContext) { + // checks for null / undefined / empty string + return savedObjectId ? true : false; + } + + 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/view_events_flyout/actions/view_events_option_action.test.ts b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.test.ts new file mode 100644 index 000000000000..99102b15091a --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { coreMock } from '../../../../../core/public/mocks'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { ViewEventsOptionAction } from './view_events_option_action'; +import { createMockErrorEmbeddable, createMockVisEmbeddable } from '../../mocks'; + +let coreStart: CoreStart; +beforeEach(async () => { + coreStart = coreMock.createStart(); +}); + +describe('ViewEventsOptionAction', () => { + // TODO: following commented out tests can be enabled when compatibility function is finalized + + // it('is incompatible with ErrorEmbeddables', async () => { + // const action = new ViewEventsOptionAction(coreStart); + // const errorEmbeddable = createMockErrorEmbeddable(); + // expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); + // }); + + // it('is compatible with VisualizeEmbeddables', async () => { + // const visEmbeddable = createMockVisEmbeddable('test-saved-obj-id', 'test-title'); + // const action = new ViewEventsOptionAction(coreStart); + // expect(await action.isCompatible({ embeddable: visEmbeddable })).toBe(true); + // }); + + // it('execute throws error if incompatible embeddable', async () => { + // const errorEmbeddable = createMockErrorEmbeddable(); + // const action = new ViewEventsOptionAction(coreStart); + // async function check() { + // await action.execute({ embeddable: errorEmbeddable }); + // } + // await expect(check()).rejects.toThrow(Error); + // }); + + it('execute calls openFlyout if compatible embeddable', async () => { + const visEmbeddable = createMockVisEmbeddable('test-saved-obj-id', 'test-title'); + const action = new ViewEventsOptionAction(coreStart); + await action.execute({ embeddable: visEmbeddable }); + expect(coreStart.overlays.openFlyout).toHaveBeenCalledTimes(1); + }); + + it('Returns display name', async () => { + const action = new ViewEventsOptionAction(coreStart); + expect(action.getDisplayName()).toBeDefined(); + }); + + it('Returns an icon type', async () => { + const action = new ViewEventsOptionAction(coreStart); + expect(action.getIconType()).toBeDefined(); + }); +}); diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx new file mode 100644 index 000000000000..3ce0d4bbcb87 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; +import { get } from 'lodash'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { VisualizeEmbeddable } from '../../../../visualizations/public'; +import { EmbeddableContext } from '../../../../embeddable/public'; +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', + }); + } + + // TODO: add the logic for compatibility here, probably from some helper fn. + // see https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3268 + public async isCompatible({ embeddable }: EmbeddableContext) { + return true; + // return embeddable instanceof VisualizeEmbeddable; + } + + 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/view_events_flyout/components/__snapshots__/error_flyout_body.test.tsx.snap b/src/plugins/vis_augmenter/public/view_events_flyout/components/__snapshots__/error_flyout_body.test.tsx.snap new file mode 100644 index 000000000000..7094a4660dbd --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/__snapshots__/error_flyout_body.test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders component 1`] = ` +
+
+
+
+
+
+
+
+
+ oh no an error! +
+
+
+
+
+
+
+
+
+`; diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/__snapshots__/loading_flyout_body.test.tsx.snap b/src/plugins/vis_augmenter/public/view_events_flyout/components/__snapshots__/loading_flyout_body.test.tsx.snap new file mode 100644 index 000000000000..6b642518fc6e --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/__snapshots__/loading_flyout_body.test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders component 1`] = ` +
+
+
+
+
+
+ +
+
+
+
+
+
+`; diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/base_vis_item.test.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/base_vis_item.test.tsx new file mode 100644 index 000000000000..ca88941f6f23 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/base_vis_item.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { BaseVisItem } from './base_vis_item'; +import { createMockVisEmbeddable } from '../../mocks'; + +jest.mock('../../services', () => { + return { + getEmbeddable: () => { + return { + getEmbeddablePanel: () => { + return 'MockEmbeddablePanel'; + }, + }; + }, + }; +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('', () => { + it('renders', async () => { + const embeddable = createMockVisEmbeddable(); + const { getByTestId } = render(); + expect(getByTestId('baseVis')).toBeInTheDocument(); + }); +}); diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/base_vis_item.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/base_vis_item.tsx new file mode 100644 index 000000000000..3840c5a1f23b --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/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/view_events_flyout/components/date_range_item.test.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/date_range_item.test.tsx new file mode 100644 index 000000000000..bd07e115d158 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/date_range_item.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { findTestSubject } from 'test_utils/helpers'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DateRangeItem } from './date_range_item'; +import { TimeRange } from '../../../../data/common'; +import { prettyDuration } from '@elastic/eui'; +import { DATE_RANGE_FORMAT } from './view_events_flyout'; + +describe('', () => { + const mockTimeRange = { + from: 'now-7d', + to: 'now', + } as TimeRange; + const mockReloadFn = jest.fn(); + + it('time range is displayed correctly', async () => { + const prettyTimeRange = prettyDuration( + mockTimeRange.from, + mockTimeRange.to, + [], + DATE_RANGE_FORMAT + ); + + const { getByText } = render(); + expect(getByText(prettyTimeRange)).toBeInTheDocument(); + }); + + it('triggers reload on clicking on refresh button', async () => { + const component = mountWithIntl( + + ); + const refreshButton = findTestSubject(component, 'refreshButton'); + refreshButton.simulate('click'); + expect(mockReloadFn).toHaveBeenCalledTimes(1); + }); + + // Note we are not creating/comparing snapshots for this component. That is because + // it will hardcode a time-specific value which can cause failures when running + // in different envs + it('renders component', async () => { + const { getByTestId } = render( + + ); + expect(getByTestId('durationText')).toBeInTheDocument(); + expect(getByTestId('refreshButton')).toBeInTheDocument(); + expect(getByTestId('refreshDescriptionText')).toBeInTheDocument(); + }); +}); diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/date_range_item.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/date_range_item.tsx new file mode 100644 index 000000000000..e2a7092f1e5f --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/date_range_item.tsx @@ -0,0 +1,65 @@ +/* + * 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)); + }} + data-test-subj="refreshButton" + > + Refresh + + + + + {`This view is not updated to load the latest events automatically. + Last updated: ${lastUpdatedTime}`} + + + + ); +} diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/error_flyout_body.test.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/error_flyout_body.test.tsx new file mode 100644 index 000000000000..d3bb447ae934 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/error_flyout_body.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { ErrorFlyoutBody } from './error_flyout_body'; + +describe('', () => { + const errorMsg = 'oh no an error!'; + it('shows error message', async () => { + const { getByText } = render(); + expect(getByText(errorMsg)).toBeInTheDocument(); + }); + it('renders component', async () => { + const { container, getByTestId } = render(); + expect(getByTestId('errorCallOut')).toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/error_flyout_body.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/error_flyout_body.tsx new file mode 100644 index 000000000000..1e0349aa18c2 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/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/view_events_flyout/components/event_vis_item.test.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/event_vis_item.test.tsx new file mode 100644 index 000000000000..99a865a9218c --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/event_vis_item.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { EventVisItem } from './event_vis_item'; +import { + createMockEventVisEmbeddableItem, + createMockVisEmbeddable, + createPluginResource, + createPointInTimeEventsVisLayer, +} from '../../mocks'; + +jest.mock('../../services', () => { + return { + getEmbeddable: () => { + return { + getEmbeddablePanel: () => { + return 'MockEmbeddablePanel'; + }, + }; + }, + getCore: () => { + return { + http: { + basePath: { + prepend: jest.fn(), + }, + }, + }; + }, + }; +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('', () => { + it('renders', async () => { + const item = createMockEventVisEmbeddableItem(); + const { getByTestId, getByText } = render(); + expect(getByTestId('eventVis')).toBeInTheDocument(); + expect(getByTestId('pluginResourceDescription')).toBeInTheDocument(); + expect(getByText(item.visLayer.pluginResource.name)).toBeInTheDocument(); + }); + + it('shows event count when rendering a PointInTimeEventsVisLayer', async () => { + const eventCount = 5; + const pluginResource = createPluginResource(); + const visLayer = createPointInTimeEventsVisLayer('test-plugin', pluginResource, eventCount); + const embeddable = createMockVisEmbeddable(); + const item = { + visLayer, + embeddable, + }; + const { getByTestId, getByText } = render(); + expect(getByTestId('eventCount')).toBeInTheDocument(); + expect(getByText(eventCount)).toBeInTheDocument(); + }); +}); diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/event_vis_item.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/event_vis_item.tsx new file mode 100644 index 000000000000..807ff7366c82 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/event_vis_item.tsx @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { get } from 'lodash'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLink, EuiNotificationBadge } from '@elastic/eui'; +import { getEmbeddable, getCore } from '../../services'; +import './styles.scss'; +import { EventVisEmbeddableItem } from '.'; +import { VisLayerTypes } 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; + + // For now we only support PointInTimeEventsVisLayers. Ensure that check here, + // and if so, set the event count to the length of the events + const showEventCount = props.item.visLayer.type === VisLayerTypes.PointInTimeEvents; + let eventCount; + if (showEventCount) { + eventCount = get(props.item.visLayer, 'events.length', 0); + } + + return ( + <> + + + + + {name} + + {showEventCount ? ( + + {eventCount} + + ) : null} + + + + + + + + + ); +} diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/events_panel.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/events_panel.tsx new file mode 100644 index 000000000000..33f3ea8bb205 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/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/view_events_flyout/components/index.ts b/src/plugins/vis_augmenter/public/view_events_flyout/components/index.ts new file mode 100644 index 000000000000..ad96fd25af55 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/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/view_events_flyout/components/loading_flyout_body.test.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/loading_flyout_body.test.tsx new file mode 100644 index 000000000000..0a06516831d5 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/loading_flyout_body.test.tsx @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { LoadingFlyoutBody } from './loading_flyout_body'; + +describe('', () => { + it('renders component', async () => { + const { container, getByTestId } = render(); + expect(getByTestId('loadingSpinner')).toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/loading_flyout_body.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/loading_flyout_body.tsx new file mode 100644 index 000000000000..90a6d5213029 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/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/view_events_flyout/components/plugin_events_panel.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/plugin_events_panel.tsx new file mode 100644 index 000000000000..8ec2e3025a37 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/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, EuiFlexGroup } from '@elastic/eui'; +import './styles.scss'; +import { EventVisItem } from './event_vis_item'; +import { EventVisEmbeddableItem } from '.'; + +interface Props { + pluginTitle: string; + items: EventVisEmbeddableItem[]; +} + +export function PluginEventsPanel(props: Props) { + return ( + <> + + + {props.pluginTitle} + + + + {props.items.map((item, index) => ( + + ))} + + ); +} diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/styles.scss b/src/plugins/vis_augmenter/public/view_events_flyout/components/styles.scss new file mode 100644 index 000000000000..a683d2fc50d9 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/styles.scss @@ -0,0 +1,54 @@ +$vis-description-width: 150px; +$event-vis-height: 55px; +$timeline-panel-height: 100px; + +.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; + } + + &__timelinePanel { + height: $timeline-panel-height; + } + + &__visDescription { + min-width: $vis-description-width; + max-width: $vis-description-width; + } + + &__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: $timeline-panel-height; +} diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/timeline_panel.test.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/timeline_panel.test.tsx new file mode 100644 index 000000000000..a22ac5aa209c --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/timeline_panel.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TimelinePanel } from './timeline_panel'; +import { createMockVisEmbeddable } from '../../mocks'; + +jest.mock('../../services', () => { + return { + getEmbeddable: () => { + return { + getEmbeddablePanel: () => { + return 'MockEmbeddablePanel'; + }, + }; + }, + }; +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('', () => { + it('renders', async () => { + const embeddable = createMockVisEmbeddable(); + const { getByTestId } = render(); + expect(getByTestId('timelineVis')).toBeInTheDocument(); + }); +}); diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/timeline_panel.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/timeline_panel.tsx new file mode 100644 index 000000000000..6507eac8cc23 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/timeline_panel.tsx @@ -0,0 +1,35 @@ +/* + * 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 ( + + + + + + + ); +} diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/utils.test.ts b/src/plugins/vis_augmenter/public/view_events_flyout/components/utils.test.ts new file mode 100644 index 000000000000..39ff9d53dd44 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/utils.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createMockErrorEmbeddable } from '../../mocks'; +import { getErrorMessage } from './utils'; + +describe('utils', () => { + describe('getErrorMessage', () => { + const errorMsg = 'oh no an error!'; + it('returns message when error field is string', async () => { + const errorEmbeddable = createMockErrorEmbeddable(); + errorEmbeddable.error = errorMsg; + expect(getErrorMessage(errorEmbeddable)).toEqual(errorMsg); + }); + it('returns message when error field is Error obj', async () => { + const errorEmbeddable = createMockErrorEmbeddable(); + errorEmbeddable.error = new Error(errorMsg); + expect(getErrorMessage(errorEmbeddable)).toEqual(errorMsg); + }); + }); +}); diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/utils.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/utils.tsx new file mode 100644 index 000000000000..f1a59d4cc23f --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/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/view_events_flyout/components/view_events_flyout.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/components/view_events_flyout.tsx new file mode 100644 index 000000000000..877d8089bc94 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/view_events_flyout.tsx @@ -0,0 +1,275 @@ +/* + * 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 '../../types'; +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 interface 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(), + }; + setTimeRange(contextInput.timeRange); + + const embeddable = (await embeddableVisFactory?.createFromSavedObject(props.savedObjectId, { + ...contextInput, + visAugmenterConfig: { + fromDashboard: false, + legendPosition: 'top', + }, + } as VisualizeInput)) 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(embeddable: VisualizeEmbeddable) { + try { + const 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: embeddable.getInput().filters, + query: embeddable.getInput().query, + timeRange: embeddable.getInput().timeRange, + }; + + await Promise.all( + visLayers.map(async (visLayer) => { + const pluginResourceType = visLayer.pluginResource.type; + const eventEmbeddable = (await embeddableVisFactory?.createFromSavedObject( + props.savedObjectId, + { + ...contextInput, + visAugmenterConfig: { + visLayerResourceIds: [visLayer.pluginResource.id as string], + showVisData: false, + fromDashboard: false, + showXAxis: false, + eventMarksFilled: false, + }, + } 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(embeddable: VisualizeEmbeddable) { + try { + const contextInput = { + filters: embeddable.getInput().filters, + query: embeddable.getInput().query, + timeRange: embeddable.getInput().timeRange, + // TODO: add some field in the visualize embeddable to define + // showing any data at all + }; + + const timelineEmbeddable = (await embeddableVisFactory?.createFromSavedObject( + props.savedObjectId, + { + ...contextInput, + visAugmenterConfig: { + fromDashboard: false, + showVisData: false, + showEventData: false, + showXAxis: true, + }, + } as VisualizeInput + )) as VisualizeEmbeddable | ErrorEmbeddable; + + if (timelineEmbeddable instanceof ErrorEmbeddable) { + throw getErrorMessage(timelineEmbeddable); + } + + timelineEmbeddable.updateInput({ + // @ts-ignore + refreshConfig: { + value: 0, + pause: true, + }, + }); + setTimelineVisEmbeddable(timelineEmbeddable); + } catch (err: any) { + setErrorMessage(String(err)); + } + } + + useEffect(() => { + fetchVisEmbeddable(); + // TODO: look into why eslint errors here + /* eslint-disable */ + }, [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/view_events_flyout/index.ts b/src/plugins/vis_augmenter/public/view_events_flyout/index.ts new file mode 100644 index 000000000000..1e5e5b0c6b40 --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './actions'; +export * from './triggers'; +export * from './components'; diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/triggers/index.ts b/src/plugins/vis_augmenter/public/view_events_flyout/triggers/index.ts new file mode 100644 index 000000000000..f7a3dc2d9dce --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/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/view_events_flyout/triggers/open_events_flyout_trigger.ts b/src/plugins/vis_augmenter/public/view_events_flyout/triggers/open_events_flyout_trigger.ts new file mode 100644 index 000000000000..e8bb9b3d290b --- /dev/null +++ b/src/plugins/vis_augmenter/public/view_events_flyout/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_type_vega/opensearch_dashboards.json b/src/plugins/vis_type_vega/opensearch_dashboards.json index 17aee4a97232..faf10c831e6e 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"], + "requiredPlugins": [ + "data", + "visualizations", + "mapsLegacy", + "expressions", + "inspector", + "uiActions" + ], "optionalPlugins": ["home", "usageCollection"], "requiredBundles": [ "opensearchDashboardsUtils", diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index b5db91558c03..9ef34519996f 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -32,7 +32,7 @@ import { SearchResponse, SearchParams } from 'elasticsearch'; import { Filter } from 'src/plugins/data/public'; import { DslQuery } from 'src/plugins/data/common'; -import { VisLayerTypes } from 'src/plugins/vis_augmenter/public'; +import { VisAugmenterEmbeddableConfig, VisLayerTypes } from 'src/plugins/vis_augmenter/public'; import { OpenSearchQueryParser } from './opensearch_query_parser'; import { EmsFileParser } from './ems_file_parser'; import { UrlParser } from './url_parser'; @@ -115,6 +115,7 @@ export interface OpenSearchDashboards { type: string; renderer: Renderer; visibleVisLayers?: Map; + visAugmenterConfig?: VisAugmenterEmbeddableConfig; } export interface VegaSpec { diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 187c4690aad5..84912e199a9b 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -44,7 +44,7 @@ import { UrlParser } from './url_parser'; import { SearchAPI } from './search_api'; import { TimeCache } from './time_cache'; import { IServiceSettings } from '../../../maps_legacy/public'; -import { VisLayerTypes } from '../../../vis_augmenter/public'; +import { VisAugmenterEmbeddableConfig, VisLayerTypes } from '../../../vis_augmenter/public'; import { Bool, Data, @@ -94,6 +94,7 @@ export class VegaParser { filters: Bool; timeCache: TimeCache; visibleVisLayers: Map; + visAugmenterConfig: VisAugmenterEmbeddableConfig; constructor( spec: VegaSpec | string, @@ -105,6 +106,7 @@ export class VegaParser { this.spec = spec as VegaSpec; this.hideWarnings = false; this.visibleVisLayers = new Map(); + this.visAugmenterConfig = {} as VisAugmenterEmbeddableConfig; this.error = undefined; this.warnings = []; @@ -162,6 +164,9 @@ The URL is an identifier only. OpenSearch Dashboards and your browser will never this._config = this._parseConfig(); this.hideWarnings = !!this._config.hideWarnings; this.visibleVisLayers = this._config.visibleVisLayers; + // TODO: this config may end up not being needed, depending + // on what we may need to change to the underlying heights/widths + this.visAugmenterConfig = this._config.visAugmenterConfig; this.useMap = this._config.type === 'map'; this.renderer = this._config.renderer === 'svg' ? 'svg' : 'canvas'; this.tooltips = this._parseTooltips(); @@ -195,15 +200,11 @@ The URL is an identifier only. OpenSearch Dashboards and your browser will never }; // If we are showing PointInTimeEventsVisLayers, it means we are showing a base vis + event vis. - // Because this will be using a vconcat spec, we can autosize the width - // via fit-x. Note the regular 'fit' (to autosize width + height) does not work here. + // Because this will be using a vconcat spec, we cannot use the default autosize settings, or set + // top-level height/width values. // See limitations: https://vega.github.io/vega-lite/docs/size.html#limitations const showPointInTimeEvents = this.visibleVisLayers.get(VisLayerTypes.PointInTimeEvents) === true; - const showPointInTimeEventsAutosize = { - type: 'fit-x', - contains: 'padding', - }; let autosize = this.spec.autosize; let useResize = true; @@ -235,14 +236,12 @@ The URL is an identifier only. OpenSearch Dashboards and your browser will never contains: string; }; useResize = Boolean(autosize?.type && autosize?.type !== 'none'); + } else if (showPointInTimeEvents) { + autosize = undefined; } else { autosize = defaultAutosize; } - if (showPointInTimeEvents) { - autosize = showPointInTimeEventsAutosize; - } - if ( useResize && ((this.spec.width && this.spec.width !== 'container') || diff --git a/src/plugins/vis_type_vega/public/expressions/helpers.ts b/src/plugins/vis_type_vega/public/expressions/helpers.ts index ca31367bf119..8adffad4b45d 100644 --- a/src/plugins/vis_type_vega/public/expressions/helpers.ts +++ b/src/plugins/vis_type_vega/public/expressions/helpers.ts @@ -8,7 +8,7 @@ import { OpenSearchDashboardsDatatableColumn, } from '../../../expressions/public'; import { VislibDimensions, VisParams } from '../../../visualizations/public'; -import { isVisLayerColumn } from '../../../vis_augmenter/public'; +import { isVisLayerColumn, VisAugmenterEmbeddableConfig } from '../../../vis_augmenter/public'; // TODO: move this to the visualization plugin that has VisParams once all of these parameters have been better defined interface ValueAxis { @@ -33,6 +33,18 @@ interface ValueAxis { type: string; } +export interface YAxisConfig { + minExtent: number; + maxExtent: number; + offset: number; + translate: number; + domainWidth: number; + labelPadding: number; + titlePadding: number; + tickOffset: number; + tickSize: number; +} + // Get the first xaxis field as only 1 setup of X Axis will be supported and // there won't be support for split series and split chart const getXAxisId = (dimensions: any, columns: OpenSearchDashboardsDatatableColumn[]): string => { @@ -43,6 +55,10 @@ export const cleanString = (rawString: string): string => { return rawString.replaceAll('"', ''); }; +export const calculateLegendOffset = (legendPosition: string): number => + // 18 is the default offset as of vega lite 5 + legendPosition === 'top' || legendPosition === 'bottom' ? 0 : 18; + export const formatDatatable = ( datatable: OpenSearchDashboardsDatatable ): OpenSearchDashboardsDatatable => { @@ -53,7 +69,7 @@ export const formatDatatable = ( return datatable; }; -export const setupConfig = (visParams: VisParams) => { +export const setupConfig = (visParams: VisParams, config: VisAugmenterEmbeddableConfig) => { const legendPosition = visParams.legendPosition; return { view: { @@ -64,12 +80,14 @@ export const setupConfig = (visParams: VisParams) => { }, legend: { orient: legendPosition, + offset: calculateLegendOffset(legendPosition), }, // This is parsed in the VegaParser and hides unnecessary warnings. // For example, 'infinite extent' warnings that cover the chart // when there is empty data for a time series kibana: { hideWarnings: true, + visAugmenterConfig: config, }, }; }; @@ -131,7 +149,8 @@ const isXAxisColumn = (column: OpenSearchDashboardsDatatableColumn): boolean => export const createSpecFromDatatable = ( datatable: OpenSearchDashboardsDatatable, visParams: VisParams, - dimensions: VislibDimensions + dimensions: VislibDimensions, + config: VisAugmenterEmbeddableConfig ): object => { // TODO: we can try to use VegaSpec type but it is currently very outdated, where many // of the fields and sub-fields don't have other optional params that we want for customizing. @@ -142,7 +161,7 @@ export const createSpecFromDatatable = ( spec.data = { values: datatable.rows, }; - spec.config = setupConfig(visParams); + spec.config = setupConfig(visParams, config); // Get the valueAxes data and generate a map to easily fetch the different valueAxes data const valueAxis = new Map(); diff --git a/src/plugins/vis_type_vega/public/expressions/index.ts b/src/plugins/vis_type_vega/public/expressions/index.ts index dce44f56c47d..e85f175d55c6 100644 --- a/src/plugins/vis_type_vega/public/expressions/index.ts +++ b/src/plugins/vis_type_vega/public/expressions/index.ts @@ -5,3 +5,4 @@ export { LineVegaSpecExpressionFunctionDefinition } from './line_vega_spec_fn'; export { VegaExpressionFunctionDefinition } from './vega_fn'; +export { YAxisConfig } from './helpers'; diff --git a/src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.ts b/src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.ts index 499310c106d2..5c48a0898ea4 100644 --- a/src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.ts +++ b/src/plugins/vis_type_vega/public/expressions/line_vega_spec_fn.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { cloneDeep, isEmpty } from 'lodash'; +import { cloneDeep, isEmpty, get } from 'lodash'; import { i18n } from '@osd/i18n'; import { ExpressionFunctionDefinition, @@ -18,6 +18,7 @@ import { addPointInTimeEventsLayersToTable, addPointInTimeEventsLayersToSpec, enableVisLayersInSpecConfig, + augmentEventChartSpec, } from '../../../vis_augmenter/public'; import { formatDatatable, createSpecFromDatatable } from './helpers'; import { VegaVisualizationDependencies } from '../plugin'; @@ -29,6 +30,7 @@ interface Arguments { visLayers: string | null; visParams: string; dimensions: string; + visAugmenterConfig: string; } export type LineVegaSpecExpressionFunctionDefinition = ExpressionFunctionDefinition< @@ -63,6 +65,11 @@ export const createLineVegaSpecFn = ( default: '""', help: '', }, + visAugmenterConfig: { + types: ['string'], + default: '""', + help: '', + }, }, async fn(input, args, context) { let table = formatDatatable(cloneDeep(input)); @@ -70,6 +77,7 @@ export const createLineVegaSpecFn = ( const visParams = JSON.parse(args.visParams) as VisParams; const dimensions = JSON.parse(args.dimensions) as VislibDimensions; const allVisLayers = (args.visLayers ? JSON.parse(args.visLayers) : []) as VisLayers; + const visAugmenterConfig = JSON.parse(args.visAugmenterConfig); // currently only supporting PointInTimeEventsVisLayer type const pointInTimeEventsVisLayers = allVisLayers.filter((visLayer: VisLayer) => @@ -80,12 +88,20 @@ export const createLineVegaSpecFn = ( table = addPointInTimeEventsLayersToTable(table, dimensions, pointInTimeEventsVisLayers); } - let spec = createSpecFromDatatable(table, visParams, dimensions); + let spec = createSpecFromDatatable(table, visParams, dimensions, visAugmenterConfig); if (!isEmpty(pointInTimeEventsVisLayers) && dimensions.x !== null) { spec = addPointInTimeEventsLayersToSpec(table, dimensions, spec); + // @ts-ignore spec.config = enableVisLayersInSpecConfig(spec, pointInTimeEventsVisLayers); } + + // Apply other formatting changes to the spec (show vis data, hide axes, etc.) based on the + // vis augmenter config. Mostly used for customizing the views on the view events flyout. + spec = augmentEventChartSpec(visAugmenterConfig, spec); + + // TODO: also force y axis to be on left side, or figure out a way to handle if on the right side. + // probably the former. Need to confirm in eligibility PR return JSON.stringify(spec); }, }); diff --git a/src/plugins/vis_type_vega/public/expressions/vega_fn.ts b/src/plugins/vis_type_vega/public/expressions/vega_fn.ts index f5ab178cbd74..cd9cf976873a 100644 --- a/src/plugins/vis_type_vega/public/expressions/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/expressions/vega_fn.ts @@ -48,9 +48,12 @@ type Output = Promise>; interface Arguments { spec: string; + savedObjectId: string; } -export type VisParams = Required; +export interface VisParams { + spec: string; +} export type VegaExpressionFunctionDefinition = ExpressionFunctionDefinition< 'vega', @@ -81,6 +84,11 @@ export const createVegaFn = ( default: '', help: '', }, + savedObjectId: { + types: ['string'], + default: '', + help: '', + }, }, async fn(input, args, context) { const vegaRequestHandler = createVegaRequestHandler(dependencies, context); @@ -100,6 +108,7 @@ export const createVegaFn = ( visType: 'vega', visConfig: { spec: args.spec, + savedObjectId: args.savedObjectId, }, }, }; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index 9751a73ccf91..3967c5351367 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -51,6 +51,8 @@ import { ConfigSchema } from '../config'; import { getVegaInspectorView } from './vega_inspector'; import { createLineVegaSpecFn } from './expressions/line_vega_spec_fn'; +import { UiActionsStart } from '../../ui_actions/public'; +import { setUiActions } from './services'; /** @internal */ export interface VegaVisualizationDependencies { @@ -73,6 +75,7 @@ export interface VegaPluginSetupDependencies { /** @internal */ export interface VegaPluginStartDependencies { data: DataPublicPluginStart; + uiActions: UiActionsStart; } /** @internal */ @@ -110,9 +113,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..b67a0959c63d 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 '../../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_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 65843ff05162..10a514c5f045 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('euiPaletteColorBlind', euiPaletteColorBlind()); @@ -84,6 +86,7 @@ export class VegaBaseView { this._destroyHandlers = []; this._initialized = false; this._enableExternalUrls = getEnableExternalUrls(); + this._visInput = opts.visInput; } async init() { @@ -268,11 +271,11 @@ export class VegaBaseView { // space and leave enough space to show the bottom view (the events vis). // Ref: https://vega.github.io/vega-lite/docs/size.html#limitations addPointInTimeEventPadding(view) { - // TODO: 100 is enough padding for now. May need to adjust once the current scrolling/overflow - // issue is handled. See https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3501 const eventVisHeight = 100; const height = Math.max(0, this._$container.height()) - eventVisHeight; - view._signals.concat_0_height.value = height; + if (view._signals.concat_0_height !== undefined) { + view._signals.concat_0_height.value = height; + } } setView(view) { @@ -297,6 +300,15 @@ export class VegaBaseView { this._addDestroyHandler(() => tthandler.hideTooltip()); } + // TODO: The filtering on the item ('annotation datapoint' vs. regular datapoint, etc.) will be handled in + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3317 + // Right now, clicking anywhere on the chart will trigger the flyout to open. + /* eslint-disable */ + view.addEventListener('click', function (event, item) { + 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..0e51b6c2068e 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 @@ -29,8 +29,6 @@ */ import { calculatePopoverPosition } from '@elastic/eui'; -import { formatValue as createTooltipContent } from 'vega-tooltip'; -import _ from 'lodash'; // Some of this code was adapted from https://github.com/vega/vega-tooltip @@ -76,6 +74,7 @@ export class TooltipHandler { 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 +86,10 @@ 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); + /* eslint-disable */ + el.innerHTML = this.createTooltipHtml(value); // add to DOM to calculate tooltip size document.body.appendChild(el); @@ -117,6 +118,13 @@ export class TooltipHandler { el.setAttribute('style', `top: ${pos.top}px; left: ${pos.left}px`); } + // TODO: This is where the custom tooltip will be handled - see here for details: + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3317 + /* eslint-disable */ + 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_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js index 4e9a2e53c144..969a722ccfb7 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.js @@ -28,7 +28,8 @@ * under the License. */ -import { VisLayerTypes } from '../../../vis_augmenter/public'; +import { get } from 'lodash'; +import { VisLayerTypes, calculateYAxisPadding } from '../../../vis_augmenter/public'; import { vega } from '../lib/vega'; import { VegaBaseView } from './vega_base_view'; @@ -45,9 +46,35 @@ export class VegaView extends VegaBaseView { view.warn = this.onWarn.bind(this); view.error = this.onError.bind(this); if (this._parser.useResize) this.updateVegaSize(view); - if (this._parser.visibleVisLayers?.get(VisLayerTypes.PointInTimeEvents) === true) { + + const showPointInTimeEvents = + this._parser.visibleVisLayers?.get(VisLayerTypes.PointInTimeEvents) === true; + + if (showPointInTimeEvents) { this.addPointInTimeEventPadding(view); + const fromDashboard = get(this, '_parser.visAugmenterConfig.fromDashboard', true); + const showVisData = get(this, '_parser.visAugmenterConfig.showVisData', true); + const yAxisConfig = get(this, '_parser.vlspec.config.axisY', {}); + + // Autosizing is needed here since autosize won't be set correctly when there is PointInTimeEventLayers. + // This is because these layers cause the spec to use `vconcat` under the hood to stack the base chart + // with the event chart. Autosize doesn't work at the vega-lite level, so we set here at the vega level. + // Details here: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3485#issuecomment-1507442348 + view.autosize({ + type: 'fit', + contains: 'padding', + }); + + if (!fromDashboard) { + view.padding({ + ...view.padding(), + // If we are not showing the x axis, it means it is a plain event chart in the view events flyout, + // which means we need to offset the chart to align with the y axis padding of the base vis chart. + left: showVisData ? 0 : calculateYAxisPadding(yAxisConfig), + }); + } } + view.initialize(this._$container.get(0), this._$controls.get(0)); if (this._parser.useHover) view.hover(); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.js b/src/plugins/vis_type_vega/public/vega_visualization.js index af5c58f8a149..9bacd1b59fa3 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 }) => @@ -90,6 +91,9 @@ export const createVegaVisualization = ({ getServiceSettings }) => serviceSettings, filterManager, timefilter, + visInput: { + savedObjectId: get(this._vis, 'params.savedObjectId'), + }, }; if (vegaParser.useMap) { diff --git a/src/plugins/vis_type_vislib/public/line_to_expression.ts b/src/plugins/vis_type_vislib/public/line_to_expression.ts index 2648e70af38d..bf57b5c3877a 100644 --- a/src/plugins/vis_type_vislib/public/line_to_expression.ts +++ b/src/plugins/vis_type_vislib/public/line_to_expression.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { get } from 'lodash'; import { buildVislibDimensions, Vis } from '../../visualizations/public'; import { buildExpression, buildExpressionFunction } from '../../expressions/public'; import { OpenSearchaggsExpressionFunctionDefinition } from '../../data/common/search/expressions'; @@ -10,6 +11,7 @@ import { VegaExpressionFunctionDefinition, LineVegaSpecExpressionFunctionDefinition, } from '../../vis_type_vega/public'; +import { VisAugmenterEmbeddableConfig } from '../../vis_augmenter/public'; export const toExpressionAst = async (vis: Vis, params: any) => { // Construct the existing expr fns that are ran for vislib line chart, up until the render fn. @@ -38,6 +40,12 @@ export const toExpressionAst = async (vis: Vis, params: any) => { return ast.toAst(); } else { const dimensions = await buildVislibDimensions(vis, params); + const visAugmenterConfig = get( + params, + 'visAugmenterConfig', + {} + ) as VisAugmenterEmbeddableConfig; + // adding the new expr fn here that takes the datatable and converts to a vega spec const vegaSpecFn = buildExpressionFunction( 'line_vega_spec', @@ -45,6 +53,7 @@ export const toExpressionAst = async (vis: Vis, params: any) => { visLayers: JSON.stringify(params.visLayers), visParams: JSON.stringify(vis.params), dimensions: JSON.stringify(dimensions), + visAugmenterConfig: JSON.stringify(visAugmenterConfig), } ); const vegaSpecFnExpressionBuilder = buildExpression([vegaSpecFn]); @@ -53,6 +62,7 @@ export const toExpressionAst = async (vis: Vis, params: any) => { // spec via 'line_vega_spec' fn, then set as the arg for the final 'vega' fn const vegaFn = buildExpressionFunction('vega', { spec: vegaSpecFnExpressionBuilder, + savedObjectId: get(vis, 'id', ''), }); const ast = buildExpression([opensearchaggsFn, vegaFn]); return ast.toAst(); diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index c0d43cd521da..14ed001983d0 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -73,6 +73,12 @@ import { buildPipelineFromAugmentVisSavedObjs, } from '../../../vis_augmenter/public'; import { VisSavedObject } from '../types'; +import { + PointInTimeEventsVisLayer, + VisLayer, + VisLayerTypes, + VisAugmenterEmbeddableConfig, +} from '../../../vis_augmenter/public'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -91,6 +97,7 @@ export interface VisualizeInput extends EmbeddableInput { }; savedVis?: SerializedVis; table?: unknown; + visAugmenterConfig?: VisAugmenterEmbeddableConfig; } export interface VisualizeOutput extends EmbeddableOutput { @@ -136,6 +143,8 @@ export class VisualizeEmbeddable >; private savedVisualizationsLoader?: SavedVisualizationsLoader; private savedAugmentVisLoader?: SavedAugmentVisLoader; + public visLayers?: VisLayer[]; + private visAugmenterConfig?: VisAugmenterEmbeddableConfig; constructor( timefilter: TimefilterContract, @@ -171,6 +180,7 @@ export class VisualizeEmbeddable this.attributeService = attributeService; this.savedVisualizationsLoader = savedVisualizationsLoader; this.savedAugmentVisLoader = savedAugmentVisLoader; + this.visAugmenterConfig = initialInput.visAugmenterConfig; this.autoRefreshFetchSubscription = timefilter .getAutoRefreshFetch$() .subscribe(this.updateHandler.bind(this)); @@ -404,13 +414,147 @@ export class VisualizeEmbeddable this.abortController = new AbortController(); const abortController = this.abortController; - const visLayers = await this.fetchVisLayers(expressionParams, abortController); + // TODO: remove hardcoded vislayers when finished testing + // const visLayers = await this.fetchVisLayers(expressionParams, abortController); + const visLayers = [ + { + originPlugin: 'anomalyDetectionDashboards', + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: 'Anomaly Detectors', + id: 'detector-a-id', + name: 'Detector A', + urlPath: 'test-plugin-resource-path', + }, + events: [ + { + timestamp: 1679781303000, + metadata: { + pluginResourceId: 'detector-a-id', + }, + }, + { + timestamp: 1680126903000, + metadata: { + pluginResourceId: 'detector-a-id', + }, + }, + { + timestamp: 1680299703000, + metadata: { + pluginResourceId: 'detector-a-id', + }, + }, + ], + }, + { + originPlugin: 'anomalyDetectionDashboards', + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: 'Anomaly Detectors', + id: 'detector-b-id', + name: 'Detector B', + urlPath: 'test-plugin-resource-path', + }, + events: [ + { + timestamp: 1679781303000, + metadata: { + pluginResourceId: 'detector-b-id', + }, + }, + ], + }, + { + originPlugin: 'alertingDashboards', + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: 'Alerting Monitors', + id: 'monitor-a-id', + name: 'Monitor A', + urlPath: 'test-plugin-resource-path', + }, + events: [ + { + timestamp: 1679781303000, + metadata: { + pluginResourceId: 'monitor-a-id', + }, + }, + ], + }, + { + originPlugin: 'alertingDashboards', + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: 'Alerting Monitors', + id: 'monitor-b-id', + name: 'Monitor B', + urlPath: 'test-plugin-resource-path', + }, + events: [ + { + timestamp: 1680126903000, + metadata: { + pluginResourceId: 'monitor-b-id', + }, + }, + ], + }, + { + originPlugin: 'alertingDashboards', + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: 'Alerting Monitors', + id: 'monitor-c-id', + name: 'Monitor C', + urlPath: 'test-plugin-resource-path', + }, + events: [ + { + timestamp: 1679781303000, + metadata: { + pluginResourceId: 'monitor-c-id', + }, + }, + ], + }, + { + originPlugin: 'alertingDashboards', + type: VisLayerTypes.PointInTimeEvents, + pluginResource: { + type: 'Alerting Monitors', + id: 'monitor-d-id', + name: 'Monitor D', + urlPath: 'test-plugin-resource-path', + }, + events: [ + { + timestamp: 1680299703000, + metadata: { + pluginResourceId: 'monitor-d-id', + }, + }, + ], + }, + ] as PointInTimeEventsVisLayer[]; + + // If visLayerResourceIds is defined on the input, then filter out the found + // VisLayers to only include ones in that specified list. + // By default, do no filtering. + this.visLayers = + this.visAugmenterConfig?.visLayerResourceIds === undefined + ? visLayers + : visLayers.filter((visLayer) => + this.visAugmenterConfig?.visLayerResourceIds?.includes(visLayer.pluginResource.id) + ); this.expression = await buildPipeline(this.vis, { timefilter: this.timefilter, timeRange: this.timeRange, abortSignal: this.abortController!.signal, - visLayers, + visLayers: this.visLayers, + visAugmenterConfig: this.visAugmenterConfig, }); if (this.handler && !abortController.signal.aborted) { diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index ffc24a81b381..957c9d8c80cd 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, diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index d751e088c99d..de41a7a48c02 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -33,7 +33,7 @@ import moment from 'moment'; import { formatExpression, SerializedFieldFormat } from '../../../../plugins/expressions/public'; import { IAggConfig, search, TimefilterContract } from '../../../../plugins/data/public'; import { Vis, VisParams } from '../types'; -import { VisLayers } from '../../../../plugins/vis_augmenter/public'; +import { VisAugmenterEmbeddableConfig, VisLayers } from '../../../../plugins/vis_augmenter/public'; const { isDateHistogramBucketAggConfig } = search.aggs; @@ -88,6 +88,7 @@ export interface BuildPipelineParams { timeRange?: any; abortSignal?: AbortSignal; visLayers?: VisLayers; + visAugmenterConfig?: VisAugmenterEmbeddableConfig; } const vislibCharts: string[] = [ diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index ec0fc098189d..0215a86da08d 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -220,6 +220,7 @@ export class VisualizationsPlugin }); setSavedAugmentVisLoader(savedAugmentVisLoader); setSavedSearchLoader(savedSearchLoader); + return { ...types, showNewVisModal,