How to Deploy a Widget¶
A widget is a bundle consisting of two objects: a definition and a React component. The bundle is typically exported from a file:
const definition = ...;
class TheComponent extends React.Component ...
export default { definition, component: TheComponent };
The definition is a declarative object describing the basic characteristics of a widget, and the inputs that it receives. In the component for the widget, the inputs are made available through a prop named inputs.
Creating a New Widget¶
To create a new widget, follow these steps:
Create a New Folder
Navigate to src/dashboard/widgets and create a new folder named TarantaWidget.
cd src/dashboard/widgets mkdir TarantaWidgetCreate the Main Parent Code
Inside the TarantaWidget folder, create a new file called TarantaWidget.tsx.
touch TarantaWidget/TarantaWidget.tsx
Create the Values Code
Inside the TarantaWidget folder, create a new file called TarantaWidgetValues.tsx.
touch TarantaWidget/TarantaWidgetValues.tsx
Create the Test Files
Inside the TarantaWidget folder, create two new files called TarantaWidget.test.tsx and TarantaWidgetValues.test.tsx.
touch TarantaWidget/TarantaWidget.test.tsx touch TarantaWidget/TarantaWidgetValues.test.tsx
Implement the Widget Definition and Component
Open TarantaWidget.tsx and add the following code:
import React, { Component, CSSProperties } from "react"; import { WidgetProps } from "../types"; import { AttributeInputDefinition, StyleInputDefinition, WidgetDefinition, } from "../../types"; import { parseCss } from "../../components/Inspector/StyleSelector"; import TarantaWidgetValues from "./TarantaWidgetValues"; type Inputs = { attribute: AttributeInputDefinition; widgetCss: StyleInputDefinition; }; type Props = WidgetProps<Inputs>; interface State { } class TarantaWidget extends Component<Props, State> { constructor(props) { super(props); } public render() { const { widgetCss, attribute } = this.props.inputs; const CustomCSS = widgetCss ? parseCss(this.props.inputs.widgetCss).data : {}; const style: CSSProperties = { ...CustomCSS, }; return (<div style={style}>{attribute.device} - {attribute.attribute} <TarantaWidgetValues attribute={attribute}></TarantaWidgetValues> </div>); } } const definition: WidgetDefinition<Inputs> = { type: "TARANTA_WIDGET", name: "Taranta Widget", defaultWidth: 10, defaultHeight: 2, inputs: { attribute: { type: "attribute", label: "", dataFormat: "scalar", required: true, }, widgetCss: { type: "style", label: "Custom CSS", default: "", }, }, }; const TarantaWidgetExport = { component: TarantaWidget, definition }; export default TarantaWidgetExport;Implement the Values Component
Open TarantaWidgetValues.tsx and add the following code:
import React from "react"; import { AttributeInput } from "../../types"; import { useSelector } from "react-redux"; import { IRootState } from "../../../shared/state/reducers/rootReducer"; import { getAttributeLastQualityFromState, getAttributeLastTimeStampFromState, getAttributeLastValueFromState } from "../../../shared/utils/getLastValueHelper"; interface Props { attribute: AttributeInput; } const TarantaWidgetValues: React.FC<Props> = ({ attribute }) => { const value = useSelector((state: IRootState) => { return getAttributeLastValueFromState( state.messages, attribute.device, attribute.attribute ); }); const timestamp = useSelector((state: IRootState) => { return getAttributeLastTimeStampFromState( state.messages, attribute.device, attribute.attribute )?.toString(); }); const quality = useSelector((state: IRootState) => { return getAttributeLastQualityFromState( state.messages, attribute.device, attribute.attribute )?.toString(); }); return (<div>[{value};{quality};{timestamp}]</div>); } export default TarantaWidgetValues;Add Tests for the Main Component
Open TarantaWidget.test.tsx and add the following code:
import React from "react"; import { configure, mount } from "enzyme"; import Adapter from "@cfaester/enzyme-adapter-react-18"; import { Provider } from "react-redux"; import configureStore from "../../../shared/state/store/configureStore"; import TarantaWidgetExport from "./TarantaWidget"; import { AttributeInputDefinition, StyleInputDefinition } from "../../types"; // Mock TarantaWidgetValues component jest.mock("./TarantaWidgetValues", () => () => <div>Mock TarantaWidgetValues</div>); configure({ adapter: new Adapter() }); const store = configureStore(); interface Inputs { attribute: AttributeInputDefinition & { write: (value: any) => Promise<void> }; widgetCss?: StyleInputDefinition; } describe("TarantaWidgetTests", () => { let myInput: Inputs; it("renders without crashing", () => { myInput = { attribute: { device: "sys/tg_test/1", attribute: "short_scalar", type: "attribute", write: jest.fn().mockResolvedValueOnce(undefined), }, }; let element = React.createElement(TarantaWidgetExport.component, { id: 1, mode: "run", t0: 1, actualWidth: 100, actualHeight: 100, inputs: myInput, }); let wrapper = mount(<Provider store={store}>{element}</Provider>); expect(wrapper.find("div").exists()).toBe(true); }); });Add Tests for the Values Component
Open TarantaWidgetValues.test.tsx and add the following code:
import React from "react"; import { render } from "@testing-library/react"; import { useSelector as useSelectorMock } from "react-redux"; import TarantaWidgetValues from "./TarantaWidgetValues"; import { AttributeInput } from "../../types"; import { getAttributeLastQualityFromState as getAttributeLastQualityFromStateMock, getAttributeLastTimeStampFromState as getAttributeLastTimeStampFromStateMock, getAttributeLastValueFromState as getAttributeLastValueFromStateMock, } from "../../../shared/utils/getLastValueHelper"; jest.mock("react-redux", () => ({ useSelector: jest.fn(), })); const getAttributeLastQualityFromState = getAttributeLastQualityFromStateMock as jest.Mock; const getAttributeLastTimeStampFromState = getAttributeLastTimeStampFromStateMock as jest.Mock; const getAttributeLastValueFromState = getAttributeLastValueFromStateMock as jest.Mock; jest.mock("../../../shared/utils/getLastValueHelper", () => ({ getAttributeLastValueFromState: jest.fn(), getAttributeLastTimeStampFromState: jest.fn(), getAttributeLastQualityFromState: jest.fn(), })); const useSelector = useSelectorMock as jest.Mock; describe("TarantaWidgetValues", () => { const mockAttribute: AttributeInput = { device: "test/device", attribute: "test_attribute", label: "", history: [], dataType: "", dataFormat: "", isNumeric: true, unit: "", enumlabels: [], write: jest.fn(), value: "", writeValue: "", quality: "", timestamp: Date.now(), }; beforeEach(() => { useSelector.mockImplementation((selectorFn: any) => selectorFn({ messages: {}, ui: { mode: "run", }, }) ); getAttributeLastValueFromState.mockReturnValue("test_value"); getAttributeLastTimeStampFromState.mockReturnValue(Date.now() / 1000); getAttributeLastQualityFromState.mockReturnValue("ATTR_VALID"); }); afterEach(() => { jest.resetAllMocks(); }); it("should render the attribute value, quality, and timestamp", () => { const { container } = render(<TarantaWidgetValues attribute={mockAttribute} />); expect(container.textContent).toContain("test_value"); expect(container.textContent).toContain("ATTR_VALID"); expect(container.textContent).toContain((Date.now() / 1000).toString().substring(0, 10)); // Just checking the timestamp part }); });