Default Settings for a custom ExtensionPoint

I'm building a custom historian module for 8.3. I want to have pre-filled-in values for some of the fields when creating it on the gateway config page.
I've implemented defaultSettings() but the settings do not show up on the config page for whatever reason (I want the poll rate to default to 60000).

This is what I have:

In REDACTEDExtentionPoint.java:

	@Override
    public Optional<REDACTEDSettings> defaultSettings() {
        REDACTEDSettings.Connectivity connectivity = new REDACTEDSettings.Connectivity(null);
        REDACTEDSettings.Advanced advanced = new REDACTEDSettings.Advanced(60000);
        REDACTEDSettings settings = new REDACTEDSettings(connectivity, advanced);
        return Optional.of(settings);
    }

REDACTEDSettings.java:

public record REDACTEDSettings(Connectivity connectivity, Advanced advanced) implements HistorianSettings {
    public record Connectivity(
            @FormCategory("Connectivity")
            @FormField(FormFieldType.REFERENCE)
            @Label("Database Connection")
            @Required
            @Description("The database connection to use.")
            @FormReferenceType("ignition/database-connection")
            String databaseConnection
    ) {}
    public record Advanced(
            @FormCategory("Advanced")
            @FormField(FormFieldType.NUMBER)
            @Label("Poll Rate")
            @Required
            @Description("The rate (in milliseconds) at which the historian should query the datasource to check for updates")
            int pollRate
    ) {}

I've also tried the @DefaultValue() annotation which didn't work either. Looking at the docs, it seems to be meant for optional parameters and applied after the user fails to specify a value, which is not what I'm looking for.

I've tried @ExampleValue("60000") as well, but that doesn't seem to fill it out either...

When stepping through with the debugger, it looks like the default settings get created correctly and are stored with the list of extension points, but the Create Historian popup doesn't use them...

Am I misunderstanding the purpose of the defaultSettings() method? If so, what should I use instead?

Your config page is just an ExtensionPointResourceForm?

Returning an instance with the desired values from defaultSettings() + the @DefaultValue("60000") annotation should be enough...

1 Like

Thanks for replying so quickly!

I added the default annotation back in:

public record Advanced(
            @FormCategory("Advanced")
            @FormField(FormFieldType.NUMBER)
            @Label("Poll Rate")
            @Required
            @DefaultValue("60000")
            @Description("The rate (in milliseconds) at which the historian should query the datasource to check for updates. Set the the value to anything less than or equal to 0 to only poll the values on the historian's startup.")
            int pollRate
    ) {}

And my config page should just be an ExtensionPointResourceForm:

    @Override
    public Optional<WebUiComponent> getWebUiComponent(ComponentType type) {
        return Optional.of(new ExtensionPointResourceForm(
                RESOURCE_TYPE,
                "Historian Settings",
                "REDACTED",
                SchemaUtil.fromType(HistorianProvider.class),
                SchemaUtil.fromType(REDACTEDSettings.class)
        ));
    }

Still no default value though:

I fell like I'm missing something simple and I just can't see it...

I'm not seeing anything that stands out yet. We're using the same pattern all over with extension points to provide default values. Are you sure you've properly cleaned, rebuilt, reinstalled your module? Can you change something else (name, description, whatever) to make sure?

3 Likes

I changed stuff on it to make sure:

Could it be because I don't have defaultProfile() implemented?

I don't think so, most/all of ours don't implement that.

Maybe something to do with using null instead of empty string here?

2 Likes

I set it to an empty string and still no luck.

I tried changing the datatype to String to see if it was just failing to cast, but that didn't help either. I also restructured my settings to see if it made a difference and it didn't...

I'm kind of out of ideas at the moment. I might come back to it in a few weeks with fresh eyes; perhaps see what I'm missing.

It's good to know that the pattern I'm using is the intended one though. Really appreciated you helping me troubleshoot.

I would expect to implement defaultSettings() or the annotations for defaults, not both.

1 Like

Still not sure why the defaults don't work, but perhaps this is related:

Implementing defaultSettings() breaks the editing functionality. I can create a historian, but can't edit:

Simply commenting out the defaultSettings() implementation fixes this edit dock...

Share your full settings class and full definition of whatever you returned in defaultSettings()?

Hi All,

I have exactly the same issue (Ignition version 8.3.3): The fields are always empty in the create form or it breaks if I try anything else. I share my code fragments below.

Here is my settings class:

public class TimescaleDbHistorianSettings implements HistorianSettings {

