Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature anywhere] Add annotation click handler #3777

Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/plugins/ui_actions/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,16 @@ export {
visualizeFieldTrigger,
VISUALIZE_GEO_FIELD_TRIGGER,
visualizeGeoFieldTrigger,
EXTERNAL_ACTION_TRIGGER,
externalActionTrigger,
} from './triggers';
export {
TriggerContextMapping,
TriggerId,
ActionContextMapping,
ActionType,
VisualizeFieldContext,
ExternalActionContext,
ACTION_VISUALIZE_FIELD,
ACTION_VISUALIZE_GEO_FIELD,
ACTION_VISUALIZE_LENS_FIELD,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Any modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { i18n } from '@osd/i18n';
import { Trigger } from '.';

export const EXTERNAL_ACTION_TRIGGER = 'EXTERNAL_ACTION_TRIGGER';
export const externalActionTrigger: Trigger<'EXTERNAL_ACTION_TRIGGER'> = {
id: EXTERNAL_ACTION_TRIGGER,
title: i18n.translate('uiActions.triggers.externalActionTitle', {
defaultMessage: 'Single click',
}),
description: i18n.translate('uiActions.triggers.externalActionDescription', {
defaultMessage:
'A data point click on the visualization used to trigger external action like show flyout, etc.',
}),
};
1 change: 1 addition & 0 deletions src/plugins/ui_actions/public/triggers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ export * from './value_click_trigger';
export * from './apply_filter_trigger';
export * from './visualize_field_trigger';
export * from './visualize_geo_field_trigger';
export * from './external_action_trigger';
export * from './default_trigger';
6 changes: 6 additions & 0 deletions src/plugins/ui_actions/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
APPLY_FILTER_TRIGGER,
VISUALIZE_FIELD_TRIGGER,
VISUALIZE_GEO_FIELD_TRIGGER,
EXTERNAL_ACTION_TRIGGER,
DEFAULT_TRIGGER,
} from './triggers';
import type { RangeSelectContext, ValueClickContext } from '../../embeddable/public';
Expand All @@ -51,6 +52,10 @@ export interface VisualizeFieldContext {
contextualFields?: string[];
}

export interface ExternalActionContext {
data: any;
amsiglan marked this conversation as resolved.
Show resolved Hide resolved
}

export type TriggerId = keyof TriggerContextMapping;

export type BaseContext = object;
Expand All @@ -63,6 +68,7 @@ export interface TriggerContextMapping {
[APPLY_FILTER_TRIGGER]: ApplyGlobalFilterActionContext;
[VISUALIZE_FIELD_TRIGGER]: VisualizeFieldContext;
[VISUALIZE_GEO_FIELD_TRIGGER]: VisualizeFieldContext;
[EXTERNAL_ACTION_TRIGGER]: ExternalActionContext;
}

const DEFAULT_ACTION = '';
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/vis_augmenter/opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"version": "opensearchDashboards",
"server": true,
"ui": true,
"requiredPlugins": ["data", "savedObjects", "opensearchDashboardsUtils", "expressions"]
"requiredPlugins": ["data", "savedObjects", "opensearchDashboardsUtils", "expressions", "uiActions"]
}
58 changes: 58 additions & 0 deletions src/plugins/vis_augmenter/public/actions/external_action_action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Any modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { ActionByType, createAction, ExternalActionContext } from '../../../ui_actions/public';
import { isPointInTimeAnnotation } from '../vega';

export type ExternalActionActionContext = ExternalActionContext;
export const ACTION_EXTERNAL_ACTION = 'ACTION_EXTERNAL_ACTION';

declare module '../../../ui_actions/public' {
export interface ActionContextMapping {
[ACTION_EXTERNAL_ACTION]: ExternalActionActionContext;
}
}

