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...
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:
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.