]> git.ipfire.org Git - thirdparty/bacula.git/commitdiff
baculum: Implement graphical status storage
authorMarcin Haba <marcin.haba@bacula.pl>
Sun, 22 Nov 2020 08:10:47 +0000 (09:10 +0100)
committerMarcin Haba <marcin.haba@bacula.pl>
Sun, 22 Nov 2020 08:10:47 +0000 (09:10 +0100)
17 files changed:
gui/baculum/protected/Common/Class/Params.php
gui/baculum/protected/Web/Lang/en/messages.mo
gui/baculum/protected/Web/Lang/en/messages.po
gui/baculum/protected/Web/Lang/ja/messages.mo
gui/baculum/protected/Web/Lang/ja/messages.po
gui/baculum/protected/Web/Lang/pl/messages.mo
gui/baculum/protected/Web/Lang/pl/messages.po
gui/baculum/protected/Web/Lang/pt/messages.mo
gui/baculum/protected/Web/Lang/pt/messages.po
gui/baculum/protected/Web/Lang/ru/messages.mo
gui/baculum/protected/Web/Lang/ru/messages.po
gui/baculum/protected/Web/Layouts/Main.tpl
gui/baculum/protected/Web/Pages/ClientView.page
gui/baculum/protected/Web/Pages/ClientView.php
gui/baculum/protected/Web/Pages/StorageView.page
gui/baculum/protected/Web/Pages/StorageView.php
gui/baculum/themes/Baculum-v2/css/baculum.css

index 6f1758b860d14a80a31f1a65f46e769b5083b1ca..831bca472061503cce1ce440da52a6e40c251ba2 100644 (file)
@@ -3,7 +3,7 @@
  * Bacula(R) - The Network Backup Solution
  * Baculum   - Bacula web interface
  *
- * Copyright (C) 2013-2019 Kern Sibbald
+ * Copyright (C) 2013-2020 Kern Sibbald
  *
  * The main author of Baculum is Marcin Haba.
  * The original author of Bacula is Kern Sibbald, with contributions
@@ -148,5 +148,25 @@ class Params extends CommonModule {
        public static function getBoolValue($value) {
                return ($value ? 'yes' : 'no');
        }
+
+       /**
+        * Get component version basing on given output.
+        * The version string in output should be component status command compatible.
+        *
+        * @param array $output component status output
+        * @return array major, minor and release numbers.
+        */
+       public static function getComponentVersion(array $output) {
+               $version = array('major' => 0, 'minor' => 0, 'release' => 0);
+               for ($i = 0; $i < count($output); $i++) {
+                       if (preg_match('/^[\w\d\s:.\-]+Version:\s+(?P<major>\d+)\.(?P<minor>\d+)\.(?P<release>\d+)\s+\(/', $output[$i], $match) === 1) {
+                               $version['major'] = intval($match['major']);
+                               $version['minor'] = intval($match['minor']);
+                               $version['release'] = intval($match['release']);
+                               break;
+                       }
+               }
+               return $version;
+       }
 }
 ?>
index a833fb463e432810e7c409b624bbe6cea9480280..ad8f9dd6daccf8d866e1adf6fb12b567c2cefde0 100644 (file)
Binary files a/gui/baculum/protected/Web/Lang/en/messages.mo and b/gui/baculum/protected/Web/Lang/en/messages.mo differ
index 3c74a4ae2d4f94b28688167eaf418300660acafc..fb6f0d2bc7b2551e51c2f90152973d9379ff85ce 100644 (file)
@@ -3080,3 +3080,120 @@ msgstr "Path (optional):"
 msgid "With the given path, the results narrow down to the files in the path only."
 msgstr "With the given path, the results narrow down to the files in the path only."
 
+msgid "Storage Daemon:"
+msgstr "Storage Daemon:"
+
+msgid "# Autochangers:"
+msgstr "# Autochangers:"
+
+msgid "# Devices:"
+msgstr "# Devices:"
+
+msgid "Devices"
+msgstr "Devices"
+
+msgid "Archive device:"
+msgstr "Archive device:"
+
+msgid "Device type:"
+msgstr "Device type:"
+
+msgid "Media type:"
+msgstr "Media type:"
+
+msgid "Device type"
+msgstr "Device type"
+
+msgid "Media type"
+msgstr "Media type"
+
+msgid "Maximum concurrent jobs:"
+msgstr "Maximum concurrent jobs:"
+
+msgid "Maximum volume size:"
+msgstr "Maximum volume size:"
+
+msgid "Mounted:"
+msgstr "Mounted:"
+
+msgid "Opened:"
+msgstr "Opened:"
+
+msgid "Waiting:"
+msgstr "Waiting:"
+
+msgid "Blocked:"
+msgstr "Blocked:"
+
+msgid "Blocked description:"
+msgstr "Blocked description:"
+
+msgid "Read-only:"
+msgstr "Read-only:"
+
+msgid "Last speed:"
+msgstr "Last speed:"
+
+msgid "Write pool:"
+msgstr "Write pool:"
+
+msgid "Write device:"
+msgstr "Write device:"
+
+msgid "Write volume:"
+msgstr "Write volume:"
+
+msgid "Read pool:"
+msgstr "Read pool:"
+
+msgid "Read device:"
+msgstr "Read device:"
+
+msgid "Read volume:"
+msgstr "Read volume:"
+
+msgid "Used space:"
+msgstr "Used space:"
+
+msgid "Status:"
+msgstr "Status:"
+
+msgid "running"
+msgstr "running"
+
+msgid "idle"
+msgstr "idle"
+
+msgid "Autochanger:"
+msgstr "Autochanger:"
+
+msgid "Single devices"
+msgstr "Single devices"
+
+msgid "Start time:"
+msgstr "Start time:"
+
+msgid "End time:"
+msgstr "End time:"
+
+msgid "Terminated jobs"
+msgstr "Terminated jobs"
+
+msgid "All devices"
+msgstr "All devices"
+
+msgid "All device types"
+msgstr "All device types"
+
+msgid "All media types"
+msgstr "All media types"
+
+msgid "All statuses"
+msgstr "All statuses"
+
+msgid "Graphical storage status is supported for Bacula storages version 9.0 and greater."
+msgstr "Graphical storage status is supported for Bacula storages version 9.0 and greater."
+
+msgid "Status request timed out. The most probably the Bacula storage is not available or it is not running."
+msgstr "Status request timed out. The most probably the Bacula storage is not available or it is not running."
+
index 3e765f7e8998e77ae171c0d869647f2582061597..0f09865cd04bf3b651810bc30f7592a0b26b1247 100644 (file)
Binary files a/gui/baculum/protected/Web/Lang/ja/messages.mo and b/gui/baculum/protected/Web/Lang/ja/messages.mo differ
index ced1b91a2c408949c2005d9429f37a81502b26e3..b5b0730de568be296280c65da4b95fd88ea12fdb 100644 (file)
@@ -3166,3 +3166,120 @@ msgstr "Path (optional):"
 msgid "With the given path, the results narrow down to the files in the path only."
 msgstr "With the given path, the results narrow down to the files in the path only."
 
+msgid "Storage Daemon:"
+msgstr "Storage Daemon:"
+
+msgid "# Autochangers:"
+msgstr "# Autochangers:"
+
+msgid "# Devices:"
+msgstr "# Devices:"
+
+msgid "Devices"
+msgstr "Devices"
+
+msgid "Archive device:"
+msgstr "Archive device:"
+
+msgid "Device type:"
+msgstr "Device type:"
+
+msgid "Media type:"
+msgstr "Media type:"
+
+msgid "Device type"
+msgstr "Device type"
+
+msgid "Media type"
+msgstr "Media type"
+
+msgid "Maximum concurrent jobs:"
+msgstr "Maximum concurrent jobs:"
+
+msgid "Maximum volume size:"
+msgstr "Maximum volume size:"
+
+msgid "Mounted:"
+msgstr "Mounted:"
+
+msgid "Opened:"
+msgstr "Opened:"
+
+msgid "Waiting:"
+msgstr "Waiting:"
+
+msgid "Blocked:"
+msgstr "Blocked:"
+
+msgid "Blocked description:"
+msgstr "Blocked description:"
+
+msgid "Read-only:"
+msgstr "Read-only:"
+
+msgid "Last speed:"
+msgstr "Last speed:"
+
+msgid "Write pool:"
+msgstr "Write pool:"
+
+msgid "Write device:"
+msgstr "Write device:"
+
+msgid "Write volume:"
+msgstr "Write volume:"
+
+msgid "Read pool:"
+msgstr "Read pool:"
+
+msgid "Read device:"
+msgstr "Read device:"
+
+msgid "Read volume:"
+msgstr "Read volume:"
+
+msgid "Used space:"
+msgstr "Used space:"
+
+msgid "Status:"
+msgstr "Status:"
+
+msgid "running"
+msgstr "running"
+
+msgid "idle"
+msgstr "idle"
+
+msgid "Autochanger:"
+msgstr "Autochanger:"
+
+msgid "Single devices"
+msgstr "Single devices"
+
+msgid "Start time:"
+msgstr "Start time:"
+
+msgid "End time:"
+msgstr "End time:"
+
+msgid "Terminated jobs"
+msgstr "Terminated jobs"
+
+msgid "All devices"
+msgstr "All devices"
+
+msgid "All device types"
+msgstr "All device types"
+
+msgid "All media types"
+msgstr "All media types"
+
+msgid "All statuses"
+msgstr "All statuses"
+
+msgid "Graphical storage status is supported for Bacula storages version 9.0 and greater."
+msgstr "Graphical storage status is supported for Bacula storages version 9.0 and greater."
+
+msgid "Status request timed out. The most probably the Bacula storage is not available or it is not running."
+msgstr "Status request timed out. The most probably the Bacula storage is not available or it is not running."
+
index 42cc53d09c3372bcbccbfd733d969c13a13ab370..df7d08979cc5844a17c4a8080cd658cda4f6d519 100644 (file)
Binary files a/gui/baculum/protected/Web/Lang/pl/messages.mo and b/gui/baculum/protected/Web/Lang/pl/messages.mo differ
index e2c8dd986a3941024afe303b170b2b93625a815f..aba90d09d113881500784f4a3b8906a0f779bb21 100644 (file)
@@ -3091,3 +3091,120 @@ msgstr "Ścieżka (opcjonalna):"
 msgid "With the given path, the results narrow down to the files in the path only."
 msgstr "Z podaną ścieżką wyniki zawężają się wyłącznie do plików w ścieżce."
 
