Blog

All Blog Posts  |  Next Post  |  Previous Post

Extend TMS WEB Core with JS Libraries with Andrew:
HexaGongs Part 5 of 5: Deployment

Friday, June 16, 2023

Photo of Andrew Simard

In our fifth and final post covering our TMS WEB Core and TMS XData project, HexaGongs, we'll start by wrapping up a few loose ends from last time. Then we'll have a look at our final set of main buttons - used for saving and loading HexaGongs projects. And then we'll finish up by covering several items related to deploying these kinds of projects.

Contents.

  1. HexaGong Sets.
  2. Saving HexaGongs Projects.
  3. Loading HexaGongs Projects.
  4. Deployment - TMS XData Project
  5. Deployment - TMS WEB Core
  6. All Done - For Now.


HexaGong Sets.

We left out the final item in our list of audio sources in our previous blog post to focus on the UI for making audio adjustments. That was in part because the last audio source doesn't need any audio adjustments. It is used to select a set of existing HexaGongs, so that they can all be played together, either in series or in parallel.

In order to select which HexaGongs are to be included, we'll need some kind of component that can list all the HexaGongs that are in the project (excluding the current HexaGong being edited). This component will then also need to allow us to select them individually, as well as provide the means to select the order they will be played.

Standard HTML components for selecting entries from a list, like the <select> element, tend to not work all that great for a few reasons. Mostly because they don't offer the flexibility that we need natively for any but the most basic tasks. But we've already got a component in our project that we've used to display lists - Tabulator.  Let's use that to create a list of HexaGongs, and then add the columns we need to provide for selection as well as for dragging the list items to rearrange the sort order.

The definition for this Tabulator table, like the one used for the Audio Clips list, can be found in WebFormCreate.

    this.tabAudioSets =  new Tabulator("#divAudioSetsTable", {
      index: "ID",
      layout: "fitColumns",
      placeholder: "No HexaGongs available.",
      rowHeight: 30,
      selectable: 1,
      headerVisible: false,
      movableRows: true,
      columnDefaults:{
        resizable: false
      },
      initialSort: [
        {column:"Sort", dir:"asc"},
        {column:"Name", dir:"asc"}
      ],
      columns: [
        { title: "ID", field: "ID", visible: false },
        { title: "Selected", field: "Selected", width: 40, cssClass: "PlayClip",
          formatter: function(cell, formatterParams, onRendered) {
            if (cell.getValue() == false) {
              return '<div style="background: purple; width: 30px; height: 29px; margin: -3px 0px 0px 4px; padding: 5px 8px; "><i class="fa-solid fa-xmark fa-xl"></i></div>'
            }
            else {
              return '<div style="background: violet; width: 30px; height: 29px; margin: -3px 0px 0px 4px; padding: 5px 4px; "><i class="fa-solid fa-check fa-xl"></i></div>'
            }
          },
          cellClick: function(e, cell) {
            pas.Unit1.Form1.tabAudioClips.selectRow(cell.getRow());
            cell.setValue(!cell.getValue());
          },
        },
        { title: "Sort", field: "Sort", width: 30, minWidth:30, formatter: "handle" },
        { title: "Name", field: "Name" },
        { title: "Length", field: "Length", width: 60, hozAlign: "right", formatter:"html" },
{ title: "PlayTime", field: "PlayTime", visible: false }
] });

In this case, most of the attention is focused on the "Selected" column - where the inclusion of each HexaGong in the set can be made. This is all handled within Tabulator itself, toggling the boolean value when the cell is clicked, displaying either a Font Awesome "check" or "xmark" icon, and adjusting the color at the same time. The list can also be sorted manually, by dragging the rows. The "hamburger" icon is included as a column, though strictly not necessary as the columns can be dragged without this being visible. This extra sorting capability adds another layer of complexity but is necessary in order to set the order of playback of the selected HexaGongs.  

