Questions on ignition-sdk-examples/perspective-component

So I'm using Ignition's perspective component examples as a close reference while I'm building my own module as I go. I was wondering if I could use this post as a general questioning post that others could potentially find useful to reference.

image

So this sample project comes with 3 components, Image, Messenger, and TagCounter. However in the designer subproject, there is only a file for the tag counter. I'm not sure what 'DesignDelegate' means exactly, and why the other two components don't seem to need a file. What purpose does this file serve exactly?

The design delegate java file is used to add UI to "decorate" the property editor or other areas as the selected component changes in the Perspective workspace. It's an optional nicety, not required at all for your component to work.

An example would be the flex container positioning buttons:

3 Likes

So I've tried creating my own component by emulating what's been done for the sample components and successfully built the module! Kind of. After installing the module onto my gateway, it's faulted.

So for learning purposes, I went through and changed all the "fakester" and "rad" to the name of my company, Kanoa. Here's the last bit of the full error message:

image

So I think I must have not properly changed a variable name correctly, but when I go to the GatewayHook file, everything looks fine throughout the whole file and within the startup function too. I'm not sure how to continue to debug this.

image

Static initialization (clinit) is failing in the Image class, on line 23.
Looking at the sample:

Line 23 is attempting to find a class resource, by fully qualified path.
Specifically, this file:

Note it's location (src/main/resources/radimage.props.json). Ensure your changed code agrees on what that file is called, and where it resides.

This is the code in the Image java file as well as the layout of the directory on the left.

On line 23, the project originally had the path as "/radimage.props.json". I've changed the file name and adjusted the path to match it. Is there something else I'm missing?

Do a ./gradlew clean build, then double check the build directory of common to ensure the files are there in the places you expect. You can also open the actual common.jar file in any ZIP tool to check its structure, as well.

1 Like

That did it, awesome! I was noticing that when I reran "gradlew build" it would go through the process but wouldn't even overwrite the previous generated modl. I would have to manually delete it, but it seems like "gradlew clean build" handles all that.

How can I get the component to have an icon? I noticed that none of the sample components have an icon so I wasn't sure where to set that.

The icon to use in the designer should be added to your ComponentBuilder chained call. You need to supply a Swing Icon; we have an InteractiveSvgIcon class you can use to create one from an SVG file in your module's resources.

Got it, so is setting the icon a new separate setter, or is it in .addPaletteEntry?

And to use that class to create the swing icon, and I passing the entire svg code into the createIcon method?

Store your .svg as a resource file (just like the component .json files).
Pass the path to InteractiveSvgIcon.createIcon(path); the return value will be the icon to use.

You would add that icon as an additional call, so e.g. between line 35 and 36 add setIcon(yourSvgIcon).

1 Like

Might be a silly question but I don't think I'm importing and using the code right. Here's my attempt:

The error is : cannot find symbol, symbol being 'InteractiveSvgIcon'

Note the package; com.inductiveautomation.ignition.designer.navtree.icon.InteractiveSVGIcon. The class only exists in the 'designer' scope, which you won't have available to you in your module's common subproject.

