Application Packaging Standard

Last updated 18-Mar-2019

New Users

In this step, you will design and develop a view source that allows customers to assign service resources to the new users.

../../../../../_images/user-step-model1.png ../../../../../_images/user-step-meta1.png ../../../../../_images/user-step-provision1.png ../../../../../_images/user-step-presentation-b.png ../../../../../_images/user-step-deploy1.png ../../../../../_images/user-step-provisioning1.png

Input, Output, and Requirements

In meta description, you added the add-user-service.js file as the source for the service assignment step in the system User Creation Wizard. When designing the view, you should issue requirement for the view development similar to those we will follow in this project:

  • The view must use the APS Biz API methods to get a list of new users from the wizard, get a list of resources from the customer subscription to analyze the resource availability.
  • Configure VPS properties by taking them initially from the newvps.json file and then allow a customer to select one of the available offers and to change properties before proceeding to the next step.
  • Using the Biz API, the application must request the platform for the offers selected by the customer. In a case when the current subscription does not allow to use the selected offer for all new users, the platform must allow the customer to increase the respective limit through the upsell mechanism.
  • Define the onNext and onPrev button handlers to direct a customer to the respective wizard step. The former handler must call the aps.apsc.next() method to forward the customer to the next wizard step. The wizard will call the aps.biz.commit method to complete the APS Biz requests created by the view.
  • Ensure the current settings on the view (for example, host names) do not reset when a customer steps forward or backward in the wizard.

In this project, let us expect the following user experience (UX) for customers:

  • A customer starts adding new users and requires assignment of the application service to those users.
  • After setting the users’ properties in the first standard wizard step, the customer will see the custom application screen to assign the application service to the new users.
  • The customer enters a host name for every VPS to be assigned to a user.
  • All VPSes must be built in accordance with a service profile (offer) chosen by the customer. For this, the application must display all offers in the current subscription as a set of tiles.
  • If the customer selects an offer whose availability in the subscription is less than the number of the new users, the platform prompts the customer to increase the offer limit and it shows the price for the purchase. If that offer is available in more than one resource rates due to the use of composite resources, the customer will be able to select one of the possible ways to increase the offer limit.

The following view layout complies with the UX requirements:

../../../../../_images/add-user-vps-layout.png

Continue Your Demo Project

This section continues the demo project from the previous step.