  private String jdbcUrl ="jdbc:postgresql://timescaledb:5432/historian?currentSchema=public";
  private String username = "postgres";
  private SecretConfig password;
  private int timeoutMs = 5000;
  private int batchSize = 100;
  private int batchIntervalMs = 5000;
  private boolean debugLogging = false;
  private String storeAndForwardEngine = "";

  public TimescaleDbHistorianSettings() {                              
  }
                                                                       
  // getters and setters ...                            

}

Three versions I tried:

  • The one, which doesn’t break the form returns Optional.empty(). Of course fields are always empty:
public class TimescaleDbHistorianExtensionPoint extends
HistorianExtensionPoint<TimescaleDbHistorianSettings> {  
  @Override                                                            
  public Optional<TimescaleDbHistorianSettings> defaultSettings() {
      return Optional.empty();
  }

  // ...
}
  • Returning Optional.of(new TimescaleDbHistorianSettings()) breaks with "Extension Point Form Not Found":
public class TimescaleDbHistorianExtensionPoint extendsHistorianExtensionPoint<TimescaleDbHistorianSettings> {
  @Override                                                            
  public Optional<TimescaleDbHistorianSettings> defaultSettings() {
      return Optional.of(new TimescaleDbHistorianSettings());
  }

  // ...
}
  • The third approach I tried was removing getWebUiComponent() entirely and
    putting the @DefaultValueDefaultValue annotations directly on the TimescaleDbHistorianSettings fields — like the core historian does. That gave me "Web UI Component type not found"

Any pointers on how to get defaults are much appreciated!

Best regards,
Gabor

Well... removing getWebUiComponent() obviously should break.

But you should have always been annotating the fields - were/are you annotating the getter methods instead?

I'll share the entirety of the extension point, it doesn't have much else in it.

From what I can tell, the ExtensionPoint interface's default implementation of settingsType() uses defaultSettings() to figure out the class. So I need to have at least one of those methods implemented for things to work. (That is why they're both there).

Historian Settings
package redacted.gateway;

import com.inductiveautomation.historian.gateway.api.config.HistorianSettings;
import com.inductiveautomation.ignition.gateway.dataroutes.openapi.annotations.*;
import com.inductiveautomation.ignition.gateway.web.nav.FormFieldType;

public record REDACTEDSettings(
        @FormCategory("Connectivity")
        @FormField(FormFieldType.REFERENCE)
        @Label("Database Connection")
        @Required
        @Description("The database connection to use")
        @FormReferenceType("ignition/database-connection")
        String databaseConnection,

        @FormCategory("Advanced")
        @Label("Poll Rate")
        @FormField(FormFieldType.NUMBER)
        @DefaultValue("60000")
        @Required
        @Description("The rate (in milliseconds) at which the historian should query the datasource to check for updates. Set the the value to anything less than or equal to 0 to only poll the values on the historian's startup.")
        int pollRate


) implements HistorianSettings {

    public REDACTEDSettings copyWithDatabaseConnection(String newDatabaseConnection) {
        return new REDACTEDSettings(newDatabaseConnection, this.pollRate);
    }

}

ExtensionPoint
package redacted.gateway;

import com.inductiveautomation.historian.gateway.api.Historian;
import com.inductiveautomation.historian.gateway.api.HistorianExtensionPoint;
import com.inductiveautomation.historian.gateway.api.HistorianProvider;
import com.inductiveautomation.historian.gateway.api.config.HistorianSettings;
import com.inductiveautomation.ignition.gateway.config.*;
import com.inductiveautomation.ignition.gateway.dataroutes.openapi.SchemaUtil;
import com.inductiveautomation.ignition.gateway.datasource.DatasourceManager;
import com.inductiveautomation.ignition.gateway.model.GatewayContext;
import com.inductiveautomation.ignition.gateway.web.nav.WebUiComponent;
import com.inductiveautomation.ignition.gateway.web.nav.ExtensionPointResourceForm;
import java.util.Optional;

