diff --git a/src/redux/csState.ts b/src/redux/csState.ts index e34f1523..35c92221 100644 --- a/src/redux/csState.ts +++ b/src/redux/csState.ts @@ -27,6 +27,14 @@ export interface PvState { readonly: boolean; } +export type PvDatum = PvState & { + effectivePvName: string; +}; + +export type PvDataCollection = { + pvData: PvDatum[]; +}; + export interface FullPvState extends PvState { initializingPvName: string; } diff --git a/src/types/props.ts b/src/types/props.ts index 1ba8fb0d..2905ed67 100644 --- a/src/types/props.ts +++ b/src/types/props.ts @@ -15,6 +15,7 @@ export type GenericProp = | boolean | number | PV + | { pvName: PV }[] | Color | Font | Border diff --git a/src/ui/hooks/useConnection.ts b/src/ui/hooks/useConnection.ts index 27c0b64f..3abbcdeb 100644 --- a/src/ui/hooks/useConnection.ts +++ b/src/ui/hooks/useConnection.ts @@ -1,7 +1,7 @@ import React from "react"; import { useSubscription } from "./useSubscription"; import { useSelector } from "react-redux"; -import { CsState } from "../../redux/csState"; +import { CsState, PvDataCollection } from "../../redux/csState"; import { pvStateSelector, PvArrayResults, pvStateComparator } from "./utils"; import { SubscriptionType } from "../../connection/plugin"; import { DType } from "../../types/dtypes"; @@ -27,6 +27,7 @@ export function useConnection( let readonly = false; let value = undefined; let effectivePvName = "undefined"; + if (pvName !== undefined) { const [pvState, effPvName] = pvResults[pvName]; effectivePvName = effPvName; @@ -36,5 +37,35 @@ export function useConnection( value = pvState.value; } } + return [effectivePvName, connected, readonly, value]; } + +export const useConnectionMultiplePv = ( + id: string, + pvNames: string[], + type?: SubscriptionType +): PvDataCollection => { + const pvNameArray = pvNames.filter(x => !!x); + const typeArray = !type ? [] : [type]; + + useSubscription(id, pvNameArray, typeArray); + + const pvResults = useSelector( + (state: CsState): PvArrayResults => pvStateSelector(pvNameArray, state), + pvStateComparator + ); + + return { + pvData: pvNameArray.map(pvName => { + const [pvState, effPvName] = pvResults[pvName]; + + return { + effectivePvName: effPvName, + connected: pvState?.connected ?? false, + readonly: pvState?.readonly ?? false, + value: pvState?.value + }; + }) + }; +}; diff --git a/src/ui/widgets/BoolButton/boolButton.test.tsx b/src/ui/widgets/BoolButton/boolButton.test.tsx index 1bcec037..ef3c4264 100644 --- a/src/ui/widgets/BoolButton/boolButton.test.tsx +++ b/src/ui/widgets/BoolButton/boolButton.test.tsx @@ -5,6 +5,7 @@ import { DType } from "../../../types/dtypes"; import { Color } from "../../../types"; import { ThemeProvider } from "@mui/material"; import { phoebusTheme } from "../../../phoebusTheme"; +import { PvDatum } from "../../../redux/csState"; const BoolButtonRenderer = (boolButtonProps: any): JSX.Element => { return ( @@ -14,8 +15,15 @@ const BoolButtonRenderer = (boolButtonProps: any): JSX.Element => { ); }; +const TEST_PVDATUM = { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: new DType({ doubleValue: 1 }) +} as Partial as PvDatum; + const TEST_PROPS = { - value: new DType({ doubleValue: 1 }), + pvData: [TEST_PVDATUM], width: 45, height: 20, onColor: Color.fromRgba(0, 235, 10), @@ -116,7 +124,7 @@ describe("", (): void => { test("on click change led colour if no text ", async (): Promise => { const boolButtonProps = { ...TEST_PROPS, - value: new DType({ doubleValue: 0 }), + pvData: [{ ...TEST_PVDATUM, value: new DType({ doubleValue: 0 }) }], onLabel: "", offLabel: "", showLed: true diff --git a/src/ui/widgets/BoolButton/boolButton.tsx b/src/ui/widgets/BoolButton/boolButton.tsx index a34b6360..f3374dd1 100644 --- a/src/ui/widgets/BoolButton/boolButton.tsx +++ b/src/ui/widgets/BoolButton/boolButton.tsx @@ -17,6 +17,7 @@ import { writePv } from "../../hooks/useSubscription"; import { DType } from "../../../types/dtypes"; import { WIDGET_DEFAULT_SIZES } from "../EmbeddedDisplay/bobParser"; import { Button as MuiButton, styled, useTheme } from "@mui/material"; +import { getPvValueAndName } from "../utils"; // For HTML button, these are the sizes of the buffer on // width and height. Must take into account when allocating @@ -88,8 +89,7 @@ export const BoolButtonComponent = ( height = WIDGET_DEFAULT_SIZES["bool_button"][1], foregroundColor = theme.palette.primary.contrastText, backgroundColor = theme.palette.primary.main, - pvName, - value, + pvData, onState = 1, offState = 0, onColor = Color.fromRgba(0, 255, 0), @@ -100,6 +100,7 @@ export const BoolButtonComponent = ( labelsFromPv = false, enabled = true } = props; + const { value, effectivePvName: pvName } = getPvValueAndName(pvData); const font = props.font?.css() ?? theme.typography; diff --git a/src/ui/widgets/ByteMonitor/byteMonitor.test.tsx b/src/ui/widgets/ByteMonitor/byteMonitor.test.tsx index 1a474c0e..f83b3b94 100644 --- a/src/ui/widgets/ByteMonitor/byteMonitor.test.tsx +++ b/src/ui/widgets/ByteMonitor/byteMonitor.test.tsx @@ -7,6 +7,7 @@ import { import { Color } from "../../../types"; import renderer, { ReactTestRendererJSON } from "react-test-renderer"; import { DType } from "../../../types/dtypes"; +import { PvDatum } from "../../../redux/csState"; const ByteMonitorRenderer = (byteMonitorProps: any): ReactTestRendererJSON => { return renderer @@ -17,7 +18,14 @@ const ByteMonitorRenderer = (byteMonitorProps: any): ReactTestRendererJSON => { describe("", (): void => { test("default properties are added to bytemonitor component", (): void => { const byteMonitorProps = { - value: new DType({ doubleValue: 15 }), + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: new DType({ doubleValue: 15 }) + } as Partial as PvDatum + ], height: 40, width: 40 }; @@ -33,7 +41,14 @@ describe("", (): void => { }); test("overwrite bytemonitor default values", (): void => { const byteMonitorProps = { - value: new DType({ doubleValue: 2 }), + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: new DType({ doubleValue: 2 }) + } as Partial as PvDatum + ], height: 50, width: 50, startBit: 8, diff --git a/src/ui/widgets/ByteMonitor/byteMonitor.tsx b/src/ui/widgets/ByteMonitor/byteMonitor.tsx index 49ec21d7..993f70c3 100644 --- a/src/ui/widgets/ByteMonitor/byteMonitor.tsx +++ b/src/ui/widgets/ByteMonitor/byteMonitor.tsx @@ -11,6 +11,7 @@ import { registerWidget } from "../register"; import classes from "./byteMonitor.module.css"; import { Color } from "../../../types/color"; import { WIDGET_DEFAULT_SIZES } from "../EmbeddedDisplay/bobParser"; +import { getPvValueAndName } from "../utils"; export const ByteMonitorProps = { width: IntPropOpt, @@ -41,7 +42,7 @@ export const ByteMonitorComponent = ( props: ByteMonitorComponentProps ): JSX.Element => { const { - value, + pvData, startBit = 0, horizontal = true, bitReverse = false, @@ -54,6 +55,7 @@ export const ByteMonitorComponent = ( width = WIDGET_DEFAULT_SIZES["byte_monitor"][0], height = WIDGET_DEFAULT_SIZES["byte_monitor"][1] } = props; + const { value } = getPvValueAndName(pvData); // Check for a value, otherwise set to 0 const doubleValue = value?.getDoubleValue() || 0; diff --git a/src/ui/widgets/Checkbox/checkbox.tsx b/src/ui/widgets/Checkbox/checkbox.tsx index 23de217f..59db5aa1 100644 --- a/src/ui/widgets/Checkbox/checkbox.tsx +++ b/src/ui/widgets/Checkbox/checkbox.tsx @@ -17,6 +17,7 @@ import { } from "@mui/material"; import { writePv } from "../../hooks/useSubscription"; import { DType } from "../../../types"; +import { getPvValueAndName } from "../utils"; export const CheckboxProps = { label: StringPropOpt, @@ -65,7 +66,8 @@ export const CheckboxComponent = ( props: CheckboxComponentProps ): JSX.Element => { const theme = useTheme(); - const { enabled = true, label = "Label", value, pvName } = props; + const { enabled = true, label = "Label", pvData } = props; + const { value, effectivePvName: pvName } = getPvValueAndName(pvData); const checked = Boolean(value?.getDoubleValue()); const handleChange = (event: any): void => { diff --git a/src/ui/widgets/ChoiceButton/choiceButton.test.tsx b/src/ui/widgets/ChoiceButton/choiceButton.test.tsx index 3ab82abc..3aa2342e 100644 --- a/src/ui/widgets/ChoiceButton/choiceButton.test.tsx +++ b/src/ui/widgets/ChoiceButton/choiceButton.test.tsx @@ -5,6 +5,7 @@ import { DDisplay, DType } from "../../../types/dtypes"; import { Color, Font } from "../../../types"; import { ThemeProvider } from "@mui/material"; import { phoebusTheme } from "../../../phoebusTheme"; +import { PvDatum } from "../../../redux/csState"; const ChoiceButtonRenderer = (choiceButtonProps: any): JSX.Element => { return ( @@ -17,7 +18,14 @@ const ChoiceButtonRenderer = (choiceButtonProps: any): JSX.Element => { describe("", (): void => { test("it renders ChoiceButton with default props", (): void => { const choiceButtonProps = { - value: null + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: undefined + } as Partial as PvDatum + ] }; const { getAllByRole } = render(ChoiceButtonRenderer(choiceButtonProps)); const buttons = getAllByRole("button") as Array; @@ -37,7 +45,14 @@ describe("", (): void => { test("pass props to widget", (): void => { const choiceButtonProps = { - value: new DType({ doubleValue: 0 }), + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: new DType({ doubleValue: 0 }) + } as Partial as PvDatum + ], width: 60, height: 140, font: new Font(12), @@ -68,12 +83,19 @@ describe("", (): void => { test("pass props to widget, using itemsFromPv", (): void => { const choiceButtonProps = { - value: new DType( - { doubleValue: 0 }, - undefined, - undefined, - new DDisplay({ choices: ["hi", "Hello"] }) - ), + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: new DType( + { doubleValue: 0 }, + undefined, + undefined, + new DDisplay({ choices: ["hi", "Hello"] }) + ) + } as Partial as PvDatum + ], items: ["one", "two", "three"], horizontal: false, itemsFromPv: true, @@ -90,7 +112,7 @@ describe("", (): void => { test("selecting a button", (): void => { const choiceButtonProps = { - value: null + pvData: [] }; const { getAllByRole } = render(ChoiceButtonRenderer(choiceButtonProps)); const buttons = getAllByRole("button") as Array; diff --git a/src/ui/widgets/ChoiceButton/choiceButton.tsx b/src/ui/widgets/ChoiceButton/choiceButton.tsx index 6e56b0fe..7339e7dc 100644 --- a/src/ui/widgets/ChoiceButton/choiceButton.tsx +++ b/src/ui/widgets/ChoiceButton/choiceButton.tsx @@ -21,6 +21,7 @@ import { useTheme } from "@mui/material"; import { Color } from "../../../types"; +import { getPvValueAndName } from "../utils"; const ChoiceButtonProps = { pvName: StringPropOpt, @@ -71,16 +72,17 @@ export const ChoiceButtonComponent = ( const { width = 100, height = 43, - value = null, + pvData, enabled = true, itemsFromPv = true, - pvName, items = ["Item 1", "Item 2"], horizontal = true, foregroundColor = theme.palette.primary.contrastText, backgroundColor = theme.palette.primary.main, selectedColor = Color.fromRgba(200, 200, 200) } = props; + const { value, effectivePvName: pvName } = getPvValueAndName(pvData); + const font = props.font?.css() ?? theme.typography; const [selected, setSelected] = useState(value?.getDoubleValue()); diff --git a/src/ui/widgets/EmbeddedDisplay/bobParser.test.ts b/src/ui/widgets/EmbeddedDisplay/bobParser.test.ts index ae65375b..b8c4b00a 100644 --- a/src/ui/widgets/EmbeddedDisplay/bobParser.test.ts +++ b/src/ui/widgets/EmbeddedDisplay/bobParser.test.ts @@ -83,7 +83,7 @@ describe("bob widget parser", (): void => { it("parses a readback widget", (): void => { const widget = parseBob(readbackString, "xxx", PREFIX) .children?.[0] as WidgetDescription; - expect(widget.pvName).toEqual(PV.parse("xxx://abc")); + expect(widget.pvMetadataList[0].pvName).toEqual(PV.parse("xxx://abc")); }); const noXString = ` diff --git a/src/ui/widgets/EmbeddedDisplay/bobParser.ts b/src/ui/widgets/EmbeddedDisplay/bobParser.ts index f0c119b0..9c5b98e5 100644 --- a/src/ui/widgets/EmbeddedDisplay/bobParser.ts +++ b/src/ui/widgets/EmbeddedDisplay/bobParser.ts @@ -255,7 +255,7 @@ function bobParseTraces(props: any): Trace[] { const traces: Trace[] = []; let parsedProps = {}; if (props) { - // If only once trace, we are passed an object instead + // If only one trace, we are passed an object instead // of an array if (props.trace.length > 1) { props.trace.forEach((trace: any) => { @@ -496,9 +496,11 @@ export function parseBob( const simpleParsers: ParserDict = { ...BOB_SIMPLE_PARSERS, - pvName: [ + pvMetadataList: [ "pv_name", - (pvName: ElementCompact): PV => opiParsePvName(pvName, defaultProtocol) + (pvName: ElementCompact): { pvName: PV }[] => [ + { pvName: opiParsePvName(pvName, defaultProtocol) } + ] ], actions: [ "actions", diff --git a/src/ui/widgets/EmbeddedDisplay/parser.ts b/src/ui/widgets/EmbeddedDisplay/parser.ts index dac078aa..55373b1a 100644 --- a/src/ui/widgets/EmbeddedDisplay/parser.ts +++ b/src/ui/widgets/EmbeddedDisplay/parser.ts @@ -109,7 +109,9 @@ export function genericParser( // plot widgets as the PV. This is a placeholder until support for // multiple PVs per widget is implemented if (newProps.hasOwnProperty("traces")) { - newProps.pvName = PV.parse(newProps.traces[0].yPv); + newProps.pvMetadataList = newProps.traces?.map((trace: any) => ({ + pvName: PV.parse(trace.yPv) + })); } return newProps; diff --git a/src/ui/widgets/Input/input.test.tsx b/src/ui/widgets/Input/input.test.tsx index 6e6dbeaa..c4bcbc0d 100644 --- a/src/ui/widgets/Input/input.test.tsx +++ b/src/ui/widgets/Input/input.test.tsx @@ -3,15 +3,20 @@ import { SmartInputComponent } from "./input"; import { render } from "@testing-library/react"; import { DAlarm } from "../../../types/dtypes"; import { dstring } from "../../../testResources"; +import { PvDatum } from "../../../redux/csState"; let input: JSX.Element; beforeEach((): void => { input = ( as PvDatum + ]} alarmSensitive={true} /> ); diff --git a/src/ui/widgets/Input/input.tsx b/src/ui/widgets/Input/input.tsx index 351869e5..34b6b697 100644 --- a/src/ui/widgets/Input/input.tsx +++ b/src/ui/widgets/Input/input.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { writePv } from "../../hooks/useSubscription"; import { Widget } from "../widget"; -import { PVInputComponent, PVWidgetPropType } from "../widgetProps"; +import { PVComponent, PVWidgetPropType } from "../widgetProps"; import { registerWidget } from "../register"; import { InferWidgetProps, @@ -16,6 +16,7 @@ import { } from "../propTypes"; import { AlarmQuality, DType } from "../../../types/dtypes"; import { TextField as MuiTextField, styled, useTheme } from "@mui/material"; +import { getPvValueAndName } from "../utils"; const InputComponentProps = { pvName: StringPropOpt, @@ -72,7 +73,7 @@ const TextField = styled(MuiTextField)({ }); export const SmartInputComponent = ( - props: PVInputComponent & InferWidgetProps + props: PVComponent & InferWidgetProps ): JSX.Element => { const theme = useTheme(); const { @@ -81,7 +82,7 @@ export const SmartInputComponent = ( transparent = false, textAlign = "left", textAlignV = "center", - value = null, + pvData, multiLine = false, alarmSensitive = true, showUnits = false, @@ -89,6 +90,8 @@ export const SmartInputComponent = ( formatType = "default" } = props; + const { value, effectivePvName: pvName } = getPvValueAndName(pvData); + // Decide what to display. const display = value?.getDisplay(); // In Phoebus, default precision -1 seems to usually be 3. The toFixed functions @@ -144,7 +147,7 @@ export const SmartInputComponent = ( let borderStyle = props.border?.css().borderStyle ?? "solid"; let borderWidth = props.border?.width ?? "0px"; - const alarmQuality = props.value?.getAlarm().quality ?? AlarmQuality.VALID; + const alarmQuality = value?.getAlarm().quality ?? AlarmQuality.VALID; if (alarmSensitive) { switch (alarmQuality) { case AlarmQuality.UNDEFINED: @@ -189,12 +192,12 @@ export const SmartInputComponent = ( const onKeyPress = (event: React.KeyboardEvent) => { if (multiLine) { if (event.key === "Enter" && event.ctrlKey) { - writePv(props.pvName, new DType({ stringValue: inputValue })); + writePv(pvName, new DType({ stringValue: inputValue })); event.currentTarget.blur(); } } else { if (event.key === "Enter") { - writePv(props.pvName, new DType({ stringValue: inputValue })); + writePv(pvName, new DType({ stringValue: inputValue })); event.currentTarget.blur(); } } diff --git a/src/ui/widgets/LED/led.test.tsx b/src/ui/widgets/LED/led.test.tsx index d747b444..9c9a0659 100644 --- a/src/ui/widgets/LED/led.test.tsx +++ b/src/ui/widgets/LED/led.test.tsx @@ -4,24 +4,29 @@ import { DAlarm, DType, AlarmQuality } from "../../../types/dtypes"; import renderer, { ReactTestRendererJSON } from "react-test-renderer"; import { Color } from "../../../types/color"; import { ddouble } from "../../../testResources"; +import { PvDatum } from "../../../redux/csState"; const createValue = (alarmType: AlarmQuality): DType => { return new DType({ stringValue: "3.141" }, new DAlarm(alarmType, "")); }; const UNUSED_VALUE = createValue(AlarmQuality.ALARM); +const BASE_PV = { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: UNUSED_VALUE +} as Partial as PvDatum; const DEFAULT_PROPS = { - value: UNUSED_VALUE, - connected: true, - readonly: false, + pvData: [BASE_PV], offColor: Color.RED, onColor: Color.GREEN }; const renderLed = (ledProps: LedComponentProps): ReactTestRendererJSON => { return renderer - .create() + .create() .toJSON() as ReactTestRendererJSON; }; @@ -38,7 +43,7 @@ describe("led changes Css properties based on alarm", (): void => { const ledProps = { ...DEFAULT_PROPS, - value, + pvData: [{ ...BASE_PV, value }], alarmSensitive: true }; @@ -52,7 +57,7 @@ describe("background color changes depending on value", (): void => { it("off color is applied if value zero", (): void => { const ledProps = { ...DEFAULT_PROPS, - value: ddouble(0) + pvData: [{ ...BASE_PV, value: ddouble(0) }] }; const renderedLed = renderLed(ledProps); @@ -63,7 +68,7 @@ describe("background color changes depending on value", (): void => { it("on color is applied if value not zero", (): void => { const ledProps = { ...DEFAULT_PROPS, - value: ddouble(1) + pvData: [{ ...BASE_PV, value: ddouble(1) }] }; const renderedLed = renderLed(ledProps); diff --git a/src/ui/widgets/LED/led.tsx b/src/ui/widgets/LED/led.tsx index 0817636f..63ce00e9 100644 --- a/src/ui/widgets/LED/led.tsx +++ b/src/ui/widgets/LED/led.tsx @@ -13,6 +13,7 @@ import classes from "./led.module.css"; import { DAlarm } from "../../../types/dtypes"; import { Color } from "../../../types"; import { WIDGET_DEFAULT_SIZES } from "../EmbeddedDisplay/bobParser"; +import { getPvValueAndName } from "../utils"; /** * width: the diameter of the LED @@ -37,7 +38,7 @@ export type LedComponentProps = InferWidgetProps & PVComponent; */ export const LedComponent = (props: LedComponentProps): JSX.Element => { const { - value, + pvData, onColor = Color.fromRgba(0, 255, 0), offColor = Color.fromRgba(60, 100, 60), lineColor = Color.fromRgba(50, 50, 50, 178), @@ -47,6 +48,8 @@ export const LedComponent = (props: LedComponentProps): JSX.Element => { bit = -1 } = props; + const { value } = getPvValueAndName(pvData); + const style: CSSProperties = {}; let ledOn = false; diff --git a/src/ui/widgets/Meter/meter.test.tsx b/src/ui/widgets/Meter/meter.test.tsx index 2d87bf25..35350f9a 100644 --- a/src/ui/widgets/Meter/meter.test.tsx +++ b/src/ui/widgets/Meter/meter.test.tsx @@ -6,6 +6,7 @@ import { Color } from "../../../types/color"; import { NumberFormatEnum } from "./meterUtilities"; import * as meterUtilities from "./meterUtilities"; import { DType, Font } from "../../../types"; +import { PvDatum } from "../../../redux/csState"; vi.mock("react-gauge-component", () => ({ GaugeComponent: vi.fn(({ value, minValue, maxValue, labels, style }) => ( @@ -40,18 +41,22 @@ vi.mock("./meterUtilities", async () => { describe("MeterComponent", () => { const defaultProps = { - connected: false, - readonly: true, - pvName: "PV:Test", - value: { - getDoubleValue: () => 50, - display: { - units: "kW", - controlRange: { min: 0, max: 100 }, - alarmRange: { min: 80, max: 100 }, - warningRange: { min: 60, max: 80 } - } - } as Partial as DType, + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: { + getDoubleValue: () => 50, + display: { + units: "kW", + controlRange: { min: 0, max: 100 }, + alarmRange: { min: 80, max: 100 }, + warningRange: { min: 60, max: 80 } + } + } as Partial as DType + } as Partial as PvDatum + ], foregroundColor: Color.fromRgba(0, 0, 0, 1), needleColor: Color.fromRgba(255, 5, 7, 1), backgroundColor: Color.fromRgba(250, 250, 250, 1) @@ -149,7 +154,7 @@ describe("MeterComponent", () => { }); it("handles missing PV value gracefully", () => { - render(); + render(); const gaugeComponent = screen.getByTestId("gauge-component"); expect(gaugeComponent.getAttribute("data-value")).toBe("0"); diff --git a/src/ui/widgets/Meter/meter.tsx b/src/ui/widgets/Meter/meter.tsx index 001afb46..28049c7e 100644 --- a/src/ui/widgets/Meter/meter.tsx +++ b/src/ui/widgets/Meter/meter.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from "react"; import { Box } from "@mui/material"; import { Widget } from "../widget"; -import { PVInputComponent, PVWidgetPropType } from "../widgetProps"; +import { PVComponent, PVWidgetPropType } from "../widgetProps"; import { registerWidget } from "../register"; import { FloatPropOpt, @@ -21,6 +21,7 @@ import { formatValue, NumberFormatEnum } from "./meterUtilities"; +import { getPvValueAndName } from "../utils"; export const MeterProps = { minimum: FloatPropOpt, @@ -40,10 +41,10 @@ export const MeterProps = { }; export const MeterComponent = ( - props: InferWidgetProps & PVInputComponent + props: InferWidgetProps & PVComponent ): JSX.Element => { const { - value, + pvData, format = NumberFormatEnum.Default, height = 120, width = 240, @@ -56,6 +57,8 @@ export const MeterComponent = ( showValue = true, transparent = false } = props; + const { value } = getPvValueAndName(pvData); + const units = value?.display.units ?? ""; const numValue = value?.getDoubleValue() ?? 0; diff --git a/src/ui/widgets/ProgressBar/progressBar.tsx b/src/ui/widgets/ProgressBar/progressBar.tsx index a4c292be..338b0edd 100644 --- a/src/ui/widgets/ProgressBar/progressBar.tsx +++ b/src/ui/widgets/ProgressBar/progressBar.tsx @@ -13,6 +13,7 @@ import { } from "../propTypes"; import { LinearProgress } from "@mui/material"; import { Color } from "../../../types/color"; +import { getPvValueAndName } from "../utils"; export const ProgressBarProps = { min: FloatPropOpt, @@ -33,7 +34,7 @@ export const ProgressBarComponent = ( props: InferWidgetProps & PVComponent ): JSX.Element => { const { - value, + pvData, limitsFromPv = false, showLabel = false, font, @@ -44,6 +45,8 @@ export const ProgressBarComponent = ( transparent = false // This property only exists in CSStudio, so default to false } = props; + const { value } = getPvValueAndName(pvData); + const backgroundColor = transparent ? "transparent" : (props.backgroundColor?.toString() ?? "rgba(250, 250, 250, 255)"); diff --git a/src/ui/widgets/Readback/readback.test.tsx b/src/ui/widgets/Readback/readback.test.tsx index 1fd9172b..c0023d67 100644 --- a/src/ui/widgets/Readback/readback.test.tsx +++ b/src/ui/widgets/Readback/readback.test.tsx @@ -4,10 +4,22 @@ import { DType, DDisplay, DAlarm, AlarmQuality } from "../../../types/dtypes"; import { render } from "@testing-library/react"; import { ThemeProvider } from "@mui/material"; import { phoebusTheme } from "../../../phoebusTheme"; +import { PvDatum } from "../../../redux/csState"; const BASE_PROPS = { - connected: true, - readonly: false, + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: false, + value: { + getDoubleValue: () => 50, + getTime: () => { + new Date(Date.now()); + } + } as Partial as DType + } as Partial as PvDatum + ], precision: 2 }; @@ -23,10 +35,17 @@ describe("", (): void => { test("numeric precision", (): void => { const props = { ...BASE_PROPS, - value: new DType({ - stringValue: "3.14159265359", - doubleValue: 3.1415926539 - }) + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: new DType({ + stringValue: "3.14159265359", + doubleValue: 3.1415926539 + }) + } as Partial as PvDatum + ] }; const { getByRole } = render(ReadbackRenderer(props)); // Check for precision. @@ -36,12 +55,19 @@ describe("", (): void => { test("string value with units", (): void => { const props = { ...BASE_PROPS, - value: new DType( - { stringValue: "hello" }, - undefined, - undefined, - new DDisplay({ units: "xyz" }) - ), + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: new DType( + { stringValue: "hello" }, + undefined, + undefined, + new DDisplay({ units: "xyz" }) + ) + } as Partial as PvDatum + ], showUnits: true }; const { getByText } = render(ReadbackRenderer(props)); @@ -52,10 +78,17 @@ describe("", (): void => { test("alarm-sensitive foreground colour", (): void => { const props = { ...BASE_PROPS, - value: new DType( - { stringValue: "hello" }, - new DAlarm(AlarmQuality.ALARM, "") - ), + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: new DType( + { stringValue: "hello" }, + new DAlarm(AlarmQuality.ALARM, "") + ) + } as Partial as PvDatum + ], alarmSensitive: true }; const { asFragment } = render(ReadbackRenderer(props)); @@ -66,10 +99,17 @@ describe("", (): void => { test("component is disabled", (): void => { const props = { ...BASE_PROPS, - value: new DType( - { stringValue: "hello" }, - new DAlarm(AlarmQuality.ALARM, "") - ), + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: new DType( + { stringValue: "hello" }, + new DAlarm(AlarmQuality.ALARM, "") + ) + } as Partial as PvDatum + ], enabled: false }; diff --git a/src/ui/widgets/Readback/readback.tsx b/src/ui/widgets/Readback/readback.tsx index 1fe12253..074b7dba 100644 --- a/src/ui/widgets/Readback/readback.tsx +++ b/src/ui/widgets/Readback/readback.tsx @@ -18,7 +18,7 @@ import { import { registerWidget } from "../register"; import { AlarmQuality, DType } from "../../../types/dtypes"; import { TextField as MuiTextField, styled, useTheme } from "@mui/material"; -import { calculateRotationTransform } from "../utils"; +import { calculateRotationTransform, getPvValueAndName } from "../utils"; import { WIDGET_DEFAULT_SIZES } from "../EmbeddedDisplay/bobParser"; const ReadbackProps = { @@ -80,7 +80,7 @@ export const ReadbackComponent = ( const theme = useTheme(); const { enabled = true, - value, + pvData, precision, formatType = "default", alarmSensitive = true, @@ -97,6 +97,8 @@ export const ReadbackComponent = ( width = WIDGET_DEFAULT_SIZES["textupdate"][0] } = props; + const { value } = getPvValueAndName(pvData); + // Decide what to display. const display = value?.getDisplay(); // In Phoebus, default precision -1 seems to usually be 3. The toFixed functions @@ -149,7 +151,7 @@ export const ReadbackComponent = ( let borderStyle = props.border?.css().borderStyle ?? "solid"; let borderWidth = props.border?.width ?? "0px"; - const alarmQuality = props.value?.getAlarm().quality ?? AlarmQuality.VALID; + const alarmQuality = value?.getAlarm().quality ?? AlarmQuality.VALID; if (alarmSensitive) { switch (alarmQuality) { case AlarmQuality.UNDEFINED: diff --git a/src/ui/widgets/SlideControl/slideControl.test.tsx b/src/ui/widgets/SlideControl/slideControl.test.tsx index 702a6a47..fc00fa7b 100644 --- a/src/ui/widgets/SlideControl/slideControl.test.tsx +++ b/src/ui/widgets/SlideControl/slideControl.test.tsx @@ -6,10 +6,14 @@ import { SlideControlComponent } from "./slideControl"; test("slideControl", () => { const { container } = render( diff --git a/src/ui/widgets/SlideControl/slideControl.tsx b/src/ui/widgets/SlideControl/slideControl.tsx index 72ac8fa2..159d08ec 100644 --- a/src/ui/widgets/SlideControl/slideControl.tsx +++ b/src/ui/widgets/SlideControl/slideControl.tsx @@ -3,7 +3,7 @@ import log from "loglevel"; import { writePv } from "../../hooks/useSubscription"; import { Widget } from "../widget"; -import { PVInputComponent, PVWidgetPropType } from "../widgetProps"; +import { PVComponent, PVWidgetPropType } from "../widgetProps"; import { BoolPropOpt, BorderPropOpt, @@ -17,6 +17,7 @@ import { registerWidget } from "../register"; import { DType } from "../../../types/dtypes"; import { Slider, useTheme } from "@mui/material"; import { WIDGET_DEFAULT_SIZES } from "../EmbeddedDisplay/bobParser"; +import { getPvValueAndName } from "../utils"; export const SliderControlProps = { minimum: FloatPropOpt, @@ -48,12 +49,11 @@ export const SliderControlProps = { }; export const SlideControlComponent = ( - props: InferWidgetProps & PVInputComponent + props: InferWidgetProps & PVComponent ): JSX.Element => { const theme = useTheme(); const { - pvName, - value = null, + pvData, enabled = true, horizontal = true, limitsFromPv = false, @@ -74,6 +74,7 @@ export const SlideControlComponent = ( height = WIDGET_DEFAULT_SIZES["scaledslider"][1] } = props; let { minimum = 0, maximum = 100 } = props; + const { value, effectivePvName: pvName } = getPvValueAndName(pvData); const font = props.font?.css() ?? theme.typography; diff --git a/src/ui/widgets/StripChart/stripChart.test.tsx b/src/ui/widgets/StripChart/stripChart.test.tsx index 24fdaf61..2947fc69 100644 --- a/src/ui/widgets/StripChart/stripChart.test.tsx +++ b/src/ui/widgets/StripChart/stripChart.test.tsx @@ -6,15 +6,18 @@ import { StripChartComponent } from "./stripChart"; import { Trace } from "../../../types/trace"; import { Axis } from "../../../types/axis"; import { convertStringTimePeriod } from "../utils"; +import { PvDatum } from "../../../redux/csState"; +import { DTime } from "../../../types/dtypes"; // Mock the MUI X-Charts components vi.mock("@mui/x-charts", () => ({ - LineChart: vi.fn(({ series, xAxis, yAxis, sx }) => ( + LineChart: vi.fn(({ dataset, series, xAxis, yAxis, sx }) => (
)) @@ -34,16 +37,24 @@ vi.mock("@mui/material", () => ({ describe("StripChartComponent", () => { // Basic test setup + const buildPvDatum = ( + pvName: string, + value: number, + date: Date = new Date() + ) => { + return { + effectivePvName: pvName, + connected: true, + readonly: true, + value: { + getDoubleValue: () => value, + getTime: () => new DTime(date) + } as Partial as DType + } as Partial as PvDatum; + }; + const defaultProps = { - value: { - getDoubleValue: () => 50, - getTime: () => { - new Date(Date.now()); - } - } as Partial as DType, - connected: true, - readonly: true, - pvName: "TEST:PV", + pvData: [buildPvDatum("TEST:PV", 50)], traces: [new Trace()], axes: [new Axis()] }; @@ -54,15 +65,33 @@ describe("StripChartComponent", () => { describe("Rendering", () => { test("renders with default props", () => { - render(); + const renderedObject = render(); const lineChart = screen.getByTestId("line-chart"); expect(lineChart).toBeDefined(); const yAxisData = JSON.parse(lineChart.getAttribute("data-yaxis") ?? ""); const xAxisData = JSON.parse(lineChart.getAttribute("data-xaxis") ?? ""); + expect(yAxisData[0].position).toBe("left"); expect(xAxisData[0].scaleType).toBe("time"); + + let dataset = JSON.parse(lineChart.getAttribute("data-dataset") ?? ""); + expect(dataset.length).toBe(1); + expect(dataset[0]["TEST:PV"]).toBe(50); + + // Render again to check that new data values are added + renderedObject.rerender( + + ); + + dataset = JSON.parse(lineChart.getAttribute("data-dataset") ?? ""); + expect(dataset.length).toBe(2); + expect(dataset[0]["TEST:PV"]).toBe(50); + expect(dataset[1]["TEST:PV"]).toBe(60); }); test("renders with 2 y axes", () => { @@ -94,8 +123,8 @@ describe("StripChartComponent", () => { test("renders with 2 traces", () => { const traces = [ - new Trace({ color: Color.ORANGE }), - new Trace({ color: Color.PINK }) + new Trace({ color: Color.ORANGE, yPv: "TEST:PV" }), + new Trace({ color: Color.PINK, yPv: "TEST:PV" }) ]; render(); const lineChart = screen.getByTestId("line-chart"); @@ -112,11 +141,159 @@ describe("StripChartComponent", () => { expect(screen.getByText("Testing Plot")).toBeDefined(); }); + + test("renders multiple PVs with multiple traces, with rerender to add second set of PV data", () => { + const traces = [ + new Trace({ color: Color.ORANGE, yPv: "PV1" }), + new Trace({ color: Color.PINK, yPv: "PV2" }), + new Trace({ color: Color.BLUE, yPv: "PV3" }) + ]; + + const renderedObject = render( + + ); + + const lineChart = screen.getByTestId("line-chart"); + expect(lineChart).toBeDefined(); + + const yAxisData = JSON.parse(lineChart.getAttribute("data-yaxis") ?? ""); + const xAxisData = JSON.parse(lineChart.getAttribute("data-xaxis") ?? ""); + + expect(yAxisData[0].position).toBe("left"); + expect(xAxisData[0].scaleType).toBe("time"); + + let dataset = JSON.parse(lineChart.getAttribute("data-dataset") ?? ""); + expect(dataset.length).toBe(1); + expect(dataset[0]["TEST:PV1"]).toBe(2); + expect(dataset[0]["TEST:PV2"]).toBe(30); + expect(dataset[0]["TEST:PV3"]).toBe(400); + + const seriesData = JSON.parse( + lineChart.getAttribute("data-series") ?? "" + ); + + expect(seriesData[0].color).toBe(Color.ORANGE.toString()); + expect(seriesData[0].dataKey).toBe("TEST:PV1"); + expect(seriesData[1].color).toBe(Color.PINK.toString()); + expect(seriesData[1].dataKey).toBe("TEST:PV2"); + expect(seriesData[2].color).toBe(Color.BLUE.toString()); + expect(seriesData[2].dataKey).toBe("TEST:PV3"); + + // Render again to check that new data values are added + renderedObject.rerender( + + ); + + dataset = JSON.parse(lineChart.getAttribute("data-dataset") ?? ""); + expect(dataset.length).toBe(2); + expect(dataset[0]["TEST:PV1"]).toBe(2); + expect(dataset[1]["TEST:PV1"]).toBe(3); + expect(dataset[0]["TEST:PV2"]).toBe(30); + expect(dataset[1]["TEST:PV2"]).toBe(40); + expect(dataset[0]["TEST:PV3"]).toBe(400); + expect(dataset[1]["TEST:PV3"]).toBe(500); + }); + + test("renders multiple data points and removes old data values", () => { + const intialDate = new Date(new Date().getTime() - 600000); + const renderedObject = render( + + ); + + const lineChart = screen.getByTestId("line-chart"); + expect(lineChart).toBeDefined(); + + let dataset = JSON.parse(lineChart.getAttribute("data-dataset") ?? ""); + expect(dataset.length).toBe(1); + expect(dataset[0]["TEST:PV"]).toBe(10); + + renderedObject.rerender( + + ); + + renderedObject.rerender( + + ); + + renderedObject.rerender( + + ); + + renderedObject.rerender( + + ); + + // Now have 80 seconds of data, this should all still be avaliable, until we add the next data point + dataset = JSON.parse(lineChart.getAttribute("data-dataset") ?? ""); + expect(dataset.length).toBe(5); + expect(dataset[0]["TEST:PV"]).toBe(10); + expect(dataset[1]["TEST:PV"]).toBe(20); + expect(dataset[2]["TEST:PV"]).toBe(30); + expect(dataset[3]["TEST:PV"]).toBe(40); + expect(dataset[4]["TEST:PV"]).toBe(50); + + renderedObject.rerender( + + ); + + // Now have 90 seconds of data, first data point should be dropped + dataset = JSON.parse(lineChart.getAttribute("data-dataset") ?? ""); + expect(dataset.length).toBe(5); + expect(dataset[0]["TEST:PV"]).toBe(20); + expect(dataset[1]["TEST:PV"]).toBe(30); + expect(dataset[2]["TEST:PV"]).toBe(40); + expect(dataset[3]["TEST:PV"]).toBe(50); + expect(dataset[4]["TEST:PV"]).toBe(60); + }); }); describe("Styling", () => { test("applies tracetype to trace", () => { - const traces = [new Trace({ traceType: 5 })]; + const traces = [new Trace({ traceType: 5, yPv: "TEST:PV" })]; render(); @@ -148,7 +325,7 @@ describe("StripChartComponent", () => { }); test("applies diamond markers to trace", () => { - const traces = [new Trace({ pointType: 3 })]; + const traces = [new Trace({ pointType: 3, yPv: "TEST:PV" })]; render(); const lineChart = screen.getByTestId("line-chart"); diff --git a/src/ui/widgets/StripChart/stripChart.tsx b/src/ui/widgets/StripChart/stripChart.tsx index 39800a6e..bc0f7562 100644 --- a/src/ui/widgets/StripChart/stripChart.tsx +++ b/src/ui/widgets/StripChart/stripChart.tsx @@ -51,13 +51,18 @@ export type StripChartComponentProps = InferWidgetProps< > & PVComponent; +interface TimeSeriesPoint { + dateTime: Date; + [key: string]: Date | number | null; +} + export const StripChartComponent = ( props: StripChartComponentProps ): JSX.Element => { const { traces, axes, - value, + pvData, title, titleFont = new Font(), scaleFont = new Font(), @@ -69,110 +74,165 @@ export const StripChartComponent = ( start = "1 minute", visible = true } = props; + // If we're passed an empty array fill in defaults - if (traces.length < 1) traces.push(new Trace()); - if (axes.length < 1) axes.push(new Axis({ xAxis: false })); + const localAxes = useMemo( + () => (axes.length > 0 ? [...axes] : [new Axis({ xAxis: false })]), + [axes] + ); // Convert start time into milliseconds period const timePeriod = useMemo(() => convertStringTimePeriod(start), [start]); - const [data, setData] = useState<{ - x: any[]; - y: any[]; - min?: Date; - max?: Date; - }>({ x: [], y: [] }); + const [dateRange, setDateRange] = useState<{ minX: Date; maxX: Date }>({ + minX: new Date(new Date().getTime() - timePeriod), + maxX: new Date() + }); + const [data, setData] = useState([]); useEffect(() => { - if (value) { - // rRemove data outside min and max bounds - const minimum = new Date(new Date().getTime() - timePeriod); - // Check if first data point in array is outside minimum, if so remove - setData(currentData => { - const xData = currentData.x; - const yData = currentData.y; - if (xData.length > 0 && xData[0].getTime() < minimum.getTime()) { - xData.shift(); - yData.shift(); + const updateDataMap = (timeSeries: TimeSeriesPoint[]) => { + if (pvData) { + const allDates = Object.values(pvData) + .map(pvItem => pvItem?.value?.getTime()?.datetime) + .filter(date => !!date); + + if (allDates.length < 1) { + // we have no useful date for the timeseries point + return timeSeries; } - return { - x: [...xData, value.getTime()?.datetime], - y: [...yData, value.getDoubleValue()], - min: minimum, - max: new Date() - }; - }); - } - }, [value, timePeriod]); - - // For some reason the below styling doesn't change axis line and tick - // colour so we set it using sx in the Line Chart below by passing this in - const yAxesStyle: any = {}; - - const yAxes: ReadonlyArray> = axes.map((item, idx) => { - const axis = { - width: 45, - id: idx, - label: item.title, - color: item.color?.toString(), - labelStyle: { - font: item.titleFont.css(), - fill: item.color.toString() - }, - tickLabelStyle: { - font: item.scaleFont.css(), - fill: item.color.toString() - }, - scaleType: item.logScale ? "symlog" : "linear", - position: "left", - min: item.autoscale ? undefined : item.minimum, - max: item.autoscale ? undefined : item.maximum - }; - yAxesStyle[`.MuiChartsAxis-id-${idx}`] = { - ".MuiChartsAxis-line": { - stroke: item.color.toString() - }, - ".MuiChartsAxis-tick": { - stroke: item.color.toString() + + const mostRecentDate = allDates.reduce( + (a, b) => (a > b ? a : b), + allDates[0] + ); + + // Remove outdated data points + let i = 0; + while ( + i < timeSeries.length && + timeSeries[i].dateTime < + new Date(mostRecentDate.getTime() - timePeriod) + ) { + i++; + } + const truncatedTimeSeries = + i - 1 > 0 ? timeSeries.slice(i - 1) : timeSeries; + + // create new data point + let newTimeseriesPoint: TimeSeriesPoint = { dateTime: mostRecentDate }; + + pvData.forEach(pvItem => { + const { effectivePvName, value } = pvItem; + newTimeseriesPoint = { + ...newTimeseriesPoint, + [effectivePvName]: value?.getDoubleValue() ?? null + }; + }); + + return [...truncatedTimeSeries, newTimeseriesPoint]; } - }; - return axis; - }); - const xAxis: ReadonlyArray> = [ - { - data: data.x, - color: foregroundColor.toString(), - dataKey: "datetime", - min: data.min, - max: data.max, - scaleType: "time" - } - ]; - - const series = traces.map(item => { - const trace = { - // If axis is set higher than number of axes, default to zero - id: item.axis <= axes.length - 1 ? item.axis : 0, - data: data.y, - label: item.name, - color: visible ? item.color.toString() : "transparent", - showMark: item.pointType === 0 ? false : true, - shape: MARKER_STYLES[item.pointType], - line: { - strokeWidth: item.lineWidth - }, - area: item.traceType === 5 ? true : false, - connectNulls: false, - curve: item.traceType === 2 ? ("stepAfter" as CurveType) : "linear" + return timeSeries; }; - return trace; - }); + + setDateRange({ + minX: new Date(new Date().getTime() - timePeriod), + maxX: new Date() + }); + setData(currentData => updateDataMap(currentData)); + }, [timePeriod, pvData]); + + const { yAxes, yAxesStyle } = useMemo(() => { + // For some reason the below styling doesn't change axis line and tick + // colour so we set it using sx in the Line Chart below by passing this in + const yAxesStyle: any = {}; + + localAxes.forEach((item, idx) => { + yAxesStyle[`.MuiChartsAxis-id-${idx}`] = { + ".MuiChartsAxis-line": { + stroke: item.color.toString() + }, + ".MuiChartsAxis-tick": { + stroke: item.color.toString() + } + }; + }); + + const yAxes: ReadonlyArray> = localAxes.map( + (item, idx): YAxis => ({ + width: 45, + id: idx, + label: item.title, + color: item.color?.toString(), + labelStyle: { + font: item.titleFont.css(), + fill: item.color.toString() + }, + tickLabelStyle: { + font: item.scaleFont.css(), + fill: item.color.toString() + }, + scaleType: item.logScale ? "symlog" : "linear", + position: "left", + min: item.autoscale ? undefined : item.minimum, + max: item.autoscale ? undefined : item.maximum + }) + ); + + return { yAxes, yAxesStyle }; + }, [localAxes]); + + const xAxis: ReadonlyArray> = useMemo( + () => [ + { + color: foregroundColor.toString(), + dataKey: "dateTime", + min: dateRange.minX, + max: dateRange.maxX, + scaleType: "time", + id: "xaxis" + } + ], + [dateRange, foregroundColor] + ); + + const series = useMemo( + () => + (traces?.length > 0 ? traces : [new Trace()]) + ?.map((item, index) => { + const pvName = item?.yPv; + const effectivePvName = pvData + ?.map(pvItem => pvItem.effectivePvName) + ?.find( + effectivePvName => pvName && effectivePvName?.endsWith(pvName) + ); + if (!effectivePvName) { + return null; + } + + return { + id: index, + dataKey: effectivePvName, + label: item.name, + color: visible ? item.color.toString() : "transparent", + showMark: item.pointType === 0 ? false : true, + shape: MARKER_STYLES[item.pointType], + line: { + strokeWidth: item.lineWidth + }, + area: item.traceType === 5 ? true : false, + connectNulls: false, + curve: item.traceType === 2 ? ("stepAfter" as CurveType) : "linear" + }; + }) + .filter(x => !!x), + [traces, pvData, visible] + ); // TO DO // Add error bars option // Apply showToolbar // Use end value - this doesn't seem to do anything in Phoebus? - return ( ); diff --git a/src/ui/widgets/Symbol/symbol.test.tsx b/src/ui/widgets/Symbol/symbol.test.tsx index 4ec7f380..d29ef063 100644 --- a/src/ui/widgets/Symbol/symbol.test.tsx +++ b/src/ui/widgets/Symbol/symbol.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import { SymbolComponent } from "./symbol"; import { DType } from "../../../types/dtypes"; +import { PvDatum } from "../../../redux/csState"; const fakeValue = new DType({ stringValue: "Fake value" }); const stringValue = new DType({ stringValue: "1.54" }); @@ -23,7 +24,14 @@ describe(" from .opi file", (): void => { const symbolProps = { showBooleanLabel: true, imageFile: "img 1.gif", - value: fakeValue + pvData: [ + { + value: fakeValue, + connected: true, + readonly: false, + effectivePvName: "pv" + } + ] }; render(); @@ -34,7 +42,14 @@ describe(" from .opi file", (): void => { const symbolProps = { showBooleanLabel: true, imageFile: "img 1.gif", - value: fakeValue + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: fakeValue + } as Partial as PvDatum + ] }; const { asFragment } = render( @@ -75,7 +90,14 @@ describe(" from .bob file", (): void => { const symbolProps = { showIndex: true, symbols: ["img 1.gif", "img 2.png"], - value: stringValue + pvData: [ + { + value: stringValue, + connected: true, + readonly: false, + effectivePvName: "pv" + } + ] }; render(); @@ -87,7 +109,14 @@ describe(" from .bob file", (): void => { showIndex: true, initialIndex: 2, symbols: ["img 1.gif", "img 2.png", "img 3.svg"], - value: undefined + pvData: [ + { + value: undefined, + connected: true, + readonly: false, + effectivePvName: "pv" + } + ] }; render(); @@ -100,7 +129,14 @@ describe(" from .bob file", (): void => { arrayIndex: 0, showIndex: true, symbols: ["img 1.gif", "img 2.png", "img 3.svg"], - value: arrayValue + pvData: [ + { + value: arrayValue, + connected: true, + readonly: false, + effectivePvName: "pv" + } + ] }; render(); @@ -110,7 +146,14 @@ describe(" from .bob file", (): void => { test("matches snapshot (without index)", (): void => { const symbolProps = { symbols: ["img 1.gif"], - value: new DType({ stringValue: "0" }) + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: new DType({ stringValue: "0" }) + } as Partial as PvDatum + ] }; const { asFragment } = render( @@ -123,7 +166,14 @@ describe(" from .bob file", (): void => { test("matches snapshot (with index)", (): void => { const symbolProps = { symbols: ["img 1.gif", "img 2.png", "img 3.svg"], - value: new DType({ stringValue: "2" }) + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: new DType({ stringValue: "2" }) + } as Partial as PvDatum + ] }; const { asFragment } = render( @@ -136,7 +186,14 @@ describe(" from .bob file", (): void => { test("matches snapshot (using fallback symbol)", (): void => { const symbolProps = { symbols: ["img 1.gif"], - value: new DType({ doubleValue: 1 }) + pvData: [ + { + effectivePvName: "TEST:PV", + connected: true, + readonly: true, + value: new DType({ doubleValue: 1 }) + } as Partial as PvDatum + ] }; const { asFragment } = render( diff --git a/src/ui/widgets/Symbol/symbol.tsx b/src/ui/widgets/Symbol/symbol.tsx index 8a72a778..088587b5 100644 --- a/src/ui/widgets/Symbol/symbol.tsx +++ b/src/ui/widgets/Symbol/symbol.tsx @@ -23,6 +23,7 @@ import { MacroContext } from "../../../types/macros"; import { ExitFileContext, FileContext } from "../../../misc/fileContext"; import { DType } from "../../../types/dtypes"; import classes from "./symbol.module.css"; +import { getPvValueAndName } from "../utils"; const SymbolProps = { imageFile: StringPropOpt, @@ -74,21 +75,24 @@ export const SymbolComponent = (props: SymbolComponentProps): JSX.Element => { transparent = true, backgroundColor = "white", showBooleanLabel = false, - enabled = true + enabled = true, + pvData } = props; + const { value } = getPvValueAndName(pvData); + const style = commonCss(props as any); // If symbols and not imagefile, we're in a bob file const isBob = props.symbols ? true : false; const symbols = props.symbols ? props.symbols : []; // Convert our value to an index, or use the initialIndex - const index = convertValueToIndex(props.value, initialIndex, arrayIndex); + const index = convertValueToIndex(value, initialIndex, arrayIndex); const regex = / [0-9]\./; let imageFile = isBob ? symbols[index] : props.imageFile; // If no provided image file if (!imageFile) imageFile = fallbackSymbol; - const intValue = DType.coerceDouble(props.value); + const intValue = DType.coerceDouble(value); if (!isNaN(intValue) && !isBob) { imageFile = imageFile.replace(regex, ` ${intValue.toFixed(0)}.`); } @@ -177,7 +181,7 @@ export const SymbolComponent = (props: SymbolComponentProps): JSX.Element => { {...props} textAlignV="bottom" backgroundColor={Color.TRANSPARENT} - text={props.value?.getStringValue()} + text={value?.getStringValue()} >
diff --git a/src/ui/widgets/Tank/tank.test.tsx b/src/ui/widgets/Tank/tank.test.tsx index 11ff88dd..873fcbe6 100644 --- a/src/ui/widgets/Tank/tank.test.tsx +++ b/src/ui/widgets/Tank/tank.test.tsx @@ -4,6 +4,7 @@ import { describe, test, expect, beforeEach, vi } from "vitest"; import { TankComponent } from "./tank"; import { Font } from "../../../types/font"; import { Color, DType } from "../../../types"; +import { PvDatum } from "../../../redux/csState"; // Mock the MUI X-Charts components vi.mock("@mui/x-charts/BarChart", () => ({ @@ -36,13 +37,17 @@ const mockFontCss = vi.fn().mockReturnValue({ describe("TankComponent", () => { // Basic test setup const defaultProps = { - value: { - getDoubleValue: () => 50, - display: { - units: "mm", - controlRange: { min: 0, max: 100 } - } - } as Partial as DType, + pvData: [ + { + value: { + getDoubleValue: () => 50, + display: { + units: "mm", + controlRange: { min: 0, max: 100 } + } + } as Partial as DType + } as Partial as PvDatum + ], connected: true, readonly: true, pvName: "TEST:PV", @@ -126,7 +131,11 @@ describe("TankComponent", () => { test("handles undefined PV value gracefully", () => { const props = { ...defaultProps, - value: undefined + pvData: [ + { + value: undefined + } as Partial as PvDatum + ] }; render(); @@ -140,13 +149,17 @@ describe("TankComponent", () => { test("handles edge case with value equal to maximum", () => { const props = { ...defaultProps, - value: { - getDoubleValue: () => 100, - display: { - units: "mm", - controlRange: { min: 0, max: 100 } - } - } as Partial as DType + pvData: [ + { + value: { + getDoubleValue: () => 100, + display: { + units: "mm", + controlRange: { min: 0, max: 100 } + } + } as Partial as DType + } as Partial as PvDatum + ] }; render(); @@ -161,13 +174,17 @@ describe("TankComponent", () => { test("handles edge case with value below minimum", () => { const props = { ...defaultProps, - value: { - getDoubleValue: () => -10, - display: { - units: "mm", - controlRange: { min: 0, max: 100 } - } - } as Partial as DType + pvData: [ + { + value: { + getDoubleValue: () => -10, + display: { + units: "mm", + controlRange: { min: 0, max: 100 } + } + } as Partial as DType + } as Partial as PvDatum + ] }; render(); diff --git a/src/ui/widgets/Tank/tank.tsx b/src/ui/widgets/Tank/tank.tsx index b04a5cd6..7981b265 100644 --- a/src/ui/widgets/Tank/tank.tsx +++ b/src/ui/widgets/Tank/tank.tsx @@ -2,7 +2,7 @@ import React from "react"; import { BarChart } from "@mui/x-charts/BarChart"; import { Box } from "@mui/material"; import { Widget } from "../widget"; -import { PVInputComponent, PVWidgetPropType } from "../widgetProps"; +import { PVComponent, PVWidgetPropType } from "../widgetProps"; import { registerWidget } from "../register"; import { FloatPropOpt, @@ -15,6 +15,7 @@ import { } from "../propTypes"; import { Color } from "../../../types/color"; import { XAxis, YAxis } from "@mui/x-charts"; +import { getPvValueAndName } from "../utils"; export const TankProps = { minimum: FloatPropOpt, @@ -35,11 +36,10 @@ export const TankProps = { }; export const TankComponent = ( - props: InferWidgetProps & PVInputComponent + props: InferWidgetProps & PVComponent ): JSX.Element => { const { - value, - pvName, + pvData, limitsFromPv = false, showLabel = false, font, @@ -53,6 +53,8 @@ export const TankComponent = ( transparent = false // This property only exists in CSStudio, so default to false } = props; + const { value, effectivePvName: pvName } = getPvValueAndName(pvData); + const backgroundColor = transparent ? "transparent" : (props.backgroundColor?.toString() ?? "rgba(250, 250, 250, 1)"); @@ -131,7 +133,7 @@ export const TankComponent = ( data: [numValue], stack: "total", color: fillColor.toString(), - label: pvName, + label: pvName?.toString(), type: "bar", valueFormatter: val => { return showUnits && units && val diff --git a/src/ui/widgets/Thermometer/themometer.test.tsx b/src/ui/widgets/Thermometer/themometer.test.tsx index 25611630..1d9ce8cb 100644 --- a/src/ui/widgets/Thermometer/themometer.test.tsx +++ b/src/ui/widgets/Thermometer/themometer.test.tsx @@ -10,6 +10,7 @@ import { } from "./thermometer"; import { Color } from "../../../types/color"; import { DType } from "../../../types"; +import { PvDatum } from "../../../redux/csState"; // Mock d3 functionality vi.mock("d3", () => { @@ -38,10 +39,15 @@ vi.mock("d3", () => { }); describe("Thermometer Component", () => { - const mandatoryProps = { - connected: false, + const pvDatum = { + effectivePvName: "PV:Test", + connected: true, readonly: true, - pvName: "PV:Test" + value: new DType({ stringValue: "1234" }) + } as Partial as PvDatum; + + const mandatoryProps = { + pvData: [pvDatum] }; const mockValue = { @@ -332,13 +338,12 @@ describe("Thermometer Component", () => { it("should render with custom props", () => { render( ); diff --git a/src/ui/widgets/Thermometer/thermometer.tsx b/src/ui/widgets/Thermometer/thermometer.tsx index ebe94043..0b93a253 100644 --- a/src/ui/widgets/Thermometer/thermometer.tsx +++ b/src/ui/widgets/Thermometer/thermometer.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef } from "react"; import * as d3 from "d3"; import { Widget } from "../widget"; -import { PVInputComponent, PVWidgetPropType } from "../widgetProps"; +import { PVComponent, PVWidgetPropType } from "../widgetProps"; import { registerWidget } from "../register"; import { FloatPropOpt, @@ -13,6 +13,7 @@ import { import { Color } from "../../../types/color"; import { Box } from "@mui/material"; import { DType } from "../../../types"; +import { getPvValueAndName } from "../utils"; // This is the angle between vertical and the line from the center of the bulb to the intersection of the stem and bulb export const bulbStemAngle = Math.PI / 5; @@ -42,17 +43,18 @@ interface ThermometerDimensions { } export const ThermometerComponent = ( - props: InferWidgetProps & PVInputComponent + props: InferWidgetProps & PVComponent ): JSX.Element => { const svgRef = useRef(null); const { - value, + pvData, limitsFromPv = false, height = 160, width = 40, fillColor = Color.fromRgba(60, 255, 60, 1) } = props; + const { value } = getPvValueAndName(pvData); const colors = useMemo( () => ({ diff --git a/src/ui/widgets/XYPlot/xyPlot.tsx b/src/ui/widgets/XYPlot/xyPlot.tsx index b6c2cf99..f3b59b58 100644 --- a/src/ui/widgets/XYPlot/xyPlot.tsx +++ b/src/ui/widgets/XYPlot/xyPlot.tsx @@ -21,7 +21,7 @@ import { NewAxisSettings } from "./xyPlotOptions"; import { Color } from "../../../types/color"; -import { trimFromString } from "../utils"; +import { getPvValueAndName, trimFromString } from "../utils"; import { Trace } from "../../../types/trace"; import { Axis } from "../../../types/axis"; @@ -50,7 +50,7 @@ export const XYPlotComponent = (props: XYPlotComponentProps): JSX.Element => { const { height = 250, width = 400, - value, + pvData, plotBackgroundColor = Color.fromRgba(255, 255, 255), title = "", titleFont, @@ -60,6 +60,8 @@ export const XYPlotComponent = (props: XYPlotComponentProps): JSX.Element => { traces = [new Trace()], axes = [new Axis({ xAxis: true }), new Axis({ xAxis: false })] } = props; + const { value } = getPvValueAndName(pvData); + // TO DO - having all these checks is not ideal if ( value?.value.arrayValue && diff --git a/src/ui/widgets/propTypes.ts b/src/ui/widgets/propTypes.ts index db21f1fd..2de58cf0 100644 --- a/src/ui/widgets/propTypes.ts +++ b/src/ui/widgets/propTypes.ts @@ -32,6 +32,10 @@ export const BoolPropOpt = PropTypes.bool; export const PvProp = PropTypes.instanceOf(PV).isRequired; export const PvPropOpt = PropTypes.instanceOf(PV); +export const PVMetadataType = PropTypes.shape({ + pvName: PvPropOpt +}); + export const ChildrenProp = PropTypes.node.isRequired; export const ChildrenPropOpt = PropTypes.node; diff --git a/src/ui/widgets/tooltip.ts b/src/ui/widgets/tooltip.ts index 0f9ae4bc..1d3c0e0c 100644 --- a/src/ui/widgets/tooltip.ts +++ b/src/ui/widgets/tooltip.ts @@ -1,4 +1,6 @@ +import { PvDatum } from "../../redux/csState"; import { AlarmQuality, DType } from "../../types/dtypes"; +import { getPvValueAndName } from "./utils"; function tooltipValue(connected?: boolean, value?: DType): string { if (value) { @@ -30,12 +32,14 @@ function tooltipValue(connected?: boolean, value?: DType): string { } export function resolveTooltip(props: { - connected: boolean; - value: DType; + pvData: PvDatum[]; tooltip: string; }): string | undefined { const pvValueRegex = /\${pvValue}|\${pv_value}/g; - const { connected, value, tooltip } = props; + const { pvData, tooltip } = props; + + const { value, connected } = getPvValueAndName(pvData); + if (tooltip.match(pvValueRegex)) { const ttval = tooltipValue(connected, value); return tooltip.replace(pvValueRegex, ttval); diff --git a/src/ui/widgets/utils.ts b/src/ui/widgets/utils.ts index 0d22c080..a5352815 100644 --- a/src/ui/widgets/utils.ts +++ b/src/ui/widgets/utils.ts @@ -1,3 +1,5 @@ +import { PvDatum } from "../../redux/csState"; + /** * Converts string from snake case to camel * case. @@ -145,3 +147,12 @@ export function convertStringTimePeriod(period: string): number { const time = (isNaN(multiplier) ? 1 : multiplier) * match.value * 1000; return time; } + +export const getPvValueAndName = (pvDataCollection: PvDatum[], index = 0) => { + const pvData = pvDataCollection ?? []; + const value = pvData[index]?.value; + const effectivePvName = pvData[index]?.effectivePvName; + const connected = pvData[index]?.connected; + + return { value, effectivePvName, connected }; +}; diff --git a/src/ui/widgets/widget.test.tsx b/src/ui/widgets/widget.test.tsx index 2033af5d..42231874 100644 --- a/src/ui/widgets/widget.test.tsx +++ b/src/ui/widgets/widget.test.tsx @@ -45,7 +45,7 @@ describe("", (): void => { const { getByText } = contextRender( ", (): void => { // simulate middle click fireEvent.mouseDown(label, { button: 1 }); expect(getByText(/.*hi.*/)).toBeInTheDocument(); - expect(copyToClipboard).toHaveBeenCalledWith(pv); + expect(copyToClipboard).toHaveBeenCalledWith(pv.toString()); }); }); diff --git a/src/ui/widgets/widget.tsx b/src/ui/widgets/widget.tsx index 9fa69860..d5d56ec7 100644 --- a/src/ui/widgets/widget.tsx +++ b/src/ui/widgets/widget.tsx @@ -1,4 +1,4 @@ -import React, { CSSProperties, useContext, useState } from "react"; +import React, { CSSProperties, useCallback, useContext, useState } from "react"; import log from "loglevel"; import copyToClipboard from "clipboard-copy"; @@ -6,10 +6,13 @@ import { ContextMenu } from "../components/ContextMenu/contextMenu"; import ctxtClasses from "../components/ContextMenu/contextMenu.module.css"; import tooltipClasses from "./tooltip.module.css"; import { useMacros } from "../hooks/useMacros"; -import { useConnection } from "../hooks/useConnection"; +import { useConnectionMultiplePv } from "../hooks/useConnection"; import { useId } from "react-id-generator"; import { useRules } from "../hooks/useRules"; -import { PVWidgetComponent, WidgetComponent } from "./widgetProps"; +import { + ConnectingComponentWidgetProps, + PVWidgetComponent +} from "./widgetProps"; import { Border, BorderStyle } from "../../types/border"; import { Color } from "../../types/color"; import { AlarmQuality } from "../../types/dtypes"; @@ -20,6 +23,24 @@ import { executeAction, WidgetAction, WidgetActions } from "./widgetActions"; import { Popover } from "react-tiny-popover"; import { resolveTooltip } from "./tooltip"; +const ALARM_SEVERITY_MAP = { + [AlarmQuality.ALARM]: 1, + [AlarmQuality.WARNING]: 2, + [AlarmQuality.INVALID]: 3, + [AlarmQuality.UNDEFINED]: 4, + [AlarmQuality.CHANGING]: 5, + [AlarmQuality.VALID]: 6 +}; + +const AlarmColorsMap = { + [AlarmQuality.VALID]: Color.BLACK, + [AlarmQuality.WARNING]: Color.WARNING, + [AlarmQuality.ALARM]: Color.ALARM, + [AlarmQuality.INVALID]: Color.INVALID, + [AlarmQuality.UNDEFINED]: Color.UNDEFINED, + [AlarmQuality.CHANGING]: Color.CHANGING +}; + /** * Return a CSSProperties object for props that multiple widgets may have. * @param props properties of the widget to be formatted @@ -60,12 +81,21 @@ export function commonCss(props: { */ export const ConnectingComponent = (props: { component: React.FC; - widgetProps: any; + widgetProps: ConnectingComponentWidgetProps; containerStyle: CSSProperties; onContextMenu?: (e: React.MouseEvent) => void; }): JSX.Element => { const Component = props.component; - const { id, alarmBorder = false, pvName, type } = props.widgetProps; + const { id, alarmBorder = false, pvMetadataList } = props.widgetProps; + + const pvName = + pvMetadataList && pvMetadataList?.length > 0 + ? pvMetadataList[0]?.pvName + : undefined; + + const pvNames = pvMetadataList + ? pvMetadataList.map(metadata => metadata?.pvName).filter(pv => !!pv) + : []; // Popover logic, used for middle-click tooltip. const [popoverOpen, setPopoverOpen] = useState(false); @@ -76,13 +106,14 @@ export const ConnectingComponent = (props: { (e.currentTarget as HTMLDivElement).classList.add( tooltipClasses.Copying ); - copyToClipboard(pvName); + copyToClipboard(pvName.toString()); } // Stop regular middle-click behaviour if showing tooltip. e.preventDefault(); e.stopPropagation(); } }; + const mouseUp = (e: React.MouseEvent): void => { if (e.button === 1 && e.currentTarget) { setPopoverOpen(false); @@ -93,38 +124,39 @@ export const ConnectingComponent = (props: { } }; - const [effectivePvName, connected, readonly, latestValue] = useConnection( + const { pvData } = useConnectionMultiplePv( id, - pvName?.qualifiedName(), - type + pvNames.map(x => x.qualifiedName()) ); - // Always indicate with border if PV is disconnected. let border = props.widgetProps.border; - if (props.widgetProps.pvName && connected === false) { - border = new Border(BorderStyle.Dotted, Color.DISCONNECTED, 3); - } else if (alarmBorder) { - // Implement alarm border for all widgets if configured. - const severity = latestValue?.getAlarm()?.quality || AlarmQuality.VALID; - const colors: { [key in AlarmQuality]: Color } = { - [AlarmQuality.VALID]: Color.BLACK, - [AlarmQuality.WARNING]: Color.WARNING, - [AlarmQuality.ALARM]: Color.ALARM, - [AlarmQuality.INVALID]: Color.INVALID, - [AlarmQuality.UNDEFINED]: Color.UNDEFINED, - [AlarmQuality.CHANGING]: Color.CHANGING - }; - if (severity !== AlarmQuality.VALID) { - border = new Border(BorderStyle.Line, colors[severity], 2); + if (pvNames) { + let alarmSeverity = AlarmQuality.VALID; + + if (alarmBorder && pvData) { + alarmSeverity = pvData + .map(x => x.value?.getAlarm()?.quality ?? AlarmQuality.VALID) + .reduce( + (mostSevereSoFar, currentItem) => + ALARM_SEVERITY_MAP[mostSevereSoFar] < + ALARM_SEVERITY_MAP[currentItem] + ? mostSevereSoFar + : currentItem, + alarmSeverity + ); + } + + if (alarmSeverity !== AlarmQuality.VALID) { + border = new Border(BorderStyle.Line, AlarmColorsMap[alarmSeverity], 2); + } else if (pvData && !pvData.every(x => x.connected)) { + border = new Border(BorderStyle.Dotted, Color.DISCONNECTED, 3); } } - const widgetProps = { + const widgetTooltipProps = { ...props.widgetProps, - pvName: effectivePvName, - value: latestValue, - connected, - readonly, + tooltip: props.widgetProps.tooltip ?? "", + pvData, border }; @@ -137,11 +169,12 @@ export const ConnectingComponent = (props: { onMouseUp={mouseUp} style={props.containerStyle} > - + ); - if (widgetProps.tooltip) { - const resolvedTooltip = resolveTooltip(widgetProps); + + if (widgetTooltipProps.tooltip) { + const resolvedTooltip = resolveTooltip(widgetTooltipProps); const popoverContent = (): JSX.Element => { return
{resolvedTooltip}
; }; @@ -180,15 +213,21 @@ const DEFAULT_TOOLTIP = "${pvName}\n${pvValue}"; * @param props * @returns JSX Element to render */ -export const Widget = ( - props: PVWidgetComponent | WidgetComponent -): JSX.Element => { +export const Widget = (props: PVWidgetComponent): JSX.Element => { const [id] = useId(); + const files = useContext(FileContext); const exitContext = useContext(ExitFileContext); - - // Logic for context menu. const [contextOpen, setContextOpen] = useState(false); + + const contextMenuTriggerCallback = useCallback( + (action: WidgetAction): void => { + executeAction(action, files, exitContext); + setContextOpen(false); + }, + [files, exitContext, setContextOpen] + ); + const [coords, setCoords] = useState<[number, number]>([0, 0]); let onContextMenu: ((e: React.MouseEvent) => void) | undefined = undefined; const actionsPresent = props.actions && props.actions.actions.length > 0; @@ -218,13 +257,13 @@ export const Widget = ( ); } - function triggerCallback(action: WidgetAction): void { - executeAction(action, files, exitContext); - setContextOpen(false); - } let tooltip = props.tooltip; // Set default tooltip only for PV-enabled widgets. - if ("pvName" in props && !props.tooltip) { + if ( + props?.pvMetadataList && + props.pvMetadataList.length > 0 && + !props.tooltip + ) { tooltip = DEFAULT_TOOLTIP; } const idProps = { ...props, id: id, tooltip: tooltip }; @@ -233,7 +272,7 @@ export const Widget = ( log.debug(`Widget id ${id}`); const macroProps = useMacros(idProps); // Then rules - const ruleProps = useRules(macroProps) as PVWidgetComponent & { id: string }; + const ruleProps = useRules(macroProps); log.debug(`ruleProps ${ruleProps}`); log.debug(ruleProps); @@ -254,7 +293,7 @@ export const Widget = ( )} ; +const PositionPropsType = { + position: PositionProp +}; -// Internal type for creating widgets -export type WidgetComponent = WidgetProps & { - baseWidget: React.FC; +export const WidgetPropType = { + ...PositionPropsType, + ...BasicPropsType }; -// Internal prop types object for properties which are not in a standard widget -const PVExtras = { - pvName: PvPropOpt, +const PvPropsAndMetdataType = { + alarmBorder: BoolPropOpt, pvType: PvTypePropOpt, - alarmBorder: BoolPropOpt + pvMetadataList: PropTypes.arrayOf(PVMetadataType) }; + // PropTypes object for a PV widget which can be expanded export const PVWidgetPropType = { - ...PVExtras, - ...WidgetPropType + ...WidgetPropType, + ...PvPropsAndMetdataType }; -export type PVWidgetProps = WidgetProps & InferWidgetProps; -export type PVWidgetComponent = PVWidgetProps & { baseWidget: React.FC }; -export type AnyProps = PVWidgetComponent & { - id: string; - connected?: boolean; - readonly?: boolean; - value?: DType; -} & { + +type BasicProps = InferWidgetProps; +type PositionProps = InferWidgetProps; + +type PvPropsAndMetdataProps = InferWidgetProps; + +type AnyOtherProps = { // All other props with valid types. + id: string; [x: string]: GenericProp; }; -export interface Component { +type BaseWidgetProps = { baseWidget: React.FC }; + +type ComponentProps = { style?: Record; -} +}; + +// Props used by the ConnectingComponentWidget wrapper +export type ConnectingComponentWidgetProps = BasicProps & + PvPropsAndMetdataProps & + PvDataCollection & + AnyOtherProps; + +// Props for the Widget wrapper component +export type PVWidgetComponent = BasicProps & + PositionProps & + BaseWidgetProps & + PvPropsAndMetdataProps; + +// type used by useMacros and useRules (not really props) +export type AnyProps = PVWidgetComponent & PvDataCollection & AnyOtherProps; -export type PVComponent = Component & PvState; -export type PVInputComponent = PVComponent & { pvName: string }; +// Types used by widget components implementations that display a value. +export type PVComponent = ComponentProps & PvDataCollection;