github.com/swiftstack/proxyfs@v0.0.0-20201223034610-5434d919416e/httpserver/html_templates.go (about)

     1  package httpserver
     2  
     3  // To use: fmt.Sprintf(indexDotHTMLTemplate, proxyfsVersion, globals.ipAddrTCPPort)
     4  const indexDotHTMLTemplate string = `<!doctype html>
     5  <html lang="en">
     6    <head>
     7      <meta charset="utf-8">
     8      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     9      <link rel="stylesheet" href="/bootstrap.min.css">
    10      <link rel="stylesheet" href="/styles.css">
    11      <title>ProxyFS Management - %[2]v</title>
    12    </head>
    13    <body>
    14      <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
    15        <a class="navbar-brand" href="#">%[2]v</a>
    16        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
    17          <span class="navbar-toggler-icon"></span>
    18        </button>
    19        <div class="collapse navbar-collapse" id="navbarNavDropdown">
    20          <ul class="navbar-nav mr-auto">
    21            <li class="nav-item active">
    22              <a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a>
    23            </li>
    24            <li class="nav-item">
    25              <a class="nav-link" href="/config">Config</a>
    26            </li>
    27            <li class="nav-item">
    28              <a class="nav-link" href="/metrics">StatsD/Prometheus</a>
    29            </li>
    30            <li class="nav-item">
    31              <a class="nav-link" href="/trigger">Triggers</a>
    32            </li>
    33            <li class="nav-item">
    34              <a class="nav-link" href="/volume">Volumes</a>
    35            </li>
    36          </ul>
    37          <span class="navbar-text">Version %[1]v</span>
    38        </div>
    39      </nav>
    40      <div class="container">
    41        <nav aria-label="breadcrumb">
    42          <ol class="breadcrumb">
    43            <li class="breadcrumb-item active" aria-current="page">Home</li>
    44          </ol>
    45        </nav>
    46        <h1 class="display-4">
    47          ProxyFS Management
    48        </h1>
    49        <div class="card-deck">
    50          <div class="card mb-4">
    51            <div class="card-body">
    52              <h5 class="card-title">Configuration parameters</h5>
    53              <p class="card-text">Diplays a JSON representation of the active configuration.</p>
    54            </div>
    55            <ul class="list-group list-group-flush">
    56              <li class="list-group-item">
    57                <a href="/config" class="card-link">Configuration Parameters</a>
    58            </ul>
    59          </div>
    60          <div class="w-100 d-none d-sm-block d-md-none"><!-- wrap every 1 on sm--></div>
    61          <div class="card mb-4">
    62            <div class="card-body">
    63              <h5 class="card-title">StatsD/Prometheus</h5>
    64              <p class="card-text">Displays current statistics.</p>
    65            </div>
    66            <ul class="list-group list-group-flush">
    67              <li class="list-group-item">
    68                <a href="/metrics" class="card-link">StatsD/Prometheus Page</a>
    69            </ul>
    70          </div>
    71          <div class="w-100 d-none d-sm-block d-md-none"><!-- wrap every 1 on sm--></div>
    72          <div class="w-100 d-none d-md-block d-lg-none"><!-- wrap every 2 on md--></div>
    73          <div class="w-100 d-none d-lg-block d-xl-none"><!-- wrap every 2 on lg--></div>
    74          <div class="w-100 d-none d-xl-block"><!-- wrap every 3 on xl--></div>
    75          <div class="card mb-4">
    76            <div class="card-body">
    77              <h5 class="card-title">Triggers</h5>
    78              <p class="card-text">Manage triggers for simulating failures.</p>
    79            </div>
    80            <ul class="list-group list-group-flush">
    81              <li class="list-group-item">
    82                <a class="card-link" href="/trigger">Triggers Page</a>
    83              </li>
    84            </ul>
    85          </div>
    86          <div class="w-100 d-none d-sm-block d-md-none"><!-- wrap every 1 on sm--></div>
    87          <div class="card mb-4">
    88            <div class="card-body">
    89              <h5 class="card-title">Volumes</h5>
    90              <p class="card-text">Examine volumes currently active on this ProxyFS node.</p>
    91            </div>
    92            <ul class="list-group list-group-flush">
    93              <li class="list-group-item">
    94                <a href="/volume" class="card-link">Volume Page</a>
    95            </ul>
    96          </div>
    97        </div>
    98      </div>
    99      <script src="/jquery.min.js"></script>
   100      <script src="/popper.min.js"></script>
   101      <script src="/bootstrap.min.js"></script>
   102    </body>
   103  </html>
   104  `
   105  
   106  // To use: fmt.Sprintf(configTemplate, proxyfsVersion, globals.ipAddrTCPPort, confMapJSONString)
   107  const configTemplate string = `<!doctype html>
   108  <html lang="en">
   109    <head>
   110      <meta charset="utf-8">
   111      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
   112      <link rel="stylesheet" href="/bootstrap.min.css">
   113      <link rel="stylesheet" href="/styles.css">
   114      <title>Config - %[2]v</title>
   115    </head>
   116    <body>
   117      <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
   118        <a class="navbar-brand" href="#">%[2]v</a>
   119        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
   120          <span class="navbar-toggler-icon"></span>
   121        </button>
   122        <div class="collapse navbar-collapse" id="navbarNavDropdown">
   123          <ul class="navbar-nav mr-auto">
   124            <li class="nav-item">
   125              <a class="nav-link" href="/">Home</a>
   126            </li>
   127            <li class="nav-item active">
   128              <a class="nav-link" href="/config">Config <span class="sr-only">(current)</span></a>
   129            </li>
   130            <li class="nav-item">
   131              <a class="nav-link" href="/metrics">StatsD/Prometheus</a>
   132            </li>
   133            <li class="nav-item">
   134              <a class="nav-link" href="/trigger">Triggers</a>
   135            </li>
   136            <li class="nav-item">
   137              <a class="nav-link" href="/volume">Volumes</a>
   138            </li>
   139          </ul>
   140          <span class="navbar-text">Version %[1]v</span>
   141        </div>
   142      </nav>
   143      <div class="container">
   144        <nav aria-label="breadcrumb">
   145          <ol class="breadcrumb">
   146            <li class="breadcrumb-item"><a href="/">Home</a></li>
   147            <li class="breadcrumb-item active" aria-current="page">Config</li>
   148          </ol>
   149        </nav>
   150        <h1 class="display-4">
   151          Config
   152        </h1>
   153        <pre class="code" id="json_data"></pre>
   154      </div>
   155      <script src="/jquery.min.js"></script>
   156      <script src="/popper.min.js"></script>
   157      <script src="/bootstrap.min.js"></script>
   158      <script src="/jsontree.js"></script>
   159      <script type="text/javascript">
   160        var json_data = %[3]v;
   161        document.getElementById("json_data").innerHTML = JSONTree.create(json_data, null, 1);
   162        JSONTree.collapse();
   163      </script>
   164    </body>
   165  </html>
   166  `
   167  
   168  // To use: fmt.Sprintf(metricsTemplate, proxyfsVersion, globals.ipAddrTCPPort, metricsJSONString)
   169  const metricsTemplate string = `<!doctype html>
   170  <html lang="en">
   171    <head>
   172      <meta charset="utf-8">
   173      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
   174      <link rel="stylesheet" href="/bootstrap.min.css">
   175      <link rel="stylesheet" href="/styles.css">
   176      <title>Metrics - %[2]v</title>
   177    </head>
   178    <body>
   179      <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
   180        <a class="navbar-brand" href="#">%[2]v</a>
   181        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
   182          <span class="navbar-toggler-icon"></span>
   183        </button>
   184        <div class="collapse navbar-collapse" id="navbarNavDropdown">
   185          <ul class="navbar-nav mr-auto">
   186            <li class="nav-item">
   187              <a class="nav-link" href="/">Home</a>
   188            </li>
   189            <li class="nav-item">
   190              <a class="nav-link" href="/config">Config</a>
   191            </li>
   192            <li class="nav-item active">
   193              <a class="nav-link" href="/metrics">StatsD/Prometheus <span class="sr-only">(current)</span></a>
   194            </li>
   195            <li class="nav-item">
   196              <a class="nav-link" href="/trigger">Triggers</a>
   197            </li>
   198            <li class="nav-item">
   199              <a class="nav-link" href="/volume">Volumes</a>
   200            </li>
   201          </ul>
   202          <span class="navbar-text">Version %[1]v</span>
   203        </div>
   204      </nav>
   205      <div class="container">
   206        <nav aria-label="breadcrumb">
   207          <ol class="breadcrumb">
   208            <li class="breadcrumb-item"><a href="/">Home</a></li>
   209            <li class="breadcrumb-item active" aria-current="page">StatsD/Prometheus</li>
   210          </ol>
   211        </nav>
   212        <h1 class="display-4">StatsD/Prometheus</h1>
   213        <div class="text-center">
   214          <div class="btn-group btn-group-toggle" data-toggle="buttons" id="tab-bar"></div>
   215        </div>
   216        <br>
   217        <table class="table table-sm table-striped table-hover">
   218          <tbody id="metrics-data"></tbody>
   219        </table>
   220      </div>
   221      <script src="/jquery.min.js"></script>
   222      <script src="/popper.min.js"></script>
   223      <script src="/bootstrap.min.js"></script>
   224      <script type="text/javascript">
   225        var json_data = %[3]v;
   226        var getPrefixes = function(data, levels) {
   227          var prefixes = new Set();
   228          for (var key in data) {
   229            prefixes.add(key.split("_", levels).join("_"));
   230          }
   231          return Array.from(prefixes);
   232        };
   233        var getTabBarButtonMarkup = function(prefix, text, active) {
   234          var button_markup = "";
   235          button_markup += "          <label class=\"btn btn-sm btn-primary" + (active ? " active" : "") + "\" onclick=\"newPrefixSelected('" + prefix + "');\">\n";
   236          button_markup += "            <input type=\"radio\" name=\"options\" id=\"option-" + prefix + "\"> " + text + "\n";
   237          button_markup += "          </label>\n";
   238          return button_markup;
   239        };
   240        var buildTabBarWithPrefixes = function(tab_bar_id, prefixes, selected_prefix) {
   241          var tab_bar_markup = "";
   242          tab_bar_markup += getTabBarButtonMarkup("", "All", selected_prefix == "");
   243          var prefixes_length = prefixes.length;
   244          for (var i = 0; i < prefixes_length; i++) {
   245            var prefix = prefixes[i];
   246            tab_bar_markup += getTabBarButtonMarkup(prefix, prefix, selected_prefix == prefix);
   247          }
   248          document.getElementById(tab_bar_id).innerHTML = tab_bar_markup;
   249        };
   250        var filterDataByPrefix = function(data, prefix) {
   251          var prefix_to_search = prefix;
   252          if (prefix_to_search !== "") {
   253            prefix_to_search += "_";
   254          }
   255          var filtered = {};
   256          for (var key in data) {
   257            if (key.startsWith(prefix_to_search)) {
   258              filtered[key] = data[key];
   259            }
   260          }
   261          return filtered;
   262        };
   263        var getTableMarkupWithData = function(data) {
   264          var table_markup = "";
   265          for (var key in data) {
   266            table_markup += "          <tr>\n";
   267            table_markup += "            <th scope=\"row\">" + key + "</th>\n";
   268            table_markup += "            <td class=\"text-right\"><pre class=\"no-margin\">" + data[key] + "</pre></td>\n";
   269            table_markup += "          </tr>\n";
   270          }
   271          return table_markup;
   272        };
   273        var newPrefixSelected = function(prefix) {
   274          var filteredData = filterDataByPrefix(json_data, prefix);
   275          document.getElementById("metrics-data").innerHTML = getTableMarkupWithData(filteredData);
   276          window.location.hash = prefix;
   277        };
   278        var prefixes = getPrefixes(json_data, 2);
   279        var anchor = window.location.hash;
   280        if (anchor !== "") {
   281          anchor = anchor.substr(1);
   282          if (prefixes.indexOf(anchor) == -1) {
   283            anchor = "";
   284          }
   285        }
   286        buildTabBarWithPrefixes("tab-bar", prefixes, anchor);
   287        newPrefixSelected(anchor);
   288      </script>
   289    </body>
   290  </html>
   291  `
   292  
   293  // To use: fmt.Sprintf(volumeListTopTemplate, proxyfsVersion, globals.ipAddrTCPPort)
   294  const volumeListTopTemplate string = `<!doctype html>
   295  <html lang="en">
   296    <head>
   297      <meta charset="utf-8">
   298      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
   299      <link rel="stylesheet" href="/bootstrap.min.css">
   300      <link rel="stylesheet" href="/styles.css">
   301      <title>Volumes - %[2]v</title>
   302    </head>
   303    <body>
   304      <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
   305        <a class="navbar-brand" href="#">%[2]v</a>
   306        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
   307          <span class="navbar-toggler-icon"></span>
   308        </button>
   309        <div class="collapse navbar-collapse" id="navbarNavDropdown">
   310          <ul class="navbar-nav mr-auto">
   311            <li class="nav-item">
   312              <a class="nav-link" href="/">Home</a>
   313            </li>
   314            <li class="nav-item">
   315              <a class="nav-link" href="/config">Config</a>
   316            </li>
   317            <li class="nav-item">
   318              <a class="nav-link" href="/metrics">StatsD/Prometheus</a>
   319            </li>
   320            <li class="nav-item">
   321              <a class="nav-link" href="/trigger">Triggers</a>
   322            </li>
   323            <li class="nav-item active">
   324              <a class="nav-link" href="/volume">Volumes <span class="sr-only">(current)</span></a>
   325            </li>
   326          </ul>
   327          <span class="navbar-text">Version %[1]v</span>
   328        </div>
   329      </nav>
   330      <div class="container">
   331        <nav aria-label="breadcrumb">
   332          <ol class="breadcrumb">
   333            <li class="breadcrumb-item"><a href="/">Home</a></li>
   334            <li class="breadcrumb-item active" aria-current="page">Volumes</li>
   335          </ol>
   336        </nav>
   337  
   338        <h1 class="display-4">Volumes</h1>
   339        <table class="table table-sm table-striped table-hover">
   340          <thead>
   341            <tr>
   342              <th scope="col">Volume Name</th>
   343              <th class="fit">&nbsp;</th>
   344              <th class="fit">&nbsp;</th>
   345              <th class="fit">&nbsp;</th>
   346              <th class="fit">&nbsp;</th>
   347              <th class="fit">&nbsp;</th>
   348            </tr>
   349          </thead>
   350          <tbody>
   351  `
   352  
   353  // To use: fmt.Sprintf(volumeListPerVolumeTemplate, volumeName)
   354  const volumeListPerVolumeTemplate string = `          <tr>
   355              <td>%[1]v</td>
   356              <td class="fit"><a href="/volume/%[1]v/snapshot" class="btn btn-sm btn-primary">SnapShots</a></td>
   357              <td class="fit"><a href="/volume/%[1]v/fsck-job" class="btn btn-sm btn-primary">FSCK jobs</a></td>
   358              <td class="fit"><a href="/volume/%[1]v/scrub-job" class="btn btn-sm btn-primary">SCRUB jobs</a></td>
   359              <td class="fit"><a href="/volume/%[1]v/layout-report" class="btn btn-sm btn-primary">Layout Report</a></td>
   360              <td class="fit"><a href="/volume/%[1]v/extent-map" class="btn btn-sm btn-primary">Extent Map</a></td>
   361            </tr>
   362  `
   363  
   364  const volumeListBottom string = `        </tbody>
   365        </table>
   366      </div>
   367      <script src="/jquery.min.js"></script>
   368      <script src="/popper.min.js"></script>
   369      <script src="/bootstrap.min.js"></script>
   370    </body>
   371  </html>
   372  `
   373  
   374  // To use: fmt.Sprintf(snapShotsTopTemplate, proxyfsVersion, globals.ipAddrTCPPort, volumeName)
   375  const snapShotsTopTemplate string = `<!doctype html>
   376  <html lang="en">
   377    <head>
   378      <meta charset="utf-8">
   379      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
   380      <link rel="stylesheet" href="/bootstrap.min.css">
   381      <link rel="stylesheet" href="/styles.css">
   382      <link href="/open-iconic/font/css/open-iconic-bootstrap.min.css" rel="stylesheet">
   383      <title>%[2]v SnapShots - %[2]v</title>
   384    </head>
   385    <body>
   386      <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
   387        <a class="navbar-brand" href="#">%[2]v</a>
   388        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
   389          <span class="navbar-toggler-icon"></span>
   390        </button>
   391        <div class="collapse navbar-collapse" id="navbarNavDropdown">
   392          <ul class="navbar-nav mr-auto">
   393            <li class="nav-item">
   394              <a class="nav-link" href="/">Home</a>
   395            </li>
   396            <li class="nav-item">
   397              <a class="nav-link" href="/config">Config</a>
   398            </li>
   399            <li class="nav-item">
   400              <a class="nav-link" href="/metrics">StatsD/Prometheus</a>
   401            </li>
   402            <li class="nav-item">
   403              <a class="nav-link" href="/trigger">Triggers</a>
   404            </li>
   405            <li class="nav-item active">
   406              <a class="nav-link" href="/volume">Volumes <span class="sr-only">(current)</span></a>
   407            </li>
   408          </ul>
   409          <span class="navbar-text">Version %[1]v</span>
   410        </div>
   411      </nav>
   412      <div class="container">
   413        <nav aria-label="breadcrumb">
   414          <ol class="breadcrumb">
   415            <li class="breadcrumb-item"><a href="/">Home</a></li>
   416            <li class="breadcrumb-item"><a href="/volume">Volumes</a></li>
   417            <li class="breadcrumb-item active" aria-current="page">SnapShots %[3]v</li>
   418          </ol>
   419        </nav>
   420        <div id="alert-area"></div>
   421        <h1 class="display-4">
   422          SnapShots
   423          <small class="text-muted">%[3]v</small>
   424        </h1>
   425        <form class="float-right" onsubmit="return createSnapShot();">
   426          <div class="input-group mb-3">
   427            <input type="text" name="name" class="form-control form-control-sm mb-2" id="new-snapshot-name" placeholder="New snapshot name" aria-label="Name" autofocus="autofocus">
   428            <div class="input-group-append">
   429              <button type="submit" class="btn btn-sm btn-primary mb-2">Create snapshot</span></button>
   430            </div>
   431          </div>
   432        </form>
   433        <table class="table table-sm table-striped table-hover">
   434          <thead>
   435            <tr>
   436              <th scope="col" id="header-id" class="fit clickable">ID</th>
   437              <th scope="col" id="header-timestamp" class="w-25 clickable">Time</th>
   438              <th scope="col" id="header-name" class="clickable">Name</th>
   439              <th class="fit">&nbsp;</th>
   440            </tr>
   441          </thead>
   442          <tbody>
   443  `
   444  
   445  // To use: fmt.Sprintf(snapShotsPerSnapShotTemplate, id, timeStamp.Format(time.RFC3339), name)
   446  const snapShotsPerSnapShotTemplate string = `          <tr>
   447              <td>%[1]v</td>
   448              <td>%[2]v</td>
   449              <td>%[3]v</td>
   450              <td class="fit"><a href="#" class="btn btn-sm btn-danger" onclick="deleteSnapShot(%[1]v);"><span class="oi oi-trash" title="Delete" aria-hidden="true"></a></td>
   451            </tr>
   452  `
   453  
   454  // To use: fmt.Sprintf(snapShotsBottomTemplate, volumeName)
   455  const snapShotsBottomTemplate string = `        </tbody>
   456        </table>
   457        <br />
   458      </div>
   459      <script src="/jquery.min.js"></script>
   460      <script src="/popper.min.js"></script>
   461      <script src="/bootstrap.min.js"></script>
   462      <script type="text/javascript">
   463        volumeName = "%[1]v";
   464        hideAlert = function() {
   465          document.getElementById('alert-area').innerHTML = '';
   466        };
   467        showAlertWithMsg = function(msg) {
   468          var html = '<div class="alert alert-danger alert-dismissible fade show" role="alert">\n';
   469          html += '  ' + msg + '\n';
   470          html += '  <button type="button" class="close" data-dismiss="alert" aria-label="Close">';
   471          html += '    <span aria-hidden="true">&times;</span>\n';
   472          html += '  </button>\n';
   473          html += '</div>\n';
   474          document.getElementById('alert-area').innerHTML = html;
   475        };
   476        showDeleteError = function(id, jqXHR, textStatus, errorThrown) {
   477          var msg = 'Error deleting snapshot with ID <em>' + id + '</em>: ' + jqXHR.status + ' ' + jqXHR.statusText;
   478          showAlertWithMsg(msg);
   479        };
   480        showCreateError = function(name, jqXHR, textStatus, errorThrown) {
   481          var msg = 'Error creating snapshot with name <em>' + name + '</em>: ' + jqXHR.status + ' ' + jqXHR.statusText;
   482          showAlertWithMsg(msg);
   483        };
   484        deleteSnapShot = function(id) {
   485          hideAlert();
   486          var url = '/volume/' + volumeName + '/snapshot/' + id;
   487          $.ajax({
   488            url: url,
   489            method: 'DELETE',
   490            dataType: 'json',
   491            success: function(data, textStatus, jqXHR) {
   492              location.reload();
   493            },
   494            error: function(jqXHR, textStatus, errorThrown) {
   495              showDeleteError(id, jqXHR, textStatus, errorThrown);
   496            }
   497          });
   498        };
   499        createSnapShot = function() {
   500          hideAlert();
   501          document.getElementById('new-snapshot-name').select();
   502          var new_snapshot_name = $.trim($('#new-snapshot-name').val());
   503          if (new_snapshot_name == "") {
   504            showAlertWithMsg("SnapShot name can't be blank.");
   505            return false;
   506          }
   507          var url = '/volume/' + volumeName + '/snapshot/';
   508          $.ajax({
   509            url: url,
   510            method: 'POST',
   511            data: {'name': new_snapshot_name},
   512            success: function(data, textStatus, jqXHR) {
   513              location.reload();
   514            },
   515            error: function(jqXHR, textStatus, errorThrown) {
   516              showCreateError(new_snapshot_name, jqXHR, textStatus, errorThrown);
   517            }
   518          });
   519          return false;
   520        };
   521        getQueryVariable = function(variable) {
   522          var query = window.location.search.substring(1);
   523          var vars = query.split('&');
   524          for (var i = 0; i < vars.length; i++) {
   525            var pair = vars[i].split('=');
   526            if (decodeURIComponent(pair[0]) == variable) {
   527              return decodeURIComponent(pair[1]);
   528            }
   529          }
   530          return null;
   531        }
   532        getQueryVariableOrDefault = function(variable, default_value, to_lower, allowed_values) {
   533          var value = getQueryVariable(variable);
   534          if (value === null) {
   535            value = default_value;
   536          } else if (to_lower) {
   537            value = value.toLowerCase();
   538          }
   539          // allowed_values is optional
   540          if (typeof allowed_values !== 'undefined' && allowed_values.indexOf(value) == -1) {
   541            value = default_value;
   542          }
   543          return value;
   544        };
   545        getHeaderIdForField = function(field) {
   546          return 'header-' + field;
   547        };
   548        addCaretToElement = function(element, direction) {
   549          if (direction == 'desc') {
   550            element.insertAdjacentHTML('beforeend', ' <span class="oi oi-chevron-bottom"></span>');
   551          } else if (direction == 'asc') {
   552            element.insertAdjacentHTML('beforeend', ' <span class="oi oi-chevron-top"></span>');
   553          }
   554        };
   555        var orderby_default = 'timestamp';
   556        var orderby_allowed_values = ['id', 'timestamp', 'name'];
   557        var direction_default = 'asc';
   558        var direction_allowed_values = ['asc', 'desc'];
   559        displaySortingCaret = function() {
   560          var orderby = getQueryVariableOrDefault('orderby', orderby_default, true, orderby_allowed_values);
   561          var direction = getQueryVariableOrDefault('direction', direction_default, true, direction_allowed_values);
   562          var header = document.getElementById(getHeaderIdForField(orderby));
   563          if (header === null) {
   564            console.error("Could not get element by id: " + getHeaderIdForField(orderby));
   565            return {'orderby': null, 'direction': null};
   566          }
   567          addCaretToElement(header, direction);
   568          return {'orderby': orderby, 'direction': direction};
   569        };
   570        var current_sorting = displaySortingCaret();
   571        for (let i = orderby_allowed_values.length - 1; i >= 0; i--) {
   572          var id = getHeaderIdForField(orderby_allowed_values[i]);
   573          $("#" + id).on("click", function(){
   574            var new_order_by = orderby_allowed_values[i];
   575            var current_order_by = current_sorting['orderby'];
   576            if (new_order_by == current_order_by) {
   577              if (current_sorting['direction'] == 'asc') {
   578                var new_direction = 'desc';
   579              } else {
   580                var new_direction = 'asc';
   581              }
   582            } else {
   583              var new_direction = direction_default;
   584            }
   585            window.location = window.location.origin + window.location.pathname + "?orderby=" + new_order_by + "&direction=" + new_direction;
   586          });
   587        }
   588      </script>
   589    </body>
   590  </html>
   591  `
   592  
   593  // To use: fmt.Sprintf(jobsTopTemplate, proxyfsVersion, globals.ipAddrTCPPort, volumeName, {"FSCK"|"SCRUB"})
   594  const jobsTopTemplate string = `<!doctype html>
   595  <html lang="en">
   596    <head>
   597      <meta charset="utf-8">
   598      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
   599      <link rel="stylesheet" href="/bootstrap.min.css">
   600      <link rel="stylesheet" href="/styles.css">
   601      <title>%[4]v Jobs %[3]v - %[2]v</title>
   602    </head>
   603    <body>
   604      <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
   605        <a class="navbar-brand" href="#">%[2]v</a>
   606        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
   607          <span class="navbar-toggler-icon"></span>
   608        </button>
   609        <div class="collapse navbar-collapse" id="navbarNavDropdown">
   610          <ul class="navbar-nav mr-auto">
   611            <li class="nav-item">
   612              <a class="nav-link" href="/">Home</a>
   613            </li>
   614            <li class="nav-item">
   615              <a class="nav-link" href="/config">Config</a>
   616            </li>
   617            <li class="nav-item">
   618              <a class="nav-link" href="/metrics">StatsD/Prometheus</a>
   619            </li>
   620            <li class="nav-item">
   621              <a class="nav-link" href="/trigger">Triggers</a>
   622            </li>
   623            <li class="nav-item active">
   624              <a class="nav-link" href="/volume">Volumes <span class="sr-only">(current)</span></a>
   625            </li>
   626          </ul>
   627          <span class="navbar-text">Version %[1]v</span>
   628        </div>
   629      </nav>
   630      <div class="container">
   631        <nav aria-label="breadcrumb">
   632          <ol class="breadcrumb">
   633            <li class="breadcrumb-item"><a href="/">Home</a></li>
   634            <li class="breadcrumb-item"><a href="/volume">Volumes</a></li>
   635            <li class="breadcrumb-item active" aria-current="page">%[4]v Jobs %[3]v</li>
   636          </ol>
   637        </nav>
   638        <h1 class="display-4">
   639          %[4]v Jobs
   640          <small class="text-muted">%[3]v</small>
   641        </h1>
   642        <table class="table table-sm table-striped table-hover">
   643          <thead>
   644            <tr>
   645              <th scope="col">Job ID</th>
   646              <th>Start Time</th>
   647              <th>End Time</th>
   648              <th>Status</th>
   649              <th class="fit">&nbsp;</th>
   650            </tr>
   651          </thead>
   652          <tbody>
   653  `
   654  
   655  // To use: fmt.Sprintf(jobsPerRunningJobTemplate, jobID, job.startTime.Format(time.RFC3339), volumeName, {"fsck"|"scrub"})
   656  const jobsPerRunningJobTemplate string = `          <tr>
   657              <td>%[1]v</td>
   658              <td>%[2]v</td>
   659              <td></td>
   660              <td>Running</td>
   661              <td class="fit"><a href="/volume/%[3]v/%[4]v-job/%[1]v" class="btn btn-sm btn-primary">View</a></td>
   662            </tr>
   663  `
   664  
   665  // To use: fmt.Sprintf(jobsPerHaltedJobTemplate, jobID, job.startTime.Format(time.RFC3339), job.endTime.Format(time.RFC3339), volumeName, {"fsck"|"scrub"})
   666  const jobsPerHaltedJobTemplate string = `          <tr class="table-info">
   667              <td>%[1]v</td>
   668              <td>%[2]v</td>
   669              <td>%[3]v</td>
   670              <td>Halted</td>
   671              <td class="fit"><a href="/volume/%[4]v/%[5]v-job/%[1]v" class="btn btn-sm btn-primary">View</a></td>
   672            </tr>
   673  `
   674  
   675  // To use: fmt.Sprintf(jobsPerSuccessfulJobTemplate, jobID, job.startTime.Format(time.RFC3339), job.endTime.Format(time.RFC3339), volumeName, {"fsck"|"scrub"})
   676  const jobsPerSuccessfulJobTemplate string = `          <tr class="table-success">
   677              <td>%[1]v</td>
   678              <td>%[2]v</td>
   679              <td>%[3]v</td>
   680              <td>Successful</td>
   681              <td class="fit"><a href="/volume/%[4]v/%[5]v-job/%[1]v" class="btn btn-sm btn-primary">View</a></td>
   682            </tr>
   683  `
   684  
   685  // To use: fmt.Sprintf(jobsPerFailedJobTemplate, jobID, job.startTime.Format(time.RFC3339), job.endTime.Format(time.RFC3339), volumeName, {"fsck"|"scrub"})
   686  const jobsPerFailedJobTemplate string = `          <tr class="table-danger">
   687              <td>%[1]v</td>
   688              <td>%[2]v</td>
   689              <td>%[3]v</td>
   690              <td>Failed</td>
   691              <td class="fit"><a href="/volume/%[4]v/%[5]v-job/%[1]v" class="btn btn-sm btn-primary">View</a></td>
   692            </tr>
   693  `
   694  
   695  const jobsListBottom string = `        </tbody>
   696        </table>
   697      <br />
   698  `
   699  
   700  // To use: fmt.Sprintf(jobsStartJobButtonTemplate, volumeName, {"fsck"|"scrub"})
   701  const jobsStartJobButtonTemplate string = `    <form method="post" action="/volume/%[1]v/%[2]v-job">
   702        <input type="submit" value="Start new job" class="btn btn-sm btn-primary">
   703      </form>
   704  `
   705  
   706  const jobsBottom string = `    <script src="/jquery.min.js"></script>
   707      <script src="/popper.min.js"></script>
   708      <script src="/bootstrap.min.js"></script>
   709    </body>
   710  </html>
   711  `
   712  
   713  // To use: fmt.Sprintf(jobTemplate, proxyfsVersion, globals.ipAddrTCPPort, volumeName, {"FSCK"|"SCRUB"}, {"fsck"|"scrub"}, jobID, jobStatusJSONString)
   714  const jobTemplate string = `<!doctype html>
   715  <html lang="en">
   716    <head>
   717      <meta charset="utf-8">
   718      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
   719      <link rel="stylesheet" href="/bootstrap.min.css">
   720      <link rel="stylesheet" href="/styles.css">
   721      <title>%[6]v %[4]v Job - %[2]v</title>
   722    </head>
   723    <body>
   724      <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
   725        <a class="navbar-brand" href="#">%[2]v</a>
   726        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
   727          <span class="navbar-toggler-icon"></span>
   728        </button>
   729        <div class="collapse navbar-collapse" id="navbarNavDropdown">
   730          <ul class="navbar-nav mr-auto">
   731            <li class="nav-item">
   732              <a class="nav-link" href="/">Home</a>
   733            </li>
   734            <li class="nav-item">
   735              <a class="nav-link" href="/config">Config</a>
   736            </li>
   737            <li class="nav-item">
   738              <a class="nav-link" href="/metrics">StatsD/Prometheus</a>
   739            </li>
   740            <li class="nav-item">
   741              <a class="nav-link" href="/trigger">Triggers</a>
   742            </li>
   743            <li class="nav-item active">
   744              <a class="nav-link" href="/volume">Volumes <span class="sr-only">(current)</span></a>
   745            </li>
   746          </ul>
   747          <span class="navbar-text">Version %[1]v</span>
   748        </div>
   749      </nav>
   750      <div class="container">
   751        <nav aria-label="breadcrumb">
   752          <ol class="breadcrumb">
   753            <li class="breadcrumb-item"><a href="/">Home</a></li>
   754            <li class="breadcrumb-item"><a href="/volume">Volumes</a></li>
   755            <li class="breadcrumb-item"><a href="/volume/%[3]v/%[5]v-job">%[4]v Jobs %[3]v</a></li>
   756            <li class="breadcrumb-item active" aria-current="page">%[6]v</li>
   757          </ol>
   758        </nav>
   759        <h1 class="display-4">
   760          %[4]v Job
   761          <small class="text-muted">%[6]v</small>
   762        </h1>
   763        <br>
   764        <dl class="row" id="job-info"></dl>
   765      </div>
   766      <script src="/jquery.min.js"></script>
   767      <script src="/popper.min.js"></script>
   768      <script src="/bootstrap.min.js"></script>
   769      <script type="text/javascript">
   770        var json_data = %[7]v;
   771        var getDescriptionListEntryMarkup = function(dt, dd) {
   772          var markup = "";
   773          markup += "         <dt class=\"col-sm-2\">\n";
   774          markup += "           " + dt + "\n";
   775          markup += "         </dt>\n";
   776          markup += "         <dd class=\"col-sm-10\">\n";
   777          markup += "           " + dd + "\n";
   778          markup += "         </dd>\n";
   779          return markup;
   780        };
   781        var getLogEntryContentsMarkup = function(log_entries, entries_type) {
   782          var log_entries_length = log_entries.length;
   783          if (log_entries_length == 0) {
   784            return "No " + entries_type;
   785          }
   786          var markup = "";
   787          var timestamp = "";
   788          var description = "";
   789          var timestamp_end_pos = 0;
   790          var entry = "";
   791          markup += "           <table class=\"table table-sm table-striped table-hover\">\n";
   792          for (var i = 0; i < log_entries_length; i++) {
   793            entry = log_entries[i];
   794            timestamp_end_pos = entry.indexOf(" ");
   795            timestamp = entry.slice(0, timestamp_end_pos);
   796            description = entry.slice(timestamp_end_pos);
   797            markup += "             <tr>\n";
   798            markup += "               <td class=\"fit align-text-top\">\n";
   799            markup += "                 <nobr>" + timestamp + "&nbsp;</nobr>\n";
   800            markup += "               </td>\n";
   801            markup += "               <td class=\"align-text-top\">\n";
   802            markup += "                 " + description + "\n";
   803            markup += "               </td>\n";
   804            markup += "             </tr>\n";
   805          }
   806          markup += "           </table>\n";
   807          return markup;
   808        };
   809        var job_info = "";
   810        if (json_data["halt time"] !== "") {
   811          var state = "Halted";
   812        } else if (json_data["done time"] !== "") {
   813          var state = "Completed";
   814        } else {
   815          var state = "Running";
   816        }
   817        job_info += getDescriptionListEntryMarkup("State", state);
   818        job_info += getDescriptionListEntryMarkup("Start time", json_data["start time"]);
   819        if (json_data["halt time"] !== "") {
   820          job_info += getDescriptionListEntryMarkup("Halt time", json_data["halt time"]);
   821        }
   822        if (json_data["done time"] !== "") {
   823          job_info += getDescriptionListEntryMarkup("Done time", json_data["done time"]);
   824        }
   825        job_info += getDescriptionListEntryMarkup("Errors", getLogEntryContentsMarkup(json_data["error list"], "errors"));
   826        job_info += getDescriptionListEntryMarkup("Info", getLogEntryContentsMarkup(json_data["info list"], "info"));
   827        document.getElementById("job-info").innerHTML = job_info;
   828      </script>
   829    </body>
   830  </html>
   831  `
   832  
   833  // To use: fmt.Sprintf(layoutReportTopTemplate, proxyfsVersion, globals.ipAddrTCPPort, volumeName)
   834  const layoutReportTopTemplate string = `<!doctype html>
   835  <html lang="en">
   836    <head>
   837      <meta charset="utf-8">
   838      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
   839      <link rel="stylesheet" href="/bootstrap.min.css">
   840      <link rel="stylesheet" href="/styles.css">
   841      <title>Layout Report %[3]v - %[2]v</title>
   842    </head>
   843    <body>
   844      <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
   845        <a class="navbar-brand" href="#">%[2]v</a>
   846        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
   847          <span class="navbar-toggler-icon"></span>
   848        </button>
   849        <div class="collapse navbar-collapse" id="navbarNavDropdown">
   850          <ul class="navbar-nav mr-auto">
   851            <li class="nav-item">
   852              <a class="nav-link" href="/">Home</a>
   853            </li>
   854            <li class="nav-item">
   855              <a class="nav-link" href="/config">Config</a>
   856            </li>
   857            <li class="nav-item">
   858              <a class="nav-link" href="/metrics">StatsD/Prometheus</a>
   859            </li>
   860            <li class="nav-item">
   861              <a class="nav-link" href="/trigger">Triggers</a>
   862            </li>
   863            <li class="nav-item active">
   864              <a class="nav-link" href="/volume">Volumes <span class="sr-only">(current)</span></a>
   865            </li>
   866          </ul>
   867          <span class="navbar-text">Version %[1]v</span>
   868        </div>
   869      </nav>
   870      <div class="container">
   871        <nav aria-label="breadcrumb">
   872          <ol class="breadcrumb">
   873            <li class="breadcrumb-item"><a href="/">Home</a></li>
   874            <li class="breadcrumb-item"><a href="/volume">Volumes</a></li>
   875            <li class="breadcrumb-item active" aria-current="page">Layout Report %[3]v</li>
   876          </ol>
   877        </nav>
   878        <h1 class="display-4">
   879          Layout Report
   880          <small class="text-muted">%[3]v</small>
   881        </h1>
   882        <a id="validate-button" class="btn btn-primary float-right" href="#">Show validated version</a><br><br>
   883  `
   884  
   885  // To use: fmt.Sprintf(layoutReportTableTopTemplate, TreeName, NumDiscrepencies, {"success"|"danger"})
   886  const layoutReportTableTopTemplate string = `      <br>
   887        <div class="d-flex justify-content-between">
   888          <h3>%[1]v</h3><h4><span class="badge badge-%[3]v d-none">%[2]v discrepancies</span></h4>
   889        </div>
   890  	    <table class="table table-sm table-striped table-hover">
   891          <thead>
   892            <tr>
   893              <th scope="col" class="w-50">ObjectName</th>
   894              <th scope="col" class="w-50">ObjectBytes</th>
   895            </tr>
   896          </thead>
   897          <tbody>
   898  `
   899  
   900  // To use: fmt.Sprintf(layoutReportTableRowTemplate, ObjectName, ObjectBytes)
   901  const layoutReportTableRowTemplate string = `          <tr>
   902              <td><pre class="no-margin">%016[1]X</pre></td>
   903  			      <td><pre class="no-margin">%[2]v</pre></td>
   904            </tr>
   905  `
   906  
   907  const layoutReportTableBottom string = `        </tbody>
   908        </table>
   909  `
   910  
   911  const layoutReportBottom string = `    <div>
   912      <script src="/jquery.min.js"></script>
   913      <script src="/popper.min.js"></script>
   914      <script src="/bootstrap.min.js"></script>
   915      <script type="text/javascript">
   916        var url_params = new URLSearchParams(window.location.search);
   917        var validate = false;
   918        var validate_button = document.getElementById('validate-button');
   919        if (
   920          url_params.has("validate") &&
   921          url_params.get("validate") != "0" &&
   922          url_params.get("validate").toLowerCase() != "false"
   923        ) {
   924          validate = true;
   925        }
   926        if (validate) {
   927          validate_button.innerHTML = 'Show non-validated version';
   928          validate_button.href = window.location.origin + window.location.pathname + "?validate=0";
   929          $("h4 .badge").removeClass("d-none");
   930        } else {
   931          validate_button.innerHTML = 'Show validated version';
   932          validate_button.href = window.location.origin + window.location.pathname + "?validate=1";
   933        }
   934      </script>
   935    </body>
   936  </html>
   937  `
   938  
   939  // To use: fmt.Sprintf(extentMapTemplate, proxyfsVersion, globals.ipAddrTCPPort, volumeName, extentMapJSONString, pathDoubleQuotedString, serverErrorBoolString)
   940  const extentMapTemplate string = `<!doctype html>
   941  <html lang="en">
   942    <head>
   943      <meta charset="utf-8">
   944      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
   945      <link rel="stylesheet" href="/bootstrap.min.css">
   946      <link rel="stylesheet" href="/styles.css">
   947      <title>Extent Map %[3]v - %[2]v</title>
   948    </head>
   949    <body>
   950      <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
   951        <a class="navbar-brand" href="#">%[2]v</a>
   952        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
   953          <span class="navbar-toggler-icon"></span>
   954        </button>
   955        <div class="collapse navbar-collapse" id="navbarNavDropdown">
   956          <ul class="navbar-nav mr-auto">
   957            <li class="nav-item">
   958              <a class="nav-link" href="/">Home</a>
   959            </li>
   960            <li class="nav-item">
   961              <a class="nav-link" href="/config">Config</a>
   962            </li>
   963            <li class="nav-item">
   964              <a class="nav-link" href="/metrics">StatsD/Prometheus</a>
   965            </li>
   966            <li class="nav-item">
   967              <a class="nav-link" href="/trigger">Triggers</a>
   968            </li>
   969            <li class="nav-item active">
   970              <a class="nav-link" href="/volume">Volumes <span class="sr-only">(current)</span></a>
   971            </li>
   972          </ul>
   973          <span class="navbar-text">Version %[1]v</span>
   974        </div>
   975      </nav>
   976      <div class="container">
   977        <nav aria-label="breadcrumb">
   978          <ol class="breadcrumb">
   979            <li class="breadcrumb-item"><a href="/">Home</a></li>
   980            <li class="breadcrumb-item"><a href="/volume">Volumes</a></li>
   981            <li class="breadcrumb-item active" aria-current="page">Extent Map %[3]v</li>
   982          </ol>
   983        </nav>
   984  
   985        <h1 class="display-4">
   986          Extent Map
   987          <small class="text-muted">%[3]v</small>
   988        </h1>
   989  
   990        <div class="alert alert-danger" id="error-message" role="alert"></div>
   991  
   992        <form id="new-path-form">
   993          <div class="input-group mb-3">
   994            <input type="text" id="path-text-box" class="form-control path-text-box" placeholder="path/to/check" aria-label="Path to check">
   995            <div class="input-group-append">
   996              <input type="submit" class="btn btn-primary" value="Search">
   997            </div>
   998          </div>
   999        </form>
  1000  
  1001        <br>
  1002        <table class="table table-sm table-striped table-hover" id="extent-map-table">
  1003          <thead>
  1004            <tr>
  1005              <th scope="col">File Offset</th>
  1006              <th scope="col" class="w-50">Container/Object</th>
  1007              <th scope="col">Object Offset</th>
  1008              <th scope="col">Length</th>
  1009            </tr>
  1010          </thead>
  1011          <tbody id="extent-map-data"></tbody>
  1012        </table>
  1013      </div>
  1014      <script src="/jquery.min.js"></script>
  1015      <script src="popper.min.js"></script>
  1016      <script src="/bootstrap.min.js"></script>
  1017      <script type="text/javascript">
  1018        var json_data = %[4]v
  1019        var path = %[5]v;
  1020        var volume = "%[3]v";
  1021        var server_error = %[6]v;
  1022  
  1023        $("#new-path-form").submit(function(e){
  1024          e.preventDefault();
  1025          var new_path = $("#path-text-box").val().trim();
  1026          if (new_path != "" && !new_path.startsWith("/")) {
  1027            new_path = "/" + new_path;
  1028          }
  1029          var new_url = "/volume/" + volume + "/extent-map" + new_path;
  1030          window.location = new_url;
  1031        });
  1032  
  1033        var hideError = function() {
  1034          document.getElementById("error-message").style.display = "none";
  1035        };
  1036  
  1037        var showPathError = function(path) {
  1038          var msg_to_print = "";
  1039          if (path !== null) {
  1040            msg_to_print = "<p>There was an error getting extent map for path:</p><pre>" + path + "</pre>";
  1041          } else {
  1042            msg_to_print = "<p>There was an error getting extent map for path:</p><pre>(error retrieving input path)</pre>";
  1043          }
  1044          showCustomError(msg_to_print);
  1045        };
  1046  
  1047        var showCustomError = function(text) {
  1048          document.getElementById("error-message").innerHTML = text;
  1049        };
  1050  
  1051        var getTableMarkupWithData = function(data) {
  1052          var table_markup = "";
  1053          for (var key in data) {
  1054            table_markup += "          <tr>\n";
  1055            table_markup += "            <td><pre class=\"no-margin\">" + data[key]["file_offset"] + "</pre></td>\n";
  1056            table_markup += "            <td><pre class=\"no-margin\">" + data[key]["container_name"] + "/" + data[key]["object_name"] + "</pre></td>\n";
  1057            table_markup += "            <td><pre class=\"no-margin\">" + data[key]["object_offset"] + "</pre></td>\n";
  1058            table_markup += "            <td><pre class=\"no-margin\">" + data[key]["length"] + "</pre></td>\n";
  1059            table_markup += "          </tr>\n";
  1060          }
  1061          return table_markup;
  1062        };
  1063  
  1064        var buildTable = function(data) {
  1065          document.getElementById("extent-map-data").innerHTML = getTableMarkupWithData(data);
  1066        };
  1067  
  1068        var hideTable = function() {
  1069          document.getElementById("extent-map-table").style.display = "none";
  1070        };
  1071  
  1072        var fillInTextBox = function(path) {
  1073          document.getElementById("path-text-box").value = path;
  1074        };
  1075  
  1076        var selectSearchText = function() {
  1077          var pat_text_box = document.getElementById("path-text-box");
  1078          pat_text_box.setSelectionRange(0, pat_text_box.value.length)
  1079        };
  1080  
  1081        if (server_error) {
  1082          // Error finding path
  1083          hideTable();
  1084          fillInTextBox(path);
  1085          showPathError(path);
  1086          selectSearchText();
  1087        } else if (json_data === null && path === null) {
  1088          // Empty form
  1089          hideTable();
  1090          hideError();
  1091          selectSearchText();
  1092        } else if (json_data === null || path === null) {
  1093          // This should never happen!
  1094          hideTable();
  1095          var msg_to_print = "<p>Oops, that's embarrassing... Something went wrong server side: ";
  1096          if (json_data === null) {
  1097            msg_to_print += "'json_data' is null, but 'path' is not:</p><pre>" + path + "</pre>";
  1098            fillInTextBox(path);
  1099          } else {
  1100            msg_to_print += "'path' is null, but 'json_data' is not:</p><pre>" + JSON.stringify(json_data, null, 2) + "</pre>";
  1101          }
  1102          showCustomError(msg_to_print);
  1103          selectSearchText();
  1104        } else {
  1105          // Path has been found
  1106          hideError();
  1107          fillInTextBox(path);
  1108          buildTable(json_data);
  1109          selectSearchText();
  1110        }
  1111      </script>
  1112    </body>
  1113  </html>
  1114  `
  1115  
  1116  // To use: fmt.Sprintf(triggerTopTemplate, proxyfsVersion, globals.ipAddrTCPPort)
  1117  const triggerTopTemplate string = `<!doctype html>
  1118  <html lang="en">
  1119    <head>
  1120      <meta charset="utf-8">
  1121      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  1122      <link rel="stylesheet" href="/bootstrap.min.css">
  1123      <link rel="stylesheet" href="/styles.css">
  1124      <title>Triggers - %[2]v</title>
  1125    </head>
  1126    <body>
  1127      <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
  1128        <a class="navbar-brand" href="#">%[2]v</a>
  1129        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
  1130          <span class="navbar-toggler-icon"></span>
  1131        </button>
  1132        <div class="collapse navbar-collapse" id="navbarNavDropdown">
  1133          <ul class="navbar-nav mr-auto">
  1134            <li class="nav-item">
  1135              <a class="nav-link" href="/">Home</a>
  1136            </li>
  1137            <li class="nav-item">
  1138              <a class="nav-link" href="/config">Config</a>
  1139            </li>
  1140            <li class="nav-item">
  1141              <a class="nav-link" href="/metrics">StatsD/Prometheus</a>
  1142            </li>
  1143            <li class="nav-item active">
  1144              <a class="nav-link" href="/trigger">Triggers <span class="sr-only">(current)</span></a>
  1145            </li>
  1146            <li class="nav-item">
  1147              <a class="nav-link" href="/volume">Volumes</a>
  1148            </li>
  1149          </ul>
  1150          <span class="navbar-text">Version %[1]v</span>
  1151        </div>
  1152      </nav>
  1153      <div class="container">
  1154        <nav aria-label="breadcrumb">
  1155          <ol class="breadcrumb">
  1156            <li class="breadcrumb-item"><a href="/">Home</a></li>
  1157            <li class="breadcrumb-item active" aria-current="page">Triggers</li>
  1158          </ol>
  1159        </nav>
  1160        <h1 class="display-4">Triggers</h1>
  1161  `
  1162  
  1163  const triggerAllActive string = `      <div class="text-center">
  1164          <div class="btn-group">
  1165            <a href="/trigger" class="btn btn-sm btn-primary active">All</a>
  1166            <a href="/trigger?armed=true" class="btn btn-sm btn-primary">Armed</a>
  1167            <a href="/trigger?armed=false" class="btn btn-sm btn-primary">Disarmed</a>
  1168          </div>
  1169        </div>
  1170  `
  1171  
  1172  const triggerArmedActive string = `      <div class="text-center">
  1173          <div class="btn-group">
  1174            <a href="/trigger" class="btn btn-sm btn-primary">All</a>
  1175            <a href="/trigger?armed=true" class="btn btn-sm btn-primary active">Armed</a>
  1176            <a href="/trigger?armed=false" class="btn btn-sm btn-primary">Disarmed</a>
  1177          </div>
  1178        </div>
  1179  `
  1180  
  1181  const triggerDisarmedActive string = `      <div class="text-center">
  1182          <div class="btn-group">
  1183            <a href="/trigger" class="btn btn-sm btn-primary">All</a>
  1184            <a href="/trigger?armed=true" class="btn btn-sm btn-primary">Armed</a>
  1185            <a href="/trigger?armed=false" class="btn btn-sm btn-primary active">Disarmed</a>
  1186          </div>
  1187        </div>
  1188  `
  1189  
  1190  const triggerTableTop string = `      <br>
  1191        <table class="table table-sm table-striped table-hover">
  1192          <thead>
  1193            <tr>
  1194              <th scope="col">Halt Label</th>
  1195              <th scope="col" class="w-25">Halt After Count</th>
  1196            </tr>
  1197          </thead>
  1198          <tbody>
  1199  `
  1200  
  1201  // To use: fmt.Sprintf(triggerTableRowTemplate, haltTriggerString, haltTriggerCount)
  1202  const triggerTableRowTemplate string = `          <tr>
  1203              <td class="halt-label">%[1]v</td>
  1204              <td>
  1205                <div class="input-group">
  1206                  <input type="number" class="form-control form-control-sm haltTriggerCount" min="0" max="4294967295" value="%[2]v">
  1207                  <div class="valid-feedback">
  1208                    New value successfully saved.
  1209                  </div>
  1210                  <div class="invalid-feedback">
  1211                    There was an error saving the new value.
  1212                  </div>
  1213                </div>
  1214              </td>
  1215            </tr>
  1216  `
  1217  
  1218  const triggerBottom string = `        </tbody>
  1219        </table>
  1220      </div>
  1221      <script src="/jquery.min.js"></script>
  1222      <script src="/popper.min.js"></script>
  1223      <script src="/bootstrap.min.js"></script>
  1224      <script type="text/javascript">
  1225        markValid = function(elem) {elem.removeClass("is-valid is-invalid").addClass("is-valid");};
  1226        markInvalid = function(elem) {elem.removeClass("is-valid is-invalid").addClass("is-invalid");};
  1227        unmark = function(elem) {elem.removeClass("is-valid is-invalid");};
  1228        updateErrorMsg = function(elem, text) {elem.siblings(".invalid-feedback").html(text);};
  1229        getLabelForCount = function(elem) {return elem.parent().parent().siblings(".halt-label").html();};
  1230        var timeout_unmark = 2000;
  1231        $(".haltTriggerCount").on("change", function(){
  1232          that = $( this );
  1233          $.ajax({
  1234            url: '/trigger/' + getLabelForCount(that),
  1235            method: 'POST',
  1236            data: {'count': that.val()},
  1237            dataType: 'json',
  1238            success: function(data, textStatus, jqXHR) {
  1239              markValid(that);
  1240              window.setTimeout(function(){unmark(that);}, timeout_unmark);
  1241            },
  1242            error: function(jqXHR, textStatus, errorThrown) {
  1243              var msg = "Error: " + jqXHR.status + " " + jqXHR.statusText  // Do we want to use jqXHR.responseText?
  1244              updateErrorMsg(that, msg);
  1245              markInvalid(that);
  1246            }
  1247          });
  1248        });
  1249      </script>
  1250    </body>
  1251  </html>
  1252  `