This set list is created dynamically each time the Options dialog is shown, so as to have the most current information about HexaGong play times, names, and so on. The last settings for a particular HexaGong selection of sets are then applied to this updated list. A bit of work, but this is needed to keep things consistent as HexaGongs are changed, while also being able to save and redisplay the selections when the list contents have changed.

  asm
    // Get list of known HexaGongs (not including the current HexaGong or deleted HexaGongs)
    this.OptionsAudioSetsData = [];
    for (var i = 0; i < this.GongData['HexaGongs'].length; i++) {
      if (parseInt(this.GongID) !== i) {
        if (this.GongData['HexaGongs'][i]['Deleted'] !== true) {
          this.OptionsAudioSetsData.push({
            "ID": i,
            "Selected": false,
            "Sort": -1,
            "Name": this.GongData['HexaGongs'][i]['Name'],
            "Length": '<div style="padding-right: 8px;">'+this.GongData['HexaGongs'][i]['Audio Time'].toFixed(1)+'s'+'</div>',
"PlayTime": parseFloat(this.GongData['HexaGongs'][i]['Audio Time']) }); } } } // update the list to reflect the last sort order and selection for this HexaGong. for (var i = 0; i < this.GongData['HexaGongs'][this.GongID]['Audio Sets Data'].length; i++) { var id = this.GongData['HexaGongs'][this.GongID]['Audio Sets Data'][i].ID; for (var j = 0; j < this.OptionsAudioSetsData.length; j++) { if (this.OptionsAudioSetsData[j].ID == id) { this.OptionsAudioSetsData[j].Sort = this.GongData['HexaGongs'][this.GongID]['Audio Sets Data'][i].Sort; this.OptionsAudioSetsData[j].Selected = this.GongData['HexaGongs'][this.GongID]['Audio Sets Data'][i].Selected; } } } this.tabAudioSets.setData(this.OptionsAudioSetsData); this.tabAudioSets.setSort([ {column:"Name", dir:"asc"}, {column:"Sort", dir:"asc"} ]); end;

Ultimately, we end up with a new "list box" where we can select any number of HexaGongs to include in the set, as well as change the sort order manually and display whatever other supporting information (like play length) that we might want to include.

TMS Software Delphi  Components
Audio Set Editing.