+msgid "Storage Daemon:"
+msgstr "Magazyn danych:"
+
+msgid "# Autochangers:"
+msgstr "# Urządzenia Autochanger:"
+
+msgid "# Devices:"
+msgstr "# Urządzenia:"
+
+msgid "Devices"
+msgstr "Urządzenia"
+
+msgid "Archive device:"
+msgstr "Urządzenie archiwizujące:"
+
+msgid "Device type:"
+msgstr "Typ urządzenia:"
+
+msgid "Media type:"
+msgstr "Typ media:"
+
+msgid "Device type"
+msgstr "Typ urządzenia"
+
+msgid "Media type"
+msgstr "Typ media"
+
+msgid "Maximum concurrent jobs:"
+msgstr "Maksymalna ilość jednoczesnych zadań:"
+
+msgid "Maximum volume size:"
+msgstr "Maksymalny rozmiar wolumena:"
+
+msgid "Mounted:"
+msgstr "Zamontowany:"
+
+msgid "Opened:"
+msgstr "Otwarty:"
+
+msgid "Waiting:"
+msgstr "Oczekujący:"
+
+msgid "Blocked:"
+msgstr "Zablokowany:"
+
+msgid "Blocked description:"
+msgstr "Opis zablokowania:"
+
+msgid "Read-only:"
+msgstr "Tylko do odczytu:"
+
+msgid "Last speed:"
+msgstr "Prędkość chwilowa:"
+
+msgid "Write pool:"
+msgstr "Pula do zapisu:"
+
+msgid "Write device:"
+msgstr "Urządzenie zapisujące:"
+
+msgid "Write volume:"
+msgstr "Zapisywany wolumen:"
+
+msgid "Read pool:"
+msgstr "Pula do odczytu:"
+
+msgid "Read device:"
+msgstr "Urządzenie odczytujące:"
+
+msgid "Read volume:"
+msgstr "Odczytywany wolumen:"
+
+msgid "Used space:"
+msgstr "Użyte miejsce:"
+
+msgid "Status:"
+msgstr "Status:"
+
+msgid "running"
+msgstr "działający"
+
+msgid "idle"
+msgstr "bezczynny"
+
+msgid "Autochanger:"
+msgstr "Autochanger:"
+
+msgid "Single devices"
+msgstr "Pojedyncze urządzenia"
+
+msgid "Start time:"
+msgstr "Czas rozpoczęcia:"
+
+msgid "End time:"
+msgstr "Czas zakończenia:"
+
+msgid "Terminated jobs"
+msgstr "Zakończone zadania"
+
+msgid "All devices"
+msgstr "Wszystkie urządzenia"
+
+msgid "All device types"
+msgstr "Wszystkie typy urządzeń"
+
+msgid "All media types"
+msgstr "Wszystkie typy media"
+
+msgid "All statuses"
+msgstr "Wszystkie statusy"
+
+msgid "Graphical storage status is supported for Bacula storages version 9.0 and greater."
+msgstr "Graficzny status magazynu jest wspierany dla magazynów Bacula w wersji 9.0 i większych."
+
+msgid "Status request timed out. The most probably the Bacula storage is not available or it is not running."
+msgstr "Przekroczono limit czasu żądania statusu. Najprawdopodobniej magazyn Bacula jest niedostępny lub nie jest uruchomiony."
+
index 67c155c803aabd437261cd530c252a6f9460440d..69d3a1ee296033ef59f707be04199ffad6530d49 100644 (file)
Binary files a/gui/baculum/protected/Web/Lang/pt/messages.mo and b/gui/baculum/protected/Web/Lang/pt/messages.mo differ
index b93d08fe32afbd9fd4c605c32bdc3fb90e0db010..cc934fa4290e5570907df268e178482ba151547d 100644 (file)
@@ -3090,3 +3090,120 @@ msgstr "Path (optional):"
 msgid "With the given path, the results narrow down to the files in the path only."
 msgstr "With the given path, the results narrow down to the files in the path only."
 
+msgid "Storage Daemon:"
+msgstr "Storage Daemon:"
+
+msgid "# Autochangers:"
+msgstr "# Autochangers:"
+
+msgid "# Devices:"
+msgstr "# Devices:"
+
+msgid "Devices"
+msgstr "Devices"
+
+msgid "Archive device:"
+msgstr "Archive device:"
+
+msgid "Device type:"
+msgstr "Device type:"
+
+msgid "Media type:"
+msgstr "Media type:"
+
+msgid "Device type"
+msgstr "Device type"
+
+msgid "Media type"
+msgstr "Media type"
+
+msgid "Maximum concurrent jobs:"
+msgstr "Maximum concurrent jobs:"
+
+msgid "Maximum volume size:"
+msgstr "Maximum volume size:"
+
+msgid "Mounted:"
+msgstr "Mounted:"
+
+msgid "Opened:"
+msgstr "Opened:"
+
+msgid "Waiting:"
+msgstr "Waiting:"
+
+msgid "Blocked:"
+msgstr "Blocked:"
+
+msgid "Blocked description:"
+msgstr "Blocked description:"
+
+msgid "Read-only:"
+msgstr "Read-only:"
+
+msgid "Last speed:"
+msgstr "Last speed:"
+
+msgid "Write pool:"
+msgstr "Write pool:"
+
+msgid "Write device:"
+msgstr "Write device:"
+
+msgid "Write volume:"
+msgstr "Write volume:"
+
+msgid "Read pool:"
+msgstr "Read pool:"
+
+msgid "Read device:"
+msgstr "Read device:"
+
+msgid "Read volume:"
+msgstr "Read volume:"
+
+msgid "Used space:"
+msgstr "Used space:"
+
+msgid "Status:"
+msgstr "Status:"
+
+msgid "running"
+msgstr "running"
+
+msgid "idle"
+msgstr "idle"
+
+msgid "Autochanger:"
+msgstr "Autochanger:"
+
+msgid "Single devices"
+msgstr "Single devices"
+
+msgid "Start time:"
+msgstr "Start time:"
+
+msgid "End time:"
+msgstr "End time:"
+
+msgid "Terminated jobs"
+msgstr "Terminated jobs"
+
+msgid "All devices"
+msgstr "All devices"
+
+msgid "All device types"
+msgstr "All device types"
+
+msgid "All media types"
+msgstr "All media types"
+
+msgid "All statuses"
+msgstr "All statuses"
+
+msgid "Graphical storage status is supported for Bacula storages version 9.0 and greater."
+msgstr "Graphical storage status is supported for Bacula storages version 9.0 and greater."
+
+msgid "Status request timed out. The most probably the Bacula storage is not available or it is not running."
+msgstr "Status request timed out. The most probably the Bacula storage is not available or it is not running."
+
index 7ea98c190ff800ec668ec198f4e2c2da822f1b51..44dba88853f50c3e6caf643ee89743cd8dfeac38 100644 (file)
Binary files a/gui/baculum/protected/Web/Lang/ru/messages.mo and b/gui/baculum/protected/Web/Lang/ru/messages.mo differ
index 716ed5e19da74920f350e043c0c084b4ad7b96bb..927e053fe18e5595c1ae625be7fa4467efea73b9 100644 (file)
@@ -3090,3 +3090,120 @@ msgstr "Path (optional):"
 msgid "With the given path, the results narrow down to the files in the path only."
 msgstr "With the given path, the results narrow down to the files in the path only."
 
+msgid "Storage Daemon:"
+msgstr "Storage Daemon:"
+
+msgid "# Autochangers:"
+msgstr "# Autochangers:"
+
+msgid "# Devices:"
+msgstr "# Devices:"
+
+msgid "Devices"
+msgstr "Devices"
+
+msgid "Archive device:"
+msgstr "Archive device:"
+
+msgid "Device type:"
+msgstr "Device type:"
+
+msgid "Media type:"
+msgstr "Media type:"
+
+msgid "Device type"
+msgstr "Device type"
+
+msgid "Media type"
+msgstr "Media type"
+
+msgid "Maximum concurrent jobs:"
+msgstr "Maximum concurrent jobs:"
+
+msgid "Maximum volume size:"
+msgstr "Maximum volume size:"
+
+msgid "Mounted:"
+msgstr "Mounted:"
+
+msgid "Opened:"
+msgstr "Opened:"
+
+msgid "Waiting:"
+msgstr "Waiting:"
+
+msgid "Blocked:"
+msgstr "Blocked:"
+
+msgid "Blocked description:"
+msgstr "Blocked description:"
+
+msgid "Read-only:"
+msgstr "Read-only:"
+
+msgid "Last speed:"
+msgstr "Last speed:"
+
+msgid "Write pool:"
+msgstr "Write pool:"
+
+msgid "Write device:"
+msgstr "Write device:"
+
+msgid "Write volume:"
+msgstr "Write volume:"
+
+msgid "Read pool:"
+msgstr "Read pool:"
+
+msgid "Read device:"
+msgstr "Read device:"
+
+msgid "Read volume:"
+msgstr "Read volume:"
+
+msgid "Used space:"
+msgstr "Used space:"
+
+msgid "Status:"
+msgstr "Status:"
+
+msgid "running"
+msgstr "running"
+
+msgid "idle"
+msgstr "idle"
+
+msgid "Autochanger:"
+msgstr "Autochanger:"
+
+msgid "Single devices"
+msgstr "Single devices"
+
+msgid "Start time:"
+msgstr "Start time:"
+
+msgid "End time:"
+msgstr "End time:"
+
+msgid "Terminated jobs"
+msgstr "Terminated jobs"
+
+msgid "All devices"
+msgstr "All devices"
+
+msgid "All device types"
+msgstr "All device types"
+
+msgid "All media types"
+msgstr "All media types"
+
+msgid "All statuses"
+msgstr "All statuses"
+
+msgid "Graphical storage status is supported for Bacula storages version 9.0 and greater."
+msgstr "Graphical storage status is supported for Bacula storages version 9.0 and greater."
+
+msgid "Status request timed out. The most probably the Bacula storage is not available or it is not running."
+msgstr "Status request timed out. The most probably the Bacula storage is not available or it is not running."
+
index 6a41a3e45d17adb09b2f49ea7ba29fd5a77ac3b2..7bf70b74e1400fee7d9462a9727cb5cdda0a4891 100644 (file)
@@ -22,6 +22,7 @@
                        <com:BClientScript ScriptUrl=<%~ ../JavaScript/misc.js %> />
                        <com:BClientScript ScriptUrl=<%~ ../JavaScript/graph.js %> />
                        <com:BClientScript ScriptUrl=<%~ ../JavaScript/statistics.js %> />
+                       <com:BClientScript ScriptUrl=<%~ ../JavaScript/gauge.js %> />
                        <!-- Top container -->
                        <div class="w3-bar w3-top w3-black w3-large" style="z-index:4">
                                <button type="button" class="w3-bar-item w3-button w3-hover-none w3-hover-text-light-grey" onclick="W3SideBar.open();"><i class="fa fa-bars"></i>  Menu</button>
