Cross-distro UI for OWA

Hi everyone,

During discussion in Planning Soldevelo work while Rafał’s on holiday @darius suggested that someone could investigate what might be a possible way to make UI’s look native on any distribution. I did some research and prototyping, you can see effects at: openmrs-module-crossdistro

This module is basically wrapper for 2 OWAs: distro and crossdistro. distro OWA simulates content provided by distribution and contains distro.bundle.js and distro.css. crossdistro OWA is client app, and it showcases proposed solution for creating cross distro UI.

Most important part is script in index.html. It tries to load Angular module provided by distro, and if unsuccessful, loads fallback module. Fallback module has to be provided by app, in case if deployed to headless platform. Notice that fallback bundle is not loaded as long as it is not needed.

Effectively, same component, like <distro-message> in this example OWA, can have totally different looks and functionalities based on what distribution provides. Same goes with looks of basic elements of page like tables, because stylesheets are overwritten by CSS provided by distribution. If you try to deploy openmrs-module-crossdistro on your sever, by default crossdistro OWA will be able to use code from distro. But if distro OWA is deleted, it gracefully falls back to a bit different look. You can see example screenshots in README.

With this solution, we can easily make OWAs use Angular components and stylesheets provided by distributions. Important issue is that we would have to come up with specification, how distributions should expose those components(using another OWA is just for needs of this proof of concept) and how common components(like header, breadcrumbs, etc.) Angular interfaces should look like.

What do you think?

Cc: @darius, @ssmusoke, @mksd, @raff

2 Likes

Hi @gutkowski,

This is very exciting!! Bear with me because I have a super busy week but I will definitely look into it and come back with comments/questions.

Thanks so much for looking into it!

I’m also very excited that you’ve done this, but I’m going to be super busy in the short term, so my response will be slow. (Also, @bharatak, see the first post in this thread.)

@gutkowski thank you for doing this will be looking into it this evening and tomorrow to provide feedback

@gutkowski, thanks for looking into that!

It would help, if you could highlight pieces of js code, which are responsible for loading components conditionally.

Is this what we should be looking at:

//'angular.module(...)' throws exception if specified module cannot be found
try{
    distroModuleName = angular.module('distro').name
} catch(err) {
    distroModuleName = "fallback";
}

let crossdistroModule = angular.module('crossdistro', [ uiRouter, Home.name, distroModuleName, 'openmrs-contrib-uicommons'
    ])
    .component('main', mainComponent);

export default crossdistroModule;

?

You basically load conditionally an angular module from distro, which declares the distroMessage component. If it fails, you try to load a fallback module from the current owa, which declares the distroMessage component.

I feel we need something a bit different. We need to load our openmrs-contrib-uicommons angular module (library of components) and possibly another module declared by a distribution globally, which overwrites some components from openmrs-contrib-uicommons.

Now when you write an OWA and say angular.module(“openmrs-contrib-uicommons”), it will load a module combined of components from the distribution and openmrs-contrib-uicommons. If a component is declared in both distribution and openmrs-contrib-uicommons then the one from distribution should be used.

Do you feel you could tweak the openmrs-contrib-uicommons angular module to do conditional loading of submodules with components?

To showcase you could prepare a distribution customized openmrs-header component.

Yes, part you quoted takes care to inject distro/fallback Angular module into main module, script I linked in my original post:

  //distro module is provided by distro.bundle.js, if it is found, just proceed with bootstrapping angular
  angular.module('distro');
  angular.bootstrap(document, ['main']);
}
catch(e) {
  //if distro.bundle.js is not available, fallback to fallback.bundle.js from this app
  var script = document.createElement('script');
  script.src = 'fallback.bundle.js';
  script.type = "text/javascript";
  script.onload = function(){
    //wait with bootstrapping till fallback bundle is loaded
    angular.bootstrap(document, ['main']);
  };
  document.getElementsByTagName('head')[0].appendChild(script);
}

handles requesting fallback bundle from server if needed.

There’s one problem with this concept: Angular doesn’t allow multiple components/directives with with same signatures and throws $compile:multidir error at start up. Unfortunately, unlike directives, components don’t support any kind of priority. I will look if we can work it around.


EDIT: It seems that module provided by distribution just needs to declare overriding component in submodule with same name as in uicommons. Maybe you meant it in your post and I didn’t get it at first. Anyway, I will let you know here when I will update crossdistro example with header overriding, as you requested.

I’ve updated example project, you can find detailed description how it works now in README

Thanks Paweł! Looking much better now!

Can we move

//'angular.module(...)' throws exception if specified module cannot be found
var dependencies = [ uiRouter, 'openmrs-contrib-uicommons',...];
try {
    distroModuleName = angular.module('distro').name;
    dependencies.push(distroModuleName);
} catch(err) {
    console.log('No distro module');
}

let crossdistroModule = angular.module('crossdistro', dependencies)

into openmrs-contrib-uicommons so that it is handled by our library instead of copying it to each OWA?

We could have a convention, which says the distro module should be named “openmrs-contrib-uicommons-customized” for the sake of calling angular.module(‘openmrs-contrib-uicommons-customized’).name.