public class REDACTEDExtensionPoint extends HistorianExtensionPoint<REDACTEDSettings> {

    public static final String TYPE_ID = "redacted.REDACTED";

    public REDACTEDExtensionPoint() {
        super(TYPE_ID, "REDACTED.Meta.Name", "REDACTED.Meta.Desc");
    }

    @Override
    public Historian createHistorianProvider(GatewayContext gatewayContext, DecodedResource<ExtensionPointConfig<HistorianProvider, HistorianSettings>> decodedResource) throws Exception {
        REDACTEDSettings settings = this.getSettings(decodedResource.config()).get();
        addReferenceProperty("databaseConnection",
                ref -> ref
                        .targetType(DatasourceManager.DATABASE_CONNECTION_RESOURCE)
                        .value(REDACTEDSettings::databaseConnection)
                        .onUpdate(REDACTEDSettings::copyWithDatabaseConnection)
        );
        return new REDACTED(gatewayContext, decodedResource.name(), settings);
    }

    @Override
    public Optional<Class<REDACTEDSettings>> settingsType() {
        return Optional.of(REDACTEDSettings.class);
    }

    @Override
    public Optional<REDACTEDSettings> defaultSettings() {
        return Optional.of(new REDACTEDSettings("", 60000));
    }

	@Override
    public boolean canCreate() {
        return true;
    }
    
    @Override
    public Optional<WebUiComponent> getWebUiComponent(ComponentType type) {
        return Optional.of(new ExtensionPointResourceForm(
                RESOURCE_TYPE,
                "Historian",
                TYPE_ID,
                SchemaUtil.fromType(HistorianProvider.class),
                SchemaUtil.fromType(REDACTEDSettings.class)
        ));
    }

}
getExtensionPoints in module hook
    @Override
    public List<? extends ExtensionPoint<?>> getExtensionPoints() {
        return List.of(new REDACTEDExtensionPoint());
    }

The above code works for creating a historian, but breaks with Extension Point Form Not Found when trying to edit it.

Looking at the SDK examples, some extension points implement the defaults in a constructor of the record class, in addition to implementing defaultSettings() and the @Default annotations. This is the case with: User Source Profile, and Secret Provider. I tried this briefly as well, but no luck.

Stepping through with a debugger, reveals some interesting things. On the historians page, in the list of extension points, addComponent and editComponent are nulls for all the IA historians, but instances of ExtensionPointResourceForm for my custom one:

Screenshot

However, doing the same for the Secret Provider example reveals that all the extension points have ExtensionPointResourceForm instances (not just the example module):

Screenshot

I'm not sure if that explains anything, but because of these inconsistencies compiling the example modules and comparing them to my own has not been as helpful as I hoped.

In the screenshot, you can see that the default settings have been created with the correct values and (as far as I can tell) in the correct format:

Screenshot

I had used a separate public record TimescaleDbHistorianConfig for annotations (similarly as in the example of djs above).

I have just made an attempt to move the annotations to the fields on TimescaleDbHistorianSettings
(not on getters). Fragment:

public class TimescaleDbHistorianSettings implements HistorianSettings {
...
@FormCategory("Connection")
@Label("Username*")
@FormField(FormFieldType.TEXT)
@DefaultValue("postgres")
@Required
@Description("Database username")
private String username = "postgres";
...

and then

@Override
public Optional<TimescaleDbHistorianSettings> defaultSettings() {
   return Optional.of(new TimescaleDbHistorianSettings());
} 

The labels, descriptions, categories, and field types
all render correctly. Only the @DefaultValue doesn't populate the
input box.

Just a quick note: this isn't working because editing settings breaks when there isn't a separate record.

Any pointers on how to fix this would be much appreciated.

I pinged somebody on the history team to look into this because I think there may be an issue specific to this system.

1 Like

Unfortunately, this will require fixes on our end. I’ll get it prioritized and update this thread once it’s merged into a nightly.

5 Likes