]> git.ipfire.org Git - thirdparty/bacula.git/commitdiff
baculum: Add graphs to job view page
authorMarcin Haba <marcin.haba@bacula.pl>
Sun, 1 Nov 2020 04:42:04 +0000 (05:42 +0100)
committerMarcin Haba <marcin.haba@bacula.pl>
Sun, 1 Nov 2020 04:42:04 +0000 (05:42 +0100)
There also have been changed:

 - Fix selecting pool for copy or migrate jobs running from job
   history page by 'Run job again' job button
 - Unified getting information about job from show job output
 - Improved pie graphs look

18 files changed:
gui/baculum/protected/Web/Class/JobInfo.php [new file with mode: 0644]
gui/baculum/protected/Web/JavaScript/graph.js
gui/baculum/protected/Web/JavaScript/misc.js
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/Pages/JobHistoryList.php
gui/baculum/protected/Web/Pages/JobHistoryView.php
gui/baculum/protected/Web/Pages/JobView.page
gui/baculum/protected/Web/Pages/JobView.php
gui/baculum/protected/Web/Pages/config.xml
gui/baculum/protected/Web/Portlets/RunJob.php
gui/baculum/themes/Baculum-v2/css/baculum.css

diff --git a/gui/baculum/protected/Web/Class/JobInfo.php b/gui/baculum/protected/Web/Class/JobInfo.php
new file mode 100644 (file)
index 0000000..5da6ba8
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2020 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+Prado::using('Application.:Web.Class.WebModule');
+
+/**
+ * Module responsible for providing information about job.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category Module
+ * @package Baculum Web
+ */
+class JobInfo extends WebModule {
+
+       const RESOURCE_PATTERN = '/(?P<resource>\S+(?=:))?:?(\s+((?P<directive>\S+)=(?P<value>[\s\S]*?(?=\s\S+=.+|$))))/';
+
+       public function parseResourceDirectives(array $show_out) {
+               $result = [];
+               $resource = [];
+               $res = null;
+               for ($i = 1; $i < count($show_out); $i++) {
+                       if (preg_match_all(self::RESOURCE_PATTERN, $show_out[$i], $match) > 0) {
+                               if (!empty($match['resource'][0])) {
+                                       if (count($resource) == 1)  {
+                                               /**
+                                                * Check key to not overwrite already existing resource
+                                                * because in some cases there can be for example two
+                                                * Autochanger resources: one from Pool and second from NextPool.
+                                                */
+                                               if (!key_exists($res, $result))  {
+                                                       $result = array_merge($result, $resource);
+                                               }
+                                               $resource = [];
+                                       }
+                                       $res = strtolower($match['resource'][0]);
+                               }
+                               if (!key_exists($res, $resource)) {
+                                       $resource[$res] = [];
+                               }
+                               for ($j = 0; $j < count($match['directive']); $j++) {
+                                       $directive = strtolower($match['directive'][$j]);
+                                       $value = $match['value'][$j];
+                                       $resource[$res][$directive] = $value;
+                               }
+                       }
+               }
+               if (count($resource) == 1) {
+                       $result = array_merge($result, $resource);
+               }
+               return $result;
+       }
+}
+?>
index 9e60632995eca79694646664e524555b8d498882..e4655ea6f89231217e43a56ea822d74048c997b1 100644 (file)
@@ -1238,13 +1238,14 @@ var GraphPieClass = jQuery.klass({
        series: null,
        pie: null,
        graph_options: {
-               colors: ['#63c422', '#d70808', '#FFFF66', 'orange', 'blue'],
+               colors: ['#63c422', '#d70808', '#FFFF66', 'orange', '#2980B9'],
                HtmlText: false,
                fontColor: '#000000',
                grid: {
                        verticalLines : false,
                        horizontalLines : false,
                        outlineWidth: 0,
+                       color: 'black'
                },
                xaxis: { showLabels : false,},
                yaxis: { showLabels : false },
@@ -1253,7 +1254,6 @@ var GraphPieClass = jQuery.klass({
                        explode : 6,
                        labelFormatter: PieGraph.pie_label_formatter,
                        shadowSize: 4,
-                       fillOpacity: 1,
                        sizeRatio: 0.77
                },
                mouse: {
@@ -1268,9 +1268,10 @@ var GraphPieClass = jQuery.klass({
                        margin: 0
                }
        },
-       initialize: function(jobs, container_id) {
-               this.jobs = jobs;
-               this.container = document.getElementById(container_id);
+       initialize: function(prop) {
+               this.jobs = prop.jobs;
+               this.title = prop.hasOwnProperty('title') ? prop.title : null;
+               this.container = document.getElementById(prop.container_id);
                this.series = this.prepare_series();
                this.draw_grah();
        },
@@ -1284,14 +1285,21 @@ var GraphPieClass = jQuery.klass({
                        jobs_count = this.jobs[label].length;
                        serie = {
                                data: [[0, jobs_count]],
-                               label: label + ' (' + jobs_count.toString() + ')'
+                               label: label + ' (' + jobs_count.toString() + ')',
+                               pie: {
+                                       explode: 12
+                               }
                        }
                        series.push(serie);
                }
                return series;
        },
        draw_grah: function() {
-               this.pie = Flotr.draw(this.container, this.series, this.graph_options);
+               var graph_options = $.extend({}, this.graph_options);
+               if (this.title) {
+                       graph_options.title = this.title;
+               }
+               this.pie = Flotr.draw(this.container, this.series, graph_options);
        }
 });
 
index 1bca58b070efbd27d67e13a803e5fd7fa8b92d6e..2f429f1fff47c77b244f91bd1c0d71fe32f605ce 100644 (file)
@@ -797,7 +797,10 @@ var Dashboard = {
                if (this.pie != null) {
                        this.pie.pie.destroy();
                }
-               this.pie = new GraphPieClass(this.stats.jobs_summary, this.ids.pie_summary);
+               this.pie = new GraphPieClass({
+                       jobs: this.stats.jobs_summary,
+                       container_id: this.ids.pie_summary
+               });
        }
 }
 
index 9a2a56d63656ace9f8b4e5fff3bfd6647bdef78e..6d79dd3738b800f4c4c2d688f8d950b86793e2b7 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 11ee4e7991554358555c54e44c92a5fdf82d8f3a..9992452e5d80b0f78e5a3262bf5f00dbcd7b1950 100644 (file)
@@ -3082,3 +3082,6 @@ msgstr "saved"
 
 msgid "deleted"
 msgstr "deleted"
+
+msgid "last %days days"
+msgstr "last %days days"
index 1bc58cd094f26562d2c24fb6076a17856a247819..9f73d7e3239bd259daee5b4d4e91d5dad47625e8 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 72415e8207d3ad91c252a6991dee83cdc42f8e99..da88b94b54245e262485330c96191499770547d5 100644 (file)
@@ -3168,3 +3168,6 @@ msgstr "saved"
 
 msgid "deleted"
 msgstr "deleted"
+
+msgid "last %days days"
+msgstr "last %days days"
index 12b8c083d9c81e09611d0e251424c0da17f76475..c9aeb04ecf151126f8be1cd8d126c5f7f0e41583 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 5b593d85fac75d1471d392bdc9af4aec9c1e23c6..23e644bef67e95b831b1de08b811c01ecf310d88 100644 (file)
@@ -3093,3 +3093,6 @@ msgstr "zapisany"
 
 msgid "deleted"
 msgstr "skasowany"
+
+msgid "last %days days"
+msgstr "ostatnie %days dni"
index da0155da423bfafefa370237744982f772bf700e..ac649390b490ec49bcb97b092eb2d9fb500fd34c 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 2154506f1e441f9f6fd15c617e702fc1f6a8da3b..c0bee3ad96e597eee31ec452a4cb483a273111b7 100644 (file)
@@ -3092,3 +3092,6 @@ msgstr "saved"
 
 msgid "deleted"
 msgstr "deleted"
+
+msgid "last %days days"
+msgstr "last %days days"
index ffd20d02c26b0e82549b7093bd8a0528085d557c..6fba287fbf043679d3633e3d4e4bd2e4f1fb28af 100644 (file)
@@ -36,6 +36,8 @@ class JobHistoryList extends BaculumWebPage {
 
        const USE_CACHE = true;
 
+       const DEFAULT_JOB_PRIORITY = 10;
+
        public function loadRunJobModal($sender, $param) {
                $this->RunJobModal->loadData();
        }
@@ -54,26 +56,35 @@ class JobHistoryList extends BaculumWebPage {
                        $level = trim($jobdata->level);
                        $params['level'] = !empty($level) ? $level : 'F'; // Admin job has empty level
                        $job_show = $this->getModule('api')->get(
-                               array('jobs', $jobid, 'show'), null, true, self::USE_CACHE
+                               ['jobs', $jobid, 'show'],
+                               null,
+                               true,
+                               self::USE_CACHE
                        )->output;
+                       $job_info = $this->getModule('job_info')->parseResourceDirectives($job_show);
                        if ($jobdata->filesetid > 0) {
                                $params['filesetid'] = $jobdata->filesetid;
                        } else {
-                               $params['fileset'] = RunJob::getResourceName('fileset', $job_show);
+                               $params['fileset'] = key_exists('fileset', $job_info) ? $job_info['fileset']['name'] : '';
                        }
                        $params['clientid'] = $jobdata->clientid;
-                       $params['storage'] = RunJob::getResourceName('storage', $job_show);
-                       if (empty($params['storage'])) {
-                               $params['storage'] = RunJob::getResourceName('autochanger', $job_show);
-                       }
-                       if ($jobdata->poolid > 0) {
+                       $storage = key_exists('storage', $job_info) ? $job_info['storage']['name'] : null;
+                       $autochanger = key_exists('autochanger', $job_info) ? $job_info['autochanger']['name'] : null;
+                       $params['storage'] = $storage ?: $autochanger;
+
+                       /**
+                        * For 'c' type (Copy Job) and 'g' type (Migration Job) the in job table in poolid property is written
+                        * write pool, not read pool. Here in 'pool' property is set read pool and from this reason for 'c'
+                        * and 'g' types the pool cannot be taken from job table.
+                        */
+                       if ($jobdata->poolid > 0 && $jobdata->type != 'c' && $jobdata->type != 'g') {
                                $params['poolid'] = $jobdata->poolid;
                        } else {
-                               $params['pool'] = RunJob::getResourceName('pool', $job_show);
+                               $params['pool'] = key_exists('pool', $job_info) ? $job_info['pool']['name'] : '';
                        }
-                       $job_attr = RunJob::getJobAttr($job_show);
-                       $params['priority'] = key_exists('priority', $job_attr) ? $job_attr['priority'] : 10;
-                       $params['accurate'] = (key_exists('accurate', $job_attr) && $job_attr['accurate'] == 1);
+                       $params['priority'] = key_exists('job', $job_info) ? $job_info['job']['priority'] : self::DEFAULT_JOB_PRIORITY;
+                       $accurate = key_exists('job', $job_info) && key_exists('accurate', $job_info['job']) ? $job_info['job']['accurate'] : 0;
+                       $params['accurate'] =  ($accurate == 1);
 
                        $result = $this->getModule('api')->create(array('jobs', 'run'), $params);
                        if ($result->error === 0) {
index b9cadaad8a6998e4093a378b0d0e3a2c9f1975b4..0d9f9eefab37bfadd2477d7d849f8dcfe6f37522 100644 (file)
@@ -42,14 +42,13 @@ class JobHistoryView extends BaculumWebPage {
        const JOB_LEVEL = 'JobLevel';
        const JOB_TYPE = 'JobType';
        const CLIENTID = 'ClientId';
+       const JOB_INFO = 'JobInfo';
 
        const USE_CACHE = true;
 
        const SORT_ASC = 0;
        const SORT_DESC = 1;
 
-       const RESOURCE_SHOW_PATTERN = '/^\s+--> %resource: name=(.+?(?=\s\S+\=.+)|.+$)/i';
-
        public $is_running = false;
        public $allow_graph_mode = false;
        public $allow_list_files_mode = false;
@@ -91,9 +90,15 @@ class JobHistoryView extends BaculumWebPage {
 
        public function onInit($param) {
                parent::onInit($param);
+               $this->JobConfig->attachEventHandler('OnSave', [$this, 'reloadJobInfo']);
+               $job_name = $this->getJobName();
                $this->RunJobModal->setJobId($this->getJobId());
-               $this->RunJobModal->setJobName($this->getJobName());
+               $this->RunJobModal->setJobName($job_name);
                $this->FileList->setJobId($this->getJobId());
+               if ($this->IsCallBack || $this->IsPostBack) {
+                       return;
+               }
+               $this->setJobInfo($job_name);
        }
 
        public function onLoad($param) {
@@ -276,6 +281,45 @@ class JobHistoryView extends BaculumWebPage {
                return $this->getViewState(self::JOB_LEVEL);
        }
 
+       /**
+        * Set job information from show job output.
+        *
+        * @return none
+        */
+       public function setJobInfo($job_name) {
+               $job_show = $this->getModule('api')->get(
+                       array('jobs', 'show', '?name='. rawurlencode($job_name)),
+                       null,
+                       true,
+                       false
+               );
+               if ($job_show->error == 0) {
+                       $job_info = $this->getModule('job_info')->parseResourceDirectives($job_show->output);
+                       $this->setViewState(self::JOB_INFO, $job_info);
+               }
+       }
+
+       /**
+        * Get job information.
+        *
+        * @return array job information
+        */
+       public function getJobInfo() {
+               return $this->getViewState(self::JOB_INFO, []);
+       }
+
+
+       /**
+        * Reload job information.
+        *
+        * @param mixed $param save event parameter
+        * @return none
+        */
+       public function reloadJobInfo($param) {
+               $job_name = $this->getJobName();
+               $this->setJobInfo($job_name);
+       }
+
        /**
         * Refresh job log page and load latest logs.
         *
@@ -340,41 +384,27 @@ class JobHistoryView extends BaculumWebPage {
                }
        }
 
-       public function getResourceName($resource, $jobshow) {
-               $resource_name = null;
-               $pattern = str_replace('%resource', $resource, self::RESOURCE_SHOW_PATTERN);
-               for ($i = 0; $i < count($jobshow); $i++) {
-                       if (preg_match($pattern, $jobshow[$i], $match) === 1) {
-                               $resource_name = $match[1];
-                               break;
-                       }
-               }
-               return $resource_name;
-       }
-
        public function loadFileSetConfig($sender, $param) {
                if (!empty($_SESSION['dir'])) {
-                       $jobshow = $this->getModule('api')->get(array(
-                               'jobs', $this->getJobId(), 'show'
-                       ))->output;
-                       $fileset = $this->getResourceName('fileset', $jobshow);
-                       $this->FileSetConfig->setComponentName($_SESSION['dir']);
-                       $this->FileSetConfig->setResourceName($fileset);
-                       $this->FileSetConfig->setLoadValues(true);
-                       $this->FileSetConfig->raiseEvent('OnDirectiveListLoad', $this, null);
+                       $job_info = $this->getJobInfo();
+                       if (key_exists('fileset', $job_info)) {
+                               $this->FileSetConfig->setComponentName($_SESSION['dir']);
+                               $this->FileSetConfig->setResourceName($job_info['fileset']['name']);
+                               $this->FileSetConfig->setLoadValues(true);
+                               $this->FileSetConfig->raiseEvent('OnDirectiveListLoad', $this, null);
+                       }
                }
        }
 
        public function loadScheduleConfig($sender, $param) {
                if (!empty($_SESSION['dir'])) {
-                       $jobshow = $this->getModule('api')->get(array(
-                               'jobs', $this->getJobId(), 'show'
-                       ))->output;
-                       $schedule = $this->getResourceName('schedule', $jobshow);
-                       $this->ScheduleConfig->setComponentName($_SESSION['dir']);
-                       $this->ScheduleConfig->setResourceName($schedule);
-                       $this->ScheduleConfig->setLoadValues(true);
-                       $this->ScheduleConfig->raiseEvent('OnDirectiveListLoad', $this, null);
+                       $job_info = $this->getJobInfo();
+                       if (key_exists('schedule', $job_info)) {
+                               $this->ScheduleConfig->setComponentName($_SESSION['dir']);
+                               $this->ScheduleConfig->setResourceName($job_info['schedule']['name']);
+                               $this->ScheduleConfig->setLoadValues(true);
+                               $this->ScheduleConfig->raiseEvent('OnDirectiveListLoad', $this, null);
+                       }
                }
        }
 
index d390fab0c27afc33c0abf2821f862159cb7e4fee..8499094f179b26d8704874df1b1ed923d0da372e 100644 (file)
        </div>
        <div class="w3-container tab_item" id="job_actions">
                <com:TActiveLinkButton
-                       CssClass="w3-button w3-green w3-margin-bottom"
+                       CssClass="w3-button w3-green"
                        OnClick="loadRunJobModal"
                        Attributes.onclick="document.getElementById('run_job').style.display='block'"
                >
                        <prop:Text><%=Prado::localize('Run job')%> &nbsp;<i class="fa fa-undo"></i></prop:Text>
                </com:TActiveLinkButton>
                <com:Application.Web.Portlets.RunJob ID="RunJobModal" />
+               <div id="job_graph_container">
+                       <div>
+                               <div id="jobs_summary_graph"></div>
+                       </div>
+                       <div>
+                               <div id="job_size_graph" style="height: 390px"></div>
+                       </div>
+                       <div>
+                               <div id="job_files_graph" style="height: 390px"></div>
+                       </div>
+               </div>
+               <script>
+var oJobGraphs = {
+       ids: {
+               jobs_summary_graph : 'jobs_summary_graph',
+               job_size_graph : 'job_size_graph',
+               job_files_graph : 'job_files_graph',
+               job_actions: 'job_actions'
+       },
+       graphs: {
+               job_summary: null,
+               job_size: null,
+               job_files: null
+       },
+       colors: {
+               F: '#63c422',
+               I: '#2980B9',
+               D: '#D68910',
+               O: 'red'
+       },
+       graph_options: {
+               legend: {
+                       show: true,
+                       noColumns: 9,
+                       labelBoxHeight: 10,
+                       fontColor: '#000000',
+                       position : 'ne'
+               },
+               bars: {
+                       show: true,
+                       fill: true,
+                       horizontal : false,
+                       shadowSize : 0
+               },
+               xaxis: {
+                       mode : 'time',
+                       timeMode: 'local',
+                       labelsAngle : 45,
+                       autoscale: true,
+                       color: 'black',
+                       showLabels: true
+               },
+               yaxis: {
+                       color: 'black',
+                       min: 0
+               },
+               lines: {
+                       show: true,
+                       lineWidth: 0,
+                       fill: true,
+                       steps: true
+               },
+               selection: {
+                       mode : 'x'
+               },
+               grid: {
+                       color: '#000000',
+                       outlineWidth: 0
+               },
+               HtmlText: false
+       },
+       txt: {
+               job_summary: {
+                       graph_title: '<%[ Job status summary ]%>'
+               },
+               job_size: {
+                       graph_title: '<%[ Job size / Time ]%> - <%[ last %days days ]%>'.replace('%days', 30),
+                       xaxis_title: '<%[ Time ]%>',
+                       yaxis_title: '<%[ Job size ]%>'
+               },
+               job_files: {
+                       graph_title: '<%[ Job files / Time ]%> - <%[ last %days days ]%>'.replace('%days', 30),
+                       xaxis_title: '<%[ Time ]%>',
+                       yaxis_title: '<%[ Files count ]%>'
+               },
+       },
+       initialized: false,
+       extended_graph_jt: ['B'],
+       job_info: <%=json_encode($this->getJobInfo())%>,
+       init: function() {
+               this.set_events();
+       },
+       update: function() {
+               if (!$('#' + this.ids.job_actions).is(':visible') || oData.jobs.length == 0) {
+                       // do update only if tab with graphs is opened and if there are finished jobs
+                       return;
+               }
+
+               // job summary pie graph
+               this.prepare_job_summary();
+
+               if (this.display_extended_graphs()) {
+                       // job size - last 30 days
+                       this.prepare_job_size();
+
+                       // job files - last 30 days
+                       this.prepare_job_files();
+               }
+
+               if (!this.initialized) {
+                       /**
+                        * Initialization and events has to be done when graphs already exists.
+                        * From this reason it is done at the end of update and only once.
+                        */
+                       this.initialized = true;
+                       this.init();
+               }
+       },
+       display_extended_graphs: function() {
+               var disp_ext_graphs = false;
+               if (this.job_info.hasOwnProperty('job') && this.job_info.job.hasOwnProperty('jobtype')) {
+                       job_type = String.fromCharCode(this.job_info.job.jobtype);
+                       disp_ext_graphs = (this.extended_graph_jt.indexOf(job_type) !== -1);
+               }
+               return disp_ext_graphs;
+       },
+       set_events: function() {
+               if (!this.display_extended_graphs()) {
+                       return;
+               }
+
+               var select_area = function(area) {
+                       var opts = {
+                               xaxis : {
+                                       min : area.x1,
+                                       max : area.x2,
+                                       mode : 'time',
+                                       timeMode: 'local',
+                                       labelsAngle : 45,
+                                       color: 'black',
+                                       autoscale: true
+                                       },
+                               yaxis : {
+                                       min : area.y1,
+                                       max : area.y2,
+                                       color: 'black',
+                                       autoscale: true
+                               }
+                       };
+                       return opts;
+               };
+
+               // JOB SIZE
+
+               var job_size_select_cb = function(area) {
+                       var opts = select_area(area);
+                       this.prepare_job_size(opts);
+               }.bind(this);
+
+               var job_size_graph_container = document.getElementById(this.ids.job_size_graph);
+
+               // set Flotr-specific select area event for job size graph
+               Flotr.EventAdapter.observe(job_size_graph_container, 'flotr:select', job_size_select_cb);
+
+               // set Flotr-specific click area event (zoom reset) for job size graph
+               Flotr.EventAdapter.observe(job_size_graph_container, 'flotr:click', function () {
+                       this.prepare_job_size();
+               }.bind(this));
+
+               // JOB FILES
+
+               var job_files_select_cb = function(area) {
+                       var opts = select_area(area);
+                       this.prepare_job_files(opts);
+               }.bind(this);
+
+               var job_files_graph_container = document.getElementById(this.ids.job_files_graph);
+
+               // set Flotr-specific select area event for job files graph
+               Flotr.EventAdapter.observe(job_files_graph_container, 'flotr:select', job_files_select_cb);
+
+               // set Flotr-specific click area event (zoom reset) for job files graph
+               Flotr.EventAdapter.observe(job_files_graph_container, 'flotr:click', function (e) {
+                       this.prepare_job_files();
+               }.bind(this));
+       },
+       prepare_job_objs: function(jobs, graph_type)  {
+               var job;
+               var job_objs = [];
+               for (var i = 0; i < jobs.length; i++) {
+                       if (jobs[i].jobstatus == 'R' || jobs[i].jobstatus == 'C' || jobs[i].endtime === null) {
+                               continue;
+                       }
+                       job = new JobClass(jobs[i], graph_type);
+                       job_objs.push(job);
+               }
+               return job_objs;
+       },
+       prepare_job_summary: function() {
+               this.destroy_job_summary();
+
+               Statistics.grab_statistics(oData, JobStatus.get_states());
+               this.graphs.job_summary = new GraphPieClass({
+                       jobs: Statistics.jobs_summary,
+                       container_id: this.ids.jobs_summary_graph,
+                       title: this.txt.job_summary.graph_title
+               });
+       },
+       prepare_job_size: function(opts) {
+               var options = {
+                       title: this.txt.job_size.graph_title,
+                       xaxis: {
+                               title: this.txt.job_size.xaxis_title
+                       },
+                       yaxis: {
+                               title: this.txt.job_size.yaxis_title,
+                               tickFormatter: function(val, axis_opts) {
+                                       return Units.get_formatted_size(val);
+                               }
+                       }
+               };
+               var jobs = this.prepare_job_objs(oData.jobs, 'job_size');
+               var container = document.getElementById(this.ids.job_size_graph);
+               opts = $.extend(true, opts || {}, options);
+               this.graphs.job_size = this.prepare_job_graph(jobs, container, opts);
+       },
+       prepare_job_files: function(opts) {
+               var options = {
+                       title: this.txt.job_files.graph_title,
+                       xaxis: {
+                               title: this.txt.job_files.xaxis_title
+                       },
+                       yaxis: {
+                               title: this.txt.job_files.yaxis_title,
+                               tickFormatter: function(val, axis_opts) {
+                                       return parseInt(val, 10);
+                               }
+                       }
+               };
+               var jobs = this.prepare_job_objs(oData.jobs, 'job_files');
+               var container = document.getElementById(this.ids.job_files_graph);
+               opts = $.extend(true, opts || {}, options);
+               this.graphs.job_files = this.prepare_job_graph(jobs, container, opts);
+       },
+       prepare_job_graph: function(jobs, container, opts) {
+               var now = (new Date()).getTime();
+               var options = $.extend(true, this.graph_options, {
+                       xaxis: {
+                               min: (now - 2592000000),
+                               max: now
+                       },
+                       yaxis: {
+                               autoscale: true,
+                               min: 0,
+                               max: null
+                       }
+               });
+               if (opts) {
+                       options = $.extend(true, options, opts);
+               }
+
+               var series_uniq = {};
+               for (var i = 0; i < jobs.length; i++) {
+                       if(jobs[i].start_stamp < this.graph_options.xaxis.min || jobs[i].end_stamp > this.graph_options.xaxis.max) {
+                               continue;
+                       }
+                       if (series_uniq.hasOwnProperty(jobs[i].job.level) == false) {
+                               series_uniq[jobs[i].job.level] = [];
+                       }
+                       series_uniq[jobs[i].job.level].push(jobs[i].start_point, jobs[i].end_point, [null, null]);
+
+               }
+               var serie, series = [], label;
+               for (var key in series_uniq) {
+                       serie = [];
+                       for (var i = 0; i < series_uniq[key].length; i++) {
+                               serie.push(series_uniq[key][i]);
+                       }
+                       label = JobLevel.get_level(key);
+                       var color = this.colors.O;
+                       if (this.colors.hasOwnProperty(key)) {
+                               color = this.colors[key];
+                       }
+                       series.push({
+                               data: serie,
+                               label: label,
+                               color: color
+                       });
+               }
+               return this.draw_graph(container, series, options);
+       },
+       draw_graph: function(container, series, opts) {
+               return Flotr.draw(
+                       container,
+                       series,
+                       opts
+               );
+       },
+       destroy_job_summary: function() {
+               if (this.graphs.job_summary) {
+                       this.graphs.job_summary.pie.destroy();
+               }
+       }
+};
+               </script>
        </div>
        <div class="w3-container tab_item" id="job_config" style="display: none">
                <com:Application.Web.Portlets.BaculaConfigDirectives
@@ -380,7 +685,10 @@ MonitorParams = {
        }
 };
 $(function() {
-       MonitorCallsInterval.push(function() { oJobHistoryList.init(); });
+       MonitorCallsInterval.push(function() {
+               oJobGraphs.update();
+               oJobHistoryList.init();
+       });
 });
 </script>
        </div>
index d323f651997a9eb23a1d7bf4042811e9ccb5625f..470d386f54089face26b63f668a3fa462e6f8243 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
@@ -35,18 +35,11 @@ Prado::using('Application.Web.Class.BaculumWebPage');
 class JobView extends BaculumWebPage {
 
        const JOB_NAME = 'JobName';
-
-       const USE_CACHE = true;
-
-       const RESOURCE_SHOW_PATTERN = '/^\s+--> %resource: name=(.+?(?=\s\S+\=.+)|.+$)/i';
-
-       private $jobdata;
-       public $is_running = false;
-       public $fileset;
-       public $schedule;
+       const JOB_INFO = 'JobInfo';
 
        public function onInit($param) {
                parent::onInit($param);
+               $this->JobConfig->attachEventHandler('OnSave', [$this, 'reloadJobInfo']);
                if ($this->IsCallBack || $this->IsPostBack) {
                        return;
                }
@@ -58,6 +51,7 @@ class JobView extends BaculumWebPage {
                $this->setJobName($job_name);
                $this->Schedules->setJob($job_name);
                $this->Schedules->setDays(90);
+               $this->setJobInfo($job_name);
        }
 
        /**
@@ -78,6 +72,45 @@ class JobView extends BaculumWebPage {
                return $this->getViewState(self::JOB_NAME);
        }
 
+       /**
+        * Set job information from show job output.
+        *
+        * @return none
+        */
+       public function setJobInfo($job_name) {
+               $job_show = $this->getModule('api')->get(
+                       array('jobs', 'show', '?name='. rawurlencode($job_name)),
+                       null,
+                       true,
+                       false
+               );
+               if ($job_show->error == 0) {
+                       $job_info = $this->getModule('job_info')->parseResourceDirectives($job_show->output);
+                       $this->setViewState(self::JOB_INFO, $job_info);
+               }
+       }
+
+       /**
+        * Get job information.
+        *
+        * @return array job information
+        */
+       public function getJobInfo() {
+               return $this->getViewState(self::JOB_INFO, []);
+       }
+
+       /**
+        * Reload job information.
+        *
+        * @param mixed $param save event parameter
+        * @return none
+        */
+       public function reloadJobInfo($param) {
+               if ($this->Request->contains('job')) {
+                       $this->setJobInfo($this->Request['job']);
+               }
+       }
+
        public function loadRunJobModal($sender, $param) {
                $this->RunJobModal->loadData();
        }
@@ -91,41 +124,29 @@ class JobView extends BaculumWebPage {
                }
        }
 
-       public function getResourceName($resource, $jobshow) {
-               $resource_name = null;
-               $pattern = str_replace('%resource', $resource, self::RESOURCE_SHOW_PATTERN);
-               for ($i = 0; $i < count($jobshow); $i++) {
-                       if (preg_match($pattern, $jobshow[$i], $match) === 1) {
-                               $resource_name = $match[1];
-                               break;
-                       }
-               }
-               return $resource_name;
-       }
-
        public function loadFileSetConfig($sender, $param) {
                if (!empty($_SESSION['dir'])) {
-                       $jobshow = $this->getModule('api')->get(array(
-                               'jobs', 'show', '?name=' . rawurlencode($this->getJobName())
-                       ))->output;
-                       $fileset = $this->getResourceName('fileset', $jobshow);
-                       $this->FileSetConfig->setComponentName($_SESSION['dir']);
-                       $this->FileSetConfig->setResourceName($fileset);
-                       $this->FileSetConfig->setLoadValues(true);
-                       $this->FileSetConfig->raiseEvent('OnDirectiveListLoad', $this, null);
+                       $job_info = $this->getJobInfo();
+                       if (key_exists('fileset', $job_info)) {
+                               $this->FileSetConfig->setComponentName($_SESSION['dir']);
+                               $this->FileSetConfig->setResourceName($job_info['fileset']['name']);
+                               $this->FileSetConfig->setLoadValues(true);
+                               $this->FileSetConfig->raiseEvent('OnDirectiveListLoad', $this, null);
+                       }
                }
        }
 
        public function loadScheduleConfig($sender, $param) {
                if (!empty($_SESSION['dir'])) {
-                       $jobshow = $this->getModule('api')->get(array(
-                               'jobs', 'show', '?name=' . rawurlencode($this->getJobName())
-                       ))->output;
-                       $schedule = $this->getResourceName('schedule', $jobshow);
-                       $this->ScheduleConfig->setComponentName($_SESSION['dir']);
-                       $this->ScheduleConfig->setResourceName($schedule);
-                       $this->ScheduleConfig->setLoadValues(true);
-                       $this->ScheduleConfig->raiseEvent('OnDirectiveListLoad', $this, null);
+                       $job_info = $this->getJobInfo();
+                       if (key_exists('schedule', $job_info)) {
+                               $this->ScheduleConfig->setComponentName($_SESSION['dir']);
+                               $this->ScheduleConfig->setResourceName($job_info['schedule']['name']);
+                               $this->ScheduleConfig->setLoadValues(true);
+                               $this->ScheduleConfig->raiseEvent('OnDirectiveListLoad', $this, null);
+                       } else {
+                               $this->ScheduleConfig->unloadDirectives();
+                       }
                }
        }
 
index 5a9b90eb6ea888a92d7b9164c1f416f31276670d..242e853348dd8cd7e5f584d7308d842a049ec482 100644 (file)
@@ -25,5 +25,7 @@
                <module id="user_role" class="Application.Web.Class.WebUserRoles" />
                <module id="auth" class="System.Security.TAuthManager" UserManager="users" LoginPage="LoginPage" />
                <module id="users" class="Application.Web.Class.WebUserManager" UserClass="Application.Web.Class.WebUser" />
+               <!-- data modules -->
+               <module id="job_info" class="Application.Web.Class.JobInfo" />
        </modules>
 </configuration>
index 006e255942506314c06a0e095e8ba71e9efffca4..1e3a0aaf063ed19c0edfdc5d7217ab5494ee98fc 100644 (file)
@@ -44,10 +44,6 @@ class RunJob extends Portlets {
 
        const USE_CACHE = true;
 
-       const RESOURCE_SHOW_PATTERN = '/^\s+--> %resource: name=(.+?(?=\s\S+\=.+)|.+$)/i';
-       const JOB_SHOW_PATTERN = '/^Job:\sname=(?P<jobname>.+)\sJobType=\d+\slevel=(?P<level>\w+)?\sPriority=(?P<priority>\d+)/i';
-       const ACCURATE_PATTERN = '/^\s+Accurate=(?P<accurate>\d)/i';
-
        const DEFAULT_JOB_PRIORITY = 10;
 
        public $job_to_verify = array('C', 'O', 'd', 'A');
@@ -58,15 +54,36 @@ class RunJob extends Portlets {
                $jobid = $this->getJobId();
                $jobname = $this->getJobName();
                $jobdata = null;
+               $job_info = [];
+
                if ($jobid > 0) {
-                       $jobdata = $this->getModule('api')->get(array('jobs', $jobid), null, true, self::USE_CACHE)->output;
+                       $jobdata = $this->getModule('api')->get(
+                               ['jobs', $jobid],
+                               null,
+                               true,
+                               self::USE_CACHE
+                       );
+                       if ($jobdata->error == 0) {
+                               $jobname = $jobdata->name;
+                       }
+               }
+
+               if (!empty($jobname)) {
                        $job_show = $this->getModule('api')->get(
-                               array('jobs', 'show', '?name='. rawurlencode($jobdata->name)),
+                               ['jobs', 'show', '?name='. rawurlencode($jobname)],
                                null,
                                true,
                                self::USE_CACHE
-                       )->output;
-                       $jobdata->storage = $this->getResourceName('(?:storage|autochanger)', $job_show);
+                       );
+                       if ($job_show->error == 0) {
+                               $job_info = $this->getModule('job_info')->parseResourceDirectives($job_show->output);
+                       }
+               }
+
+               if ($jobid > 0) {
+                       $storage = key_exists('storage', $job_info) ? $job_info['storage']['name'] : null;
+                       $autochanger = key_exists('autochanger', $job_info) ? $job_info['autochanger']['name'] : null;
+                       $jobdata->storage = $storage ?: $autochanger;
                        $this->getPage()->getCallbackClient()->show('run_job_storage_from_config_info');
                } elseif (!empty($jobname)) {
                        $jobdata = new stdClass;
@@ -78,17 +95,23 @@ class RunJob extends Portlets {
                        )->output;
                        $levels = $this->getModule('misc')->getJobLevels();
                        $levels_flip = array_flip($levels);
-                       $job_attr = $this->getJobAttr($job_show);
 
-                       if (!empty($job_attr['level'])) {
-                               $jobdata->level = $levels_flip[$job_attr['level']];
+                       if (key_exists('job', $job_info) && !empty($job_info['job']['level'])) {
+                               $jobdata->level = $levels_flip[$job_info['job']['level']];
                        }
-                       $jobdata->client = $this->getResourceName('client', $job_show);
-                       $jobdata->fileset = $this->getResourceName('fileset', $job_show);
-                       $jobdata->pool = $this->getResourceName('pool', $job_show);
-                       $jobdata->storage = $this->getResourceName('(?:storage|autochanger)', $job_show);
-                       $jobdata->priorjobid = $job_attr['priority'];
-                       $jobdata->accurate = (key_exists('accurate', $job_attr) && $job_attr['accurate'] == 1);
+                       $client = key_exists('client', $job_info) ? $job_info['client']['name'] : null;
+                       $fileset = key_exists('fileset', $job_info) ? $job_info['fileset']['name'] : null;
+                       $pool = key_exists('pool', $job_info) ? $job_info['pool']['name'] : null;
+                       $storage = key_exists('storage', $job_info) ? $job_info['storage']['name'] : null;
+                       $autochanger = key_exists('autochanger', $job_info) ? $job_info['autochanger']['name'] : null;
+                       $priority = key_exists('job', $job_info) ? $job_info['job']['priority'] : self::DEFAULT_JOB_PRIORITY;
+                       $accurate = key_exists('job', $job_info) && key_exists('accurate', $job_info['job']) ? $job_info['job']['accurate'] : 0;
+                       $jobdata->client = $client;
+                       $jobdata->fileset = $fileset;
+                       $jobdata->pool = $pool;
+                       $jobdata->storage = $storage ?: $autochanger;
+                       $jobdata->priorjobid = $priority;
+                       $jobdata->accurate = ($accurate == 1);
                } else {
                        $jobs = array();
                        $job_list = $this->getModule('api')->get(array('jobs', 'resnames'), null, true, self::USE_CACHE)->output;
@@ -280,35 +303,6 @@ class RunJob extends Portlets {
                return $verifyVals;
        }
 
-       public function getResourceName($resource, $jobshow) {
-               $resource_name = null;
-               $pattern = str_replace('%resource', $resource, self::RESOURCE_SHOW_PATTERN);
-               for ($i = 0; $i < count($jobshow); $i++) {
-                       if (preg_match($pattern, $jobshow[$i], $match) === 1) {
-                               $resource_name = $match[1];
-                               break;
-                       }
-               }
-               return $resource_name;
-       }
-
-       public function getJobAttr($jobshow) {
-               $attr = array();
-               for ($i = 0; $i < count($jobshow); $i++) {
-                       if (preg_match(self::JOB_SHOW_PATTERN, $jobshow[$i], $match) === 1) {
-                               $attr['jobname'] = $match['jobname'];
-                               $attr['level'] = $match['level'];
-                               $attr['priority'] = $match['priority'];
-                       }
-                       if (preg_match(self::ACCURATE_PATTERN, $jobshow[$i], $match) === 1) {
-                               $attr['accurate'] = $match['accurate'];
-                               break;
-                       }
-               }
-               return $attr;
-       }
-
-
        public function estimate($sender, $param) {
                $params = array();
                $jobid = $this->getJobId();
index c8a6550f86c4d3dcc8545cbfd095398c3de9d608..1da9320d3dbe263f11a768c90056253436f0e672 100644 (file)
@@ -8,10 +8,6 @@
        z-index: 0;
 }
 
-#jobs_summary_graph canvas:nth-child(2) {
-       position: static !important;
-}
-
 .monospace {
        font-family: "Courier New", Courier, monospace !important;
 }
@@ -432,3 +428,13 @@ table.component td:nth-of-type(1) {
 .field_invalid {
        border: 1px solid red !important;
 }
+
+#job_graph_container {
+       display: flex;
+       flex-wrap: wrap;
+}
+
+#job_graph_container > div {
+       width: 420px;
+       margin: 10px;
+}