UI and settings

View as Markdown

Settings

A connector typically needs to maintain settings to operate. For example, the Google Analytics connector needs to store values for "Measurement ID" and "API Secret," so a specific data type is defined:

type innerSettings struct {
	MeasurementID      string `json:"measurementID"`
	APISecret          string `json:"apiSecret"`
	CollectionEndpoint string `json:"collectionEndpoint"`
}

Defining a specific type for settings is convenient because settings are exchanged in JSON format between the connector and Krenalis. Therefore, having a dedicated data structure allows for easier serialization and deserialization into JSON. For instance, the settings for an instance of the Google Analytics connector could be serialized like this:

{"measurementID":"G-2XYZBEB6AB","apiSecret":"ZuHCHFZbRBi8V7u8crWFUz","collectionEndpoint":"Global"}

Krenalis does not impose any constraints on how settings are serialized, except that they must be in JSON format.

When Krenalis creates an instance of a connector, it passes the current settings through the Settings field and a function through the SetSettings field, which the connector can use to update the settings. For example, the constructor of the Google Analytics connector stores the configuration passed as an argument and the SetSettings function, and deserializes the settings:

func New(env *connectors.APIEnv) (*Analytics, error) {
    c := Analytics{env: env}
    if len(env.Settings) > 0 {
        err := json.Unmarshal(env.Settings, &c.settings)
        if err != nil {
            return nil, errors.New("cannot unmarshal settings of Google Analytics connector")
        }
    }
    return &c, nil
}

Later, when it needs to update the settings, it serializes them and calls the SetSettings function:

...
settings, err := json.Marshal(s.settings)
if err != nil {
    return err
}
err = ga.env.SetSettings(ctx, settings)
if err != nil {
    return err
}
...

User interface

If the connector requires all or part of the settings to be configurable by the user, it can define a user interface (UI) that will be integrated into the Krenalis Admin console. Various components are available to create the interface for a connector: Input, Select, Checkbox, ColorPicker, Radios, Range, Switch, KeyValue, FieldSet, AlternativeFieldSets, Text, Button, and Alert.

For example, for the previous two settings of Google Analytics, the interface could be defined as follows:

&connectors.UI{
    Fields: []connectors.Component{
        &connectors.Input{
            Name:        "MeasurementID",
            Label:       "Measurement ID",
            Placeholder: "G-2XYZBEB6AB",
            Type:        "text",
            MinLength:   2,
            MaxLength:   20,
            HelpText:    "Follow these instructions to get your Measurement ID: https://support.google.com/analytics/answer/9539598#find-G-ID",
        },
        &connectors.Input{
            Name:        "APISecret",
            Label:       "API Secret",
            Placeholder: "ZuHCHFZbRBi8V7u8crWFUz",
            Type:        "text",
            MinLength:   1,
            MaxLength:   40,
        },
    },
    Settings: settings,
    Buttons:  []connectors.Button{connectors.SaveButton},
}

Two Input components are present, "MeasurementID" and "APISecret," and a Button component with an event named "save." A Button component with the "save" event must always be present, as it ensures Krenalis saves the values from the interface fields in the connector's settings.

Buttons

Every UI response must include at least one button in Buttons []connectors.Button. The typical case is to use connectors.SaveButton for the standard save action — its event is "save", which is the special event that triggers settings persistence:

return &connectors.UI{
    Fields:   fields,
    Settings: settings,
    Buttons:  []connectors.Button{connectors.SaveButton},
}, nil

The Settings field, of type json.Value, contains the settings of the interface components in JSON format. For example, Settings could have the following value:

json.Value(`{"MeasurementID":"G-2XYZBEB6AB","APISecret":"ZuHCHFZbRBi8V7u8crWFUz"}`)

Serving the UI

To provide the interface to the user and respond to events triggered by user interaction, the connector must implement the ServeUI method and declare in its configuration that it has settings (see the documentation specific for the connector type):

ServeUI(ctx context.Context, event string, settings json.Value, role connectors.Role) (*connectors.UI, error)
  • ctx: Context, it's never nil.
  • event: Event to serve.
  • settings: Settings of the interface components, serialized in JSON. If not nil, it's always valid JSON.
  • role: Connection's role, it can be Source or Destination.

The ServeUI method must serve the "load" and "save" events:

  • "load": Initially, when the connector's interface is loaded, ServeUI is called with the "load" event and settings set to nil. If no errors occur, the method must return a non-nil interface.
  • "save": When the user clicks the "Add" or "Save" button, ServeUI is called with the "save" event and settings representing the user-entered settings serialized in JSON. The method should validate and save the settings and return a nil interface.

Whenever ServeUI returns a non-nil *connectors.UI, that interface must always include at least one button in Buttons []connectors.Button. In the typical case this is Buttons: []connectors.Button{connectors.SaveButton}, since connectors.SaveButton emits the "save" event that triggers settings persistence.

ServeUI must also serve any other event associated with the buttons present in the interface. When called for one of these events, settings is the JSON-serialized version of the user-entered settings. If the method returns a non-nil interface, the UI is updated with the returned interface.

If the event is not among those expected, the method should return the ErrUIEventNotExist error. If the settings passed as arguments are not valid, it should return an error of type InvalidSettingsError. You can use the connectors.NewInvalidSettingsError function for this purpose.

Example

The following is the ServeUI method of the Google Analytics connector:

func (ga *Analytics) ServeUI(ctx context.Context, event string, settings json.Value, role connectors.Role) (*connectors.UI, error) {

    switch event {
    case "load":
        // Load the user interface.
        var s settings
        if ga.settings != nil {
            s = *ga.settings
        }
        settings, _ = json.Marshal(s)
    case "save":
        // Validate and save the settings.
        s, err := validateSettings(settings)
        if err != nil {
            return nil, err
        }
        return nil, ga.env.SetSettings(ctx, s)
    default:
        return nil, connectors.ErrEventNotExist
    }

    ui := &connectors.UI{
        Fields: []connectors.Component{
            &connectors.Input{Name: "MeasurementID", Label: "Measurement ID", Placeholder: "G-2XYZBEB6AB", Type: "text", MinLength: 2, MaxLength: 20, HelpText: "Follow these instructions to get your Measurement ID: https://support.google.com/analytics/answer/9539598#find-G-ID"},
            &connectors.Input{Name: "APISecret", Label: "API Secret", Placeholder: "ZuHCHFZbRBi8V7u8crWFUz", Type: "text", MinLength: 1, MaxLength: 40},
        },
        Settings: settings,
        Buttons:  []connectors.Button{connectors.SaveButton},
    }

    return ui, nil
}

Pay attention to the following:

  • If event is "load", it returns the user interface.
  • If event is "save", it validates and saves the settings of the interface fields in the settings using the SetSettings function, and returns a nil interface.
  • Before saving, the settings are validated through its own method, which returns an InvalidSettingsError error if the settings do not pass validation.
  • If event is unknown, it returns the ErrUIEventNotExist error.
  • The serialized JSON settings are directly assigned to the Settings field of the returned interface. This is because in this case, each interface field is associated with a setting with the same name as the field.