export function createExternalActionAction(): ActionByType<typeof ACTION_EXTERNAL_ACTION> {
return createAction<typeof ACTION_EXTERNAL_ACTION>({
type: ACTION_EXTERNAL_ACTION,
id: ACTION_EXTERNAL_ACTION,
shouldAutoExecute: async () => true,
execute: async (context: ExternalActionActionContext) => {
if (isPointInTimeAnnotation(context.data.item)) {
if (context.data.event === 'click') {
// TODO: show events flyout
} else if (context.data.event === 'mouseover') {
// TODO: show custom tooltip
}
}
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have these actions split into 2? One for triggering the flyout opening, and another for triggering the custom tooltip? Curious of thoughts from @ashwin-pc on this as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on whether we want to keep events from the visualization generic and have the handler distinguish based off of the data. This way we will keep the events minimal and avoid adding specific info into the general framework.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - I don't have a strong preference either way. Agreed we don't want to overload too specific of events here. Maybe one per VisLayer implementation makes sense - in this particular case, handling click/mouseover/mouseout actions for the PointInTimeEventsVisLayer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, perhaps we do want to have these split up. We will need a mechanism for disabling the click event behavior, but still enabling the custom tooltip behavior in the view events flyout. Do you see a way we could pass that logic here? Or if we split into 2 events maybe (1 for custom click event, 1 for custom hover event), be able to disable one and enable the other?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actions dont need to be defined here. The plugin listen for the trigger directly. If we have a standard contract for triggers (Which this Pr is adding), the plugin can simply listen for that trigger and do as they please.

Copy link
Member

@ohltyler ohltyler May 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amsiglan thanks. This makes sense to me in the context of the click event; I can defined a UIAction that listens for this trigger to open the view events flyout. But there's 2 things I'd like to clarify:

  1. For the scenario of disabling the clicking, but still enabling the custom tooltips, how do you propose I do this? One solution I could see is altering addVisEventSignalsToSpecConfig() to only have on for the mouse events, and just omit the click event.
  2. This is maybe a bit of a miss on my part when reviewing the latest revisions made, but for the tooltips, how should we get sufficient context passed such that a UIAction can render the tooltip correctly? The original proposal was making changes to the implemented TooltipHandler class, which is using the out-of-the-box tooltip() function provided by vega to have a customized tooltip to render it OUI-style. It requires a lot of extra context, such as the container the vega view is in, the view itself, and other positioning parameters. I actually still think we may want to treat the tooltip action as just a param within the vis itself, that determines what tooltip html we render. The example I originally had is seen here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For 1. We don't need to disable the clicks, we could just check if the flyout is already open then just no-op in the event listener.
For 2. we will need to do a quick POC and check if the provided event has all the information we need. I see it has the view and the position related information
image

Copy link
Member

@ohltyler ohltyler May 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to disable the clicks, we could just check if the flyout is already open then just no-op in the event listener.

How would you propose this? We don't persist global flyout state so how would we be able to make such a check within the UIAction?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ohltyler Why do we need to disable click? what is the interaction pattern that we want to achieve with that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So user can't infinitely re-open the flyout when clicking - details in the issue #3317

});
}
15 changes: 14 additions & 1 deletion src/plugins/vis_augmenter/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'
import { visLayers } from './expressions';
import { setSavedAugmentVisLoader } from './services';
import { createSavedAugmentVisLoader, SavedAugmentVisLoader } from './saved_augment_vis';
import {
UiActionsSetup,
EXTERNAL_ACTION_TRIGGER,
externalActionTrigger,
} from '../../ui_actions/public';
import { createExternalActionAction } from './actions/external_action_action';

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface VisAugmenterSetup {}
Expand All @@ -20,6 +26,7 @@ export interface VisAugmenterStart {
export interface VisAugmenterSetupDeps {
data: DataPublicPluginSetup;
expressions: ExpressionsSetup;
uiActions: UiActionsSetup;
}

export interface VisAugmenterStartDeps {
Expand All @@ -33,9 +40,15 @@ export class VisAugmenterPlugin

public setup(
core: CoreSetup<VisAugmenterStartDeps, VisAugmenterStart>,
{ data, expressions }: VisAugmenterSetupDeps
{ data, expressions, uiActions }: VisAugmenterSetupDeps
): VisAugmenterSetup {
expressions.registerType(visLayers);
try {
uiActions.registerTrigger(externalActionTrigger);
uiActions.addTriggerAction(EXTERNAL_ACTION_TRIGGER, createExternalActionAction());
} catch (error: any) {
// NYI
}
return {};
}

Expand Down
7 changes: 6 additions & 1 deletion src/plugins/vis_augmenter/public/test_constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { OpenSearchDashboardsDatatable } from '../../expressions/public';
import { VIS_LAYER_COLUMN_TYPE, VisLayerTypes, HOVER_PARAM } from './';
import { VisAnnotationType } from './vega/constants';

const TEST_X_AXIS_ID = 'test-x-axis-id';
const TEST_X_AXIS_ID_DIRTY = 'test.x.axis.id';
Expand Down Expand Up @@ -490,7 +491,10 @@ const TEST_EVENTS_LAYER_SINGLE_VIS_LAYER = {
filled: true,
opacity: 1,
},
transform: [{ filter: `datum['${TEST_PLUGIN_RESOURCE_ID}'] > 0` }],
transform: [
{ filter: `datum['${TEST_PLUGIN_RESOURCE_ID}'] > 0` },
{ calculate: `'${VisAnnotationType.POINT_IN_TIME_ANNOTATION}'`, as: 'annotationType' },
],
params: [{ name: HOVER_PARAM, select: { type: 'point', on: 'mouseover' } }],
encoding: {
x: {
Expand Down Expand Up @@ -536,6 +540,7 @@ const TEST_EVENTS_LAYER_MULTIPLE_VIS_LAYERS = {
{
filter: `datum['${TEST_PLUGIN_RESOURCE_ID}'] > 0 || datum['${TEST_PLUGIN_RESOURCE_ID_2}'] > 0`,
},
{ calculate: `'${VisAnnotationType.POINT_IN_TIME_ANNOTATION}'`, as: 'annotationType' },
],
};

Expand Down
8 changes: 8 additions & 0 deletions src/plugins/vis_augmenter/public/vega/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export enum VisAnnotationType {
POINT_IN_TIME_ANNOTATION = 'POINT_IN_TIME_ANNOTATION',
}
1 change: 1 addition & 0 deletions src/plugins/vis_augmenter/public/vega/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
TEST_RESULT_SPEC_MULTIPLE_VIS_LAYERS,
TEST_RESULT_SPEC_SINGLE_VIS_LAYER,
TEST_RESULT_SPEC_SINGLE_VIS_LAYER_EMPTY,
TEST_RESULT_SPEC_WITH_VIS_INTERACTION_CONFIG,
amsiglan marked this conversation as resolved.
Show resolved Hide resolved
TEST_SPEC_MULTIPLE_VIS_LAYERS,
TEST_SPEC_NO_VIS_LAYERS,
TEST_SPEC_SINGLE_VIS_LAYER,
Expand Down
11 changes: 10 additions & 1 deletion src/plugins/vis_augmenter/public/vega/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import moment from 'moment';
import { cloneDeep, isEmpty, get } from 'lodash';
import { Item } from 'vega';
import {
OpenSearchDashboardsDatatable,
OpenSearchDashboardsDatatableColumn,
Expand All @@ -24,6 +25,7 @@ import {
VisLayers,
VisLayerTypes,
} from '../';
import { VisAnnotationType } from './constants';

// Given any visLayers, create a map to indicate which VisLayer types are present.
// Convert to an array since ES6 Maps cannot be stringified.
Expand Down Expand Up @@ -314,7 +316,10 @@ export const addPointInTimeEventsLayersToSpec = (
filled: true,
opacity: 1,
},
transform: [{ filter: generateVisLayerFilterString(visLayerColumnIds) }],
transform: [
{ filter: generateVisLayerFilterString(visLayerColumnIds) },
{ calculate: `'${VisAnnotationType.POINT_IN_TIME_ANNOTATION}'`, as: 'annotationType' },
],
params: [{ name: HOVER_PARAM, select: { type: 'point', on: 'mouseover' } }],
encoding: {
x: {
Expand All @@ -340,3 +345,7 @@ export const addPointInTimeEventsLayersToSpec = (

return newSpec;
};

export const isPointInTimeAnnotation = (item?: Item | null) => {
return item?.datum?.annotationType === VisAnnotationType.POINT_IN_TIME_ANNOTATION;
};
10 changes: 10 additions & 0 deletions src/plugins/vis_type_vega/public/vega_view/vega_base_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export class VegaBaseView {
this._serviceSettings = opts.serviceSettings;
this._filterManager = opts.filterManager;
this._applyFilter = opts.applyFilter;
this._triggerExternalAction = opts.externalAction;
this._timefilter = opts.timefilter;
this._view = null;
this._vegaViewConfig = null;
Expand Down Expand Up @@ -297,6 +298,15 @@ export class VegaBaseView {
this._addDestroyHandler(() => tthandler.hideTooltip());
}

['click', 'mouseover', 'mouseout'].forEach((eventName) => {
amsiglan marked this conversation as resolved.
Show resolved Hide resolved
view.addEventListener(eventName, (_event, item) => {
this._triggerExternalAction({
event: eventName,
item,
});
});
});

return view.runAsync(); // Allows callers to await rendering
}
}
Expand Down
1 change: 1 addition & 0 deletions src/plugins/vis_type_vega/public/vega_visualization.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const createVegaVisualization = ({ getServiceSettings }) =>
serviceSettings,
filterManager,
timefilter,
externalAction: this._vis.API.events.externalAction,
};

if (vegaParser.useMap) {
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/visualizations/public/embeddable/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,19 @@ import {
APPLY_FILTER_TRIGGER,
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
EXTERNAL_ACTION_TRIGGER,
} from '../../../ui_actions/public';

export interface VisEventToTrigger {
['applyFilter']: typeof APPLY_FILTER_TRIGGER;
['brush']: typeof SELECT_RANGE_TRIGGER;
['filter']: typeof VALUE_CLICK_TRIGGER;
['externalAction']: typeof EXTERNAL_ACTION_TRIGGER;
}

export const VIS_EVENT_TO_TRIGGER: VisEventToTrigger = {
applyFilter: APPLY_FILTER_TRIGGER,
brush: SELECT_RANGE_TRIGGER,
filter: VALUE_CLICK_TRIGGER,
externalAction: EXTERNAL_ACTION_TRIGGER,
};
5 changes: 5 additions & 0 deletions src/plugins/visualizations/public/expressions/vis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface ExprVisAPIEvents {
filter: (data: any) => void;
brush: (data: any) => void;
applyFilter: (data: any) => void;
externalAction: (data: any) => void;
}

export interface ExprVisAPI {
Expand Down Expand Up @@ -99,6 +100,10 @@ export class ExprVis extends EventEmitter {
if (!this.eventsSubject) return;
this.eventsSubject.next({ name: 'applyFilter', data });
},
externalAction: (data: any) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this planned for functional testing, or can it be unit tested?

if (!this.eventsSubject) return;
this.eventsSubject.next({ name: 'externalAction', data });
},
},
};
}
Expand Down