Skip to content

Commit

Permalink
feat: Add Asset Hierarchy loading & Asset Tree support
Browse files Browse the repository at this point in the history
The PR allows access to the Asset Hierarchy as a tree
  • Loading branch information
gareth-amazon committed Dec 10, 2021
1 parent b4fde90 commit 6adc67e
Show file tree
Hide file tree
Showing 21 changed files with 1,221 additions and 96 deletions.
19 changes: 18 additions & 1 deletion packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { AnyDataStreamQuery, AssetSummaryQuery, DataModule, Request } from "@iot-app-kit/core";
import { AnyDataStreamQuery, AssetSummaryQuery, AssetTreeSubscription, DataModule, Request, SiteWiseAssetTreeQuery } from "@iot-app-kit/core";
import { DataStream, MinimalViewPortConfig } from "@synchro-charts/core";
import { TableProps } from "@awsui/components-react/table";
import { EmptyStateProps, UseTreeCollection } from "@iot-app-kit/related-table";
Expand All @@ -14,6 +14,10 @@ export namespace Components {
interface IotAssetDetails {
"query": AssetSummaryQuery;
}
interface IotAssetTreeDemo {
"query": SiteWiseAssetTreeQuery;
"subscription": AssetTreeSubscription;
}
interface IotBarChart {
"appKit": DataModule;
"isEditing": boolean | undefined;
Expand Down Expand Up @@ -103,6 +107,12 @@ declare global {
prototype: HTMLIotAssetDetailsElement;
new (): HTMLIotAssetDetailsElement;
};
interface HTMLIotAssetTreeDemoElement extends Components.IotAssetTreeDemo, HTMLStencilElement {
}
var HTMLIotAssetTreeDemoElement: {
prototype: HTMLIotAssetTreeDemoElement;
new (): HTMLIotAssetTreeDemoElement;
};
interface HTMLIotBarChartElement extends Components.IotBarChart, HTMLStencilElement {
}
var HTMLIotBarChartElement: {
Expand Down Expand Up @@ -177,6 +187,7 @@ declare global {
};
interface HTMLElementTagNameMap {
"iot-asset-details": HTMLIotAssetDetailsElement;
"iot-asset-tree-demo": HTMLIotAssetTreeDemoElement;
"iot-bar-chart": HTMLIotBarChartElement;
"iot-connector": HTMLIotConnectorElement;
"iot-kpi": HTMLIotKpiElement;
Expand All @@ -195,6 +206,10 @@ declare namespace LocalJSX {
interface IotAssetDetails {
"query"?: AssetSummaryQuery;
}
interface IotAssetTreeDemo {
"query"?: SiteWiseAssetTreeQuery;
"subscription"?: AssetTreeSubscription;
}
interface IotBarChart {
"appKit"?: DataModule;
"isEditing"?: boolean | undefined;
Expand Down Expand Up @@ -278,6 +293,7 @@ declare namespace LocalJSX {
}
interface IntrinsicElements {
"iot-asset-details": IotAssetDetails;
"iot-asset-tree-demo": IotAssetTreeDemo;
"iot-bar-chart": IotBarChart;
"iot-connector": IotConnector;
"iot-kpi": IotKpi;
Expand All @@ -297,6 +313,7 @@ declare module "@stencil/core" {
export namespace JSX {
interface IntrinsicElements {
"iot-asset-details": LocalJSX.IotAssetDetails & JSXBase.HTMLAttributes<HTMLIotAssetDetailsElement>;
"iot-asset-tree-demo": LocalJSX.IotAssetTreeDemo & JSXBase.HTMLAttributes<HTMLIotAssetTreeDemoElement>;
"iot-bar-chart": LocalJSX.IotBarChart & JSXBase.HTMLAttributes<HTMLIotBarChartElement>;
"iot-connector": LocalJSX.IotConnector & JSXBase.HTMLAttributes<HTMLIotConnectorElement>;
"iot-kpi": LocalJSX.IotKpi & JSXBase.HTMLAttributes<HTMLIotKpiElement>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Component, h, Prop, State, Watch } from '@stencil/core';
import {
getSiteWiseAssetModule,
SiteWiseAssetTreeModule,
SiteWiseAssetTreeQuery,
SiteWiseAssetTreeNode,
SiteWiseAssetTreeSession,
BranchReference,
AssetTreeSubscription,
HierarchyGroup,
} from '@iot-app-kit/core';

@Component({
tag: 'iot-asset-tree-demo',
shadow: false,
})
export class IotAssetTreeDemo {
@Prop() query: SiteWiseAssetTreeQuery;
@Prop() subscription: AssetTreeSubscription;
@State() roots: SiteWiseAssetTreeNode[] = [];

componentDidLoad() {
// TODO: this needs to be done elsewhere...
let session: SiteWiseAssetTreeSession = new SiteWiseAssetTreeModule(getSiteWiseAssetModule()).startSession(
this.query
);
this.subscription = session.subscribe((newTree) => {
this.roots = newTree;
// check the tree for any new unexpanded nodes and expand them:
this.expandNodes(newTree);
});
}

expandNodes(nodes: SiteWiseAssetTreeNode[]) {
nodes.forEach((node) => {
Array.from(node.hierarchies.values()).forEach((hierarchyGroup) => {
if (!hierarchyGroup.isExpanded) {
this.subscription.expand(new BranchReference(node.asset.id, hierarchyGroup.id));
}
this.expandNodes(hierarchyGroup.children);
});
});
}

componentWillUnmount() {
this.subscription.unsubscribe();
}

render() {
return (
<div>
<h1>Tree Demo</h1>
{this.renderAssetList(this.roots)}
</div>
);
}

renderAssetList(assets: SiteWiseAssetTreeNode[]) {
if (!assets) {
return '';
}

return <ul>{assets.map((asset) => this.renderAsset(asset))}</ul>;
}

renderAsset(assetNode: SiteWiseAssetTreeNode) {
return (
<li key={'asset-' + assetNode.asset?.id}>
{assetNode.asset?.name}
{this.renderHierarchies(assetNode)}
</li>
);
}

renderHierarchies(node: SiteWiseAssetTreeNode) {
if (!node.hierarchies || !node.hierarchies.size) {
return;
}

return <ul>{Array.from(node.hierarchies.values()).map((hierarchy) => this.renderHierarchy(hierarchy))}</ul>;
}

renderHierarchy(hierarchy: HierarchyGroup) {
if (hierarchy.children) {
return (
<li key={'hierarchy-' + hierarchy.id}>
{hierarchy.name}
{this.renderAssetList(hierarchy?.children)}
</li>
);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const STRING_ASSET_ID = '888dbcd1-cdfe-44ba-a99b-0ad3ca19a019';
const STRING_PROPERTY_ID = '9bd13790-377b-429f-87b0-43382b1709fd';
const STRING_ASSET_ID = 'ab94a0c7-7546-4dc6-9e25-a248f242b362';
const STRING_PROPERTY_ID = '2b3f10ae-dee5-44a9-9a91-a801ee52c854';

export const DEMO_TURBINE_ASSET_1 = '25963bcd-cde2-44ef-8e59-7b54da426409';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export class TestingGround {
</div>
</div>
<iot-asset-details query={ASSET_DETAILS_QUERY} />
<iot-asset-tree-demo query={{ rootAssetId: undefined }} />
<sc-webgl-context />
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/asset-modules/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from './sitewise/types';
export * from './sitewise/siteWiseAssetModule';
export * from './sitewise/session';
export * from './sitewise-asset-tree/types';
export * from './sitewise-asset-tree/assetTreeModule';
export * from './sitewise-asset-tree/assetTreeSession';
167 changes: 167 additions & 0 deletions packages/core/src/asset-modules/mocks.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import {
AssetHierarchyQuery, assetHierarchyQueryKey,
AssetModelQuery,
AssetPropertyValueQuery,
AssetSummaryQuery,
HierarchyAssetSummaryList,
isAssetHierarchyQuery,
isAssetModelQuery,
isAssetPropertyValueQuery, isAssetSummaryQuery,
SiteWiseAssetModuleInterface,
SiteWiseAssetSessionInterface
} from './sitewise/types';
import { AssetState, DescribeAssetModelResponse, DescribeAssetResponse, Quality } from '@aws-sdk/client-iotsitewise';
import { Observable, Subscription } from 'rxjs';
import { AssetPropertyValue, AssetSummary } from '@aws-sdk/client-iotsitewise/dist-types/ts3.4';

export const ASSET_ID = 'assetABC123';
export const ASSET_MODEL_ID = 'assetModelABC123';
export const ASSET_PROPERTY_ID = 'assetPropertyIdAbc123';
export const HIERARCHY_ID = 'hierarchyIdAbc123';
export const creationDate: Date = new Date(2000, 0, 0);
export const lastUpdatedDate: Date = new Date(2021, 0, 0);
export const sampleAssetSummary: AssetSummary = {
id: ASSET_ID,
assetModelId: ASSET_MODEL_ID,
name: 'assetName',
arn: 'arn:assetArn',
creationDate: creationDate,
lastUpdateDate: lastUpdatedDate,
hierarchies: [],
status: {
error: {
code: undefined,
details: undefined,
message: undefined,
},
state: AssetState.ACTIVE,
},
};
export const sampleAssetDescription: DescribeAssetResponse = {
assetId: ASSET_ID,
assetModelId: ASSET_MODEL_ID,
assetName: 'assetName',
assetArn: 'arn:assetArn',
assetCreationDate: creationDate,
assetLastUpdateDate: lastUpdatedDate,
assetHierarchies: [],
assetStatus: {
error: {
code: undefined,
details: undefined,
message: undefined,
},
state: AssetState.ACTIVE,
},
assetCompositeModels: [],
assetProperties: [],
};
export const sampleAssetModel: DescribeAssetModelResponse = {
assetModelId: ASSET_MODEL_ID,
assetModelName: 'Asset Model Name',
assetModelDescription: 'a happy little asset model',
assetModelArn: 'arn:assetModelArn',
assetModelCreationDate: creationDate,
assetModelLastUpdateDate: lastUpdatedDate,
assetModelProperties: [],
assetModelCompositeModels: [],
assetModelHierarchies: [],
assetModelStatus: {
error: {
code: undefined,
details: undefined,
message: undefined,
},
state: AssetState.ACTIVE,
},
};
export const samplePropertyValue: AssetPropertyValue = {
value: { stringValue: undefined, booleanValue: undefined, doubleValue: undefined, integerValue: 1234 },
quality: Quality.GOOD,
timestamp: {
timeInSeconds: 100,
offsetInNanos: 100,
},
};

export class MockSiteWiseAssetsReplayData {
public models: Map<string, DescribeAssetModelResponse> = new Map<string, DescribeAssetModelResponse>();
public hierarchies: Map<string, HierarchyAssetSummaryList> = new Map<string, HierarchyAssetSummaryList>();
public properties: Map<string, AssetPropertyValue> = new Map<string, AssetPropertyValue>();
public assets: Map<string, AssetSummary> = new Map<string, AssetSummary>();

public addAssetModels(newModels: DescribeAssetModelResponse[]) {
newModels.forEach(model => this.models.set(model.assetModelId as string, model));
}

public addAssetSummaries(newAssetSummaries: AssetSummary[]) {
newAssetSummaries.forEach(summary => this.assets.set(summary.id as string, summary));
}

public addAssetPropertyValues(propertyValue: {assetId: string, propertyId: string, value: AssetPropertyValue}) {
this.properties.set(propertyValue.assetId + ':' + propertyValue.propertyId, propertyValue.value);
}

public addHierarchyAssetSummaryList(query: AssetHierarchyQuery, newHierarchyAssetSummaryList: HierarchyAssetSummaryList) {
this.hierarchies.set(assetHierarchyQueryKey(query), newHierarchyAssetSummaryList);
}
}


export class MockSiteWiseAssetSession implements SiteWiseAssetSessionInterface {
private readonly replayData: MockSiteWiseAssetsReplayData;

constructor(replayData: MockSiteWiseAssetsReplayData) {
this.replayData = replayData;
}

addRequest(query: AssetModelQuery, observer: (assetModel: DescribeAssetModelResponse) => void): Subscription;
addRequest(query: AssetPropertyValueQuery, observer: (assetPropertyValue: AssetPropertyValue) => void): Subscription;
addRequest(query: AssetHierarchyQuery, observer: (assetSummary: HierarchyAssetSummaryList) => void): Subscription;
addRequest(query: AssetSummaryQuery, observer: (assetSummary: AssetSummary) => void): Subscription;
addRequest(query: AssetModelQuery | AssetPropertyValueQuery | AssetHierarchyQuery | AssetSummaryQuery,
observer: ((assetModel: DescribeAssetModelResponse) => void)
| ((assetPropertyValue: AssetPropertyValue) => void)
| ((assetSummary: HierarchyAssetSummaryList) => void)
| ((assetSummary: AssetSummary) => void)): Subscription {
let observable: Observable<any>;
if (isAssetModelQuery(query)) {
observable = new Observable<DescribeAssetModelResponse>((observer) => {
observer.next(this.replayData.models.get(query.assetModelId));
});
} else if (isAssetPropertyValueQuery(query)) {
observable = new Observable<AssetPropertyValue>((observer) => {
observer.next(this.replayData.properties.get(query.assetId + ':' + query.propertyId));
});
} else if (isAssetHierarchyQuery(query)) {
observable = new Observable<HierarchyAssetSummaryList>((observer) => {
observer.next(this.replayData.hierarchies.get(assetHierarchyQueryKey(query)));
});
} else if (isAssetSummaryQuery(query)) {
observable = new Observable<AssetSummary>((observer) => {
observer.next(this.replayData.assets.get(query.assetId));
});
} else {
throw 'Unexpected request type: the type of the request object could not be determined';
}

return observable.subscribe(observer);
}

close(): void {
}
}

export class MockSiteWiseAssetModule implements SiteWiseAssetModuleInterface {
private readonly replayData: MockSiteWiseAssetsReplayData;

constructor(replayData: MockSiteWiseAssetsReplayData) {
this.replayData = replayData;
}

startSession(): SiteWiseAssetSessionInterface {
return new MockSiteWiseAssetSession(this.replayData);
}
}

it('no-op', () => { expect(true).toBeTruthy()});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { SiteWiseAssetTreeModule } from './assetTreeModule';
import { MockSiteWiseAssetModule, MockSiteWiseAssetsReplayData, sampleAssetSummary } from '../mocks.spec';
import { HIERARCHY_ROOT_ID, HierarchyAssetSummaryList, LoadingStateEnum } from '../sitewise/types';

it('initializes', () => {
expect(
() =>
new SiteWiseAssetTreeModule(new MockSiteWiseAssetModule(new MockSiteWiseAssetsReplayData()))
).not.toThrowError();
});

it('returns a session', () => {
let replayData = new MockSiteWiseAssetsReplayData();
let testData:HierarchyAssetSummaryList = {
assetHierarchyId: HIERARCHY_ROOT_ID,
assets: [sampleAssetSummary],
loadingState: LoadingStateEnum.LOADED
}
replayData.addHierarchyAssetSummaryList({assetHierarchyId: HIERARCHY_ROOT_ID}, testData);
replayData.addAssetSummaries([sampleAssetSummary]);
expect(
() =>
new SiteWiseAssetTreeModule(new MockSiteWiseAssetModule(replayData))
.startSession({rootAssetId: undefined})
).not.toBeUndefined();
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SiteWiseAssetTreeQuery } from './types';
import { SiteWiseAssetModule } from '../sitewise/siteWiseAssetModule';
import { SiteWiseAssetTreeSession } from './assetTreeSession';
import { SiteWiseAssetModuleInterface } from '../sitewise/types';

export class SiteWiseAssetTreeModule {
private assetModule: SiteWiseAssetModuleInterface;

constructor(assetModule: SiteWiseAssetModuleInterface) {
this.assetModule = assetModule;
}

public startSession(query: SiteWiseAssetTreeQuery) {
return new SiteWiseAssetTreeSession(this.assetModule.startSession(), query);
}
}

0 comments on commit 6adc67e

Please sign in to comment.