When creating the ui/add-user-service.js script from scratch, follow these steps.

  1. Create the script skeleton:

    define([
          "dojo/_base/declare",
          "dojo/_base/lang",
          "dojox/mvc/getPlainValue",
          "dojox/mvc/at",
          "dojo/promise/all",
          "dojox/mvc/getStateful",
          "dojox/mvc/StatefulArray",
          "aps/ResourceStore",
          "aps/Memory",
          "aps/TextBox",
          "aps/json!./wizard/newvps.json",
          "aps/View"
       ],
       function (declare, lang, getPlainValue, at, all, getStateful, StatefulArray,
          Store, Memory, TextBox, newVPS, View) {
    
          /* Declare global variables */
    
          /* Define a global function to request the platform for creation of new VPSes */
          function resourceRequest(tileId, offerById, view, subscriptionId) {
             // Send a Biz API request for resource usage change
          }
    
          return declare(View, {
             init: function() {
                /* Render a gird cell to allow entering a host name */
    
                /* Set a watcher for selection of a tile */
    
                /* Define and return widgets */
    
             }, // End of Init
    
             /* Process data available at the onContext phase */
             onContext: function() {
    
                /* Define data for the APS Biz API */
    
                /* Split into 2 cases */
                /* First case - I'm back from another wizard step */
                if (aps.context.wizardData[
                      "http://aps-standard.org/samples/suwizard1p#add-user-service"
                   ]) {
                   all({
                      // Retrieve only a list of users
                   }).then(function(data) {
                      // Update only the grid with users and host names
                   }).then(function() {
                      aps.apsc.hideLoading();
                   });
                }
    
                /* Second case - I'm here for the first time. */
                else {
                   all({
                      // Retrieve all resources to be processed
                   }).then(function(data) {
                   /* Update the model with a list of users */
    
                   /* Prepare the dictionary for offers */
    
                   /* Define the offer selection list */
    
                   /* Define the tiles presenting offers */
    
                   /* Check if the default Offer is available */
    
                   })
                   /* Hide loading once all async operations are completed */
                   .then(function() {
                      aps.apsc.hideLoading();
                   });
                }
             }, // End of onContext
    
          onNext: function() {
             // Go to the next wizard step
          },
    
          onPrev: function() {
             // Return to the previous step
          }
    
       });  // End of Declare
    });     // End of Define
    

    Keynotes:

    • Before the definition of the standard workflow methods, define the resourceRequest function to operate the Biz API requests.
    • The init method sets a watcher for the selection of an offer tile and defines the screen general layout containing a grid with the user list and a set of tiles with offers to select.
    • The onContext method will fill in the data sources with actual data from the APS controller and from the the User Creation wizard. It must analyze the offers available in the subscription and then build the list of offers as a set of tiles with the respective descriptions.
  2. Declare the global variables, the view itself and the Offer-by-ID dictionary that makes easy to find an offer by its APS ID:

    var self,
       offerById = {};
    
  3. Define the global resourceRequest function:

    function resourceRequest(tileId, offerById, view, subscriptionId) {
       var offerId = view.byId(tileId).get('offerId');
       var offer = offerById[offerId];
       var vpsModel = getStateful({"data": newVPS}); // Default VPS properties
       vpsModel.data.set({
           offer: {aps: {id: offerId}},
           platform: offer.platform,
           hardware: offer.hardware
       });
       var usersQuery = view.byId("addUsers_grid").get("store").query();
       all({
           users: usersQuery,
           total: usersQuery.total
       }).then(function(data) {
           var users = data.users,
               usersTotal = data.total;
    
           var deltas = [
               {
                   apsType: "http://aps-standard.org/samples/suwizard1p/vps/1.0",
                   delta: usersTotal
               },
               {
                   apsType: "http://aps-standard.org/samples/suwizard1p/offer/1.0",
                   apsId: offerId,
                   delta: usersTotal
               }
           ];
    
           /* Call the APS Biz API to provision VPSes for the new users*/
           var request = {};
           request[subscriptionId] = {
               deltas: deltas,
               operations: users.map(function(user) {
                   return {
                       provision: lang.mixin(getPlainValue(vpsModel.data), {
                           name: user.hostName,
                           user: {aps: {id: user.id}},
                           userName: user.name
                       })
                   };
               })
           };
           aps.biz.requestUsageChange(request).then(null, function (err) {
               console.error(err);     // For debugging only
           });
       });
    } // End of resourceRequest() definition
    

    Keynotes:

    • The function receives the ID of the selected offer tile and sends an aps.biz.requestUsageChange request to order the offer for all new users. On receiving that request, the platform must identify if the subscription limits allow the customer to increase the resource usage. If there are not enough available resources, the platform must prompt the customer to buy additional resources as illustrated in the Add Users provisioning step.
    • Both the number of new VPSes and the number of required offers equal the number of the new users.
  4. In the init method, define the renderCell handler called renderHostName here and used to customize a grid column. The grid with the new users must allow a customer to enter host names. Use the aps/TextBox widget for this:

    self = this;
    
    var renderHostName = function(row) {
       return new TextBox({
          value: at(row, "hostName"),
          required: true
       });
    };
    

    Each row of the grid is based on its own model created by the getStateful method inside the onContext method and passed as the input argument row when calling the handler. The latter synchronizes the cell with that model using the at method.

  5. In the init method, set a watcher for a stateful array. This array will be assigned to the selectionArray of the set of tiles. When a customer selects an offer tile, the watcher will call the resourceRequest function to send an APS Biz request to the platform.

    /* Set a watcher for selection of a tile */
    var selectedTiles = new StatefulArray();
    selectedTiles.watchElements(function(index, removals, adds){
       if(adds.length == 1) resourceRequest(
          adds[0],
          offerById,
          self,
          aps.context.subscriptionId
       );
    });
    
  6. In the init method, define the page layout by a set of widgets returned by the method:

    return [
       ["aps/Grid",{
          id: this.genId("addUsers_grid"),
          store: new Memory({data: [], idProperty: "id"}),
          columns: [
             { name: "User Name", field: "name" },
             { name: "Host Name", field: "hostName", renderCell: renderHostName }
          ]
       }],
       ["aps/Tiles", {
              id: this.genId("addUsers_offerSet"),
              title: "Select an offer",
              selectionMode: "single",
              required: true,
              store: new Memory({data: []}),
              selectionArray: selectedTiles
          }, [
          [ "aps/Tile", {
                  offerId: at("rel:", "id"),
                  id: this.genId("${offerId}"),
                  gridSize: "md-4 xs-12",
                  title: at("rel:", "name")
              }, [
                  [ "aps/Output", {
                      value: at("rel:", "os"),
                      content: "Operating System: ${value}"
                  }],
                  [ "aps/Output", {
                      value: at("rel:", "cpu"),
                      content: "CPU cores: ${value}"
                  }],
                  [ "aps/Output", {
                      value: at("rel:", "memory"),
                      content: "Memory: ${value} MB"
                  }],
                  [ "aps/Output", {
                      value: at("rel:", "diskspace"),
                      content: "Disk space: ${value} GB"
                  }],
                  [ "aps/Output", {
                      value: at("rel:", "priceText")
                  }],
                  [ "aps/Output", {
                      usage: at("rel:", "usage"),
                      limit: at("limit"),
                      content: "${usage} of ${limit} servers are used."
                  }]
          ]]
       ]]
     ];
    

    In accordance with the design requirements, there are two top-level containers:

    • The user-host grid draws the names of the new users and requires the customer to type the names of the VPSes that will be created for the users.
    • The Select an offer selection group provides a list of offer tiles.
    • Both containers use the stores that initially are empty, but will be filled in by the onContext method later.
    • The selectionArray property refers to the selectedTiles array declared earlier.
  7. In the onContext method, define data necessary to build the widgets and to call the Biz API requests.

    • Specify the current subscription (find its ID in the input variable context) as the one used by default by all requests to the APS controller:

      aps.context.subscriptionId = aps.context.vars.context.aps.subscription;
      
    • Prepare a store to retrieve all offers of the application:

      var offerStore = new Store({
        target: "/aps/2/resources",
        apsType: "http://aps-standard.org/samples/suwizard1p/offer/1.0"
      });
      

      This will allow the view module to get any technical details of the offers.

    • Prepare a filter to retrieve all subscribed offers of the application:

      var offerFilter = { filter:
        [{ apsType: "http://aps-standard.org/samples/suwizard1p/offer/1.0" }]
      };
      

      This will allow the view to propose the proper offers and retrieve their commercial data.

    • Define the parent container, where the method will update the offer tiles:

      var parentContainer = self.byId("addUsers_offerSet"); // ``aps/Tiles``
      
  8. In the onContext method, define the data processing when the customer re-enters the view from another wizard step:

    /* I'm back from another wizard step */
    if (aps.context.wizardData[
             "http://aps-standard.org/samples/suwizard1p#add-user-service"
       ]) {
       all({
          newUsers: aps.biz.getResourcesToBind(),
          oldUsers: self.byId("addUsers_grid").get("store").query()
       }).then(function(data) {
          var newUsers = data.newUsers.users,
              oldUsers = data.oldUsers;
    
          /* User processing */
          var users = newUsers.map(function(user) {
              var oldUser = oldUsers.find(function(oldUser) {
                  return oldUser.id === user.aps.id;
              });
    
              return getStateful({
                  id: user.aps.id,
                  name: user.fullName,
                  hostName: oldUser ? oldUser.hostName : ""
              });
          });
          var usersStore = new Memory({data: users, idProperty: "id"});
          self.byId("addUsers_grid").set("store", usersStore);
    
          /* Check if the default Offer is available for the updated user list */
          resourceRequest(parentContainer.get(
             "selectionArray")[0],
             offerById,
             self,
             aps.context.subscriptionId
          );
    
       }).then(function() {
          aps.apsc.hideLoading();
       });
    }
    

    The above code anticipates some changes in the list of new users. It restores the host names for those users that remained in the list. This makes the UI more friendly for the customer. In this case, there is no need to change anything in the set of offers saved from the previous visit of the view.

  9. In the onContext method, define the data processing when the customer enters the view for the first time:

    /* I'm here for the 1st time */
    else {
       /* Retrieve the resources to be processed */
       all({
          newUsers: aps.biz.getResourcesToBind(), // New users
          subscribedOffers: aps.biz.getResourcesInfo(offerFilter), // Subscribed offers
          allOffers: offerStore.query()  // All offers of the application
       }).then(function(data) {
          var newUsers = data.newUsers,
              subscribedOffers = data.subscribedOffers,
              allOffers = data.allOffers;
    
          if(subscribedOffers.length === 0) return; // No offers in the subscription
    
          /* User processing */
          var users = newUsers.users.map(function(user) {
              return getStateful({
                  id: user.aps.id,
                  name: user.fullName,
                  hostName: ""
              });
          });
          var usersStore = new Memory({data: users, idProperty: "id"});
          self.byId("addUsers_grid").set("store", usersStore);
    
          /* Fill in the Offer-by-ID dictionary */
          allOffers.forEach(function(offer) {
              offerById[offer.aps.id] = offer;
          });
    
          /* Define the offer selection list */
          var offers = subscribedOffers.map(function(offer) {
              var offerApsResource = offerById[offer.apsId];
              return {
                  id: offer.apsId,
                  name: offerApsResource.name,
                  os: offerApsResource.platform.OS.name,
                  cpu: offerApsResource.hardware.CPU.number,
                  memory: offerApsResource.hardware.memory,
                  diskspace: offerApsResource.hardware.diskspace,
                  limit: offer.limit,
                  usage: offer.usage,
                  priceText: offer.priceText
              };
          });
          var offerCache = new Memory({data: offers, idProperty: "id"});
    
          /* Define the tiles presenting the subscribed offers */
          parentContainer.removeAll();        // Remove all child tiles
          parentContainer.set('store', offerCache);
    
          /* Check if the default Offer is available for all new users */
          resourceRequest(
             parentContainer.get("selectionArray")[0],
             offerById,
             self,
             aps.context.subscriptionId
          );
    
       })// End of all-then callback function
    
       /* Hide loading once all async operations are completed */
       .then(function() {
          aps.apsc.hideLoading();
       });
    }
    

    The above code consists of the following sections:

    • User processing - to display the new users in the grid declared in the init method.
    • Fill in the Offer-by-ID dictionary - to have a dictionary with the full representation of offers as APS resources. The dictionary will allow the application methods to get technical data about offers by their APS ID.
    • Define the offer selection list - to have a store for the tiles that will display commercial data about the offers.
    • Define the tiles presenting the subscribed offers - to draw a set of tiles showing all offers in the subscription.
    • The section that uses the resourceRequest function to verify if the default offer is available for all new users.
  10. In the onNext navigation handler, validate the current view and then forward the customer to the next wizard step passing any data to the wizard. The passed data allows the onContext method to identify if the customer enters the view for the first time or re-enters the view from another wizard step.

    /* Clear out all messages and validate the data */
    var page = this.byId("apsPageContainer");
    page.get("messageList").removeAll();
    if (!page.validate()) {
       aps.apsc.cancelProcessing();
       return;
    }
    aps.apsc.next("I was here"); // Flag for the case I'll be back
    
  11. In the onPrev navigation handler, validate the current view and then forward the customer to the previous wizard step passing any data to the wizard.

    aps.apsc.prev("I was here"); // Flag for the case I'll be back
    

Conclusion

You have completed the development of a view source code in the ui/add-user-service.js file.

Note

All service resources (VPSes) created in this case will have identical properties. To change them, the customer will need to edit them after creation of the resources.

The project file you have created is similar to the respective file in the sample package.