-
Notifications
You must be signed in to change notification settings - Fork 915
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
Conversation
…text Signed-off-by: Tyler Ohlsen <[email protected]>
Codecov Report
@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
|
The build workflow that is failing is unrelated to this change and is also seen in The codecov failure is fixed in follow up PR #4516 that will bump the coverage. |
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. |
There was a problem hiding this comment.
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:
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. |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
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: |
@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', |
There was a problem hiding this comment.
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?
.filter((augmentVisObj) => augmentVisObj.visId === savedObjectId) | ||
.map((augmentVisObj) => augmentVisObj.id as string); |
There was a problem hiding this comment.
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.
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'), |
There was a problem hiding this comment.
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.
description: get(opts, 'description', ''), | ||
originPlugin: get(opts, 'originPlugin', ''), | ||
pluginResource: get(opts, 'pluginResource', {}), | ||
visId: get(opts, 'visId', ''), | ||
visLayerExpressionFn: get(opts, 'visLayerExpressionFn', {}), |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
?
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', |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
vis.params?.seriesParams !== undefined && | ||
vis.params?.seriesParams?.every( | ||
(seriesParam: { type: string }) => seriesParam.type === 'line' | ||
) && | ||
vis.params?.type === 'line'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can do better.
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); |
There was a problem hiding this comment.
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!'; |
There was a problem hiding this comment.
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.
const name = get(props, 'item.visLayer.pluginResource.name', ''); | ||
const urlPath = get(props, 'item.visLayer.pluginResource.urlPath', ''); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 ( |
There was a problem hiding this comment.
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?
$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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for get
filters: embeddable.getInput().filters, | ||
query: embeddable.getInput().query, | ||
timeRange: embeddable.getInput().timeRange, |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
Description
Manual backport of #4361 . There was a changelog conflict.
Check List
yarn test:jest
yarn test:jest_integration
yarn test:ftr