CloudHaven - Universal Application Portal (Vue-based dynamic UI-as-a-Service)

CloudHaven provides a unique way of developing Vue-based user interfaces using UI-as-a-Service that dynamically generates Vue-based UIs from json provided by a REST server. (CloudHaven is much more than this UIaaS however)

CloudHaven is an open source project under the MIT license currently being developed by 25+ university students and Rich Vann.

CloudHaven holistically combines authentication/user management, personal data control/management and a User-Interface-as-a-Service (UIaaS) to give anyone a sort of universal application portal. It is a great platform for developing workflow applications or other applications that are composites of components from multiple vendors/organizations - while also providing centralized storage and control of user data, centralized control of groups and permissions, and a single login.

Cal Poly San Luis Obispo accepted CloudHaven as a capstone project for software engineering students for this past year (20/21) as a vehicle for teaching full stack development. Other universities are also considering CloudHaven as a basis for student projects.

There are currently two versions of the system, one based on React that Cal Poly is developing and one based on Vue/Vuetify that I am developing (Other component libraries like Quasar could also easily be swapped in). UI development under CloudHaven differs substantially from typical UI development - instead of “baking in” how the UI works, CloudHaven’s UIaaS dynamically renders Vue or React-based UIs from json provided by vendor “back-end” servers via a REST interface. Under this model, components of a UI can be drawn from multiple vendor back-ends “on-the-fly”. Vendor back-ends register their applications and components with CloudHaven, providing a URL to their back-end server. Users can then subscribe to those applications or other vendors can reference the components in their own applications.

But its more than just a UIaaS. Since CloudHaven also stores user data, providing a central repository where users’ data can remain under users’ control for the lifetime of that data. This model eases data regulatory compliance since applications need not touch user data (a potential antidote for big data “surveillance capitalism”). CloudHaven also provides authentication, user management, groups, permissions, sharing controls, etc.

From a software engineers perspective: CloudHaven extracts the “user entity” (authentication, user management, user data, user-related functionality and even low level UI components) that is implemented in thousands of applications with massive redundancy, and consolidates it into a single instance under users’ control.

CloudHaven is a paradigm shift: instead of “an application having many users”, “a user is served by many applications” - it’s a shift that opens up many possibilities.

You can read more about the project at www.cloudhaven.net - a pitch deck to be found under “Presentations” on the main menu. There is also a UIaaS API programmers guide that can be found under Resources, Documents.

Note that this is an open source project under MIT license and intended as a community service, not profit-driven, project. It is intended to be operated as something like a platform cooperative or other equitably-owned, democratically-governed, vendor neutral entity. If eventually widely accepted it could even act as a way to “unionize” all users on the Internet.

CloudHaven welcomes anyone interested in contributing to this project - signup on the website under “JOIN THE TEAM”.

  • Rich Vann

[image] www.cloudhaven.net

Below is sample code demonstrating the json “Page” object that powers a CloudHaven page maintaining a list of ICD10 codes:

const dataModel = {
  dialog: false,
  valid: true,
  headers: [
    { text: 'Actions', value: 'name', sortable: false, align:'center' },
    {
      text: 'Code',
      align: 'left',
      sortable: true,
      value: 'code'
    },
    { text: 'Description', align:'left', sortable:true, value: 'description' }
  ],
  editedIndex: -1,
  editedItem: {
    code: '',
    description: ''
  },
  icd10Codes: []
};
const uiMethods = {
  mounted: {
    body: `
    debugger;
    this.loadIcd10Codes();
    `
  },
  resetEditObject: {
    body: `this.editedItem = {code: '', description: ''}; this.editedIndex = -1; `
  },
  loadIcd10Codes: {
    body: `
    debugger;
    this._appGet('icd10codes/list', function(data) {
      debugger;
      this.icd10Codes = data;
    })
    `
  },
  required: {
    args: ["v"],
    body: `
    return !!v || "Required."
    `
  },
  editItem: {
    args: ["item"],
    body: `
    this.editedIndex = this.icd10Codes.indexOf(item)
    this.editedItem = Object.assign({}, item)
    if (!this.editedItem._id) {
      this.$refs.form.reset()
    }
    this.dialog = true;
    `
  },
  deleteItem: {
    args: ["item"],
    body: `
    confirm('Are you sure you want to delete '+item.code+'?') && 
      this._appPost('icd10codes', {op:'delete', id:item._id}, function(){
        this._showNotification( item.code + ' deleted.');
        this.loadIcd10Codes();
      })
    `
  },
  cancel: {
    body: `
    this.dialog = false;`
  },
  close: {
    body: `
    setTimeout(() => {
      this.resetEditObject();
    }, 300)`
  },
  save: {
    body: `
    if (!this.valid) return;
    this._appPost('icd10codes', {op:this.editedIndex > -1?'update':'create', data:this.editedItem}, function(result) {
      if (result.errMsg) {
        this._showError( 'ICD10 Code ('+this.editedItem.code+'): '+result.errMsg );
      } else {
        this._showNotification( this.editedItem.code+' '+(this.editedIndex > -1?'updated':'added')+'.');
        this.loadIcd10Codes();
      }
      this.dialog = false;
    });
    `
  }
};
const computed = {
  formTitle: {
    body: "return this.editedIndex === -1 ? 'New ICD10 Code' : 'Edit ICD10 Code'"
  }
};