I’d also suggest deploying the customized js and css files simply as an OWA with just those 2 files so that they can be used by all other OWAs. We could call the OWA uicommons-customized and then you would just have to declare in all OWAs:

<!--last stylesheets have the highest precedence, effectively distribution can take over app styling -->
<link rel="stylesheet" type="text/css" href="/owa/uicommons-customized/uicommons.css"/>

<!--As with stylesheets, angular modules from distro bundle override app submodules-->
<script src="/owa/uicommons-customized/uicommons.js" type="text/javascript"></script>

How does it feel?

If you want to customize components or css for all OWAs in your distribution you would only have to create uicommons-customized OWA and deploy it to your server.

1 Like

@raff @gutkowski What is the planned convention for adding an OWA to an existing module which already has an omod - say dataintegrity. Is it:

  1. An owa sub-module (what is the convention for this)

  2. An owa project - say openmrs-module-dataintegrity-owa

@darius @wyclif et al If 1, is the owa be deployed automatically from the omod package - so that it is a single step? What is the planned deployment architecture?

See https://github.com/openmrs/openmrs-module-metadatamapping, which has the OWA project inside omod, which is automatically discovered and deployed together with the module.

Even though it is part of the module build, it can be conveniently developed using standard OWA tooling i.e. npm and webpack. You can also disable the app installed with the module and deploy a replacement if needed.

I’d say if your OWA depends only on one module, then embed it in that module, but if it depends on functionalities provided by a couple of modules or doesn’t require any backend from a module then do a standalone OWA project. Also if you do not own the module, but want to provide an alternative UI go with a standalone OWA project.

1 Like

@raff [quote=“raff, post:8, topic:8240”] Can we move … into openmrs-contrib-uicommons so that it is handled by our library instead of copying it to each OWA? [/quote]

Yeah, we can, that’s awesome. I’ve added this part to uicommons, and Travis CI is releasing this at the moment[quote=“raff, post:8, topic:8240”] I’d also suggest deploying the customized js and css files simply as an OWA with just those 2 files so that they can be used by all other OWAs. [/quote] I was thinking about it too. I think in that case we should modify owa module to explicitly support OWAs without apps.

Well, it can be an app with just one page explaining what it is for. It doesn’t have to be linked from anywhere in the UI other than manage apps page.

I’m reading through this thread now and trying to get caught up. It’s a bit exciting and a bit confusing. Let me know if I’m understanding right.

The landscape is:

  • We have the openmrs-contrib-uicommons library
  • We have a module (Data Integrity) that needs to work in both the Reference Application and Bahmni UIs. It defines its UI in an OWA, and it knows about the uicommons library so it includes those resources in its bundle, but it doesn’t know about any specific distributions.
  • We have the OpenMRS Reference Application; this doesn’t currently have a “distro OWA” but it should eventually
  • Bahmni has a different UI styling, so it wants to provide this in its own “distro OWA”

The key tricks are that:

  • The uicommons library provides default components and styling
  • everyone who writes any OWAs includes this uicommons library
  • everyone who writes any OWA includes in the head of all their html pages two “special” resources: <link rel="stylesheet" type="text/css" href="/owa/uicommons-customized/uicommons.css"/> <script src="/owa/uicommons-customized/uicommons.js" type="text/javascript"></script>
  • each distro (RefApp, Bahmni, etc) needs to provide their own OWA, which is always called “uicommons-customized”, and it needs to have uicommons.css and uicommons.js in it.
  • the uicommons-customized/uicommons.js file needs to provide an angular module called “openmrs-contrib-uicommons-customized” (which I suggest we instead call “distro-customization”), and the uicommons library automatically loads this.

We’re definitely getting somewhere, but I don’t think this is right yet. Some concerns:

  • every distro is going to have to publish an OWA with the same name, and it’s going to be very confusing to have multiple things with the same name floating around.
  • the uicommons library will grow quite large as we put more and more shared components there. But OpenMRS history shows that not everyone is going to want to use these. I think we’d need to break things up so that the absolute minimum lives in a separate library that encapsulates all these concerns, and uicommons is free to grow and even get bloated, without requiring everyone to deal with it.

@darius you got it right, but you overlooked the fact, that an OWA is not really forced to use uicommons, in this case it just have to take care of conditional loading of distro module(like in snippet quoted by @raff in this post), and provide its own implementations for components. It makes sense if dev wants to make really small app and needs just the distribution’s header.

Another possibility is to change the way uicommons is packaged to make it possible to import just a single component, because it seems that right now it is always whole bundle.

Maybe we could specify in manifest.webapp of distro OWA that it is implementation of customized uicommons, and make OWA module deploy it to uicommons-customized directory?

This seems like the right approach to me.

This is a great idea! Can you ticket this for the OWA module? (And even work on it?)

@darius I will tweak uicommons packaging soon. I’ve created issue: https://issues.openmrs.org/browse/OWA-9, I will work on it when it’s marked as ready for work :slight_smile:

For what it’s worth, I added a ticket about this here:

https://issues.openmrs.org/browse/RAUI-5