Skip to content

Commit

Permalink
Add a new augment-vis saved object type (#3109)
Browse files Browse the repository at this point in the history
Client-side:
- creates & defines an augment vis saved object class
- creates helper fns to convert from client-side definition (with no references) to server-side definition (with references) & vice-versa
- introduce a saved object loader for the new saved object type. This loader provides a clean way to interact with all saved objects of this type, and can be consumed in dependent plugins for performing CRUD operations on saved objects of this type
- creates and sets the new loader in the `vis_augmenter` plugin's `start` lifecycle step

Server-side:
- creates & defines a augment vis saved object type + index mapping
- registers the new type in the `vis_augmenter` plugin's `setup` lifecycle step

It also registers the saved object in the Saved Objects Management plugin, by:
- adding a management section in the `augment-vis` saved object server-side definition,
- registering a capabilities provider to allow some of the actions to work in the management plugin, and
- adding the type to the plugin registry

Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
  • Loading branch information
ohltyler authored Feb 8, 2023
1 parent a990ab9 commit eeed599
Show file tree
Hide file tree
Showing 26 changed files with 968 additions and 58 deletions.
160 changes: 109 additions & 51 deletions src/plugins/saved_objects/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,78 @@

The saved object plugin provides all the core services and functionalities of saved objects. It is utilized by many core plugins such as [`visualization`](../visualizations/), [`dashboard`](../dashboard/) and [`visBuilder`](../vis_builder/), as well as external plugins. Saved object is the primary way to store app and plugin data in a standardized form in OpenSearch Dashboards. They allow plugin developers to manage creating, saving, editing and retrieving data for the application. They can also make reference to other saved objects and have useful features out of the box, such as migrations and strict typings. The saved objects can be managed by the Saved Object Management UI.

## Save relationships to index pattern
### Relationships

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.

A standard saved object and its index pattern relationship:
Some saved object fields have pre-defined logic. For example, if a saved object type has a `searchSource` field indicating an index pattern relationship, a reference will automatically be created 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.

An example of a visualization saved object and its index pattern relationship:

```ts

"kibanaSavedObjectMeta" : {
"searchSourceJSON" : """{"filter":[],"query":{"query":"","language":"kuery"},"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}"""
}
},
"type" : "visualization",
"references" : [
{
"name" : "kibanaSavedObjectMeta.searchSourceJSON.index",
"type" : "index-pattern",
"id" : "90943e30-9a47-11e8-b64d-95841ca0b247"
}
],
}
"type" : "visualization",
"references" : [
{
"name" : "kibanaSavedObjectMeta.searchSourceJSON.index",
"type" : "index-pattern",
"id" : "90943e30-9a47-11e8-b64d-95841ca0b247"
}
],

```

### Saving a saved object

When saving a saved object and its relationship to the index pattern:
When saving a saved object and its relationship to the index pattern:

1. A saved object will be built using [`buildSavedObject`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts#L46) function. Services such as hydrating index pattern, initializing and serializing the saved object are set, and configs such as saved object id, migration version are defined.
2. The saved object will then be serialized by three steps:

a. By using [`extractReferences`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/data/common/search/search_source/extract_references.ts#L35) function from the data plugin, the index pattern information will be extracted using the index pattern id within the `kibanaSavedObjectMeta`, and the id will be replaced by a reference name, such as `indexRefName`. A corresponding index pattern object will then be created to include more detailed information of the index pattern: name (`kibanaSavedObjectMeta.searchSourceJSON.index`), type, and id.

```ts
let searchSourceFields = { ...state };
const references = [];

if (searchSourceFields.index) {
const indexId = searchSourceFields.index.id || searchSourceFields.index;
const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index';
references.push({
name: refName,
type: 'index-pattern',
id: indexId
});
searchSourceFields = { ...searchSourceFields,
indexRefName: refName,
index: undefined
};
}
```
2. The saved object will then be serialized by three steps:

b. The `indexRefName` along with other information will be stringified and saved into `kibanaSavedObjectMeta.searchSourceJSON`.

c. Saved object client will create the reference array attribute, and the index pattern object will be pushed into the reference array.
a. By using [`extractReferences`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/data/common/search/search_source/extract_references.ts#L35) function from the data plugin, the index pattern information will be extracted using the index pattern id within the `kibanaSavedObjectMeta`, and the id will be replaced by a reference name, such as `indexRefName`. A corresponding index pattern object will then be created to include more detailed information of the index pattern: name (`kibanaSavedObjectMeta.searchSourceJSON.index`), type, and id.

```ts
let searchSourceFields = { ...state };
const references = [];

if (searchSourceFields.index) {
const indexId = searchSourceFields.index.id || searchSourceFields.index;
const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index';
references.push({
name: refName,
type: 'index-pattern',
id: indexId,
});
searchSourceFields = { ...searchSourceFields, indexRefName: refName, index: undefined };
}
```

b. The `indexRefName` along with other information will be stringified and saved into `kibanaSavedObjectMeta.searchSourceJSON`.

c. Saved object client will create the reference array attribute, and the index pattern object will be pushed into the reference array.

### Loading an existing or creating a new saved object

1. When loading an existing object or creating a new saved object, [`initializeSavedObject`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/saved_objects/public/saved_object/helpers/initialize_saved_object.ts#L38) function will be called.
1. When loading an existing object or creating a new saved object, [`initializeSavedObject`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/saved_objects/public/saved_object/helpers/initialize_saved_object.ts#L38) function will be called.
2. The saved object will be deserialized in the [`applyOpenSearchResp`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/saved_objects/public/saved_object/helpers/apply_opensearch_resp.ts#L50) function.

a. Using [`injectReferences`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/data/common/search/search_source/inject_references.ts#L34) function from the data plugin, the index pattern reference name within the `kibanaSavedObject` will be substituted by the index pattern id and the corresponding index pattern reference object will be deleted if filters are applied.
a. Using [`injectReferences`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/data/common/search/search_source/inject_references.ts#L34) function from the data plugin, the index pattern reference name within the `kibanaSavedObject` will be substituted by the index pattern id and the corresponding index pattern reference object will be deleted if filters are applied.

```ts
searchSourceReturnFields.index = reference.id;
delete searchSourceReturnFields.indexRefName;
```

```ts
searchSourceReturnFields.index = reference.id;
delete searchSourceReturnFields.indexRefName;
```
### Creating a new saved object type

### 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.

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.

```ts
class SavedVis extends SavedObjectClass {
Expand All @@ -85,14 +86,71 @@ class SavedVis extends SavedObjectClass {
afterOpenSearchResp: async (savedObject: SavedObject) => {
const savedVis = (savedObject as any) as ISavedVis;
... ...

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:
```ts
class SavedObjectLoaderVisualize extends SavedObjectLoader {
mapHitSource = (source: Record<string, any>, id: string) => {
const visTypes = visualizationTypes;
... ...
let typeName = source.typeName;
if (source.visState) {
try {
typeName = JSON.parse(String(source.visState)).type;
} catch (e) {
/* missing typename handled below */
}
}

if (!typeName || !visTypes.get(typeName)) {
source.error = 'Unknown visualization type';
return source;
}
... ...
return source;
};
```
The loader can then be instantiated once and referenced when needed. For example, the `visualizations` plugin creates and sets it in its `services` in the plugin's start lifecycle:
```ts
public start(
core: CoreStart,
{ data, expressions, uiActions, embeddable, dashboard }: VisualizationsStartDeps
): VisualizationsStart {
... ...
const savedVisualizationsLoader = createSavedVisLoader({
savedObjectsClient: core.savedObjects.client,
indexPatterns: data.indexPatterns,
search: data.search,
chrome: core.chrome,
overlays: core.overlays,
visualizationTypes: types,
});
setSavedVisualizationsLoader(savedVisualizationsLoader);
... ...
}
```
Server-side:
1. Define the new type that is of type `SavedObjectsType`, which is where various settings can be configured, including the index mappings when the object is stored in the system index. To see an example type definition, you can refer to the [visualization saved object type](src/plugins/visualizations/server/saved_objects/visualization.ts).
2. Register the new type in the respective plugin's setup lifecycle function. For example, the `visualizations` plugin registers the `visualization` saved object type like below:
```ts
core.savedObjects.registerType(visualizationSavedObjectType);
```
To make the new type manageable in the `saved_objects_management` plugin, refer to the [plugin README](src/plugins/saved_objects_management/README.md)
## Migration
When a saved object is created using a previous version, the migration will trigger if there is a new way of saving the saved object and the migration functions alter the structure of the old saved object to follow the new structure. Migrations can be defined in the specific saved object type in the plugin's server folder. For example,
When a saved object is created using a previous version, the migration will trigger if there is a new way of saving the saved object and the migration functions alter the structure of the old saved object to follow the new structure. Migrations can be defined in the specific saved object type in the plugin's server folder. For example,
```ts
export const visualizationSavedObjectType: SavedObjectsType = {
Expand All @@ -116,4 +174,4 @@ The migraton version will be saved as a `migrationVersion` attribute in the save
},
```
For a more detailed explanation on the migration, refer to [`saved objects management`](src/core/server/saved_objects/migrations/README.md).
For a more detailed explanation on the migration, refer to [`saved objects management`](src/core/server/saved_objects/migrations/README.md).
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
"server": true,
"ui": true,
"requiredPlugins": ["management", "data"],
"optionalPlugins": ["dashboard", "visualizations", "discover", "home", "visBuilder"],
"optionalPlugins": [
"dashboard",
"visualizations",
"discover",
"home",
"visBuilder",
"visAugmenter"
],
"extraPublicDirs": ["public/lib"],
"requiredBundles": ["opensearchDashboardsReact", "home"]
}
16 changes: 16 additions & 0 deletions src/plugins/saved_objects_management/public/lib/in_app_url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,22 @@ describe('canViewInApp', () => {
expect(canViewInApp(uiCapabilities, 'visualizations')).toEqual(false);
});

it('should handle augment-vis', () => {
let uiCapabilities = createCapabilities({
visAugmenter: {
show: true,
},
});
expect(canViewInApp(uiCapabilities, 'augment-vis')).toEqual(true);

uiCapabilities = createCapabilities({
visAugmenter: {
show: false,
},
});
expect(canViewInApp(uiCapabilities, 'augment-vis')).toEqual(false);
});

it('should handle index patterns', () => {
let uiCapabilities = createCapabilities({
management: {
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/saved_objects_management/public/lib/in_app_url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export function canViewInApp(uiCapabilities: Capabilities, type: string): boolea
case 'visualization':
case 'visualizations':
return uiCapabilities.visualize.show as boolean;
case 'augment-vis':
return uiCapabilities.visAugmenter.show as boolean;
case 'index-pattern':
case 'index-patterns':
case 'indexPatterns':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export class SavedObjectEdition extends Component<
);
if (confirmed) {
await savedObjectsClient.delete(type, id);
notifications.toasts.addSuccess(`Deleted '${object!.attributes.title}' ${type} object`);
notifications.toasts.addSuccess(`Deleted ${this.formatTitle(object)} ${type} object`);
this.redirectToListing();
}
}
Expand All @@ -179,10 +179,14 @@ export class SavedObjectEdition extends Component<
const { object, type } = this.state;

await savedObjectsClient.update(object!.type, object!.id, attributes, { references });
notifications.toasts.addSuccess(`Updated '${attributes.title}' ${type} object`);
notifications.toasts.addSuccess(`Updated ${this.formatTitle(object)} ${type} object`);
this.redirectToListing();
};

formatTitle = (object: SimpleSavedObject<any> | undefined) => {
return object?.attributes?.title ? `'${object.attributes.title}'` : '';
};

redirectToListing() {
this.props.history.push('/');
}
Expand Down
Loading

0 comments on commit eeed599

Please sign in to comment.