Skip to content

Commit

Permalink
feat(dashboard): query for describing unmodeled data stream
Browse files Browse the repository at this point in the history
  • Loading branch information
corteggiano authored and diehbria committed Nov 21, 2023
1 parent a93613a commit 2d1226d
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DescribeTimeSeriesCommand, IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise';
import invariant from 'tiny-invariant';

const isEnabled = (input?: string): input is string => Boolean(input);

export const getDescribedTimeSeries = async ({
client,
assetId,
propertyId,
alias,
}: {
client: IoTSiteWiseClient;
assetId?: string;
propertyId?: string;
alias?: string;
}) => {
invariant(
isEnabled(alias) || (isEnabled(assetId) && isEnabled(propertyId)),
'Expected alias or assetID+propertyID to be defined as required by the enabled flag.'
);

const params = { assetId, propertyId, alias };
const command = new DescribeTimeSeriesCommand(params);

try {
const data = await client.send(command);
return { data, isSuccess: true };
} catch (error) {
console.error(`Failed to get described time series. Error: ${error}`);
console.info('Request input:');
console.table(params);

throw error;
}
};
10 changes: 6 additions & 4 deletions packages/dashboard/src/components/csvDownloadButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ import { Button, ButtonProps } from '@cloudscape-design/components';
import { unparse } from 'papaparse';
import { StyledSiteWiseQueryConfig } from '~/customization/widgets/types';
import { useViewportData } from '~/components/csvDownloadButton/useViewportData';
import { IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise';

const CSVDownloadButton = ({
queryConfig,
fileName,
client,
...rest
}: { queryConfig: StyledSiteWiseQueryConfig; fileName: string } & ButtonProps) => {
const { fetchViewportData } = useViewportData({ queryConfig });
}: { queryConfig: StyledSiteWiseQueryConfig; client: IoTSiteWiseClient; fileName: string } & ButtonProps) => {
const { fetchViewportData } = useViewportData({ queryConfig, client });

const onClickDownload = () => {
const onClickDownload = async () => {
const requestDateMS = Date.now();
const requestDateString = new Date(requestDateMS).toISOString();

const data = fetchViewportData(requestDateMS);
const data = await fetchViewportData(requestDateMS);
const stringCSVData = unparse(data);

const file = new Blob([stringCSVData], { type: 'text/csv' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export type CSVDownloadObject = {
propertyAlias?: string;
assetId?: string;
dataType?: string;
dataTypeSpec?: string;
propertyId?: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { StyledSiteWiseQueryConfig } from '~/customization/widgets/types';
import { useAssetDescriptionMapQuery } from '~/hooks/useAssetDescriptionQueries';
import { useQueries } from '~/components/dashboard/queryContext';
import { CSVDownloadObject } from './types';
import { IoTSiteWiseClient, Quality } from '@aws-sdk/client-iotsitewise';
import { getDescribedTimeSeries } from './getDescribedTimeSeries';

const DEFAULT_VIEWPORT = { duration: '10m' };

Expand Down Expand Up @@ -31,12 +33,13 @@ const isTimeWithinViewport = (dataPointTimestamp: number, viewport: Viewport, ti
export const useViewportData = ({
queryConfig,
viewport: passedInViewport,
client,
}: {
queryConfig: StyledSiteWiseQueryConfig;
client: IoTSiteWiseClient;
viewport?: Viewport;
}) => {
const queries = useQueries(queryConfig.query);

const { dataStreams } = useTimeSeriesData({ queries });

const describedAssetsMapQuery = useAssetDescriptionMapQuery(queryConfig.query);
Expand All @@ -46,50 +49,74 @@ export const useViewportData = ({
const viewport = passedInViewport || injectedViewport || DEFAULT_VIEWPORT;

// flatten all the data in a single dataStream into one array of CSVDownloadObject
const flattenDataPoints = (dataStream: DataStream, timeOfRequestMS: number) => {
const flattenDataPoints = async (dataStream: DataStream, timeOfRequestMS: number) => {
const { id, unit, resolution, aggregationType, data } = dataStream;
const isUnmodeledData = queryConfig.query?.properties?.some((pr) => pr.propertyAlias === id);
const { data: unmodeledDescribedTimeSeries } = isUnmodeledData
? await getDescribedTimeSeries({
client,
alias: id,
})
: { data: undefined };

return data.reduce((flattenedData: CSVDownloadObject[], currentDataPoint: DataPoint) => {
const { x: xValue, y: yValue } = currentDataPoint;
const pointWithinViewport = isTimeWithinViewport(xValue, viewport, timeOfRequestMS);

// do not include data point if it falls outside viewport range
if (pointWithinViewport) {
const isUnmodeledData = queryConfig.query?.properties?.some((pr) => pr.propertyAlias === dataStream.id);
const assetPropId = !isUnmodeledData ? dataStream.id.split('---') : []; // modeled datastream IDs follow the pattern {assetID}---{propertyID}
const describedModelProperty = describedAssetsMap[assetPropId[0]]?.properties.find(
(p) => p.propertyId === assetPropId[1]
);

const flatDataPoint: CSVDownloadObject = {
assetName: describedAssetsMap[assetPropId[0]]?.assetName,
propertyName: describedModelProperty?.name,
propertyAlias: isUnmodeledData ? id : describedModelProperty?.alias,
const commonData = {
value: yValue,
unit,
timestamp: new Date(xValue).toISOString(),
aggregationType: aggregationType,
resolution,
dataType: describedModelProperty?.dataType,
dataQuality: 'GOOD',
assetId: !isUnmodeledData ? assetPropId[0] : undefined,
propertyId: !isUnmodeledData ? assetPropId[1] : undefined,
dataQuality: Quality.GOOD,
};

flattenedData.push(flatDataPoint);
if (isUnmodeledData) {
const unmodeledDataPoint: CSVDownloadObject = {
...commonData,
propertyAlias: unmodeledDescribedTimeSeries?.alias,
dataType: unmodeledDescribedTimeSeries?.dataType,
dataTypeSpec: unmodeledDescribedTimeSeries?.dataTypeSpec,
assetId: unmodeledDescribedTimeSeries?.assetId,
propertyId: undefined,
};

flattenedData.push(unmodeledDataPoint);
} else {
const assetPropId = dataStream.id.split('---');
const describedModelProperty = describedAssetsMap[assetPropId[0]]?.properties.find(
({ propertyId }) => propertyId === assetPropId[1]
);

const modeledDataPoint: CSVDownloadObject = {
...commonData,
assetName: describedAssetsMap[assetPropId[0]]?.assetName,
propertyName: describedModelProperty?.name,
propertyAlias: describedModelProperty?.alias,
dataType: describedModelProperty?.dataType,
dataTypeSpec: undefined,
assetId: assetPropId[0],
propertyId: assetPropId[1],
};

flattenedData.push(modeledDataPoint);
}
}
return flattenedData;
}, [] as CSVDownloadObject[]);
};

const fetchViewportData = (timeOfRequestMS: number) => {
return dataStreams.reduce((flattenedStreams: CSVDownloadObject[], currentDataStream: DataStream) => {
flattenedStreams.push(...flattenDataPoints(currentDataStream, timeOfRequestMS));
return flattenedStreams;
}, [] as CSVDownloadObject[]);
const fetchViewportData = async (timeOfRequestMS: number) => {
const promises = dataStreams.map((dataStream: DataStream) => flattenDataPoints(dataStream, timeOfRequestMS));
const flatData = await Promise.all(promises);
return flatData.flat(1);
};

return {
fetchViewportData,
canDownloadData: dataStreams.length === 0,
};
};
5 changes: 4 additions & 1 deletion packages/dashboard/src/components/widgets/tile/tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { onChangeDashboardGridEnabledAction } from '~/store/actions';
import CSVDownloadButton from '~/components/csvDownloadButton';
import { StyledSiteWiseQueryConfig } from '~/customization/widgets/types';
import { useGetConfigValue } from '@iot-app-kit/react-components';
import { useClients } from '~/components/dashboard/clientContext';

type DeletableTileActionProps = {
handleDelete: CancelableEventHandler<ClickDetail>;
Expand Down Expand Up @@ -56,6 +57,7 @@ const WidgetTile: React.FC<WidgetTileProps> = ({ children, widget, title, remove
const isReadOnly = useSelector((state: DashboardState) => state.readOnly);
const dispatch = useDispatch();
const [visible, setVisible] = useState(false);
const { iotSiteWiseClient } = useClients();
const { onDelete } = useDeleteWidgets();
const isCSVDownloadEnabled = useGetConfigValue('useCSVDownload');

Expand Down Expand Up @@ -104,8 +106,9 @@ const WidgetTile: React.FC<WidgetTileProps> = ({ children, widget, title, remove
{title}
</Box>
<SpaceBetween size='s' direction='horizontal'>
{widget.type !== 'text' && isCSVDownloadEnabled && (
{widget.type !== 'text' && isCSVDownloadEnabled && iotSiteWiseClient && (
<CSVDownloadButton
client={iotSiteWiseClient}
queryConfig={widget.properties.queryConfig as StyledSiteWiseQueryConfig}
fileName={`${widget.properties.title ?? widget.type}`}
/>
Expand Down

0 comments on commit 2d1226d

Please sign in to comment.