index 79b28698cc550f21b47a50c97b7ae2a63d6ac463..9ee14b6d9d0cf3d3a59acd7c7144d7dd7adf3d5e 100644 (file)
@@ -157,8 +157,7 @@ var oGraphicalClientStatus = {
                        return el;
                },
                started_epoch: function(value) {
-                       var t = parseInt(value, 10) * 1000;
-                       return (new Date(t)).toLocaleString();
+                       return Units.format_date(value);
                },
                jobs_run: function(value) {
                        return (this.data.hasOwnProperty('running') ? this.data.running.length : 0);
index c1525596b056cbd6da706cfcd4d572737361f401..bbd61990a8de8d29d17f2123941da096ebbd6a52 100644 (file)
@@ -3,7 +3,7 @@
  * Bacula(R) - The Network Backup Solution
  * Baculum   - Bacula web interface
  *
- * Copyright (C) 2013-2019 Kern Sibbald
+ * Copyright (C) 2013-2020 Kern Sibbald
  *
  * The main author of Baculum is Marcin Haba.
  * The original author of Bacula is Kern Sibbald, with contributions
@@ -24,6 +24,7 @@ Prado::using('System.Web.UI.ActiveControls.TActiveLabel');
 Prado::using('System.Web.UI.ActiveControls.TActiveLinkButton');
 Prado::using('System.Web.UI.ActiveControls.TCallback');
 Prado::using('System.Web.UI.JuiControls.TJuiProgressbar');
+Prado::using('Application.Common.Class.Params');
 Prado::using('Application.Web.Class.BaculumWebPage'); 
 
 /**
@@ -139,7 +140,7 @@ class ClientView extends BaculumWebPage {
                $client_status = array(
                        'header' => array(),
                        'running' => array(),
-                       'version' => $this->getClientVersion($raw_status)
+                       'version' => Params::getComponentVersion($raw_status)
                );
                if ($graph_status->error === 0) {
                        $client_status['header'] = $graph_status->output;
@@ -178,18 +179,5 @@ class ClientView extends BaculumWebPage {
                        $this->JobBandwidth->setJobUname($job_uname);
                }
        }
-
-       private function getClientVersion($output) {
-               $version = array('major' => 0, 'minor' => 0, 'release' => 0);
-               for ($i = 0; $i < count($output); $i++) {
-                       if (preg_match('/^[\w\d\s:.\-]+Version:\s+(?P<major>\d+)\.(?P<minor>\d+)\.(?P<release>\d+)\s+\(/', $output[$i], $match) === 1) {
-                               $version['major'] = intval($match['major']);
-                               $version['minor'] = intval($match['minor']);
-                               $version['release'] = intval($match['release']);
-                               break;
-                       }
-               }
-               return $version;
-       }
 }
 ?>
index 3c7d251b2db46b03ec653535a00c2a6984dcfe4b..ce24a2a4b9246e9c4520ef079b4b68891b6828c4 100644 (file)
                        OnClick="status"
                        CssClass="w3-button w3-green w3-margin-bottom"
                        CausesValidation="false"
-                       ClientSide.OnLoading="$('#status_storage_loading').show();"
-                       ClientSide.OnSuccess="$('#status_storage_loading').hide();"
+                       ClientSide.OnLoading="$('#status_storage_loading').show();$('#status_storage_error').hide();"
+                       ClientSide.OnSuccess="$('#status_storage_loading').hide(); $('#show_storage_container').hide();$('#status_storage_container').show();oGraphicalStorageStatus.set_refresh_timeout(document.getElementById('status_storage_refresh_interval').value);"
+                       ClientSide.OnFailure="$('#status_storage_loading').hide();status_storage_show_error(parameter);"
+                       Attributes.onclick="hide_action_text_output(event);"
                >
                        <prop:Text><%=Prado::localize('Status storage')%> &nbsp;<i class="fa fa-file-medical-alt"></i></prop:Text>
                </com:TActiveLinkButton>
@@ -58,7 +60,7 @@
                        ValidationGroup="AutoChangerGroup"
                        CausesValidation="true"
                        ClientSide.OnLoading="$('#status_storage_loading').show();"
-                       ClientSide.OnSuccess="$('#status_storage_loading').hide();"
+                       ClientSide.OnSuccess="$('#status_storage_loading').hide();$('#storage_action_text_output').slideDown();"
                >
                        <prop:Text><%=Prado::localize('Mount')%> &nbsp;<i class="fa fa-caret-down"></i></prop:Text>
                </com:TActiveLinkButton>
@@ -69,7 +71,7 @@
                        ValidationGroup="AutoChangerGroup"
                        CausesValidation="true"
                        ClientSide.OnLoading="$('#status_storage_loading').show();"
-                       ClientSide.OnSuccess="$('#status_storage_loading').hide();"
+                       ClientSide.OnSuccess="$('#status_storage_loading').hide();$('#storage_action_text_output').slideDown();"
                >
                        <prop:Text><%=Prado::localize('Release')%> &nbsp;<i class="fa fa-caret-right"></i></prop:Text>
                </com:TActiveLinkButton>
                        ValidationGroup="AutoChangerGroup"
                        CausesValidation="true"
                        ClientSide.OnLoading="$('#status_storage_loading').show();"
-                       ClientSide.OnSuccess="$('#status_storage_loading').hide();"
+                       ClientSide.OnSuccess="$('#status_storage_loading').hide();$('#storage_action_text_output').slideDown();"
                >
                        <prop:Text><%=Prado::localize('Umount')%> &nbsp;<i class="fa fa-caret-up"></i></prop:Text>
                </com:TActiveLinkButton>
-               <i id="status_storage_loading" class="fa fa-sync w3-spin" style="display: none; vertical-align: super;"></i>
-               <com:TActivePanel ID="Autochanger" Display="None">
-                       <div class="w3-row">
-                               <div class="w3-quarter w3-container">
+               <i id="status_storage_loading" class="fa fa-sync w3-spin" style="display: none; vertical-align: super;"></i> <span id="status_storage_error" class="w3-text-red" style="display: none"></span>
+               <com:TActivePanel ID="Autochanger" Display="None" Height="61px">
+                               <div class="w3-left w3-margin-right">
                                                <label><%[ Drive number: ]%></label>
                                                <com:TActiveTextBox ID="Drive" AutoPostBack="false" Text="0" MaxLength="3" CssClass="w3-input smallbox" />
                                                <com:TDataTypeValidator ID="DriveValidator" ValidationGroup="AutoChangerGroup" ControlToValidate="Drive" ErrorMessage="<%[ Drive number must be integer. ]%>" Display="None" DataType="Integer" />
                                </div>
-                       </div>
-                       <div class="w3-row">
-                               <div class="w3-quarter w3-container">
+                               <div class="w3-left">
                                        <%[ Slot number: ]%>
                                        <com:TActiveTextBox ID="Slot" AutoPostBack="false" Text="0" MaxLength="3" CssClass="w3-input smallbox" />
                                        <com:TDataTypeValidator ID="SlotValidator" ValidationGroup="AutoChangerGroup" ControlToValidate="Slot" ErrorMessage="<%[ Slot number must be integer. ]%>" Display="None" DataType="Integer" />
                                </div>
-                       </div>
                </com:TActivePanel>
-               <div class="w3-panel w3-card w3-light-grey">
-                       <div class="w3-code notranslate">
-                               <pre><com:TActiveLabel ID="StorageLog" /></pre>
+               <div id="storage_action_text_output" class="w3-code" style="display: none; clear: both;">
+                       <pre><com:TActiveLabel ID="StorageActionLog" /></pre>
+               </div>
+               <div id="status_storage_container" class="w3-clear w3-margin-top" style="display: none">
+                       <div class="w3-right w3-margin-top w3-margin-right" title="<%[ To disable refreshing please type 0. ]%>">
+                               <span style="line-height: 41px"><%[ Refresh interval (sec.): ]%></span> <input type="text" id="status_storage_refresh_interval" class="w3-input w3-border w3-right w3-margin-left" value="10" style="width: 50px"/>
+                       </div>
+                       <div class="w3-panel w3-card w3-light-grey" style="padding-bottom: 16px;">
+                               <div class="w3-row">
+                                       <a href="javascript:void(0)" onclick="W3SubTabs.open('status_storage_subtab_graphical', 'status_storage_graphical_output');">
+                                               <div id="status_storage_subtab_graphical" class="subtab_btn w3-half w3-bottombar w3-hover-light-grey w3-border-red w3-padding"><%[ Graphical status ]%></div>
+                                        </a>
+                                       <a href="javascript:void(0)" onclick="W3SubTabs.open('status_storage_subtab_text', 'status_storage_text_output');">
+                                               <div id="status_storage_subtab_text" class="subtab_btn w3-half w3-bottombar w3-hover-light-grey w3-padding"><%[ Raw status ]%></div>
+                                       </a>
+                               </div>
+                               <div id="status_storage_graphical_output" class="subtab_item">
+                                       <h4 id="status_storage_status_not_supported" style="display: none"><%[ Graphical storage status is supported for Bacula storages version 9.0 and greater. ]%></h4>
+                                       <div id="status_storage_graphical_container">
+                                               <h4><%[ Storage Daemon: ]%> <span id="status_storage_name"></span></h4>
+                                               <table class="w3-table w3-stripped w3-border status_table">
+                                                       <tr>
+                                                               <td><%[ Version: ]%></td>
+                                                               <td id="status_storage_version"></td>
+                                                       </tr>
+                                                       <tr>
+                                                               <td><%[ Uname: ]%></td>
+                                                               <td id="status_storage_uname"></td>
+                                                       </tr>
+                                                       <tr>
+                                                               <td><%[ Started time: ]%></td>
+                                                               <td id="status_storage_started_time"></td>
+                                                       </tr>
+                                                       <tr>
+                                                               <td><%[ Running jobs: ]%></td>
+                                                               <td><span id="status_storage_jobs_running"></span></td>
+                                                       </tr>
+                                                       <tr>
+                                                               <td><%[ Plugins: ]%></td>
+                                                               <td id="status_storage_plugins"></td>
+                                                       </tr>
+                                                       <tr>
+                                                               <td><%[ # Autochangers: ]%></td>
+                                                               <td id="status_storage_no_autochangers"></td>
+                                                       </tr>
+                                                       <tr>
+                                                               <td><%[ # Devices: ]%></td>
+                                                               <td id="status_storage_no_devices"></td>
+                                                       </tr>
+                                               </table>
+                                               <div id="status_storage_filters"></div>
+                                               <div id="status_storage_devices"></div>
+                                               <h4><%[ Terminated jobs ]%></h4>
+                                               <div id="status_storage_terminated"></div>
+                                       </div>
+<com:TJuiProgressbar Display="None" />
+<script>
+var oGraphicalStorageStatus = {
+       data: {},
+       refresh_timeout: null,
+       running_jobs: [],
+       ids: {
+               refresh_interval: 'status_storage_refresh_interval',
+               status_not_supported: 'status_storage_status_not_supported',
+               graphical_container: 'status_storage_graphical_container',
+               devices: 'status_storage_devices',
+               terminated: 'status_storage_terminated',
+               filters: 'status_storage_filters',
+               header: {
+                       name: 'status_storage_name',
+                       version: 'status_storage_version',
+                       uname: 'status_storage_uname',
+                       started_epoch: 'status_storage_started_time',
+                       jobs_running: 'status_storage_jobs_running',
+                       plugins: 'status_storage_plugins',
+                       nautochgr: 'status_storage_no_autochangers',
+                       ndevices: 'status_storage_no_devices'
+               }
+       },
+       css: {
+               gauge: 'gauge',
+               gauge_label: 'gauge_label',
+               gauge_container: 'gauge_container',
+               status_header: 'status_header',
+               device_columns: 'device_columns',
+               device_colinfo: 'device_colinfo',
+               device_header: 'device_header',
+               device_table: 'device_table',
+               running_job_header: 'running_job_header',
+               running_job_table: 'running_job_table',
+               terminated_job_header: 'terminated_job_header',
+               terminated_job_table: 'terminated_job_table'
+       },
+       gauges: {
+               space: {
+                       angle: -0.2, // The span of the gauge arc
+                       lineWidth: 0.2, // The line thickness
+                       radiusScale: 1, // Relative radius
+                       pointer: {
+                               length: 0.6, // // Relative to gauge radius
+                               strokeWidth: 0.035, // The thickness
+                               color: '#000000' // Fill color
+                       },
+                       limitMax: false,     // If false, max value increases automatically if value > maxValue
+                       limitMin: false,     // If true, the min value of the gauge will be fixed
+                       colorStart: '#6FADCF',   // Colors
+                       colorStop: '#8FC0DA',    // just experiment with them
+                       strokeColor: '#E0E0E0',  // to see which ones work best for you
+                       generateGradient: true,
+                       highDpiSupport: true,     // High resolution support
+                       // renderTicks is Optional
+                       renderTicks: {
+                               divisions: 5,
+                               divWidth: 1.1,
+                               divLength: 0.7,
+                               divColor: '#333333',
+                               subDivisions: 3,
+                               subLength: 0.5,
+                               subWidth: 0.6,
+                               subColor: '#666666'
+                       },
+                       staticLabels: {
+                               font: "10px sans-serif",  // Specifies font
+                               color: "#000000",  // Optional: Label text color
+                               fractionDigits: 0  // Optional: Numerical precision. 0=round off.
+                       }
+               },
+               speed: {
+                       angle: -0.2, // The span of the gauge arc
+                       lineWidth: 0.2, // The line thickness
+                       radiusScale: 1, // Relative radius
+                       pointer: {
+                               length: 0.6, // // Relative to gauge radius
+                               strokeWidth: 0.035, // The thickness
+                               color: '#000000' // Fill color
+                       },
+                       limitMax: false,     // If false, max value increases automatically if value > maxValue
+                       limitMin: false,     // If true, the min value of the gauge will be fixed
+                       colorStart: '#6FADCF',   // Colors
+                       colorStop: '#8FC0DA',    // just experiment with them
+                       strokeColor: '#E0E0E0',  // to see which ones work best for you
+                       generateGradient: true,
+                       highDpiSupport: true,     // High resolution support
+                       // renderTicks is Optional
+                       renderTicks: {
+                               divisions: 5,
+                               divWidth: 1.1,
+                               divLength: 0.7,
+                               divColor: '#333333',
+                               subDivisions: 3,
+                               subLength: 0.5,
+                               subWidth: 0.6,
+                               subColor: '#666666'
+                       },
+                       staticLabels: {
+                               font: "10px sans-serif",  // Specifies font
+                               color: "#000000",  // Optional: Label text color
+                               fractionDigits: 0  // Optional: Numerical precision. 0=round off.
+                       }
+               }
+       },
+       filters: {
+               name: {
+                       label: '<%[ Device: ]%>',
+                       init_item: '<%[ All devices ]%>',
+                       val: null
+               },
+               type: {
+                       label: '<%[ Device type: ]%>',
+                       init_item: '<%[ All device types ]%>',
+                       val: null
+               },
+               media_type: {
+                       label: '<%[ Media type: ]%>',
+                       init_item: '<%[ All media types ]%>',
+                       val: null
+               },
+               status: {
+                       label: '<%[ Status: ]%>',
+                       init_item: '<%[ All statuses ]%>',
+                       val: null,
+                       items: {
+                               running: '<%[ running ]%>',
+                               idle: '<%[ idle ]%>'
+                       },
+                       change: function(value) {
+                               // clear all devices
+                               this.remove_elements('#' + this.ids.devices, true);
+                               var devices_running = [];
+                               var devices_idle = [];
+                               for (var i = 0; i < this.running_jobs.length; i++) {
+                                       if (this.running_jobs[i].read_device && devices_running.indexOf(this.running_jobs[i].read_device) == -1) {
+                                               devices_running.push(this.running_jobs[i].read_device);
+                                       }
+                                       if (this.running_jobs[i].write_device && devices_running.indexOf(this.running_jobs[i].write_device) == -1) {
+                                               devices_running.push(this.running_jobs[i].write_device);
+                                       }
+                               }
+                               if (value === 'running') {
+                                       this.filters['name'].val = devices_running;
+                               } else if (value === 'idle') {
+                                       for (var i = 0; i < this.data.devices.length; i++) {
+                                               if (devices_running.indexOf(this.data.devices[i].name) === -1) {
+                                                       devices_idle.push(this.data.devices[i].name);
+                                               }
+                                       }
+                                       this.filters['name'].val = devices_idle;
+                               } else {
+                                       this.filters['name'].val = null;
+                               }
+                               this.update_status();
+                       }
+               }
+       },
+       initialized: false,
+       init: function() {
+               this.set_events();
+       },
+       set_data: function(data) {
+               this.data = data;
+       },
+       set_events: function() {
+               var refresh_interval_el = document.getElementById(this.ids.refresh_interval);
+               refresh_interval_el.addEventListener('keyup', function(e) {
+                       var interval = refresh_interval_el.value;
+                       this.set_refresh_timeout(interval);
+               }.bind(this));
+       },
+       formatters: {
+               uname: function(value) {
+                       var img = document.createElement('I');
+                       img.className = 'fab fa-2x';
+                       if (/win\d{2}/i.test(value)) {
+                               img.className += ' fa-windows';
+                       } else if (/ubuntu/i.test(value)) {
+                               img.className += ' fa-ubuntu';
+                       } else if (/fedora/i.test(value)) {
+                               img.className += ' fa-fedora';
+                       } else if (/centos/i.test(value)) {
+                               img.className += ' fa-centos';
+                       } else if (/redhat/i.test(value)) {
+                               img.className += ' fa-redhat';
+                       } else if (/suse/i.test(value)) {
+                               img.className += ' fa-suse';
+                       } else if (/linux/i.test(value)) {
+                               img.className += ' fa-linux';
+                       } else if (/freebsd/i.test(value)) {
+                               img.className += ' fa-freebsd';
+                       } else if (/(darwin|mac\s?os)/i.test(value)) {
+                               img.className += ' fa-apple';
+                       } else {
+                               img.className = 'fa fa-2x fa-question';
+                       }
+                       img.style.marginRight = '10px';
+                       var el = document.createElement('SPAN');
+                       var text = document.createTextNode(value);
+                       el.appendChild(img);
+                       el.appendChild(text);
+                       return el;
+               },
+               started_epoch: function(value) {
+                       return Units.format_date(value);
+               },
+               plugins: function(value) {
+                       var val;
+                       if (value) {
+                               val = value.replace(/-fd\.so/g, '').replace(/-fd\.dll/g, '').replace(/,/g, ', ');
+                       } else {
+                               val = document.createElement('I');
+                               val.className = 'fa';
+                               val.className += ' fa-minus';
+                       }
+                       return val;
+               },
+               check: function(value) {
+                       var img = document.createElement('I');
+                       img.className = 'fa';
+                       if (value == 1) {
+                               img.className += ' fa-check';
+                               img.title = '<%[ Yes ]%>';
+                       } else {
+                               img.className += ' fa-minus';
+                               img.title = '<%[ No ]%>';
+                       }
+                       return img;
+               },
+               value: function(value) {
+                       var val;
+                       if (value) {
+                               val = value;
+                       } else {
+                               val = document.createElement('I');
+                               val.className = 'fa';
+                               val.className += ' fa-minus';
+                       }
+                       return val;
+               }
+       },
+       update: function(data) {
+               this.set_data(data);
+               if (this.is_status_supported() === false) {
+                       return;
+               }
+               this.set_headers();
+               var full_refresh = this.check_full_refresh();
+               if (full_refresh) {
+                       // remembered job list empty so job list changed - full refresh list
+                       this.remove_elements('.' + this.css.running_job_header);
+                       this.remove_elements('.' + this.css.running_job_table);
+                       this.remove_elements('.' + this.css.terminated_job_header);
+                       this.remove_elements('.' + this.css.terminated_job_table);
+               }
+               if (this.data.hasOwnProperty('devices')) {
+                       var dev_running_jobs;
+                       OUTER: for (var i = 0; i < this.data.devices.length; i++) {
+                               INNER: for (var key in this.filters) {
+                                       if (typeof(this.filters[key].val) == 'string' && this.filters[key].val !== this.data.devices[i][key]) {
+                                               continue OUTER;
+                                       } else if (Array.isArray(this.filters[key].val) && this.filters[key].val.indexOf(this.data.devices[i][key]) == -1) {
+                                               continue OUTER;
+                                       }
+                               }
+                               this.add_autochanger(this.data.devices[i]);
+                               this.add_device(this.data.devices[i]);
+                               if (!this.initialized) {
+                                       for (var type in this.filters) {
+                                               this.set_filter(type, this.data.devices[i][type], this.filters[type].label, this.filters[type].init_item);
+                                       }
+                               }
+                       }
+               }
+               if (this.data.hasOwnProperty('terminated')) {
+                       for (var i = 0; i < this.data.terminated.length; i++) {
+                               this.add_terminated_job(i, this.data.terminated[i]);
+                       }
+               }
+               this.initialized = true;
+       },
+       set_headers: function() {
+               var el, val;
+               ['header'].forEach(function(section) {
+                       if (!this.data.hasOwnProperty(section)) {
+                               return;
+                       }
+                       for (var key in this.ids[section]) {
+                               if (!this.data[section].hasOwnProperty(key)) {
+                                       continue;
+                               }
+                               el = document.getElementById(this.ids[section][key]);
+                               val = this.formatters.hasOwnProperty(key) ? this.formatters[key].call(this, this.data[section][key]) : this.data[section][key];
+                               if (val instanceof HTMLElement) {
+                                       el.innerHTML = val.outerHTML;
+                               } else {
+                                       el.textContent = val;
+                               }
+                       }
+               }.bind(this));
+       },
+       check_full_refresh: function() {
+               var full_refresh = false;
+               var running_jobs = [];
+               var found;
+               var to_add = [];
+               var to_rm = [];
+               // check if with data came new jobs or disapeared some finished jobs
+               for (var i = 0; i < this.data.running.length; i++) {
+                       running_jobs.push(this.data.running[i]);
+                       found = false;
+                       for (var j = 0; j < this.running_jobs.length; j++) {
+                               if (this.running_jobs[j].jobid === this.data.running[i].jobid) {
+                                       found = true;
+                                       break;
+                               }
+                       }
+                       if (!found) {
+                               to_add.push(this.data.running[i]);
+                               full_refresh = true;
+                       }
+               }
+               if (!full_refresh) {
+                       for (var i = 0; i < this.running_jobs.length; i++) {
+                               found = false;
+                               for (var j = 0; j < running_jobs.length; j++) {
+                                       if (running_jobs[j].jobid === this.running_jobs[i].jobid) {
+                                               found = true;
+                                               break;
+                                       }
+                               }
+                               if (!found) {
+                                       to_rm.push(this.running_jobs[i]);
+                                       full_refresh = true;
+                               }
+                       }
+               }
+
+               // add new jobs
+               for (var i = 0; i < to_add.length; i++) {
+                       this.running_jobs.push(to_add[i]);
+               }
+               // rm terminated jobs
+               for (var i = 0; i < to_rm.length; i++) {
+                       for (var j = 0; j < this.running_jobs.length; j++) {
+                               if (this.running_jobs[j].jobid === to_rm[i].jobid) {
+                                       this.running_jobs.splice(j, 1);
+                                       break;
+                               }
+                       }
+               }
+               return full_refresh;
+       },
+       set_filter: function(type, value, label, init_item) {
+               var filters = document.getElementById(this.ids.filters);
+               var select = filters.querySelector('select[rel="' + type + '"]');
+               var option, label;
+               if (!select) {
+                       var title = document.createTextNode(label);
+                       select = document.createElement('SELECT');
+                       select.className = 'w3-select w3-border w3-margin-right';
+                       select.setAttribute('rel', type);
+                       var onchange_func;
+                       if (this.filters[type].hasOwnProperty('change')) {
+                               // custom onchange function
+                               onchange_func = function(e) {
+                                       this.filters[type].change.call(this, select.value);
+                               }.bind(this);
+                       } else {
+                               // default onchange function
+                               onchange_func = function(e) {
+                                       // clear all devices
+                                       this.remove_elements('#' + this.ids.devices, true);
+                                       if (select.value) {
+                                               this.filters[type].val = select.value;
+                                       } else {
+                                               this.filters[type].val = null;
+                                       }
+                                       this.update_status();
+                               }.bind(this)
+                       }
+                       select.addEventListener('change', onchange_func);
+                       var option = document.createElement('OPTION');
+                       option.value = '';
+                       var label = document.createTextNode(init_item);
+                       option.appendChild(label);
+                       select.appendChild(option);
+                       filters.appendChild(title);
+                       filters.appendChild(select);
+                       if (this.filters[type].hasOwnProperty('items')) {
+                               // predefined items
+                               for (var val in this.filters[type].items) {
+                                       option = document.createElement('OPTION');
+                                       option.value = val;
+                                       label = document.createTextNode(this.filters[type].items[val]);
+                                       option.appendChild(label);
+                                       select.appendChild(option);
+                               }
+                       }
+               }
+
+               if (!this.filters[type].hasOwnProperty('items')) {
+                       // dynamically defined items
+                       var found = false;
+                       for (var i = 0; i < select.options.length; i++) {
+                               if (select.options[i].value === value) {
+                                       found = true;
+                                       break;
+                               }
+                       }
+                       if (!found) {
+                               option = document.createElement('OPTION');
+                               option.value = value;
+                               label = document.createTextNode(value);
+                               option.appendChild(label);
+                               select.appendChild(option);
+                       }
+               }
+       },
+       get_show_info: function(device) {
+               var info;
+               if (this.data.hasOwnProperty('show')) {
+                       for (var i = 0; i < this.data.show.length; i++) {
+                               if (this.data.show[i].devicename === device) {
+                                       info = this.data.show[i];
+                                       break;
+                               }
+                       }
+               }
+               return info;
+       },
+       remove_elements: function(selector, empty) {
+               var elements = document.querySelectorAll(selector);
+               for (var i = 0; i < elements.length; i++) {
+                       while (elements[i].firstChild) {
+                               elements[i].removeChild(elements[i].firstChild);
+                       }
+               }
+
+               if (!empty) {
+                       var els = [].slice.call(elements);
+                       var els_len = els.length;
+                       for (var i = 0; i < els_len; i++) {
+                               els[i].parentNode.removeChild(els[i]);
+                       }
+               }
+       },
+       add_row: function(table, key, value) {
+               var tr = document.createElement('TR');
+               var tdl = document.createElement('TD');
+               var tdr = document.createElement('TD');
+               tdl.textContent = key;
+               if (value instanceof HTMLElement) {
+                       tdr.appendChild(value);
+               } else {
+                       tdr.innerHTML = value;
+               }
+               tr.appendChild(tdl);
+               tr.appendChild(tdr);
+               table.appendChild(tr);
+       },
+       get_device_headers: function() {
+               var header = document.createElement('DIV');
+               header.className = this.css.device_columns;
+               var device = document.createElement('DIV');
+               device.className = 'w3-container w3-cell w3-mobile';
+               device.textContent = '<%[ Device ]%>';
+               var status = document.createElement('DIV');
+               status.textContent = '<%[ Status ]%>';
+               var dev_type = document.createElement('DIV');
+               dev_type.textContent = '<%[ Device type ]%>';
+               var media_type = document.createElement('DIV');
+               media_type.textContent = '<%[ Media type ]%>';
+               header.appendChild(device);
+               header.appendChild(status);
+               header.appendChild(dev_type);
+               header.appendChild(media_type);
+               return header;
+       },
+       add_autochanger: function(device) {
+               if (!device.autochanger) {
+                       // no autochanger, single device
+                       this.add_single_device(device);
+                       return;
+               }
+               var devices_el = document.getElementById(this.ids.devices);
+               var ach_container = document.createElement('DIV');
+               ach_container.setAttribute('rel', 'ach_' + device.autochanger);
+               var ach_label = document.createElement('H4');
+               ach_label.textContent = '<%[ Autochanger: ]%> ' + device.autochanger;
+               ach_container.appendChild(ach_label);
+               var ach_info = document.createElement('DIV');
+               ach_info.setAttribute('rel', 'ach_info_' + device.autochanger);
+               var info = this.get_show_info(device.autochanger);
+               if (info) {
+                       var ach_jobs = document.createElement('DIV');
+                       var ach_jobs_label = document.createElement('SPAN');
+                       var ach_num_jobs = document.createElement('SPAN');
+                       var ach_max_jobs = document.createElement('SPAN');
+                       var ach_sep = document.createTextNode(' / ');
+                       var maxjobs = info.maxjobs == 0 ? '<%[ unlimited ]%>' : info.maxjobs;
+                       ach_jobs_label.textContent = '<%[ Running jobs: ]%> ';
+                       ach_num_jobs.textContent = ' ' + info.numjobs;
+                       ach_max_jobs.textContent = maxjobs;
+                       ach_info.appendChild(ach_jobs_label);
+                       ach_info.appendChild(ach_num_jobs);
+                       ach_info.appendChild(ach_sep);
+                       ach_info.appendChild(ach_max_jobs);
+                       ach_container.appendChild(ach_info);
+               }
+               var ach_el = document.querySelector('div[rel="ach_' + device.autochanger + '"]');
+               var a = document.querySelector('div[rel="ach_info_' + device.autochanger + '"]');
+               if (a) {
+                       ach_el.replaceChild(ach_info, a);
+               } else if (!ach_el)  {
+                       var device_headers = this.get_device_headers();
+                       ach_container.appendChild(device_headers);
+                       devices_el.appendChild(ach_container);
+               }
+       },
+       add_single_device: function(device) {
+               var sd_el = document.querySelector('div[rel="single_devices"]');
+               if (sd_el) {
+                       // single devices container already exists, return
+                       return;
+               }
+               var devices_el = document.getElementById(this.ids.devices);
+               var sd_container = document.createElement('DIV');
+               sd_container.setAttribute('rel', 'single_devices');
+               var sd_label = document.createElement('H4');
+               sd_label.textContent = '<%[ Single devices ]%>';
+               sd_container.appendChild(sd_label);
+               devices_el.appendChild(sd_container);
+               var device_headers = this.get_device_headers();
+               sd_container.appendChild(device_headers);
+       },
+       add_device: function(device) {
+               var container = document.createElement('DIV');
+               container.className = [
+                       this.css.device_table,
+                       'w3-border'
+               ].join(' ');
+               container.setAttribute('rel', 'device_' + device.name);
+               var table = document.createElement('TABLE');
+               container.appendChild(table);
+
+               // arrow icon
+               var dev_arrow_img = document.createElement('I');
+               dev_arrow_img.className = 'w3-margin-right';
+
+               var header = document.createElement('DIV');
+               var open = document.querySelector('div[rel="header_device_' + device.name + '"]');
+               if (open) {
+                       var data_open = open.getAttribute('data-open');
+                       header.setAttribute('data-open', data_open);
+                       if (data_open == 1) {
+                               dev_arrow_img.className += ' fas fa-chevron-up';
+                       } else {
+                               dev_arrow_img.className += ' fas fa-chevron-down';
+                       }
+               } else {
+                       header.setAttribute('data-open', 0);
+                       dev_arrow_img.className += ' fas fa-chevron-down';
+               }
+               header.setAttribute('rel', 'header_device_' + device.name);
+               header.className = [
+                       this.css.device_header,
+                       this.css.status_header
+               ].join(' ');
+
+               // device label
+               var dev_label = document.createElement('DIV');
+               dev_label.className = 'w3-container w3-cell w3-mobile';
+               dev_label.appendChild(dev_arrow_img);
+               var dev_label_colinfo = document.createElement('SPAN');
+               dev_label_colinfo.className = this.css.device_colinfo;
+               dev_label_colinfo.innerHTML = '<%[ Device ]%>: &nbsp;';
+               dev_label.appendChild(dev_label_colinfo);
+               var dev_label_txt = document.createTextNode(' ' + device.name);
+               dev_label.appendChild(dev_label_txt);
+
+               // device running
+               var dev_running_jobs = this.get_device_jobs(device.name);
+               var dev_running = document.createElement('DIV')
+               dev_running.className = 'w3-container w3-cell w3-mobile w3-center';
+               var dev_running_img = document.createElement('I');
+               var dev_running_colinfo = document.createElement('SPAN');
+               dev_running_colinfo.className = this.css.device_colinfo;
+               dev_running_colinfo.innerHTML = '<%[ Status ]%>: &nbsp;';
+               var dev_running_desc = document.createElement('SPAN');
+               var dev_running_jobs_len = dev_running_jobs.length;
+               if (dev_running_jobs_len > 0) {
+                       dev_running_img.className = 'fas fa-cog fa-spin';
+                       dev_running_desc.innerHTML = '&nbsp; <%[ running ]%> (' + dev_running_jobs_len + ')';
+               } else {
+                       dev_running_img.className = 'fas fa-minus';
+                       dev_running_desc.innerHTML = '&nbsp; <%[ idle ]%>';
+               }
+               dev_running.appendChild(dev_running_colinfo);
+               dev_running.appendChild(dev_running_img);
+               dev_running.appendChild(dev_running_desc);
+
+               // add running jobs
+               if (dev_running_jobs.length > 0) {
+                       var dev_job_container = document.createElement('DIV');
+                       dev_job_container.setAttribute('rel', 'device_jobs_' + device.name);
+                       for (var i = 0; i < dev_running_jobs.length; i++) {
+                               this.add_running_job(dev_job_container, i, dev_running_jobs[i]);
+                       }
+                       container.appendChild(dev_job_container);
+               }
+
+               // device type
+               var dev_type = document.createElement('DIV');
+               dev_type.className = 'w3-container w3-cell w3-mobile w3-center';
+               var dev_type_colinfo = document.createElement('SPAN');
+               dev_type_colinfo.className = this.css.device_colinfo;
+               dev_type_colinfo.innerHTML = '<%[ Device type ]%>: &nbsp;';
+               dev_type_icon = document.createElement('I');
+               switch (device.type) {
+                       case 'Tape': {
+                               dev_type_icon.className = 'fas fa-tape';
+                               break;
+                       }
+                       case 'File': {
+                               dev_type_icon.className = 'fas fa-hdd';
+                               break;
+                       }
+                       case 'Fifo': {
+                               dev_type_icon.className = 'fas fa-forward';
+                               break;
+                       }
+                       default: {
+                               dev_type_icon.className = 'fas fa-hdd';
+                       }
+               }
+               var dev_type_desc = document.createElement('SPAN');
+               dev_type_desc.innerHTML = '&nbsp; ' + device.type;
+               dev_type.appendChild(dev_type_colinfo);
+               dev_type.appendChild(dev_type_icon);
+               dev_type.appendChild(dev_type_desc);
+
+               // media type
+               var dev_media_type = document.createElement('DIV')
+               dev_media_type.className = 'w3-container w3-cell w3-mobile w3-center';
+               var dev_media_type_img = document.createElement('I');
+               var dev_media_type_colinfo = document.createElement('SPAN');
+               dev_media_type_colinfo.className = this.css.device_colinfo;
+               dev_media_type_colinfo.innerHTML = '<%[ Media type ]%>: &nbsp;';
+               dev_media_type_img.className = 'fas fa-hashtag';
+               var dev_media_type_txt = document.createElement('SPAN');
+               dev_media_type_txt.innerHTML = ' &nbsp;' + device.media_type;
+               dev_media_type.appendChild(dev_media_type_colinfo);
+               dev_media_type.appendChild(dev_media_type_img);
+               dev_media_type.appendChild(dev_media_type_txt);
+
+               var set_space_gauge;
+               header.appendChild(dev_label);
+               header.appendChild(dev_running);
+               header.appendChild(dev_type);
+               header.appendChild(dev_media_type);
+               header.addEventListener('click', function(e) {
+                       var container = document.querySelector('div[rel="device_' + device.name + '"]');
+                       var open = this.getAttribute('data-open');
+                       if (open == 1) {
+                               $(container).slideUp();
+                               this.setAttribute('data-open', 0);
+                               dev_arrow_img.className = 'fas fa-chevron-down w3-margin-right';
+                       } else {
+                               $(container).slideDown('normal', function() {
+                                       set_space_gauge(true);
+                               });
+                               this.setAttribute('data-open', 1);
+                               dev_arrow_img.className = 'fas fa-chevron-up w3-margin-right';
+                       }
+               });
+
+               table.className = 'w3-table w3-stripped status_table device_table';
+
+               // Archive device
+               this.add_row(table, '<%[ Archive device: ]%>', device.archive_device);
+
+               // Maximum concurrent jobs
+               var mcj = device.maximum_concurrent_jobs == 0 ? '<%[ unlimited ]%>' : device.maximum_concurrent_jobs;
+               var running_max_jobs = dev_running_jobs.length + ' / ' + mcj;
+               this.add_row(table, '<%[ Running jobs ]%> / <%[ Maximum concurrent jobs: ]%>', running_max_jobs);
+
+               // Maximum volume size
+               var mvs = Units.get_formatted_size(device.maximum_volume_size);
+               this.add_row(table, '<%[ Maximum volume size: ]%>', mvs);
+
+               // Is enabled
+               var enabled = this.formatters.check(device.enabled);
+               this.add_row(table, '<%[ Enabled: ]%>', enabled);
+
+               // Is autoselect
+               var autoselect = this.formatters.check(device.autoselect);
+               this.add_row(table, 'AutoSelect:', autoselect);
+
+               // Is mounted
+               var mounted = this.formatters.check(device.mounted);
+               this.add_row(table, '<%[ Mounted: ]%>', mounted);
+
+               // Is opened
+               var open = this.formatters.check(device.open);
+               this.add_row(table, '<%[ Opened: ]%>', open);
+
+               // Is waiting
+               var waiting = this.formatters.check(device.waiting);
+               this.add_row(table, '<%[ Waiting: ]%>', waiting);
+
+               // Is blocked
+               var blocked = this.formatters.check(device.blocked);
+               this.add_row(table, '<%[ Blocked: ]%>', blocked);
+
+               // Blocked description
+               if (device.blocked_desc) {
+                       this.add_row(table, '<%[ Blocked description: ]%>', device.blocked_desc);
+               }
+
+               // Is read-only
+               var read_only = this.formatters.check(device.read_only);
+               this.add_row(table, '<%[ Read-only: ]%>', read_only);
+
+               // Pool
+               var pool = this.formatters.value(device.pool);
+               this.add_row(table, '<%[ Pool: ]%>', pool);
+
+               // Volume
+               var volume = this.formatters.value(device.volume);
+               this.add_row(table, '<%[ Volume: ]%>', volume);
+
+               // Free/used device space - part 1
+               var total_space = parseInt(device.total_space, 10);
+               var free_space = parseInt(device.free_space, 10);
+               var used_space = (total_space - free_space);
+               if (total_space)  {
+                       var space_container = document.createElement('DIV');
+                       space_container.className = this.css.gauge_container;
+                       var space_label = document.createElement('SPAN');
+                       space_label.className = this.css.gauge_label;
+                       space_label.textContent = Units.get_formatted_size(used_space);
+                       var space_gauge = document.createElement('CANVAS');
+                       space_gauge.setAttribute('rel', 'space');
+                       space_gauge.className = this.css.gauge;
+                       space_container.appendChild(space_gauge);
+                       space_container.appendChild(space_label);
+                       this.add_row(table, '<%[ Used space: ]%>', space_container);
+               } else {
+                       this.add_row(table, '<%[ Used space: ]%>', this.formatters.value(false));
+               }
+
+               // set default container visibility
+               if (header.getAttribute('data-open') == 0) {
+                       container.style.display = 'none';
+               }
+
+               set_space_gauge = function(open) {
+                       if (!total_space) {
+                               // simple used space text value, no gauge
+                               return;
+                       }
+                       var space_gauge = document.querySelector('div[rel="device_' + device.name + '"] canvas[rel="space"]');
+                       var total_space_gb, used_space_gb;
+                       if (typeof(SIZE_VALUES_UNIT) === 'string') {
+                               if (SIZE_VALUES_UNIT === 'decimal') {
+                                       total_space_gb = total_space/1000/1000/1000;
+                                       used_space_gb = used_space/1000/1000/1000;
+                               } else if (SIZE_VALUES_UNIT === 'binary') {
+                                       total_space_gb = total_space/1024/1024/1024;
+                                       used_space_gb = used_space/1024/1024/1024;
+                               }
+                       } else {
+                               // Default decimal bytes if unit not defined
+                               total_space_gb = total_space/1000/1000/1000;
+                               used_space_gb = used_space/1000/1000/1000;
+                       }
+
+                       var val1 = 0;
+                       var val2 = parseInt(total_space_gb/100*20, 10);
+                       var val3 = parseInt(total_space_gb/100*40, 10);
+                       var val4 = parseInt(total_space_gb/100*60, 10);
+                       var val5 = parseInt(total_space_gb/100*80, 10);
+                       var val6 = total_space_gb;
+                       var curr_opts = {
+                               staticLabels: {
+                                       labels: [val1, val2, val3, val4, val5, val6]  // Print labels at these values
+                               },
+                               staticZones: [
+                                       {strokeStyle: "#30B32D", min: 0, max: val4}, // Green
+                                       {strokeStyle: "#FFDD00", min: val4, max: val5}, // Yellow
+                                       {strokeStyle: "#F03E3E", min: val5, max: val6}  // Red
+                               ]
+                       };
+                       var opts = $.extend(true, curr_opts, this.gauges.space);
+                       var gauge = new Gauge(space_gauge).setOptions(opts); // create gauge
+                       gauge.maxValue = total_space_gb // set max gauge value
+                       gauge.setMinValue(0);  // Prefer setter over gauge.minValue = 0
+                       gauge.animationSpeed = open ? 80 : 1; // set animation speed
+                       gauge.set(used_space_gb); // set actual value
+               }.bind(this);
+
+               var devices_el;
+               if (device.autochanger) {
+                       devices_el = document.querySelector('div[rel="ach_' + device.autochanger + '"]');
+               } else {
+                       devices_el = document.getElementById(this.ids.devices);
+               }
+               var d = devices_el.querySelector('div[rel="device_' + device.name + '"]');
+               var h = devices_el.querySelector('div[rel="header_device_' + device.name + '"]');
+               if (!d && !h) {
+                       devices_el.appendChild(header);
+                       devices_el.appendChild(container);
+               } else {
+                       devices_el.replaceChild(container, d);
+                       devices_el.replaceChild(header, h);
+               }
+
+               // Free/used device space - part 2
+               set_space_gauge();
+
+               var gauge, cname;
+               for (var i = 0; i < dev_running_jobs.length; i++) {
+                       // last speed
+                       cname = 'last_speed';
+                       gauge = document.querySelector('div[rel="job_' + dev_running_jobs[i].jobid + '"] canvas[rel="' + cname + '"]');
+                       this.set_job_gauge(dev_running_jobs[i].jobid, dev_running_jobs[i].lastbytes_sec, gauge, cname);
+
+                       // average speed
+                       cname = 'ave_speed';
+                       gauge = document.querySelector('div[rel="job_' + dev_running_jobs[i].jobid + '"] canvas[rel="' + cname + '"]');
+                       this.set_job_gauge(dev_running_jobs[i].jobid, dev_running_jobs[i].avebytes_sec, gauge, cname);
+               }
+       },
+       add_running_job: function(main_container, job_nb, job) {
+               var container = document.createElement('DIV');
+               container.className = 'w3-border';
+               container.setAttribute('rel', 'job_' + job.jobid);
+               var table = document.createElement('TABLE');
+               table.className = 'w3-table w3-stripped status_table running_job_table';
+               container.appendChild(table);
+
+               // arrow icon
+               var job_arrow_img = document.createElement('I');
+               job_arrow_img.className = 'w3-margin-right';
+
+               var header = document.createElement('DIV');
+               var open = document.querySelector('div[rel="header_job_' + job.jobid + '"]');
+               if (open) {
+                       var data_open = open.getAttribute('data-open');
+                       header.setAttribute('data-open', data_open);
+                       if (data_open == 1) {
+                               job_arrow_img.className += ' fas fa-chevron-up';
+                       } else {
+                               job_arrow_img.className += ' fas fa-chevron-down';
+                       }
+               } else {
+                       header.setAttribute('data-open', 0);
+                       job_arrow_img.className += ' fas fa-chevron-down';
+               }
+               header.setAttribute('rel', 'header_job_' + job.jobid);
+               header.className = [
+                       this.css.status_header,
+                       this.css.running_job_header
+               ].join(' ');
+               // set default container visibility
+               if (header.getAttribute('data-open') == 0) {
+                       container.style.display = 'none';
+               }
+               var self = this;
+               header.addEventListener('click', function(e) {
+                       var container = document.querySelector('div[rel="job_' + job.jobid + '"]');
+                       var open = this.getAttribute('data-open');
+                       if (open == 1) {
+                               $(container).slideUp();
+                               this.setAttribute('data-open', 0);
+                               job_arrow_img.className = 'fas fa-chevron-down w3-margin-right';
+                       } else {
+                               $(container).slideDown('normal', function() {
+                                       // last speed
+                                       var cname = 'last_speed';
+                                       var gauge = document.querySelector('div[rel="job_' + job.jobid + '"] canvas[rel="' + cname + '"]');
+                                       self.set_job_gauge(job.jobid, job.lastbytes_sec, gauge, cname, true);
+
+                                       // average speed
+                                       var cname = 'ave_speed';
+                                       var gauge = document.querySelector('div[rel="job_' + job.jobid + '"] canvas[rel="' + cname + '"]');
+                                       self.set_job_gauge(job.jobid, job.avebytes_sec, gauge, cname, true);
+                               });
+                               this.setAttribute('data-open', 1);
+                               job_arrow_img.className = 'fas fa-chevron-up w3-margin-right';
+                       }
+               });
+
+               // job label
+               var job_label = document.createElement('DIV');
+               job_label.className = 'w3-container w3-cell w3-mobile';
+               job_label.style.flexBasis = '770px';
+               job_label.appendChild(job_arrow_img);
+               var job_label_txt = document.createTextNode('<%[ Job: ]%> #' + (job_nb+1) + ' ' + job.job);
+               job_label.appendChild(job_label_txt);
+               header.appendChild(job_label);
+
+               // device running
+               var job_running = document.createElement('DIV')
+               job_running.className = 'w3-container w3-cell w3-mobile';
+               var job_running_img = document.createElement('I');
+               var job_running_label = document.createElement('SPAN');
+               job_running_label.innerHTML = '<%[ Status: ]%> &nbsp;';
+               var job_running_desc = document.createElement('SPAN');
+               job_running_img.className = 'fas fa-cog fa-spin';
+               job_running_desc.innerHTML = '&nbsp; <%[ running ]%>';
+               job_running.appendChild(job_running_label);
+               job_running.appendChild(job_running_img);
+               job_running.appendChild(job_running_desc);
+               header.appendChild(job_running);
+
+               // JobId
+               var jobid_img = document.createElement('I');
+               jobid_img.className = 'fa fa-external-link-alt fa-xs';
+               var jobid_a = document.createElement('A');
+               jobid_a.href = '/web/job/history/' + job.jobid + '/';
+               jobid_a.appendChild(jobid_img);
+               jobid_a.title = '<%[ Go to job with jobid %jobid ]%>'.replace('%jobid', job.jobid);
+               var jobid = job.jobid + ' ' + jobid_a.outerHTML;
+               this.add_row(table, 'JobId', jobid);
+
+               // Type
+               var type = JobType.get_type(job.type);
+               this.add_row(table, '<%[ Type: ]%>', type);
+
+               // Level
+               var level = job.type === 'R' ? '-' : JobLevel.get_level(job.level);
+               this.add_row(table, '<%[ Level: ]%>', level);
+
+               // Last job speed
+               var last_speed = Units.format_speed(job.lastbytes_sec, null, true, true);
+               var last_job_speed = last_speed.value.toFixed(2) + ' ' + last_speed.format;
+               //this.add_row(table, '<%[ Last speed: ]%>', last_job_speed);
+
+               // Last speed - part 1
+               var last_speed_container = document.createElement('DIV');
+               last_speed_container.className = this.css.gauge_container;
+               var last_speed_label = document.createElement('SPAN');
+               last_speed_label.className = this.css.gauge_label;
+               last_speed_label.textContent = last_job_speed;
+               var last_speed_gauge = document.createElement('CANVAS');
+               last_speed_gauge.setAttribute('rel', 'last_speed');
+               last_speed_gauge.className = this.css.gauge;
+               last_speed_container.appendChild(last_speed_gauge);
+               last_speed_container.appendChild(last_speed_label);
+               this.add_row(table, '<%[ Last speed: ]%>', last_speed_container);
+
+               // Average job speed
+               var ave_speed = Units.format_speed(job.avebytes_sec, null, true, true);
+               var ave_job_speed = ave_speed.value.toFixed(2) + ' ' + ave_speed.format;
+               //this.add_row(table, '<%[ Average speed: ]%>', ave_job_speed);
+
+               // Average job speed - part 1
+               var ave_speed_container = document.createElement('DIV');
+               ave_speed_container.className = this.css.gauge_container;
+               var ave_speed_label = document.createElement('SPAN');
+               ave_speed_label.className = this.css.gauge_label;
+               ave_speed_label.textContent = ave_job_speed;
+               var ave_speed_gauge = document.createElement('CANVAS');
+               ave_speed_gauge.setAttribute('rel', 'ave_speed');
+               ave_speed_gauge.className = this.css.gauge;
+               ave_speed_container.appendChild(ave_speed_gauge);
+               ave_speed_container.appendChild(ave_speed_label);
+               this.add_row(table, '<%[ Average speed: ]%>', ave_speed_container);
+
+
+               // Processing file
+               if (job.hasOwnProperty('processing_file') && job.processing_file) {
+                       var processing_file = document.createElement('SPAN');
+                       processing_file.title = job.processing_file;
+                       if (job.processing_file.length > 60) {
+                               processing_file.textContent = job.processing_file.substr(0, 17) + ' (..) ' + job.processing_file.substr(-37);
+                       } else {
+                               processing_file.textContent = job.processing_file;
+                       }
+                       this.add_row(table, '<%[ Processing file: ]%>', processing_file.outerHTML);
+               }
+
+               var job_name = job.job.replace(/\.\d{4}-\d{2}-\d{2}_\d{2}.\d{2}.\d{2}_\d{2}$/, '');
+               var bytes = parseInt(job.jobbytes, 10);
+               var files = parseInt(job.jobfiles, 10);
+               files = files > 0 ? (files - 1) : 0;
+               var est = estimate_job(oData.jobs, job_name, job.level);
+
+               // Progress bar bytes
+               var bytes_progress;
+               if (job.type === 'B' && est.est_bytes > 0) {
+                       bytes_progress = document.createElement('DIV');
+                       bytes_progress.className = 'progressbar';
+                       bytes_progress.title = '<%[ Progress bar displays estimated values ]%>';
+                       var bytes_label = document.createElement('DIV');
+                       bytes_label.className = 'progressbar-label';
+                       var bytes_perc = ((100 * bytes) / est.est_bytes);
+                       if (bytes_perc > 100) {
+                               bytes_perc = 100;
+                       }
+                       bytes_label.textContent =  Units.get_formatted_size(bytes) + ' / <%[ est. ]%> ' +  Units.get_formatted_size(est.est_bytes) + ' (' + bytes_perc.toFixed(1) + '%' + ')';
+                       bytes_progress.style.width = '70%';
+                       bytes_progress.appendChild(bytes_label);
+                       var bytes_bar = $(bytes_progress);
+                       bytes_bar.progressbar({
+                               max: est.est_bytes,
+                               value: bytes
+                       });
+               } else {
+                       bytes_progress = '<%[ Not available ]%>';
+               }
+               this.add_row(table, '<%[ Byte progress bar: ]%>', bytes_progress);
+
+
+               // Progress bar files
+               var files_progress;
+               if (job.type === 'B' && est.est_files > 0) {
+                       files_progress = document.createElement('DIV');
+                       files_progress.className = 'progressbar';
+                       files_progress.title = '<%[ Progress bar displays estimated values ]%>';
+                       var files_label = document.createElement('DIV');
+                       files_label.className = 'progressbar-label';
+                       var files_perc = ((100 * files) / est.est_files);
+                       if (files_perc > 100) {
+                               files_perc = 100;
+                       }
+                       files_label.textContent =  files + ' / <%[ est. ]%> ' +  parseInt(est.est_files, 10) + ' (' + files_perc.toFixed(1) + '%' + ')';
+                       files_progress.style.width = '70%';
+                       files_progress.appendChild(files_label);
+                       var files_bar = $(files_progress);
+                       files_bar.progressbar({
+                               max: est.est_files,
+                               value: files
+                       });
+               } else if (job.type === 'R' && job.hasOwnProperty('expected_files') && job.expected_files > 0) {
+                       files_progress = document.createElement('DIV');
+                       files_progress.className = 'progressbar';
+                       var files_label = document.createElement('DIV');
+                       files_label.className = 'progressbar-label';
+                       var fexamined = parseInt(job.files_examined, 10);
+                       var fexpected = parseInt(job.expected_files, 10);
+                       var files_perc = ((100 * fexamined) / fexpected);
+                       if (files_perc > 100) {
+                               files_perc = 100;
+                       }
+                       files_label.textContent =  fexamined + ' / ' +  fexpected + ' (' + files_perc.toFixed(1) + '%' + ')';
+                       files_progress.style.width = '70%';
+                       files_progress.appendChild(files_label);
+                       var files_bar = $(files_progress);
+                       files_bar.progressbar({
+                               max: fexpected,
+                               value: fexamined
+                       });
+               } else {
+                       files_progress = '<%[ Not available ]%>';
+               }
+               this.add_row(table, '<%[ File progress bar: ]%>', files_progress);
+
+               // Read pool
+               if (job.read_pool) {
+                       this.add_row(table, '<%[ Read pool: ]%>', job.read_pool);
+               }
+               // Read device
+               if (job.read_device) {
+                       this.add_row(table, '<%[ Read device: ]%>', job.read_device);
+               }
+               // Read pool
+               if (job.read_volume) {
+                       this.add_row(table, '<%[ Read volume: ]%>', job.read_volume);
+               }
+
+               // Write pool
+               if (job.write_pool) {
+                       this.add_row(table, '<%[ Write pool: ]%>', job.write_pool);
+               }
+               // Write device
+               if (job.write_device) {
+                       this.add_row(table, '<%[ Write device: ]%>', job.write_device);
+               }
+               // Write pool
+               if (job.write_volume) {
+                       this.add_row(table, '<%[ Write volume: ]%>', job.write_volume);
+               }
+
+               // Job errors
+               this.add_row(table, '<%[ Job errors: ]%>', job.errors);
+
+               // Job bytes
+               var job_bytes = Units.get_formatted_size(job.jobbytes);
+               this.add_row(table, '<%[ Job bytes: ]%>', job_bytes);
+
+               // Examined files
+               this.add_row(table, '<%[ Job files: ]%>', job.jobfiles);
+
+               main_container.appendChild(header);
+               main_container.appendChild(container);
+       },
+       add_terminated_job: function(job_nb, job) {
+               var main_container = document.getElementById(this.ids.terminated);
+               var container = document.createElement('DIV');
+               container.className = [
+                       this.css.terminated_job_table,
+                       'w3-border'
+               ].join(' ');
+               container.setAttribute('rel', 'job_terminated_' + job.jobid);
+               var table = document.createElement('TABLE');
+               table.className = 'w3-table w3-stripped status_table';
+               container.appendChild(table);
+
+               // arrow icon
+               var job_arrow_img = document.createElement('I');
+               job_arrow_img.className = 'w3-margin-right';
+
+               var header = document.createElement('DIV');
+               var open = document.querySelector('div[rel="header_job_terminated_' + job.jobid + '"]');
+               if (open) {
+                       var data_open = open.getAttribute('data-open');
+                       header.setAttribute('data-open', data_open);
+                       if (data_open == 1) {
+                               job_arrow_img.className += ' fas fa-chevron-up';
+                       } else {
+                               job_arrow_img.className += ' fas fa-chevron-down';
+                       }
+               } else {
+                       header.setAttribute('data-open', 0);
+                       job_arrow_img.className += ' fas fa-chevron-down';
+               }
+               header.setAttribute('rel', 'header_job_terminated_' + job.jobid);
+               header.className = [
+                       this.css.status_header,
+                       this.css.terminated_job_header
+               ].join(' ');
+               // set default container visibility
+               if (header.getAttribute('data-open') == 0) {
+                       container.style.display = 'none';
+               }
+               var self = this;
+               header.addEventListener('click', function(e) {
+                       var container = document.querySelector('div[rel="job_terminated_' + job.jobid + '"]');
+                       var open = this.getAttribute('data-open');
+                       if (open == 1) {
+                               $(container).slideUp();
+                               this.setAttribute('data-open', 0);
+                               job_arrow_img.className = 'fas fa-chevron-down w3-margin-right';
+                       } else {
+                               $(container).slideDown();
+                               this.setAttribute('data-open', 1);
+                               job_arrow_img.className = 'fas fa-chevron-up w3-margin-right';
+                       }
+               });
+
+               // job label
+               var job_label = document.createElement('DIV');
+               job_label.className = 'w3-container w3-cell w3-mobile';
+               job_label.style.flexBasis = '770px';
+               job_label.appendChild(job_arrow_img);
+               var job_label_txt = document.createTextNode('<%[ Job: ]%> #' + (job_nb+1) + ' ' + job.job);
+               job_label.appendChild(job_label_txt);
+               header.appendChild(job_label);
+
+               // device terminated
+               var job_terminated = document.createElement('DIV')
+               job_terminated.className = 'w3-container w3-cell w3-mobile';
+               var job_terminated_img = document.createElement('I');
+               var job_terminated_label = document.createElement('SPAN');
+               job_terminated_label.innerHTML = '<%[ Status: ]%> &nbsp;';
+               var job_terminated_desc = document.createElement('SPAN');
+               job_terminated_desc.innerHTML = JobStatus.get_icon(job.status).outerHTML;
+               job_terminated.appendChild(job_terminated_label);
+               job_terminated.appendChild(job_terminated_img);
+               job_terminated.appendChild(job_terminated_desc);
+               header.appendChild(job_terminated);
+
+               // JobId
+               var jobid_img = document.createElement('I');
+               jobid_img.className = 'fa fa-external-link-alt fa-xs';
+               var jobid_a = document.createElement('A');
+               jobid_a.href = '/web/job/history/' + job.jobid + '/';
+               jobid_a.appendChild(jobid_img);
+               jobid_a.title = '<%[ Go to job with jobid %jobid ]%>'.replace('%jobid', job.jobid);
+               var jobid = job.jobid + ' ' + jobid_a.outerHTML;
+               this.add_row(table, 'JobId', jobid);
+
+               // Type
+               var type = JobType.get_type(job.type);
+               this.add_row(table, '<%[ Type: ]%>', type);
+
+               // Start time
+               var starttime = Units.format_date(job.starttime_epoch);
+               this.add_row(table, '<%[ Start time: ]%>', starttime);
+
+               // End time
+               var endtime = Units.format_date(job.endtime_epoch);
+               this.add_row(table, '<%[ End time: ]%>', endtime);
+
+               // Level
+               var level = job.type === 'R' ? '-' : JobLevel.get_level(job.level);
+               this.add_row(table, '<%[ Level: ]%>', level);
+
+               // Job errors
+               this.add_row(table, '<%[ Job errors: ]%>', job.errors);
+
+               // Job bytes
+               var job_bytes = Units.get_formatted_size(job.jobbytes);
+               this.add_row(table, '<%[ Job bytes: ]%>', job_bytes);
+
+               // Examined files
+               this.add_row(table, '<%[ Job files: ]%>', job.jobfiles);
+
+               var d = main_container.querySelector('div[rel="job_terminated_' + job.jobid + '"]');
+               if (d) {
+                       main_container.replaceChild(container, d);
+               } else {
+                       main_container.appendChild(header);
+                       main_container.appendChild(container);
+               }
+       },
+       set_gauge: function(gauge, options, value) {
+               var opts = $.extend(true, {}, this.gauges.speed, options);
+               var gauge = new Gauge(gauge).setOptions(opts); // create gauge
+               gauge.maxValue = options.maxValue; // set max gauge value
+               gauge.setMinValue(0);  // Prefer setter over gauge.minValue = 0
+               gauge.animationSpeed = options.animationSpeed; // set animation speed
+               gauge.set(value); // set actual value
+       },
+       set_job_gauge: function(jobid, value, gauge, name, open) {
+               value = parseInt(value, 10);
+               var value_kbps = value/1000; // covert to KB/s
+               var prev_job = this.get_running_job(jobid);
+               var max_value_kbps = 0;
+               if (prev_job) {
+                       if (prev_job.hasOwnProperty(name)) {
+                               max_value_kbps = prev_job[name];
+                       }
+               }
+               if (value_kbps > max_value_kbps) {
+                       max_value_kbps = value_kbps;
+               }
+
+               var val1 = 0;
+               var val2 = parseInt(max_value_kbps/100*20, 10);
+               var val3 = parseInt(max_value_kbps/100*40, 10);
+               var val4 = parseInt(max_value_kbps/100*60, 10);
+               var val5 = parseInt(max_value_kbps/100*80, 10);
+               var val6 = max_value_kbps;
+               var options = {
+                       staticLabels: {
+                               labels: [val1, val2, val3, val4, val5, val6]  // Print labels at these values
+                       },
+                       staticZones: [
+                               {strokeStyle: "#30B32D", min: 0, max: val4}, // Green
+                               {strokeStyle: "#FFDD00", min: val4, max: val5}, // Yellow
+                               {strokeStyle: "#F03E3E", min: val5, max: val6}  // Red
+                       ],
+                       animationSpeed: (open ? 80 : 1),
+                       maxValue: max_value_kbps
+               };
+               this.set_gauge(gauge, options, value_kbps);
+               if (prev_job) {
+                       prev_job[name] = max_value_kbps;
+                       this.update_running_job(prev_job);
+               }
+       },
+       set_refresh_timeout: function(timeout) {
+               timeout = parseInt(timeout, 10) * 1000;
+               if (isNaN(timeout)) {
+                       return;
+               }
+               if (this.refresh_timeout !== null) {
+                       clearTimeout(this.refresh_timeout);
+               }
+               if (timeout === 0) {
+                       return;
+               }
+               this.refresh_timeout = setTimeout(function() {
+                       this.update_status();
+               }.bind(this), timeout);
+       },
+       update_status: function() {
+               $('#<%=$this->StorageStatusBtn->ClientID%>').click();
+       },
+       is_status_supported: function() {
+               var supported = false;
+               var not_supported = document.getElementById(this.ids.status_not_supported);
+               var graphical_container = document.getElementById(this.ids.graphical_container);
+               if (this.data && this.data.hasOwnProperty('version') && this.data.version.hasOwnProperty('major') && this.data.version.major >= 9 && this.data.version.minor >= 0 && this.data.version.release >= 0) {
+                       supported = true;
+                       not_supported.style.display = 'none';
+                       graphical_container.style.display = '';
+               } else if (not_supported.style.display == 'none') {
+                       not_supported.style.display = '';
+                       graphical_container.style.display = 'none';
+                       W3SubTabs.open('status_storage_subtab_text', 'status_storage_text_output');
+               }
+               return supported;
+       },
+       get_device_jobs: function(device) {
+               var jobs = [];
+               if (this.data.hasOwnProperty('running')) {
+                       for (var i = 0; i < this.data.running.length; i++) {
+                               if (this.data.running[i].read_device === device || this.data.running[i].write_device === device) {
+                                       jobs.push(this.data.running[i]);
+                               }
+                       }
+               }
+               return jobs;
+       },
+       get_running_job: function(jobid) {
+               var job;
+               for (var i = 0; i < this.running_jobs.length; i++) {
+                       if (this.running_jobs[i].jobid === jobid) {
+                               job = this.running_jobs[i];
+                               break;
+                       }
+               }
+               return job;
+       },
+       update_running_job: function(job) {
+               var update = false;
+               for (var i = 0; i < this.running_jobs.length; i++) {
+                       if (this.running_jobs[i].jobid === job.jobid) {
+                               this.running_jobs[i] = job;
+                               update = true;
+                               break;
+                       }
+               }
+               return update;
+       }
+};
+
+function hide_action_text_output(e) {
+       if (e.hasOwnProperty('originalEvent') && e.originalEvent.type == 'click') {
+               $('#storage_action_text_output').slideUp('fast');
+       }
+}
+
+function init_graphical_storage_status(data) {
+       oGraphicalStorageStatus.update(data);
+}
+function status_storage_show_error(error) {
+       var errmsg = error;
+       if (error === 'timeout') {
+               errmsg = '<%[ Status request timed out. The most probably the Bacula storage is not available or it is not running. ]%>';
+       }
+       var err_el = document.getElementById('status_storage_error');
+       err_el.textContent = errmsg;
+       err_el.style.display = '';
+}
+oGraphicalStorageStatus.init();
+MonitorParams = {
+       jobs: null
+};
+</script>
+                               </div>
+                               <div id="status_storage_text_output" class="w3-code subtab_item" style="display: none">
+                                       <pre><com:TActiveLabel ID="StorageLog" /></pre>
+                               </div>
                        </div>
                </div>
        </div>
index 45bb7c71f61d6d3f0ac4d0230237c2865effa6cc..e693b801a1b0a722b6c7cf81e70e4eb39a454c51 100644 (file)
@@ -25,6 +25,7 @@ Prado::using('System.Web.UI.ActiveControls.TActivePanel');
 Prado::using('System.Web.UI.ActiveControls.TActiveTextBox');
 Prado::using('System.Web.UI.ActiveControls.TActiveRepeater');
 Prado::using('System.Web.UI.ActiveControls.TActiveLinkButton');
+Prado::using('Application.Common.Class.Params');
 Prado::using('Application.Web.Class.BaculumWebPage'); 
 
 /**
@@ -76,7 +77,7 @@ class StorageView extends BaculumWebPage {
                $storageshow = $this->Application->getModule('api')->get(
                        array('storages', $storage->storageid, 'show')
                )->output;
-               $this->StorageLog->Text = implode(PHP_EOL, $storageshow);
+               $this->StorageActionLog->Text = implode(PHP_EOL, $storageshow);
                $this->setStorageDevice($storageshow);
                $this->setDevices();
        }
@@ -197,51 +198,113 @@ class StorageView extends BaculumWebPage {
        }
 
        public function status($sender, $param) {
-               $status = $this->getModule('api')->get(
-                       array('storages', $this->getStorageId(), 'status')
+               $raw_status = $this->getModule('api')->get(
+                       ['storages', $this->getStorageId(), 'status']
                )->output;
-               $this->StorageLog->Text = implode(PHP_EOL, $status);
+               $this->StorageLog->Text = implode(PHP_EOL, $raw_status);
+
+               $query_str = '?name=' . rawurlencode($this->getStorageName()) . '&type=header';
+               $graph_status = $this->getModule('api')->get(
+                       ['status', 'storage', $query_str]
+               );
+               $storage_status = [
+                       'header' => [],
+                       'devices' => [],
+                       'running' => [],
+                       'terminated' => [],
+                       'version' => Params::getComponentVersion($raw_status)
+               ];
+               if ($graph_status->error === 0) {
+                       $storage_status['header'] = $graph_status->output;
+               }
+
+               // running
+               $query_str = '?name=' . rawurlencode($this->getStorageName()) . '&type=running';
+               $graph_status = $this->getModule('api')->get(
+                       array('status', 'storage', $query_str)
+               );
+               if ($graph_status->error === 0) {
+                       $storage_status['running'] = $graph_status->output;
+               }
+
+               // terminated
+               $query_str = '?name=' . rawurlencode($this->getStorageName()) . '&type=terminated';
+               $graph_status = $this->getModule('api')->get(
+                       array('status', 'storage', $query_str)
+               );
+               if ($graph_status->error === 0) {
+                       $storage_status['terminated'] = $graph_status->output;
+               }
+
+               // devices
+               $query_str = '?name=' . rawurlencode($this->getStorageName()) . '&type=devices';
+               $graph_status = $this->getModule('api')->get(
+                       array('status', 'storage', $query_str)
+               );
+               if ($graph_status->error === 0) {
+                       $storage_status['devices'] = $graph_status->output;
+               }
+
+               // show
+               $query_str = '?output=json';
+               $show = $this->getModule('api')->get(
+                       array('storages', 'show', $query_str)
+               );
+               if ($show->error === 0) {
+                       $storage_status['show'] = $show->output;
+               }
+
+               $this->getCallbackClient()->callClientFunction('init_graphical_storage_status', [$storage_status]);
        }
 
        public function mount($sender, $param) {
                $drive = $this->getIsAutochanger() ? intval($this->Drive->Text) : 0;
                $slot = $this->getIsAutochanger() ? intval($this->Slot->Text) : 0;
-               $query = '?drive=' . rawurlencode($drive);
-               $query .= 'slot=' . rawurlencode($slot);
+               $params = [
+                       'drive' => $drive,
+                       'slot' => $slot
+               ];
+               $query = '?' . http_build_query($params);
                $mount = $this->getModule('api')->get(
                        array('storages', $this->getStorageId(), 'mount', $query)
                );
                if ($mount->error === 0) {
-                       $this->StorageLog->Text = implode(PHP_EOL, $mount->output);
+                       $this->StorageActionLog->Text = implode(PHP_EOL, $mount->output);
                } else {
-                       $this->StorageLog->Text = $mount->output;
+                       $this->StorageActionLog->Text = $mount->output;
                }
        }
 
        public function umount($sender, $param) {
                $drive = $this->getIsAutochanger() ? intval($this->Drive->Text) : 0;
-               $query = '?drive=' . rawurlencode($drive);
+               $params = [
+                       'drive' => $drive
+               ];
+               $query = '?' . http_build_query($params);
                $umount = $this->getModule('api')->get(
                        array('storages', $this->getStorageId(), 'umount', $query)
                );
                if ($umount->error === 0) {
-                       $this->StorageLog->Text = implode(PHP_EOL, $umount->output);
+                       $this->StorageActionLog->Text = implode(PHP_EOL, $umount->output);
                } else {
-                       $this->StorageLog->Text = $umount->output;
+                       $this->StorageActionLog->Text = $umount->output;
                }
 
        }
 
        public function release($sender, $param) {
                $drive = $this->getIsAutochanger() ? intval($this->Drive->Text) : 0;
-               $query = '?drive=' . rawurlencode($drive);
+               $params = [
+                       'drive' => $drive
+               ];
+               $query = '?' . http_build_query($params);
                $release = $this->getModule('api')->get(
                        array('storages', $this->getStorageId(), 'release', $query)
                );
                if ($release->error === 0) {
-                       $this->StorageLog->Text = implode(PHP_EOL, $release->output);
+                       $this->StorageActionLog->Text = implode(PHP_EOL, $release->output);
                } else {
-                       $this->StorageLog->Text = $release->output;
+                       $this->StorageActionLog->Text = $release->output;
                }
        }
 
index 74e617e4ca5a44d3f11ef70b2352d5b5dc88fddd..14dd9635ee76f7ac738810f84fc8f24be56cbd08 100644 (file)
@@ -446,3 +446,73 @@ table.component td:nth-of-type(1) {
        width: 420px;
        margin: 10px;
 }
+
+.gauge, .gauge_container {
+       width: 300px;
+       height: 150px;
+}
+
+.gauge_label {
+       display: inline-block;
+       position: relative;
+       top: -44px;
+       left: 122px;
+}
+
+.device_colinfo {
+       display: none;
+}
+
+.status_header, .device_columns {
+       padding: 5px;
+       font-size: 17px;
+       margin: 3px 0;
+       display: flex;
+}
+
+.device_columns > div {
+       text-align: center;
+       font-weight: bold;
+}
+
+
+.status_header {
+       cursor: pointer;
+       border: 1px solid #dddddd;
+}
+
+.status_header:hover {
+       background-color: #e9e9e9;
+       border-radius: 3px;
+}
+
+.status_header > div, .device_columns > div {
+       flex-basis: 385px;
+}
+
+#status_storage_filters select {
+       width: 250px;
+       margin-left: 10px;
+       margin-top: 5px;
+}
+
+@media only screen and (max-width: 670px) {
+       .gauge, .gauge_container {
+               width: 150px;
+               height: 75px;
+       }
+
+       .gauge_label {
+               top: -19px;
+               left: 45px;
+       }
+       .status_header {
+               flex-wrap: wrap;
+       }
+       .device_columns {
+               display: none;
+       }
+       .device_colinfo {
+               display: inline;
+       }
+}