Musson Industrial's Embr-Periscope Module

Sure:

Use Case 1, Component Modification

  1. Adding extra buttons to the bottom of a tag browse tree.
  2. Adding extra buttons to the header of an alarm table.
  3. Assigning a pattern to an input text field (this example assigns a pattern that requires a PIN of exactly 4 numbers.
View JSON
{
  "custom": {},
  "params": {},
  "props": {},
  "root": {
    "children": [
      {
        "meta": {
          "domId": "alarms",
          "name": "AlarmStatusTable"
        },
        "position": {
          "height": 280,
          "width": 613,
          "x": 5,
          "y": 422
        },
        "type": "ia.display.alarmstatustable"
      },
      {
        "children": [
          {
            "children": [
              {
                "meta": {
                  "name": "Button"
                },
                "position": {
                  "shrink": 0
                },
                "props": {
                  "text": "Extra Action"
                },
                "type": "ia.input.button"
              }
            ],
            "meta": {
              "name": "FlexContainer"
            },
            "position": {
              "basis": "auto",
              "grow": 0,
              "shrink": 0
            },
            "props": {
              "style": {
                "margin": "0.5rem",
                "overflow": "visible"
              }
            },
            "type": "ia.container.flex"
          }
        ],
        "meta": {
          "name": "AlarmStatusTableExtras"
        },
        "position": {
          "height": 300,
          "width": 300,
          "x": 372,
          "y": 546
        },
        "props": {
          "element": "() \u003d\u003e document.getElementById(\u0027alarms\u0027).getElementsByClassName(\u0027toolbarTabContainer\u0027)[0]"
        },
        "type": "embr.periscope.embedding.portal"
      },
      {
        "meta": {
          "domId": "tagTree",
          "name": "TagBrowseTree"
        },
        "position": {
          "height": 387,
          "width": 257,
          "x": 16,
          "y": 21
        },
        "type": "ia.display.tag-browse-tree"
      },
      {
        "children": [
          {
            "children": [
              {
                "meta": {
                  "name": "Button"
                },
                "position": {
                  "grow": 1,
                  "shrink": 0
                },
                "props": {
                  "text": "Extra Action"
                },
                "type": "ia.input.button"
              },
              {
                "meta": {
                  "name": "Button_0"
                },
                "position": {
                  "grow": 1,
                  "shrink": 0
                },
                "props": {
                  "text": "Extra Action 2"
                },
                "type": "ia.input.button"
              }
            ],
            "meta": {
              "name": "FlexContainer"
            },
            "position": {
              "basis": "auto",
              "grow": 0,
              "shrink": 0
            },
            "props": {
              "alignContent": "center",
              "direction": "column",
              "style": {
                "gap": "0.5rem",
                "margin": "0.5rem",
                "overflow": "visible"
              }
            },
            "type": "ia.container.flex"
          }
        ],
        "meta": {
          "name": "TagBrowseTreeExtras"
        },
        "position": {
          "height": 300,
          "width": 300,
          "x": 372,
          "y": 546
        },
        "props": {
          "element": "() \u003d\u003e document.getElementById(\u0027tagTree\u0027)"
        },
        "type": "embr.periscope.embedding.portal"
      },
      {
        "meta": {
          "domId": "textField",
          "name": "TextField"
        },
        "position": {
          "height": 32,
          "width": 150,
          "x": 325,
          "y": 91
        },
        "props": {
          "text": "123"
        },
        "type": "ia.input.text-field"
      },
      {
        "meta": {
          "name": "TextFieldExtras"
        },
        "position": {
          "height": 300,
          "width": 300,
          "x": 372,
          "y": 546
        },
        "props": {
          "element": "() \u003d\u003e document.getElementById(\u0027textField\u0027)",
          "events": {
            "target": {
              "lifecycle": {
                "onMount": "(target) \u003d\u003e {\ntarget.pattern \u003d \u0027\\\\d{4,4}\u0027\n}"
              }
            }
          }
        },
        "type": "embr.periscope.embedding.portal"
      }
    ],
    "meta": {
      "name": "root"
    },
    "type": "ia.container.coord"
  }
}

Use Case 2, Inverted Rendering

This example uses a Portal to break out of the normal rendering structure in order to simplify view development. Here's the problem statement:

  1. There is a main view, consisting of a header and a child view.
  2. There are several different child views available for selection.
  3. Depending on which child view is shown, different options need to be shown in the header.

In order to accomplish this without Portals, it is the responsibility of the main view to hide/show the different options (probably using some embedded views). 100% possible but kind of annoying to manage.

With Portals, each child view can directly add its options to the header. No conditional logic is needed on the main view, and no extra embedded views are needed for the option buttons. All of the configuration for the child view is contained inside the child view.

periscope_portal_header_example.zip (15.7 KB)

8 Likes

The Toasts Update

Periscope version 0.7.0 includes toasts powered by react-toastify.

The toasts also work in the designer :wink:.

Creating a Toast

These are client-side toasts, therefore all the interaction is done via system.perspective.runJavaScriptAsync.

periscope.toast is directly mapped to react-toastify's toast object, all features are supported.

Simplest Example

system.perspective.runJavaScriptAsync('''() => {
	periscope.toast('Your first toast!')
}''')

Embedded View Toast

This is somewhat advanced, but if you're reading this I trust you can figure it out.

A new helper function perspective.createView is available to remove the React/component store boilerplate; however, you still need to provide a unique and stable mount path. The easiest method for creating the mount path is by using the toastId available in the toastProps (basically just copy this example).

system.perspective.runJavaScriptAsync('''() => {		
	periscope.toast(({ toastProps }) => {
		return perspective.createView({
			resourcePath: 'Path/To/Your/View',
			mountPath: `toast-${toastProps.toastId}`,
			params: {}
		})
	})
}''')

More Information

For full details, check out the attached example views as well as the react-toastify documentation.

Example Views

periscope_toasts.zip (62.4 KB)

Side Note

If you update to Embr-Charts 2.2.4, you can also create toasts from within chart callbacks :wink: (the beginning of a shared JavaScript function registry system is in progress....)

15 Likes

Hey Ben, this looks sweet! Can you give us dummies over here (speaking only for myself of course) how you would structure the params dict? Is it literally just how you would normally in python?
Can't wait to try these out on my current project :slight_smile:

1 Like

Yup, that's exactly it. The keys of the params object are passed as the view's parameters.

As a refresher, you can pass arguments to runJavaScriptAsync; it takes a dictionary of arguments where the keys are mapped to the names of the arguments of the arrow function.

It's a mouthful, so here's an example:

args = {
	'param': 'value1',
	'anotherParam': 'value2'
}

system.perspective.runJavaScriptAsync('''(param, anotherParam) => {
	console.log('param:', param)
	console.log('anotherParam:', anotherParam)
}''', args)

Using this you could build a reusable view-as-toast function by doing something like:

# Definition
def showViewToast(viewPath, viewParams, toastOptions):
	
	args = {
		'viewPath': viewPath,
		'viewParams': viewParams,
		'toastOptions': toastOptions
	}
	
	system.perspective.runJavaScriptAsync('''(viewPath, viewParams, toastOptions) => {		
		periscope.toast(({ toastProps }) => {
			return perspective.createView({
				resourcePath: viewPath,
				mountPath: `toast-${toastProps.toastId}`,
				params: viewParams
			})
		}, toastOptions)
	}''', args)
	

# Usage
showViewToast(
	'Periscope/toasts/resources/EmbeddedView', 
	{ 'myParam': 'Text Value'}, 
	{ 'type': 'success', 'theme': 'colored' }
)

If you want access to toast-specific information you should pass those in as view params too. The example views show the isPaused status as well as the toastId (the toastId is used for interacting with the toast after it has been created, allowing you to close it, change its state/options, etc.).

Using the spread operator makes it pretty convenient:

system.perspective.runJavaScriptAsync('''(viewPath, viewParams, toastOptions) => {		
	periscope.toast(({ toastProps }) => {
		return perspective.createView({
			resourcePath: viewPath,
			mountPath: `toast-${toastProps.toastId}`,
			params: {
				...viewParams,                // Spread the viewParams object.
				toastId: toastProps.toastId   // Also include the toastId.
			}
		})
	}, toastOptions)
}''', args)
2 Likes

Tried it for the first time today. Looks really nice, but is there a reason for it to be so long to install/restart compared to other modules.

On the gateway, I go to Config -> Modules, restart any module is almost instantaneous, but this one takes a long time, and when I get in the logs, I see that it triggered "Restarting gateway scripts..." on all our projects. Is there a way for me to prevent this?

Any module that supplies any jython extension function will trigger this, and it cannot be avoided.

2 Likes

What Phil said.

Also, in 8.3 hot loading of modules will be gone.

3 Likes

Not planning on hot loading modules, our server started to use full CPU at the same moment I installed it, so, of course, it was a suspect to the performance issue, and the delay loading was bugging me. I am now sure this module was not the issue, but I needed to check

1 Like

This is great, thanks for all the help, as always!
I shouldn't (other priorities), but I'll most likely try this out today :grin:

If there's ever another community service award at ICC like Phil received 2 years back, you'll be my first vote! :+1: @Carl.Gould

5 Likes

Slight issue, docks have a higher z-index than the toast does. Originally I didn't think the simple example was working, but it turned out it was showing underneath my side dock and navigation dock

1 Like

Eek, a coworker just pointed this out too.

I think that the toasts should be contained within the center view and not overlap with the docks.
Any objections?

2 Likes

I think that'll work. I think most of the time I would be making it bottom-centred. It wooould be nice to have bottom right as well but over the top of docks, but if too hard then centre view is all good

I’d eventually like the top level ToastContainer to be configurable through session properties, however there’s no good mechanism at the moment for module authors to do that.

In the meantime I think having center-bound toasts is the safest option.

1 Like

Cheers!

1 Like

With the the FlexRepeaterPlus, is there any way to get the row index automatically (without generating and pass in as a param)? I want to colour alternating rows differently, but not sure how. I can only see the key UUID

It's an implicit parameter, it should be accessible by adding parameter named index in the child view.

I don't think it's documented anywhere...

Also a note about the UUID keys: there's nothing wrong with using the automatically generated UUIDs, but if you're generating the instances list from a binding/script you should be providing something more meaningful, like a database primary key or a unique ID.

As you add/remove/reorder the instances array, React uses the key property to track what changes are happening to each instance.

Example, deleting an instance:

# Before
 index | key
---------------
   [0] | "key-0"
   [1] | "key-1"
   [2] | "key-2"  <- To be deleted.
   [3] | "key-3"
   [4] | "key-4"

# After
 index | key
---------------
   [0] | "key-0"
   [1] | "key-1"
   [2] | "key-3"
   [3] | "key-4"

Order of operations:

  1. You have 5 instances, each with unique keys.
  2. You delete instance[2] from the list using the popInstance() method.
    • The index parameter for the instances is recalculated and updated on the gateway's model of the view.
    • This means bindings against the index will update immediately, even before the new instances list is ever rendered.
  3. React sees the instances list has changed and re-renders.
    • It remembers that it had already rendered instances with keys key-3 and key-4 before, only now they are in different positions.
    • Nothing happens to instances key-3 and key-4, except they have a new index.
    • This is the biggest difference between IA's version and the + version.
1 Like

Not sure about the FlexRepeaterPlus, but with the normal FlexRepeater, you can colour alternating rows using pure CSS. See this topic:

2 Likes

Yeah, you can totally do this with the Flex Repeater +. The DOM structure should be identical to the regular Flex Repeater.

1 Like

FYI

Yeah it’s a thing :worried:

@PGriffith do you see anything wrong with this? It works in the designer, but complains in the gateway.

…
        "style": {
            "$ref": "urn:ignition-schema:schemas/style-properties.schema.json",
            "default": {
                "classes": "",
                "overflow": "auto"
            }
        }
…
2 Likes