Ignition 8.3: Guidance/example on end-to-end WebUI / Gateway interaction

I’ve been able to update my company’s module to function correctly under Ignition 8.3 except for the configuration UI. In other words, I’m successfully migrating the configuration from the old SQLite record stuff to the new JSON-based resource stuff. And I can edit those JSON files as a “brute force” method of configuring my module. But this obviously isn’t good enough for our customers.

I have the ignition-sdk-examples/webui-webpage at ignition-8.3 · inductiveautomation/ignition-sdk-examples · GitHub example integrated into my module.

The missing piece is that I don’t see any example (or even mention) of how to facilitate communication between the web ui and the gateway. In my case, the need is very simple: Read and write a relatively small JSON blob of configuration from the web UI.

Do I need to expose a new API from the Gateway somehow?

Yes. That example includes custom APIs for the example.

For parts that write back, see the discussion of the CSRF token in this topic:

I must be blind… I don’t see any Java code in the webui example that looks like an API, nor do I see any Typescript code that does a fetch.

The only thing I see that is somewhat related to a backend API is this placeholder:

Hmmm. You're right. The gateway hook's .mountRouteHandlers() method is empty.

That's where you need to register your APIs and connect their implementations.

This was where I got the inspiration for my drivers:

(I didn't register them in the OpenAPI docs.)

This is a working config settings example that I wrote, it’s not perfect but it’s fully functional with the API written inside.

1 Like

OK thanks for the input, I was able to get everything going.

@pturmel’s pointer to 8.3 File Upload route permissions issue - #8 by pturmel was really key for posting – i.e., whenever you use PermissionType.WRITE. I was able to access the ReduxRootState exactly as he did. Otherwise you will get a non-descript 403 Forbidden error.

For those following along and looking for more examples of what works, here’s what my mountRouteHandlers() looked like in my GatewayHook class:

    public void mountRouteHandlers(RouteGroup routes) {
        routes.newRoute("/settings")
                .type(RouteGroup.TYPE_JSON)
                .handler(this::getSettingsJson)
                .method(HttpMethod.GET)
                .requirePermission(PermissionType.READ)
                .mount();

        routes.newRoute("/settings")
                .type(RouteGroup.TYPE_JSON)
                .handler(this::putSettingsJson)
                .method(HttpMethod.PUT)
                .requirePermission(PermissionType.WRITE)
                .mount();
    }

and here’s what my Settings.service.ts looks like on the frontend:

export const getSettings = async (): Promise<SeeqSettings> => {
  const response = await fetch("/data/seeq/settings", {
    method: "GET",
    headers: {
      Accept: "application/json",
    },
  });

  if (!response.ok) {
    throw new Error(`Failed to fetch settings: ${response.statusText}`);
  }

  const payload: SeeqSettingsPayload = await response.json();

  // Flatten the structure for UI convenience
  return {
    enabled: payload.enabled,
    ...payload.config,
  };
};

export const updateSettings = async (
  settings: Partial<SeeqSettings>,
  csrfToken: string
): Promise<UpdateSettingsResponse> => {
  // Extract enabled and wrap the rest in config
  const { enabled, ...config } = settings;

  const payload: Partial<SeeqSettingsPayload> = {
    enabled,
    config: config as IgnitionModuleConfigurationV2,
  };

  const response = await fetch("/data/seeq/settings", {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
      "X-CSRF-Token": csrfToken,
    },
    body: JSON.stringify(payload),
  });

  const data: UpdateSettingsResponse = await response.json();

  // Check if the operation was successful
  if (!data.success) {
    throw new Error(data.message || "Failed to update settings");
  }

  return data;
};

where csrfToken was retrieved via this code:

  const csrfToken = useSelector(
    (state: ReduxRootState) => state.userSession.csrfToken || ""
  );
2 Likes

Thats actually one thing I had sidestepped in a bit of a hack-fix way, I just did it with an OPEN_ROUTE access control strategy. Not exactly secure, but functional. Thanks to your prompting here, I have updated my code to use the token and the READ/WRITE permissions instead, which is working nicely.