Making it easier to diff Perspective view.json files

If you use git a lot and are frequently annoyed at trying to read diffs of scripts in Perspective view.json files, I’ve got something you might like.

Git has a feature called textconv which can be used to convert files to a text-friendly format before diffing them. It’s not expected to create a perfect or reversible text version, just something convenient for spotting changes. The perennial example is a PDF-to-txt converter, which would let you at least see text changes in PDF documents when you compare them.

Nothing says you can’t use this concept on files that are already text, though.

I put together this view_textconv script to “flatten” the JSON-encoded scripts into normal function text. It loads the whole JSON, walks the tree looking for script blocks, and then converts any it finds into a normal def somename(): code block at the top of the output. The “somename” is actually a hint to where it was found in the view, on which sub-component, and whether it was a binding or a change event or a custom method. After all the python blocks have been extracted this way, the remaining JSON is output, and where the code blocks used to be now has just a “somename” reference.

I’m actually finding this textconv tool very handy when exploring complicated views that I didn’t design or have forgotten how I set up. It gives me a flat file I can search to cross-reference where properties are used, and also quickly lets me see all the places that Python scripting is used in a view without having to click through all the component property views.

For example, the output of the Header/Header view of the “Menu Nav 8.1” template that comes with Ignition looks like this:

def view.root['props.style.backgroundColor'].BINDING[0]():
        if "dark" in value:
                return "var(--neutral-10)"
        else:
                return ""
# END view.root['props.style.backgroundColor'].BINDING[0]

def view.root.User.EVENT.dom.onClick[0]():
        if self.session.props.auth.authenticated:
                system.perspective.logout()
        else:
                system.perspective.login()
# END view.root.User.EVENT.dom.onClick[0]

def view.root.Sign In.EVENT.dom.onClick[0]():
        if self.session.props.auth.authenticated:
                system.perspective.logout()
        else:
                system.perspective.login()
# END view.root.Sign In.EVENT.dom.onClick[0]

def view.root.Alarms['custom.numAlarms'].BINDING[0]():
        return len(system.alarm.queryStatus(state=["ActiveUnacked"]))
# END view.root.Alarms['custom.numAlarms'].BINDING[0]

view_json = """
{
    "custom": {},
    "params": {
        "params": {},
        "size": "medium"
    },
    "propConfig": {
        "params.params": {
            "paramDirection": "input",
            "persistent": true
        },

(…cut off the rest of the output for simplicity…)

I can quickly see there are 4 locations in the view that use scripts: a binding of the props.style.backgroundColor property of the view.root component, the onClick event of view.root.User, the onClick event of view.root.Sign In, and a binding on the custom.numAlarms property of view.root.Alarms.

But the best part is that once I setup my git to use this when diffing view.json files, I can do git diff or git log -p and any script changes in views look like normal code diffs.

There may be other places that scripts could exist in the view JSON, but this tool handles all the ones I found across all of my company’s projects.

I didn’t bother extracting Ignition expressions. That could be added without much difficulty, but our expressions tend to be very short and simple and I can read them directly in the JSON-encoded diff without as much difficulty.

Instructions on how to setup git to use this are given in the top comment block of the script.

If any of you find this useful or have any improvements or changes you’d like, let me know.

6 Likes

I tweaked the script in Feb 2023 to output the remainder in YAML by default since I'm finding that even easier to read and diff, but there is a global variable that makes it easy to switch back and forth if you prefer the other way.