Skip to content

Commit

Permalink
feat(TM-source): add property value query
Browse files Browse the repository at this point in the history
  • Loading branch information
sheilaXu committed Aug 23, 2023
1 parent e1f2357 commit 21091d9
Show file tree
Hide file tree
Showing 35 changed files with 727 additions and 231 deletions.
9 changes: 6 additions & 3 deletions packages/core/src/data-module/data-cache/requestTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ export type HistoricalViewport = { start: Date; end: Date; group?: string };

export type Viewport = DurationViewport | HistoricalViewport;

export type DataRequest = {
viewport?: Viewport;
settings?: TimeSeriesDataRequestSettings;
};
/**
* Request Information utilized by consumers of the widgets to connect the `data-provider` to their data source.
*/
export type TimeSeriesDataRequest = {
export interface TimeSeriesDataRequest extends DataRequest {
viewport: Viewport;
settings?: TimeSeriesDataRequestSettings;
};
}

export type ResolutionConfig = ResolutionMapping | string;

Expand Down
30 changes: 21 additions & 9 deletions packages/core/src/data-module/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,30 @@ export type StreamAssociation = {
};

export type Timestamp = number;
export type DataPoint<T extends Primitive = Primitive> = {
x: Timestamp;

export type DataPointBase<T extends Primitive = Primitive> = {
y: T;
};

export interface DataPoint<T extends Primitive = Primitive> extends DataPointBase<T> {
x: Timestamp;
}

export type Resolution = number;

export type Primitive = string | number | boolean;

export type DataStreamId = string;

export type TimeSeriesData = {
export type DataBase = {
dataStreams: DataStreamBase[];
};

export interface TimeSeriesData extends DataBase {
dataStreams: DataStream[];
viewport: Viewport;
thresholds: Threshold[];
};
}

// Reference which can be used to associate styles to the associated results from a query
export type RefId = string;
Expand All @@ -53,12 +61,19 @@ export type ComparisonOperator = 'LT' | 'GT' | 'LTE' | 'GTE' | 'EQ' | 'CONTAINS'

export type StatusIconType = 'error' | 'active' | 'normal' | 'acknowledged' | 'snoozed' | 'disabled' | 'latched';

export interface DataStream<T extends Primitive = Primitive> {
export interface DataStreamBase<T extends Primitive = Primitive> {
data: DataPointBase<T>[];
error?: ErrorDetails;
dataType?: DataType;
// Mechanism to associate some information about the data stream
meta?: Record<string, string | number | boolean>;
}

export interface DataStream<T extends Primitive = Primitive> extends DataStreamBase<T> {
id: DataStreamId;
data: DataPoint<T>[];
resolution: number;
aggregationType?: AggregateType;
dataType?: DataType;
refId?: string;
name?: string;
detailedName?: string;
Expand All @@ -68,9 +83,6 @@ export interface DataStream<T extends Primitive = Primitive> {
associatedStreams?: StreamAssociation[];
isLoading?: boolean;
isRefreshing?: boolean;
error?: ErrorDetails;
// Mechanism to associate some information about the data stream
meta?: Record<string, string | number | boolean>;
}

export type DataSource<Query extends DataStreamQuery = AnyDataStreamQuery> = {
Expand Down
12 changes: 10 additions & 2 deletions packages/scene-composer/src/components/StateManager.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,18 @@ describe('StateManager', () => {
});

expect(mockBuild).toBeCalledTimes(2);
expect(mockBuild).toHaveBeenNthCalledWith(2, 'default', {
viewport,
settings: { refreshRate: undefined, fetchFromStartToEnd: true },
});
expect(mockCombinedPrvider.subscribe).toBeCalledTimes(1);
expect(mockCombinedPrvider.unsubscribe).not.toBeCalled();

// unsubscribe before subscribe when queries updated
await act(async () => {
container.update(
<StateManager
viewport={viewport}
viewport={{ duration: '5m' }}
sceneLoader={mockSceneLoader}
sceneMetadataModule={mockSceneMetadataModule}
config={sceneConfig}
Expand All @@ -335,14 +339,18 @@ describe('StateManager', () => {
});

expect(mockBuild).toBeCalledTimes(3);
expect(mockBuild).toHaveBeenNthCalledWith(3, 'default', {
viewport: { duration: '5m' },
settings: { refreshRate: 5000, fetchFromStartToEnd: true },
});
expect(mockCombinedPrvider.subscribe).toBeCalledTimes(2);
expect(mockCombinedPrvider.unsubscribe).toBeCalledTimes(1);

// unsubscribe after unmount
await act(async () => {
container.unmount(
<StateManager
viewport={viewport}
viewport={{ duration: '5m' }}
sceneLoader={mockSceneLoader}
sceneMetadataModule={mockSceneMetadataModule}
config={sceneConfig}
Expand Down
10 changes: 8 additions & 2 deletions packages/scene-composer/src/components/StateManager.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { DataStream, ProviderWithViewport, TimeSeriesData, combineProviders } from '@iot-app-kit/core';
import {
DurationViewport,
DataStream,
ProviderWithViewport,
TimeSeriesData,
combineProviders,
} from '@iot-app-kit/core';
import { ThreeEvent } from '@react-three/fiber';
import ab2str from 'arraybuffer-to-string';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
Expand Down Expand Up @@ -39,7 +45,6 @@ import { combineTimeSeriesData, convertDataStreamsToDataInput } from '../utils/d
import { findComponentByType } from '../utils/nodeUtils';
import sceneDocumentSnapshotCreator from '../utils/sceneDocumentSnapshotCreator';
import { createStandardUriModifier } from '../utils/uriModifiers';
import useFeature from '../hooks/useFeature';

import IntlProvider from './IntlProvider';
import { LoadingProgress } from './three-fiber/LoadingProgress';
Expand Down Expand Up @@ -342,6 +347,7 @@ const StateManager: React.FC<SceneComposerInternalProps> = ({
settings: {
// only support default settings for now until when customization is needed
fetchFromStartToEnd: true,
refreshRate: (viewport as DurationViewport).duration ? 5000 : undefined,
},
}),
),
Expand Down
6 changes: 3 additions & 3 deletions packages/scene-composer/src/hooks/useBindingData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { TimeSeriesData, Primitive } from '@iot-app-kit/core';
import { DataBase, TimeSeriesData, Primitive, DurationViewport } from '@iot-app-kit/core';
import { isEmpty } from 'lodash';
import { ITwinMakerEntityDataBindingContext } from '@iot-app-kit/source-iottwinmaker';

Expand Down Expand Up @@ -47,12 +47,12 @@ const useBindingData = (
settings: {
// only support default settings for now until when customization is needed
fetchFromStartToEnd: true,
refreshRate: (viewport as DurationViewport).duration ? 5000 : undefined,
},
});

provider.subscribe({
// TODO: support static data
next: (results: TimeSeriesData[]) => {
next: (results: TimeSeriesData[] | DataBase[]) => {
if (isEmpty(results.at(0)?.dataStreams)) {
log?.info('No data returned');
return;
Expand Down
5 changes: 2 additions & 3 deletions packages/scene-composer/src/hooks/useBindingQueries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { TimeSeriesDataRequest, Query, TimeSeriesData } from '@iot-app-kit/core';
import { DataBase, DataRequest, Query, TimeSeriesData } from '@iot-app-kit/core';
import { isEmpty } from 'lodash';

import { useSceneComposerId } from '../common/sceneComposerIdContext';
Expand All @@ -17,8 +17,7 @@ import { applyDataBindingTemplate } from '../utils/dataBindingTemplateUtils';
*/
const useBindingQueries = (
bindings: IValueDataBinding[] | undefined,
): // TODO: update data type for static data when available
{ queries: (Query<TimeSeriesData[], TimeSeriesDataRequest> | undefined)[] | undefined } => {
): { queries: (Query<TimeSeriesData[] | DataBase[], DataRequest> | undefined)[] | undefined } => {
const sceneComposerId = useSceneComposerId();
const valueDataBindingProvider = useStore(sceneComposerId)(
(state) => state.getEditorConfig().valueDataBindingProvider,
Expand Down
4 changes: 2 additions & 2 deletions packages/scene-composer/src/interfaces/sceneViewer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SceneLoader, TwinMakerSceneMetadataModule } from '@iot-app-kit/source-iottwinmaker';
import { DataStream, TimeQuery, TimeSeriesData, TimeSeriesDataRequest, Viewport } from '@iot-app-kit/core';
import { DataStream, TimeSeriesDataQuery, Viewport } from '@iot-app-kit/core';

import { IDataBindingTemplate, ISelectedDataBinding, IValueDataBindingProvider } from './dataBinding';
import { SelectionChangedEventCallback, WidgetClickEventCallback } from './components';
Expand Down Expand Up @@ -51,7 +51,7 @@ export interface SceneViewerPropsShared {
*
* Note: Need to provide a viewport to make it work.
*/
queries?: TimeQuery<TimeSeriesData[], TimeSeriesDataRequest>[];
queries?: TimeSeriesDataQuery[];
/**
* Specifies the time range of the dataStreams or the range to trigger the queries.
*/
Expand Down
4 changes: 3 additions & 1 deletion packages/scene-composer/src/utils/dataStreamUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export const convertDataStreamsToDataInput = (streams: DataStream[], viewport: V
labels,
// the boolean value y will be a string 'true' or 'false' when data is coming from app kit data source,
// but can be actual boolean when from other source
values: stream.data.map(({ y }) => (stream.dataType === 'BOOLEAN' ? y === 'true' || (y as any) === true : y)),
values: stream.data.map(({ y }) =>
stream.dataType === 'BOOLEAN' ? y === 'true' || (y as boolean) === true : y,
),
},
];

Expand Down
8 changes: 8 additions & 0 deletions packages/source-iottwinmaker/src/__mocks__/iottwinmakerSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
ExecuteQueryCommandOutput,
GetEntityCommandInput,
GetEntityCommandOutput,
GetPropertyValueCommandInput,
GetPropertyValueCommandOutput,
GetPropertyValueHistoryCommandInput,
GetPropertyValueHistoryCommandOutput,
GetSceneCommandInput,
Expand All @@ -19,6 +21,7 @@ const nonOverriddenMock = () => Promise.reject(new Error('Mock method not overri
export const createMockTwinMakerSDK = ({
getEntity = nonOverriddenMock,
getPropertyValueHistory = nonOverriddenMock,
getPropertyValue = nonOverriddenMock,
getScene = nonOverriddenMock,
listEntities = nonOverriddenMock,
updateScene = nonOverriddenMock,
Expand All @@ -28,6 +31,9 @@ export const createMockTwinMakerSDK = ({
getPropertyValueHistory?: (
input: GetPropertyValueHistoryCommandInput
) => Promise<GetPropertyValueHistoryCommandOutput>;
getPropertyValue?: (
input: GetPropertyValueCommandInput
) => Promise<GetPropertyValueCommandOutput>;
getScene?: (input: GetSceneCommandInput) => Promise<GetSceneCommandOutput>;
listEntities?: (input: ListEntitiesCommandInput) => Promise<ListEntitiesCommandOutput>;
updateScene?: (input: UpdateSceneCommandInput) => Promise<UpdateSceneCommandOutput>;
Expand All @@ -44,6 +50,8 @@ export const createMockTwinMakerSDK = ({
return getEntity(command.input);
case 'GetPropertyValueHistoryCommand':
return getPropertyValueHistory(command.input);
case 'GetPropertyValueCommand':
return getPropertyValue(command.input);
case 'GetSceneCommand':
return getScene(command.input);
case 'ListEntitiesCommand':
Expand Down
29 changes: 29 additions & 0 deletions packages/source-iottwinmaker/src/common/queryTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { RefId } from '@iot-app-kit/core';

export type PropertyQueryInfo = {
propertyName: string;

refId?: RefId;
};

export type TwinMakerBaseQuery = {
properties: PropertyQueryInfo[];
};

// Query for static property value
export interface TwinMakerEntityPropertyValueQuery extends TwinMakerBaseQuery {
entityId: string;
componentName: string;
}

// Query for time series property value
export type TwinMakerEntityHistoryQuery = TwinMakerEntityPropertyValueQuery;

// Query for time series property value for a component type
export interface TwinMakerComponentHistoryQuery extends TwinMakerBaseQuery {
componentTypeId: string;
}

export type TwinMakerHistoryQuery = TwinMakerEntityHistoryQuery | TwinMakerComponentHistoryQuery;
export type TwinMakerPropertyValueQuery = TwinMakerEntityPropertyValueQuery;
export type TwinMakerQuery = TwinMakerHistoryQuery | TwinMakerPropertyValueQuery;
3 changes: 1 addition & 2 deletions packages/source-iottwinmaker/src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
// TODO: Use the Primitive type used by other app kit datasource
export declare type Primitive = string | number | boolean;
export type { Primitive } from '@iot-app-kit/core';
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,44 @@ describe('createEntityPropertyBindingProvider', () => {
});

it('should return a correct data binding provider store', async () => {
const provider = createEntityPropertyBindingProvider({ metadataModule, timeSeriesDataQuery: jest.fn() });
const provider = createEntityPropertyBindingProvider({
metadataModule,
timeSeriesDataQuery: jest.fn(),
propertyValueQuery: jest.fn(),
});
const store = provider.createStore(false);
expect(store).toBeDefined();
expect((store as EntityPropertyBindingProviderStore)['metadataModule']).toBe(metadataModule);
});

it('should return query when data binding context has all values', async () => {
it('should return time series query when data binding context has all values', async () => {
const query = { key: 'value' };
const provider = createEntityPropertyBindingProvider({
metadataModule,
timeSeriesDataQuery: jest.fn().mockReturnValue(query),
propertyValueQuery: jest.fn(),
});
const result = provider.createQuery(mockDataBindingInput);
expect(result).toBe(query);
});

it('should return static query when data binding context has all values', async () => {
const query = { key: 'value' };
const provider = createEntityPropertyBindingProvider({
metadataModule,
timeSeriesDataQuery: jest.fn().mockReturnValue({ random: 'abc' }),
propertyValueQuery: jest.fn().mockReturnValue(query),
});
const result = provider.createQuery({ ...mockDataBindingInput, isStaticData: true });
expect(result).toBe(query);
});

it('should not return query when data binding context misses property name', async () => {
const query = { key: 'value' };
const provider = createEntityPropertyBindingProvider({
metadataModule,
timeSeriesDataQuery: jest.fn().mockReturnValue(query),
propertyValueQuery: jest.fn(),
});
const result = provider.createQuery({
dataBindingContext: { ...mockDataBindingInput.dataBindingContext, propertyName: undefined },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { TwinMakerErrorCode } from '../common/error';
import { ITwinMakerEntityDataBindingContext, IValueDataBinding, IValueDataBindingProvider } from './types';
import { EntityPropertyBindingProviderStore } from './EntityPropertyBindingProviderStore';
import { ErrorDetails, TimeSeriesDataQuery } from '@iot-app-kit/core';
import { TwinMakerQuery } from '../time-series-data/types';
import { ErrorDetails, Query, TimeSeriesDataQuery, DataBase, DataRequest } from '@iot-app-kit/core';
import { TwinMakerHistoryQuery, TwinMakerPropertyValueQuery } from '../common/queryTypes';
import { TwinMakerMetadataModule } from '../metadata-module/TwinMakerMetadataModule';

export const createEntityPropertyBindingProvider = ({
metadataModule,
timeSeriesDataQuery,
propertyValueQuery,
onError,
}: {
metadataModule: TwinMakerMetadataModule;
timeSeriesDataQuery: (query: TwinMakerQuery) => TimeSeriesDataQuery;
timeSeriesDataQuery: (query: TwinMakerHistoryQuery) => TimeSeriesDataQuery;
propertyValueQuery: (query: TwinMakerPropertyValueQuery) => Query<DataBase[], DataRequest>;
onError?: (errorCode: TwinMakerErrorCode, errorDetails?: ErrorDetails) => void;
}): IValueDataBindingProvider => {
return {
Expand All @@ -21,16 +23,18 @@ export const createEntityPropertyBindingProvider = ({
metadataModule,
onError,
}),
// TODO: add non time series data support
createQuery: (dataBinding: IValueDataBinding) => {
const context = dataBinding.dataBindingContext as ITwinMakerEntityDataBindingContext;
if (!context || !context.entityId || !context.componentName || !context.propertyName) {
return undefined;
}

if (dataBinding.isStaticData) {
// TODO: return property value query
return undefined;
return propertyValueQuery({
entityId: context.entityId,
componentName: context.componentName,
properties: [{ propertyName: context.propertyName }],
});
}

return timeSeriesDataQuery({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TimeSeriesDataRequest, Query, TimeSeriesData } from '@iot-app-kit/core';
import { DataBase, DataRequest, Query, TimeSeriesData } from '@iot-app-kit/core';

export interface ITwinMakerEntityDataBindingContext {
entityId: string;
Expand Down Expand Up @@ -75,7 +75,7 @@ export interface IValueDataBindingStore {

export interface IValueDataBindingProvider {
createStore(isDataBindingTemplateProvider: boolean): IValueDataBindingStore;
createQuery(dataBinding: IValueDataBinding): Query<TimeSeriesData[], TimeSeriesDataRequest> | undefined; // TODO: add non time series data support
createQuery(dataBinding: IValueDataBinding): Query<TimeSeriesData[] | DataBase[], DataRequest> | undefined;
}

/************************************************
Expand Down
Loading

0 comments on commit 21091d9

Please sign in to comment.