When saving this type of HexaGong audio type, we can store the selected HexaGongs as an attribute alongside the other audio parameters so that we can more readily access them when they are needed.

  asm
   var SelectedSets = [];
    for (var i = 0; i < this.tabAudioSets.getDataCount(); i++) {
      this.tabAudioSets.getRowFromPosition(i+1).getCell('Sort').setValue(i);
      if (this.tabAudioSets.getRowFromPosition(i+1).getCell('Selected').getValue() == true) {
        SelectedSets.push('Gong-'+this.tabAudioSets.getRowFromPosition(i+1).getCell('ID').getValue());
        Longest = Math.max(Longest, this.tabAudioSets.getRowFromPosition(i+1).getCell('PlayTime').getValue();
Combined += Math.max(Longest, this.tabAudioSets.getRowFromPosition(i+1).getCell('PlayTime').getValue();
} } this.GongData['HexaGongs'][this.GongID]['Audio Sets Data'] = this.tabAudioSets.getData(); this.GongData['HexaGongs'][this.GongID]['Audio Sets'] = JSON.stringify(SelectedSets);
if (this.OptionsAudioStyle == 4) {
if (this.OptionsAudioSetType == 0) {
this.OptionsAudioTime = Longest;
}
else {
this.OptionsAudioTime = Combined;
}
}
Sets = JSON.stringify(SelectedSets); end; Gongs[GongID].ElementHandle.setAttribute('audiotime', FloatToStr(OptionsAudioTime));
Gongs[GongID].ElementHandle.setAttribute('audiogain', IntToStr(OptionsAudioGain));
Gongs[GongID].ElementHandle.setAttribute('audiostart', IntToStr(OptionsAudioStart));
Gongs[GongID].ElementHandle.setAttribute('audioend', IntToStr(OptionsAudioEnd));
if OptionsAudioStyle = 4 then
begin
Gongs[GongID].ElementHandle.setAttribute('audiosets',Sets);
if OptionsAudioSetStyle = 0
then Gongs[GongID].ElementHandle.setAttribute('audiosetstyle','series')
else Gongs[GongID].ElementHandle.setAttribute('audiosetstyle','parallel');
end
else
begin
Gongs[GongID].ElementHandle.removeAttribute('audiosets');
Gongs[GongID].ElementHandle.removeAttribute('audiosetstyle');
end;


To get the selected HexaGongs all playing in parallel, we can just click on all of the buttons that are included in the set. We can retrieve the list from the attribute that they were stored in above, and just iterate through the list to "click" on each of them. Here's what that looks like.

            var sets = JSON.parse(event.target.getAttribute('audiosets'));
            if (sets !== null)  {
              sets.forEach(gong => {
                var el = document.getElementById(gong);
                el.click();
              });
              return;
            }

To get them to play in series, one after another, we'll have to delay the clicks by the length of the audio for each.  Note that if one or more of the HexaGongs happen to refer to sets themselves, then nothing really needs to be done differently here, as the length of time was also set at the time the set was saved, based either on the longest play time or the sum of all the play times.

                var playdelay = 0;
                sets.forEach(gong => {
                  var el = document.getElementById(gong);
                  setTimeout(function() { el.click(); }, playdelay);
                  playdelay += parseFloat(el.getAttribute('audiotime')) * 1000;
                });

The end result is that we've got all of our HexaGongs set up to play as required, with sets representing the ability to group them together in a couple of different ways. Lots of options to support unique combinations of HexaGong elements.


Saving HexaGongs Projects.

Our last big-ticket item relates to saving and loading all the HexaGongs as a single project file. For the most part, this involves saving the GongData JSON object, but we'll have to add some of our other arrays to it. In particular, our GongAudio array, which contains the decoded audio data. Other arrays of interest include those containing information about the positions of each of the HexaGongs.

For now, let's deal with the JSON itself. To trigger the browser's file-saving mechanism, we can use another JavaScript library, FileSaver, which we previously covered in this post. As usual, we can add this via the Project.html file directly or by using the Delphi IDE's Manage JavaScript Libraries feature.

    <script src="https://cdn.jsdelivr.net/npm/file-saver@latest/dist/FileSaver.min.js"></script>  

The implementation then looks like the following. Note that we're creating files with the "hexagongs" extension, but they're just regular JSON files, for now at least.

procedure TForm1.btnDownloadClick(Sender: TObject);
var
  FileData: String;
  FileName: String;
begin
  FileData := '';
  FileName := '';
  asm
    FileData = JSON.stringify(this.GongData);
    FileName = this.GongData['HexaGongs Project Title']+'.hexagongs';
    var blob = new Blob([FileData], {type: "application/json;charset=utf-8"});
    saveAs(blob, FileName);
  end;
end;


Note that this FileSaver mechanism doesn't prompt the user for a filename - it just downloads the file automatically, adding (1) or (2), etc. to the filename if it already exists. The filename is taken from the first page of the Options dialog. 

Addendum: This behavior seems to be browser-dependent. Firefox, for example, prompts for the filename, using the supplied filename as the default.

Ultimately, we'll want to create a new JSON object using GongData as the base, adding in whatever other bits we want, So the contents of the JSON object we're saving will change, but this saving mechanism will still work. The main obstacle, however, is the actual audio data. Once it has been decoded from the original MP3 or WAV file, it is stored internally in an "AudioBuffer" object. This is a more complex object, where the audio data itself is stored in separate channels (left and right for example) in a PCM format, along with other attributes like number of channels, playback rate, and so on. 

While JavaScript is famous for being weakly typed, it does use more complex types to handle this kind of thing.  Unfortunately, we can't just run an AudioBuffer through JSON.stringify() directly. Instead, we can convert an AudioBuffer into an ArrayBuffer, and then convert that into a Base64 string. To do that, we'll make use of another JavaScript library - AudioBuffer-ArrayBuffer-Serializer. This can be added to our project, providing the means to do this conversion. A CDN link is available, or the usual Manage JavaScript Libraries feature in the Delphi IDE can be used.

    <script src="https://cdn.jsdelivr.net/npm/audiobuffer-arraybuffer-serializer@0.0.36/aas.min.js"></script>

In addition to the audio data (stored in GongAudio), we'll also need some of the other arrays - particularly those that contain the HexaGong positioning information. Most everything else is stored in GongData. Here's what we've got, performing the audio data conversion and building out the rest of the JSON to be saved in the resulting file.

procedure TForm1.btnDownloadClick(Sender: TObject);
var
  FileData: String;
  FileName: String;
  FileTime: String;
begin
  FileTime := FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',Now);
  FileData := '';
  FileName := '';
   
  asm
    function arrayBufferToBase64( buffer ) {
      var binary = '';
      var bytes = new Uint8Array( buffer );
      var len = bytes.byteLength;
      for (var i = 0; i < len; i++) {
          binary += String.fromCharCode( bytes[ i ] );
      }
      return window.btoa( binary );
    }

    var AudioStrings = [];
    for (var i = 0; i < this.GongAudio.length; i++) {
      if ((this.GongAudio[i] !== null) && (this.GongAudio[i] !== undefined)) {
        var encoder = new aas.Encoder();
        var audiostring = arrayBufferToBase64(encoder.execute(this.GongAudio[i]));
        AudioStrings.push(audiostring);
      }
      else {
        AudioStrings.push(null);
      }
    }
    FileData = JSON.stringify({
      "AppProject":       this.AppProject,
      "AppVersion":       this.AppVersion,
      "AooRelease":       this.AppRelease,
      "SaveTimestamp":    FileTime,
      "SaveFormat":       "JSON",
      "ZoomLevel":        this.ZoomLevel,
      "AnimatedElements": this.AnimatedElements,
      "GongData":         JSON.stringify(this.GongData),
      "PositionsR":       JSON.stringify(this.PositionsR),
      "PositionsC":       JSON.stringify(this.PositionsC),
      "PositionsG":       JSON.stringify(this.PositionsG),
      "GongsP":           JSON.stringify(this.GongsP),
      "Audio":            JSON.stringify(AudioStrings)
    });
    FileName = this.GongData['HexaGongs Project Title']+'.hexagongs';
    var blob = new Blob([FileData], {type: "applications/json;charset=utf-8"});
    saveAs(blob, FileName);
  end;
end;

The end result is still a JSON file, but much larger due to the encoded audio data. This is likely not the most space-saving design, as it will store the encoded audio even when using an MP3 URL. And this encoded audio data is not compressed, so a small 5MB MP3 file might be many times that size once converted to a PCM format and then into a Base64-encoded string. The upside though is that we don't need an internet connection to get at this audio data when loading the file. So while it might end up being 100MB or more, loading it takes almost no time at all. Tradeoffs abound here.

Loading HexaGongs Projects.

We've already had a couple of examples of loading files for this project. This time out, we're doing the same thing again and, perhaps amusingly, using a third variation of the TWebOpenDialog events - the GetFileAsText option. For the "access" property, we can use the ".hexagongs" extension that we used above.

procedure TForm1.btnUploadClick(Sender: TObject);
var
  i: Integer;
begin
  WebOpenDialog1.Accept := '.hexagongs';
  await(string, WebOpenDialog1.Perform);
  // If files were selected, iterate through them
  i := 0;
  while (i < WebOpenDialog1.Files.Count) do
  begin
    WebOpenDialog1.Files.Items[i].GetFileAsText;
    i := i + 1;
  end;
end;

Once we have the file, we can then replace our data structures by reversing the process we used to create the JSON object in the saved file. In our previous initial example, we just saved the GongData JSON object. Here we can just replace it.

procedure TForm1.WebOpenDialog1GetFileAsText(Sender: TObject; AFileIndex: Integer; AText: string);
begin
  asm
    pas.Unit1.Form1.GongData = JSON.parse(AText);
  end;
end;

This works pretty well, and as this is all handled locally by the browser, there isn't any delay when it comes to the time taken to download the file. If there were a large number of HexaGongs defined in the project, each with lengthy audio clips and custom images uploaded, this file could grow to a considerable size, but again as it is local it doesn't necessarily present a problem.

To load up our more complex example, we will have to take the reverse steps for each of the arrays that were stored, including reconstituting the GongAudio array with AudioBuffers that we had converted into Base64-encoded strings. In addition, we'll have to recreate most of the attributes that were attached to the HexaGongs themselves. This could potentially be simplified by storing the "outer HTML" of the HexaGongs rather than the "inner HTML" that is stored within the GongData "HexaGongs" array. Here, we're just having to repeat some of the code that is called when saving changes to a HexaGong after closing the Options dialog.

procedure TForm1.WebOpenDialog1GetFileAsText(Sender: TObject; AFileIndex: Integer; AText: string);
var
  i: Integer;
  GongHTML: String;
  GongCount: Integer;
  GongDeleted: Boolean;
begin
  GongCount := 0;
  GongHTML := '';
 
  asm
    function base64ToArrayBuffer(base64) {
      var binary_string =  window.atob(base64);
      var len = binary_string.length;
      var bytes = new Uint8Array( len );
      for (var i = 0; i < len; i++)        {
        bytes[i] = binary_string.charCodeAt(i);
      }
      return bytes.buffer;
    }

    var This = pas.Unit1.Form1;
    var Saved = JSON.parse(AText);

    if (Saved.SaveFormat == 'JSON') {
      This.ZoomLevel        = Saved.ZoomLevel;
      This.AnimatedElements = Saved.AnimatedElements;
      This.GongData         = JSON.parse(Saved.GongData);
      This.PositionsR       = JSON.parse(Saved.PositionsR);           
      This.PositionsC       = JSON.parse(Saved.PositionsC);           
      This.PositionsG       = JSON.parse(Saved.PositionsG);           
      This.GongsP           = JSON.parse(Saved.GongsP);         
      var AudioStrings      = JSON.parse(Saved.Audio);

      for (var i = 0; i < AudioStrings.length; i++) {
        if ((AudioStrings[i] !== null) && (AudioStrings[i] !== undefined)) {
          var decoder = new aas.Decoder();
          This.GongAudio[i] = decoder.execute(base64ToArrayBuffer(AudioStrings[i]));
        }
      }

      GongCount = This.GongData['HexaGongs'].length;
    }
  end;

  setLength(Gongs, GongCount);
  i := 0;
  while i < length(Gongs) do
  begin
    GongDeleted := False;
    asm
      GongHTML = pas.Unit1.Form1.GongData['HexaGongs'][i]['Image Data'];
      GongDeleted = pas.Unit1.Form1.GongData['HexaGongs'][i]['Deleted'];
    end;

    if not(GongDeleted) then
    begin
      Gongs[i] := TWebHTMLDiv.Create('Gong-'+IntToStr(i));
      Gongs[i].Parent := divButtons;
      Gongs[i].ElementFont := efCSS;
      Gongs[i].ElementPosition := epAbsolute;
      Gongs[i].ElementHandle.setAttribute('gongid',IntToStr(i));
      Gongs[i].ElementHandle.classList.Add('Gong','d-flex','justify-content-center','align-items-center','text-white');
      Gongs[i].ElementHandle.style.setProperty('z-index','10');
      Gongs[i].ElementHandle.setAttribute('position',IntToStr(GongsP[i]));
      Gongs[i].ElementHandle.setAttribute('row',IntToStr(PositionsR[GongsP[i]]));
      Gongs[i].ElementHandle.setAttribute('column',IntToStr(PositionsC[GongsP[i]]));
      Gongs[i].HTML.Text := GongHTML;

      asm
    
        var This = pas.Unit1.Form1;
        var gong = document.getElementById('Gong-'+i).firstElementChild;
      
        This.ImageW = This.GongData['HexaGongs'][i]['Image W'];
        This.ImageH = This.GongData['HexaGongs'][i]['Image H'];
        This.ImageT = This.GongData['HexaGongs'][i]['Image T'];
        This.ImageL = This.GongData['HexaGongs'][i]['Image L'];
        This.ImageX = This.GongData['HexaGongs'][i]['Image X'];
        This.ImageY = This.GongData['HexaGongs'][i]['Image Y'];
        This.ImageR = This.GongData['HexaGongs'][i]['Image R'];
        This.ImageO = This.GongData['HexaGongs'][i]['Image O'];

        This.OptionsBGStyle = parseInt(This.GongData['HexaGongs'][i]['BG Style']);
        This.OptionsBGColor1 = This.GongData['HexaGongs'][i]['BG Color 1'];
        This.OptionsBGColor2 = This.GongData['HexaGongs'][i]['BG Color 2'];
        This.OptionsBGCustom = This.GongData['HexaGongs'][i]['BG Custom'];

        This.OptionsAudioStyle = parseInt(This.GongData['HexaGongs'][i]['Audio Style']);
        This.OptionsAudioTime = parseFloat(This.GongData['HexaGongs'][i]['Audio Time']);
        This.OptionsAudioFile = This.GongData['HexaGongs'][i]['Audio File'];
        This.OptionsAudioGain = parseInt(This.GongData['HexaGongs'][i]['Audio Gain']);
        This.OptionsAudioStart = parseInt(This.GongData['HexaGongs'][i]['Audio Start']);
        This.OptionsAudioEnd = parseInt(This.GongData['HexaGongs'][i]['Audio End']);
        This.OptionsAudioSets = This.GongData['HexaGongs'][i]['Audio Sets'];
        This.OptionsAudioSetStyle = This.GongData['HexaGongs'][i]['Audio Set Style'];
      
        gong.style.setProperty('transform',
          'translate('+(This.ImageL - 100)+'%,'+(This.ImageT - 100)+'%) '+
          'scale('+This.ImageW/100+','+This.ImageH/100+') '+
          'skew('+(-This.ImageX)+'deg,'+(This.ImageY)+'deg) '+
          'rotate('+This.ImageR+'deg) ');
        gong.style.setProperty('opacity',This.ImageO / 100);

      
      end;

      // Update UI element - Background
      if OptionsBGStyle = 0
      then Gongs[i].ElementHandle.style.setProperty('background','radial-gradient(black,'+OptionsBGColor1+')')
      else if OptionsBGStyle = 1
      then Gongs[i].ElementHandle.style.setProperty('background','linear-gradient(60deg,black,'+OptionsBGColor1+')')
      else if OptionsBGStyle = 2
      then Gongs[i].ElementHandle.style.setProperty('background',OptionsBGColor1)
      else
      begin
        Gongs[GongID].ElementHandle.style.cssText := OptionsBGCustom;
      end;

      // Update UI element - Audio
      Gongs[i].ElementHandle.setAttribute('audiotime', FloatToStr(OptionsAudioTime));
      Gongs[i].ElementHandle.setAttribute('audiogain', IntToStr(OptionsAudioGain));
      Gongs[i].ElementHandle.setAttribute('audiostart', IntToStr(OptionsAudioStart));
      Gongs[i].ElementHandle.setAttribute('audioend', IntToStr(OptionsAudioEnd));
      if OptionsAudioStyle = 4 then
      begin
        Gongs[i].ElementHandle.setAttribute('audiosets',OptionsAudioSets);
        if OptionsAudioSetStyle = 0
        then Gongs[i].ElementHandle.setAttribute('audiosetstyle','series')
        else Gongs[i].ElementHandle.setAttribute('audiosetstyle','parallel');
      end
      else
      begin
        Gongs[i].ElementHandle.removeAttribute('audiosets');
        Gongs[i].ElementHandle.removeAttribute('audiosetstyle');
      end;
    end;

    i := i + 1;
  end;
    
  // Force a complete refresh
 
  btnMainClick(nil);
  StopAnimation;
  GeneratePositions;
  DrawBackground;
  StartAnimation;
end;

Once that has all been taken care of, we just need to refresh the page to have everything fall back into place. It is likely that with a bit of work, this code could be combined with the Options dialog "save" code, as they're both doing largely the same thing. It is also possible that the saved file sizes could be reduced by storing references to URLs rather than the encoded data (both for image and audio data). Compressing these files may be another avenue worth exploring, depending on how big they actually get for a given project. 

Deployment - TMS XData Project.

Deploying an XData project primarily involves running the generated executable file, found in the release folder, on a system that is at the very least accessible to the TMS WEB Core application that is accessing it. Back in this post, a bit of a deployment checklist was developed. Here's an updated version.

  1. Come up with a creative and catchy name for the project and register the domain name.
  2. Configure the domain registrar's nameserver to resolve the domain to the server (whether it is a VPS, a dedicated machine, or whatever else you might be using).
  3. Use Let's Encrypt or an equivalent service to acquire an SSL certificate for the domain. Fortunately, there are now free options for acquiring SSL certificates.
  4. Build the "Release" version of the XData project.
  5. Copy the Release folder contents to the server.
  6. Update or add a JSON configuration file to specify the base URL and port number to use, file paths, etc.
  7. Run the XData service application, and make accommodations for it to start after the server reboots.
  8. Add the Port to the Windows firewall or other firewall service.
  9. Use the TMS HTTPSConfig tool to reserve the IP address and connect the SSL certificate.
  10. Test that the XData server is running by accessing it remotely from another system.
  11. Test that the Swagger UI connection is working.
  12. Populate any supporting data folders, like icon-sets, audio-clips, etc.
Ultimately, you'll want to just check and make sure that the XData server is accessible only over SSL from wherever you are running the main TMS WEB Core app from. Most of the time this might be public-facing websites, but all of this could be used on an internal network or over a VPN just as easily, so be sure to test it with that in mind.

The JSON configuration file is not something that comes as part of the XData Application template, but we've included it in our project here, and in most of our other XData projects as well. Just a handy thing to have, as it allows us to change the IP address, ports, and whatever other parameters we're interested in configuring, without having to recompile the project or mess about with command-line parameters. Ideally, we'd have one configuration that we use in development and one for production (or testing if you're so inclined). Using the configuration.json mechanism makes this considerably easier to manage.

In particular, we want to be able to set the baseURL property for XData, which includes the port number and is what we need to have reserved with the TMS HTTPConfig Tool. If these don't all match, then our XData service is likely not going to be accessible, or worse, might not even start.

Deployment - TMS WEB Core Project.

Generally speaking, deploying a TMS WEB Core app to the web involves copying the contents of the "release" project folder over to a public-facing web server of some kind, such as Apache, IIS, or NGINX. There are no special requirements to use a given web server - no doubt most will work just fine. 

The main thing we have to do that might be a little different is that we've configured our app to use another (very different) configuration JSON file so that we can tell it where to find the corresponding XData server. This allows us to do our development work against one XData server, and then deploy the project to another XData server without having to change or recompile any of the source code as part of the deployment step. Here's a bit of a checklist for TMS WEB Core applications.

  1. All the above steps pertaining to registering a domain name and SSL certificates.
  2. Configure a web server, like Apache or NGINX. While XData is a standalone server application, TMS WEB Core projects need a separate web server to serve up its files.
  3. Setup a VirtualHost specific to the project, if hosting more than one project on this web server.
  4. Determine the document root for the VirtualHost or the web server generally, if not using a VirtualHost.
  5. Build the release version of the TMS WEB Core project
  6. Copy the contents of the TMS WEB Core TMSWeb/Release folder, and any and all subfolders, to the document root specified above.
  7. Update or add a JSON configuration file to specify the XData URL.
  8. Adjust the permissions for the files and subfolders in the document root folder.
  9. Normally, index.html is served up automatically, so we can copy Project.html to index.html, or create a link for Project.html called index.html, or adjust the web server configuration to point at Project.html. Doesn't matter which approach, we just want the Project.html file to be loaded without having to type it in.
  10. Test URLs, like www.example.com, http://example.com, http, https, and all combinations of these.
  11. Test on other platforms.

For step (7), much like XData, TMS WEB Core projects don't automatically use a JSON file, so we've set one up for this project. The main purpose in this case is to tell the project where to find the XData server. This could be expanded to include support for multiple XData servers - for load balancing or for finding the closest XData server. Other options could include specifying a HexaGong file to load from a remote server, or other default options.

For step (10), most web servers have some kind of mechanism for rewriting the URL. This might be to ensure that only the SSL version is used. Or perhaps to enforce the "www" prefix, or alternatively to ensure that it is never used. In Apache, for example, the www prefix and SSL can be enforced using something like this. There are many examples (and many variations!) on how to do this kind of thing for whatever web server you happen to be using.

  RewriteEngine on

  RewriteRule ^ - [E=protossl]
  RewriteCond %{HTTPS} on
  RewriteRule ^ - [E=protossl:s]
  RewriteCond %{HTTPS} !=on
  RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

  RewriteCond %{HTTP_HOST} .
  RewriteCond %{HTTP_HOST} !^www\. [NC]
  RewriteRule ^ https://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

The main challenges around deploying TMS WEB Core apps generally tend to revolve around testing different browsers and ensuring that the app works in all the environments that your visitors might be using. It is a good idea to test different browsers periodically, as often there are bizarre issues that arise. 

In our project here, for example, the range sliders that are part of the Image Adjustments section are intended to show a nice CSS linear-gradient. And they do, except for Firefox. No issues on Chrome for Windows/Linux or iPadOS, but Firefox doesn't seem to want to do that - it just displays a solid color instead. The range sliders that are part of the RGB color picker have the same problem, but the same CSS linear-gradients are used elsewhere in the Options dialog without issue. Something to do about shadowDOM elements? Hard to say. Another example is that recording audio on iOS seems to not work at the moment.

When sorting through issues, it might be that some items are just annoying (like the CSS linear-gradients) or maybe deal-breakers (like iOS audio recording) that require further investigation. Fortunately, if you run into problems, there is an excellent team standing by in the TMS Support Center that can likely answer your questions. Don't be shy!

All Done - For Now.

That about covers the basic implementation for the HexaGongs project. There are a few more items to sort through, but as far as something like an "MVP" (minimum viable product) I think we're well on our way here. 

What do you think? Is there a major feature that is missing that might make this the next killer app? Is something horribly broken beyond repair? Often when working on projects such as this, it is helpful to have another set of eyes to look things over, certainly, and provide a different perspective. 

Our main objective has been achieved, however, with hexagons permeating every aspect of our UI, from the simplest TWebEdit field all the way to a more complex multi-select-list type of component using Tabulator.

HexaGongs website.
HexaGongs repository on GitHub.
HexaGongs XData repository on GitHub.


Related Posts.

HexaGongs Part 1: Background
HexaGongs Part 2: Interface
HexaGongs Part 3: Options (Edits, Memos, Buttons, Colors, Trackbars)
HexaGongs Part 4: Options (Image and Audio)
HexaGongs Part 5: Deployment


Follow Andrew on 𝕏 at @WebCoreAndMore or join our 𝕏 Web Core and More Community.



Andrew Simard




This blog post has not received any comments yet.



Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post