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:

  1. Create a New Folder

    Navigate to src/dashboard/widgets and create a new folder named TarantaWidget.

    cd src/dashboard/widgets
    mkdir TarantaWidget
    
  2. Create the Main Parent Code

    Inside the TarantaWidget folder, create a new file called TarantaWidget.tsx.

    touch TarantaWidget/TarantaWidget.tsx
    
  3. Create the Values Code

    Inside the TarantaWidget folder, create a new file called TarantaWidgetValues.tsx.

    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.

    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:

    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;
    
  6. 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;
    
  7. 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);
        });
    });
    
  8. 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
      });
    });