diff --git a/doc/2/concepts/models/index.md b/doc/2/concepts/models/index.md index 276ed1459..a8bc3924e 100644 --- a/doc/2/concepts/models/index.md +++ b/doc/2/concepts/models/index.md @@ -116,8 +116,9 @@ An asset model contains the following information: - `measures`: received measurements - `metadataMappings`: (optional) metadata mappings (See [Collection Mappings](https://docs.kuzzle.io/core/2/guides/main-concepts/data-storage/#collection-mappings)) - `defaultMetadata`: (optional) default metadata values- -- `metadataDetails`: (optional) Metadata group and translations . You can use it to keep consistency on translations between your apps +- `metadataDetails`: (optional) Metadata group and translations. You can use it to keep consistency on translations between your apps - `metadataGroups`: (optional) Groups list with translations for group name. You can use it to group metadatas by their concerns +- `tooltipModels`: (optional) Tooltip model list, each containing labels and tooltip content to be shown. You can use it to create templates that displays relevant information in dashboards It is possible to create new models on the Kuzzle IoT Platform using either: diff --git a/doc/2/controllers/models/update-asset/index.md b/doc/2/controllers/models/update-asset/index.md new file mode 100644 index 000000000..6763cf36f --- /dev/null +++ b/doc/2/controllers/models/update-asset/index.md @@ -0,0 +1,166 @@ +--- +code: true +type: page +title: updateAsset +description: Update an asset model +--- + +# updateAsset + +Update an existing asset model. + +## Query Syntax + +### HTTP + +```http +URL: http://kuzzle:7512/_/device-manager/models/assets/:model?engineGroup= +Method: PUT +``` + +### Other protocols + +```js +{ + "controller": "device-manager/assets", + "action": "updateAsset", + "engineGroup": "", + "model": "", + + "body": { + + // Optional + + "metadataMappings": { + // Metadata mappings + }, + "defaultValues": { + // Default values for metadata + }, + "metadataDetails": { + /* + Metadata details including translations and group. + [name: string]: { + group?: string; + locales: { + [locale: string]: { + friendlyName: string; + description: string; + }; + }; + }; + */ + }, + "metadataGroups": { + /* + Metadata groups list and details. + { + [groupName: string]: { + locales: { + [locale: string]: { + groupFriendlyName: string; + description: string; + }; + }; + }; + }; + */ + }, + "tooltipModels": { + /* + Tooltip models for an asset model. + [key: string]: { + tooltipLabel: string; + content: [ + { + category: "metadata"; + label?: { + locales: { + [locale: string]: { + friendlyName: string; + description: string; + }; + }; + }; + metadataPath: string; + suffix?: string; + }, + { + category: "measure"; + label?: { + locales: { + [locale: string]: { + friendlyName: string; + description: string; + }; + }; + }; + measureSlot: string; + measureValuePath: string; + suffix?: string; + }, + { + category: "static"; + label?: { + locales: { + [locale: string]: { + friendlyName: string; + description: string; + }; + }; + }; + type: "link" | "image" | "text" | "title" | "separator"; + value: string; + } + ]; + }; + */ + }, + "measures": [ + // Array of measure definition with type and name + ] + } +} +``` + +--- + +## Arguments + +- `engineGroup`: Name of the engine group +- `model`: Asset model name + +--- + +## Body properties + +- `metadataMappings`: Mappings of the metadata in Elasticsearch format +- `defaultValues`: Default values for the metadata +- `metadataDetails`: Metadata group and translations +- `metadataGroups`: Groups list with translations for group name +- `tooltipModels`: Tooltip model list, containing each labels and tooltip content to display +- `measures`: Array of measure definition. Each item defines `type` and `name` properties for the measure. + +--- + +## Response + +```js +{ + "status": 200, + "error": null, + "controller": "device-manager/models", + "action": "updateAsset", + "requestId": "", + "result": { + "_id": "", + "_source": { + // Updated asset model content + }, + } +} +``` + +## Errors + +Updating an asset with metadata mappings can cause conflicts, in this case a [ MappingsConflictsError ](../../../errors/mappings-conflicts/index.md) will be thrown with the HTTP code **409**. \ No newline at end of file diff --git a/doc/2/controllers/models/write-asset/index.md b/doc/2/controllers/models/write-asset/index.md index 86e6a50d8..f6583a4da 100644 --- a/doc/2/controllers/models/write-asset/index.md +++ b/doc/2/controllers/models/write-asset/index.md @@ -52,20 +52,70 @@ Method: POST }; */ }, - "metadataGroups"; { - /* - Metadata groups list and details. - { - [groupName: string]: { - locales: { - [locale: string]: { - groupFriendlyName: string; - description: string; + "metadataGroups": { + /* + Metadata groups list and details. + { + [groupName: string]: { + locales: { + [locale: string]: { + groupFriendlyName: string; + description: string; + }; }; }; }; - }; - */ + */ + }, + "tooltipModels": { + /* + Tooltip models for an asset model. + [key: string]: { + tooltipLabel: string; + content: [ + { + category: "metadata"; + label?: { + locales: { + [locale: string]: { + friendlyName: string; + description: string; + }; + }; + }; + metadataPath: string; + suffix?: string; + }, + { + category: "measure"; + label?: { + locales: { + [locale: string]: { + friendlyName: string; + description: string; + }; + }; + }; + measureSlot: string; + measureValuePath: string; + suffix?: string; + }, + { + category: "static"; + label?: { + locales: { + [locale: string]: { + friendlyName: string; + description: string; + }; + }; + }; + type: "link" | "image" | "text" | "title" | "separator"; + value: string; + } + ]; + }; + */ }, "measures": [ // Array of measure definition with type and name @@ -84,6 +134,7 @@ Method: POST - `defaultValues`: Default values for the metadata - `metadataDetails`: Metadata group and translations - `metadataGroups`: Groups list with translations for group name +- `tooltipModels`: Tooltip model list, containing each labels and tooltip content to display - `measures`: Array of measure definition. Each item define a `type` and `name` properties for the measure. --- diff --git a/lib/modules/model/ModelService.ts b/lib/modules/model/ModelService.ts index 2ffb88b3f..e548ac68e 100644 --- a/lib/modules/model/ModelService.ts +++ b/lib/modules/model/ModelService.ts @@ -29,6 +29,7 @@ import { MetadataGroups, MetadataMappings, ModelContent, + TooltipModels, } from "./types/ModelContent"; import { AskModelAssetGet, @@ -185,6 +186,7 @@ export class ModelService extends BaseService { metadataDetails: MetadataDetails, metadataGroups: MetadataGroups, measures: AssetModelContent["asset"]["measures"], + tooltipModels: TooltipModels, ): Promise> { if (Inflector.pascalCase(model) !== model) { throw new BadRequestError(`Asset model "${model}" must be PascalCase.`); @@ -198,6 +200,7 @@ export class ModelService extends BaseService { metadataGroups, metadataMappings, model, + tooltipModels, }, engineGroup, type: "asset", @@ -545,4 +548,81 @@ export class ModelService extends BaseService { return result.hits[0]; } + + /** + * Update an asset model + */ + async updateAsset( + engineGroup: string, + model: string, + metadataMappings: MetadataMappings, + defaultMetadata: JSONObject, + metadataDetails: MetadataDetails, + metadataGroups: MetadataGroups, + measures: AssetModelContent["asset"]["measures"], + tooltipModels: TooltipModels, + request: KuzzleRequest, + ): Promise> { + if (Inflector.pascalCase(model) !== model) { + throw new BadRequestError(`Asset model "${model}" must be PascalCase.`); + } + + this.checkDefaultValues(metadataMappings, defaultMetadata); + + const existingAsset = await this.getAsset(engineGroup, model); + + // The field must be deleted if an element of the table is to be deleted + await this.sdk.document.deleteFields( + this.config.adminIndex, + InternalCollection.MODELS, + existingAsset._id, + ["asset.tooltipModels"], + { source: true }, + ); + + const measuresUpdated = + measures.length === 0 ? existingAsset._source.asset.measures : measures; + + const assetModelContent: AssetModelContent = { + asset: { + defaultMetadata, + measures: measuresUpdated, + metadataDetails, + metadataGroups, + metadataMappings, + model, + tooltipModels, + }, + engineGroup, + type: "asset", + }; + const assetModel = { + _id: existingAsset._id, + _source: assetModelContent, + }; + + const conflicts = await ask( + "ask:device-manager:engine:doesUpdateConflict", + { twin: { models: [assetModelContent], type: "asset" } }, + ); + + if (conflicts.length > 0) { + throw new MappingsConflictsError( + `Assets mappings are causing conflicts`, + conflicts, + ); + } + + const endDocument = await this.updateDocument( + request, + assetModel, + { + collection: InternalCollection.MODELS, + engineId: this.config.adminIndex, + }, + { source: true }, + ); + + return endDocument; + } } diff --git a/lib/modules/model/ModelsController.ts b/lib/modules/model/ModelsController.ts index 71553d0be..46411fda6 100644 --- a/lib/modules/model/ModelsController.ts +++ b/lib/modules/model/ModelsController.ts @@ -5,6 +5,7 @@ import { ApiModelWriteAssetResult, ApiModelWriteDeviceResult, ApiModelWriteMeasureResult, + ApiModelUpdateAssetResult, ApiModelDeleteAssetResult, ApiModelDeleteDeviceResult, ApiModelDeleteMeasureResult, @@ -64,6 +65,12 @@ export class ModelsController { handler: this.listMeasures.bind(this), http: [{ path: "device-manager/models/measures", verb: "get" }], }, + updateAsset: { + handler: this.updateAsset.bind(this), + http: [ + { path: "device-manager/models/assets/:model", verb: "patch" }, + ], + }, writeAsset: { handler: this.writeAsset.bind(this), http: [{ path: "device-manager/models/assets", verb: "post" }], @@ -113,6 +120,7 @@ export class ModelsController { const measures = request.getBodyArray("measures", []); const metadataDetails = request.getBodyObject("metadataDetails", {}); const metadataGroups = request.getBodyObject("metadataGroups", {}); + const tooltipModels = request.getBodyObject("tooltipModels", {}); const assetModel = await this.modelService.writeAsset( engineGroup, @@ -122,6 +130,7 @@ export class ModelsController { metadataDetails, metadataGroups, measures, + tooltipModels, ); return assetModel; @@ -216,4 +225,31 @@ export class ModelsController { total: models.length, }; } + + async updateAsset( + request: KuzzleRequest, + ): Promise { + const engineGroup = request.getString("engineGroup"); + const model = request.getString("model"); + const metadataMappings = request.getBodyObject("metadataMappings", {}); + const defaultValues = request.getBodyObject("defaultValues", {}); + const measures = request.getBodyArray("measures", []); + const metadataDetails = request.getBodyObject("metadataDetails", {}); + const metadataGroups = request.getBodyObject("metadataGroups", {}); + const tooltipModels = request.getBodyObject("tooltipModels", {}); + + const updatedAssetModel = await this.modelService.updateAsset( + engineGroup, + model, + metadataMappings, + defaultValues, + metadataDetails, + metadataGroups, + measures, + tooltipModels, + request, + ); + + return updatedAssetModel; + } } diff --git a/lib/modules/model/ModelsRegister.ts b/lib/modules/model/ModelsRegister.ts index 8ad009390..f4477b3cf 100644 --- a/lib/modules/model/ModelsRegister.ts +++ b/lib/modules/model/ModelsRegister.ts @@ -16,6 +16,7 @@ import { MetadataGroups, MetadataMappings, ModelContent, + TooltipModels, } from "./types/ModelContent"; import { ModelSerializer } from "./ModelSerializer"; import { JSONObject } from "kuzzle-sdk"; @@ -59,6 +60,7 @@ export class ModelsRegister { * @param defaultMetadata - The default metadata values for the model, defaults to an empty object. * @param metadataDetails - Optional detailed metadata descriptions and localizations. * @param metadataGroups - Optional groups for organizing metadata, with localizations. + * @param tooltipModels - Optional model list for tooltip, containing labels and tooltip content. * @throws PluginImplementationError if the model name is not in PascalCase. */ registerAsset( @@ -69,6 +71,7 @@ export class ModelsRegister { defaultMetadata: JSONObject = {}, metadataDetails: MetadataDetails = {}, metadataGroups: MetadataGroups = {}, + tooltipModels: TooltipModels = {}, ) { if (Inflector.pascalCase(model) !== model) { throw new PluginImplementationError( @@ -85,6 +88,7 @@ export class ModelsRegister { metadataGroups, metadataMappings, model, + tooltipModels, }, engineGroup, type: "asset", diff --git a/lib/modules/model/collections/modelsMappings.ts b/lib/modules/model/collections/modelsMappings.ts index 61056c08b..244601273 100644 --- a/lib/modules/model/collections/modelsMappings.ts +++ b/lib/modules/model/collections/modelsMappings.ts @@ -56,6 +56,10 @@ export const modelsMappings: CollectionMappings = { name: { type: "keyword" }, }, }, + tooltipModels: { + dynamic: "false", + properties: {}, + }, }, }, diff --git a/lib/modules/model/types/ModelApi.ts b/lib/modules/model/types/ModelApi.ts index e3af7d922..c96f47b84 100644 --- a/lib/modules/model/types/ModelApi.ts +++ b/lib/modules/model/types/ModelApi.ts @@ -7,6 +7,7 @@ import { MetadataDetails, MetadataGroups, MetadataMappings, + TooltipModels, } from "./ModelContent"; import { MeasureValuesDetails } from "../../measure/types/MeasureDefinition"; @@ -44,6 +45,7 @@ export interface ApiModelWriteAssetRequest extends ModelsControllerRequest { metadataMappings?: MetadataMappings; defaultValues?: JSONObject; measures?: AssetModelContent["asset"]["measures"]; + tooltipModels?: TooltipModels; }; } export type ApiModelWriteAssetResult = KDocument; @@ -73,6 +75,23 @@ export interface ApiModelWriteMeasureRequest extends ModelsControllerRequest { } export type ApiModelWriteMeasureResult = KDocument; +export interface ApiModelUpdateAssetRequest extends ModelsControllerRequest { + action: "updateAsset"; + + engineGroup: string; + model: string; + + body: { + metadataDetails?: MetadataDetails; + metadataGroups?: MetadataGroups; + metadataMappings?: MetadataMappings; + defaultValues?: JSONObject; + measures?: AssetModelContent["asset"]["measures"]; + tooltipModels?: TooltipModels; + }; +} +export type ApiModelUpdateAssetResult = KDocument; + export interface ApiModelDeleteAssetRequest extends ModelsControllerRequest { action: "deleteAsset"; diff --git a/lib/modules/model/types/ModelContent.ts b/lib/modules/model/types/ModelContent.ts index 562fd7d61..2ddc5e66c 100644 --- a/lib/modules/model/types/ModelContent.ts +++ b/lib/modules/model/types/ModelContent.ts @@ -48,6 +48,59 @@ export interface MetadataGroups { }; } +export interface MetadataTooltipContent { + category: "metadata"; + label?: { + locales: { + [locale: string]: LocaleDetails; + }; + }; + metadataPath: string; + suffix?: string; +} + +export interface MeasureTooltipContent { + category: "measure"; + label?: { + locales: { + [locale: string]: LocaleDetails; + }; + }; + measureSlot: string; + measureValuePath: string; + suffix?: string; +} + +export interface StaticTooltipContent { + category: "static"; + label?: { + locales: { + [locale: string]: LocaleDetails; + }; + }; + type: StaticTooltipContentType; + value: string; +} + +export enum StaticTooltipContentType { + link = "link", + image = "image", + text = "text", + title = "title", + separator = "separator", +} + +export interface TooltipModels { + [key: string]: { + tooltipLabel: string; + content: ( + | MetadataTooltipContent + | MeasureTooltipContent + | StaticTooltipContent + )[]; + }; +} + export interface AssetModelContent extends KDocumentContent { type: "asset"; @@ -131,6 +184,53 @@ export interface AssetModelContent extends KDocumentContent { * ] */ measures: NamedMeasures; + /** + * List of tooltip models for this asset model + * + * @example + * [ + * "defaultTooltipKey": { + * "tooltipLabel": "Default tooltip model", + * "content": [ + * { + * "metadataPath": "geolocation", + * "label": { + * "locales": { + * "en": { + * "description": "", + * "friendlyName": "Container position" + * }, + * "fr": { + * "description": "", + * "friendlyName": "Position du conteneur" + * } + * } + * }, + * "category": "metadata" + * }, + * { + * "measureValuePath": "externalTemperature", + * "measureSlot": "externalTemperature", + * "label": { + * "locales": { + * "en": { + * "description": "", + * "friendlyName": "External temperature" + * }, + * "fr": { + * "description": "", + * "friendlyName": "Température extérieure" + * } + * } + * }, + * "category": "measure", + * "suffix": "°C" + * } + * ] + * } + * ] + */ + tooltipModels?: TooltipModels; }; } diff --git a/lib/modules/model/types/ModelDefinition.ts b/lib/modules/model/types/ModelDefinition.ts index 0c50fb5ca..32a90ad05 100644 --- a/lib/modules/model/types/ModelDefinition.ts +++ b/lib/modules/model/types/ModelDefinition.ts @@ -4,6 +4,7 @@ import { MetadataDetails, MetadataGroups, MetadataMappings, + TooltipModels, } from "./ModelContent"; /** @@ -41,16 +42,57 @@ import { * metadataGroups: { * buildingEnv: { * locales: { - * en: { - * groupFriendlyName: "Building environment", - * description: "The building environment" - * }, - * fr: { - * groupFriendlyName: "Environnement du bâtiment", - * description: "L'environnement du bâtiment" - * } + * en: { + * groupFriendlyName: "Building environment", + * description: "The building environment" + * }, + * fr: { + * groupFriendlyName: "Environnement du bâtiment", + * description: "L'environnement du bâtiment" + * } * } * } + * }, + * tooltipModels: { + * "defaultTooltipKey": { + * "tooltipLabel": "Default Tooltip Model", + * "content": [ + * { + * "category": "metadata", + * "label": { + * "locales": { + * "en": { + * "friendlyName": "Container position", + * "description": "" + * }, + * "fr": { + * "friendlyName": "Position du conteneur", + * "description": "" + * } + * } + * }, + * "metadataPath": "geolocation" + * }, + * { + * "category": "measure", + * "label": { + * "locales": { + * "en": { + * "friendlyName": "External temperature", + * "description": "" + * }, + * "fr": { + * "friendlyName": "Température extérieure", + * "description": "" + * } + * } + * }, + * "measureSlot": "externalTemperature", + * "measureValuePath": "externalTemperature", + * "suffix": "°C" + * } + * ] + * } * } * } * @@ -80,6 +122,11 @@ export type AssetModelDefinition = { * Metadata groups */ metadataGroups?: MetadataGroups; + + /** + * Tooltip models + */ + tooltipModels?: TooltipModels; }; /** diff --git a/lib/modules/plugin/DeviceManagerPlugin.ts b/lib/modules/plugin/DeviceManagerPlugin.ts index 3c9313188..3078763b6 100644 --- a/lib/modules/plugin/DeviceManagerPlugin.ts +++ b/lib/modules/plugin/DeviceManagerPlugin.ts @@ -74,6 +74,7 @@ export class DeviceManagerPlugin extends Plugin { * - defaultMetadata: Default values for metadata fields, applied when actual data is not provided. * - metadataDetails: Optional detailed descriptions for each metadata, including group association and localizations. * - metadataGroups: Optional description of metadata groups, organizing metadata logically, with localizations for group names. + * - tooltipModels: Optional tooltip model list, containing each labels and tooltip content to display. * * @example * ``` @@ -119,6 +120,47 @@ export class DeviceManagerPlugin extends Plugin { * } * } * } + * }, + * tooltipModels: { + * "defaultTooltipKey": { + * "tooltipLabel": "Default Tooltip Model", + * "content": [ + * { + * "category": "metadata", + * "label": { + * "locales": { + * "en": { + * "friendlyName": "Container position", + * "description": "" + * }, + * "fr": { + * "friendlyName": "Position du conteneur", + * "description": "" + * } + * } + * }, + * "metadataPath": "geolocation" + * }, + * { + * "category": "measure", + * "label": { + * "locales": { + * "en": { + * "friendlyName": "External temperature", + * "description": "" + * }, + * "fr": { + * "friendlyName": "Température extérieure", + * "description": "" + * } + * } + * }, + * "measureSlot": "externalTemperature", + * "measureValuePath": "externalTemperature", + * "suffix": "°C" + * } + * ] + * } * } * } * ); @@ -137,6 +179,7 @@ export class DeviceManagerPlugin extends Plugin { definition.defaultMetadata, definition.metadataDetails, definition.metadataGroups, + definition.tooltipModels, ); }, diff --git a/tests/application/assets/Container.ts b/tests/application/assets/Container.ts index 32c4b2e9e..87d85638e 100644 --- a/tests/application/assets/Container.ts +++ b/tests/application/assets/Container.ts @@ -88,9 +88,49 @@ export const containerAssetDefinition: AssetModelDefinition = { } }, }, + tooltipModels: { + defaultTooltipKey: { + tooltipLabel: "Default Tooltip Model", + content: [ + { + category: "measure", + label: { + locales: { + en: { + friendlyName: "External Temperature", + description: "" + }, + fr: { + friendlyName: "Température Externe", + description: "" + } + } + }, + measureSlot: "temperatureExt", + measureValuePath: "temperatureExt" + }, + { + category: "measure", + label: { + locales: { + en: { + friendlyName: "Internal Temperature", + description: "" + }, + fr: { + friendlyName: "Température Interne", + description: "" + } + } + }, + measureSlot: "temperatureInt", + measureValuePath: "temperatureInt" + } + ] + } + } }; - // Mocked data example to match the expected type structure const temperatureMeasureExample = { payloadUuids: ["uuid1", "uuid2"], diff --git a/tests/hooks/collections.ts b/tests/hooks/collections.ts index df913c84d..d3911e27e 100644 --- a/tests/hooks/collections.ts +++ b/tests/hooks/collections.ts @@ -21,6 +21,7 @@ async function deleteModels(sdk: Kuzzle) { "model-device-Zigbee", "model-device-Enginko", "model-asset-TestHouse", + "model-asset-AdvancedWarehouse", ], }, }, diff --git a/tests/scenario/migrated/model-controller.test.ts b/tests/scenario/migrated/model-controller.test.ts index adef3eda8..515602e66 100644 --- a/tests/scenario/migrated/model-controller.test.ts +++ b/tests/scenario/migrated/model-controller.test.ts @@ -587,6 +587,105 @@ describe("features/Model/Controller", () => { }); }); + it("Write and Retrieve an Asset model with tooltip models", async () => { + const assetModelWithTooltip = { + engineGroup: "commons", + model: "AdvancedWarehouse", + metadataMappings: { + location: { type: "geo_point" }, + floor: { type: "integer" }, + }, + measures: [{ name: "temperatureInt", type: "temperature" }], + tooltipModels: { + "example-tooltip": { + tooltipLabel: "Example Tooltip", + content: [ + { category: "static", type: "title", value: "Warehouse Info" }, + { category: "metadata", metadataPath: "floor" }, + ], + }, + }, + }; + + await sdk.query({ + controller: "device-manager/models", + action: "writeAsset", + body: assetModelWithTooltip, + }); + + const response = await sdk.document.get( + "device-manager", + "models", + "model-asset-AdvancedWarehouse" + ); + expect(response._source.asset).toHaveProperty("tooltipModels"); + delete assetModelWithTooltip.engineGroup; + expect(response._source).toMatchObject({ + type: "asset", + engineGroup: "commons", + asset: assetModelWithTooltip, + }); +}); + +it("Update the tooltip models of an Asset model", async () => { + await sdk.query({ + controller: "device-manager/models", + action: "writeAsset", + body: { + engineGroup: "commons", + model: "AdvancedWarehouse", + metadataMappings: { + location: { type: "geo_point" }, + floor: { type: "integer" }, + }, + measures: [{ name: "temperatureInt", type: "temperature" }], + tooltipModels: { + "example-tooltip": { + tooltipLabel: "Example Tooltip", + content: [ + { category: "static", type: "title", value: "Warehouse Info" }, + { category: "metadata", metadataPath: "floor" }, + ], + }, + }, + } + }); + + const updatedTooltipModels = { + "example-tooltip": { + tooltipLabel: "Updated Tooltip", + content: [ + { category: "static", type: "title", value: "Updated Warehouse Info" }, + { category: "metadata", metadataPath: "location" }, + ], + }, + }; + + await sdk.query({ + controller: "device-manager/models", + action: "updateAsset", + _id: "model-asset-AdvancedWarehouse", + engineGroup: "commons", + model: "AdvancedWarehouse", + body: { + metadataMappings: { + location: { type: "geo_point" }, + floor: { type: "integer" }, + }, + measures: [{ name: "temperatureInt", type: "temperature" }], + tooltipModels: updatedTooltipModels, + } + }); + + const response = await sdk.document.get( + "device-manager", + "models", + "model-asset-AdvancedWarehouse" + ); + expect(response._source.asset).toHaveProperty("tooltipModels"); + expect(response._source.asset.tooltipModels).toEqual(updatedTooltipModels); +}); + it("Register models from the framework", async () => { let response; let promise;