Questions on ignition-sdk-examples/perspective-component

Worth a shot, really appreciate that. Will there need to be changes made to the component's .props.json file as well?

If you want maximal integration in the designer, it looks like you want to set up the schema for the style property like this:

          "style": {
            "$ref" : "urn:ignition-schema:schemas/style-properties.schema.json"
          },

Except your key would be buttonStyle.

Hey Paul I'm revisiting this topic and wanted to show you my attempt at it. This is the DesignerHook file.

package org.kanoa.designer;

import com.inductiveautomation.ignition.common.BundleUtil;
import com.inductiveautomation.ignition.common.licensing.LicenseState;
import com.inductiveautomation.ignition.common.util.LoggerEx;
import com.inductiveautomation.perspective.common.api.ComponentDescriptor;
import com.inductiveautomation.perspective.common.api.ComponentDescriptorImpl;
import com.inductiveautomation.ignition.designer.model.AbstractDesignerModuleHook;
import com.inductiveautomation.ignition.designer.model.DesignerContext;
import com.inductiveautomation.ignition.designer.navtree.icon.InteractiveSvgIcon;
import com.inductiveautomation.perspective.designer.DesignerComponentRegistry;
import com.inductiveautomation.perspective.designer.api.ComponentDesignDelegateRegistry;
import com.inductiveautomation.perspective.designer.api.PerspectiveDesignerInterface;
import org.kanoa.common.component.display.Image;
import org.kanoa.common.component.display.Messenger;
import org.kanoa.common.component.display.TagCounter;
import org.kanoa.common.component.display.CopyToClipboard;
import org.kanoa.common.KanoaComponents;
import org.kanoa.designer.component.TagCountDesignDelegate;


/**
 * The 'hook' class for the designer scope of the module. Registered in the ignitionModule configuration of the
 * root build.gradle file.
 */
public class KanoaDesignerHook extends AbstractDesignerModuleHook {
    private static final LoggerEx logger = LoggerEx.newBuilder().build("KanoaComponents");

    private DesignerContext context;
    private DesignerComponentRegistry registry;
    private ComponentDesignDelegateRegistry delegateRegistry;

    static {
        BundleUtil.get().addBundle("kanoacomponents", KanoaDesignerHook.class.getClassLoader(), "kanoacomponents");
    }

    public KanoaDesignerHook() {
        logger.info("Registering Kanoa Components in Designer!");
    }

    @Override
    public void startup(DesignerContext context, LicenseState activationState) {
        this.context = context;
        init();
    }

    private void init() {
        logger.debug("Initializing registry entrants...");

        PerspectiveDesignerInterface pdi = PerspectiveDesignerInterface.get(context);

        registry = pdi.getDesignerComponentRegistry();
        delegateRegistry = pdi.getComponentDesignDelegateRegistry();

        InteractiveSvgIcon componentIcon = InteractiveSvgIcon.createIcon("/gateway/src/main/resources/mounted/img/content-copy.svg");
        ComponentDescriptor CTCdescriptor = ComponentDescriptorImpl.ComponentBuilder.newBuilder()
            .setPaletteCategory(KanoaComponents.COMPONENT_CATEGORY)
            .setId(COMPONENT_ID)
            .setModuleId(KanoaComponents.MODULE_ID)
            .setSchema(SCHEMA) //  this could alternatively be created purely in Java if desired
            .setName("Copy To Clipboard")
            .addPaletteEntry("", "Copy To Clipboard", "A button that copies text to the clipboard.", null, null)
            .setIcon(componentIcon)
            .setDefaultMetaName("copyToClipboard")
            .setResources(KanoaComponents.BROWSER_RESOURCES)
            .build();
        // register components to get them on the palette
        registry.registerComponent(Image.DESCRIPTOR);
        registry.registerComponent(TagCounter.DESCRIPTOR);
        registry.registerComponent(Messenger.DESCRIPTOR);
        registry.registerComponent(CTCdescriptor);

        // register design delegates to get the special config UI when a component type is selected in the designer
        delegateRegistry.register(TagCounter.COMPONENT_ID, new TagCountDesignDelegate());
    }


    @Override
    public void shutdown() {
        removeComponents();
    }

    private void removeComponents() {
        registry.removeComponent(Image.COMPONENT_ID);
        registry.removeComponent(TagCounter.COMPONENT_ID);
        registry.removeComponent(Messenger.COMPONENT_ID);
        registry.removeComponent(CopyToClipboard.COMPONENT_ID);

        delegateRegistry.remove(TagCounter.COMPONENT_ID);
    }
}

So there's a lot in the component's java file in the common subproject that seems like it would need to be imported over. I feel as if this really muddles up the modularity of the code, so I was just checking to see if this is what you meant.

Unfortunately, since the builder on ComponentDescriptorImpl doesn't have a copy constructor, there's no way to avoid the duplication. You could create your own class that implements ComponentDescriptor, instead of using ours. That way, you could use a Supplier<Icon> to allow for a null icon in the 'common' scope, but a 'real' icon in the designer scope. There's definitely improvements we could make in the API here, but there are options. You could also create a 'delegate' abstract class to use in the designer scope, something like the one below. Then when you create an instance, you can 'delegate' to the component descriptor in Common, but choose to override only the getIcon method:

Java Code
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.Icon;

import com.inductiveautomation.ignition.common.gson.JsonObject;
import com.inductiveautomation.ignition.common.jsonschema.JsonSchema;
import com.inductiveautomation.perspective.common.api.BrowserResource;
import com.inductiveautomation.perspective.common.api.ComponentDescriptor;
import com.inductiveautomation.perspective.common.api.ComponentEventDescriptor;
import com.inductiveautomation.perspective.common.api.PaletteEntry;

