diff --git a/packages/react-components/src/components/alarm-state/alarm-state-text.css b/packages/react-components/src/components/alarm-state/alarm-state-text.css
new file mode 100644
index 000000000..59ea4062c
--- /dev/null
+++ b/packages/react-components/src/components/alarm-state/alarm-state-text.css
@@ -0,0 +1,3 @@
+.alarm-state-text {
+ display: flex;
+}
diff --git a/packages/react-components/src/components/alarm-state/alarm-state-text.tsx b/packages/react-components/src/components/alarm-state/alarm-state-text.tsx
new file mode 100644
index 000000000..e88eb4201
--- /dev/null
+++ b/packages/react-components/src/components/alarm-state/alarm-state-text.tsx
@@ -0,0 +1,155 @@
+import React from 'react';
+import Box from '@cloudscape-design/components/box';
+import Icon from '@cloudscape-design/components/icon';
+import {
+ colorBorderStatusError,
+ colorBorderStatusSuccess,
+ colorBorderStatusWarning,
+ colorTextStatusInactive,
+ spaceScaledXxs,
+} from '@cloudscape-design/design-tokens';
+
+import './alarm-state-text.css';
+import { PascalCaseStateName } from '../../hooks/useAlarms/transformers';
+
+type AlarmStateTextOptions = {
+ state?: PascalCaseStateName;
+ inheritFontColor?: boolean;
+};
+
+export const AlarmStateText = ({
+ state,
+ inheritFontColor,
+}: AlarmStateTextOptions) => {
+ let icon = null;
+ let text = null;
+ let styles: React.CSSProperties = {
+ gap: spaceScaledXxs,
+ textDecoration: 'none',
+ };
+
+ let borderBottom = '1px dashed ';
+
+ switch (state) {
+ case 'Active':
+ icon = (
+
+ );
+ text = (
+
+ Active alarm
+
+ );
+ borderBottom += inheritFontColor ? 'inherit' : colorBorderStatusError;
+ styles = {
+ ...styles,
+ borderBottom: borderBottom,
+ };
+ break;
+ case 'Normal':
+ icon = (
+
+ );
+ text = (
+
+ Normal
+
+ );
+ borderBottom += inheritFontColor ? 'inherit' : colorBorderStatusSuccess;
+ styles = {
+ ...styles,
+ borderBottom: borderBottom,
+ };
+ break;
+ case 'Latched':
+ icon = (
+
+ );
+ text = (
+
+ Latched alarm
+
+ );
+ borderBottom += inheritFontColor ? 'inherit' : colorBorderStatusWarning;
+ styles = {
+ ...styles,
+ borderBottom: borderBottom,
+ };
+ break;
+ case 'Acknowledged':
+ icon = (
+
+ );
+ text = (
+
+ Acknowledged alarm
+
+ );
+ borderBottom += inheritFontColor ? 'inherit' : colorTextStatusInactive;
+ styles = {
+ ...styles,
+ borderBottom: borderBottom,
+ };
+ break;
+ case 'Disabled':
+ icon = (
+
+ );
+ text = (
+
+ Disabled alarm
+
+ );
+ borderBottom += inheritFontColor ? 'inherit' : colorTextStatusInactive;
+ styles = {
+ ...styles,
+ borderBottom: borderBottom,
+ };
+ break;
+ case 'SnoozeDisabled':
+ icon = (
+
+ );
+ text = (
+
+ Snoozed alarm
+
+ );
+ borderBottom += inheritFontColor ? 'inherit' : colorTextStatusInactive;
+ styles = {
+ ...styles,
+ borderBottom: borderBottom,
+ };
+ break;
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/react-components/src/components/data-quality/data-quality-text.tsx b/packages/react-components/src/components/data-quality/data-quality-text.tsx
index e40acc74c..724021296 100644
--- a/packages/react-components/src/components/data-quality/data-quality-text.tsx
+++ b/packages/react-components/src/components/data-quality/data-quality-text.tsx
@@ -1,60 +1,48 @@
import React from 'react';
import { Quality } from '@aws-sdk/client-iotsitewise';
import Box from '@cloudscape-design/components/box';
-import Icon from '@cloudscape-design/components/icon';
-import {
- colorBorderStatusError,
- colorBorderStatusWarning,
- spaceScaledXxs,
-} from '@cloudscape-design/design-tokens';
+import { spaceScaledXxs } from '@cloudscape-design/design-tokens';
import './data-quality-text.css';
type DataQualityTextOptions = {
quality?: Quality;
+ inheritFontColor?: boolean;
};
-export const DataQualityText = ({ quality }: DataQualityTextOptions) => {
+export const DataQualityText = ({
+ quality,
+ inheritFontColor,
+}: DataQualityTextOptions) => {
// Don't show any special UX for good quality points
if (!quality || quality === 'GOOD') return null;
- let icon = null;
let text = null;
- let styles: React.CSSProperties = {
+ const styles: React.CSSProperties = {
gap: spaceScaledXxs,
textDecoration: 'none',
};
- let borderBottom = '1px dashed ';
-
switch (quality) {
case 'BAD':
- icon = ;
- text = Bad Quality;
- borderBottom += colorBorderStatusError;
- styles = {
- ...styles,
- borderBottom: borderBottom,
- };
+ text = (
+
+ Bad Quality
+
+ );
break;
case 'UNCERTAIN':
- icon = ;
- text = Uncertain Quality;
- borderBottom += colorBorderStatusWarning;
- styles = {
- ...styles,
- borderBottom: borderBottom,
- };
+ text = (
+
+ Uncertain Quality
+
+ );
break;
}
return (
-
+
diff --git a/packages/react-components/src/components/kpi/kpi.css b/packages/react-components/src/components/kpi/kpi.css
index 71157de06..451c4c9c2 100644
--- a/packages/react-components/src/components/kpi/kpi.css
+++ b/packages/react-components/src/components/kpi/kpi.css
@@ -36,11 +36,10 @@
.kpi .timestamp-border {
height: 2px;
- background-color: #e9ebed;
}
.kpi .timestamp {
- padding: 6px;
+ padding: 8px;
height: 18px;
}
@@ -54,5 +53,5 @@
}
.aggregation {
- padding: 6px;
+ padding: 8px;
}
diff --git a/packages/react-components/src/components/kpi/kpi.tsx b/packages/react-components/src/components/kpi/kpi.tsx
index 2483961d9..e4192d05b 100644
--- a/packages/react-components/src/components/kpi/kpi.tsx
+++ b/packages/react-components/src/components/kpi/kpi.tsx
@@ -11,7 +11,10 @@ import type {
import type { KPISettings } from './types';
import { KpiBase } from './kpiBase';
import type { ComponentQuery } from '../../common/chartTypes';
-import { getTimeSeriesQueries } from '../../utils/queries';
+import { getAlarmQueries, getTimeSeriesQueries } from '../../utils/queries';
+import { convertAlarmQueryToAlarmRequest } from '../../queries/utils/convertAlarmQueryToAlarmRequest';
+import { useAlarms } from '../../hooks/useAlarms';
+import { buildTransformAlarmForSingleQueryWidgets } from '../../utils/buildTransformAlarmForSingleQueryWidgets';
export const KPI = ({
query,
@@ -31,23 +34,47 @@ export const KPI = ({
significantDigits?: number;
timeZone?: string;
}) => {
+ const { viewport } = useViewport();
+ const utilizedViewport = passedInViewport || viewport || DEFAULT_VIEWPORT; // explicitly passed in viewport overrides viewport group
+
+ const alarmQueries = getAlarmQueries([query]);
+ const timeSeriesQueries = getTimeSeriesQueries([query]);
+
+ const mapAlarmQueriesToRequests = alarmQueries.flatMap((query) =>
+ convertAlarmQueryToAlarmRequest(query)
+ );
+
+ const transformedAlarm = useAlarms({
+ iotSiteWiseClient: alarmQueries.at(0)?.iotSiteWiseClient,
+ iotEventsClient: alarmQueries.at(0)?.iotEventsClient,
+ requests: mapAlarmQueriesToRequests,
+ viewport: utilizedViewport,
+ settings: {
+ fetchThresholds: true,
+ refreshRate: alarmQueries.at(0)?.query.requestSettings?.refreshRate,
+ },
+ transform: buildTransformAlarmForSingleQueryWidgets({
+ iotSiteWiseClient: alarmQueries.at(0)?.iotSiteWiseClient,
+ iotEventsClient: alarmQueries.at(0)?.iotEventsClient,
+ }),
+ })
+ .filter((alarm) => !!alarm)
+ .at(0);
+
const { dataStreams, thresholds: queryThresholds } = useTimeSeriesData({
viewport: passedInViewport,
- queries: getTimeSeriesQueries([query]),
+ queries: transformedAlarm
+ ? transformedAlarm.timeSeriesDataQueries
+ : timeSeriesQueries,
settings: {
fetchMostRecentBeforeEnd: true,
},
styles,
});
- const { viewport } = useViewport();
-
- const utilizedViewport = passedInViewport || viewport || DEFAULT_VIEWPORT; // explicitly passed in viewport overrides viewport group
const {
propertyPoint,
- alarmPoint,
propertyThreshold,
- alarmStream,
propertyStream,
propertyResolution,
} = widgetPropertiesFromInputs({
@@ -56,17 +83,16 @@ export const KPI = ({
viewport: utilizedViewport,
});
- const name = propertyStream?.name || alarmStream?.name;
- const unit = propertyStream?.unit || alarmStream?.unit;
+ const name = propertyStream?.name;
+ const unit = propertyStream?.unit;
const backgroundColor = settings?.color || settings?.backgroundColor;
- const isLoading =
- alarmStream?.isLoading || propertyStream?.isLoading || false;
- const error = alarmStream?.error || propertyStream?.error;
+ const isLoading = propertyStream?.isLoading || false;
+ const error = propertyStream?.error;
return (
= ({
propertyPoint,
@@ -30,6 +32,7 @@ export const KpiBase: React.FC = ({
significantDigits = DEFAULT_DECIMAL_PLACES,
propertyThreshold,
timeZone,
+ alarmState,
}) => {
const {
showUnit,
@@ -59,6 +62,11 @@ export const KpiBase: React.FC = ({
const fontColor = showFilledThreshold
? highContrastColor(propertyThreshold.color)
: nonThresholdFontColor;
+ const borderColor = showFilledThreshold
+ ? highContrastColor(propertyThreshold.color)
+ : settings.backgroundColor
+ ? nonThresholdFontColor
+ : colorBorderDividerSecondary;
const point = propertyPoint;
const aggregationResolutionString = getAggregationFrequency(
@@ -88,7 +96,7 @@ export const KpiBase: React.FC = ({
);
}
- const nameAndUnit = (showName || showUnit) && (
+ const nameAndUnit = (
= ({
);
+ const alarmSection = (
+
+ );
+
+ const dataQualitySection = (
+
+
+
+ );
+
return (
= ({
>
- {nameAndUnit}
+ {alarmState && alarmSection}
+ {(showName || showUnit) && nameAndUnit}
= ({
)}
- {!isLoading && showDataQuality && (
-
- )}
+ {!isLoading && showDataQuality && dataQualitySection}
{point && (
= ({
diff --git a/packages/react-components/src/components/kpi/types.ts b/packages/react-components/src/components/kpi/types.ts
index ca866ec8d..2f9be6d20 100644
--- a/packages/react-components/src/components/kpi/types.ts
+++ b/packages/react-components/src/components/kpi/types.ts
@@ -1,10 +1,12 @@
import { StyledThreshold } from '@iot-app-kit/core';
import type { WidgetSettings } from '../../common/dataTypes';
+import { PascalCaseStateName } from '../../hooks/useAlarms/transformers';
export type KPIBaseProperties = WidgetSettings & {
settings?: Partial
;
propertyThreshold?: StyledThreshold;
timeZone?: string;
+ alarmState?: PascalCaseStateName;
};
export type KPISettings = {
diff --git a/packages/react-components/src/utils/buildTransformAlarmForSingleQueryWidgets.ts b/packages/react-components/src/utils/buildTransformAlarmForSingleQueryWidgets.ts
new file mode 100644
index 000000000..b52e29633
--- /dev/null
+++ b/packages/react-components/src/utils/buildTransformAlarmForSingleQueryWidgets.ts
@@ -0,0 +1,50 @@
+import { initialize } from '@iot-app-kit/source-iotsitewise';
+import { IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise';
+import type { IoTEventsClient } from '@aws-sdk/client-iot-events';
+import { AlarmData } from '../hooks/useAlarms';
+import { parseAlarmStateAssetProperty } from '../hooks/useAlarms/transformers';
+
+// Transforms an AlarmData object into an object containing a timeSeriesDataQuery and alarm state
+export const buildTransformAlarmForSingleQueryWidgets =
+ ({
+ iotSiteWiseClient,
+ iotEventsClient,
+ }: {
+ iotSiteWiseClient?: IoTSiteWiseClient;
+ iotEventsClient?: IoTEventsClient;
+ }) =>
+ (alarm: AlarmData) => {
+ const { inputProperty, assetId, status, state } = alarm;
+
+ const propertyId = inputProperty?.at(0)?.property.id;
+ const latestState = state?.data?.at(-1);
+
+ if (
+ status.isSuccess &&
+ assetId &&
+ propertyId &&
+ iotSiteWiseClient &&
+ iotEventsClient
+ ) {
+ const timeSeriesDataQuery = initialize({
+ iotSiteWiseClient,
+ iotEventsClient,
+ }).query.timeSeriesData({
+ assets: [
+ {
+ assetId,
+ properties: [
+ {
+ propertyId,
+ },
+ ],
+ },
+ ],
+ });
+
+ return {
+ state: parseAlarmStateAssetProperty(latestState)?.value.state,
+ timeSeriesDataQueries: [timeSeriesDataQuery],
+ };
+ }
+ };
diff --git a/packages/react-components/stories/kpi/connected-kpi.stories.tsx b/packages/react-components/stories/kpi/connected-kpi.stories.tsx
index ca03652fb..75ecdf4fc 100644
--- a/packages/react-components/stories/kpi/connected-kpi.stories.tsx
+++ b/packages/react-components/stories/kpi/connected-kpi.stories.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { KPI } from '../../src/components/kpi/kpi';
import {
+ getSingleValueAlarmDataQuery,
getSingleValueTimeSeriesDataQuery,
queryConfigured,
} from '../utils/query';
@@ -22,6 +23,8 @@ export default {
} as ComponentMeta;
export const ConnectedKPIWidget: ComponentStory = () => {
+ const hasAlarmIds = process.env.ALARM_COMPOSITE_MODEL_ID_1 !== null;
+
if (!queryConfigured()) {
return (
@@ -49,6 +52,14 @@ export const ConnectedKPIWidget: ComponentStory = () => {
query={getSingleValueTimeSeriesDataQuery()}
/>
+ {hasAlarmIds && (
+
+
+
+ )}
);
};
diff --git a/packages/react-components/stories/utils/query.ts b/packages/react-components/stories/utils/query.ts
index 07fb07886..205be46fe 100644
--- a/packages/react-components/stories/utils/query.ts
+++ b/packages/react-components/stories/utils/query.ts
@@ -1,5 +1,6 @@
import { getIotEventsClient, getSiteWiseClient } from '@iot-app-kit/core-util';
import {
+ SiteWiseAlarmDataStreamQuery,
SiteWiseDataStreamQuery,
initialize,
} from '@iot-app-kit/source-iotsitewise';
@@ -124,6 +125,35 @@ export const getSingleValueTimeSeriesDataQuery = (
});
};
+export const getSingleValueAlarmDataQuery = (
+ alarmStreamQuery?: SiteWiseAlarmDataStreamQuery
+) => {
+ if (alarmStreamQuery) {
+ return getIotSiteWiseQuery().alarmData(alarmStreamQuery);
+ }
+
+ const { assetId } = getAssetQuery();
+ const alarmId1 = process.env.ALARM_COMPOSITE_MODEL_ID_1;
+
+ if (!alarmId1) return getIotSiteWiseQuery().alarmData({});
+
+ return getIotSiteWiseQuery().alarmData({
+ alarms: [
+ {
+ assetId: assetId,
+ alarmComponents: [
+ {
+ assetCompositeModelId: alarmId1,
+ },
+ ],
+ },
+ ],
+ requestSettings: {
+ refreshRate: 5000,
+ },
+ });
+};
+
export const queryConfigured = () => {
try {
getEnvCredentials();
diff --git a/turbo.json b/turbo.json
index de11b45e4..69afb0486 100644
--- a/turbo.json
+++ b/turbo.json
@@ -40,6 +40,7 @@
"PROPERTY_ID_1",
"PROPERTY_ID_2",
"PROPERTY_ID_3",
+ "ALARM_COMPOSITE_MODEL_ID_1",
"REGION",
"TZ"
]