Probably the thing to do would be to load the icon only in your DesignerHook. When you register the component in your DesignerHook, create a new descriptor instance (you can use all the values from the one in CopyToClipboard.DESCRIPTOR and add your icon to it there.

Hey Paul, so I've been working on a copy to clipboard button, and have gotten it to work! One thing I was wondering is, is it possible to import the copy to clipboard function as a module script instead? That way we wouldn't be limited by only being able to copying to clipboard through a button.

In theory, a scripting function (Java code running on the gateway) could pass a message over the websocket to the running session, and your module could register a handler for that message, and react to it to run whatever JS code in the session.

I don't know, off the top of my head, whether all the APIs to do that are easily exposed to you as a module developer.

Got it, so it seems like just having the logic contained in the custom component will be an easier time.

On more of an educational note, what are the notable differences between having a module be signed vs. unsigned. I'm developing a component that our clients could potentially be interested and use. If I were to just have them install the unsigned module that I currently have, what would be the consequences of that vs. if I were to be able to have the signed module?

Signing is, ideally, a way to ensure that the code your customers install is the code you generated, as long as they can verify your signing key.
It's not enforced by Ignition itself beyond a default requirement that modules are signed - we don't check certificates for validity, or against a root CA, or anything.
I would encourage you to sign any versions of your module you plan to distribute, though, so that end-users don't have to run a production gateway with the "allow unsigned modules" flag set.

1 Like

I'm currently working on customization of the copy to clipboard button. The TypeScript for it is literally just a button element:

How can I set up properties that allow for targeted styling of the button element itself? I do know that some perspective components like the Table for example have headerStyle, bodyStyle, etc. I'd want to be able to customize some basic button styling in the designer like size, border, color, and text/icon.

If you type one of your props as a Style from '@inductiveautomation/perspective-client';, the class instance should have some helper methods:
image
Which you're able to use in your TSX:
image

I'm still a little lost on how to fully implement that. Here's the full typescript file of the component.

/**
 * Example of a component which displays an image, given a URL.
 */
import * as React from 'react';
import {
    Component,
    ComponentMeta,
    ComponentProps,
    PComponent,
    PropertyTree,
    SizeObject
} from '@inductiveautomation/perspective-client';


// The 'key' or 'id' for this component type.  Component must be registered with this EXACT key in the Java side as well
// as on the client side.  In the client, this is done in the index file where we import and register through the
// ComponentRegistry provided by the perspective-client API.
export const COMPONENT_TYPE = "kanoa.display.copytoclipboard";


// This is the shape of the properties we get from the perspective 'props' property tree.
export interface CopyToClipboardProps {
    text: string;   // the url of the image this component should display
    buttonStyle: object; // object of style properties for the button (?)
}

export class CopyToClipboard extends Component<ComponentProps<CopyToClipboardProps>, any> {
    render() {
        // The props we're interested in.
        const { props: { text, buttonStyle } } = this.props;
        // Read the 'url' property provided by the perspective gateway via the component 'props'.

        async function writeToClipboard() {
            await navigator.clipboard.writeText(text)
        }

        // Note that the topmost piece of dom requires the application of an element reference, events, style and
        // className as shown below otherwise the layout won't work, or any events configured will fail. See render
        // of MessengerComponent in Messenger.tsx for more details.
        return (
            <button onClick={writeToClipboard}>Copy</button>
        );
    }
}


// This is the actual thing that gets registered with the component registry.
export class CopyToClipboardMeta implements ComponentMeta {

    getComponentType(): string {
        return COMPONENT_TYPE;
    }

    // the class or React Type that this component provides
    getViewComponent(): PComponent {
        return CopyToClipboard;
    }

    getDefaultSize(): SizeObject {
        return ({
            width: 360,
            height: 360
        });
    }

    // Invoked when an update to the PropertyTree has occurred,
    // effectively mapping the state of the tree to component props.
    getPropsReducer(tree: PropertyTree): CopyToClipboardProps {
        return {
            text: tree.readString("text", ""),
            buttonStyle: tree.readObject("buttonStyle", {})
        };
    }
}

Here's my attempt at it so far from what I can understand. Would you have an example perhaps for reference?

I think (I'm not an expert in this domain, by any means) your sample would instead look something like this:

/**
 * Example of a component which displays an image, given a URL.
 */
import * as React from 'react';
import {
    Component,
    ComponentMeta,
    ComponentProps,
    PComponent,
    PropertyTree,
    SizeObject,
    Style
} from '@inductiveautomation/perspective-client';


// The 'key' or 'id' for this component type.  Component must be registered with this EXACT key in the Java side as well
// as on the client side.  In the client, this is done in the index file where we import and register through the
// ComponentRegistry provided by the perspective-client API.
export const COMPONENT_TYPE = "kanoa.display.copytoclipboard";


// This is the shape of the properties we get from the perspective 'props' property tree.
export interface CopyToClipboardProps {
    text: string;   // the url of the image this component should display
    buttonStyle: Style; // object of style properties for the button (?)
}

export class CopyToClipboard extends Component<ComponentProps<CopyToClipboardProps>, any> {
    render() {
        // The props we're interested in.
        const { props: { text, buttonStyle } } = this.props;
        // Read the 'url' property provided by the perspective gateway via the component 'props'.

        async function writeToClipboard() {
            await navigator.clipboard.writeText(text)
        }

        // Note that the topmost piece of dom requires the application of an element reference, events, style and
        // className as shown below otherwise the layout won't work, or any events configured will fail. See render
        // of MessengerComponent in Messenger.tsx for more details.
        return (
            <button onClick={writeToClipboard} { ...buttonStyle.toStyleProps() }>Copy</button>
        );
    }
}


// This is the actual thing that gets registered with the component registry.
export class CopyToClipboardMeta implements ComponentMeta {

    getComponentType(): string {
        return COMPONENT_TYPE;
    }

    // the class or React Type that this component provides
    getViewComponent(): PComponent {
        return CopyToClipboard;
    }

    getDefaultSize(): SizeObject {
        return ({
            width: 360,
            height: 360
        });
    }

    // Invoked when an update to the PropertyTree has occurred,
    // effectively mapping the state of the tree to component props.
    getPropsReducer(tree: PropertyTree): CopyToClipboardProps {
        return {
            text: tree.readString("text", ""),
            buttonStyle: tree.readStyle("buttonStyle")
        };
    }
}
1 Like