public abstract class DelegatingComponentDescriptor implements ComponentDescriptor {
    private final ComponentDescriptor delegate;

    public DelegatingComponentDescriptor(ComponentDescriptor delegate) {
        this.delegate = delegate;
    }

    @Override
    @Nonnull
    public String id() {
        return delegate.id();
    }

    @Override
    public String name() {
        return delegate.name();
    }

    @Override
    public boolean deprecated() {
        return delegate.deprecated();
    }

    @Override
    @Nonnull
    public Collection<PaletteEntry> paletteEntries() {
        return delegate.paletteEntries();
    }

    @Override
    @Nonnull
    public String paletteCategory() {
        return delegate.paletteCategory();
    }

    @Override
    @Nonnull
    public String defaultMetaName() {
        return delegate.defaultMetaName();
    }

    @Override
    @Nonnull
    public String moduleId() {
        return delegate.moduleId();
    }

    @Override
    public JsonObject defaultProperties() {
        return delegate.defaultProperties();
    }

    @Override
    public Optional<JsonObject> childPositionDefaults() {
        return delegate.childPositionDefaults();
    }

    @Override
    @Nonnull
    public Set<BrowserResource> browserResources() {
        return delegate.browserResources();
    }

    @Override
    @Nullable
    public JsonSchema schema() {
        return delegate.schema();
    }

    @Override
    @Nullable
    public JsonSchema childPositionSchema() {
        return delegate.childPositionSchema();
    }

    @Override
    @Nonnull
    public Collection<ComponentEventDescriptor> events() {
        return delegate.events();
    }

    @Override
    @Nonnull
    public Collection<ExtensionFunctionDescriptor> extensionFunctions() {
        return delegate.extensionFunctions();
    }

    @Override
    @Nullable
    public JsonObject getInitialProps(String variantId) {
        return delegate.getInitialProps(variantId);
    }

    @Override
    public Optional<JsonObject> getExampleChildPositionDefaults() {
        return delegate.getExampleChildPositionDefaults();
    }

    @Override
    @Nonnull
    public Optional<Icon> getIcon() {
        return delegate.getIcon();
    }
}
        componentRegistry.registerComponent(new DelegatingComponentDescriptor(Image.DESCRIPTOR) {
            @Nonnull
            @Override
            public Optional<Icon> getIcon() {
                return Optional.of(MY_ICON);
            }
        });

This is a common Java approach to make up for concrete classes that aren't sufficiently flexible.

Got it, I think i'd rather just be okay with not having an icon in the designer at this point, but thanks a lot anyway for the suggestion.

Do you know of any resources I could reference to generate a passable certificate and keystore in order to have the module be signed? I've done a good amount of digging online and there aren't too many resources, or they involve using really specific software. Any recommendations or pointers that you would know of?

So I'm running

keytool -genkey -alias server -keyalg RSA -keysize 2048 -keystore keystore.jks

in the root folder of the module project, and get:

'keytool' is not recognized as an internal or external command,
operable program or batch file.

keytool is part of the JDK; go to your Java installation directory and run the command there.

Awesome, got the .p7b file as the certificate fill I'm assuming. I'd like to instead of running the invocation in the module-signer repo, just be able to sign the module using the gradle project properties when i do a "gradlew clean build".

By "< value >", are they referring to the path of the file?

Yes, the file arguments should be a (probably explicit) path to the file. Note that if you're sharing the project with other users via VCS, you probably should commit gradle.properties, but you probably should not commit your keystore file paths or anything like that. You may want to add a gradle.properties in your user home directory:
https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties

I'm not 100% sure what this error means. Nor am I sure that I have everything else configured correctly but...

image

Try adding the --stacktrace flag, as it suggests; that'll give you/me a better idea where the code is hitting the unexpected null. It's likely an issue with your file paths, or a parameter with the wrong name (maybe a capitalization issue, for instance).

Here's a bit of it, I certainly don't find it helpful, maybe you might:

Here are my gradle.properties and sign.props files:

image

Line 250 of the SignModule task is here:

KeyStore.getKey is documented to return null if the alias is invalid:

Returns:
the requested key, or null if the given alias does not exist or does not identify a key-related entry.

So it looks like your file path is correct, but your keystore either has a different alias than you think, or (more likely) there's an issue with the space in the name being escaped/unescaped correctly somewhere in the process.

The easiest thing might be to regenerate your keystore at this point, or create a new alias on your keystore with a more 'programmatic' friendly name, eg. kanoa-consulting or something like that. I don't know offhand how capitals/spaces/etc work.

Got it, here are the values I gave when I ran the keytool command. I saw in an example that they used a string with a space so I thought it would be okay. So does 'alias' correspond to 'first and last name' here?

I'm pretty sure your alias is just server, directly from the CLI. The first and last name stuff is all embedded in the cert.

True, that did it! I have myself a signed module now. Paul I just wanted to acknowledge and appreciate the immense amount of help and patience you've provided for me, really lucky to have you.

So the next thing I'm getting into is exploring having scripting functions in the module. Is it advisable to have all the necessary files within the same project I've been working on it, or would it be better to have a separate module?

That's pretty much up to you and your ultimate goal for the module.
For instance, if you're developing something that's going to be used "in-house", i.e. only at your company and only on particular gateways, I don't think there's anything wrong with a "kitchen sink" module. If you're planning to distribute this to third parties, or even publicly, then it depends on whether your scripting functions integrate closely with what you're currently doing with Perspective components or not.
If they don't really tie together, then two modules might be more appropriate, so that end users can pick-and-choose what functionality they want to integrate.