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: .. code-block:: javascript 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: 1. **Create a New Folder** Navigate to `src/dashboard/widgets` and create a new folder named `TarantaWidget`. .. code-block:: bash cd src/dashboard/widgets mkdir TarantaWidget 2. **Create the Main Parent Code** Inside the `TarantaWidget` folder, create a new file called `TarantaWidget.tsx`. .. code-block:: bash touch TarantaWidget/TarantaWidget.tsx 3. **Create the Values Code** Inside the `TarantaWidget` folder, create a new file called `TarantaWidgetValues.tsx`. .. code-block:: bash touch TarantaWidget/TarantaWidgetValues.tsx 4. **Create the Test Files** Inside the `TarantaWidget` folder, create two new files called `TarantaWidget.test.tsx` and `TarantaWidgetValues.test.tsx`. .. code-block:: bash touch TarantaWidget/TarantaWidget.test.tsx touch TarantaWidget/TarantaWidgetValues.test.tsx 5. **Implement the Widget Definition and Component** Open `TarantaWidget.tsx` and add the following code: .. code-block:: javascript 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; interface State { } class TarantaWidget extends Component { 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 (
{attribute.device} - {attribute.attribute}
); } } const definition: WidgetDefinition = { 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; 6. **Implement the Values Component** Open `TarantaWidgetValues.tsx` and add the following code: .. code-block:: javascript 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 = ({ 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 (
[{value};{quality};{timestamp}]
); } export default TarantaWidgetValues; 7. **Add Tests for the Main Component** Open `TarantaWidget.test.tsx` and add the following code: .. code-block:: javascript 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", () => () =>
Mock TarantaWidgetValues
); configure({ adapter: new Adapter() }); const store = configureStore(); interface Inputs { attribute: AttributeInputDefinition & { write: (value: any) => Promise }; 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({element}); expect(wrapper.find("div").exists()).toBe(true); }); }); 8. **Add Tests for the Values Component** Open `TarantaWidgetValues.test.tsx` and add the following code: .. code-block:: javascript 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(); 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 }); });