const uiConfig = {
  requiredUserData: [],
  dataModel:dataModel,
  methods: uiMethods,
  computed:computed,
  uiSchema: {
    component: 'container',
    props: {fluid:true},
    contents: [
      {
        component: 'toolbar',
        props: {flat:true, color:'white'},
        contents: [
          {component: 'toolbarTitle', contents:"ICD10 Codes"},
          {component: 'divider', props:{insert:true, vertical:true}, attrs:{class:'mx-3'}},
          {component: 'spacer'},
          {component: 'button', props:{dark:true, color:'primary'}, class:'mb-3', contents:"New ICD10 Code",
        on:{click:{body:`resetEditObject(), dialog=true;`}}}
        ]
      },
      {
        component: 'dataTable',
        props: {":headers":"headers", ":items":"icd10Codes", "hide-default-footer":true, "disable-pagination":true},
        class:"elevation-1",
        scopedSlots: {
          item: {
            component: "tr",
            on: {
              "click.stop": "editItem(item)"  
            },
            contents:[
              { component:'td', 
                class:"d-flex justify-center align-center px-0",
                contents: [
                  {
                    component: "button", props:{icon:true},
                    contents: [{
                      component:"icon",
                      props:{medium:true},
                      on: {
                        "click.stop":"editItem(item)"
                      },
                      contents:"mdi-pencil"
                    }]
                  },
                  {
                    component: "button", props:{icon:true},
                    contents: [{
                      component:"icon",
                      props:{medium:true},
                      on: {
                        "click.stop": "deleteItem(item)"
                      },
                      contents:"mdi-trash-can"
                    }]
                  }
                ]
              },
              {component: 'template', template:"<td>{{item.code}}</td>"},
              {component: 'template', template:"<td>{{item.description}}</td>"}
            ]
          }
        }
      },
      {
        component: 'dialog',
        vmodel: 'dialog',
        style: 'background-color:white;',
        props: {"max-width":"500px"},
        on: {"keydown.esc.prevent":"dialog=false;"},
        contents: {
          component: "card",
          contents: [
            { component: "cardTitle", template: '<span class="text-h5">{{ formTitle }}</span>'},
            { component: "cardText", contents: {
                component: "form",
                vmodel: "valid",
                contents: [
                  {
                    component: "textField",
                    vmodel:"editedItem.code",
                    props: {label:"Code"},
                    attrs: {required:true},
                    rules:[
                      "required",
                      { args:["v"], body: 'return /^[A-TV-Z][0-9][A-Z0-9](\.?[A-Z0-9]{0,4})?$/.test(v) || "Please enter a valid ICD10 code. "'}
                    ]
                  },
                  {
                    component: "textField",
                    vmodel:"editedItem.description",
                    props: {label:"Description"}
                  }
                ]
            }},
            {
              component: "cardActions",
              contents: [
                { component: "button", props:{elevation:"2", color:"blue darken-1", text:true}, on: {click:"cancel"}, contents:"Cancel" },
                { component: "spacer"},
                { component: "button", props:{elevation:"2", color:"blue darken-1", text:true}, on: {click:"save"},
                  contents:[{component:"icon", props:{left:true, dark:true}, contents:"mdi-content-save"}, 
                  "Save"
                ] }
              ]
            }
          ]
        }
      }
    ]
  }
};

import BaseAction from '../actions/baseaction'
import Roles from '../../models/workflowroles'
export class ICD10CodesPage extends BaseAction{
  constructor(){
    super();
    this.roles = [Roles.BMCSysAdmin, Roles.Scheduler];
  }
  
  route() {

    this.router.get("/", (req, res) => {
      res.json(uiConfig)
    });

    return this.router;
  }
}