diff --git a/.prettierignore b/.prettierignore index ed2c32b33..9e4ed66c8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,5 +2,7 @@ node_modules www build dist +**/build/* +**/dist/* coverage packages/components/loader/* diff --git a/package.json b/package.json index eba37c2f4..578e3d106 100755 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "fix": "npm-run-all -p fix:prettier fix:eslint", "fix:eslint": "eslint --fix --ext .js,.ts,.tsx .", "fix:prettier": "prettier --write packages/**/*.{ts,tsx,js,json}", - "test": "lerna run test --stream --concurrency 1", + "test": "npm-run-all -p test:unit", + "test:unit": "lerna run test --stream --concurrency 1", "pack": "lerna run pack" }, "devDependencies": { diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index b59b40730..2d6bda095 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -5,55 +5,62 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { AssetSummaryQuery, DataStreamQuery, Request } from "@iot-app-kit/core"; +import { AnyDataStreamQuery, AssetSummaryQuery, DataModule, Request } from "@iot-app-kit/core"; import { DataStream, MinimalViewPortConfig } from "@synchro-charts/core"; export namespace Components { interface IotAssetDetails { "query": AssetSummaryQuery; } interface IotBarChart { + "appKit": DataModule; "isEditing": boolean | undefined; - "query": DataStreamQuery; + "query": AnyDataStreamQuery; "viewport": MinimalViewPortConfig; "widgetId": string; } interface IotConnector { - "query": DataStreamQuery; + "appKit": DataModule; + "query": AnyDataStreamQuery; "renderFunc": ({ dataStreams }: { dataStreams: DataStream[] }) => unknown; "requestInfo": Request; } interface IotKpi { + "appKit": DataModule; "isEditing": boolean | undefined; - "query": DataStreamQuery; + "query": AnyDataStreamQuery; "viewport": MinimalViewPortConfig; "widgetId": string; } interface IotLineChart { + "appKit": DataModule; "isEditing": boolean | undefined; - "query": DataStreamQuery; + "query": AnyDataStreamQuery; "viewport": MinimalViewPortConfig; "widgetId": string; } interface IotScatterChart { + "appKit": DataModule; "isEditing": boolean | undefined; - "query": DataStreamQuery; + "query": AnyDataStreamQuery; "viewport": MinimalViewPortConfig; "widgetId": string; } interface IotStatusGrid { + "appKit": DataModule; "isEditing": boolean | undefined; - "query": DataStreamQuery; + "query": AnyDataStreamQuery; "viewport": MinimalViewPortConfig; "widgetId": string; } interface IotStatusTimeline { + "appKit": DataModule; "isEditing": boolean | undefined; - "query": DataStreamQuery; + "query": AnyDataStreamQuery; "viewport": MinimalViewPortConfig; "widgetId": string; } interface IotTable { - "query": DataStreamQuery; + "query": AnyDataStreamQuery; "viewport": MinimalViewPortConfig; "widgetId": string; } @@ -148,48 +155,55 @@ declare namespace LocalJSX { "query"?: AssetSummaryQuery; } interface IotBarChart { + "appKit"?: DataModule; "isEditing"?: boolean | undefined; - "query"?: DataStreamQuery; + "query"?: AnyDataStreamQuery; "viewport"?: MinimalViewPortConfig; "widgetId"?: string; } interface IotConnector { - "query"?: DataStreamQuery; + "appKit"?: DataModule; + "query"?: AnyDataStreamQuery; "renderFunc"?: ({ dataStreams }: { dataStreams: DataStream[] }) => unknown; "requestInfo"?: Request; } interface IotKpi { + "appKit"?: DataModule; "isEditing"?: boolean | undefined; - "query"?: DataStreamQuery; + "query"?: AnyDataStreamQuery; "viewport"?: MinimalViewPortConfig; "widgetId"?: string; } interface IotLineChart { + "appKit"?: DataModule; "isEditing"?: boolean | undefined; - "query"?: DataStreamQuery; + "query"?: AnyDataStreamQuery; "viewport"?: MinimalViewPortConfig; "widgetId"?: string; } interface IotScatterChart { + "appKit"?: DataModule; "isEditing"?: boolean | undefined; - "query"?: DataStreamQuery; + "query"?: AnyDataStreamQuery; "viewport"?: MinimalViewPortConfig; "widgetId"?: string; } interface IotStatusGrid { + "appKit"?: DataModule; "isEditing"?: boolean | undefined; - "query"?: DataStreamQuery; + "query"?: AnyDataStreamQuery; "viewport"?: MinimalViewPortConfig; "widgetId"?: string; } interface IotStatusTimeline { + "appKit"?: DataModule; "isEditing"?: boolean | undefined; - "query"?: DataStreamQuery; + "query"?: AnyDataStreamQuery; "viewport"?: MinimalViewPortConfig; "widgetId"?: string; } interface IotTable { - "query"?: DataStreamQuery; + "query"?: AnyDataStreamQuery; "viewport"?: MinimalViewPortConfig; "widgetId"?: string; } diff --git a/packages/components/src/components/iot-bar-chart/iot-bar-chart.spec.ts b/packages/components/src/components/iot-bar-chart/iot-bar-chart.spec.ts index 2976ae53e..925177adb 100644 --- a/packages/components/src/components/iot-bar-chart/iot-bar-chart.spec.ts +++ b/packages/components/src/components/iot-bar-chart/iot-bar-chart.spec.ts @@ -2,7 +2,7 @@ import { newSpecPage } from '@stencil/core/testing'; import { MinimalLiveViewport } from '@synchro-charts/core'; import { IotBarChart } from './iot-bar-chart'; import { Components } from '../../components.d'; -import { getDataModule, initialize, SiteWiseDataStreamQuery } from '@iot-app-kit/core'; +import { registerDataSource, initialize, SiteWiseDataStreamQuery } from '@iot-app-kit/core'; import { createMockSource } from '../../testing/createMockSource'; import { DATA_STREAM } from '../../testing/mockWidgetProperties'; import { IotConnector } from '../iot-connector/iot-connector'; @@ -14,8 +14,8 @@ const viewport: MinimalLiveViewport = { }; const barChartSpecPage = async (propOverrides: Partial = {}) => { - initialize({ registerDataSources: false }); - getDataModule().registerDataSource(createMockSource([DATA_STREAM])); + const appKit = initialize({ registerDataSources: false }); + registerDataSource(appKit, createMockSource([DATA_STREAM])); const page = await newSpecPage({ components: [IotBarChart, IotConnector], @@ -24,6 +24,7 @@ const barChartSpecPage = async (propOverrides: Partial = }); const barChart = page.doc.createElement('iot-bar-chart') as CustomHTMLElement; const props: Partial = { + appKit, widgetId: 'test-bar-chart-widget', isEditing: false, query: { diff --git a/packages/components/src/components/iot-bar-chart/iot-bar-chart.tsx b/packages/components/src/components/iot-bar-chart/iot-bar-chart.tsx index 3490a8d06..9faa5c9bd 100644 --- a/packages/components/src/components/iot-bar-chart/iot-bar-chart.tsx +++ b/packages/components/src/components/iot-bar-chart/iot-bar-chart.tsx @@ -1,6 +1,6 @@ import { Component, Prop, h } from '@stencil/core'; import { MinimalViewPortConfig } from '@synchro-charts/core'; -import { DataStreamQuery, Request } from '@iot-app-kit/core'; +import { AnyDataStreamQuery, DataModule, Request } from '@iot-app-kit/core'; const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; @@ -9,7 +9,9 @@ const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; shadow: false, }) export class IotBarChart { - @Prop() query: DataStreamQuery; + @Prop() appKit: DataModule; + + @Prop() query: AnyDataStreamQuery; @Prop() viewport: MinimalViewPortConfig = DEFAULT_VIEWPORT; @@ -28,6 +30,7 @@ export class IotBarChart { const requestInfo = this.requestInfo(); return ( ( diff --git a/packages/components/src/components/iot-connector/iot-connector.spec.ts b/packages/components/src/components/iot-connector/iot-connector.spec.ts index a8473cac2..515519a6b 100644 --- a/packages/components/src/components/iot-connector/iot-connector.spec.ts +++ b/packages/components/src/components/iot-connector/iot-connector.spec.ts @@ -1,7 +1,7 @@ import { newSpecPage } from '@stencil/core/testing'; import { MinimalLiveViewport } from '@synchro-charts/core'; import flushPromises from 'flush-promises'; -import { getDataModule, initialize, SiteWiseDataStreamQuery } from '@iot-app-kit/core'; +import { registerDataSource, initialize, SiteWiseDataStreamQuery } from '@iot-app-kit/core'; import { IotConnector } from './iot-connector'; import { createMockSource } from '../../testing/createMockSource'; import { update } from '../../testing/update'; @@ -16,8 +16,8 @@ const viewport: MinimalLiveViewport = { const connectorSpecPage = async (propOverrides: Partial = {}) => { /** Initialize data source and register mock data source */ - initialize({ registerDataSources: false }); - getDataModule().registerDataSource(createMockSource([DATA_STREAM])); + const appKit = initialize({ registerDataSources: false }); + registerDataSource(appKit, createMockSource([DATA_STREAM])); const page = await newSpecPage({ components: [IotConnector], @@ -26,6 +26,7 @@ const connectorSpecPage = async (propOverrides: Partial }); const connector = page.doc.createElement('iot-connector') as CustomHTMLElement; const props: Partial = { + appKit, query: { source: 'test-mock', assets: [], diff --git a/packages/components/src/components/iot-connector/iot-connector.tsx b/packages/components/src/components/iot-connector/iot-connector.tsx index f1d6118b0..34509a692 100644 --- a/packages/components/src/components/iot-connector/iot-connector.tsx +++ b/packages/components/src/components/iot-connector/iot-connector.tsx @@ -1,14 +1,16 @@ import { Component, Prop, State, Watch } from '@stencil/core'; import { DataStream } from '@synchro-charts/core'; import isEqual from 'lodash.isequal'; -import { getDataModule, Request, DataStreamQuery, SubscriptionUpdate } from '@iot-app-kit/core'; +import { Request, AnyDataStreamQuery, SubscriptionUpdate, subscribeToDataStreams, DataModule } from '@iot-app-kit/core'; @Component({ tag: 'iot-connector', shadow: false, }) export class IotConnector { - @Prop() query: DataStreamQuery; + @Prop() appKit: DataModule; + + @Prop() query: AnyDataStreamQuery; @Prop() requestInfo: Request; @@ -16,13 +18,14 @@ export class IotConnector { @State() dataStreams: DataStream[] = []; - private update: (subscriptionUpdate: SubscriptionUpdate) => void; + private update: (subscriptionUpdate: SubscriptionUpdate) => void; private unsubscribe: () => void; componentWillLoad() { // Subscribe to data module for the requested `query` - const { update, unsubscribe } = getDataModule().subscribeToDataStreams( + const { update, unsubscribe } = subscribeToDataStreams( + this.appKit, { query: this.query, requestInfo: this.requestInfo, diff --git a/packages/components/src/components/iot-kpi/iot-kpi.spec.ts b/packages/components/src/components/iot-kpi/iot-kpi.spec.ts index 5d354305a..fa3e00b64 100644 --- a/packages/components/src/components/iot-kpi/iot-kpi.spec.ts +++ b/packages/components/src/components/iot-kpi/iot-kpi.spec.ts @@ -1,5 +1,5 @@ import { newSpecPage } from '@stencil/core/testing'; -import { getDataModule, SiteWiseDataStreamQuery, initialize } from '@iot-app-kit/core'; +import { registerDataSource, SiteWiseDataStreamQuery, initialize } from '@iot-app-kit/core'; import { MinimalLiveViewport } from '@synchro-charts/core'; import { IotKpi } from './iot-kpi'; import { Components } from '../../components.d'; @@ -14,8 +14,8 @@ const viewport: MinimalLiveViewport = { }; const kpiSpecPage = async (propOverrides: Partial = {}) => { - initialize({ registerDataSources: false }); - getDataModule().registerDataSource(createMockSource([DATA_STREAM])); + const appKit = initialize({ registerDataSources: false }); + registerDataSource(appKit, createMockSource([DATA_STREAM])); const page = await newSpecPage({ components: [IotKpi, IotConnector], @@ -24,6 +24,7 @@ const kpiSpecPage = async (propOverrides: Partial = {}) => { }); const kpi = page.doc.createElement('iot-kpi') as CustomHTMLElement; const props: Partial = { + appKit, widgetId: 'test-kpi-widget', isEditing: false, query: { diff --git a/packages/components/src/components/iot-kpi/iot-kpi.tsx b/packages/components/src/components/iot-kpi/iot-kpi.tsx index 0acb9ee76..0d9e889bd 100644 --- a/packages/components/src/components/iot-kpi/iot-kpi.tsx +++ b/packages/components/src/components/iot-kpi/iot-kpi.tsx @@ -1,6 +1,6 @@ import { Component, Prop, h } from '@stencil/core'; import { MinimalViewPortConfig } from '@synchro-charts/core'; -import { DataStreamQuery } from '@iot-app-kit/core'; +import { AnyDataStreamQuery, DataModule } from '@iot-app-kit/core'; const DEFAULT_VIEWPORT = { duration: 10 * 1000 }; @@ -9,7 +9,9 @@ const DEFAULT_VIEWPORT = { duration: 10 * 1000 }; shadow: false, }) export class IotKpi { - @Prop() query: DataStreamQuery; + @Prop() appKit: DataModule; + + @Prop() query: AnyDataStreamQuery; @Prop() viewport: MinimalViewPortConfig = DEFAULT_VIEWPORT; @@ -28,6 +30,7 @@ export class IotKpi { const requestInfo = this.requestInfo(); return ( ( diff --git a/packages/components/src/components/iot-line-chart/iot-line-chart.spec.ts b/packages/components/src/components/iot-line-chart/iot-line-chart.spec.ts index 9166abad2..e52a197b1 100644 --- a/packages/components/src/components/iot-line-chart/iot-line-chart.spec.ts +++ b/packages/components/src/components/iot-line-chart/iot-line-chart.spec.ts @@ -1,7 +1,7 @@ import { newSpecPage } from '@stencil/core/testing'; import { MinimalLiveViewport } from '@synchro-charts/core'; import { IotLineChart } from './iot-line-chart'; -import { getDataModule } from '@iot-app-kit/core/src/data-module'; +import { registerDataSource } from '@iot-app-kit/core'; import { Components } from '../../components.d'; import { createMockSource } from '../../testing/createMockSource'; import { DATA_STREAM } from '../../testing/mockWidgetProperties'; @@ -15,8 +15,8 @@ const viewport: MinimalLiveViewport = { }; const lineChartSpecPage = async (propOverrides: Partial = {}) => { - initialize({ registerDataSources: false }); - getDataModule().registerDataSource(createMockSource([DATA_STREAM])); + const appKit = initialize({ registerDataSources: false }); + registerDataSource(appKit, createMockSource([DATA_STREAM])); const page = await newSpecPage({ components: [IotLineChart, IotConnector], @@ -25,6 +25,7 @@ const lineChartSpecPage = async (propOverrides: Partial = {}) }); const lineChart = page.doc.createElement('iot-line-chart') as CustomHTMLElement; const props: Partial = { + appKit, widgetId: 'test-line-chart-widget', isEditing: false, query: { diff --git a/packages/components/src/components/iot-line-chart/iot-line-chart.tsx b/packages/components/src/components/iot-line-chart/iot-line-chart.tsx index e95251cf7..b764a410f 100644 --- a/packages/components/src/components/iot-line-chart/iot-line-chart.tsx +++ b/packages/components/src/components/iot-line-chart/iot-line-chart.tsx @@ -1,6 +1,6 @@ import { Component, Prop, h } from '@stencil/core'; import { MinimalViewPortConfig } from '@synchro-charts/core'; -import { DataStreamQuery, Request } from '@iot-app-kit/core'; +import { AnyDataStreamQuery, DataModule, Request } from '@iot-app-kit/core'; const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; @@ -9,7 +9,9 @@ const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; shadow: false, }) export class IotLineChart { - @Prop() query: DataStreamQuery; + @Prop() appKit: DataModule; + + @Prop() query: AnyDataStreamQuery; @Prop() viewport: MinimalViewPortConfig = DEFAULT_VIEWPORT; @@ -28,6 +30,7 @@ export class IotLineChart { const requestInfo = this.requestInfo(); return ( ( diff --git a/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.spec.ts b/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.spec.ts index 090ab8dd5..c5d6d1950 100644 --- a/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.spec.ts +++ b/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.spec.ts @@ -1,9 +1,8 @@ import { newSpecPage } from '@stencil/core/testing'; import { MinimalLiveViewport } from '@synchro-charts/core'; import { IotScatterChart } from './iot-scatter-chart'; -import { getDataModule } from '@iot-app-kit/core/src/data-module'; import { Components } from '../../components.d'; -import { initialize, SiteWiseDataStreamQuery } from '@iot-app-kit/core'; +import { initialize, registerDataSource, SiteWiseDataStreamQuery } from '@iot-app-kit/core'; import { createMockSource } from '../../testing/createMockSource'; import { IotConnector } from '../iot-connector/iot-connector'; import { CustomHTMLElement } from '../../testing/types'; @@ -15,8 +14,8 @@ const viewport: MinimalLiveViewport = { }; const scatterChartSpecPage = async (propOverrides: Partial = {}) => { - initialize({ registerDataSources: false }); - getDataModule().registerDataSource(createMockSource([DATA_STREAM])); + const appKit = initialize({ registerDataSources: false }); + registerDataSource(appKit, createMockSource([DATA_STREAM])); const page = await newSpecPage({ components: [IotScatterChart, IotConnector], @@ -25,6 +24,7 @@ const scatterChartSpecPage = async (propOverrides: Partial; const props: Partial = { + appKit, widgetId: 'test-scatter-chart-widget', isEditing: false, query: { diff --git a/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.tsx b/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.tsx index 069398742..a4e88295d 100644 --- a/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.tsx +++ b/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.tsx @@ -1,6 +1,6 @@ import { Component, Prop, h } from '@stencil/core'; import { MinimalViewPortConfig } from '@synchro-charts/core'; -import { DataStreamQuery, Request } from '@iot-app-kit/core'; +import { AnyDataStreamQuery, DataModule, Request } from '@iot-app-kit/core'; const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; @@ -9,7 +9,9 @@ const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; shadow: false, }) export class IotScatterChart { - @Prop() query: DataStreamQuery; + @Prop() appKit: DataModule; + + @Prop() query: AnyDataStreamQuery; @Prop() viewport: MinimalViewPortConfig = DEFAULT_VIEWPORT; @@ -28,6 +30,7 @@ export class IotScatterChart { const requestInfo = this.requestInfo(); return ( ( diff --git a/packages/components/src/components/iot-status-grid/iot-status-grid.spec.ts b/packages/components/src/components/iot-status-grid/iot-status-grid.spec.ts index e20afdceb..905208fb2 100644 --- a/packages/components/src/components/iot-status-grid/iot-status-grid.spec.ts +++ b/packages/components/src/components/iot-status-grid/iot-status-grid.spec.ts @@ -2,7 +2,7 @@ import { newSpecPage } from '@stencil/core/testing'; import { MinimalLiveViewport } from '@synchro-charts/core'; import { IotStatusGrid } from './iot-status-grid'; import { Components } from '../../components.d'; -import { initialize, getDataModule, SiteWiseDataStreamQuery } from '@iot-app-kit/core'; +import { initialize, registerDataSource, SiteWiseDataStreamQuery } from '@iot-app-kit/core'; import { createMockSource } from '../../testing/createMockSource'; import { DATA_STREAM } from '../../testing/mockWidgetProperties'; import { IotConnector } from '../iot-connector/iot-connector'; @@ -14,8 +14,8 @@ const viewport: MinimalLiveViewport = { }; const statusGridSpecPage = async (propOverrides: Partial = {}) => { - initialize({ registerDataSources: false }); - getDataModule().registerDataSource(createMockSource([DATA_STREAM])); + const appKit = initialize({ registerDataSources: false }); + registerDataSource(appKit, createMockSource([DATA_STREAM])); const page = await newSpecPage({ components: [IotStatusGrid, IotConnector], @@ -24,6 +24,7 @@ const statusGridSpecPage = async (propOverrides: Partial = {} }); const statusGrid = page.doc.createElement('iot-status-grid') as CustomHTMLElement; const props: Partial = { + appKit, widgetId: 'test-status-grid-widget', isEditing: false, query: { diff --git a/packages/components/src/components/iot-status-grid/iot-status-grid.tsx b/packages/components/src/components/iot-status-grid/iot-status-grid.tsx index e852a3ca4..d9ad72447 100644 --- a/packages/components/src/components/iot-status-grid/iot-status-grid.tsx +++ b/packages/components/src/components/iot-status-grid/iot-status-grid.tsx @@ -1,6 +1,6 @@ import { Component, Prop, h } from '@stencil/core'; import { MinimalViewPortConfig } from '@synchro-charts/core'; -import { DataStreamQuery, Request } from '@iot-app-kit/core'; +import { AnyDataStreamQuery, DataModule, Request } from '@iot-app-kit/core'; const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; @@ -9,7 +9,9 @@ const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; shadow: false, }) export class IotStatusGrid { - @Prop() query: DataStreamQuery; + @Prop() appKit: DataModule; + + @Prop() query: AnyDataStreamQuery; @Prop() viewport: MinimalViewPortConfig = DEFAULT_VIEWPORT; @@ -28,6 +30,7 @@ export class IotStatusGrid { const requestInfo = this.requestInfo(); return ( ( diff --git a/packages/components/src/components/iot-status-timeline/iot-status-timeline.spec.ts b/packages/components/src/components/iot-status-timeline/iot-status-timeline.spec.ts index 7308453fe..62726e1e9 100644 --- a/packages/components/src/components/iot-status-timeline/iot-status-timeline.spec.ts +++ b/packages/components/src/components/iot-status-timeline/iot-status-timeline.spec.ts @@ -2,7 +2,7 @@ import { newSpecPage } from '@stencil/core/testing'; import { MinimalLiveViewport } from '@synchro-charts/core'; import { IotStatusTimeline } from './iot-status-timeline'; import { Components } from '../../components.d'; -import { getDataModule, initialize, SiteWiseDataStreamQuery } from '@iot-app-kit/core'; +import { registerDataSource, initialize, SiteWiseDataStreamQuery } from '@iot-app-kit/core'; import { createMockSource } from '../../testing/createMockSource'; import { DATA_STREAM } from '../../testing/mockWidgetProperties'; import { IotConnector } from '../iot-connector/iot-connector'; @@ -14,8 +14,8 @@ const viewport: MinimalLiveViewport = { }; const statusTimelineSpecPage = async (propOverrides: Partial = {}) => { - initialize({ registerDataSources: false }); - getDataModule().registerDataSource(createMockSource([DATA_STREAM])); + const appKit = initialize({ registerDataSources: false }); + registerDataSource(appKit, createMockSource([DATA_STREAM])); const page = await newSpecPage({ components: [IotStatusTimeline, IotConnector], @@ -26,6 +26,7 @@ const statusTimelineSpecPage = async (propOverrides: Partial; const props: Partial = { + appKit, widgetId: 'test-status-timeline-chart-widget', isEditing: false, query: { diff --git a/packages/components/src/components/iot-status-timeline/iot-status-timeline.tsx b/packages/components/src/components/iot-status-timeline/iot-status-timeline.tsx index 46e8edcfc..7873d26f0 100644 --- a/packages/components/src/components/iot-status-timeline/iot-status-timeline.tsx +++ b/packages/components/src/components/iot-status-timeline/iot-status-timeline.tsx @@ -1,6 +1,6 @@ import { Component, Prop, h } from '@stencil/core'; import { MinimalViewPortConfig } from '@synchro-charts/core'; -import { DataStreamQuery, Request } from '@iot-app-kit/core'; +import { AnyDataStreamQuery, DataModule, Request } from '@iot-app-kit/core'; const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; @@ -9,7 +9,9 @@ const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; shadow: false, }) export class IotStatusTimeline { - @Prop() query: DataStreamQuery; + @Prop() appKit: DataModule; + + @Prop() query: AnyDataStreamQuery; @Prop() viewport: MinimalViewPortConfig = DEFAULT_VIEWPORT; @@ -28,6 +30,7 @@ export class IotStatusTimeline { const requestInfo = this.requestInfo(); return ( ( diff --git a/packages/components/src/components/iot-table/iot-table.spec.ts b/packages/components/src/components/iot-table/iot-table.spec.ts index d87804292..bda99b399 100644 --- a/packages/components/src/components/iot-table/iot-table.spec.ts +++ b/packages/components/src/components/iot-table/iot-table.spec.ts @@ -2,7 +2,7 @@ import { newSpecPage } from '@stencil/core/testing'; import { MinimalLiveViewport } from '@synchro-charts/core'; import { IotTable } from './iot-table'; import { Components } from '../../components.d'; -import { getDataModule, initialize, SiteWiseDataStreamQuery } from '@iot-app-kit/core'; +import { registerDataSource, initialize, SiteWiseDataStreamQuery } from '@iot-app-kit/core'; import { createMockSource } from '../../testing/createMockSource'; import { DATA_STREAM } from '../../testing/mockWidgetProperties'; import { IotConnector } from '../iot-connector/iot-connector'; @@ -14,8 +14,8 @@ const viewport: MinimalLiveViewport = { }; const tableSpecPage = async (propOverrides: Partial = {}) => { - initialize({ registerDataSources: false }); - getDataModule().registerDataSource(createMockSource([DATA_STREAM])); + const appKit = initialize({ registerDataSources: false }); + registerDataSource(appKit, createMockSource([DATA_STREAM])); const page = await newSpecPage({ components: [IotTable, IotConnector], @@ -24,6 +24,7 @@ const tableSpecPage = async (propOverrides: Partial = {}) => }); const table = page.doc.createElement('iot-table') as CustomHTMLElement; const props: Partial = { + appKit, widgetId: 'test-table-widget', isEditing: false, query: { diff --git a/packages/components/src/components/iot-table/iot-table.tsx b/packages/components/src/components/iot-table/iot-table.tsx index cf980e277..b099a289f 100644 --- a/packages/components/src/components/iot-table/iot-table.tsx +++ b/packages/components/src/components/iot-table/iot-table.tsx @@ -1,6 +1,6 @@ import { Component, Prop, h } from '@stencil/core'; import { MinimalViewPortConfig } from '@synchro-charts/core'; -import { DataStreamQuery, Request } from '@iot-app-kit/core'; +import { AnyDataStreamQuery, DataModule, Request } from '@iot-app-kit/core'; const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; @@ -9,7 +9,9 @@ const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; shadow: false, }) export class IotTable { - @Prop() query: DataStreamQuery; + @Prop() appKit: DataModule; + + @Prop() query: AnyDataStreamQuery; @Prop() viewport: MinimalViewPortConfig = DEFAULT_VIEWPORT; @@ -26,6 +28,7 @@ export class IotTable { const requestInfo = this.requestInfo(); return ( ( diff --git a/packages/components/src/testing/testing-ground/siteWiseQueries.ts b/packages/components/src/testing/testing-ground/siteWiseQueries.ts index 116db750f..a6d930a82 100644 --- a/packages/components/src/testing/testing-ground/siteWiseQueries.ts +++ b/packages/components/src/testing/testing-ground/siteWiseQueries.ts @@ -1,12 +1,12 @@ const STRING_ASSET_ID = '888dbcd1-cdfe-44ba-a99b-0ad3ca19a019'; const STRING_PROPERTY_ID = '9bd13790-377b-429f-87b0-43382b1709fd'; -const DEMO_TURBINE_ASSET_1 = '4df123fc-dc29-470e-8fd2-9242a2d3fa17'; +export const DEMO_TURBINE_ASSET_1 = '25963bcd-cde2-44ef-8e59-7b54da426409'; -const DEMO_TURBINE_ASSET_1_PROPERTY_1 = '59868144-6bb2-4a96-9413-aa58d9398a07'; -const DEMO_TURBINE_ASSET_1_PROPERTY_2 = '089948a3-71e5-4fbe-bd13-18f12a076ca7'; -const DEMO_TURBINE_ASSET_1_PROPERTY_3 = '04f5dcbd-f5b1-4f41-a526-84203d3af3aa'; -const DEMO_TURBINE_ASSET_1_PROPERTY_4 = 'e7a6fce5-4486-4a23-b40b-8a68cb493f02'; +export const DEMO_TURBINE_ASSET_1_PROPERTY_1 = 'b86ad68c-d102-48df-8ea7-935241112eff'; +export const DEMO_TURBINE_ASSET_1_PROPERTY_2 = 'a8506274-bf65-48dc-a382-1f941e2360db'; +export const DEMO_TURBINE_ASSET_1_PROPERTY_3 = '6d55449b-d0ff-4233-9f02-fc53d6318954'; +export const DEMO_TURBINE_ASSET_1_PROPERTY_4 = '8d9ed440-a8dd-48bd-a35f-70db6f2e860c'; export const STRING_QUERY = { source: 'site-wise', @@ -22,12 +22,7 @@ export const NUMBER_QUERY = { assets: [ { assetId: DEMO_TURBINE_ASSET_1, - propertyIds: [ - DEMO_TURBINE_ASSET_1_PROPERTY_1, - DEMO_TURBINE_ASSET_1_PROPERTY_2, - DEMO_TURBINE_ASSET_1_PROPERTY_3, - DEMO_TURBINE_ASSET_1_PROPERTY_4, - ], + propertyIds: [DEMO_TURBINE_ASSET_1_PROPERTY_1, DEMO_TURBINE_ASSET_1_PROPERTY_4], }, ], }; diff --git a/packages/components/src/testing/testing-ground/testing-ground.tsx b/packages/components/src/testing/testing-ground/testing-ground.tsx index 4f46115fb..861857e91 100755 --- a/packages/components/src/testing/testing-ground/testing-ground.tsx +++ b/packages/components/src/testing/testing-ground/testing-ground.tsx @@ -1,9 +1,16 @@ import { Component, h } from '@stencil/core'; import { initialize } from '@iot-app-kit/core'; -import { NUMBER_QUERY, ASSET_DETAILS_QUERY } from './siteWiseQueries'; +import { + ASSET_DETAILS_QUERY, + DEMO_TURBINE_ASSET_1, + DEMO_TURBINE_ASSET_1_PROPERTY_1, + DEMO_TURBINE_ASSET_1_PROPERTY_2, + DEMO_TURBINE_ASSET_1_PROPERTY_3, + DEMO_TURBINE_ASSET_1_PROPERTY_4, +} from './siteWiseQueries'; import { getEnvCredentials } from './getEnvCredentials'; -const VIEWPORT = { duration: 3 * 1000 * 60 }; +const VIEWPORT = { duration: '5m' }; @Component({ tag: 'testing-ground', @@ -11,19 +18,64 @@ const VIEWPORT = { duration: 3 * 1000 * 60 }; }) export class TestingGround { componentWillLoad() { - initialize({ awsCredentials: getEnvCredentials(), awsRegion: 'us-east-1' }); + initialize({ awsCredentials: getEnvCredentials(), awsRegion: 'us-west-2' }); } render() { return ( -
- - - -
- +
+
+
+
+
+ +
+ +
+
+ +
+
); } diff --git a/packages/core/cypress.json b/packages/core/cypress.json deleted file mode 100755 index f73dd1a9e..000000000 --- a/packages/core/cypress.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "videoUploadOnPasses": false, - "video": false, - "viewportWidth": 1400, - "viewportHeight": 2000, - "watchForFileChanges": true, - "defaultCommandTimeout": 8000, - "retries": { "openMode": 0, "runMode": 2 }, - "ignoreTestFiles": ["**/__snapshots__/*", "**/__image_snapshots__/*"] -} diff --git a/packages/core/cypress/.eslintrc.json b/packages/core/cypress/.eslintrc.json deleted file mode 100755 index 4946a4564..000000000 --- a/packages/core/cypress/.eslintrc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": [ - "plugin:cypress/recommended" - ], - "rules": { - "cypress/no-unnecessary-waiting": "off" - }, - "overrides": [ - { - "files": ["*.spec.tsx", "*.spec.ts"], - "rules": { - "max-len": 0 - } - } - ] -} diff --git a/packages/core/cypress/fixtures/example.json b/packages/core/cypress/fixtures/example.json deleted file mode 100755 index da18d9352..000000000 --- a/packages/core/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} \ No newline at end of file diff --git a/packages/core/cypress/plugins/index.js b/packages/core/cypress/plugins/index.js deleted file mode 100755 index dfd1b6aa7..000000000 --- a/packages/core/cypress/plugins/index.js +++ /dev/null @@ -1,9 +0,0 @@ -const wp = require('@cypress/webpack-preprocessor'); -const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin'); -const webpackOptions = require('../webpack.config'); - -module.exports = (on, config) => { - on('file:preprocessor', wp({ webpackOptions })); - addMatchImageSnapshotPlugin(on, config); - return config; -}; diff --git a/packages/core/cypress/support/chartCommands.ts b/packages/core/cypress/support/chartCommands.ts deleted file mode 100644 index f8bb8bd2f..000000000 --- a/packages/core/cypress/support/chartCommands.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const addChartCommands = () => { - Cypress.Commands.add( - 'matchImageSnapshotOnCI', - { prevSubject: 'optional' }, - (subject, nameOrOptions?: string | Object) => { - if (!Cypress.env('disableSnapshotTests')) { - if (subject) { - cy.wrap(subject).matchImageSnapshot(nameOrOptions); - } else { - cy.matchImageSnapshot(nameOrOptions); - } - } - } - ); -}; diff --git a/packages/core/cypress/support/commands.js b/packages/core/cypress/support/commands.js deleted file mode 100755 index ec3cb37b6..000000000 --- a/packages/core/cypress/support/commands.js +++ /dev/null @@ -1,37 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) -import 'cypress-shadow-dom'; -import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command'; - -import { addChartCommands } from './chartCommands'; - -addChartCommands(); - -addMatchImageSnapshotCommand({ - failureThresholdType: 'pixel', - failureThreshold: 2, - customDiffConfig: { threshold: 0.144 }, -}); diff --git a/packages/core/cypress/support/index.d.ts b/packages/core/cypress/support/index.d.ts deleted file mode 100755 index d889bb4f5..000000000 --- a/packages/core/cypress/support/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable */ -/// - -declare namespace Cypress { - interface Chainable { - /** - * Custom command to wait until a chart is done with it's initial rendering - */ - waitForChart(): Chainable; - waitForStatusTimeline(): Chainable; - matchImageSnapshotOnCI(nameOrOptions?: string | Object): void; - } -} diff --git a/packages/core/cypress/support/index.js b/packages/core/cypress/support/index.js deleted file mode 100755 index fd058638e..000000000 --- a/packages/core/cypress/support/index.js +++ /dev/null @@ -1,15 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** -import './commands'; diff --git a/packages/core/cypress/tsconfig.json b/packages/core/cypress/tsconfig.json deleted file mode 100755 index a4e7e4457..000000000 --- a/packages/core/cypress/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "baseUrl": "../node_modules", - "target": "es5", - "lib": ["es2019", "dom"], - "types": ["cypress", "cypress-shadow-dom", "@types/cypress-image-snapshot"] - }, - "include": [ - "**/*.ts" - ] -} diff --git a/packages/core/cypress/webpack.config.js b/packages/core/cypress/webpack.config.js deleted file mode 100755 index 4c332dabc..000000000 --- a/packages/core/cypress/webpack.config.js +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = { - mode: 'development', - // make sure the source maps work - devtool: 'eval-source-map', - // webpack will transpile TS and JS files - resolve: { - extensions: ['.ts', '.js'], - }, - module: { - rules: [ - { - // every time webpack sees a TS file (except for node_modules) - // webpack will use "ts-loader" to transpile it to JavaScript - test: /\.ts$/, - exclude: [/node_modules/], - use: [ - { - loader: 'ts-loader', - options: { - // skip typechecking for speed - transpileOnly: true, - }, - }, - ], - }, - ], - }, -}; diff --git a/packages/core/package.json b/packages/core/package.json index 9d7a803b1..e07252848 100755 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,23 +23,14 @@ }, "scripts": { "clean": "rm -rf dist && rm -rf screenshot", - "build": "npm-run-all build:stencil", - "build:stencil": "stencil build", + "build": "stencil build", "start": "stencil build --dev --watch --serve", - "test": "npm-run-all -p test:unit-test lint test:typescript", - "test:dev": "npm-run-all test test:integ", + "test": "npm-run-all -p test:unit-test test:typescript", "test:unit-test": "TZ=UTC stencil test --spec --coverage", - "test:integ": "npm run test:cypress", - "test:cypress": "cypress run", - "test:cypress-dev": "cypress open --env disableSnapshotTests=true", "test:typescript": "tsc --noEmit", "test.watch": "TZ=UTC stencil test --spec --watchAll", "copy:license": "cp ../../LICENSE LICENSE", "copy:notice": "cp ../../NOTICE NOTICE", - "lint": "yarn run lint:stylelint", - "fix": "yarn run lint:fix-stylelint", - "lint:fix-stylelint": "stylelint 'src/**/*.css' --fix", - "lint:stylelint": "stylelint 'src/**/*.css' --max-warnings 0", "release": "yarn run clean && npm-run-all -p build test lint", "prepublishOnly": "yarn release", "prepack": "yarn run copy:license && yarn run copy:notice", @@ -61,7 +52,6 @@ "lodash.uniqby": "^4.7.0", "parse-duration": "^1.0.0", "redux": "^4.0.4", - "redux-thunk": "^2.3.0", "rxjs": "^7.4.0", "typescript": "4.4.4", "uuid": "^3.3.2" diff --git a/packages/core/src/data-module/IotAppKitDataModule.spec.ts b/packages/core/src/data-module/IotAppKitDataModule.spec.ts index 86612ca3a..84410ba7b 100644 --- a/packages/core/src/data-module/IotAppKitDataModule.spec.ts +++ b/packages/core/src/data-module/IotAppKitDataModule.spec.ts @@ -1,42 +1,66 @@ import flushPromises from 'flush-promises'; import { DATA_STREAM, DATA_STREAM_INFO, STRING_INFO_1 } from '../testing/__mocks__/mockWidgetProperties'; -import { AnyDataStreamQuery, DataSource, DataSourceRequest } from './types.d'; -import { DataStream, DataStreamInfo } from '@synchro-charts/core'; +import { DataSource, DataSourceRequest } from './types.d'; +import { DataPoint, DataStream, DataStreamInfo, Resolution } from '@synchro-charts/core'; import { Request } from './data-cache/requestTypes'; import { DataStreamsStore, DataStreamStore } from './data-cache/types'; import { EMPTY_CACHE } from './data-cache/caching/caching'; import { createSiteWiseLegacyDataSource } from '../data-sources/site-wise-legacy/data-source'; -import { MONTH_IN_MS, SECOND_IN_MS } from '../common/time'; +import { MINUTE_IN_MS, MONTH_IN_MS, SECOND_IN_MS } from '../common/time'; import { IotAppKitDataModule } from './IotAppKitDataModule'; -import Mock = jest.Mock; import { SITEWISE_DATA_SOURCE } from '../data-sources/site-wise/data-source'; import { wait } from '../testing/wait'; +import { SiteWiseDataStreamQuery } from '../data-sources/site-wise/types'; +import { toDataStreamId, toSiteWiseAssetProperty } from '../data-sources/site-wise/util/dataStreamId'; + +import Mock = jest.Mock; + +const { propertyId: PROPERTY_ID, assetId: ASSET_ID } = toSiteWiseAssetProperty(DATA_STREAM.id); + +const DATA_STREAM_QUERY: SiteWiseDataStreamQuery = { + source: 'site-wise', + assets: [ + { + assetId: ASSET_ID, + propertyIds: [PROPERTY_ID], + }, + ], +}; // A simple mock data source, which will always immediately return a successful response of your choosing. -const createMockSource = (dataStreams: DataStream[]): DataSource => ({ +const createMockSiteWiseDataSource = ( + dataStreams: DataStream[], + resolution: number = 0 +): DataSource => ({ name: SITEWISE_DATA_SOURCE, - initiateRequest: jest.fn(({ onSuccess }: DataSourceRequest) => onSuccess(dataStreams)), - getRequestsFromQuery: () => - dataStreams.map(({ data, aggregates, ...dataStreamInfo }) => ({ - ...dataStreamInfo, - start: new Date(), - end: new Date(), - })), + initiateRequest: jest.fn(({ onSuccess }: DataSourceRequest) => onSuccess(dataStreams)), + getRequestsFromQuery: (query) => + query.assets + .map(({ assetId, propertyIds }) => + propertyIds.map((propertyId) => ({ + id: toDataStreamId({ assetId, propertyId }), + resolution, + })) + ) + .flat(), }); it('subscribes to an empty set of queries', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSource([]); + const dataSource = createMockSiteWiseDataSource([]); dataModule.registerDataSource(dataSource); const onSuccess = jest.fn(); dataModule.subscribeToDataStreams( { - query: { source: dataSource.name, dataStreamInfos: [] }, + query: { source: dataSource.name, assets: [] }, requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date(2000, 0, 2) }, onlyFetchLatestValue: false, + requestConfig: { + requestBuffer: 0, + }, }, }, onSuccess @@ -45,11 +69,46 @@ it('subscribes to an empty set of queries', async () => { expect(onSuccess).not.toBeCalled(); }); +describe('update subscription', () => { + it('provides new data streams when subscription is updated', async () => { + const dataModule = new IotAppKitDataModule(); + const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + + dataModule.registerDataSource(dataSource); + + const dataStreamCallback = jest.fn(); + + const query: SiteWiseDataStreamQuery = { source: dataSource.name, assets: [] }; + + const { update } = dataModule.subscribeToDataStreams( + { + query, + requestInfo: { + viewport: { start: new Date(2000, 0, 0), end: new Date(2001, 0, 0) }, + onlyFetchLatestValue: false, + }, + }, + dataStreamCallback + ); + + dataStreamCallback.mockClear(); + (dataSource.initiateRequest as Mock).mockClear(); + + update({ query: DATA_STREAM_QUERY }); + + await flushPromises(); + await wait(SECOND_IN_MS); + + // expect(dataStreamCallback).toHaveBeenLastCalledWith([expect.objectContaining({ id: DATA_STREAM.id })]); + expect(dataSource.initiateRequest).toBeCalled(); + }); +}); + describe('initial request', () => { it('does not load request data streams which are not provided from a data-source', async () => { const dataModule = new IotAppKitDataModule(); const dataSource: DataSource = { - name: 'site-wise-legacy', + name: 'some-data-source', initiateRequest: jest.fn(), getRequestsFromQuery: () => [], }; @@ -58,11 +117,9 @@ describe('initial request', () => { const dataStreamCallback = jest.fn(); - const query = { source: dataSource.name, dataStreamInfos: [DATA_STREAM_INFO] }; - dataModule.subscribeToDataStreams( { - query, + query: { source: dataSource.name }, requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date(2001, 0, 0) }, onlyFetchLatestValue: false, @@ -75,53 +132,59 @@ describe('initial request', () => { expect(dataSource.initiateRequest).not.toBeCalled(); }); - it('does load request data streams which are not provided from a data-source', () => { + it('initiates a request for a data stream', () => { const START = new Date(2000, 0, 0); const END = new Date(); const dataModule = new IotAppKitDataModule(); const dataSource: DataSource = { - name: 'site-wise-legacy', + ...createMockSiteWiseDataSource([DATA_STREAM]), initiateRequest: jest.fn(), - getRequestsFromQuery: () => [DATA_STREAM_INFO], }; - const query = { source: dataSource.name, dataStreamInfos: [DATA_STREAM_INFO] }; - dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); dataModule.subscribeToDataStreams( { - query, + query: DATA_STREAM_QUERY, requestInfo: { viewport: { start: START, end: END }, onlyFetchLatestValue: false }, }, dataStreamCallback ); expect(dataStreamCallback).toBeCalledWith([ expect.objectContaining({ - id: DATA_STREAM_INFO.id, + id: DATA_STREAM.id, isLoading: true, isRefreshing: true, } as DataStreamStore), ]); - expect(dataSource.initiateRequest).toBeCalledWith(expect.objectContaining({ query }), [ - { id: DATA_STREAM_INFO.id, resolution: DATA_STREAM_INFO.resolution, start: START, end: END }, + expect(dataSource.initiateRequest).toBeCalledWith(expect.objectContaining({ query: DATA_STREAM_QUERY }), [ + { id: DATA_STREAM.id, resolution: DATA_STREAM.resolution, start: START, end: END }, ]); }); }); it('subscribes to a single data stream', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); dataModule.registerDataSource(dataSource); + const { propertyId, assetId } = toSiteWiseAssetProperty(DATA_STREAM.id); const dataStreamCallback = jest.fn(); dataModule.subscribeToDataStreams( { - query: { source: SITEWISE_DATA_SOURCE, dataStreamInfos: [DATA_STREAM_INFO] }, + query: { + source: SITEWISE_DATA_SOURCE, + assets: [ + { + assetId, + propertyIds: [propertyId], + }, + ], + }, requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date(2002, 0, 0) }, onlyFetchLatestValue: false, @@ -145,7 +208,7 @@ it('throws error when subscribing to a non-existent data source', () => { expect(() => dataModule.subscribeToDataStreams( { - query: { source: 'fake-source', dataStreamInfos: [] }, + query: { source: 'fake-source', assets: [] }, requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date(2002, 0, 0) }, onlyFetchLatestValue: false, @@ -157,7 +220,8 @@ it('throws error when subscribing to a non-existent data source', () => { }); it('requests data from a custom data source', () => { - const customSource = createMockSource([DATA_STREAM]); + const customSource = createMockSiteWiseDataSource([DATA_STREAM]); + const { propertyId, assetId } = toSiteWiseAssetProperty(DATA_STREAM.id); const dataModule = new IotAppKitDataModule(); const onSuccess = jest.fn(); @@ -167,7 +231,7 @@ it('requests data from a custom data source', () => { dataModule.subscribeToDataStreams( { query: { - dataStreamInfos: [DATA_STREAM_INFO], + assets: [{ assetId, propertyIds: [propertyId] }], source: customSource.name, }, requestInfo: { @@ -178,7 +242,7 @@ it('requests data from a custom data source', () => { onSuccess ); - expect(onSuccess).toHaveBeenCalledWith([expect.objectContaining({ id: DATA_STREAM_INFO.id })]); + expect(onSuccess).toHaveBeenCalledWith([expect.objectContaining({ id: DATA_STREAM.id })]); }); it('subscribes to multiple data streams', () => { @@ -237,7 +301,9 @@ it('only requests latest value', () => { ); }); -it.skip('does not request a data stream which has an error associated with it', () => { +describe('error handling', () => { + const ERR_MSG = 'An error has occurred!'; + const CACHE_WITH_ERROR: DataStreamsStore = { [DATA_STREAM_INFO.id]: { [DATA_STREAM_INFO.resolution]: { @@ -248,37 +314,93 @@ it.skip('does not request a data stream which has an error associated with it', requestHistory: [], isLoading: false, isRefreshing: false, - error: 'An error has occurred!', + error: ERR_MSG, }, }, }; - const customSource = createMockSource([]); + const CACHE_WITHOUT_ERROR: DataStreamsStore = { + [DATA_STREAM_INFO.id]: { + [DATA_STREAM_INFO.resolution]: { + id: DATA_STREAM.id, + resolution: DATA_STREAM.resolution, + dataCache: EMPTY_CACHE, + requestCache: EMPTY_CACHE, + requestHistory: [], + isLoading: false, + isRefreshing: false, + error: undefined, + }, + }, + }; - const dataModule = new IotAppKitDataModule(CACHE_WITH_ERROR); + it('provides a data stream which has an error associated with it on initial subscription', () => { + const customSource = createMockSiteWiseDataSource([DATA_STREAM]); - const onSuccess = jest.fn(); + const dataModule = new IotAppKitDataModule(CACHE_WITH_ERROR); + const dataStreamCallback = jest.fn(); - dataModule.registerDataSource(customSource); + dataModule.registerDataSource(customSource); - dataModule.subscribeToDataStreams( - { - query: { - dataStreamInfos: [DATA_STREAM_INFO], - source: customSource.name, + dataModule.subscribeToDataStreams( + { + query: DATA_STREAM_QUERY, + requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date() }, onlyFetchLatestValue: false }, }, - requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date() }, onlyFetchLatestValue: false }, - }, - onSuccess - ); + dataStreamCallback + ); - expect(onSuccess).not.toBeCalled(); + expect(dataStreamCallback).toBeCalledTimes(1); + expect(dataStreamCallback).toBeCalledWith([expect.objectContaining({ error: ERR_MSG })]); + }); + + it('does not re-request a data stream with an error associated with it', async () => { + const customSource = createMockSiteWiseDataSource([DATA_STREAM]); + + const dataModule = new IotAppKitDataModule(CACHE_WITH_ERROR); + const dataStreamCallback = jest.fn(); + + dataModule.registerDataSource(customSource); + + dataModule.subscribeToDataStreams( + { + query: DATA_STREAM_QUERY, + requestInfo: { viewport: { duration: 900000 }, onlyFetchLatestValue: false, refreshRate: SECOND_IN_MS / 10 }, + }, + dataStreamCallback + ); + + dataStreamCallback.mockClear(); + await wait(SECOND_IN_MS * 0.11); + expect(dataStreamCallback).not.toBeCalled(); + }); + + it('does request a data stream which has no error associated with it', () => { + const customSource = createMockSiteWiseDataSource([DATA_STREAM]); + + const dataModule = new IotAppKitDataModule(CACHE_WITHOUT_ERROR); + + const dataStreamCallback = jest.fn(); + + dataModule.registerDataSource(customSource); + + dataModule.subscribeToDataStreams( + { + query: DATA_STREAM_QUERY, + requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date() }, onlyFetchLatestValue: false }, + }, + dataStreamCallback + ); + + expect(dataStreamCallback).toBeCalledTimes(1); + expect(dataStreamCallback).toBeCalledWith([expect.objectContaining({ error: undefined })]); + }); }); describe('caching', () => { it('does not request already cached data', () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); dataModule.registerDataSource(dataSource); const START_1 = new Date(2000, 1, 0); @@ -290,7 +412,7 @@ describe('caching', () => { const dataStreamCallback = jest.fn(); const { update } = dataModule.subscribeToDataStreams( { - query: { source: SITEWISE_DATA_SOURCE, dataStreamInfos: [DATA_STREAM_INFO] }, + query: DATA_STREAM_QUERY, requestInfo: { viewport: { start: START_1, end: END_1 }, onlyFetchLatestValue: false }, }, dataStreamCallback @@ -306,7 +428,7 @@ describe('caching', () => { it('requests only uncached data', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); dataModule.registerDataSource(dataSource); // Order of points through time: @@ -314,15 +436,15 @@ describe('caching', () => { // results in the intervals requested from the cache // [START_2 ----- START_1] [END_1 ------- END_2] // Which omits the time in-between START_1 and END_1 (The inner interval of time). - const START_1 = new Date(2000, 1, 0); - const END_1 = new Date(2000, 2, 0); + const START_1 = new Date(2000, 1, 1); + const END_1 = new Date(2000, 2, 1); const START_2 = new Date(START_1.getTime() - MONTH_IN_MS); const END_2 = new Date(END_1.getTime() + MONTH_IN_MS); const dataStreamCallback = jest.fn(); const { update } = dataModule.subscribeToDataStreams( { - query: { source: SITEWISE_DATA_SOURCE, dataStreamInfos: [DATA_STREAM_INFO] }, + query: DATA_STREAM_QUERY, requestInfo: { viewport: { start: START_1, end: END_1 }, onlyFetchLatestValue: false }, }, dataStreamCallback @@ -352,7 +474,7 @@ describe('caching', () => { it('immediately request when subscribed to an entirely new time interval not previously requested', () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); dataModule.registerDataSource(dataSource); const START_1 = new Date(2000, 1, 0); @@ -363,7 +485,7 @@ describe('caching', () => { const dataStreamCallback = jest.fn(); const { update } = dataModule.subscribeToDataStreams( { - query: { source: SITEWISE_DATA_SOURCE, dataStreamInfos: [DATA_STREAM_INFO] }, + query: DATA_STREAM_QUERY, requestInfo: { viewport: { start: START_1, end: END_1 }, onlyFetchLatestValue: false }, }, dataStreamCallback @@ -385,96 +507,110 @@ describe('caching', () => { }); describe('request scheduler', () => { - it('starts the request scheduler when subscribing to a duration based query', async () => { + it('periodically requests duration based queries', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); const { unsubscribe } = dataModule.subscribeToDataStreams( { - query: { source: dataSource.name, dataStreamInfos: [DATA_STREAM_INFO] }, - requestInfo: { viewport: { duration: SECOND_IN_MS }, onlyFetchLatestValue: false, refreshRate: SECOND_IN_MS }, + query: DATA_STREAM_QUERY, + requestInfo: { viewport: { duration: 900000 }, onlyFetchLatestValue: false, refreshRate: SECOND_IN_MS * 0.1 }, }, dataStreamCallback ); dataStreamCallback.mockClear(); - await wait(SECOND_IN_MS * 1.1); + await wait(SECOND_IN_MS * 0.11); expect(dataStreamCallback).toBeCalledTimes(2); dataStreamCallback.mockClear(); - await wait(SECOND_IN_MS * 1.1); + await wait(SECOND_IN_MS * 0.11); expect(dataStreamCallback).toBeCalledTimes(2); unsubscribe(); }); - it('stops requesting for data after unsubscribe from the data module', async () => { + it('stops requesting for data after unsubscribing', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); const { unsubscribe } = dataModule.subscribeToDataStreams( { - query: { source: dataSource.name, dataStreamInfos: [DATA_STREAM_INFO] }, - requestInfo: { viewport: { duration: SECOND_IN_MS }, onlyFetchLatestValue: false }, + query: DATA_STREAM_QUERY, + requestInfo: { + viewport: { duration: SECOND_IN_MS }, + onlyFetchLatestValue: false, + refreshRate: SECOND_IN_MS * 0.1, + }, }, dataStreamCallback ); unsubscribe(); + await flushPromises(); dataStreamCallback.mockClear(); - await wait(SECOND_IN_MS * 1.1); + await wait(SECOND_IN_MS * 0.11); expect(dataStreamCallback).not.toHaveBeenCalled(); }); - it('starts the request scheduler when the request info gets updated from static viewport to duration', async () => { + it('periodically requests data after switching from static to duration based viewport', async () => { + const DATA_POINT: DataPoint = { x: Date.now(), y: 1921 }; const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource([{ ...DATA_STREAM, data: [DATA_POINT] }]); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); const { update, unsubscribe } = dataModule.subscribeToDataStreams( { - query: { source: dataSource.name, dataStreamInfos: [DATA_STREAM_INFO] }, + query: DATA_STREAM_QUERY, requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date(2001, 0, 0) }, - onlyFetchLatestValue: false, - refreshRate: SECOND_IN_MS, + onlyFetchLatestValue: true, + refreshRate: SECOND_IN_MS * 0.1, }, }, dataStreamCallback ); update({ - requestInfo: { viewport: { duration: SECOND_IN_MS }, refreshRate: SECOND_IN_MS, onlyFetchLatestValue: false }, + requestInfo: { + viewport: { duration: MINUTE_IN_MS }, + refreshRate: SECOND_IN_MS * 0.1, + onlyFetchLatestValue: false, + }, }); dataStreamCallback.mockClear(); - await wait(SECOND_IN_MS * 1.1); + await wait(SECOND_IN_MS * 0.11); + expect(dataStreamCallback).toBeCalledTimes(2); + dataStreamCallback.mockClear(); + await wait(SECOND_IN_MS * 0.11); expect(dataStreamCallback).toBeCalledTimes(2); + unsubscribe(); }); it('stops the request scheduler when we first update request info to have duration and then call unsubscribe', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); const { update, unsubscribe } = dataModule.subscribeToDataStreams( { - query: { source: dataSource.name, dataStreamInfos: [DATA_STREAM_INFO] }, + query: DATA_STREAM_QUERY, requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date(2001, 0, 0) }, onlyFetchLatestValue: false, - refreshRate: SECOND_IN_MS, + refreshRate: SECOND_IN_MS * 0.1, }, }, dataStreamCallback @@ -486,19 +622,19 @@ describe('request scheduler', () => { unsubscribe(); dataStreamCallback.mockClear(); - await wait(SECOND_IN_MS * 1.1); + await wait(SECOND_IN_MS * 0.11); expect(dataStreamCallback).not.toHaveBeenCalled(); }); it('stops the request scheduler when request info gets updated with static viewport', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); const { update } = dataModule.subscribeToDataStreams( { - query: { source: SITEWISE_DATA_SOURCE, dataStreamInfos: [DATA_STREAM_INFO] }, + query: DATA_STREAM_QUERY, requestInfo: { viewport: { duration: SECOND_IN_MS }, onlyFetchLatestValue: false }, }, dataStreamCallback @@ -507,12 +643,13 @@ describe('request scheduler', () => { update({ requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date(2001, 0, 0) }, + refreshRate: SECOND_IN_MS * 0.1, onlyFetchLatestValue: false, }, }); dataStreamCallback.mockClear(); - await wait(SECOND_IN_MS * 1.1); + await wait(SECOND_IN_MS * 0.11); expect(dataStreamCallback).not.toBeCalled(); }); @@ -520,7 +657,7 @@ describe('request scheduler', () => { it('requests data range with buffer', () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); @@ -532,7 +669,7 @@ it('requests data range with buffer', () => { const { unsubscribe } = dataModule.subscribeToDataStreams( { - query: { source: 'site-wise', dataStreamInfos: [DATA_STREAM_INFO] }, + query: DATA_STREAM_QUERY, requestInfo: { viewport: { start, end }, onlyFetchLatestValue: false, requestConfig: { requestBuffer } }, }, dataStreamCallback diff --git a/packages/core/src/data-module/IotAppKitDataModule.ts b/packages/core/src/data-module/IotAppKitDataModule.ts index 391eeb1d7..ff8c1b90f 100644 --- a/packages/core/src/data-module/IotAppKitDataModule.ts +++ b/packages/core/src/data-module/IotAppKitDataModule.ts @@ -1,4 +1,3 @@ -import { DataStream, MinimalLiveViewport } from '@synchro-charts/core'; import { v4 } from 'uuid'; import SubscriptionStore from './subscription-store/subscriptionStore'; import { @@ -12,33 +11,28 @@ import { SubscriptionUpdate, } from './types.d'; import { DataStreamsStore } from './data-cache/types'; -import { toDataStreams } from './data-cache/toDataStreams'; import DataSourceStore from './data-source-store/dataSourceStore'; import { SubscriptionResponse } from '../data-sources/site-wise/types.d'; -import RequestScheduler from './request-scheduler/requestScheduler'; import { DataCache } from './data-cache/dataCacheWrapped'; import { Request } from './data-cache/requestTypes'; import { requestRange } from './data-cache/requestRange'; import { getDateRangesToRequest } from './data-cache/caching/caching'; import { viewportEndDate, viewportStartDate } from '../common/viewport'; -import { parseDuration } from '../common/time'; export class IotAppKitDataModule implements DataModule { private dataCache: DataCache; - private subscriptions = new SubscriptionStore(); + private subscriptions: SubscriptionStore; private dataSourceStore = new DataSourceStore(); - private scheduler = new RequestScheduler(); - /** * Create a new data module, optionally with a pre-hydrated data cache. * */ constructor(initialDataCache?: DataStreamsStore) { this.dataCache = new DataCache(initialDataCache); - this.publishToSubscriptions(); + this.subscriptions = new SubscriptionStore({ dataSourceStore: this.dataSourceStore, dataCache: this.dataCache }); } public registerDataSource = this.dataSourceStore.registerDataSource; @@ -54,16 +48,19 @@ export class IotAppKitDataModule implements DataModule { query, start, end, - subscriptionId, requestInfo, }: { query: DataStreamQuery; start: Date; end: Date; - subscriptionId: string; requestInfo: Request; }) => { - const requiredStreams = this.dataSourceStore.getRequestsFromQuery(query); + const requestedStreams = this.dataSourceStore.getRequestsFromQuery(query); + + const isRequestedDataStream = ({ id, resolution }: RequestInformation) => + this.dataCache.shouldRequestDataStream({ dataStreamId: id, resolution }); + + const requiredStreams = requestedStreams.filter(isRequestedDataStream); // Get the date range to request data for. // Pass in 'now' for max since we don't want to request for data in the future yet - it doesn't exist yet. @@ -95,7 +92,6 @@ export class IotAppKitDataModule implements DataModule { dateRanges.map(([rangeStart, rangeEnd]) => ({ start: rangeStart, end: rangeEnd, ...request })) ); - // TODO: Prevent from requesting when an error is present in the data cache /** Indicate within the cache that the following queries are being requested */ requests.forEach(({ start: reqStart, end: reqEnd, id, resolution }) => this.dataCache.onRequest({ @@ -107,10 +103,28 @@ export class IotAppKitDataModule implements DataModule { ); if (requests.length > 0) { - this.registerRequest(this.subscriptions.getSubscription(subscriptionId), requests); + this.registerRequest({ query, requestInfo }, requests); } }; + public subscribeToDataStreamsFrom = (source: string, callback: DataStreamCallback) => { + const subscriptionId = v4(); + + this.subscriptions.addSubscription(subscriptionId, { + source, + emit: callback, + }); + + /** + * subscription management + */ + const unsubscribe = () => { + this.unsubscribe(subscriptionId); + }; + + return { unsubscribe }; + }; + public subscribeToDataStreams = ( { query, requestInfo }: DataModuleSubscription, callback: DataStreamCallback @@ -121,22 +135,16 @@ export class IotAppKitDataModule implements DataModule { query, requestInfo, emit: callback, + fulfill: () => { + this.fulfillQuery({ + start: viewportStartDate(requestInfo.viewport), + end: viewportEndDate(requestInfo.viewport), + query, + requestInfo, + }); + }, }); - // call it once - this.fulfillQuery({ - start: viewportStartDate(requestInfo.viewport), - end: viewportEndDate(requestInfo.viewport), - query, - subscriptionId, - requestInfo, - }); - - // If duration exists, we want to start the request scheduler - if ('duration' in requestInfo.viewport) { - this.registerQueryForPolling({ query, requestInfo }, subscriptionId); - } - /** * subscription management */ @@ -152,89 +160,30 @@ export class IotAppKitDataModule implements DataModule { return { unsubscribe, update }; }; - private registerQueryForPolling( - { query, requestInfo }: DataModuleSubscription, - subscriptionId: string - ): void { - if (!('duration' in requestInfo.viewport)) { - return; - } - - const cb = ({ start, end }: { start: Date; end: Date }): void => - this.fulfillQuery({ - start, - end, - query, - subscriptionId, - requestInfo, - }); - - // TODO: Pass in refresh rate to customize the rate at which data is requested - this.scheduler.create({ - id: subscriptionId, - duration: parseDuration((requestInfo.viewport as MinimalLiveViewport).duration), - cb, - refreshRate: requestInfo.refreshRate, - }); - } - - /** - * Listen to every data cache change, and provide the data streams for every subscriber. - * - * TODO: Only publish when the corresponding data streams have changed. - */ - private publishToSubscriptions(): void { - this.dataCache.onChange(() => { - this.subscriptions.getSubscriptions().forEach((subscription) => this.publishDataStreams(subscription)); - }); - } - - /** - * Publish the queried data for the provided subscription - */ - private publishDataStreams({ query, emit }: Subscription): void { - const dataStreams = this.getDataStreams(this.dataSourceStore.getRequestsFromQuery(query)); - emit(dataStreams); - } - private update = ( subscriptionId: string, subscriptionUpdate: SubscriptionUpdate ): void => { - // Update subscription - this.subscriptions.updateSubscription(subscriptionId, subscriptionUpdate); - - // Publish updated information const subscription = this.subscriptions.getSubscription(subscriptionId); - const { requestInfo } = subscription; - const { viewport } = requestInfo; - - const requestStart = viewportStartDate(viewport); - const requestEnd = viewportEndDate(viewport); - - this.fulfillQuery({ - start: requestStart, - end: requestEnd, - query: subscription.query, - subscriptionId, - requestInfo, - }); - // If user updated the request info to contain duration and there is no internal clock attached to the - // subscription id, we will create an internal clock - if ('duration' in viewport && !this.scheduler.hasScheduler(subscriptionId)) { - this.registerQueryForPolling( - { query: subscription.query, requestInfo: subscription.requestInfo }, - subscriptionId - ); - } else { - // Otherwise we can call stop tick. - this.scheduler.remove(subscriptionId); + const updatedSubscription = Object.assign({}, subscription, subscriptionUpdate) as Subscription; + if ('query' in updatedSubscription) { + this.subscriptions.updateSubscription(subscriptionId, { + ...updatedSubscription, + fulfill: () => { + this.fulfillQuery({ + start: viewportStartDate(updatedSubscription.requestInfo.viewport), + end: viewportEndDate(updatedSubscription.requestInfo.viewport), + query: updatedSubscription.query, + requestInfo: updatedSubscription.requestInfo, + }); + }, + }); } }; private registerRequest = ( - subscription: Subscription, + subscription: { query: Query; requestInfo: Request }, requestInformations: RequestInformationAndRange[] ): void => { this.dataSourceStore.initiateRequest( @@ -248,15 +197,6 @@ export class IotAppKitDataModule implements DataModule { ); }; - /** - * Gets data streams corresponding to the data stream infos. - */ - private getDataStreams = (requestInformations: RequestInformation[]): DataStream[] => - toDataStreams({ - dataStreamsStores: this.dataCache.getState(), - dataStreamInfo: requestInformations, - }); - /** * Unsubscribe from the data module. * @@ -264,7 +204,6 @@ export class IotAppKitDataModule implements DataModule { * the previously queried data streams from being queried any longer. */ private unsubscribe = (subscriptionId: string): void => { - this.scheduler.remove(subscriptionId); this.subscriptions.removeSubscription(subscriptionId); }; } diff --git a/packages/core/src/data-module/data-cache/createStore.ts b/packages/core/src/data-module/data-cache/createStore.ts index 5a9c6e0e6..281e01622 100755 --- a/packages/core/src/data-module/data-cache/createStore.ts +++ b/packages/core/src/data-module/data-cache/createStore.ts @@ -1,8 +1,6 @@ -import { applyMiddleware, createStore, Store } from 'redux'; +import { createStore, Store } from 'redux'; -import thunk from 'redux-thunk'; import { dataReducer } from './dataReducer'; import { DataStreamsStore } from './types'; -export const configureStore = (initialState: DataStreamsStore = {}): Store => - createStore(dataReducer, initialState, applyMiddleware(thunk)); +export const configureStore = (initialState: DataStreamsStore = {}): Store => createStore(dataReducer, initialState); diff --git a/packages/core/src/data-module/data-cache/dataCacheWrapped.spec.ts b/packages/core/src/data-module/data-cache/dataCacheWrapped.spec.ts index 21a73047e..09ce68ba0 100644 --- a/packages/core/src/data-module/data-cache/dataCacheWrapped.spec.ts +++ b/packages/core/src/data-module/data-cache/dataCacheWrapped.spec.ts @@ -1,51 +1,66 @@ import { DataCache } from './dataCacheWrapped'; import { SECOND_IN_MS } from '../../common/time'; -import { DATA_STREAM } from '../../testing/__mocks__/mockWidgetProperties'; +import { DATA_STREAM, DATA_STREAM_INFO } from '../../testing/__mocks__/mockWidgetProperties'; +import { DataStreamsStore } from './types'; +import { EMPTY_CACHE } from './caching/caching'; it('initializes', () => { expect(() => new DataCache()).not.toThrowError(); }); -it('defaults to an empty cache', () => { - const dataCache = new DataCache(); - expect(dataCache.getState()).toEqual({}); -}); - -describe('onChange', () => { - it('no changes occur on initiation', () => { - const dataCache = new DataCache(); - const listener = jest.fn(); - - dataCache.onChange(listener); - expect(listener).not.toBeCalled(); +describe('shouldRequestDataStream', () => { + const ERR_MSG = 'An error has occurred!'; + + const CACHE_WITH_ERROR: DataStreamsStore = { + [DATA_STREAM_INFO.id]: { + [DATA_STREAM_INFO.resolution]: { + id: DATA_STREAM.id, + resolution: DATA_STREAM.resolution, + dataCache: EMPTY_CACHE, + requestCache: EMPTY_CACHE, + requestHistory: [], + isLoading: false, + isRefreshing: false, + error: ERR_MSG, + }, + }, + }; + + const CACHE_WITHOUT_ERROR: DataStreamsStore = { + [DATA_STREAM_INFO.id]: { + [DATA_STREAM_INFO.resolution]: { + id: DATA_STREAM.id, + resolution: DATA_STREAM.resolution, + dataCache: EMPTY_CACHE, + requestCache: EMPTY_CACHE, + requestHistory: [], + isLoading: false, + isRefreshing: false, + error: undefined, + }, + }, + }; + + it('should not request data stream when error associated with DataStream', () => { + const cache = new DataCache(CACHE_WITH_ERROR); + + expect(cache.shouldRequestDataStream({ dataStreamId: DATA_STREAM.id, resolution: DATA_STREAM.resolution })).toBe( + false + ); }); - it('change is emitted when action is made', () => { - const dataCache = new DataCache(); - - const ID = 'some-id'; - const RESOLUTION = SECOND_IN_MS; - - const listener = jest.fn(); - dataCache.onChange(listener); - dataCache.onRequest({ id: ID, resolution: RESOLUTION, first: new Date(), last: new Date() }); + it('should request data stream when no error associated with DataStream', () => { + const cache = new DataCache(CACHE_WITHOUT_ERROR); - expect(listener).toBeCalledTimes(1); + expect(cache.shouldRequestDataStream({ dataStreamId: DATA_STREAM.id, resolution: DATA_STREAM.resolution })).toBe( + true + ); }); +}); - it('change is emitted twice when two actions are made', () => { - const dataCache = new DataCache(); - - const ID = 'some-id'; - const RESOLUTION = SECOND_IN_MS; - - const listener = jest.fn(); - dataCache.onChange(listener); - dataCache.onRequest({ id: ID, resolution: RESOLUTION, first: new Date(), last: new Date() }); - dataCache.onRequest({ id: ID, resolution: RESOLUTION, first: new Date(), last: new Date() }); - - expect(listener).toBeCalledTimes(2); - }); +it('defaults to an empty cache', () => { + const dataCache = new DataCache(); + expect(dataCache.getState()).toEqual({}); }); describe('actions', () => { diff --git a/packages/core/src/data-module/data-cache/dataCacheWrapped.ts b/packages/core/src/data-module/data-cache/dataCacheWrapped.ts index a5a341467..aadd5b312 100644 --- a/packages/core/src/data-module/data-cache/dataCacheWrapped.ts +++ b/packages/core/src/data-module/data-cache/dataCacheWrapped.ts @@ -5,6 +5,27 @@ import { configureStore } from './createStore'; import { Request } from './requestTypes'; import { onErrorAction, onRequestAction, onSuccessAction } from './dataActions'; import { viewportEndDate, viewportStartDate } from '../../common/viewport'; +import { getDataStreamStore } from './getDataStreamStore'; +import { Observable, map, startWith, pairwise, from } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { DataStreamCallback, RequestInformation } from '../types'; +import { toDataStreams } from './toDataStreams'; + +type StoreChange = { prevDataCache: DataStreamsStore; currDataCache: DataStreamsStore }; + +/** + * Referential comparison of information related to the requested information. + */ +const hasRequestedInformationChanged = ( + { prevDataCache, currDataCache }: StoreChange, + requestInformation: RequestInformation +): boolean => { + const prevDataStreamStore = getDataStreamStore(requestInformation.id, requestInformation.resolution, prevDataCache); + const currDataStreamStore = getDataStreamStore(requestInformation.id, requestInformation.resolution, currDataCache); + + const hasChanged = prevDataStreamStore != currDataStreamStore; + return hasChanged; +}; /** * Data Cache Wrapper @@ -15,13 +36,52 @@ import { viewportEndDate, viewportStartDate } from '../../common/viewport'; */ export class DataCache { private dataCache: Store; + private observableStore: Observable; constructor(initialDataCache?: DataStreamsStore) { this.dataCache = configureStore(initialDataCache); + + this.observableStore = from(this.dataCache).pipe( + startWith(undefined), + pairwise(), + map(([prevDataCache, currDataCache]) => ({ prevDataCache, currDataCache })) + ); } - public onChange = (cb: () => void) => { - this.dataCache.subscribe(cb); + public subscribe = (requestInformations: RequestInformation[], emit: DataStreamCallback) => { + const subscription = this.observableStore + .pipe( + // Filter out any changes that don't effect the requested informations + filter(({ currDataCache, prevDataCache }) => + requestInformations.some((requestInformation) => + hasRequestedInformationChanged( + { + currDataCache, + prevDataCache, + }, + requestInformation + ) + ) + ) + ) + .subscribe((stores) => { + const dataStreams = toDataStreams({ + dataStreamsStores: stores.currDataCache, + requestInformations: requestInformations, + }); + + emit(dataStreams); + }); + + return () => { + subscription.unsubscribe(); + }; + }; + + public shouldRequestDataStream = ({ dataStreamId, resolution }: { dataStreamId: string; resolution: number }) => { + const associatedStore = getDataStreamStore(dataStreamId, resolution, this.getState()); + const hasError = associatedStore ? associatedStore.error != null : false; + return !hasError; }; public getState = (): DataStreamsStore => this.dataCache.getState(); diff --git a/packages/core/src/data-module/data-cache/getDataStreamStore.ts b/packages/core/src/data-module/data-cache/getDataStreamStore.ts index 91634f8d7..9c28b903e 100755 --- a/packages/core/src/data-module/data-cache/getDataStreamStore.ts +++ b/packages/core/src/data-module/data-cache/getDataStreamStore.ts @@ -3,9 +3,9 @@ import { DataStreamsStore, DataStreamStore } from './types'; export const getDataStreamStore = ( dataStreamId: string, resolution: number, - store: DataStreamsStore + store: DataStreamsStore | undefined ): DataStreamStore | undefined => { - const resolutionCache = store[dataStreamId]; + const resolutionCache = store && store[dataStreamId]; if (resolutionCache == null) { return undefined; } diff --git a/packages/core/src/data-module/data-cache/toDataStreams.spec.ts b/packages/core/src/data-module/data-cache/toDataStreams.spec.ts index 7b5e16087..5f4e191ea 100755 --- a/packages/core/src/data-module/data-cache/toDataStreams.spec.ts +++ b/packages/core/src/data-module/data-cache/toDataStreams.spec.ts @@ -42,15 +42,18 @@ const STORE_WITH_NUMBERS_ONLY: DataStreamsStore = { }; it('returns no data streams when provided no infos or stores', () => { - expect(toDataStreams({ dataStreamInfo: [], dataStreamsStores: {} })).toBeEmpty(); + expect(toDataStreams({ requestInformations: [], dataStreamsStores: {} })).toBeEmpty(); }); it('returns no data streams when provided no infos with a non-empty store', () => { - expect(toDataStreams({ dataStreamInfo: [], dataStreamsStores: STORE_WITH_NUMBERS_ONLY })).toBeEmpty(); + expect(toDataStreams({ requestInformations: [], dataStreamsStores: STORE_WITH_NUMBERS_ONLY })).toBeEmpty(); }); it('returns a single data stream containing all the available resolutions', () => { - const [stream] = toDataStreams({ dataStreamInfo: [ALARM_STREAM_INFO], dataStreamsStores: STORE_WITH_NUMBERS_ONLY }); + const [stream] = toDataStreams({ + requestInformations: [ALARM_STREAM_INFO], + dataStreamsStores: STORE_WITH_NUMBERS_ONLY, + }); expect(stream.resolution).toEqual(ALARM_STREAM_INFO.resolution); expect(stream.id).toEqual(ALARM_STREAM_INFO.id); // expect(stream.detailedName).toEqual(ALARM_STREAM_INFO.detailedName); diff --git a/packages/core/src/data-module/data-cache/toDataStreams.ts b/packages/core/src/data-module/data-cache/toDataStreams.ts index 8555cc022..e5e541afa 100755 --- a/packages/core/src/data-module/data-cache/toDataStreams.ts +++ b/packages/core/src/data-module/data-cache/toDataStreams.ts @@ -9,13 +9,13 @@ import { RequestInformation } from '../types.d'; * Returns the data streams, with the various resolutions of data collapsed into a single corresponding data stream. */ export const toDataStreams = ({ - dataStreamInfo, + requestInformations, dataStreamsStores, }: { - dataStreamInfo: RequestInformation[]; + requestInformations: RequestInformation[]; dataStreamsStores: DataStreamsStore; }): DataStream[] => { - return dataStreamInfo.map((info) => { + return requestInformations.map((info) => { const streamsResolutions = dataStreamsStores[info.id] || {}; const resolutions = Object.keys(streamsResolutions); const aggregatedData = resolutions diff --git a/packages/core/src/data-module/index.ts b/packages/core/src/data-module/index.ts index fb71a4edd..721e4f6b6 100644 --- a/packages/core/src/data-module/index.ts +++ b/packages/core/src/data-module/index.ts @@ -1,21 +1,29 @@ -import { IotAppKitDataModule } from './IotAppKitDataModule'; -import { DataModule } from './types.d'; +import { DataModule, RegisterDataSource, SubscribeToDataStreams, SubscribeToDataStreamsFrom } from './types.d'; import { createDataSource } from '../data-sources/site-wise/data-source'; import { sitewiseSdk } from '../data-sources/site-wise/sitewise-sdk'; import { Credentials, Provider } from '@aws-sdk/types'; import { SiteWiseAssetModule } from '../asset-modules'; +import { IotAppKitDataModule } from './IotAppKitDataModule'; + +let siteWiseAssetModule: SiteWiseAssetModule | undefined = undefined; /** - * register data sources + * Core public API */ +export const registerDataSource: RegisterDataSource = (dataModule, ...inputs) => + dataModule.registerDataSource(...inputs); -let dataModule: DataModule | undefined = undefined; -let siteWiseAssetModule: SiteWiseAssetModule | undefined = undefined; +export const subscribeToDataStreams: SubscribeToDataStreams = (dataModule, ...inputs) => + dataModule.subscribeToDataStreams(...inputs); + +export const subscribeToDataStreamsFrom: SubscribeToDataStreamsFrom = (dataModule, ...inputs) => + dataModule.subscribeToDataStreamsFrom(...inputs); /** * Initialize IoT App Kit * * @param awsCredentials - https://www.npmjs.com/package/@aws-sdk/credential-providers + * @param awsRegion - Region for AWS based data sources to point towards, i.e. us-east-1 */ export const initialize = ({ awsCredentials, @@ -25,26 +33,22 @@ export const initialize = ({ awsCredentials?: Credentials | Provider; awsRegion?: string; registerDataSources?: boolean; -}) => { - dataModule = new IotAppKitDataModule(); +}): DataModule => { + const dataModule = new IotAppKitDataModule(); if (registerDataSources && awsCredentials != null) { /** Automatically registered data sources */ const siteWiseSdk = sitewiseSdk(awsCredentials, awsRegion); - dataModule.registerDataSource(createDataSource(siteWiseSdk)); siteWiseAssetModule = new SiteWiseAssetModule(siteWiseSdk); + + registerDataSource(dataModule, createDataSource(sitewiseSdk(awsCredentials, awsRegion))); } else if (registerDataSources && awsCredentials == null) { console.warn( 'site-wise data-source failed to register. Must provide field `awsCredentials` for the site-wise data-source to register.' ); } -}; -export const getDataModule = (): DataModule => { - if (dataModule != null) { - return dataModule; - } - throw new Error('No data module initialize: you must first call initialize to ensure a data module is present.'); + return dataModule; }; export const getSiteWiseAssetModule = (): SiteWiseAssetModule => { diff --git a/packages/core/src/data-module/request-scheduler/requestScheduler.spec.ts b/packages/core/src/data-module/request-scheduler/requestScheduler.spec.ts index a8d29e12d..80ebdbe7f 100644 --- a/packages/core/src/data-module/request-scheduler/requestScheduler.spec.ts +++ b/packages/core/src/data-module/request-scheduler/requestScheduler.spec.ts @@ -82,5 +82,5 @@ it('returns true when the given id exists within the scheduler store', () => { const secondsElapsed = 2.4; jest.advanceTimersByTime(secondsElapsed * SECOND_IN_MS); - expect(scheduler.hasScheduler(id)).toBeTrue(); + expect(scheduler.isScheduled(id)).toBeTrue(); }); diff --git a/packages/core/src/data-module/request-scheduler/requestScheduler.ts b/packages/core/src/data-module/request-scheduler/requestScheduler.ts index e1f2e7f15..4b1d28385 100644 --- a/packages/core/src/data-module/request-scheduler/requestScheduler.ts +++ b/packages/core/src/data-module/request-scheduler/requestScheduler.ts @@ -2,10 +2,12 @@ import { SECOND_IN_MS } from '../../common/time'; const DEFAULT_REFRESH_RATE = 5 * SECOND_IN_MS; +type IntervalMap = { + [id: string]: number; +}; + export default class RequestScheduler { - private intervalMap: { - [id: string]: { start: Date; end: Date; intervalId?: number }; - } = {}; + private intervalMap: IntervalMap = {}; public create = ({ id, @@ -22,13 +24,8 @@ export default class RequestScheduler { return; } - this.intervalMap[id] = { start: new Date(new Date().getTime() - duration), end: new Date() }; - this.intervalMap[id].intervalId = (setInterval(() => { - const newStart = new Date(Date.now() - duration); - const newEnd = new Date(); - - this.intervalMap[id] = { ...this.intervalMap[id], start: newStart, end: newEnd }; - cb({ start: newStart, end: newEnd }); + this.intervalMap[id] = (setInterval(() => { + cb({ start: new Date(Date.now() - duration), end: new Date() }); }, refreshRate) as unknown) as number; }; @@ -37,9 +34,9 @@ export default class RequestScheduler { return; } - clearInterval(this.intervalMap[id].intervalId); + clearInterval(this.intervalMap[id]); delete this.intervalMap[id]; }; - public hasScheduler = (id: string): boolean => id in this.intervalMap; + public isScheduled = (id: string): boolean => id in this.intervalMap; } diff --git a/packages/core/src/data-module/subscription-store/subscriptionStore.spec.ts b/packages/core/src/data-module/subscription-store/subscriptionStore.spec.ts index 4192e7c5c..461bb4d0a 100644 --- a/packages/core/src/data-module/subscription-store/subscriptionStore.spec.ts +++ b/packages/core/src/data-module/subscription-store/subscriptionStore.spec.ts @@ -1,16 +1,33 @@ import SubscriptionStore from './subscriptionStore'; import { Subscription } from '../types.d'; -import { DATA_STREAM_INFO } from '../../testing/__mocks__/mockWidgetProperties'; -import { SiteWiseLegacyDataStreamQuery } from '../../data-sources/site-wise-legacy/types.d'; +import { DataCache } from '../data-cache/dataCacheWrapped'; +import DataSourceStore from '../data-source-store/dataSourceStore'; +import { SiteWiseDataStreamQuery } from '../../data-sources/site-wise/types'; +import { SITEWISE_DATA_SOURCE } from '../../data-sources'; -const MOCK_SUBSCRIPTION: Subscription = { +const createSubscriptionStore = () => { + const store = new DataSourceStore(); + store.registerDataSource({ + name: SITEWISE_DATA_SOURCE, + initiateRequest: () => {}, + getRequestsFromQuery: () => [], + }); + + return new SubscriptionStore({ + dataCache: new DataCache(), + dataSourceStore: store, + }); +}; + +const MOCK_SUBSCRIPTION: Subscription = { emit: () => {}, - query: { source: 'site-wise-legacy', dataStreamInfos: [] }, - requestInfo: { viewport: { start: new Date(), end: new Date() }, onlyFetchLatestValue: false }, + query: { source: SITEWISE_DATA_SOURCE, assets: [] }, + requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date() }, onlyFetchLatestValue: false }, + fulfill: () => {}, }; it('adds subscription', () => { - const subscriptionStore = new SubscriptionStore(); + const subscriptionStore = createSubscriptionStore(); subscriptionStore.addSubscription('some-id', MOCK_SUBSCRIPTION); expect(subscriptionStore.getSubscriptions()).toEqual([MOCK_SUBSCRIPTION]); @@ -18,22 +35,22 @@ it('adds subscription', () => { it('updates subscription', () => { const SUBSCRIPTION_ID = 'some-id'; - const subscriptionStore = new SubscriptionStore(); - const dataStreamInfos = [DATA_STREAM_INFO]; + const subscriptionStore = createSubscriptionStore(); + + const query = { + source: SITEWISE_DATA_SOURCE, + assets: [{ assetId: '123', propertyIds: ['prop1', 'prop2'] }], + }; subscriptionStore.addSubscription(SUBSCRIPTION_ID, MOCK_SUBSCRIPTION); - subscriptionStore.updateSubscription(SUBSCRIPTION_ID, { - query: { source: MOCK_SUBSCRIPTION.query.source, dataStreamInfos }, - }); + subscriptionStore.updateSubscription(SUBSCRIPTION_ID, { query }); - expect(subscriptionStore.getSubscriptions()).toEqual([ - { ...MOCK_SUBSCRIPTION, query: { source: 'site-wise-legacy', dataStreamInfos } }, - ]); + expect(subscriptionStore.getSubscriptions()).toEqual([{ ...MOCK_SUBSCRIPTION, query }]); }); it('removes subscription', () => { const SUBSCRIPTION_ID = 'some-id'; - const subscriptionStore = new SubscriptionStore(); + const subscriptionStore = createSubscriptionStore(); subscriptionStore.addSubscription(SUBSCRIPTION_ID, MOCK_SUBSCRIPTION); subscriptionStore.removeSubscription(SUBSCRIPTION_ID); @@ -41,7 +58,7 @@ it('removes subscription', () => { }); it('gets subscription by subscriptionId', () => { - const subscriptionStore = new SubscriptionStore(); + const subscriptionStore = createSubscriptionStore(); subscriptionStore.addSubscription('some-id', MOCK_SUBSCRIPTION); expect(subscriptionStore.getSubscription('some-id')).toEqual(MOCK_SUBSCRIPTION); @@ -49,18 +66,19 @@ it('gets subscription by subscriptionId', () => { describe('throws errors when', () => { it('throws error when trying to update non-existent subscription', () => { - const subscriptionStore = new SubscriptionStore(); + const subscriptionStore = createSubscriptionStore(); expect(() => subscriptionStore.updateSubscription('some-id', {})).toThrowError(/some-id/); }); it('throws error when trying to remove non-existent subscription', () => { - const subscriptionStore = new SubscriptionStore(); + const subscriptionStore = createSubscriptionStore(); expect(() => subscriptionStore.removeSubscription('some-id')).toThrowError(/some-id/); }); it('throws error when trying to add the same subscription id twice', () => { const SUBSCRIPTION_ID = 'some-id'; - const subscriptionStore = new SubscriptionStore(); + const subscriptionStore = createSubscriptionStore(); + subscriptionStore.addSubscription(SUBSCRIPTION_ID, MOCK_SUBSCRIPTION); expect(() => subscriptionStore.addSubscription(SUBSCRIPTION_ID, MOCK_SUBSCRIPTION)).toThrowError(/some-id/); }); diff --git a/packages/core/src/data-module/subscription-store/subscriptionStore.ts b/packages/core/src/data-module/subscription-store/subscriptionStore.ts index 541adcf1d..ab986531a 100644 --- a/packages/core/src/data-module/subscription-store/subscriptionStore.ts +++ b/packages/core/src/data-module/subscription-store/subscriptionStore.ts @@ -1,4 +1,9 @@ import { AnyDataStreamQuery, DataStreamQuery, Subscription, SubscriptionUpdate } from '../types.d'; +import { DataCache } from '../data-cache/dataCacheWrapped'; +import DataSourceStore from '../data-source-store/dataSourceStore'; +import RequestScheduler from '../request-scheduler/requestScheduler'; +import { MinimalLiveViewport } from '@synchro-charts/core'; +import { parseDuration } from '../../common/time'; /** * Subscription store @@ -6,16 +11,58 @@ import { AnyDataStreamQuery, DataStreamQuery, Subscription, SubscriptionUpdate } * Manages the collection of subscriptions */ export default class SubscriptionStore { + private dataSourceStore: DataSourceStore; + private dataCache: DataCache; + private unsubscribeMap: { [subscriberId: string]: Function } = {}; + private scheduler: RequestScheduler = new RequestScheduler(); private subscriptions: { [subscriptionId: string]: Subscription } = {}; + constructor({ dataSourceStore, dataCache }: { dataSourceStore: DataSourceStore; dataCache: DataCache }) { + this.dataCache = dataCache; + this.dataSourceStore = dataSourceStore; + } + addSubscription(subscriptionId: string, subscription: Subscription): void { - if (this.subscriptions[subscriptionId] != null) { + if (this.subscriptions[subscriptionId] == null) { + /** + * If the subscription is query based + */ + if ('query' in subscription) { + subscription.fulfill(); + + if ('duration' in subscription.requestInfo.viewport) { + /** has a duration, so periodically request for data */ + this.scheduler.create({ + id: subscriptionId, + cb: () => subscription.fulfill(), + duration: parseDuration((subscription.requestInfo.viewport as MinimalLiveViewport).duration), + refreshRate: subscription.requestInfo.refreshRate, + }); + } + + // Subscribe to changes from the data cache + const unsubscribe = this.dataCache.subscribe( + this.dataSourceStore.getRequestsFromQuery(subscription.query), + subscription.emit + ); + + this.unsubscribeMap[subscriptionId] = () => { + // unsubscribe from listening to the data cache changes + unsubscribe(); + + // unsubscribe from re-occurring requests + if (this.scheduler.isScheduled(subscriptionId)) { + this.scheduler.remove(subscriptionId); + } + }; + + this.subscriptions[subscriptionId] = subscription; + } + } else { throw new Error( `Attempted to add a subscription with an id of "${subscriptionId}", but the provided subscriptionId is already present.` ); } - - this.subscriptions[subscriptionId] = subscription; } updateSubscription( @@ -28,10 +75,14 @@ export default class SubscriptionStore { ); } - this.subscriptions[subscriptionId] = { + const updatedSubscription = { ...this.subscriptions[subscriptionId], ...subscriptionUpdate, }; + + this.removeSubscription(subscriptionId); + + this.addSubscription(subscriptionId, updatedSubscription); } removeSubscription = (subscriptionId: string): void => { @@ -41,10 +92,15 @@ export default class SubscriptionStore { ); } + if (this.unsubscribeMap[subscriptionId]) { + this.unsubscribeMap[subscriptionId](); + delete this.unsubscribeMap[subscriptionId]; + } + delete this.subscriptions[subscriptionId]; }; - getSubscriptions = (): Subscription[] => Object.values(this.subscriptions); + getSubscriptions = (): Subscription[] => Object.values(this.subscriptions); - getSubscription = (subscriptionId: string): Subscription => this.subscriptions[subscriptionId]; + getSubscription = (subscriptionId: string): Subscription => this.subscriptions[subscriptionId]; } diff --git a/packages/core/src/data-module/types.d.ts b/packages/core/src/data-module/types.d.ts index a8de543ac..774227800 100644 --- a/packages/core/src/data-module/types.d.ts +++ b/packages/core/src/data-module/types.d.ts @@ -15,12 +15,28 @@ export type DataSource = { export type DataStreamCallback = (dataStreams: DataStream[]) => void; -export type Subscription = { - query: Query; - requestInfo: Request; +export type QuerySubscription = + | { + query: Query; + requestInfo: Request; + emit: DataStreamCallback; + // Initiate requests for the subscription + fulfill: () => void; + } + | { + emit: DataStreamCallback; + source: string; + }; + +export type SourceSubscription = { emit: DataStreamCallback; + source: string; }; +export type Subscription = + | QuerySubscription + | SourceSubscription; + export type DataModuleSubscription = { requestInfo: Request; query: Query; @@ -32,16 +48,33 @@ export type DataStreamQuery = { export type AnyDataStreamQuery = DataStreamQuery & any; +export type ErrorCallback = ({ id, resolution, error }) => void; + export type SubscriptionUpdate = Partial, 'emit'>>; export type DataSourceRequest = { requestInfo: Request; query: Query; onSuccess: DataStreamCallback; - onError: Function; + onError: ErrorCallback; +}; + +/** + * Subscribe to data streams + * + * Adds a subscription to the data-module. + * The data-module will ensure that the requested data is provided to the subscriber. + */ +export type SubscribeToDataStreams = ( + dataModule: DataModule, + { query, requestInfo }: DataModuleSubscription, + callback: DataStreamCallback +) => { + unsubscribe: () => void; + update: (subscriptionUpdate: SubscriptionUpdate) => void; }; -type SubscribeToDataStreams = ( +type SubscribeToDataStreamsPrivate = ( { query, requestInfo }: DataModuleSubscription, callback: DataStreamCallback ) => { @@ -49,20 +82,39 @@ type SubscribeToDataStreams = ( update: (subscriptionUpdate: SubscriptionUpdate) => void; }; +/** + * Subscribe to data streams from a custom data source + * + * Adds a subscription to the data-module, pointing to some existing source + */ +export type SubscribeToDataStreamsFrom = ( + dataModule: DataModule, + source: string, + emit: DataStreamCallback +) => { + unsubscribe: () => void; +}; + +type SubscribeToDataStreamsFromPrivate = ( + source: string, + emit: DataStreamCallback +) => { + unsubscribe: () => void; +}; + +/** + * Register custom data source to the data module. + */ +export type RegisterDataSource = ( + dataModule: DataModule, + dataSource: DataSource +) => void; + /** * The core of the IoT App Kit, manages the data, and getting data to those who subscribe. */ export interface DataModule { - /** - * Register custom data source to the data module. - */ - registerDataSource: (dataSource: DataSource) => void; - - /** - * Subscribe to data streams - * - * Adds a subscription to the data-module. - * The data-module will ensure that the requested data is provided to the subscriber. - */ - subscribeToDataStreams: SubscribeToDataStreams; + registerDataSource: RegisterDataSourcePrivate; + subscribeToDataStreams: SubscribeToDataStreamsPrivate; + subscribeToDataStreamsFrom: SubscribeToDataStreamsFromPrivate; } diff --git a/packages/core/src/data-sources/site-wise/client/client.ts b/packages/core/src/data-sources/site-wise/client/client.ts index 7ead065be..9c512bbfa 100644 --- a/packages/core/src/data-sources/site-wise/client/client.ts +++ b/packages/core/src/data-sources/site-wise/client/client.ts @@ -3,7 +3,7 @@ import { SiteWiseDataStreamQuery } from '../types'; import { getLatestPropertyDataPoint } from './getLatestPropertyDataPoint'; import { getHistoricalPropertyDataPoints } from './getHistoricalPropertyDataPoints'; import { getAggregatedPropertyDataPoints } from './getAggregatedPropertyDataPoints'; -import { DataStreamCallback } from '../../../data-module/types.d'; +import { DataStreamCallback, ErrorCallback } from '../../../data-module/types.d'; export class SiteWiseClient { private siteWiseSdk: IoTSiteWiseClient; @@ -15,7 +15,7 @@ export class SiteWiseClient { getLatestPropertyDataPoint(options: { query: SiteWiseDataStreamQuery; onSuccess: DataStreamCallback; - onError: Function; + onError: ErrorCallback; }): Promise { return getLatestPropertyDataPoint({ client: this.siteWiseSdk, ...options }); } @@ -25,7 +25,7 @@ export class SiteWiseClient { start: Date; end: Date; maxResults?: number; - onError: Function; + onError: ErrorCallback; onSuccess: DataStreamCallback; }): Promise { return getHistoricalPropertyDataPoints({ client: this.siteWiseSdk, ...options }); diff --git a/packages/core/src/data-sources/site-wise/client/getHistoricalPropertyDataPoints.ts b/packages/core/src/data-sources/site-wise/client/getHistoricalPropertyDataPoints.ts index 5a084c20a..f341b2810 100644 --- a/packages/core/src/data-sources/site-wise/client/getHistoricalPropertyDataPoints.ts +++ b/packages/core/src/data-sources/site-wise/client/getHistoricalPropertyDataPoints.ts @@ -2,8 +2,9 @@ import { GetAssetPropertyValueHistoryCommand, IoTSiteWiseClient, TimeOrdering } import { AssetId, AssetPropertyId, SiteWiseDataStreamQuery } from '../types'; import { toDataPoint } from '../util/toDataPoint'; import { dataStreamFromSiteWise } from '../dataStreamFromSiteWise'; -import { DataStreamCallback } from '../../../data-module/types'; +import { DataStreamCallback, ErrorCallback } from '../../../data-module/types'; import { isDefined } from '../../../common/predicates'; +import { toDataStreamId } from '../util/dataStreamId'; const getHistoricalPropertyDataPointsForProperty = ({ assetId, @@ -12,6 +13,7 @@ const getHistoricalPropertyDataPointsForProperty = ({ end, maxResults, onSuccess, + onError, nextToken: prevToken, client, }: { @@ -20,6 +22,7 @@ const getHistoricalPropertyDataPointsForProperty = ({ start: Date; end: Date; maxResults?: number; + onError: ErrorCallback; onSuccess: DataStreamCallback; client: IoTSiteWiseClient; nextToken?: string; @@ -53,11 +56,16 @@ const getHistoricalPropertyDataPointsForProperty = ({ start, end, maxResults, + onError, onSuccess, nextToken, client, }); } + }) + .catch((err) => { + const id = toDataStreamId({ assetId, propertyId }); + onError({ id, resolution: 0, error: err.message }); }); }; @@ -74,7 +82,7 @@ export const getHistoricalPropertyDataPoints = async ({ start: Date; end: Date; maxResults?: number; - onError: Function; + onError: ErrorCallback; onSuccess: DataStreamCallback; client: IoTSiteWiseClient; }) => { @@ -89,12 +97,11 @@ export const getHistoricalPropertyDataPoints = async ({ end, maxResults, onSuccess, + onError, }) ) ) .flat(); - await Promise.all(requests).catch((err) => { - onError(err); - }); + await Promise.all(requests); }; diff --git a/packages/core/src/data-sources/site-wise/client/getLatestPropertyDataPoint.ts b/packages/core/src/data-sources/site-wise/client/getLatestPropertyDataPoint.ts index 4369074ae..5486e12a5 100644 --- a/packages/core/src/data-sources/site-wise/client/getLatestPropertyDataPoint.ts +++ b/packages/core/src/data-sources/site-wise/client/getLatestPropertyDataPoint.ts @@ -3,7 +3,8 @@ import { SiteWiseDataStreamQuery } from '../types'; import { toDataPoint } from '../util/toDataPoint'; import { isDefined } from '../../../common/predicates'; import { dataStreamFromSiteWise } from '../dataStreamFromSiteWise'; -import { DataStreamCallback } from '../../../data-module/types'; +import { DataStreamCallback, ErrorCallback } from '../../../data-module/types'; +import { toDataStreamId } from '../util/dataStreamId'; export const getLatestPropertyDataPoint = async ({ query: { assets }, @@ -13,27 +14,32 @@ export const getLatestPropertyDataPoint = async ({ }: { query: SiteWiseDataStreamQuery; onSuccess: DataStreamCallback; - onError: Function; + onError: ErrorCallback; client: IoTSiteWiseClient; }): Promise => { const requests = assets .map(({ assetId, propertyIds }) => - propertyIds.map((propertyId) => - client.send(new GetAssetPropertyValueCommand({ assetId, propertyId })).then((res) => ({ - dataPoints: [toDataPoint(res.propertyValue)].filter(isDefined), - assetId, - propertyId, - })) - ) + propertyIds.map((propertyId) => { + return client + .send(new GetAssetPropertyValueCommand({ assetId, propertyId })) + .then((res) => ({ + dataPoints: [toDataPoint(res.propertyValue)].filter(isDefined), + assetId, + propertyId, + })) + .catch((error) => { + const dataStreamId = toDataStreamId({ assetId, propertyId }); + onError({ id: dataStreamId, resolution: 0, error: error.message }); + return undefined; + }); + }) ) .flat(); - await Promise.all(requests) - .then((results) => { - const dataStreams = results.map(dataStreamFromSiteWise); + await Promise.all(requests).then((results) => { + const dataStreams = results.filter(isDefined).map(dataStreamFromSiteWise); + if (dataStreams.length > 0) { onSuccess(dataStreams); - }) - .catch((errorMessage: string) => { - onError(errorMessage); - }); + } + }); }; diff --git a/packages/core/src/data-sources/site-wise/data-source.spec.ts b/packages/core/src/data-sources/site-wise/data-source.spec.ts index 9b5956451..6d0bf7eae 100644 --- a/packages/core/src/data-sources/site-wise/data-source.spec.ts +++ b/packages/core/src/data-sources/site-wise/data-source.spec.ts @@ -6,6 +6,7 @@ import { SiteWiseDataStreamQuery } from './types.d'; import { ASSET_PROPERTY_DOUBLE_VALUE } from '../../common/tests/mocks/assetPropertyValue'; import { createSiteWiseSDK } from '../../common/tests/util'; import { toDataStreamId } from './util/dataStreamId'; +import { IotAppKitDataModule } from '../../data-module/IotAppKitDataModule'; it('initializes', () => { expect(() => createDataSource(new IoTSiteWiseClient({ region: 'us-east' }))).not.toThrowError(); @@ -56,7 +57,7 @@ describe('initiateRequest', () => { describe('on error', () => { it('calls `onError` callback', async () => { const ERR_MESSAGE = 'some critical error! page oncall immediately'; - const getAssetPropertyValue = jest.fn().mockRejectedValue(ERR_MESSAGE); + const getAssetPropertyValue = jest.fn().mockRejectedValue(new Error(ERR_MESSAGE)); const mockSDK = createSiteWiseSDK({ getAssetPropertyValue }); @@ -91,7 +92,11 @@ describe('initiateRequest', () => { await flushPromises(); expect(onSuccess).not.toBeCalled(); - expect(onError).toBeCalledWith(ERR_MESSAGE); + expect(onError).toBeCalledWith({ + id: toDataStreamId({ assetId: ASSET_1, propertyId: PROPERTY_1 }), + resolution: 0, + error: ERR_MESSAGE, + }); }); }); @@ -251,3 +256,87 @@ describe('initiateRequest', () => { }); }); }); + +describe('e2e through data-module', () => { + describe('fetching range of historical data', () => { + it('reports error occurred on request initiation', async () => { + const dataModule = new IotAppKitDataModule(); + + const ERR_MESSAGE = 'some critical error! page oncall immediately'; + const getAssetPropertyValueHistory = jest.fn().mockRejectedValue(new Error(ERR_MESSAGE)); + + const mockSDK = createSiteWiseSDK({ getAssetPropertyValueHistory }); + const dataSource = createDataSource(mockSDK); + + dataModule.registerDataSource(dataSource); + + const dataStreamCallback = jest.fn(); + const assetId = 'asset-id'; + const propertyId = 'property-id'; + + dataModule.subscribeToDataStreams( + { + query: { + assets: [{ assetId, propertyIds: [propertyId] }], + source: dataSource.name, + } as SiteWiseDataStreamQuery, + requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date() }, onlyFetchLatestValue: false }, + }, + dataStreamCallback + ); + + await flushPromises(); + + expect(dataStreamCallback).toBeCalledTimes(2); + expect(dataStreamCallback).toHaveBeenLastCalledWith([ + expect.objectContaining({ + id: toDataStreamId({ assetId, propertyId }), + error: ERR_MESSAGE, + isLoading: false, + isRefreshing: false, + }), + ]); + }); + }); + + describe('fetching latest value', () => { + it('reports error occurred on request initiation', async () => { + const dataModule = new IotAppKitDataModule(); + + const ERR_MESSAGE = 'some critical error! page oncall immediately'; + const getAssetPropertyValue = jest.fn().mockRejectedValue(new Error(ERR_MESSAGE)); + + const mockSDK = createSiteWiseSDK({ getAssetPropertyValue }); + const dataSource = createDataSource(mockSDK); + + dataModule.registerDataSource(dataSource); + + const dataStreamCallback = jest.fn(); + const assetId = 'asset-id'; + const propertyId = 'property-id'; + + dataModule.subscribeToDataStreams( + { + query: { + assets: [{ assetId, propertyIds: [propertyId] }], + source: dataSource.name, + } as SiteWiseDataStreamQuery, + requestInfo: { viewport: { start: new Date(2000, 0, 0), end: new Date() }, onlyFetchLatestValue: true }, + }, + dataStreamCallback + ); + + await flushPromises(); + + expect(dataStreamCallback).toBeCalledTimes(2); + expect(dataStreamCallback).toHaveBeenLastCalledWith([ + expect.objectContaining({ + id: toDataStreamId({ assetId, propertyId }), + error: ERR_MESSAGE, + isLoading: false, + isRefreshing: false, + }), + ]); + }); + }); +}); diff --git a/packages/core/src/testing/__mocks__/mockWidgetProperties.ts b/packages/core/src/testing/__mocks__/mockWidgetProperties.ts index 9c80d2c95..adad67732 100755 --- a/packages/core/src/testing/__mocks__/mockWidgetProperties.ts +++ b/packages/core/src/testing/__mocks__/mockWidgetProperties.ts @@ -29,6 +29,7 @@ export const NUMBER_INFO_1: DataStreamInfo = { color: 'cyan', name: 'number-some-name', }; + export const NUMBER_STREAM_1: DataStream = { id: NUMBER_INFO_1.id, color: 'cyan', diff --git a/packages/core/stencil.config.ts b/packages/core/stencil.config.ts index d2632e7b6..d8cf459a6 100755 --- a/packages/core/stencil.config.ts +++ b/packages/core/stencil.config.ts @@ -24,10 +24,10 @@ export const config: Config = { modulePathIgnorePatterns: ['cypress'], coverageThreshold: { global: { - branches: 60, - functions: 70, - lines: 70, - statements: 70, + statements: 80, + branches: 65, + functions: 75, + lines: 80, }, }, }, diff --git a/packages/related-table/package.json b/packages/related-table/package.json index 3c34b4a83..88ad858cd 100644 --- a/packages/related-table/package.json +++ b/packages/related-table/package.json @@ -22,9 +22,7 @@ "clean": "rm -rf dist", "build": "microbundle --no-compress --format modern,esm,cjs --jsx React.createElement", "generate-barrels": "barrelsby --config ./barrel-config.json", - "test": "yarn run lint && jest", - "lint": "eslint src --max-warnings=2", - "lint:fix": "eslint src --fix", + "test": "jest", "storybook": "start-storybook -p 6006", "prepublishOnly": "npm run build", "copy:license": "cp ../../LICENSE LICENSE", diff --git a/packages/related-table/src/RelatedTable/Common/TreeLines.tsx b/packages/related-table/src/RelatedTable/Common/TreeLines.tsx index 26df2d4c6..f052f4876 100644 --- a/packages/related-table/src/RelatedTable/Common/TreeLines.tsx +++ b/packages/related-table/src/RelatedTable/Common/TreeLines.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/no-array-index-key */ import * as awsui from '@awsui/design-tokens'; import React from 'react'; import { ITreeNode, LinePrefixTypes } from '../../Model/TreeNode'; diff --git a/packages/related-table/src/utils/build.ts b/packages/related-table/src/utils/build.ts index 3f87e6307..3acbacc1b 100644 --- a/packages/related-table/src/utils/build.ts +++ b/packages/related-table/src/utils/build.ts @@ -20,6 +20,7 @@ const createOrSetParentNode = ( const updateNode = (node: ITreeNode, newData: T) => { Object.keys(newData).forEach((prop) => { + // eslint-disable-next-line no-param-reassign (node as any)[prop] = (newData as any)[prop]; }); };