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

[Backport 2.x] Enable plugins to augment visualizations with additional data and context (#4361) #4517

Merged
merged 2 commits into from
Jul 7, 2023

Conversation

ohltyler
Copy link
Member

@ohltyler ohltyler commented Jul 7, 2023

Description

Manual backport of #4361 . There was a changelog conflict.

Check List

  • All tests pass
    • yarn test:jest
    • yarn test:jest_integration
    • yarn test:ftr
  • New functionality includes testing.
  • New functionality has been documented.
  • Update CHANGELOG.md
  • Commits are signed per the DCO using --signoff

@codecov
Copy link

codecov bot commented Jul 7, 2023

Codecov Report

Merging #4517 (9b47ba2) into 2.x (1e72f59) will increase coverage by 0.06%.
The diff coverage is 64.95%.

@@            Coverage Diff             @@
##              2.x    #4517      +/-   ##
==========================================
+ Coverage   66.40%   66.46%   +0.06%     
==========================================
  Files        3245     3287      +42     
  Lines       62451    63286     +835     
  Branches     9712     9844     +132     
==========================================
+ Hits        41470    42063     +593     
- Misses      18624    18844     +220     
- Partials     2357     2379      +22     
Flag Coverage Δ
Linux 66.39% <64.95%> (+0.04%) ⬆️
Windows 66.41% <64.95%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
...s/embeddable/public/lib/panel/embeddable_panel.tsx 64.13% <ø> (ø)
src/plugins/embeddable/public/plugin.tsx 76.53% <ø> (ø)
..._collection/server/collectors/management/schema.ts 100.00% <ø> (ø)
...c/plugins/saved_objects_management/public/index.ts 0.00% <ø> (ø)
src/plugins/ui_actions/public/index.ts 100.00% <ø> (ø)
src/plugins/vis_augmenter/public/index.ts 0.00% <0.00%> (ø)
src/plugins/vis_augmenter/public/plugin.ts 0.00% <0.00%> (ø)
...lic/view_events_flyout/components/events_panel.tsx 0.00% <0.00%> (ø)
...w_events_flyout/components/plugin_events_panel.tsx 0.00% <0.00%> (ø)
...is_type_vega/public/components/vega_vis_editor.tsx 14.28% <ø> (ø)
... and 57 more

... and 5 files with indirect coverage changes

@ohltyler
Copy link
Member Author

ohltyler commented Jul 7, 2023

The build workflow that is failing is unrelated to this change and is also seen in main.

The codecov failure is fixed in follow up PR #4516 that will bump the coverage.

lezzago
lezzago previously approved these changes Jul 7, 2023
Co-authored-by: Miki <[email protected]>
Signed-off-by: Tyler Ohlsen <[email protected]>

Saved objects that have relationships to index patterns are saved using the [`kibanaSavedObjectMeta`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts#L59) attribute and the [`references`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts#L60) array structure. Functions from the data plugin are used by the saved object plugin to manage this index pattern relationship.
Saved objects can persist parent/child relationships to other saved objects via `references`. These relationships can be viewed on the UI in the [saved objects management plugin](src/core/server/saved_objects_management/README.md). Relationships can be useful to combine existing saved objects to produce new ones, such as using an index pattern as the source for a visualization, or a dashboard consisting of many visualizations.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe in a follow-up PR:

Suggested change
Saved objects can persist parent/child relationships to other saved objects via `references`. These relationships can be viewed on the UI in the [saved objects management plugin](src/core/server/saved_objects_management/README.md). Relationships can be useful to combine existing saved objects to produce new ones, such as using an index pattern as the source for a visualization, or a dashboard consisting of many visualizations.
Any parent/child relationships among saved objects can persist using their `references` property. These relationships can be viewed on the UI in the [saved objects management plugin](src/core/server/saved_objects_management/README.md). Relationships can be useful to combine existing saved objects to produce new ones, such as using an index pattern as the source for a visualization, or a dashboard consisting of many visualizations.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure - I am currently starting a followup documentation PR anyways so I can include these changes there. thanks!

"type" : "visualization",
"references" : [
{
"name" : "kibanaSavedObjectMeta.searchSourceJSON.index",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is kibanaSavedObjectMeta a keyword? If not, can we use our own name?

Copy link
Member Author

Choose a reason for hiding this comment

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

this is just what exists currently for visualizations. it's a saved object attribute for the visualization saved obj type. i haven't changed anything here

Copy link
Member

Choose a reason for hiding this comment

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

it is a keyword unfortunately

### Others

If a saved object type wishes to have additional custom functionalities when extracting/injecting references, or after OpenSearch's response, it can define functions in the class constructor when extending the `SavedObjectClass`. For example, visualization plugin's [`SavedVis`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#L91) class has additional `extractReferences`, `injectReferences` and `afterOpenSearchResp` functions defined in [`_saved_vis.ts`](../visualizations/public/saved_visualizations/_saved_vis.ts).
Steps need to be done on both the public/client-side & the server-side for creating a new saved object type.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Steps need to be done on both the public/client-side & the server-side for creating a new saved object type.
Certain actions need to be completed on both, the client-side and the server-side, for creating a new saved object type.


Client-side:

1. Define a class that extends `SavedObjectClass`. This is where custom functionalities, such as extracting/injecting references, or overriding `afterOpenSearchResp` can be set in the constructor. For example, visualization plugin's [`SavedVis`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#L91) class has additional `extractReferences`, `injectReferences` and `afterOpenSearchResp` functions defined in [`_saved_vis.ts`](../visualizations/public/saved_visualizations/_saved_vis.ts), and set in the `SavedVis` constructor.
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit:

Suggested change
1. Define a class that extends `SavedObjectClass`. This is where custom functionalities, such as extracting/injecting references, or overriding `afterOpenSearchResp` can be set in the constructor. For example, visualization plugin's [`SavedVis`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#L91) class has additional `extractReferences`, `injectReferences` and `afterOpenSearchResp` functions defined in [`_saved_vis.ts`](../visualizations/public/saved_visualizations/_saved_vis.ts), and set in the `SavedVis` constructor.
1. Define a class that extends `SavedObjectClass`. This is where custom functionalities, such as extracting/injecting references or overriding `afterOpenSearchResp`, can be set in the constructor. For example, visualization plugin's [`SavedVis`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#L91) class has additional `extractReferences`, `injectReferences`, and `afterOpenSearchResp` functions defined in [`_saved_vis.ts`](../visualizations/public/saved_visualizations/_saved_vis.ts) and set in the `SavedVis` constructor.

return (savedVis as any) as SavedObject;
},
```

2. Optionally create a loader class that extends `SavedObjectLoader`. This can be useful for performing default CRUD operations on this particular saved object type, as well as overriding default utility functions like `find`. For example, the `visualization` saved object overrides `mapHitSource` (used in `find` & `findAll`) to do additional checking on the returned source object, such as if the returned type is valid:
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit:

Suggested change
2. Optionally create a loader class that extends `SavedObjectLoader`. This can be useful for performing default CRUD operations on this particular saved object type, as well as overriding default utility functions like `find`. For example, the `visualization` saved object overrides `mapHitSource` (used in `find` & `findAll`) to do additional checking on the returned source object, such as if the returned type is valid:
2. Optionally, create a loader class that extends `SavedObjectLoader`. This can be useful for performing default CRUD operations on this particular saved object type and overriding default utility functions like `find`. For example, the `visualization` saved object overrides `mapHitSource` (used in `find` and `findAll`) to perform additional checks on the returned source object, such as checking if the returned type is valid:

@ashwin-pc
Copy link
Member

ashwin-pc commented Jul 7, 2023

@ohltyler All of Miki's comments are about doc fixes and additional tests, both of which you are working on i believe in a follow up.


public getDisplayName() {
return i18n.translate('dashboard.actions.deleteSavedObject.name', {
defaultMessage: 'Clean up augment-vis saved objects associated to a deleted vis',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we change this to non-shortened text?

Comment on lines +51 to +52
.filter((augmentVisObj) => augmentVisObj.visId === savedObjectId)
.map((augmentVisObj) => augmentVisObj.id as string);
Copy link
Collaborator

@AMoo-Miki AMoo-Miki Jul 7, 2023

Choose a reason for hiding this comment

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

Should we be concerned that the tests didn't cover these; being "delete" worries me.

Comment on lines +216 to +219
type: get(resource, 'type', 'test-resource-type'),
id: get(resource, 'id', 'test-resource-id'),
name: get(resource, 'name', 'test-resource-name'),
urlPath: get(resource, 'urlPath', 'test-resource-url-path'),
Copy link
Collaborator

Choose a reason for hiding this comment

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

These have to be replaced; there is no need for get here.

Comment on lines +51 to +55
description: get(opts, 'description', ''),
originPlugin: get(opts, 'originPlugin', ''),
pluginResource: get(opts, 'pluginResource', {}),
visId: get(opts, 'visId', ''),
visLayerExpressionFn: get(opts, 'visLayerExpressionFn', {}),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, there is no need to use get here.

* SPDX-License-Identifier: Apache-2.0
*/

import { get, isEmpty } from 'lodash';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let;s not introduce more uses of lodash; we are actively trying to get rid of it.

if (updatedAttributes.visId) {
updatedReferences.push({
name: VIS_REFERENCE_NAME,
type: 'visualization',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the term visualization a constant somewhere we can import?

uiSettings?: IUiSettingsClient
): Promise<any> => {
// Using optional services provided, or the built-in services from this plugin
const loader = savedObjLoader !== undefined ? savedObjLoader : getSavedAugmentVisLoader();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wouldn't this be safer? What if savedObjLoader is null?

Suggested change
const loader = savedObjLoader !== undefined ? savedObjLoader : getSavedAugmentVisLoader();
const loader = savedObjLoader || getSavedAugmentVisLoader();

export const pluginResourceDeleteTrigger: Trigger<'PLUGIN_RESOURCE_DELETE_TRIGGER'> = {
id: PLUGIN_RESOURCE_DELETE_TRIGGER,
title: i18n.translate('visAugmenter.triggers.pluginResourceDeleteTitle', {
defaultMessage: 'Plugin resource delete',
Copy link
Collaborator

Choose a reason for hiding this comment

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

As a trigger, this title is wrong.

defaultMessage: 'Plugin resource delete',
}),
description: i18n.translate('visAugmenter.triggers.pluginResourceDeleteDescription', {
defaultMessage: 'Delete augment-vis saved objs associated to the deleted plugin resource',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's use full forms.

Comment on lines +30 to +34
vis.data?.aggs !== undefined &&
vis.data.aggs?.byTypeName('date_histogram').length === 1 &&
vis.params.categoryAxes.length === 1 &&
vis.params.categoryAxes[0].position === 'bottom' &&
vis.data.aggs?.bySchemaName('segment').length > 0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
vis.data?.aggs !== undefined &&
vis.data.aggs?.byTypeName('date_histogram').length === 1 &&
vis.params.categoryAxes.length === 1 &&
vis.params.categoryAxes[0].position === 'bottom' &&
vis.data.aggs?.bySchemaName('segment').length > 0;
vis.data?.aggs?.byTypeName &&
vis.data.aggs.byTypeName('date_histogram').length === 1 &&
vis.params.categoryAxes.length === 1 &&
vis.params.categoryAxes[0].position === 'bottom' &&
vis.data.aggs.bySchemaName('segment').length > 0;

Comment on lines +38 to +41
const hasCorrectAggregationCount =
vis.data?.aggs !== undefined &&
vis.data.aggs?.bySchemaName('metric').length > 0 &&
vis.data.aggs?.bySchemaName('metric').length === vis.data.aggs?.aggs.length - 1;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const hasCorrectAggregationCount =
vis.data?.aggs !== undefined &&
vis.data.aggs?.bySchemaName('metric').length > 0 &&
vis.data.aggs?.bySchemaName('metric').length === vis.data.aggs?.aggs.length - 1;
const visAggsMetricLength = vis.data.aggs?.bySchemaName?.('metric')?.length || 0;
const hasCorrectAggregationCount =
visAggsMetricLength > 0 &&
visAggsMetricLength === vis.data.aggs?.aggs.length - 1;

Comment on lines +43 to +47
vis.params?.seriesParams !== undefined &&
vis.params?.seriesParams?.every(
(seriesParam: { type: string }) => seriesParam.type === 'line'
) &&
vis.params?.type === 'line';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
vis.params?.seriesParams !== undefined &&
vis.params?.seriesParams?.every(
(seriesParam: { type: string }) => seriesParam.type === 'line'
) &&
vis.params?.type === 'line';
vis.params?.type === 'line' &&
vis.params.seriesParams?.every?.(
(seriesParam: { type: string }) => seriesParam.type === 'line'
);

// Checks if the augmentation setting is enabled
const config = uiSettingsClient ?? getUISettings();
const isAugmentationEnabled = config.get(PLUGIN_AUGMENTATION_ENABLE_SETTING);
return isAugmentationEnabled && hasValidXaxis && hasCorrectAggregationCount && hasOnlyLineSeries;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of doing all those things above and finally responding, we should return false after each one is assessed; doing it all at once is wasteful.

uiSettings?: IUiSettingsClient | undefined
): Promise<ISavedAugmentVis[]> => {
// Using optional services provided, or the built-in services from this plugin
const config = uiSettings !== undefined ? uiSettings : getUISettings();
Copy link
Collaborator

Choose a reason for hiding this comment

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

I see

const config = uiSettingsClient ?? getUISettings();

and

const config = uiSettings !== undefined ? uiSettings : getUISettings();

used in different places. While it would be a lot nicer to have just one of them, I would argue both are wrong and we should do

const config = uiSettings || getUISettings();

): Promise<ISavedAugmentVis[]> => {
try {
const resp = await loader?.findAll();
return (get(resp, 'hits', []) as any[]) as ISavedAugmentVis[];
Copy link
Collaborator

Choose a reason for hiding this comment

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

We can do better.

Comment on lines +171 to +178
const objIdsToDelete = [] as string[];
staleVisLayers.forEach((staleVisLayer) => {
// Match the VisLayer to its origin saved obj to extract the to-be-deleted saved obj ID
const deletedPluginResourceId = staleVisLayer.pluginResource.id;
const savedObjId = augmentVisSavedObjs.find(
(savedObj) => savedObj.pluginResource.id === deletedPluginResourceId
)?.id;
if (savedObjId !== undefined) objIdsToDelete.push(savedObjId);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This would be faster if we throw the augmentVisSavedObjs[].pluginResource.id into a set or object than search an array for each.

import { ErrorFlyoutBody } from './error_flyout_body';

describe('<ErrorFlyoutBody/>', () => {
const errorMsg = 'oh no an error!';
Copy link
Collaborator

@AMoo-Miki AMoo-Miki Jul 7, 2023

Choose a reason for hiding this comment

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

This has to change even if it is just a test.

Comment on lines +22 to +23
const name = get(props, 'item.visLayer.pluginResource.name', '');
const urlPath = get(props, 'item.visLayer.pluginResource.urlPath', '');
Copy link
Collaborator

Choose a reason for hiding this comment

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

No need for get.

const onButtonClick = () => setIsErrorPopoverOpen((isOpen) => !isOpen);
const closeErrorPopover = () => setIsErrorPopoverOpen(false);

const errorMsg = get(props, 'visLayer.error.message', undefined) as string | undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

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

No need for get.

}

export function EventsPanel(props: Props) {
return (
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we not need to test this?

Comment on lines +1 to +6
$vis-description-width: 200px;
$event-vis-height: 55px;
$timeline-panel-height: 100px;
$content-padding-top: 110px; // Padding needed within view events flyout content to sit comfortably below flyout header
$date-range-height: 45px; // Static height we want for the date range picker component
$error-icon-padding-right: -8px; // This is so the error icon is aligned consistent with the event count icons
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do the flyout and date-picker change sizes with text being enlarged? If yes, we should use em or rem here.

}

embeddable.updateInput({
// @ts-ignore
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should document the reason for having the ts-ignore

const visLayers = (get(embeddable, 'visLayers', []) as VisLayer[]).filter((visLayer) =>
isPointInTimeEventsVisLayer(visLayer)
) as PointInTimeEventsVisLayer[];
if (visLayers !== undefined) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wouldn't visLayers always be an array making this condition always true?

const map = new Map<string, EventVisEmbeddableItem[]>() as EventVisEmbeddablesMap;
// Currently only support PointInTimeEventVisLayers. Different layer types
// may require different logic in here
const visLayers = (get(embeddable, 'visLayers', []) as VisLayer[]).filter((visLayer) =>
Copy link
Collaborator

Choose a reason for hiding this comment

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

No need for get

Comment on lines +144 to +146
filters: embeddable.getInput().filters,
query: embeddable.getInput().query,
timeRange: embeddable.getInput().timeRange,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is embeddable.getInput() expensive? if so, we should fetch it once into a variable and reuse it.

const embeddableVisFactory = getEmbeddable().getEmbeddableFactory('visualization');
try {
const { left, right } = getValueAxisPositions(embeddable);
const map = new Map<string, EventVisEmbeddableItem[]>() as EventVisEmbeddablesMap;
Copy link
Collaborator

Choose a reason for hiding this comment

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

map is not a good choice for a variable name; let's find a better name for it.

},
});

const curList = (map.get(pluginResourceType) === undefined
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we care for it to be specifically undefined? if not, .has() would be a better choice.

Copy link
Member

@ashwin-pc ashwin-pc left a comment

Choose a reason for hiding this comment

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

@ohltyler please address @AMoo-Miki's comments in a fast follow.

@ohltyler ohltyler merged commit 16f6f8a into opensearch-project:2.x Jul 7, 2023
44 of 45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants