Hot Reloading / Faster Iteration for Module React UI Development

Hello,

I’m currently developing a React UI for my module and was wondering if there’s a way to enable hot reloading (or a similar workflow for faster iteration).

I noticed that the package.json in the example web-ui project includes a dev server script alias:

"run:dev": "webpack serve --env isStandalone"

However, running this in my own repository doesn’t seem to do anything.

At the moment, I have to recompile the module, install it, and restart Ignition every time I make a change to the UI. I can’t imagine this is how UI development is done internally, so I’m hoping there’s a more efficient approach I can take.

Any tips or guidance on how to improve this workflow would be greatly appreciated!

1 Like

If you run your gateway with a system property override (e.g. via wrapper.java.additional in the ignition.conf file) following a format of res.path.moduleID, pointing to a semicolon delimited list of absolute file paths, the module's contained resources will be skipped and we'll go directly to those paths to try to load contained resources.

That's a long sentence, but tl;dr:
-Dres.path.com.inductiveautomation.reporting=/Users/pgriffith/Projects/ignition-81/Modules/reporting/web/reporting-components/build/dist;/Users/pgriffith/Projects/ignition-81/Modules/reporting/web/status/build/dist

The folder(s) you point to must match the final structure that would be delivered in your module's return from getMountedResourceFolder call.

Also, all this is different in 8.3. I can explain that in another post.

3 Likes

Thanks for the response. I realize I should have clarified that my module is targeting 8.3. Would you be able to share how this behavior differs in 8.3?

Ah, actually, looks like the 8.3 solution is backwards compatible with the override format from 8.1 - so the same working code should apply. The 'new' thing that I was aware of but hadn't studied in depth is two new parameters:

-Dignition.gateway.resourceOverrideFile=/Users/pgriffith/Projects/ignition/config/resources/resource-config.json
-Dignition.gateway.systemJsOverrideFile=/Users/pgriffith/Projects/ignition/config/resources/systemjs-config.json

But they're an intermediary looked at after checking the res.path.moduleId, per internal documentation (reproduced below) so I would just continue using the 8.1 style.

/config/resources files

This directory contains files that can be specified to override various default behaviors about how resources are loaded
at runtime. These files are not part of the build - they are used in the development environment only, and not
distributed
.

resource-config.json

Specify this file using -Dignition.gateway.resourceOverrideFile=<filepath>
This file is used by the ResourceServlet to override resource loading logic. The ResourceServlet loads files using the
following three methods (in reverse order of precedence):

  1. Normally, resources are loaded by the
    classloader, because they are embedded in production jar files. So, if a request comes in for /res/module-id/filename
    the servlet will ask module-id's classloader to look for mounted/filename.
    (See GatewayModuleHook.getMountedResourceFolder()).
  2. ... unless there is a system property -Dignition.gateway.resourceOverrideFile= specified, and that file contains an entry for
    module-id in it, which points to a spot on the local disk. Then the ResourceServlet will look for the file in
    that local folder instead (new since 8.3.0)
  3. ... unless there is a System property like -Dres.path.module-id=<path/to/local/folder>. In this case,
    ResourceServlet will load the resource /res/module-id/filename from new File("path/to/local/folder/filename")

So, in summary, the location of a resource will first honor the -Dres.path.module-id system property, if specified.
If not, it will look in the file at -Dignition.gateway.resourceOverrideFile, if specified. If not, it will have the resource load
from the classloader.

systemjs-config.json

Specify this file using -Dignition.gateway.systemJsOverrideFile=<filepath>
This file will be used by the WebUIServlet when generating the
import map that is embedded in the main page that
mounts the 8.3+ react-based web ui for the gateway.

Normally, the URLs for the systemjs modules (fancy term for a named javascript file) are registered by modules and the
platform using the SystemJsModuleRegistry class which can be found on the gateway context's WebResourceManager.

Using this file and the "imports" key, one can override the URLs for each named module. Reasons one might want to do
this:

  • The module may be being watched, built, and hosted at localhost:some-port by the webpack
    dev-server. By specifying this alternate location, the developer has faster hot-reloading.
  • It may be that a minified, production version of the js module is embedded into the platform or module, but during
    development, a non-minified or "development" build is more convenient. The URL to the dev version of the module (which)
    may be hosted on a CDN) can be specified instead.

Using the "scripts" key, one can override the default scripts that are placed directly on the page.

3 Likes

Sorry I am not even sure I am asking the right question…

So in the ignition.conf file I would add something like

wrapper.java.additional.7=-Dres.path.com.inductiveautomation.reporting=PATH TO PROJECT DIST

I am a little confused why would have two dist files for one area. Is it injecting both at the same time?

Yes, but com.inductiveautomation.reporting would be your own module ID, which by convention should be a similar/the same reverse DNS name as your Java package(s) used in your module code.

There's two distributions in the snippet I copied above because one is for the Perspective components added by the reporting module, and one is for the custom React page used as the Reporting module's gateway status page. They are both React, but in different contexts and don't share any common code. If you're not doing anything like that, you probably don't have any need for a second path.

1 Like

@paul-griffith Hi, could you expand some on the systemjs-config.json file? I’m trying to get hot reloading of the same webui sample project so I can start developing a gateway UI for a module for 8.3.

Running it locally doesn’t really work, I just get an error in the log from IgnitionWebUI, probably since it cant get the data from the gateway for the page. This is how im running it, maybe this is not correct:
npm run run:dev (which then calls)
webpack serve --env isStandalone

Would I use the systemjs-config to point to the localhost:9999 url? I tried and it didnt work. Could you send an example of the systemjs-config?

Update, i was able to get the hot reloading working, at least for it to change the page in the gateway but now I just get

ERROR: Failed to Load

module: the.module.path

Component: HelloIgnition

This is still due to the error with the reading 30
The neutral palette in the debugger is undefined seems to be the cause. I havent changed anything about the webui project yet, so I’m not sure why this error shows up.

I finally got at least the localhost dev server working, not embedded yet in the running gateway with hot reload. But this is much better

const ThemeProvider = IAU.CustomThemeProvider;

....

root.render(
  <ThemeProvider>
    <HelloIgnition />
  </ThemeProvider>
);


You need to change e the root.component.js and add this.

EDIT: had to also change the webpack setting to not use isStandalone, this makes it so you dont get the page in a browser at localhost, but works if you load the json to the gateway. trying to do both probably isnt worth it, but this does what i need

I wouldn't go down the "have webpack serve your files and try to intermingle that into the gateway context" route, I would go down "have webpack watch and continuously build your output into a known directory that the gateway can pick up" route. The latter is what we use internally, the former requires you to blaze your own trail.

Hm, im confused then. I couldnt seem to get this working with local directories. Do you mean I should instead put the path of my generated-resources/mounted folder here?

{
  "imports": {
    "com.modulename.module.WebuiWebpage": "http://localhost:9998/helloIgnition.js"
  }

}

That's what we do in the internal resource-config file:

{
    "platform": {
        "locations": [
            "../../Platform/gateway-web/authentication/build/dist",
            "../../Platform/gateway-web/commissioner/build/dist",
            "../../Platform/gateway-web/ncldownload/build/dist",
            "../../Platform/gateway-web/sass/build/dist",
            "../../Platform/gateway-api-web/ignition-gateway-lib/build/dist",
            "../../Platform/gateway-api-web/ignition-web-ui/build/dist",
            "../../Platform/gateway-api-web/ignition-icons/build/dist",
            "../../Platform/gateway-web/build/generated-resources/mounted",
            "../../Platform/gateway-web/build/collected-resources/mounted",
            "../../Platform/gateway/src/main/resources/mounted",
            "../../Platform/gateway-api/build/generated-resources/mounted"
        ]
    },
    "modules": {
        "com.inductiveautomation.perspective": {
            "locations": [
                "../../Modules/perspective/app/common/build/dist",
                "../../Modules/perspective/app/client/build/dist",
                "../../Modules/perspective/app/amcharts/build/dist",
                "../../Modules/perspective/app/barcode/build/dist",
                "../../Modules/perspective/app/components/build/dist",
                "../../Modules/perspective/app/designer/build/dist",
                "../../Modules/perspective/app/designer-components/build/dist",
                "../../Modules/perspective/app/drawing-common/build/dist",
                "../../Modules/perspective/app/map/build/dist",
                "../../Modules/perspective/app/google-map/build/dist",
                "../../Modules/perspective/app/pdf-viewer/build/dist",
                "../../Modules/perspective/app/react-timeseries/build/dist",
                "../../Modules/perspective/app/unsupported/build/dist",
                "../../Modules/perspective/app/build/generated-resources/mounted",
                "../../Modules/perspective/perspective-gateway-web/build/generated-resources/mounted",
                "../../Modules/perspective/perspective-gateway/src/main/resources/mounted"
            ]
        },
        "com.inductiveautomation.connectors.kafka": {
            "locations": [
                "../../Modules/connector-kafka/web-ui/build/generated-resources/mounted"
            ]
        },
        "com.inductiveautomation.opcua.drivers.bacnet": {
            "locations": [
                "../../Modules/driver-bacnet/web-ui/build/generated-resources/mounted"
            ]
        },
        "com.inductiveautomation.opcua.drivers.dnp3v2": {
            "locations": [
                "../../Modules/driver-dnp3-v2/web-ui/build/generated-resources/mounted"
            ]
        },
        "com.inductiveautomation.opcua.drivers.micro800": {
            "locations": [
                "../../Modules/driver-micro800/web-ui/build/generated-resources/mounted"
            ]
        },
        "com.inductiveautomation.opcua.drivers.omron": {
            "locations": [
                "../../Modules/driver-omron/web/build/generated-resources/mounted"
            ]
        },
        "com.inductiveautomation.reporting": {
            "locations": [
                "../../Modules/reporting/web-ui/build/generated-resources/mounted",
                "../../Modules/reporting/reporting-gateway/src/main/resources/mounted",
                "../../Modules/reporting/web/reporting-components/build/dist",
                "../../Modules/reporting/web/status/build/dist"
            ]
        },
        "com.inductiveautomation.historian.sql": {
            "locations": [
                "../../Modules/historian-sql/web-ui/build/generated-resources/mounted"
            ]
        }
    }
}
1 Like

Ah right, I mean for the systemjs-config file

Hi @Robert_McIlvenna ,

Can you provide some details on your current project? Are you working on a singleton resource or a list resource?

Looking through the thread, there are a few things I would like to point out. Our systemjs-config file is used for overriding the source location of where we import our web packages from. The example you have looks fine, assuming those are the correct identifiers/names. What you can do is use webpack to create a temporary dev server to serve up those your bundled package. This can be achieved by running webpack serve --port <port-number-here>. The undefined issue you are running into is an issue with modules not being able to access our internal theme’s augmented types. In order to fix that issue include this import at the top of your root component file.
import type {} from '@inductiveautomation/ignition-web-ui/typings/Theme';

1 Like

Hey Ayu, I’m trying to create a Gateway webpage for configuring a module. It’s development tools for Ignition we use internally, hoping to release publicly at some point.

My root.component.js file? I was able to fix the theme issue with the theme provider as above, I cant import types as its a JS file

Hi @Robert_McIlvenna ,

Hmm. I was under the impression that our SDK example was using Typescript, but it doesn’t look like that’s the case. The undefined issue might be something else. Let me reach out to the team. Thanks.

1 Like

For those having problems with this can you try the following and see if it fixes the issue?

  1. Make a copy of your current setup as a backup in case you want to revert
  2. Revert all the changes you’ve made to the original state of the sdk example
  3. Modify the following file:

with the following code:

import React from "react";
import { Provider } from "react-redux";
import store from "../../store";
import HelloIgnition from "../HelloIgnition";
import { CustomThemeProvider } from "@inductiveautomation/ignition-web-ui";

const RootPage = () => {
  return (
    <Provider store={store}>
      <CustomThemeProvider>
        <HelloIgnition />
      </CustomThemeProvider>
    </Provider>
  );
};

export default RootPage;
  1. in the web-ui sub directory: issue gradle build
  2. then yarn run run:dev or npm run run:dev
1 Like