From: Eric Bollengier Date: Tue, 8 Aug 2006 21:45:32 +0000 (+0000) Subject: ebl add to cvs X-Git-Tag: Release-2.0.0~633 X-Git-Url: https://git.sur5r.net/?a=commitdiff_plain;h=ac70e2cae538f5aa1ace32fc59ea79bfa1c095de;p=bacula%2Fbacula ebl add to cvs git-svn-id: https://bacula.svn.sourceforge.net/svnroot/bacula/trunk@3272 91ce42f0-d328-0410-95d8-f526ca767f89 --- diff --git a/gui/bweb/INSTALL b/gui/bweb/INSTALL new file mode 100644 index 0000000000..9c5317e424 --- /dev/null +++ b/gui/bweb/INSTALL @@ -0,0 +1,164 @@ +################################################################ +# INSTALL NOTES # +################################################################ + +Bweb works well with 1.39 release. + +1) install perl lib +2) copy your files +3) intialise your configuration file +4) do some sql stuff (for postgresql user) +5) get a bconsole that work with Expect +6) get bacula log more useful +7) bweb limitation +8) using sudo with autochanger + +################ FILE COPY ##################################### + + # first, copy bweb perl librarie in your PERL5 INC path + install -m 644 -o root -g root bweb/lib/*.pm /usr/share/perl5 + + # copy bweb perl program to you cgi location + mkdir -m 755 /usr/lib/cgi-bin/bweb + install -m 755 -o root -g root bweb/cgi/*.pl /usr/lib/cgi-bin/bweb + + # get a config file + mkdir -m 750 /etc/bweb + chown root:www-data /etc/bweb + echo "template_dir = /usr/share/bweb/tpl" > /etc/bweb/config + chown www-data /etc/bweb/config + + # copy bweb template file + mkdir -p /usr/share/bweb/tpl + install -m 644 -o root -g root bweb/tpl/*.tpl /usr/share/bweb/tpl + + # copy bweb graphics elements (bweb elements must reside on /bweb) + mkdir /var/www/bweb + install -m 644 -o root -g root bweb/html/*.{js,png,css,gif} /var/www/bweb + + # done ! + +################ INSTALL PERL LIBRARY ########################## + + - perl modules + - DBI (with mysql or postgresql support DBD::Pg and DBD::mysql) + - Gd::Graph + - HTML::Template + - CGI + - Expect + + You can install perl modules with CPAN + perl -e shell -MCPAN + > install Expect + + Or use your distribution + apt-get install libgd-graph-perl libhtml-template-perl libexpect-perl + apt-get install libdbd-mysql-perl libdbd-pg-perl libdbi-perl + +################ APACHE CONFIGURATION ########################## + +It could be a good idea to protect your bweb installation. + +Put this in you httpd.conf, and add user with htpasswd + + + Options ExecCGI -MultiViews +SymLinksIfOwnerMatch + AuthType Basic + AuthName MyPrivateFile + AuthUserFile /etc/apache/htpasswd + AllowOverride None + Require valid-user + + + +################ CONFIGURATION ################################# + +/etc/bweb/config look like : (you can edit it inside bweb) + +dbi = DBI:Pg:database=bacula;host=192.168.1.2 +user = bacula +password = test +template_dir = /usr/share/bweb/tpl +graph_font = /usr/share/fonts/truetype/msttcorefonts/Arial.ttf +email_media = eric@localhost +bconsole = /usr/local/bacula/sbin/bconsole -c /usr/local/bacula/etc/bconsole.conf + +################ BRESTORE ###################################### + +If you want to use brestore with bweb, you must associate mime type +text/brestore with your brestore.pl. + +################ POSTGRESQL NOTES ############################## + +If you think that Mysql is not a great database (or just a toy), you must add +function to the Postgresql bacula database to get Bweb works. + +psql -u bacula bacula < script/bweb-postgresql.sql + +################ BCONSOLE NOTES ################################ + +You must use bconsole without conio/readline support ! You can have 2 bconsole +binary at the same time. + +./configure --disable-conio +cd src/lib +make +cd .. +cd console +make +cp bconsole + +################ BACULA LOG #################################### + +To use Bweb log engine, you must apply this little patch and have the +new Log table in your database. + +After, you can fill your database with : +tail -f /tmp/log.sql | bacula -u bacula bacula + +cd bacula-src +patch < message.patch +--- cvs/src/lib/message.c 2006-07-27 21:06:20.000000000 +0200 ++++ cvs/src/lib/message.c.director 2006-07-28 13:46:49.171083494 +0200 +@@ -716,6 +716,18 @@ + } + fputs(dt, d->fd); + fputs(msg, d->fd); ++ void db_escape_string(char *snew, char *old, int len); ++ if (jcr) { ++ char ed1[50]; ++ POOL_MEM cmd(PM_MESSAGE); ++ int len = strlen(msg); ++ char *p = (char *)malloc(len * 2 + 1); ++ db_escape_string(p, msg, len); ++ FILE *fp = fopen("/tmp/log.sql", "a"); ++ fprintf(fp, "INSERT INTO Log (Time, JobId, LogText) VALUES (NOW(),%s, '%s');\n", edit_int64(jcr->JobId, ed1), p); ++ fclose(fp); ++ } ++ + break; + case MD_DIRECTOR: + Dmsg1(850, "DIRECTOR for following msg: %s", msg); + + + + +################ BWEB LIMITATION ############################### + +To get bweb working, you must follow these rules + - Media, Storage and Pool must have [A-Za-z_0-9\.-]+ (no space) + - AutoChanger name must be same as Storage name device in bacula + +################ SUDO CONFIGURATION ############################ + +*** At this time, autochanger module works only at home :) + +If you use sudo, put this on you /etc/sudoers + +www-data ALL = (root) NOPASSWD: /usr/sbin/mtx -f /dev/changer transfer * +www-data ALL = (root) NOPASSWD: /usr/sbin/mtx -f /dev/changer status +www-data ALL = (root) NOPASSWD: /usr/sbin/mtx -f /dev/changer load * +www-data ALL = (root) NOPASSWD: /usr/sbin/mtx -f /dev/changer unload * + + +Enjoy ! diff --git a/gui/bweb/cgi/bgraph.pl b/gui/bweb/cgi/bgraph.pl new file mode 100755 index 0000000000..c691e26597 --- /dev/null +++ b/gui/bweb/cgi/bgraph.pl @@ -0,0 +1,298 @@ +#!/usr/bin/perl -w +use strict; + +=head1 LICENSE + + Copyright (C) 2006 Eric Bollengier + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +=head1 VERSION + + $Id$ + +=cut + +use Bweb; + +use Data::Dumper; +use CGI; + +use POSIX qw/strftime/; + +my $conf = new Bweb::Config(config_file => '/etc/bweb/config'); +$conf->load(); + +my $bweb = new Bweb(info => $conf); +$bweb->connect_db(); +my $dbh = $bweb->{dbh}; +my $debug = $bweb->{debug}; + +my $graph = CGI::param('graph') || 'begin'; + +my $arg = $bweb->get_form(qw/width height limit offset age + jfilesets level status jjobnames jclients/); + +my ($limitq, $label) = $bweb->get_limit(age => $arg->{age}, + limit => $arg->{limit}, + offset=> $arg->{offset}, + order => 'Job.StartTime ASC', + ); + +my $statusq=''; +if ($arg->{status} and $arg->{status} ne 'Any') { + $statusq = " AND Job.JobStatus = '$arg->{status}' "; +} + +my $levelq=''; +if ($arg->{level} and $arg->{level} ne 'Any') { + $levelq = " AND Job.Level = '$arg->{level}' "; +} + +my $filesetq=''; +if ($arg->{jfilesets}) { + $filesetq = " AND FileSet.FileSet IN ($arg->{qfilesets}) "; +} + +my $jobnameq=''; +if ($arg->{jjobnames}) { + $jobnameq = " AND Job.Name IN ($arg->{jjobnames}) "; +} else { + $arg->{jjobnames} = 'all'; # skip warning +} + +my $clientq=''; +if ($arg->{jclients}) { + $clientq = " AND Client.Name IN ($arg->{jclients}) "; +} else { + $arg->{jclients} = 'all'; # skip warning +} + +my $gtype = CGI::param('gtype') || 'bars'; + +print CGI::header('image/png'); + +sub get_graph +{ + my (@options) = @_; + my $graph; + if ($gtype eq 'lines') { + use GD::Graph::lines; + $graph = GD::Graph::lines->new ( $arg->{width}, $arg->{height} ); + + } elsif ($gtype eq 'bars') { + use GD::Graph::bars; + $graph = GD::Graph::bars->new ( $arg->{width}, $arg->{height} ); + + } elsif ($gtype eq 'linespoints') { + use GD::Graph::linespoints; + $graph = GD::Graph::linespoints->new ( $arg->{width}, $arg->{height} ); + + } elsif ($gtype eq 'bars3d') { + use GD::Graph::bars3d; + $graph = GD::Graph::bars3d->new ( $arg->{width}, $arg->{height} ); + + } else { + return undef; + } + + $graph->set('x_label' => 'Time', + 'x_number_format' => sub { strftime('%D', localtime($_[0])) }, + 'x_tick_number' => 1, + @options, + ); + + return $graph; +} + +sub make_tab +{ + my ($all_row) = @_; + + my $i=0; + my $last_date=0; + + my $ret = {}; + + foreach my $row (@$all_row) { + my $label = $row->[1] . "/" . $row->[2] ; # client/backup name + + $ret->{date}->[$i] = $row->[0]; + $ret->{$label}->[$i] = $row->[3]; + $i++; + $last_date = $row->[0]; + } + + # insert a fake element + foreach my $elt ( keys %{$ret}) { + $ret->{$elt}->[$i] = undef; + } + + $ret->{date}->[$i] = $last_date + 1; + + my $date = $ret->{date} ; + delete $ret->{date}; + + return ($date, $ret); +} + +if ($graph eq 'job_size') { + + my $query = " +SELECT + UNIX_TIMESTAMP(Job.StartTime) AS starttime, + Client.Name AS clientname, + Job.Name AS jobname, + Job.JobBytes AS jobbytes +FROM Job, Client, FileSet +WHERE Job.ClientId = Client.ClientId + AND Job.FileSetId = FileSet.FileSetId + AND Job.Type = 'B' + $clientq + $statusq + $filesetq + $levelq + $jobnameq +$limitq +"; + + print STDERR $query if ($debug); + + my $obj = get_graph('title' => "Job Size : $arg->{jclients}/$arg->{jjobnames}", + 'y_label' => 'Size', + 'y_min_value' => 0, + 'y_number_format' => \&Bweb::human_size, + ); + + my $all = $dbh->selectall_arrayref($query) ; + + my ($d, $ret) = make_tab($all); + $obj->set_legend(keys %$ret); + print $obj->plot([$d, values %$ret])->png; +} + +if ($graph eq 'job_file') { + + my $query = " +SELECT + UNIX_TIMESTAMP(Job.StartTime) AS starttime, + Client.Name AS clientname, + Job.Name AS jobname, + Job.JobFiles AS jobfiles +FROM Job, Client, FileSet +WHERE Job.ClientId = Client.ClientId + AND Job.FileSetId = FileSet.FileSetId + AND Job.Type = 'B' + $clientq + $statusq + $filesetq + $levelq + $jobnameq +$limitq +"; + + print STDERR $query if ($debug); + + my $obj = get_graph('title' => "Job Files : $arg->{jclients}/$arg->{jjobnames}", + 'y_label' => 'Number Files', + 'y_min_value' => 0, + ); + + my $all = $dbh->selectall_arrayref($query) ; + + my ($d, $ret) = make_tab($all); + $obj->set_legend(keys %$ret); + print $obj->plot([$d, values %$ret])->png; +} + +elsif ($graph eq 'job_rate') { + + my $query = " +SELECT + UNIX_TIMESTAMP(Job.StartTime) AS starttime, + Client.Name AS clientname, + Job.Name AS jobname, + Job.JobBytes / + ($bweb->{sql}->{SEC_TO_INT}( + $bweb->{sql}->{UNIX_TIMESTAMP}(EndTime) + - $bweb->{sql}->{UNIX_TIMESTAMP}(StartTime)) + 0.01) + AS rate + +FROM Job, Client, FileSet +WHERE Job.ClientId = Client.ClientId + AND Job.FileSetId = FileSet.FileSetId + AND Job.Type = 'B' + $clientq + $statusq + $filesetq + $levelq + $jobnameq +$limitq +"; + + print STDERR $query if ($debug); + + my $obj = get_graph('title' => "Job Rate : $arg->{jclients}/$arg->{jjobnames}", + 'y_label' => 'Rate b/s', + 'y_min_value' => 0, + 'y_number_format' => \&Bweb::human_size, + ); + + my $all = $dbh->selectall_arrayref($query) ; + + my ($d, $ret) = make_tab($all); + $obj->set_legend(keys %$ret); + print $obj->plot([$d, values %$ret])->png; +} + + + +elsif ($graph eq 'job_duration') { + + my $query = " +SELECT + UNIX_TIMESTAMP(Job.StartTime) AS starttime, + Client.Name AS clientname, + Job.Name AS jobname, + $bweb->{sql}->{SEC_TO_INT}( $bweb->{sql}->{UNIX_TIMESTAMP}(EndTime) + - $bweb->{sql}->{UNIX_TIMESTAMP}(StartTime)) + AS duration +FROM Job, Client, FileSet +WHERE Job.ClientId = Client.ClientId + AND Job.FileSetId = FileSet.FileSetId + AND Job.Type = 'B' + $clientq + $statusq + $filesetq + $levelq + $jobnameq +$limitq +"; + + print STDERR $query if ($debug); + + my $obj = get_graph('title' => "Job Duration : $arg->{jclients}/$arg->{jjobnames}", + 'y_label' => 'Duration', + 'y_min_value' => 0, + 'y_number_format' => \&Bweb::human_sec, + ); + my $all = $dbh->selectall_arrayref($query) ; + + my ($d, $ret) = make_tab($all); + $obj->set_legend(keys %$ret); + print $obj->plot([$d, values %$ret])->png; +} + diff --git a/gui/bweb/cgi/bweb.pl b/gui/bweb/cgi/bweb.pl new file mode 100755 index 0000000000..f802b2d06d --- /dev/null +++ b/gui/bweb/cgi/bweb.pl @@ -0,0 +1,356 @@ +#!/usr/bin/perl -w +use strict ; + +=head1 LICENSE + + Copyright (C) 2006 Eric Bollengier + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +=head1 VERSION + + $Id$ + +=cut + +use Data::Dumper; +use Bweb; +use CGI; + +my $client_re = qr/^([\w\d\.-]+)$/; +my $pool_re = $client_re; + +my $action = CGI::param('action') || 'begin'; + +if ($action eq 'restore') { + print CGI::header('text/brestore'); # specialy to run brestore.pl + +} else { + print CGI::header('text/html'); +} + +# loading config file +my $conf = new Bweb::Config(config_file => '/etc/bweb/config'); +$conf->load(); + +my $bweb = new Bweb(info => $conf); + +# just send data with text/brestore content +if ($action eq 'restore') { + $bweb->restore(); + exit 0; +} + +my $arg = $bweb->get_form('jobid', 'limit', 'offset', 'age'); + +$bweb->display_begin(); + +if ($action eq 'begin') { # main display + print "
\n"; + $bweb->display_general(age => $arg->{age}); + $bweb->display_running_jobs(0); + print ""; + print " +
+

Statistics (last 48 hours)

+
+
+ +Nothing to display + +
"; + print "
"; + $bweb->display_job(limit => 10); + +} elsif ($action eq 'view_conf') { + $conf->view() + +} elsif ($action eq 'edit_conf') { + $conf->edit(); + +} elsif ($action eq 'apply_conf') { + $conf->modify(); + +} elsif ($action eq 'client') { + $bweb->display_clients(); + +} elsif ($action eq 'pool') { + $bweb->display_pool(); + +} elsif ($action eq 'location_edit') { + $bweb->location_edit(); + +} elsif ($action eq 'location_save') { + $bweb->location_save(); + +} elsif ($action eq 'location_add') { + $bweb->location_add(); + +#} elsif ($action eq 'del_location') { +# $bweb->del_location(); +# +} elsif ($action eq 'media') { + $bweb->display_media(); + +} elsif ($action eq 'medias') { + $bweb->display_medias(); + +} elsif ($action eq 'eject') { + my $a = Bweb::Autochanger::get('SDLT-1-2', $bweb); + + $a->status(); + foreach my $slot (CGI::param('slot')) { + print $a->{error} unless $a->send_to_io($slot); + } + + foreach my $media (CGI::param('media')) { + my $slot = $a->get_media_slot($media); + print $a->{error} unless $a->send_to_io($slot); + } + + $a->display_content(); + +} elsif ($action eq 'eject_media') { + $bweb->eject_media(); + +} elsif ($action eq 'clear_io') { + my $a = Bweb::Autochanger::get('SDLT-1-2', $bweb); + $a->status(); + $a->clear_io(); + $a->display_content(); + +} elsif ($action eq 'ach_view') { + # TODO : get autochanger name and create it + $bweb->connect_db(); + my $a = Bweb::Autochanger::get('SDLT-1-2', $bweb); + $a->status(); + $a->display_content(); + +} elsif ($action eq 'ach_load') { + my $arg = $bweb->get_form('drive', 'slot'); + my $a = Bweb::Autochanger::get('SDLT-1-2', $bweb); + $a->status(); + $a->load($arg->{drive}, $arg->{slot}) ; + +} elsif ($action eq 'ach_unload') { + my $arg = $bweb->get_form('drive', 'slot'); + my $a = Bweb::Autochanger::get('SDLT-1-2', $bweb); + $a->status(); + $a->unload($arg->{drive}, $arg->{slot}) ; + +} elsif ($action eq 'intern_media') { + $bweb->help_intern(); + +} elsif ($action eq 'compute_intern_media') { + $bweb->help_intern_compute(); + +} elsif ($action eq 'extern_media') { + $bweb->help_extern(); + +} elsif ($action eq 'compute_extern_media') { + $bweb->help_extern_compute(); + +} elsif ($action eq 'extern') { + print "TODO : Eject ", join(",", CGI::param('media')); + $bweb->move_media(); + +} elsif ($action eq 'change_location') { + $bweb->change_location(); + +} elsif ($action eq 'location') { + $bweb->display_location(); + +} elsif ($action eq 'about') { + $bweb->display($bweb, 'about.tpl'); + +} elsif ($action eq 'intern') { + $bweb->move_media(); # TODO : remove that + +} elsif ($action eq 'move_media') { + $bweb->move_media(); + +} elsif ($action eq 'save_location') { + $bweb->save_location(); + +} elsif ($action eq 'update_location') { + $bweb->update_location(); + +} elsif ($action eq 'update_media') { + $bweb->update_media(); + +} elsif ($action eq 'do_update_media') { + $bweb->do_update_media(); + +} elsif ($action eq 'update_slots') { + $bweb->update_slots(); + +} elsif ($action eq 'graph') { + $bweb->display_graph(); + +} elsif ($action eq 'next_job') { + $bweb->director_show_sched(); + +} elsif ($action eq 'enable_job') { + $bweb->enable_disable_job(1); + +} elsif ($action eq 'disable_job') { + $bweb->enable_disable_job(0); + +} elsif ($action eq 'job') { + + print "
\n"; + my $fields = $bweb->get_form(qw/status level db_clients db_filesets + limit age offset qclients qfilesets + jobtype/); + $bweb->display($fields, "display_form_job.tpl"); + + print ""; + $bweb->display_job(age => $arg->{age}, # last 7 days + offset => $arg->{offset}, + limit => $arg->{limit}); + print "
"; +} elsif ($action eq 'client_stats') { + + foreach my $client (CGI::param('client')) { + if ($client =~ m/$client_re/) { + $bweb->display_client_stats(clientname => $1, + age => $arg->{age}); + } + } + + + +} elsif ($action eq 'running') { + $bweb->display_running_jobs(1); + +} elsif ($action eq 'dsp_cur_job') { + $bweb->display_running_job(); + +} elsif ($action eq 'update_from_pool') { + my $elt = $bweb->get_form(qw/media pool/); + unless ($elt->{media} || $elt->{pool}) { + $bweb->error("Can't get media or pool param"); + } else { + my $b = new Bconsole(pref => $conf) ; + + $bweb->display({ + content => $b->send_cmd("update volume=$elt->{media} fromPool=$elt->{pool}"), + title => "Update pool", + name => "update volume=$elt->{media} fromPool=$elt->{pool}", + }, "command.tpl"); + } + + $bweb->update_media(); + +} elsif ($action eq 'client_status') { + my $b; + foreach my $client (CGI::param('client')) { + if ($client =~ m/$client_re/) { + $client = $1; + $b = new Bconsole(pref => $conf) + unless ($b) ; + + $bweb->display({ + content => $b->send_cmd("st client=$client"), + title => "Client status", + name => $client, + }, "command.tpl"); + + } else { + $bweb->error("Can't get client selection"); + } + } + +} elsif ($action eq 'cancel_job') { + $bweb->cancel_job(); + +} elsif ($action eq 'media_zoom') { + $bweb->display_media_zoom(); + +} elsif ($action eq 'job_zoom') { + if ($arg->{jobid}) { + $bweb->display_job_zoom($arg->{jobid}); + $bweb->get_job_log(); + } +} elsif ($action eq 'job_log') { + $bweb->get_job_log(); + +} elsif ($action eq 'prune') { + $bweb->prune(); + +} elsif ($action eq 'purge') { + $bweb->purge(); + +} elsif ($action eq 'run_job') { + $bweb->run_job(); + +} elsif ($action eq 'run_job_mod') { + $bweb->run_job_mod(); + +} elsif ($action eq 'run_job_now') { + $bweb->run_job_now(); + +} elsif ($action eq 'label_barcodes') { + $bweb->label_barcodes(); + +} elsif ($action eq 'delete') { + $bweb->delete(); + +} else { + $bweb->error("Sorry, this action don't exist"); +} + +$bweb->display_end(); + + +__END__ + +TODO : + + o Affichage des job en cours, termines + o Affichage du detail d'un job (status client) + o Acces aux log d'une sauvegarde + o Cancel d'un job + o Lancement d'un job + + o Affichage des medias (pool, cf bacweb) + o Affichage de la liste des cartouches + o Affichage d'un autochangeur + o Mise a jour des slots + o Label barcodes + o Affichage des medias qui ont besoin d'etre change + + o Affichage des stats sur les dernieres sauvegardes (cf bacula-web) + o Affichage des stats sur un type de job + o Affichage des infos de query.sql + + - Affichage des du TapeAlert sur le site + - Recuperation des erreurs SCSI de /var/log/kern.log + + o update d'un volume + o update d'un pool + + - Configuration des autochanger a la main dans un hash dumper + + { + L10 => { + name => 'L10', + drive_name => ['SDLT-1', 'STLD-2'], + login => 'bacula', + host => 'storehost', + device => '/dev/changer', + }, + } diff --git a/gui/bweb/html/A.png b/gui/bweb/html/A.png new file mode 100644 index 0000000000..89c212493e Binary files /dev/null and b/gui/bweb/html/A.png differ diff --git a/gui/bweb/html/E.png b/gui/bweb/html/E.png new file mode 100644 index 0000000000..89c212493e Binary files /dev/null and b/gui/bweb/html/E.png differ diff --git a/gui/bweb/html/R.png b/gui/bweb/html/R.png new file mode 100644 index 0000000000..166c2201cf Binary files /dev/null and b/gui/bweb/html/R.png differ diff --git a/gui/bweb/html/T.png b/gui/bweb/html/T.png new file mode 100644 index 0000000000..d92d88d8a4 Binary files /dev/null and b/gui/bweb/html/T.png differ diff --git a/gui/bweb/html/add.png b/gui/bweb/html/add.png new file mode 100644 index 0000000000..062d005208 Binary files /dev/null and b/gui/bweb/html/add.png differ diff --git a/gui/bweb/html/apply.png b/gui/bweb/html/apply.png new file mode 100644 index 0000000000..58a64cfc48 Binary files /dev/null and b/gui/bweb/html/apply.png differ diff --git a/gui/bweb/html/bweb.css b/gui/bweb/html/bweb.css new file mode 100644 index 0000000000..bb6e678b4c --- /dev/null +++ b/gui/bweb/html/bweb.css @@ -0,0 +1,11 @@ +body { background-color: #ffffff; font-family: verdana,arial,helvetica; font-size: 8pt;} +a { text-decoration: none;} +.code_display { background-color: #b9b9ac; + border: 1px solid #9d9d94; +} + +abutton.formulaire { class: formulaire; font-size: 9; height: 48px; width: 80px; background-color: transparent; } + +button.formulaire { class: formulaire; border: 0px; font-size: 9; background-color: transparent; } + +td.joberr { background-color: red; font-color: white;} diff --git a/gui/bweb/html/bweb.js b/gui/bweb/html/bweb.js new file mode 100644 index 0000000000..69fc247738 --- /dev/null +++ b/gui/bweb/html/bweb.js @@ -0,0 +1,55 @@ + var even_cell_color = "#FFFFFF"; + var odd_cell_color = "#EEEEEE"; + var header_color = "#E1E0DA"; + var rows_per_page = 20; + var up_icon = "/bweb/up.gif"; + var down_icon = "/bweb/down.gif"; + var prev_icon = "/bweb/left.gif"; + var next_icon = "/bweb/right.gif"; + var rew_icon = "/bweb/first.gif"; + var fwd_icon = "/bweb/last.gif"; + + var jobstatus = { + 'C': 'created but not yet running', + 'R': 'running', + 'B': 'blocked', + 'T': 'terminated normally', + 'E': 'Job terminated in error', + 'e': 'Non-fatal error', + 'f': 'Fatal error', + 'D': 'Verify differences', + 'A': 'canceled by user', + 'F': 'waiting on File daemon', + 'S': 'waiting on the Storage daemon', + 'm': 'waiting for new media', + 'M': 'waiting for Mount', + 's': 'Waiting for storage resource', + 'j': 'Waiting for job resource', + 'c': 'Waiting for Client resource', + 'd': 'Waiting for maximum jobs', + 't': 'Waiting for start time', + 'p': 'Waiting for higher priority jobs to finish' +}; + +var joblevel = { + 'F': 'Full backup', + 'I': 'Incr (since last backup)', + 'D': 'Diff (since last full backup)', + 'C': 'verify from catalog', + 'V': 'verify save (init DB)', + 'O': 'verify Volume to catalog entries', + 'd': 'verify Disk attributes to catalog', + 'A': 'verify data on volume', + 'B': 'Base level job' +}; + + +var refresh_time = 60000; + +function bweb_refresh() { + location.reload(true) +} +function bweb_add_refresh(){ + window.setInterval("bweb_refresh()",refresh_time); +} + diff --git a/gui/bweb/html/cancel.png b/gui/bweb/html/cancel.png new file mode 100644 index 0000000000..89c212493e Binary files /dev/null and b/gui/bweb/html/cancel.png differ diff --git a/gui/bweb/html/chart.png b/gui/bweb/html/chart.png new file mode 100644 index 0000000000..333769d1fd Binary files /dev/null and b/gui/bweb/html/chart.png differ diff --git a/gui/bweb/html/down.gif b/gui/bweb/html/down.gif new file mode 100755 index 0000000000..ce294fdf3c Binary files /dev/null and b/gui/bweb/html/down.gif differ diff --git a/gui/bweb/html/edit.png b/gui/bweb/html/edit.png new file mode 100644 index 0000000000..36775390f1 Binary files /dev/null and b/gui/bweb/html/edit.png differ diff --git a/gui/bweb/html/extern.png b/gui/bweb/html/extern.png new file mode 100644 index 0000000000..cfc3b35559 Binary files /dev/null and b/gui/bweb/html/extern.png differ diff --git a/gui/bweb/html/f.png b/gui/bweb/html/f.png new file mode 100644 index 0000000000..89c212493e Binary files /dev/null and b/gui/bweb/html/f.png differ diff --git a/gui/bweb/html/filename.png b/gui/bweb/html/filename.png new file mode 100644 index 0000000000..7e5776a787 Binary files /dev/null and b/gui/bweb/html/filename.png differ diff --git a/gui/bweb/html/first.gif b/gui/bweb/html/first.gif new file mode 100755 index 0000000000..10a4e7eb90 Binary files /dev/null and b/gui/bweb/html/first.gif differ diff --git a/gui/bweb/html/first.png b/gui/bweb/html/first.png new file mode 100644 index 0000000000..5e8aa7a08f Binary files /dev/null and b/gui/bweb/html/first.png differ diff --git a/gui/bweb/html/inflag0.png b/gui/bweb/html/inflag0.png new file mode 100644 index 0000000000..6478554f62 Binary files /dev/null and b/gui/bweb/html/inflag0.png differ diff --git a/gui/bweb/html/inflag1.png b/gui/bweb/html/inflag1.png new file mode 100644 index 0000000000..e061e7f17c Binary files /dev/null and b/gui/bweb/html/inflag1.png differ diff --git a/gui/bweb/html/intern.png b/gui/bweb/html/intern.png new file mode 100644 index 0000000000..64b8300d22 Binary files /dev/null and b/gui/bweb/html/intern.png differ diff --git a/gui/bweb/html/kaiska.css b/gui/bweb/html/kaiska.css new file mode 100644 index 0000000000..00d51a03bb --- /dev/null +++ b/gui/bweb/html/kaiska.css @@ -0,0 +1,970 @@ +/* +* ---------------------------------------------------------------------------- +* "THE BEER-WARE LICENSE" (Revision 42): +* Willy Morin (kaiska@ifrance.com) wrote this file. As long as you retain this +* notice you can do whatever you want with this stuff. If we meet some day, +* and you think this stuff is worth it, you can buy me a beer in return +* +* Willy Morin +* http://kaiska.flinkserver.net +* +* Addendum Étienne Bersac ( commentsbrowser ), http://bersace03.free.fr +* ---------------------------------------------------------------------------- +*/ + +* { font-family: sans-serif, monospace; } + +body { background: #f1f1f1; } + +a:link { + color: #36598E; + text-decoration: none; +} + +a:visited { + color: #B64545; + text-decoration: none; +} + +h1.rubrique_info { + color: #552 !important; + margin: 0px 0px 0px 0px; + padding: 0em; + border: 0px; + font-size: 12px; +} +h1.connexe { + font-size: 12px; + padding: 0em; + margin: 0px 0px 0px 0px; + color: #900; +} + +a.rubrique_infolink { text-decoration: none; } +ul.rubrique_infoul { + display: inline; + list-style-type: square; +} +ul.rubrique_infoul * { width: 100%; } +li.rubrique_infoul { margin-left: 15px; } + +.leftbox ul.rubrique_infoul { + display:block; + padding: 0 10px 0 10px; + margin-right: -10px; +} + +h1 { color: #990033; } + +div.main { + margin: 15px 10px 0px 10px; + border: 2px #ddd solid; + text-align: left; + font-size: 0.8em; +} + +div.lsfnbanner { + position: absolute; + top: 0; + left: 4px; + width: 99%; + text-align: center; + font-size: .8em; + padding: 0; + margin: 0; + border: 0; +} + +div.footer { + padding-top: 5px; + padding-bottom: 3px; + border-top: 1px #ccc solid; + border-left: 1px #ccc solid; + border-right: 1px #ccc solid; + text-align: left; + font-size: 9px; + background: #eeeae6; + margin-top: 40px; + margin-left: 20px; + margin-right: 20px; +} +div.footer p { + margin-left: 10px; + margin-top: 2px; + margin-bottom: 2px; +} +div.menubartop { + margin: 0; + padding:0; + background:#ddd; +} + +div.smallmenubar { + padding: 4px; + padding-left: 10px; + padding-right: 10em; + margin-top: -4px; + font-weight: normal; + font-size: 0.8em; + text-align: left; + background: #e0ddd8; +} + +div.smallmenubar a:hover{ + margin-bottom:4px; +} +div.menuevent { + float: left; + width: 350px; + font-size: 11px; + text-align: left; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 10px; + font-weight: bold; + margin: 0px; +} +div.menubar { + background: #eeeae6; + border-top: 1px #999 solid; + border-bottom: 1px #999 solid; + padding: 2px 10px 3px 10px; + font-weight:bold; + font-size: 1em; +} +div.menubar a { + text-decoration: none; +} +div.menubar p { + padding: 0px; + margin: 0px; +} + +.menudate { visibility:hidden;height:0;margin:0;padding:0;} + +div.menusearch { + float: none; + position: absolute; + top: 19px; + right:22px; + margin:0; + text-align: left; + padding: 0; + width: auto; + background: transparent url('/images/search.png') 2px no-repeat; + padding-left:25px; +} +div.menusearch p { + margin: 0; +} +div.newsletter { + float: right; + text-align: right; + margin: 0px; + padding: 0px; + font-size: 8px; + font-weight: normal; +} +div.newsletter p { margin: 0px 0px 0px 0px; } +.searchinput { + border: solid 1px #ccc; + padding-left: 5px; + padding-right: 5px; + height:1.1em; +} +.searchinput:focus{ + border: solid 1px #777; + background: #f5f5e9; + padding-left: 5px; + padding-right: 5px; +} + +.newsletterinput { + border: solid 1px black; + font-weight: normal; +} +a#menulinkselect { color: #ed7e00; } + +div.leftbox { + width: 200px; + float: left; + padding-right: 5px; + padding-bottom: 5px; + border-right: 0px solid #ccc; + border-bottom: 0px solid #ccc; + background: white; + margin-bottom: 10px; + margin-right: 20px; + overflow: hidden; +} +div.leftbox h1 { + text-transform: uppercase; + font-weight: bold; + color: #ed7e00; + font-size: 10px; + margin: 0px; +} +div.leftbox h2 { + font-weight: bold; + font-size: 12px; + margin: 0px; +} +div.leftbox ul { + list-style-type: square; + color: #552; + margin-left: 5px; + margin-right: 5px; + padding-left: 10px; + border: 1px solid #bbb; + background: #eeeae6; + -moz-border-radius: 10px; +} +div.leftbox li { + margin-left: 10px; + margin-top: 3px; + padding-bottom: 3px; + border-bottom: 1px solid #ddd; + font-size:0.8em; +} + +div.leftbox p { + color: #552; + text-align:center; +} + +div.rightbox { + width: 140px; + float: right; + padding-top: 10px; + padding-left: 5px; + padding-right: 15px; + padding-bottom: 5px; + border: 1px solid #ccc; + margin: 10px; + -moz-border-radius: 10px; + text-align: left; + background: #eeeae6; +} +div.rightbox h1{ + color: #552; +} + +div.rightbox ul { + list-style-type: square; + color: #552; + padding:0; +} +div.rightbox li { + margin-left: 10px; + margin-top: 3px; + padding-bottom: 3px; + border-bottom: 1px solid #ddd; + font-size:0.8em; +} + +div.journaldiv { + margin-left: 0px; + margin-right: 0px; + margin-top: 20px; + margin-bottom: 20px; + border: 1px solid #ccc; + padding-top: 5px; + padding-bottom: 5px; + padding-right: 10px; + padding-left: 10px; + background-color: #eeeae6; + -moz-border-radius: 10px; +} +div.journaldiv p { + margin-top: 10px; + margin-bottom: 0px; +} +div.journaldiv h1 { + color: #552; + margin: 0; + border-bottom: 1px dotted #bbb; +} +div.journaldiv h2 { + font-size: 10px; + margin: 0; +} +div.tipdiv { + margin-right: 50px; + padding-top: 5px; + padding-right: 10px; + padding-left: 10px; + text-align: justify; + background-color: #eee; + border: black solid 1px; + margin-left: 10px; +} +div.tipdiv h1 { + font-weight: bold; + font-size: 14px; + color: black; + margin-top: 0; + margin-bottom: 20px; +} +div.tipdiv h2 { + text-transform: uppercase; + font-weight: bold; + color: #ed7e00; + font-size: 12px; + margin: 0; +} + +div.newsdiv { + margin-left: 220px; + margin-right: 180px; + margin-top: 10px; + margin-bottom: 20px; + text-align: justify; +} +div.newsdiv h1 { + font-weight: bold; + font-size: 14px; + margin: 0px; +} +div.newsdiv h2 { + font-weight: normal; + font-size: 12px; + margin: 0px; +} +div.newsdiv h3 { + font-weight: normal; + font-size: 12px; + margin-bottom: 20px; +} +div.objdiv { + margin-left: 220px; + margin-right: 20px; + margin-top: 10px; + margin-bottom: 20px; +} + +h1.newstitle { + text-align: left; + font-size: 14px; + margin: 0px 0px 0px 0px; + color: #552; +} +div.titlediv { + border-top: solid #cac2a8 0px; + margin-top: 20px; + padding-top: 5px; + background-color: #eeeae6; + padding-left: 10px; + font-size: 11px; + color:#666; + -moz-border-radius: 10px 10px 0 0; +} +div.bodydiv { + border-top: solid #ccc 0px; + border-bottom: solid #ccc 1px; + border-left: solid #ccc 1px; + border-right: solid #ccc 1px; + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; + padding-bottom: 5px; + margin-top: 0; + text-align: justify; + background: #f5f5f5; + -moz-border-radius: 0 0 10px 10px; +} + +div.comments { + padding: 10px; + border-top: solid 2px #552; + border-bottom: solid 2px #552; + border-left: 1px solid #ccc; + border-right: 1px solid #ccc; + margin-top: 20px; + margin-bottom: 10px; + background: #dad6d1; + font-size: 12px; + line-height: 1.3em; + -moz-border-radius: 10px; +} + +.comment-vote { font-style: italic; color:#666; font-size:0.9em;} + +p.commentsbody { + border-left: 1px solid #aaa; + padding-left: 10px; + text-align: justify; + margin-right: 20px; +} +div.commentsreply { + text-align: center; + margin-top: 10px; + font-size:0.9em; + color: #666; +} + +div.commentsreply .searchinput{ height:auto; } + +ul.commentsul { + list-style-type: none; + margin-bottom: 10px; + margin-left: 1.25em; + padding-left: 0; +} +ul.commentsli { margin: 10px; } +div.comments li { + margin-top: 20px; + margin-left: 2px; +} + +div.comments h1 { + font-size: 12px; + color: black; + margin: 0px 20px 3px 0px; + background:#ebe7e2; + padding-left: 1px; + font-weight: normal; + -moz-border-radius: 10px; +} + +div.articlediv { + padding-left: 20px; + padding-right: 20px; + padding-top: 10px; + padding-bottom: 20px; + margin-right: 180px; + margin-left: 220px; + margin-top: 10px; + text-align: justify; + background: #dad6d1; + -moz-border-radius: 10px; + border: 1px solid #ccc; +} +img { border: 0; } +div.sectionimg { + float: left; + margin-right: 10px; + margin-top: 5px; +} +p.hautpage { + margin-top: 20px; + margin-bottom: 20px; + margin-left: 10px; +} +div.leftcol { + width: 202px; + float: left; + padding: 0px; +} +div.logodiv { + border-right: 0px black solid; + border-bottom: 0px black solid; + padding: 0px; + line-height: 0px +} +div.loginbox { + margin-left: 4px; + border: solid #bbb 1px; + margin-top: 5px; + padding: 5px; + background-color: #E0DDD8; + font-size: 0.8em; + -moz-border-radius: 10px; +} +div.loginbox p { + margin: 0px; + padding: 0px; +} +div.loginbox ul { + margin-left: 2px; + margin-top: 0px; + margin-bottom: 0px; + padding: 0px; + list-style-type: none; + border-left: 1px solid #aaa; + padding-left: 8px; +} +div.loginbox h1 { + text-transform: none; + font-weight: bold; + color: #552; + font-size: 1.2em; + margin: 0px; + margin-top:10px; + border-bottom:1px solid #ccc; +} +div.loginbox h2 { + font-weight: bold; + font-size: 1.1em; + margin: 0px; +} +div.loginbox h3 { + margin-top: 0px; + margin-bottom: 5px; + font-size: 12px; +} +#poll div.otherbox ul { + margin-left: 0px; + margin-top: 0px; + margin-bottom: 10px; + padding: 0px; + list-style-type: none; +} +div.otherboxtitle { + margin-bottom: 2px; + background-color: #e0ddd8; + margin-left: 4px; + margin-top: 5px; + margin-bottom: 0px; + padding-left: 5px; + padding-top: 2px; + font-size: 11px; + border-top: 1px solid #ccc; + border-bottom: 0px solid #ccc; + font-size: 1em; + font-weight: bold; + -moz-border-radius: 10px 10px 0px 0px; + text-transform: none; +} +div.otherbox { + margin-left: 4px; + border: 1px #ccc solid; + border-top:0px; + margin-top: 0px; + padding: 5px; + text-align: left; + background: #e0ddd8; + font-size: 10px; + -moz-border-radius: 0px 0px 10px 10px; +} +div.otherbox h1 { + text-transform: uppercase; + font-weight: bold; + color: #552; + font-size: 1.2em; + margin: 0px; + text-transform: none; + border-bottom: 1px dotted #ccc; +} +div.otherbox h2 { + font-weight: bold; + font-size: 11px; + margin: 0px; +} +div.otherbox p { + margin-top: 5px; + margin-bottom: 10px; +} +div.rightlogo { + width: 90px; + float: right; + margin-right: 0px; + padding-top: 0px; + padding-left: 0px; + padding-bottom: 0px; +} +div.centraldiv { + margin-left: 220px; + margin-right: 10px; + margin-bottom: 20px; + margin-top: 10px; +} +div.centraldiv h1 { + font-weight: bold; + font-size: 14px; + margin: 0px; +} +div.centraldiv h2 { + font-weight: normal; + font-size: 12px; + margin: 0px; +} +div.centraldiv h3 { + font-weight: normal; + font-size: 12px; + margin-bottom: 20px; +} +div.lefttopbox { + padding: 5px; + border: 2px #ddd solid; + text-align: justify; + background: #f9f9f9; + font-size: 0.8em; + float: left; + -moz-border-radius: 10px; +} +div.lefttopbox p { margin-top: 5px; margin-bottom: 10px; } +div.lefttopbox h1 { + font-size: 0px; + font-weight: bold; + margin: 0px; + text-align: right; +} +div.lefttopbox h2 { + text-transform: uppercase; + font-weight: bold; + color: #ed7e00; + font-size: 13px; + margin: 0px; +} +div.lefttopbox h3 { + font-weight: bold; + font-size: 1.1em; + text-align:left; + margin: 0px; +} +div.righttopbox { + border: 1px #ccc solid; + background: white; + padding: 5px; + text-align: justify; + font-size: 0.8em; + width: 34%; + float: right; + -moz-border-radius: 10px; +} +div.righttopbox h1 { + text-transform:none; + font-weight: bold; + font-size: 10px; + margin: 0px; + color: #552; + border-bottom: 1px solid #ccc; +} +div.righttopbox h2 { + font-weight: bold; + font-size: 1.1em; + color: #666; + margin: 0px; +} +div.boardindex { + text-align: justify; + font-size: 11px; + padding: 10px; + margin-left: 20px; + margin-right: 20px; +} +a.boardindex:link,a.boardindex:visited,a.boardindex:active { + text-decoration:none; + color: red; +} +div.boardleftmsg { + float: left; + margin-top: 3px; +} +div.boardrightmsg { + margin-left: 130px; + margin-top: 3px; + padding-left: 5px; +} +div.journalbody { + margin-left: 40px; + margin-top: 40px; +} +div.journalbody h1 { + font-size: 15px; + font-weight: bold; +} +div.journalbody p { + margin-bottom: 20px; +} +.formulaire { + border: solid 1px black; +font-size: 12px; +background-color: #fffbf7; +color: #000000; +} + +.newcomments { + color: red; + font-weight: bold; +} +div.commentsreplythanks { + margin-left: 100px; + margin-top: 50px; + margin-right: 100px; + background-color: #eee; + border: black solid 1px; + padding: 10px; +} +div.archivediv { margin-right: 20px; } +.archivedate { color:#f30; } +.archivelink { + font-size: 14px; + font-weight: bold; + text-decoration: underline; +} +div.forumgroup { + text-align: left; + border: solid black 1px; + padding: 10px 20px 20px 10px; + background-color: #eeeae6; + border: 1px solid #ccc; + -moz-border-radius: 10px; +} + +div.forumgroup b{ + color: #552; + background: #e0ddd8; + font-size: 1.1em; + display:block; + -moz-border-radius: 10px; + padding-left: 10px; + padding-right: 10px; + margin-left:-15px; + margin-right:-15px; + margin-top:-3px; +} + +div.forumgroup td{ + -moz-border-radius: 10px; +} + +div.adminall { + font-size: 12px; + padding: 10px; +} +div.floatwin { + position:absolute; + top: 150px; + left: 450px; + width:200px; + visibility:hidden; + background: #dcdff4; + padding: 5px; + border: solid black 1px; +} + +div.poll-result-bar { + background: #eeeae6; + color: black; + border: solid 1px #ccc; + font-size: 0.7em; + -moz-border-radius: 10px; +} +div.funbanner { + text-align: right; + border-top: #aaa solid 1px; + border-bottom: #aaa solid 1px; + padding-right: 20px; + padding-top: 2px; + padding-bottom: 2px; + font-size: 10px; + background-color: #fff2e8; + font-weight: bold; + margin-top: 10px; +} + +div.main +{ + margin-bottom: 3em !important; +} + +.replie +{ + display: none; +} + +.deplie +{ + display: block; +} + +#commentsnav +{ + display: none; + text-align: right; +} + +#commentsbrowser +{ + border: 1px solid #baa !important; + background: #dfd6d1 !important; + color: #333333; + position: fixed; + bottom: 0.5ex; + max-height: 0.8ex; + padding: 2px 0 0; + overflow: hidden; + left: 19px !important; + right: 19px !important; + font-size: 10px; +} + +#commentsbrowser:hover +{ + max-height: none; + padding: 0.5ex; + overflow: hidden; +} + +div.comments h1.nouveau +{ + border: 1px solid rgb(211, 117, 55); + /*border: 1px solid #D58E64;*/ + background-color: rgb(225, 220, 220); + + color: #111111; +} + +#pallierdiv, #csspersodiv +{ + position: fixed; + bottom: 5em; + left: 3%; + max-width: 17em; + border: 1px solid #333333; + background: #EEEEEE; + color: #333333; + padding: 0.6em; + display: none; +} + +#csspersodiv +{ + left: 37%; + max-width: 35em; +} + +#pallierdiv input, #csspersodiv input +{ + margin-left: 0.2em; +} + +form +{ + margin: 0; +} + +#cssurl +{ + width: 12em; +} + +#pallierdiv h3, #csspersodiv h3 +{ + font-size: 12px; + padding: 0; + margin: 0 0 0.5em 0; + color: #990033; +} + +tt +{ + font-size: 1em; + background:#fffbec; + color: #330; +} + +pre, code, blockquote,quote +{ + font-size: 1em; + display:block; + background:#fffbec; + margin: 5px 15px 5px 15px; + padding: 5px; + border-left: 1px solid #552; + color: #330; +} + +tt, pre, code +{ + font-family: "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; +} + +blockquote { font-style:italic; } + +div.titledivmini { + border-top: solid #cac2a8 0px; + padding-top: 5px; + background-color: #eeeae6; + padding-left: 10px; + color:#666; + -moz-border-radius: 10px 10px 0 0; +} +h1.newstitlemini { + text-align: left; + margin: 0px 0px 0px 0px; + color: #552; + font-size: 12px !important; +} +div.subtitlemini { + border-top: solid #cac2a8 0px; + margin-top: 0px; + padding-top: 5px; + background-color: #eeeae6; + padding-left: 1px; + font-size: 11px; + color:#666; +} +div.bodydivmini { + border-top: solid #ccc 0px; + border-bottom: solid #ccc 1px; + border-left: solid #ccc 1px; + border-right: solid #ccc 1px; + padding-left: 2px; + padding-right: 2px; + padding-top: 1px; + padding-bottom: 1px; + margin-top: 0; + margin-bottom: 2px; + text-align: justify; + background: #f5f5f5; + font-size: 11px; + -moz-border-radius: 0 0 10px 10px; +} + +blockquote:before { + content: "\00AB"; + font-size:1.3em; + font-weight: bold; + color: #552; +} + +blockquote:after { + content: "\00BB"; + font-size:1.3em; + font-weight: bold; + color: #552; +} + +em{ font-style: italic; } + +#palliercontainer { + text-align: left; + margin-left: 20px; +} + +#csscourantecontainer { + text-align: center; + padding-left: 20%; +} + +#newcommentsnav { + float: right; + text-align: right; +} + +.centraldiv table { +border: 0; +} +.centraldiv table td { +border: thin solid #BBBBBB; +background:#E0DDD8; +} + +.centraldiv table th { +padding:1em; +background:#CCCCCC; +border:0; +border-top:1em solid white; +} + +div.extrait { +visibility:hidden; +position:fixed; +bottom:1em; +right:1em; +left:1em; +background:#E0DDD8; +border:thin solid #BBBBBB; +} diff --git a/gui/bweb/html/label.png b/gui/bweb/html/label.png new file mode 100644 index 0000000000..23a5f7cbd2 Binary files /dev/null and b/gui/bweb/html/label.png differ diff --git a/gui/bweb/html/last.gif b/gui/bweb/html/last.gif new file mode 100755 index 0000000000..986868c4bf Binary files /dev/null and b/gui/bweb/html/last.gif differ diff --git a/gui/bweb/html/last.png b/gui/bweb/html/last.png new file mode 100644 index 0000000000..81303ad4b4 Binary files /dev/null and b/gui/bweb/html/last.png differ diff --git a/gui/bweb/html/lcorner.png b/gui/bweb/html/lcorner.png new file mode 100644 index 0000000000..068aed64b5 Binary files /dev/null and b/gui/bweb/html/lcorner.png differ diff --git a/gui/bweb/html/left.gif b/gui/bweb/html/left.gif new file mode 100755 index 0000000000..c9d91e361f Binary files /dev/null and b/gui/bweb/html/left.gif differ diff --git a/gui/bweb/html/left.png b/gui/bweb/html/left.png new file mode 100644 index 0000000000..4fa61eac9d Binary files /dev/null and b/gui/bweb/html/left.png differ diff --git a/gui/bweb/html/load.png b/gui/bweb/html/load.png new file mode 100644 index 0000000000..493e149666 Binary files /dev/null and b/gui/bweb/html/load.png differ diff --git a/gui/bweb/html/lock.png b/gui/bweb/html/lock.png new file mode 100644 index 0000000000..9c46dbb291 Binary files /dev/null and b/gui/bweb/html/lock.png differ diff --git a/gui/bweb/html/natcompare.js b/gui/bweb/html/natcompare.js new file mode 100755 index 0000000000..2f99489476 --- /dev/null +++ b/gui/bweb/html/natcompare.js @@ -0,0 +1,157 @@ +/* +natcompare.js -- Perform 'natural order' comparisons of strings in JavaScript. +Copyright (C) 2005 by SCK-CEN (Belgian Nucleair Research Centre) +Written by Kristof Coomans + +Based on the Java version by Pierre-Luc Paour, of which this is more or less a straight conversion. +Copyright (C) 2003 by Pierre-Luc Paour + +The Java version was based on the C version by Martin Pool. +Copyright (C) 2000 by Martin Pool + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not +claim that you wrote the original software. If you use this software +in a product, an acknowledgment in the product documentation would be +appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be +misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + + +function isWhitespaceChar(a) +{ + var charCode; + charCode = a.charCodeAt(0); + + if ( charCode <= 32 ) + { + return true; + } + else + { + return false; + } +} + +function isDigitChar(a) +{ + var charCode; + charCode = a.charCodeAt(0); + + if ( charCode >= 48 && charCode <= 57 ) + { + return true; + } + else + { + return false; + } +} + +function compareRight(a,b) +{ + var bias = 0; + var ia = 0; + var ib = 0; + + var ca; + var cb; + + // The longest run of digits wins. That aside, the greatest + // value wins, but we can't know that it will until we've scanned + // both numbers to know that they have the same magnitude, so we + // remember it in BIAS. + for (;; ia++, ib++) { + ca = a.charAt(ia); + cb = b.charAt(ib); + + if (!isDigitChar(ca) + && !isDigitChar(cb)) { + return bias; + } else if (!isDigitChar(ca)) { + return -1; + } else if (!isDigitChar(cb)) { + return +1; + } else if (ca < cb) { + if (bias == 0) { + bias = -1; + } + } else if (ca > cb) { + if (bias == 0) + bias = +1; + } else if (ca == 0 && cb == 0) { + return bias; + } + } +} + +function natcompare(a,b) { + + var ia = 0, ib = 0; + var nza = 0, nzb = 0; + var ca, cb; + var result; + + while (true) + { + // only count the number of zeroes leading the last number compared + nza = nzb = 0; + + ca = a.charAt(ia); + cb = b.charAt(ib); + + // skip over leading spaces or zeros + while ( isWhitespaceChar( ca ) || ca =='0' ) { + if (ca == '0') { + nza++; + } else { + // only count consecutive zeroes + nza = 0; + } + + ca = a.charAt(++ia); + } + + while ( isWhitespaceChar( cb ) || cb == '0') { + if (cb == '0') { + nzb++; + } else { + // only count consecutive zeroes + nzb = 0; + } + + cb = b.charAt(++ib); + } + + // process run of digits + if (isDigitChar(ca) && isDigitChar(cb)) { + if ((result = compareRight(a.substring(ia), b.substring(ib))) != 0) { + return result; + } + } + + if (ca == 0 && cb == 0) { + // The strings compare the same. Perhaps the caller + // will want to call strcmp to break the tie. + return nza - nzb; + } + + if (ca < cb) { + return -1; + } else if (ca > cb) { + return +1; + } + + ++ia; ++ib; + } +} + diff --git a/gui/bweb/html/next.png b/gui/bweb/html/next.png new file mode 100644 index 0000000000..1a2162688f Binary files /dev/null and b/gui/bweb/html/next.png differ diff --git a/gui/bweb/html/nrs_table.js b/gui/bweb/html/nrs_table.js new file mode 100755 index 0000000000..e84daa8816 --- /dev/null +++ b/gui/bweb/html/nrs_table.js @@ -0,0 +1,1071 @@ +/** + * Copyright 2005 New Roads School + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * \class nrsTable + * This describes the nrsTable, which is a table created in JavaScript that is + * able to be sorted and displayed in different ways based on teh configuration + * parameters passed to it. + * to create a new table one only needs to call the setup function like so: + *
+ * nrsTable.setup(
+ * {
+ * 	table_name: "table_container",
+ * 	table_data: d,
+ * 	table_header: h
+ * }
+ * );
+ * 
+ * Where table_name is the name of the table to build. THis must be defined in + * your HTML by putting a table declaration, such as + *
. This will declare where your table will be shown. + * All sorts of parameters can be customized here. For details look at the + * function setup. + * \see setup + */ + + +/** + * Debug function. Set debug to tru to view messages. + * \param msg Message to display in an alert. + */ +debug = false; +function DEBUG(msg) +{ + if(debug) + alert(msg); +} + +/** + * There is a memory leak problem that I can't seem to fix. I'm attching + * something that I found from Aaron Boodman, which will clean up all the + * memory leaks in the page (this can be found at http://youngpup.net/2005/0221010713 + * ). This is a little clunky, but it will do till I track this problem. + * Oh, and this problem only occurrs is IE. + */ +if (window.attachEvent) { + var clearElementProps = [ + 'data', + 'onmouseover', + 'onmouseout', + 'onmousedown', + 'onmouseup', + 'ondblclick', + 'onclick', + 'onselectstart', + 'oncontextmenu' + ]; + + window.attachEvent("onunload", function() { + var el; + for(var d = document.all.length;d--;){ + el = document.all[d]; + for(var c = clearElementProps.length;c--;){ + el[clearElementProps[c]] = null; + } + } + }); +} + + +/** + * This is the constructor. + * It only needs the name of the table. This should never be called directly, + * instead use setup function. + * \param table The name of the table to create. + * \see setup + */ +function nrsTable(table) +{ + this.my_table = table; + this.field_to_sort = 0; + this.field_asc = true; +} + +new nrsTable(''); +/** + * This function is responsible for setting up an nrsTable. All the parameters + * can be configured directly from this function. The params array of this + * function is a class (or a associative array, depending on how you want to + * look at it) with the following possible parameters: + * - table_name: required. The id of the table tag. + * - table_header: required. An array containing the header names. + * - table_data: optional. A 2D array of strings containing the cell contents. + * - caption: optional. A caption to include on the table. + * - row_links: optional. An array with hyperlinks per row. Must be a javascript function. + * - cell_links: optional. A 2D array with links on every cell. Must be a javascript function + * - up_icon: optional. A path to the ascending sort arrow. + * - down_icon: optional. A path to the descending sort arrow. + * - prev_icon: optional. A path to the previous page icon in the navigation. + * - next_icon: optional. A path to the next page icon in the navigation. + * - rew_icon: optional. A path to the first page icon in the navigation. + * - fwd_icon: optional. A path to the last page icon in the navigation. + * - rows_per_page: optional. The number of rows per page to display at any one time. + * - display_nav: optional. Displays navigation (prev, next, first, last) + * - foot_headers: optional. Whether to display th eheaders at the foot of the table. + * - header_color: optional. The color of the header cells. Will default to whatever is defined in CSS. + * - even_cell_color: optional. The color of the even data cells. Will default to whatever is defined in CSS. + * - odd_cell_color: optional. The color of the odd data cells. Will default to whatever is defined in CSS. + * - footer_color: optional. The color of the footer cells. Will default to whatever is defined in CSS. + * - hover_color: optional. The color tha a row should turn when the mouse is over it. + * - padding: optional. Individual cell padding, in pixels. + * - natural_compare: optional. Uses the natural compare algorithm (separate from this program) to sort. + * - disable_sorting: optional. An array specifying the columns top disable sorting on (0 is the first column). + * + * \params params An array as described above. + */ +nrsTable.setup = function(params) +{ + //here we assign all the veriables that we are passed, or the defaults if + //they are not defined. + //Note that the only requirements are a table name and a header. + if(typeof params['table_name'] == "undefined") + { + alert("Error! You must supply a table name!"); + return null; + } + if(typeof params['table_header'] == "undefined") + { + alert("Error! You must supply a table header!"); + return null; + } + + //check if the global array exists, else create it. + if(typeof(nrsTables) == "undefined") + { + eval("nrsTables = new Array();"); + } + nrsTables[params['table_name']] = new nrsTable(params['table_name']); + nrsTables[params['table_name']].heading = params['table_header'].concat(); + + //now the non-required elements. Data elements first + nrsTables[params['table_name']].data = (typeof params['table_data'] == "undefined" || !params['table_data'])? null: params['table_data'].concat(); + nrsTables[params['table_name']].caption = (typeof params['caption'] == "undefined")? null: params['caption']; + nrsTables[params['table_name']].row_links = (typeof params['row_links'] == "undefined" || !params['row_links'])? null: params['row_links'].concat(); + nrsTables[params['table_name']].cell_links = (typeof params['cell_links'] == "undefined" || !params['row_links'])? null: params['cell_links'].concat(); + + //these are the icons. + nrsTables[params['table_name']].up_icon = (typeof params['up_icon'] == "undefined")? "up.gif": params['up_icon']; + nrsTables[params['table_name']].down_icon = (typeof params['down_icon'] == "undefined")? "down.gif": params['down_icon']; + nrsTables[params['table_name']].prev_icon = (typeof params['prev_icon'] == "undefined")? "left.gif": params['prev_icon']; + nrsTables[params['table_name']].next_icon = (typeof params['next_icon'] == "undefined")? "right.gif": params['next_icon']; + nrsTables[params['table_name']].rew_icon = (typeof params['rew_icon'] == "undefined")? "first.gif": params['rew_icon']; + nrsTables[params['table_name']].fwd_icon = (typeof params['fwd_icon'] == "undefined")? "last.gif": params['fwd_icon']; + + //now the look and feel options. + nrsTables[params['table_name']].rows_per_page = (typeof params['rows_per_page'] == "undefined")? -1: params['rows_per_page']; + nrsTables[params['table_name']].page_nav = (typeof params['page_nav'] == "undefined")? false: params['page_nav']; + nrsTables[params['table_name']].foot_headers = (typeof params['foot_headers'] == "undefined")? false: params['foot_headers']; + nrsTables[params['table_name']].header_color = (typeof params['header_color'] == "undefined")? null: params['header_color']; + nrsTables[params['table_name']].even_cell_color = (typeof params['even_cell_color'] == "undefined")? null: params['even_cell_color']; + nrsTables[params['table_name']].odd_cell_color = (typeof params['odd_cell_color'] == "undefined")? null: params['odd_cell_color']; + nrsTables[params['table_name']].footer_color = (typeof params['footer_color'] == "undefined")? null: params['footer_color']; + nrsTables[params['table_name']].hover_color = (typeof params['hover_color'] == "undefined")? null: params['hover_color']; + nrsTables[params['table_name']].padding = (typeof params['padding'] == "undefined")? null: params['padding']; + nrsTables[params['table_name']].natural_compare = (typeof params['natural_compare'] == "undefined")? false: true; + nrsTables[params['table_name']].disable_sorting = + (typeof params['disable_sorting'] == "undefined")? false: "." + params['disable_sorting'].join(".") + "."; + //finally, build the table + nrsTables[params['table_name']].buildTable(); +}; + + +/** + * This is the Javascript quicksort implementation. This will sort the + * this.data and the this.data_nodes based on the this.field_to_sort parameter. + * \param left The left index of the array. + * \param right The right index of the array + */ +nrsTable.prototype.quickSort = function(left, right) +{ + if(!this.data || this.data.length == 0) + return; +// alert("left = " + left + " right = " + right); + var i = left; + var j = right; + var k = this.data[Math.round((left + right) / 2)][this.field_to_sort]; + if (isNaN(k)) { + k = k.toLowerCase(); + } else { + k = parseInt(k, 10); + } + + while(j > i) + { + if(this.field_asc) + { + while(this.data[i][this.field_to_sort].toLowerCase() < k) + i++; + while(this.data[j][this.field_to_sort].toLowerCase() > k) + j--; + } + else + { + while(this.data[i][this.field_to_sort].toLowerCase() > k) + i++; + while(this.data[j][this.field_to_sort].toLowerCase() < k) + j--; + } + if(i <= j ) + { + //swap both values + //sort data + var temp = this.data[i]; + this.data[i] = this.data[j]; + this.data[j] = temp; + + //sort contents + var temp = this.data_nodes[i]; + this.data_nodes[i] = this.data_nodes[j]; + this.data_nodes[j] = temp; + i++; + j--; + } + } + if(left < j) + this.quickSort(left, j); + if(right > i) + this.quickSort(i, right); +} + +/** + * This is the Javascript natural sort function. Because of some obscure JavaScript + * quirck, we could not do quicsort while calling natcompare to compare, so this + * function will so a simple bubble sort using the natural compare algorithm. + */ +nrsTable.prototype.natSort = function() +{ + if(!this.data || this.data.length == 0) + return; + var swap; + for(i = 0; i < this.data.length - 1; i++) + { + for(j = i; j < this.data.length; j++) + { + if(!this.field_asc) + { + if(natcompare(this.data[i][this.field_to_sort].toLowerCase(), + this.data[j][this.field_to_sort].toLowerCase()) == -1) + swap = true; + else + swap = false; + } + else + { + if(natcompare(this.data[i][this.field_to_sort].toLowerCase(), + this.data[j][this.field_to_sort].toLowerCase()) == 1) + swap = true; + else + swap = false; + } + if(swap) + { + //swap both values + //sort data + var temp = this.data[i]; + this.data[i] = this.data[j]; + this.data[j] = temp; + + //sort contents + var temp = this.data_nodes[i]; + this.data_nodes[i] = this.data_nodes[j]; + this.data_nodes[j] = temp; + } + } + } +} + +/** + * This function will recolor all the the nodes to conform to the alternating + * row colors. + */ +nrsTable.prototype.recolorRows = function() +{ + if(this.even_cell_color || this.odd_cell_color) + { + DEBUG("Recoloring Rows. length = " + this.data_nodes.length); + for(var i = 0; i < this.data_nodes.length; i++) + { + if(i % 2 == 0) + { + if(this.even_cell_color) + this.data_nodes[i].style.backgroundColor = this.even_cell_color; + this.data_nodes[i].setAttribute("id", "even_row"); + } + else + { + if(this.odd_cell_color) + this.data_nodes[i].style.backgroundColor = this.odd_cell_color; + this.data_nodes[i].setAttribute("id", "odd_row"); + } + } + } +} + +/** + * This function will create the Data Nodes, which are a reference to the table + * rows in the HTML. + */ +nrsTable.prototype.createDataNodes = function() +{ + if(this.data_nodes) + delete this.data_nodes; + this.data_nodes = new Array(); + if(!this.data) + return; + for(var i = 0; i < this.data.length; i++) + { + var curr_row = document.createElement("TR"); + + for(var j = 0; j < this.data[i].length; j++) + { + var curr_cell = document.createElement("TD"); + //do we need to create links on every cell? + if(this.cell_links) + { + var fn = new Function("", this.cell_links[i][j]); + curr_cell.onclick = fn; + curr_cell.style.cursor = 'pointer'; + } + //workaround for IE + curr_cell.setAttribute("className", "dataTD" + j); + //assign the padding + if(this.padding) + { + curr_cell.style.paddingLeft = this.padding + "px"; + curr_cell.style.paddingRight = this.padding + "px"; + } + + if (typeof this.data[i][j] == "object") { + curr_cell.appendChild(this.data[i][j]); + } else { + curr_cell.appendChild(document.createTextNode(this.data[i][j])); + } + + curr_row.appendChild(curr_cell); + } + //do we need to create links on every row? + if(!this.cell_links && this.row_links) + { + var fn = new Function("", this.row_links[i]); + curr_row.onclick = fn; + curr_row.style.cursor = 'pointer'; + } + //sets the id for odd and even rows. + if(i % 2 == 0) + { + curr_row.setAttribute("id", "even_row"); + if(this.even_cell_color) + curr_row.style.backgroundColor = this.even_cell_color; + } + else + { + curr_row.setAttribute("id", "odd_row"); + if(this.odd_cell_color) + curr_row.style.backgroundColor = this.odd_cell_color; + } + if(this.hover_color) + { + curr_row.onmouseover = new Function("", "this.style.backgroundColor='" + this.hover_color + "';"); + curr_row.onmouseout = new Function("", "this.style.backgroundColor=(this.id=='even_row')?'" + + this.even_cell_color + "':'" + this.odd_cell_color + "';"); + } + this.data_nodes[i] = curr_row; + } +} + +/** + * This function will update the nav page display. + */ +nrsTable.prototype.updateNav = function() +{ + if(this.page_nav) + { + var p = 0; + if(this.foot_headers) + p++; + var t = document.getElementById(this.my_table); + var nav = t.tFoot.childNodes[p]; + if(nav) + { + var caption = t.tFoot.childNodes[p].childNodes[0].childNodes[2]; + caption.innerHTML = "Page " + (this.current_page + 1) + " of " + this.num_pages; + } + else + { + if(this.num_pages > 1) + { + this.insertNav(); + nav = t.tFoot.childNodes[p]; + } + } + if(nav) + { + if(this.current_page == 0) + this.hideLeftArrows(); + else + this.showLeftArrows(); + + if(this.current_page + 1 == this.num_pages) + this.hideRightArrows(); + else + this.showRightArrows(); + } + } +} + +/** + * This function will flip the sort arrow in place. If a heading is used in the + * footer, then it will flip that one too. + */ +nrsTable.prototype.flipSortArrow = function() +{ + this.field_asc = !this.field_asc; + //flip the arrow on the heading. + var heading = document.getElementById(this.my_table).tHead.childNodes[0].childNodes[this.field_to_sort]; + if(this.field_asc) + heading.getElementsByTagName("IMG")[0].setAttribute("src", this.up_icon); + else + heading.getElementsByTagName("IMG")[0].setAttribute("src", this.down_icon); + //is there a heading in the footer? + if(this.foot_headers) + { + //yes, so flip that arrow too. + var footer = document.getElementById(this.my_table).tFoot.childNodes[0].childNodes[this.field_to_sort]; + if(this.field_asc) + footer.getElementsByTagName("IMG")[0].setAttribute("src", this.up_icon); + else + footer.getElementsByTagName("IMG")[0].setAttribute("src", this.down_icon); + } +} + +/** + * This function will move the sorting arrow from the place specified in + * this.field_to_sort to the passed parameter. It will also set + * this.field_to_sort to the new value. It will also do it in the footers, + * if they exists. + * \param field The new field to move it to. + */ +nrsTable.prototype.moveSortArrow = function(field) +{ + var heading = document.getElementById(this.my_table).tHead.childNodes[0].childNodes[this.field_to_sort]; + var img = heading.removeChild(heading.getElementsByTagName("IMG")[0]); + heading = document.getElementById(this.my_table).tHead.childNodes[0].childNodes[field]; + heading.appendChild(img); + //are there headers in the footers. + if(this.foot_headers) + { + //yes, so switch them too. + var footer = document.getElementById(this.my_table).tFoot.childNodes[0].childNodes[this.field_to_sort]; + var img = footer.removeChild(footer.getElementsByTagName("IMG")[0]); + footer = document.getElementById(this.my_table).tFoot.childNodes[0].childNodes[field]; + footer.appendChild(img); + } + //finally, set the field to sort by. + this.field_to_sort = field; +} + +/** + * This function completely destroys a table. Should be used only when building + * a brand new table (ie, new headers). Else you should use a function like + * buildNewData which only deletes the TBody section. + */ +nrsTable.prototype.emptyTable = function() +{ + var t = document.getElementById(this.my_table); + while(t.childNodes.length != 0) + t.removeChild(t.childNodes[0]); +}; + +/** + * This function builds a brand new table from scratch. This function should + * only be called when a brand new table (with headers, footers, etc) needs + * to be created. NOT when refreshing data or changing data. + */ +nrsTable.prototype.buildTable = function() +{ + //reset the sorting information. + this.field_to_sort = 0; + this.field_asc = true; + + //remove the nodes links. + delete this.data_nodes; + + //do we have to calculate the number of pages? + if(this.data && this.rows_per_page != -1) + { + //we do. + this.num_pages = Math.ceil(this.data.length / this.rows_per_page); + this.current_page = 0; + } + + //blank out the table. + this.emptyTable(); + + //this is the table that we will be using. + var table = document.getElementById(this.my_table); + + //is there a caption? + if(this.caption) + { + var caption = document.createElement("CAPTION"); + caption.setAttribute("align", "top"); + caption.appendChild(document.createTextNode(this.caption)); + table.appendChild(caption); + } + + //do the heading first + var table_header = document.createElement("THEAD"); + var table_heading = document.createElement("TR"); + //since this is a new table the first field is what's being sorted. + var curr_cell = document.createElement("TH"); + var fn = new Function("", "nrsTables['" + this.my_table + "'].fieldSort(" + 0 + ");"); + if(!this.disable_sorting || this.disable_sorting.indexOf(".0.") == -1) + curr_cell.onclick = fn; + if(this.header_color) + curr_cell.style.backgroundColor = this.header_color; + curr_cell.style.cursor = 'pointer'; + var img = document.createElement("IMG"); + img.setAttribute("src", this.up_icon); + img.setAttribute("border", "0"); + img.setAttribute("height", "8"); + img.setAttribute("width", "8"); + curr_cell.appendChild(document.createTextNode(this.heading[0])); + curr_cell.appendChild(img); + table_heading.appendChild(curr_cell); + //now do the rest of the heading. + for(var i = 1; i < this.heading.length; i++) + { + curr_cell = document.createElement("TH"); + var fn = new Function("", "nrsTables['" + this.my_table + "'].fieldSort(" + i + ");"); + if(!this.disable_sorting || this.disable_sorting.indexOf("." + i + ".") == -1) + curr_cell.onclick = fn; + if(this.header_color) + curr_cell.style.backgroundColor = this.header_color; + curr_cell.style.cursor = 'pointer'; + //build the sorter + curr_cell.appendChild(document.createTextNode(this.heading[i])); + table_heading.appendChild(curr_cell); + } + table_header.appendChild(table_heading); + + //now the content + var table_body = document.createElement("TBODY"); + this.createDataNodes(); + if(this.data) + { + if(this.natural_compare) + this.natSort(0, this.data.length - 1); + else + this.quickSort(0, this.data.length - 1); + this.recolorRows(); + } + + //finally, the footer + var table_footer = document.createElement("TFOOT"); + if(this.foot_headers) + { + table_footer.appendChild(table_heading.cloneNode(true)); + } + + if(this.page_nav && this.num_pages > 1) + { + //print out the page navigation + //first and previous page + var nav = document.createElement("TR"); + var nav_cell = document.createElement("TH"); + nav_cell.colSpan = this.heading.length; + + var left = document.createElement("DIV"); + if(document.attachEvent) + left.style.styleFloat = "left"; + else + left.style.cssFloat = "left"; + var img = document.createElement("IMG"); + img.setAttribute("src", this.rew_icon); + img.setAttribute("border", "0"); + img.setAttribute("height", "10"); + img.setAttribute("width", "10"); + img.onclick = new Function("", "nrsTables['" + this.my_table + "'].firstPage();"); + img.style.cursor = 'pointer'; + left.appendChild(img); + //hack to space the arrows, cause IE is absolute crap + left.appendChild(document.createTextNode(" ")); + img = document.createElement("IMG"); + img.setAttribute("src", this.prev_icon); + img.setAttribute("border", "0"); + img.setAttribute("height", "10"); + img.setAttribute("width", "10"); + img.onclick = new Function("", "nrsTables['" + this.my_table + "'].prevPage();"); + img.style.cursor = 'pointer'; + left.appendChild(img); + //apend it to the cell + nav_cell.appendChild(left); + + //next and last pages + var right = document.createElement("DIV"); + if(document.attachEvent) + right.style.styleFloat = "right"; + else + right.style.cssFloat = "right"; + img = document.createElement("IMG"); + img.setAttribute("src", this.next_icon); + img.setAttribute("border", "0"); + img.setAttribute("height", "10"); + img.setAttribute("width", "10"); + img.onclick = new Function("", "nrsTables['" + this.my_table + "'].nextPage();"); + img.style.cursor = 'pointer'; + right.appendChild(img); + //hack to space the arrows, cause IE is absolute crap + right.appendChild(document.createTextNode(" ")); + img = document.createElement("IMG"); + img.setAttribute("src", this.fwd_icon); + img.setAttribute("border", "0"); + img.setAttribute("height", "10"); + img.setAttribute("width", "10"); + img.onclick = new Function("", "JavaScript:nrsTables['" + this.my_table + "'].lastPage();"); + img.style.cursor = 'pointer'; + right.appendChild(img); + //apend it to the cell + nav_cell.appendChild(right); + + //page position + var pos = document.createElement("SPAN"); + pos.setAttribute("id", "nav_pos"); + pos.appendChild(document.createTextNode("Page " + + (this.current_page + 1) + " of " + this.num_pages)); + //append it to the cell. + nav_cell.appendChild(pos); + + nav.appendChild(nav_cell); + //append it to the footer + table_footer.appendChild(nav); + } + + if(this.footer_color) + { + for(var i = 0; i < table_footer.childNodes.length; i++) + table_footer.childNodes[i].style.backgroundColor = this.footer_color; + } + + //append the data + table.appendChild(table_header); + table.appendChild(table_body); + table.appendChild(table_footer); + if(this.data) + { + if(this.natural_compare) + this.natSort(0, this.data.length - 1); + else + this.quickSort(0, this.data.length - 1); + } + this.refreshTable(); +}; + +/** + * This function will remove the elements in teh TBody section of the table and + * return an array of references of those elements. This array can then be + * sorted and re-inserted into the table. + * \return An array to references of the TBody contents. + */ +nrsTable.prototype.extractElements = function() +{ + var tbody = document.getElementById(this.my_table).tBodies[0]; + var nodes = new Array(); + var i = 0; + while(tbody.childNodes.length > 0) + { + nodes[i] = tbody.removeChild(tbody.childNodes[0]); + i++; + } + return nodes; +} + +/** + * This function will re-insert an array of elements into the TBody of a table. + * Note that the array elements are stored in the this.data_nodes reference. + */ +nrsTable.prototype.insertElements = function() +{ + var tbody = document.getElementById(this.my_table).tBodies[0]; + var start = 0; + var num_elements = this.data_nodes.length; + if(this.rows_per_page != -1) + { + start = this.current_page * this.rows_per_page; + num_elements = (this.data_nodes.length - start) > this.rows_per_page? + this.rows_per_page + start: + this.data_nodes.length; + } + DEBUG("start is " + start + " and num_elements is " + num_elements); + for(var i = start; i < num_elements; i++) + { + tbody.appendChild(this.data_nodes[i]); + } +} + +/** + * This function will sort the table's data by a specific field. The field + * parameter referes to which field index should be sorted. + * \param field The field index which to sort on. + */ +nrsTable.prototype.fieldSort = function(field) +{ + if(this.field_to_sort == field) + { + //only need to reverse the array. + if(this.data) + { + this.data.reverse(); + this.data_nodes.reverse(); + } + //flip the arrow on the heading. + this.flipSortArrow(); + } + else + { + //In this case, we need to sort the array. We'll sort it last, first + //make sure that the arrow images are set correctly. + this.moveSortArrow(field); + //finally, set the field to sort by. + this.field_to_sort = field; + if(this.data) + { + //we'll be using our implementation of quicksort + if(this.natural_compare) + this.natSort(0, this.data.length - 1); + else + this.quickSort(0, this.data.length - 1); + } + } + //finally, we refresh the table. + this.refreshTable(); +}; + +/** + * This function will refresh the data in the table. This function should be + * used whenever the nodes have changed, or when chanign pages. Note that + * this will NOT re-sort. + */ +nrsTable.prototype.refreshTable = function() +{ + this.extractElements(); + this.recolorRows(); + this.insertElements(); + //finally, if there is a nav, upate it. + this.updateNav(); +} + +/** + * This function will advance a page. If we are already at the last page, then + * it will remain there. + */ +nrsTable.prototype.nextPage = function() +{ + DEBUG("current page is " + this.current_page + " and num_pages is " + this.num_pages); + if(this.current_page + 1 != this.num_pages) + { + this.current_page++; + this.refreshTable(); + } + DEBUG("current page is " + this.current_page + " and num_pages is " + this.num_pages); +} + +/** + * This function will go back a page. If we are already at the first page, then + * it will remain there. + */ +nrsTable.prototype.prevPage = function() +{ + DEBUG("current page is " + this.current_page + " and num_pages is " + this.num_pages); + if(this.current_page != 0) + { + this.current_page--; + this.refreshTable(); + } + DEBUG("current page is " + this.current_page + " and num_pages is " + this.num_pages); +} + +/** + * This function will go to the first page. + */ +nrsTable.prototype.firstPage = function() +{ + if(this.current_page != 0) + { + this.current_page = 0; + this.refreshTable(); + } +} + +/** + * This function will go to the last page. + */ +nrsTable.prototype.lastPage = function() +{ + DEBUG("lastPage(), current_page: " + this.current_page + " and num_pages: " + this.num_pages); + if(this.current_page != (this.num_pages - 1)) + { + this.current_page = this.num_pages - 1; + this.refreshTable(); + } +} + +/** + * This function will go to a specific page. valid values are pages 1 to + * however many number of pages there are. + * \param page The page number to go to. + */ +nrsTable.prototype.gotoPage = function(page) +{ + page--; + if(page >=0 && page < this.num_pages) + { + this.current_page = page; + this.refreshTable(); + } +} + +/** + * This function can be used to change the number of entries per row displayed + * on the fly. + * \param entries The number of entries per page. + */ +nrsTable.prototype.changeNumRows = function(entries) +{ + if(entries > 0) + { + this.rows_per_page = entries; + //we do. + this.num_pages = Math.ceil(this.data.length / this.rows_per_page); + this.refreshTable(); + } +} + +/** + * This function will take in a new data array and , optionally, a new cell_link + * array OR a new row_link array. Only one will be used, with the cell_link + * array taking precedence. It will then re-build the table with the new data + * array. + * \param new_data This is the new data array. This is required. + * \param cell_links This is the new cell links array, a 2D array for each cell. + * \param row_links This is the new row links array, a 1D array for each row. + */ +nrsTable.prototype.newData = function(new_data, cell_links, row_links) +{ + //extract the elements from teh table to clear the table. + this.extractElements(); + //now delete all the data related to this table. I do this so that + //(hopefully) the memory will be freed. This is realy needed for IE, whose + //memory handling is almost non-existant + delete this.data; + delete this.data_nodes; + delete this.cell_links; + delete this.row_links + //now re-assign. + this.data = new_data; + this.cell_links = cell_links; + this.row_links = row_links; + if(this.rows_per_page != -1) + { + //we do. + this.num_pages = Math.ceil(this.data.length / this.rows_per_page); + if(this.num_pages <= 1 && this.page_nav) + this.removeNav(); + else if(this.page_nav) + this.insertNav(); + this.current_page = 0; + } + this.createDataNodes(); + if(this.field_to_sort != 0) + this.moveSortArrow(0); + if(!this.field_asc) + this.flipSortArrow(); + this.insertElements(); + this.updateNav(); +} + +/** + * This function will remove the NAV bar (if one exists) from the table. + */ +nrsTable.prototype.removeNav = function() +{ + if(this.page_nav) + { + //in this case, remove the nav from the existing structure. + var table = document.getElementById(this.my_table); + var p = 0; + if(this.foot_headers) + p++; + var nav = table.tFoot.childNodes[p]; + if(nav) + { + table.tFoot.removeChild(nav); + delete nav; + } + } +} + +/** + * This function wil re-insert the nav into the table. + */ +nrsTable.prototype.insertNav = function() +{ + table = document.getElementById(this.my_table); + var p = 0; + if(this.foot_headers) + p++; + if(this.page_nav && !table.tFoot.childNodes[p]) + { + //this means there should be a nav and there isn't one. + //print out the page navigation + //first and previous page + var nav = document.createElement("TR"); + var nav_cell = document.createElement("TH"); + nav_cell.colSpan = this.heading.length; + + var left = document.createElement("DIV"); + if(document.attachEvent) + left.style.styleFloat = "left"; + else + left.style.cssFloat = "left"; + var img = document.createElement("IMG"); + img.setAttribute("src", this.rew_icon); + img.setAttribute("border", "0"); + img.setAttribute("height", "10"); + img.setAttribute("width", "10"); + img.onclick = new Function("", "nrsTables['" + this.my_table + "'].firstPage();"); + img.style.cursor = 'pointer'; + left.appendChild(img); + //hack to space the arrows, cause IE is absolute crap + left.appendChild(document.createTextNode(" ")); + img = document.createElement("IMG"); + img.setAttribute("src", this.prev_icon); + img.setAttribute("border", "0"); + img.setAttribute("height", "10"); + img.setAttribute("width", "10"); + img.onclick = new Function("", "nrsTables['" + this.my_table + "'].prevPage();"); + img.style.cursor = 'pointer'; + left.appendChild(img); + //apend it to the cell + nav_cell.appendChild(left); + + //next and last pages + var right = document.createElement("DIV"); + if(document.attachEvent) + right.style.styleFloat = "right"; + else + right.style.cssFloat = "right"; + img = document.createElement("IMG"); + img.setAttribute("src", this.next_icon); + img.setAttribute("border", "0"); + img.setAttribute("height", "10"); + img.setAttribute("width", "10"); + img.onclick = new Function("", "nrsTables['" + this.my_table + "'].nextPage();"); + img.style.cursor = 'pointer'; + right.appendChild(img); + //hack to space the arrows, cause IE is absolute crap + right.appendChild(document.createTextNode(" ")); + img = document.createElement("IMG"); + img.setAttribute("src", this.fwd_icon); + img.setAttribute("border", "0"); + img.setAttribute("height", "10"); + img.setAttribute("width", "10"); + img.onclick = new Function("", "JavaScript:nrsTables['" + this.my_table + "'].lastPage();"); + img.style.cursor = 'pointer'; + right.appendChild(img); + //apend it to the cell + nav_cell.appendChild(right); + + //page position + var pos = document.createElement("SPAN"); + pos.setAttribute("id", "nav_pos"); + pos.appendChild(document.createTextNode("Page " + + (this.current_page + 1) + " of " + this.num_pages)); + //append it to the cell. + nav_cell.appendChild(pos); + + nav.appendChild(nav_cell); + //append it to the footer + table.tFoot.appendChild(nav); + } +} + +/** + * This function will hide the previous arrow and the rewind arrows from the + * nav field. + */ +nrsTable.prototype.hideLeftArrows = function() +{ + if(!this.page_nav) + return; + var myTable = document.getElementById(this.my_table); + var p = 0; + if(this.foot_headers) + p++; + var nav = myTable.tFoot.childNodes[p]; + nav.childNodes[0].childNodes[0].style.display = "none"; +} + +/** + * This function will show the previous arrow and the rewind arrows from the + * nav field. + */ +nrsTable.prototype.showLeftArrows = function() +{ + if(!this.page_nav) + return; + table = document.getElementById(this.my_table); + var p = 0; + if(this.foot_headers) + p++; + var nav = table.tFoot.childNodes[p]; + nav.childNodes[0].childNodes[0].style.display = "block"; +} + +/** + * This function will hide the next arrow and the fast foward arrows from the + * nav field. + */ +nrsTable.prototype.hideRightArrows = function() +{ + if(!this.page_nav) + return; + table = document.getElementById(this.my_table); + var p = 0; + if(this.foot_headers) + p++; + var nav = table.tFoot.childNodes[p]; + nav.childNodes[0].childNodes[1].style.display = "none"; +} + +/** + * This function will show the next arrow and the fast foward arrows from the + * nav field. + */ +nrsTable.prototype.showRightArrows = function() +{ + if(!this.page_nav) + return; + table = document.getElementById(this.my_table); + var p = 0; + if(this.foot_headers) + p++; + var nav = table.tFoot.childNodes[p]; + nav.childNodes[0].childNodes[1].style.display = "block"; +} diff --git a/gui/bweb/html/prev.png b/gui/bweb/html/prev.png new file mode 100644 index 0000000000..c60eb07c11 Binary files /dev/null and b/gui/bweb/html/prev.png differ diff --git a/gui/bweb/html/prune.png b/gui/bweb/html/prune.png new file mode 100644 index 0000000000..96b6d57ddf Binary files /dev/null and b/gui/bweb/html/prune.png differ diff --git a/gui/bweb/html/purge.png b/gui/bweb/html/purge.png new file mode 100644 index 0000000000..9ba7c5325d Binary files /dev/null and b/gui/bweb/html/purge.png differ diff --git a/gui/bweb/html/rcorner.png b/gui/bweb/html/rcorner.png new file mode 100644 index 0000000000..1099733004 Binary files /dev/null and b/gui/bweb/html/rcorner.png differ diff --git a/gui/bweb/html/remove.png b/gui/bweb/html/remove.png new file mode 100644 index 0000000000..19eaef3ef5 Binary files /dev/null and b/gui/bweb/html/remove.png differ diff --git a/gui/bweb/html/right.gif b/gui/bweb/html/right.gif new file mode 100755 index 0000000000..9ab5a5f011 Binary files /dev/null and b/gui/bweb/html/right.gif differ diff --git a/gui/bweb/html/right.png b/gui/bweb/html/right.png new file mode 100644 index 0000000000..ac0c51ae16 Binary files /dev/null and b/gui/bweb/html/right.png differ diff --git a/gui/bweb/html/save.png b/gui/bweb/html/save.png new file mode 100644 index 0000000000..b52d1d71fa Binary files /dev/null and b/gui/bweb/html/save.png differ diff --git a/gui/bweb/html/style.css b/gui/bweb/html/style.css new file mode 100644 index 0000000000..2831248d62 --- /dev/null +++ b/gui/bweb/html/style.css @@ -0,0 +1,720 @@ +* { + font-family: Verdana, Arial, Helvetica, sans-serif, monospace; +} + +body { + background: #606060; + color: #000000; +} + +a:link { + color: #0000FF; + background: transparent; + text-decoration: none; +} + +a:visited { + color: #990099; + background: transparent; + text-decoration: none; +} + +a:active { + color: #000000; + background: #ADD8E6; + text-decoration: none; +} + +h1.rubrique_info { + color: #990033; + margin: 0px 0px 0px 0px; + padding: 0em; + border: 0px; + font-size: 12px; +} +h1.connexe { + font-size: 12px; + padding: 0em; + margin: 0px 0px 0px 0px; + color: #990033; +} + +a.rubrique_infolink { + text-decoration: none; +} +ul.rubrique_infoul { + display: inline; + list-style-type: square; +} +ul.rubrique_infoul * { + width: 100%; +} +li.rubrique_infoul { + margin-left: 15px; +} +h1 { + color: #990033; +} + +div.main { + background: white; + color: #000000; + margin-left: 5px; + margin-right: 5px; + /*padding-left: 10px; + padding-right: 10px;*/ + border: 1px black solid; + text-align: left; + font-size: 12px; +} +div.lsfnbanner { + margin-left: 150px; + margin-right: 170px; + border-top: none; + padding-left: 10px; + padding-right: 10px; + border-bottom: 1px black solid; + border-right: 1px black solid; + border-left: 1px black solid; + text-align: left; + font-size: 11px; + padding-top: 2px; + background-color: #eeeae6; +} +div.footer { + padding-top: 5px; + padding-bottom: 3px; + border-top: 1px black solid; + border-left: 1px black solid; + border-right: 1px black solid; + text-align: left; + font-size: 9px; + background: #dcdff4; + margin-top: 40px; + margin-left: 20px; + margin-right: 20px; +} +div.footer p { + margin-left: 10px; + margin-top: 2px; + margin-bottom: 2px; +} +a.lsfnlink:link,a.lsfnlink:visited,a.lsfnlink:active { + text-decoration: none; + color: #333333; + font-size: 10px; +} +a.lsfnlink:hover { + text-decoration: underline; + color: black; +} +div.menubartop { + margin-bottom: 10px; + padding-left: 10px; + padding-right: 10px; + padding-top: 0px; + font-size: 13px; +} +div.smallmenubar { + background: white; + padding-left: 10px; + padding-right: 10px; + padding-top: 0px; + padding-bottom: 0px; + font-weight:bold; + font-size: 10px; + text-align: right; +} +div.menuevent { + float: left; + width: 350px; + font-size: 11px; + text-align: left; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 10px; + font-weight: bold; + margin: 0px; +} +div.menubar { + background: #cac2a8; + border-top: 1px black solid; + border-bottom: 1px black solid; + padding-left: 10px; + padding-right: 10px; + padding-top: 3px; + padding-bottom: 2px; + font-weight:bold; + font-size: 13px; +} +div.menubar a { + text-decoration: none; +} +div.menubar p { + padding: 0px; + margin: 0px; +} +div.menudate { + float: left; + width: 130px; + padding-top: 5px; +} +div.menuaccroche { + margin-left: 30px; + float: left; + text-decoration: underline; + font-size: 14px; +} +div.menusearch { + float: right; + text-align: right; + padding-top: 5px; + width: 170px; +} +div.menusearch p { + margin: 0px 0px 0px 0px; +} +div.newsletter { + float: right; + text-align: right; + margin: 0px; + padding: 0px; + font-size: 8px; + font-weight: normal; +} +div.newsletter p { + margin: 0px 0px 0px 0px; +} +.searchinput { + border: solid 1px black; +} +.newsletterinput { + border: solid 1px black; + font-weight: normal; +} +a#menulinkselect { + color: #ed7e00; +} + +div.leftbox { + width: 200px; + float: left; + padding-left: 5px; + padding-right: 5px; + padding-bottom: 5px; + border-right: 1px black solid; + border-bottom: 1px black solid; + background: white; + margin-bottom: 10px; + margin-right: 20px; + overflow: hidden; +} +div.leftbox h1 { + text-transform: uppercase; + font-weight: bold; + color: #ed7e00; + font-size: 10px; + margin: 0px; +} +div.leftbox h2 { + font-weight: bold; + font-size: 12px; + margin: 0px; +} +div.leftbox ul { + list-style-type: square; + margin-bottom: 10px; + margin-left: 0em; + padding-left: 0em; +} +div.leftbox li { + margin-left: 10px; + margin-top: 10px; +} + +div.rightbox { + width: 150px; + float: right; + padding-top: 10px; + padding-left: 5px; + padding-right: 15px; + padding-bottom: 5px; + border-left: 1px black solid; + border-bottom: 1px black solid; + text-align: left; +} +div.tipdiv { + margin-right: 50px; + padding-top: 5px; + padding-right: 10px; + padding-left: 10px; + text-align: justify; + background-color: #eee; + border: black solid 1px; + margin-left: 10px; +} +div.tipdiv h1 { + font-weight: bold; + font-size: 14px; + color: black; + margin-top: 0px; + margin-bottom: 20px; +} +div.tipdiv h2 { + text-transform: uppercase; + font-weight: bold; + color: #ed7e00; + font-size: 12px; + margin: 0px; +} + +div.newsdiv { + margin-left: 220px; + margin-right: 180px; + margin-top: 10px; + margin-bottom: 20px; + text-align: justify; +} +div.newsdiv h1 { + font-weight: bold; + font-size: 14px; + margin: 0px; +} +div.newsdiv h2 { + font-weight: normal; + font-size: 12px; + margin: 0px; +} +div.newsdiv h3 { + font-weight: normal; + font-size: 12px; + margin-bottom: 20px; +} +div.objdiv { + margin-left: 220px; + margin-right: 20px; + margin-top: 10px; + margin-bottom: 20px; +} + +h1.newstitle { + text-align: left; + font-size: 14px; + margin: 0px 0px 0px 0px; + color: black; +} +div.titlediv { + border-top: solid #cac2a8 2px; + margin-top: 20px; + background-color: #eeeae6; + padding-left: 10px; + font-size: 11px; +} +div.bodydiv { + border: solid #9e9784 1px; + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; + padding-bottom: 5px; + margin-top: 2px; + text-align: justify; +} +div.bodydiv ul { + list-style-type: square; +} + +div.comments { + padding: 10px; + border-top: solid 2px #d37537; + border-bottom: solid 2px #d37537; + margin-top: 20px; + margin-bottom: 10px; + background-color: #cacaca; + font-size: 12px; + line-height: 1.3em; +} + +.comment-vote { + font-style: italic; +} + +p.commentsbody { + border-left-style: solid; + border-width: 1px; + border-color: rgb(0, 0, 0); + padding-left: 10px; + text-align: justify; + margin-right: 20px; +} +div.commentsreply { + /*margin-left: 220px;*/ + text-align: center; + margin-top: 50px; +} + +ul.commentsul { + list-style-type: none; + margin-bottom: 10px; + margin-left: 1.25em; + padding-left: 0em; + /*border-left: 1px solid black;*/ +} +ul.commentsli { + margin: 10px; +} +div.comments li { + margin-top: 20px; + margin-left: 2px; +} + +div.comments h1 { + font-size: 12px; + color: black; + margin: 0px 20px 3px 0px; + background-color: rgb(226, 226, 226); + padding-left: 1px; + font-weight: normal; +} + +div.articlediv { + padding-left: 20px; + padding-right: 20px; + padding-top: 10px; + padding-bottom: 20px; + margin-right: 180px; + margin-left: 220px; + border: solid 1px black; + margin-top: 10px; + text-align: justify; + background-color: #E2E2E2; +} +img { + border: 0px; +} +div.sectionimg { + float: left; + margin-right: 10px; + margin-top: 5px; +} +p.hautpage { + margin-top: 20px; + margin-bottom: 20px; + margin-left: 10px; +} +div.leftcol { + /*width: 202px;*/ + width: 202px; + float: left; + padding: 0px; + /*overflow: hidden;*/ +} +div.logodiv { + border-right: 1px black solid; + border-bottom: 1px black solid; + padding: 0px; + line-height: 0px +} +div.loginbox { + margin-left: 4px; + border: solid #a59f8b 1px; + margin-top: 2px; + padding: 5px; + background-color: #fff2e8; + font-size: 10px; +} +div.loginbox p { + margin: 0px; + padding: 0px; +} +div.loginbox ul { + margin-left: 10px; + margin-top: 0px; + margin-bottom: 0px; + padding: 0px; + list-style-type: none; +} +div.loginbox h1 { + text-transform: uppercase; + font-weight: bold; + color: #ed7e00; + font-size: 10px; + margin: 0px; +} +div.loginbox h2 { + font-weight: bold; + font-size: 12px; + margin: 0px; +} +div.loginbox h3 { + margin-top: 0px; + margin-bottom: 5px; + font-size: 12px; +} +div.polldivtitle { + margin-bottom: 1px; + background-color: #cac2a8; + margin-left: 4px; + margin-top: 15px; + padding-left: 5px; + font-size: 10px; + border-top: solid #a59f8b 1px; + border-bottom: solid #a59f8b 1px; + text-transform: uppercase; +} +div.polldiv { + margin-left: 4px; + border: 1px #a59f8b solid; + margin-top: 0px; + padding: 5px; + background: #fff2e8; +} +div.polldiv p { +margin: 5px; padding: 0px; +} +div.polldiv ul { + margin-left: 5px; + margin-top: 0px; + margin-bottom: 10px; + padding: 0px; + list-style-type: none; +} +div.otherboxtitle { + margin-bottom: 2px; + background-color: #e3dabc; + margin-left: 4px; + margin-top: 10px; + padding-left: 5px; + padding-top: 2px; + font-size: 11px; + border-top: solid #777364 1px; + border-bottom: solid #777364 1px; + /*text-transform: uppercase;*/ + /*font-weight: bold;*/ +} +div.otherbox { + margin-left: 4px; + margin-right: 1px; + border: 1px #a59f8b solid; + margin-top: 0px; + padding: 5px; + text-align: justify; + background: #fff2e8; + font-size: 10px; +} +div.otherbox h1 { + text-transform: uppercase; + font-weight: bold; + color: #ed7e00; + font-size: 10px; + margin: 0px; +} +div.otherbox h2 { + font-weight: bold; + font-size: 11px; + margin: 0px; +} +div.otherbox p { + margin-top: 5px; + margin-bottom: 10px; +} +div.rightlogo { + width: 90px; + float: right; + margin-right: 0px; + padding-top: 0px; + padding-left: 0px; + padding-bottom: 0px; +} +div.centraldiv { + margin-left: 220px; + margin-right: 10px; + margin-bottom: 20px; + margin-top: 10px; +} +div.centraldiv h1 { + font-weight: bold; + font-size: 14px; + margin: 0px; +} +div.centraldiv h2 { + font-weight: normal; + font-size: 12px; + margin: 0px; +} +div.centraldiv h3 { + font-weight: normal; + font-size: 12px; + margin-bottom: 20px; +} +div.centralinfo { + margin-bottom: 20px; +} +div.lefttopbox { + padding-left: 5px; + padding-right: 5px; + padding-top: 5px; + text-align: justify; + border: 2px #a59f8b solid; + background: #ffffbb; + /* + background: #c8ff9b; + */ + font-size: 13px; + width: 60%; + float: left; +} +div.lefttopbox p { + margin-top: 5px; margin-bottom: 10px; +} +div.lefttopbox h1 { + font-size: 13px; + font-weight: bold; + margin: 0px; + text-align: right; +} +div.lefttopbox h2 { + text-transform: uppercase; + font-weight: bold; + color: #ed7e00; + font-size: 13px; + margin: 0px; +} +div.lefttopbox h3 { + font-weight: bold; + font-size: 14px; + margin: 0px; +} +div.righttopbox { + border: 1px #a59f8b solid; + background: white; + padding: 5px; + text-align: justify; + font-size: 12px; + width: 34%; + float: right; +} +div.righttopbox h1 { + text-transform: uppercase; + font-weight: bold; + color: #ed7e00; + font-size: 10px; + margin: 0px; +} +div.righttopbox h2 { + font-weight: bold; + font-size: 12px; + margin: 0px; +} +div.boardindex { + text-align: justify; + font-size: 11px; + padding: 10px; + margin-left: 20px; + margin-right: 20px; +} +a.boardindex:link,a.boardindex:visited,a.boardindex:active { + text-decoration:none; + color: red; +} +div.boardleftmsg { + float: left; + margin-top: 3px; +} +div.boardrightmsg { + margin-left: 130px; + margin-top: 3px; + padding-left: 5px; +} +div.journalbody { + margin-left: 40px; + margin-top: 40px; +} +div.journalbody h1 { + font-size: 15px; + font-weight: bold; +} +div.journalbody p { + margin-bottom: 20px; +} +.formulaire { + border: solid 1px black; + font-size: 12px; + background-color: #fffbf7; + color: #000000; +} +.formulaire:focus { + background-color: #eeeae6; + border: 1px solid #777; + color: #000000; +} +.newcomments { + color: red; + font-weight: bold; +} +.misspelled { + color: red; + font-weight: bold; +} +div.commentsreplythanks { + margin-left: 100px; + margin-top: 50px; + margin-right: 100px; + background-color: #eee; + border: black solid 1px; + padding: 10px; +} +div.archivediv { + margin-right: 20px; +} +.archivedate { + color:#f30; +} +.archivelink { + font-size: 14px; + font-weight: bold; + text-decoration: underline; +} +div.forumgroup { + text-align: left; + border: solid black 1px; + padding: 10px 20px 20px 10px; + background-color: #eee; +} + +div.adminall { + font-size: 12px; + padding: 10px; +} +div.floatwin { + position:absolute; + top: 150px; + left: 450px; + width:200px; + visibility:hidden; + background: #dcdff4; + padding: 5px; + border: solid black 1px; +} + +div.poll-result-bar { + background-color: #bbb; + color: black; + border: solid 1px black; +} +div.funbanner { + text-align: right; + border-top: #aaa solid 1px; + border-bottom: #aaa solid 1px; + padding-right: 20px; + padding-top: 2px; + padding-bottom: 2px; + font-size: 10px; + background-color: #fff2e8; + font-weight: bold; + margin-top: 10px; +} + +div.replie +{ + display: none; +} diff --git a/gui/bweb/html/unload.png b/gui/bweb/html/unload.png new file mode 100644 index 0000000000..ce290ba8d6 Binary files /dev/null and b/gui/bweb/html/unload.png differ diff --git a/gui/bweb/html/up.gif b/gui/bweb/html/up.gif new file mode 100755 index 0000000000..482498512b Binary files /dev/null and b/gui/bweb/html/up.gif differ diff --git a/gui/bweb/html/update.png b/gui/bweb/html/update.png new file mode 100644 index 0000000000..c7e691b9c7 Binary files /dev/null and b/gui/bweb/html/update.png differ diff --git a/gui/bweb/html/zoom.png b/gui/bweb/html/zoom.png new file mode 100644 index 0000000000..1ac4864d4a Binary files /dev/null and b/gui/bweb/html/zoom.png differ diff --git a/gui/bweb/lib/Bconsole.pm b/gui/bweb/lib/Bconsole.pm new file mode 100644 index 0000000000..9bac7558b6 --- /dev/null +++ b/gui/bweb/lib/Bconsole.pm @@ -0,0 +1,408 @@ +use strict; + +=head1 LICENSE + + Copyright (C) 2006 Eric Bollengier + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +=head1 VERSION + + $Id$ + +=cut + + +################################################################ +# Manage with Expect the bconsole tool +package Bconsole; +use Expect; +use POSIX qw/_exit/; + +# my $pref = new Pref(config_file => 'brestore.conf'); +# my $bconsole = new Bconsole(pref => $pref); +sub new +{ + my ($class, %arg) = @_; + + my $self = bless { + pref => $arg{pref}, # Pref object + bconsole => undef, # Expect object + log_stdout => $arg{log_stdout} || 0, + timeout => $arg{debug} || 10, + debug => $arg{debug} || 0, + }; + + return $self; +} + +sub run +{ + my ($self, %arg) = @_; + + my $cmd = 'run '; + for my $key (keys %arg) { + if ($arg{$key}) { + $arg{$key} =~ tr/""/ /; + $cmd .= "$key=\"$arg{$key}\" "; + } + } + + unless ($self->connect()) { + return 0; + } + + print "=> $cmd yes\n"; + + $self->{bconsole}->clear_accum(); + $self->send("$cmd yes\n"); + $self->expect_it('-re',qr/^[*]/); + my $ret = $self->before(); + if ($ret =~ /jobid=(\d+)/is) { + return $1; + } else { + return 0; + } +} + +sub send +{ + my ($self, $what) = @_; + $self->{bconsole}->send($what); +} + +sub expect_it +{ + my ($self, @what) = @_; + unless ($self->{bconsole}->expect($self->{timeout}, @what)) { + $self->{error} = $!; + return 0; + } + return 1; +} + +sub connect +{ + my ($self) = @_; + + if ($self->{error}) { + return 0 ; + } + + unless ($self->{bconsole}) { + my @cmd = split(/\s+/, $self->{pref}->{bconsole}) ; + unless (@cmd) { + $self->{error} = "bconsole string not found"; + return 0; + } + $self->{bconsole} = new Expect; + $self->{bconsole}->raw_pty(0); + $self->{bconsole}->debug($self->{debug}); + $self->{bconsole}->log_stdout($self->{debug} || $self->{log_stdout}); + + # WARNING : die is trapped by gtk_main_loop() + # and exit() closes DBI connection + my $ret; + { + my $sav = $SIG{__DIE__}; + $SIG{__DIE__} = sub { _exit 1 ;}; + $ret = $self->{bconsole}->spawn(@cmd) ; + $SIG{__DIE__} = $sav; + } + + unless ($ret) { + $self->{error} = $!; + return 0; + } + + # TODO : we must verify that expect return the good value + + $self->expect_it('*'); + $self->send_cmd('gui on'); + } + return 1 ; +} + +sub cancel +{ + my ($self, $jobid) = @_; + return $self->send_cmd("cancel jobid=$jobid"); +} + +# get text between to expect +sub before +{ + my ($self) = @_; + return $self->{bconsole}->before(); +} + +sub send_cmd +{ + my ($self, $cmd) = @_; + unless ($self->connect()) { + return ''; + } + $self->send("$cmd\n"); + $self->expect_it($cmd); + $self->{bconsole}->clear_accum(); + $self->expect_it('-re',qr/^[*]/); + return $self->before(); +} + +sub send_cmd_yes +{ + my ($self, $cmd) = @_; + unless ($self->connect()) { + return ''; + } + $self->send("$cmd\n"); + $self->expect_it('-re', '[?].+:'); + + $self->send("yes\n"); + $self->expect_it("yes"); + $self->{bconsole}->clear_accum(); + $self->expect_it('-re',qr/^[*]/); + return $self->before(); +} + +sub send_cmd_with_drive +{ + my ($self, $cmd, $drive) = @_; + $drive = $drive || '0'; + + unless ($self->connect()) { + return ''; + } + $self->send("$cmd\n"); + $self->expect_it('-re', '\[0\]\s*:'); + + $self->send("$drive\n"); + $self->expect_it('-re', '[0-9]'); + $self->{bconsole}->clear_accum(); + $self->expect_it('-re',qr/^[*]/); + return $self->before(); +} + +sub label_barcodes +{ + my ($self, %arg) = @_; + + unless ($arg{storage}) { + return ''; + } + + unless ($self->connect()) { + return ''; + } + + $arg{drive} = $arg{drive} || '0' ; + $arg{pool} = $arg{pool} || 'Scratch'; + + my $cmd = "label barcodes pool=\"$arg{pool}\" storage=\"$arg{storage}\""; + + if ($arg{slots}) { + $cmd .= " slots=$arg{slots}"; + } + + $self->send("$cmd\n"); + $self->expect_it('-re', '\[0\]\s*:'); + $self->send("$arg{drive}\n"); + $self->expect_it('-re', '[?].+\)\s*:'); + my $res = $self->before(); + $self->send("yes\n"); + $self->expect_it("yes"); + $res .= $self->before(); + $self->expect_it('-re',qr/^[*]/); + $res .= $self->before(); + return $res; +} + +# +# return [ { name => 'test1', vol => '00001', ... }, +# { name => 'test2', vol => '00002', ... }... ] +# +sub director_get_sched +{ + my ($self, $days) = @_ ; + + $days = $days || 1; + + unless ($self->connect()) { + return ''; + } + + my $status = $self->send_cmd("st director days=$days") ; + + my @ret; + foreach my $l (split(/\r?\n/, $status)) { + #Level Type Pri Scheduled Name Volume + #Incremental Backup 11 03-ao-06 23:05 TEST_DATA 000001 + if ($l =~ /^(I|F|Di)\w+\s+\w+\s+\d+/i) { + my ($level, $type, $pri, $d, $h, @name_vol) = split(/\s+/, $l); + + my $vol = pop @name_vol; # last element + my $name = join(" ", @name_vol); # can contains space + + push @ret, { + level => $level, + type => $type, + priority => $pri, + date => "$d $h", + name => $name, + volume => $vol, + }; + } + + } + return \@ret; +} + +sub update_slots +{ + my ($self, $storage, $drive) = @_; + + return $self->send_cmd_with_drive("update slots storage=$storage", $drive); +} + +sub list_job +{ + my ($self) = @_; + return split(/\r\n/, $self->send_cmd(".jobs")); +} + +sub list_fileset +{ + my ($self) = @_; + return split(/\r\n/, $self->send_cmd(".filesets")); +} + +sub list_storage +{ + my ($self) = @_; + return split(/\r\n/, $self->send_cmd(".storage")); +} + +sub list_client +{ + my ($self) = @_; + return split(/\r\n/, $self->send_cmd(".clients")); +} + +use Time::ParseDate qw/parsedate/; +use POSIX qw/strftime/; +use Data::Dumper; + +sub _get_volume +{ + my ($self, @volume) = @_; + return '' unless (@volume); + + my $sel=''; + foreach my $vol (@volume) { + if ($vol =~ /^([\w\d\.-]+)$/) { + $sel .= " volume=$1"; + + } else { + $self->{error} = "Sorry media is bad"; + return ''; + } + } + + return $sel; +} + +sub purge_volume +{ + my ($self, @volume) = @_; + + my $sel = $self->_get_volume(@volume); + my $ret; + if ($sel) { + $ret = $self->send_cmd("purge $sel"); + } else { + $ret = $self->{error}; + } + return $ret; +} + +sub prune_volume +{ + my ($self, @volume) = @_; + + my $sel = $self->_get_volume(@volume); + my $ret; + if ($sel) { + $ret = $self->send_cmd_yes("prune $sel"); + } else { + $ret = $self->{error}; + } + return $ret; +} + +sub purge_job +{ + my ($self, @jobid) = @_; + + return 0 unless (@jobid); + + my $sel=''; + foreach my $job (@jobid) { + if ($job =~ /^(\d+)$/) { + $sel .= " jobid=$1"; + + } else { + $self->{error} = "Sorry jobid is bad"; + return 0; + } + } + + $self->send_cmd("purge $sel"); +} + +sub close +{ + my ($self) = @_; + $self->send("quit\n"); + $self->{bconsole}->soft_close(); + $self->{bconsole} = undef; +} + +1; + +__END__ + +# to use this +# grep -v __END__ Bconsole.pm | perl + +package main; + +print "test sans conio\n"; + +my $c = new Bconsole(pref => { + bconsole => '/usr/local/bacula/sbin/bconsole -c /usr/local/bacula/etc/bconsole.conf', +}, + debug => 1); + +print "fileset : ", join(',', $c->list_fileset()), "\n"; +print "job : ", join(',', $c->list_job()), "\n"; +print "storage : ", join(',', $c->list_storage()), "\n"; +#print "prune : " . $c->prune_volume('000001'), "\n"; +#print "update : " . $c->send_cmd_with_drive('update slots storage=SDLT-1-2'), "\n"; +print "label : ", join(',', $c->label_barcodes(storage => 'SDLT-1-2', + slots => 6, + drive => 0)), "\n"; + + diff --git a/gui/bweb/lib/Bweb.pm b/gui/bweb/lib/Bweb.pm new file mode 100644 index 0000000000..7badb6a637 --- /dev/null +++ b/gui/bweb/lib/Bweb.pm @@ -0,0 +1,2775 @@ +################################################################ +use strict; + +=head1 LICENSE + + Copyright (C) 2006 Eric Bollengier + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +=head1 VERSION + + $Id$ + +=cut + +package Bweb::Gui; + +=head1 PACKAGE + + Bweb::Gui - Base package for all Bweb object + +=head2 DESCRIPTION + + This package define base fonction like new, display, etc.. + +=cut + +use HTML::Template; +our $template_dir='/usr/share/bweb/tpl'; + + +=head1 FUNCTION + + new - creation a of new Bweb object + +=head2 DESCRIPTION + + This function take an hash of argument and place them + on bless ref + + IE : $obj = new Obj(name => 'test', age => '10'); + + $obj->{name} eq 'test' and $obj->{age} eq 10 + +=cut + +sub new +{ + my ($class, %arg) = @_; + my $self = bless { + name => undef, + }, $class; + + map { $self->{lc($_)} = $arg{$_} } keys %arg ; + + return $self; +} + +sub debug +{ + my ($self, $what) = @_; + + if ($self->{debug}) { + if (ref $what) { + print "
" . Data::Dumper::Dumper($what) . "
"; + } else { + print "
$what
"; + } + } +} + +=head1 FUNCTION + + error - display an error to the user + +=head2 DESCRIPTION + + this function set $self->{error} with arg, display a message with + error.tpl and return 0 + +=head2 EXAMPLE + + unless (...) { + return $self->error("Can't use this file"); + } + +=cut + +sub error +{ + my ($self, $what) = @_; + $self->{error} = $what; + $self->display($self, 'error.tpl'); + return 0; +} + +=head1 FUNCTION + + display - display an html page with HTML::Template + +=head2 DESCRIPTION + + this function is use to render all html codes. it takes an + ref hash as arg in which all param are usable in template. + + it will use global template_dir to search the template file. + + hash keys are not sensitive. See HTML::Template for more + explanations about the hash ref. (it's can be quiet hard to understand) + +=head2 EXAMPLE + + $ref = { name => 'me', age => 26 }; + $self->display($ref, "people.tpl"); + +=cut + +sub display +{ + my ($self, $hash, $tpl) = @_ ; + + my $template = HTML::Template->new(filename => $tpl, + path =>[$template_dir], + die_on_bad_params => 0, + case_sensitive => 0); + + foreach my $var (qw/limit offset/) { + + unless ($hash->{$var}) { + my $value = CGI::param($var) || ''; + + if ($value =~ /^(\d+)$/) { + $template->param($var, $1) ; + } + } + } + + $template->param('thisurl', CGI::url(-relative => 1, -query=>1)); + $template->param('loginname', CGI::remote_user()); + + $template->param($hash); + print $template->output(); +} +1; + +################################################################ + +package Bweb::Config; + +use base q/Bweb::Gui/; + +=head1 PACKAGE + + Bweb::Config - read, write, display, modify configuration + +=head2 DESCRIPTION + + this package is used for manage configuration + +=head2 USAGE + + $conf = new Bweb::Config(config_file => '/path/to/conf'); + $conf->load(); + + $conf->edit(); + + $conf->save(); + +=cut + +use CGI; + +=head1 PACKAGE VARIABLE + + %k_re - hash of all acceptable option. + +=head2 DESCRIPTION + + this variable permit to check all option with a regexp. + +=cut + +our %k_re = ( dbi => qr/^(dbi:(Pg|mysql):(?:\w+=[\w\d\.-]+;?)+)$/i, + user => qr/^([\w\d\.-]+)$/i, + password => qr/^(.*)$/i, + template_dir => qr!^([/\w\d\.-]+)$!, + debug => qr/^(on)?$/, + email_media => qr/^([\w\d\.-]+@[\d\w\.-]+)$/, + graph_font => qr!^([/\w\d\.-]+.ttf)$!, + bconsole => qr!^(.+)?$!, + syslog_file => qr!^(.+)?$!, + log_dir => qr!^(.+)?$!, + ); + +=head1 FUNCTION + + load - load config_file + +=head2 DESCRIPTION + + this function load the specified config_file. + +=cut + +sub load +{ + my ($self) = @_ ; + + unless (open(FP, $self->{config_file})) + { + return $self->error("$self->{config_file} : $!"); + } + + while (my $line = ) + { + chomp($line); + my ($k, $v) = split(/\s*=\s*/, $line, 2); + $self->{$k} = $v; + } + + close(FP); + return 1; +} + +=head1 FUNCTION + + save - save the current configuration to config_file + +=cut + +sub save +{ + my ($self) = @_ ; + + unless (open(FP, ">$self->{config_file}")) + { + return $self->error("$self->{config_file} : $!"); + } + + foreach my $k (keys %$self) + { + next unless (exists $k_re{$k}) ; + print FP "$k = $self->{$k}\n"; + } + + close(FP); + return 1; +} + +=head1 FUNCTIONS + + edit, view, modify - html form ouput + +=cut + +sub edit +{ + my ($self) = @_ ; + + $self->display($self, "config_edit.tpl"); +} + +sub view +{ + my ($self) = @_ ; + + $self->display($self, "config_view.tpl"); +} + +sub modify +{ + my ($self) = @_; + + $self->{error} = ''; + $self->{debug} = 0; + + foreach my $k (CGI::param()) + { + next unless (exists $k_re{$k}) ; + my $val = CGI::param($k); + if ($val =~ $k_re{$k}) { + $self->{$k} = $1; + } else { + $self->{error} .= "bad parameter : $k = [$val]"; + } + } + + $self->display($self, "config_view.tpl"); + + if ($self->{error}) { # an error as occured + $self->display($self, 'error.tpl'); + } else { + $self->save(); + } +} + +1; + +################################################################ + +package Bweb::Client; + +use base q/Bweb::Gui/; + +=head1 PACKAGE + + Bweb::Client - Bacula FD + +=head2 DESCRIPTION + + this package is use to do all Client operations like, parse status etc... + +=head2 USAGE + + $client = new Bweb::Client(name => 'zog-fd'); + $client->status(); # do a 'status client=zog-fd' + +=cut + +=head1 FUNCTION + + display_running_job - Html display of a running job + +=head2 DESCRIPTION + + this function is used to display information about a current job + +=cut + +sub display_running_job +{ + my ($self, $conf, $jobid) = @_ ; + + my $status = $self->status($conf); + + if ($jobid) { + if ($status->{$jobid}) { + $self->display($status->{$jobid}, "client_job_status.tpl"); + } + } else { + for my $id (keys %$status) { + $self->display($status->{$id}, "client_job_status.tpl"); + } + } +} + +=head1 FUNCTION + + $client = new Bweb::Client(name => 'plume-fd'); + + $client->status($bweb); + +=head2 DESCRIPTION + + dirty hack to parse "status client=xxx-fd" + +=head2 INPUT + + JobId 105 Job Full_plume.2006-06-06_17.22.23 is running. + Backup Job started: 06-jun-06 17:22 + Files=8,971 Bytes=194,484,132 Bytes/sec=7,480,158 + Files Examined=10,697 + Processing file: /home/eric/.openoffice.org2/user/config/standard.sod + SDReadSeqNo=5 fd=5 + +=head2 OUTPUT + + $VAR1 = { 105 => { + JobName => Full_plume.2006-06-06_17.22.23, + JobId => 105, + Files => 8,971, + Bytes => 194,484,132, + ... + }, + ... + }; + +=cut + +sub status +{ + my ($self, $conf) = @_ ; + + if (defined $self->{cur_jobs}) { + return $self->{cur_jobs} ; + } + + my $arg = {}; + my $b = new Bconsole(pref => $conf); + my $ret = $b->send_cmd("st client=$self->{name}"); + my @param; + my $jobid; + + for my $r (split(/\n/, $ret)) { + chomp($r); + $r =~ s/(^\s+|\s+$)//g; + if ($r =~ /JobId (\d+) Job (\S+)/) { + if ($jobid) { + $arg->{$jobid} = { @param, JobId => $jobid } ; + } + + $jobid = $1; + @param = ( JobName => $2 ); + + } elsif ($r =~ /=.+=/) { + push @param, split(/\s+|\s*=\s*/, $r) ; + + } elsif ($r =~ /=/) { # one per line + push @param, split(/\s*=\s*/, $r) ; + + } elsif ($r =~ /:/) { # one per line + push @param, split(/\s*:\s*/, $r, 2) ; + } + } + + if ($jobid and @param) { + $arg->{$jobid} = { @param, + JobId => $jobid, + Client => $self->{name}, + } ; + } + + $self->{cur_jobs} = $arg ; + + return $arg; +} +1; + +################################################################ + +package Bweb::Autochanger; + +use base q/Bweb::Gui/; + +=head1 PACKAGE + + Bweb::Autochanger - Object to manage Autochanger + +=head2 DESCRIPTION + + this package will parse the mtx output and manage drives. + +=head2 USAGE + + $auto = new Bweb::Autochanger(precmd => 'sudo'); + or + $auto = new Bweb::Autochanger(precmd => 'ssh root@robot'); + + $auto->status(); + + $auto->slot_is_full(10); + $auto->transfer(10, 11); + +=cut + +# TODO : get autochanger definition from config/dump file +my %ach_list ; + +sub get +{ + my ($name, $bweb) = @_; + my $a = new Bweb::Autochanger(debug => $bweb->{debug}, + bweb => $bweb, + name => 'SDLT-1-2', + precmd => 'sudo', + drive_name => ['SDLT-1', 'SDLT-2'], + ); + return $a; +} + +sub new +{ + my ($class, %arg) = @_; + + my $self = bless { + name => '', # autochanger name + label => {}, # where are volume { label1 => 40, label2 => drive0 } + drive => [], # drive use [ 'media1', 'empty', ..] + slot => [], # slot use [ undef, 'empty', 'empty', ..] no slot 0 + io => [], # io slot number list [ 41, 42, 43...] + info => {slot => 0, # informations (slot, drive, io) + io => 0, + drive=> 0, + }, + mtxcmd => '/usr/sbin/mtx', + debug => 0, + device => '/dev/changer', + precmd => '', # ssh command + bweb => undef, # link to bacula web object (use for display) + } ; + + map { $self->{lc($_)} = $arg{$_} } keys %arg ; + + return $self; +} + +=head1 FUNCTION + + status - parse the output of mtx status + +=head2 DESCRIPTION + + this function will launch mtx status and parse the output. it will + give a perlish view of the autochanger content. + + it uses ssh if the autochanger is on a other host. + +=cut + +sub status +{ + my ($self) = @_; + my @out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} status` ; + + # TODO : reset all infos + $self->{info}->{drive} = 0; + $self->{info}->{slot} = 0; + $self->{info}->{io} = 0; + + #my @out = `cat /home/eric/travail/brestore/plume/mtx` ; + +# +# Storage Changer /dev/changer:2 Drives, 45 Slots ( 5 Import/Export ) +#Data Transfer Element 0:Full (Storage Element 1 Loaded):VolumeTag = 000000 +#Data Transfer Element 1:Empty +# Storage Element 1:Empty +# Storage Element 2:Full :VolumeTag=000002 +# Storage Element 3:Empty +# Storage Element 4:Full :VolumeTag=000004 +# Storage Element 5:Full :VolumeTag=000001 +# Storage Element 6:Full :VolumeTag=000003 +# Storage Element 7:Empty +# Storage Element 41 IMPORT/EXPORT:Empty +# Storage Element 41 IMPORT/EXPORT:Full :VolumeTag=000002 +# + + for my $l (@out) { + + # Storage Element 7:Empty + # Storage Element 2:Full :VolumeTag=000002 + if ($l =~ /Storage Element (\d+):(Empty|Full)(\s+:VolumeTag=([\w\d]+))?/){ + + if ($2 eq 'Empty') { + $self->set_empty_slot($1); + } else { + $self->set_slot($1, $4); + } + + } elsif ($l =~ /Data Transfer.+(\d+):(Full|Empty)(\s+.Storage Element (\d+) Loaded.(:VolumeTag = ([\w\d]+))?)?/) { + + if ($2 eq 'Empty') { + $self->set_empty_drive($1); + } else { + $self->set_drive($1, $4, $6); + } + + } elsif ($l =~ /Storage Element (\d+).+IMPORT\/EXPORT:(Empty|Full)( :VolumeTag=([\d\w]+))?/) + { + if ($2 eq 'Empty') { + $self->set_empty_io($1); + } else { + $self->set_io($1, $4); + } + +# Storage Changer /dev/changer:2 Drives, 30 Slots ( 1 Import/Export ) + + } elsif ($l =~ /Storage Changer .+:(\d+) Drives, (\d+) Slots/) { + $self->{info}->{drive} = $1; + $self->{info}->{slot} = $2; + if ($l =~ /(\d+)\s+Import/) { + $self->{info}->{io} = $1 ; + } else { + $self->{info}->{io} = 0; + } + } + } + + $self->debug($self) ; +} + +sub is_slot_loaded +{ + my ($self, $slot) = @_; + + # no barcodes + if ($self->{slot}->[$slot] eq 'loaded') { + return 1; + } + + my $label = $self->{slot}->[$slot] ; + + return $self->is_media_loaded($label); +} + +sub unload +{ + my ($self, $drive, $slot) = @_; + + return 0 if (not defined $drive or $self->{drive}->[$drive] eq 'empty') ; + return 0 if ($self->slot_is_full($slot)) ; + + my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} unload $slot $drive 2>&1`; + + if ($? == 0) { + my $content = $self->get_slot($slot); + print "content = $content
$drive => $slot
"; + $self->set_empty_drive($drive); + $self->set_slot($slot, $content); + return 1; + } else { + $self->{error} = $out; + return 0; + } +} + +# TODO: load/unload have to use mtx script from bacula +sub load +{ + my ($self, $drive, $slot) = @_; + + return 0 if (not defined $drive or $self->{drive}->[$drive] ne 'empty') ; + return 0 unless ($self->slot_is_full($slot)) ; + + print "Loading drive $drive with slot $slot
\n"; + my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} load $slot $drive 2>&1`; + + if ($? == 0) { + my $content = $self->get_slot($slot); + print "content = $content
$slot => $drive
"; + $self->set_drive($drive, $slot, $content); + return 1; + } else { + $self->{error} = $out; + print $out; + return 0; + } +} + +sub is_media_loaded +{ + my ($self, $media) = @_; + + unless ($self->{label}->{$media}) { + return 0; + } + + if ($self->{label}->{$media} =~ /drive\d+/) { + return 1; + } + + return 0; +} + +sub have_io +{ + my ($self) = @_; + return (defined $self->{info}->{io} and $self->{info}->{io} > 0); +} + +sub set_io +{ + my ($self, $slot, $tag) = @_; + $self->{slot}->[$slot] = $tag || 'full'; + push @{ $self->{io} }, $slot; + + if ($tag) { + $self->{label}->{$tag} = $slot; + } +} + +sub set_empty_io +{ + my ($self, $slot) = @_; + + push @{ $self->{io} }, $slot; + + unless ($self->{slot}->[$slot]) { # can be loaded (parse before) + $self->{slot}->[$slot] = 'empty'; + } +} + +sub get_slot +{ + my ($self, $slot) = @_; + return $self->{slot}->[$slot]; +} + +sub set_slot +{ + my ($self, $slot, $tag) = @_; + $self->{slot}->[$slot] = $tag || 'full'; + + if ($tag) { + $self->{label}->{$tag} = $slot; + } +} + +sub set_empty_slot +{ + my ($self, $slot) = @_; + + unless ($self->{slot}->[$slot]) { # can be loaded (parse before) + $self->{slot}->[$slot] = 'empty'; + } +} + +sub set_empty_drive +{ + my ($self, $drive) = @_; + $self->{drive}->[$drive] = 'empty'; +} + +sub set_drive +{ + my ($self, $drive, $slot, $tag) = @_; + $self->{drive}->[$drive] = $tag || $slot; + + $self->{slot}->[$slot] = $tag || 'loaded'; + + if ($tag) { + $self->{label}->{$tag} = "drive$drive"; + } +} + +sub slot_is_full +{ + my ($self, $slot) = @_; + + # slot don't exists => full + if (not defined $self->{slot}->[$slot]) { + return 0 ; + } + + if ($self->{slot}->[$slot] eq 'empty') { + return 0; + } + return 1; # vol, full, loaded +} + +sub slot_get_first_free +{ + my ($self) = @_; + for (my $slot=1; $slot < $self->{info}->{slot}; $slot++) { + return $slot unless ($self->slot_is_full($slot)); + } +} + +sub io_get_first_free +{ + my ($self) = @_; + + foreach my $slot (@{ $self->{io} }) { + return $slot unless ($self->slot_is_full($slot)); + } + return 0; +} + +sub get_media_slot +{ + my ($self, $media) = @_; + + return $self->{label}->{$media} ; +} + +sub have_media +{ + my ($self, $media) = @_; + + return defined $self->{label}->{$media} ; +} + +sub send_to_io +{ + my ($self, $slot) = @_; + + unless ($self->slot_is_full($slot)) { + print "Autochanger $self->{name} slot $slot is empty\n"; + return 1; # ok + } + + # first, eject it + if ($self->is_slot_loaded($slot)) { + # bconsole->umount + # self->eject + print "Autochanger $self->{name} $slot is currently in use\n"; + return 0; + } + + # autochanger must have I/O + unless ($self->have_io()) { + print "Autochanger $self->{name} don't have I/O, you can take media yourself\n"; + return 0; + } + + my $dst = $self->io_get_first_free(); + + unless ($dst) { + print "Autochanger $self->{name} you must empty I/O first\n"; + } + + $self->transfer($slot, $dst); +} + +sub transfer +{ + my ($self, $src, $dst) = @_ ; + print "$self->{precmd} $self->{mtxcmd} -f $self->{device} transfer $src $dst\n"; + my $out = `$self->{precmd} $self->{mtxcmd} -f $self->{device} transfer $src $dst 2>&1`; + + if ($? == 0) { + my $content = $self->get_slot($src); + print "content = $content
$src => $dst
"; + $self->{slot}->[$src] = 'empty'; + $self->set_slot($dst, $content); + return 1; + } else { + $self->{error} = $out; + return 0; + } +} + +# TODO : do a tapeinfo request to get informations +sub tapeinfo +{ + my ($self) = @_; +} + +sub clear_io +{ + my ($self) = @_; + + for my $slot (@{$self->{io}}) + { + if ($self->is_slot_loaded($slot)) { + print "$slot is currently loaded\n"; + next; + } + + if ($self->slot_is_full($slot)) + { + my $free = $self->slot_get_first_free() ; + print "want to move $slot to $free\n"; + + if ($free) { + $self->transfer($slot, $free) || print "$self->{error}\n"; + + } else { + $self->{error} = "E : Can't find free slot"; + } + } + } +} + +# TODO : this is with mtx status output, +# we can do an other function from bacula view (with StorageId) +sub display_content +{ + my ($self) = @_; + my $bweb = $self->{bweb}; + + # $self->{label} => ('vol1', 'vol2', 'vol3', ..); + my $media_list = $bweb->dbh_join( keys %{ $self->{label} }); + + my $query=" +SELECT Media.VolumeName AS volumename, + Media.VolStatus AS volstatus, + Media.LastWritten AS lastwritten, + Media.VolBytes AS volbytes, + Media.MediaType AS mediatype, + Media.Slot AS slot, + Media.InChanger AS inchanger, + Pool.Name AS name, + $bweb->{sql}->{FROM_UNIXTIME}( + $bweb->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten) + + $bweb->{sql}->{TO_SEC}(Media.VolRetention) + ) AS expire +FROM Media + INNER JOIN Pool USING (PoolId) + +WHERE Media.VolumeName IN ($media_list) +"; + + my $all = $bweb->dbh_selectall_hashref($query, 'volumename') ; + + # TODO : verify slot and bacula slot + my $param = []; + my @to_update; + + for (my $slot=1; $slot <= $self->{info}->{slot} ; $slot++) { + + if ($self->slot_is_full($slot)) { + + my $vol = $self->{slot}->[$slot]; + if (defined $all->{$vol}) { # TODO : autochanger without barcodes + + my $bslot = $all->{$vol}->{slot} ; + my $inchanger = $all->{$vol}->{inchanger}; + + # if bacula slot or inchanger flag is bad, we display a message + if ($bslot != $slot or !$inchanger) { + push @to_update, $slot; + } + + $all->{$vol}->{realslot} = $slot; + $all->{$vol}->{volbytes} = Bweb::human_size($all->{$vol}->{volbytes}) ; + + push @{ $param }, $all->{$vol}; + + } else { # empty or no label + push @{ $param }, {realslot => $slot, + volstatus => 'Unknow', + volumename => $self->{slot}->[$slot]} ; + } + } else { # empty + push @{ $param }, {realslot => $slot, volumename => 'empty'} ; + } + } + + my $i=0; my $drives = [] ; + foreach my $d (@{ $self->{drive} }) { + $drives->[$i] = { index => $i, + load => $self->{drive}->[$i], + name => $self->{drive_name}->[$i], + }; + $i++; + } + + $bweb->display({ Name => $self->{name}, + nb_drive => $self->{info}->{drive}, + nb_io => $self->{info}->{io}, + Drives => $drives, + Slots => $param, + Update => scalar(@to_update) }, + 'ach_content.tpl'); + +} + +1; + + +################################################################ + +package Bweb; + +use base q/Bweb::Gui/; + +=head1 PACKAGE + + Bweb - main Bweb package + +=head2 + + this package is use to compute and display informations + +=cut + +use DBI; +use POSIX qw/strftime/; + +our $bpath="/usr/local/bacula"; +our $bconsole="$bpath/sbin/bconsole -c $bpath/etc/bconsole.conf"; + +our $cur_id=0; + +=head1 VARIABLE + + %sql_func - hash to make query mysql/postgresql compliant + +=cut + +our %sql_func = ( + Pg => { + UNIX_TIMESTAMP => '', + FROM_UNIXTIME => '', + TO_SEC => " interval '1 second' * ", + SEC_TO_INT => "SEC_TO_INT", + SEC_TO_TIME => '', + }, + mysql => { + UNIX_TIMESTAMP => 'UNIX_TIMESTAMP', + FROM_UNIXTIME => 'FROM_UNIXTIME', + SEC_TO_INT => '', + TO_SEC => '', + SEC_TO_TIME => 'SEC_TO_TIME', + }, + ); + +sub dbh_selectall_arrayref +{ + my ($self, $query) = @_; + $self->connect_db(); + $self->debug($query); + return $self->{dbh}->selectall_arrayref($query); +} + +sub dbh_join +{ + my ($self, @what) = @_; + return join(',', $self->dbh_quote(@what)) ; +} + +sub dbh_quote +{ + my ($self, @what) = @_; + + $self->connect_db(); + if (wantarray) { + return map { $self->{dbh}->quote($_) } @what; + } else { + return $self->{dbh}->quote($what[0]) ; + } +} + +sub dbh_do +{ + my ($self, $query) = @_ ; + $self->connect_db(); + $self->debug($query); + return $self->{dbh}->do($query); +} + +sub dbh_selectall_hashref +{ + my ($self, $query, $join) = @_; + + $self->connect_db(); + $self->debug($query); + return $self->{dbh}->selectall_hashref($query, $join) ; +} + +sub dbh_selectrow_hashref +{ + my ($self, $query) = @_; + + $self->connect_db(); + $self->debug($query); + return $self->{dbh}->selectrow_hashref($query) ; +} + +# display Mb/Gb/Kb +sub human_size +{ + my @unit = qw(b Kb Mb Gb Tb); + my $val = shift || 0; + my $i=0; + my $format = '%i %s'; + while ($val / 1024 > 1) { + $i++; + $val /= 1024; + } + $format = ($i>0)?'%0.1f %s':'%i %s'; + return sprintf($format, $val, $unit[$i]); +} + +# display Day, Hour, Year +sub human_sec +{ + use integer; + + my $val = shift; + $val /= 60; # sec -> min + + if ($val / 60 <= 1) { + return "$val mins"; + } + + $val /= 60; # min -> hour + if ($val / 24 <= 1) { + return "$val hours"; + } + + $val /= 24; # hour -> day + if ($val / 365 < 2) { + return "$val days"; + } + + $val /= 365 ; # day -> year + + return "$val years"; +} + +# get Day, Hour, Year +sub from_human_sec +{ + use integer; + + my $val = shift; + unless ($val =~ /^\s*(\d+)\s*(\w)\w*\s*$/) { + return 0; + } + + my %times = ( m => 60, + h => 60*60, + d => 60*60*24, + m => 60*60*24*31, + y => 60*60*24*365, + ); + my $mult = $times{$2} || 0; + + return $1 * $mult; +} + + +sub connect_db +{ + my ($self) = @_; + + unless ($self->{dbh}) { + $self->{dbh} = DBI->connect($self->{info}->{dbi}, + $self->{info}->{user}, + $self->{info}->{password}); + + print "Can't connect to your database, see error log\n" + unless ($self->{dbh}); + + $self->{dbh}->{FetchHashKeyName} = 'NAME_lc'; + } +} + +sub new +{ + my ($class, %arg) = @_; + my $self = bless { + dbh => undef, # connect_db(); + info => { + dbi => 'DBI:Pg:database=bacula;host=127.0.0.1', + user => 'bacula', + password => 'test', + }, + } ; + + map { $self->{lc($_)} = $arg{$_} } keys %arg ; + + if ($self->{info}->{dbi} =~ /DBI:(\w+):/i) { + $self->{sql} = $sql_func{$1}; + } + + $self->{debug} = $self->{info}->{debug}; + $Bweb::Gui::template_dir = $self->{info}->{template_dir}; + + return $self; +} + +sub display_begin +{ + my ($self) = @_; + $self->display($self->{info}, "begin.tpl"); +} + +sub display_end +{ + my ($self) = @_; + $self->display($self->{info}, "end.tpl"); +} + +sub display_clients +{ + my ($self) = @_; + + my $query = " +SELECT Name AS name, + Uname AS uname, + AutoPrune AS autoprune, + FileRetention AS fileretention, + JobRetention AS jobretention + +FROM Client +"; + + my $all = $self->dbh_selectall_hashref($query, 'name') ; + + foreach (values %$all) { + $_->{fileretention} = human_sec($_->{fileretention}); + $_->{jobretention} = human_sec($_->{jobretention}); + } + + my $arg = { ID => $cur_id++, + clients => [ values %$all] }; + + $self->display($arg, "client_list.tpl") ; +} + +sub get_limit +{ + my ($self, %arg) = @_; + + my $limit = ''; + my $label = ''; + + if ($arg{age}) { + $limit = + "AND $self->{sql}->{UNIX_TIMESTAMP}(EndTime) + > + ( $self->{sql}->{UNIX_TIMESTAMP}(NOW()) + - + $self->{sql}->{TO_SEC}($arg{age}) + )" ; + + $label = "last " . human_sec($arg{age}); + } + + if ($arg{order}) { + $limit .= " ORDER BY $arg{order} "; + } + + if ($arg{limit}) { + $limit .= " LIMIT $arg{limit} "; + $label .= " limited to $arg{limit}"; + } + + if ($arg{offset}) { + $limit .= " OFFSET $arg{offset} "; + $label .= " with $arg{offset} offset "; + } + + unless ($label) { + $label = 'no filter'; + } + + return ($limit, $label); +} + +=head1 FUNCTION + + $bweb->get_form(...) - Get useful stuff + +=head2 DESCRIPTION + + This function get and check parameters against regexp. + + If word begin with 'q', the return will be quoted or join quoted + if it's end with 's'. + + +=head2 EXAMPLE + + $bweb->get_form('jobid', 'qclient', 'qpools') ; + + { jobid => 12, + qclient => 'plume-fd', + qpools => "'plume-fd', 'test-fd', '...'", + } + +=cut + +sub get_form +{ + my ($self, @what) = @_; + my %what = map { $_ => 1 } @what; + my %ret; + + my %opt_i = ( + limit => 100, + cost => 10, + offset => 0, + width => 640, + height => 480, + jobid => 0, + slot => 0, + drive => undef, + priority => 10, + age => 60*60*24*7, + days => 1, + ); + + my %opt_s = ( # default to '' + ach => 1, + status => 1, + client => 1, + level => 1, + pool => 1, + media => 1, + ach => 1, + jobtype=> 1, + ); + + foreach my $i (@what) { + if (exists $opt_i{$i}) {# integer param + my $value = CGI::param($i) || $opt_i{$i} ; + if ($value =~ /^(\d+)$/) { + $ret{$i} = $1; + } + } elsif ($opt_s{$i}) { # simple string param + my $value = CGI::param($i) || ''; + if ($value =~ /^([\w\d\.-]+)$/) { + $ret{$i} = $1; + } + } elsif ($i =~ /^j(\w+)s$/) { # quote join args + my @value = CGI::param($1) ; + if (@value) { + $ret{$i} = $self->dbh_join(@value) ; + } + + } elsif ($i =~ /^q(\w+[^s])$/) { # 'arg1' + my $value = CGI::param($1) ; + if ($value) { + $ret{$i} = $self->dbh_quote($value); + } + + } elsif ($i =~ /^q(\w+)s$/) { #[ 'arg1', 'arg2'] + $ret{$i} = [ map { { name => $self->dbh_quote($_) } } + CGI::param($1) ]; + } + } + + if ($what{slots}) { + foreach my $s (CGI::param('slot')) { + if ($s =~ /^(\d+)$/) { + push @{$ret{slots}}, $s; + } + } + } + + if ($what{db_clients}) { + my $query = " +SELECT Client.Name as clientname +FROM Client +"; + + my $clients = $self->dbh_selectall_hashref($query, 'clientname'); + $ret{db_clients} = [sort {$a->{clientname} cmp $b->{clientname} } + values %$clients] ; + } + + if ($what{db_mediatypes}) { + my $query = " +SELECT MediaType as mediatype +FROM MediaType +"; + + my $medias = $self->dbh_selectall_hashref($query, 'mediatype'); + $ret{db_mediatypes} = [sort {$a->{mediatype} cmp $b->{mediatype} } + values %$medias] ; + } + + if ($what{db_locations}) { + my $query = " +SELECT Location as location, Cost as cost FROM Location +"; + my $loc = $self->dbh_selectall_hashref($query, 'location'); + $ret{db_locations} = [ sort { $a->{location} + cmp + $b->{location} + } values %$loc ]; + } + + if ($what{db_pools}) { + my $query = "SELECT Name as name FROM Pool"; + + my $all = $self->dbh_selectall_hashref($query, 'name') ; + $ret{db_pools} = [ sort { $a->{name} cmp $b->{name} } values %$all ]; + } + + if ($what{db_filesets}) { + my $query = " +SELECT FileSet.FileSet AS fileset +FROM FileSet +"; + + my $filesets = $self->dbh_selectall_hashref($query, 'fileset'); + + $ret{db_filesets} = [sort {lc($a->{fileset}) cmp lc($b->{fileset}) } + values %$filesets] ; + + } + + return \%ret; +} + +sub display_graph +{ + my ($self) = @_; + + my $fields = $self->get_form(qw/age level status clients filesets + db_clients limit db_filesets width height + qclients qfilesets/); + + + my $url = CGI::url(-full => 0, + -base => 0, + -query => 1); + $url =~ s/^.+?\?//; # http://path/to/bweb.pl?arg => arg + + my $type = CGI::param('graph') || ''; + if ($type =~ /^(\w+)$/) { + $fields->{graph} = $1; + } + + my $gtype = CGI::param('gtype') || ''; + if ($gtype =~ /^(\w+)$/) { + $fields->{gtype} = $1; + } + +# this organisation is to keep user choice between 2 click +# TODO : fileset and client selection doesn't work + + $self->display({ + url => $url, + %$fields, + }, "graph.tpl") + +} + +sub display_client_job +{ + my ($self, %arg) = @_ ; + + $arg{order} = ' Job.JobId DESC '; + my ($limit, $label) = $self->get_limit(%arg); + + my $clientname = $self->dbh_quote($arg{clientname}); + + my $query=" +SELECT DISTINCT Job.JobId AS jobid, + Job.Name AS jobname, + FileSet.FileSet AS fileset, + Level AS level, + StartTime AS starttime, + JobFiles AS jobfiles, + JobBytes AS jobbytes, + JobStatus AS jobstatus, + JobErrors AS joberrors + + FROM Client,Job,FileSet + WHERE Client.Name=$clientname + AND Client.ClientId=Job.ClientId + AND Job.FileSetId=FileSet.FileSetId + $limit +"; + + my $all = $self->dbh_selectall_hashref($query, 'jobid') ; + + foreach (values %$all) { + $_->{jobbytes} = human_size($_->{jobbytes}) ; + } + + $self->display({ clientname => $arg{clientname}, + Filter => $label, + ID => $cur_id++, + Jobs => [ values %$all ], + }, + "display_client_job.tpl") ; +} + +sub get_selected_media_location +{ + my ($self) = @_ ; + + my $medias = $self->get_form('jmedias'); + + unless ($medias->{jmedias}) { + return undef; + } + + my $query = " +SELECT Media.VolumeName AS volumename, Location.Location AS location +FROM Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId) +WHERE Media.VolumeName IN ($medias->{jmedias}) +"; + + my $all = $self->dbh_selectall_hashref($query, 'volumename') ; + + # { 'vol1' => { [volumename => 'vol1', location => 'ici'], + # .. + # } + # } + return $all; +} + +sub move_media +{ + my ($self) = @_ ; + + my $medias = $self->get_selected_media_location(); + + unless ($medias) { + return ; + } + + my $elt = $self->get_form('db_locations'); + + $self->display({ ID => $cur_id++, + %$elt, # db_locations + medias => [ + sort { $a->{volumename} cmp $b->{volumename} } values %$medias + ], + }, + "move_media.tpl"); +} + +sub help_extern +{ + my ($self) = @_ ; + + my $elt = $self->get_form(qw/db_pools db_mediatypes db_locations/) ; + $self->debug($elt); + $self->display($elt, "help_extern.tpl"); +} + +sub help_extern_compute +{ + my ($self) = @_; + + my $number = CGI::param('limit') || '' ; + unless ($number =~ /^(\d+)$/) { + return $self->error("Bad arg number : $number "); + } + + my ($sql, undef) = $self->get_param('pools', + 'locations', 'mediatypes'); + + my $query = " +SELECT Media.VolumeName AS volumename, + Media.VolStatus AS volstatus, + Media.LastWritten AS lastwritten, + Media.MediaType AS mediatype, + Media.VolMounts AS volmounts, + Pool.Name AS name, + Media.Recycle AS recycle, + $self->{sql}->{FROM_UNIXTIME}( + $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten) + + $self->{sql}->{TO_SEC}(Media.VolRetention) + ) AS expire +FROM Media + INNER JOIN Pool ON (Pool.PoolId = Media.PoolId) + LEFT JOIN Location ON (Media.LocationId = Location.LocationId) + +WHERE Media.InChanger = 1 + AND Media.VolStatus IN ('Disabled', 'Error', 'Full') + $sql +ORDER BY expire DESC, recycle, Media.VolMounts DESC +LIMIT $number +" ; + + my $all = $self->dbh_selectall_hashref($query, 'volumename') ; + + $self->display({ Medias => [ values %$all ] }, + "help_extern_compute.tpl"); +} + +sub help_intern +{ + my ($self) = @_ ; + + my $param = $self->get_form(qw/db_locations db_pools db_mediatypes/) ; + $self->display($param, "help_intern.tpl"); +} + +sub help_intern_compute +{ + my ($self) = @_; + + my $number = CGI::param('limit') || '' ; + unless ($number =~ /^(\d+)$/) { + return $self->error("Bad arg number : $number "); + } + + my ($sql, undef) = $self->get_param('pools', 'locations', 'mediatypes'); + + if (CGI::param('expired')) { + $sql = " +AND ( $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten) + + $self->{sql}->{TO_SEC}(Media.VolRetention) + ) < NOW() + " . $sql ; + } + + my $query = " +SELECT Media.VolumeName AS volumename, + Media.VolStatus AS volstatus, + Media.LastWritten AS lastwritten, + Media.MediaType AS mediatype, + Media.VolMounts AS volmounts, + Pool.Name AS name, + $self->{sql}->{FROM_UNIXTIME}( + $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten) + + $self->{sql}->{TO_SEC}(Media.VolRetention) + ) AS expire +FROM Media + INNER JOIN Pool ON (Pool.PoolId = Media.PoolId) + LEFT JOIN Location ON (Location.LocationId = Media.LocationId) + +WHERE Media.InChanger <> 1 + AND Media.VolStatus IN ('Purged', 'Full', 'Append') + AND Media.Recycle = 1 + $sql +ORDER BY Media.VolUseDuration DESC, Media.VolMounts ASC, expire ASC +LIMIT $number +" ; + + my $all = $self->dbh_selectall_hashref($query, 'volumename') ; + + $self->display({ Medias => [ values %$all ] }, + "help_intern_compute.tpl"); + +} + +sub display_general +{ + my ($self, %arg) = @_ ; + + my ($limit, $label) = $self->get_limit(%arg); + + my $query = " +SELECT + (SELECT count(Pool.PoolId) FROM Pool) AS nb_pool, + (SELECT count(Media.MediaId) FROM Media) AS nb_media, + (SELECT count(Job.JobId) FROM Job) AS nb_job, + (SELECT sum(VolBytes) FROM Media) AS nb_bytes, + (SELECT count(Job.JobId) + FROM Job + WHERE Job.JobStatus IN ('E','e','f','A') + $limit + ) AS nb_err, + (SELECT count(Client.ClientId) FROM Client) AS nb_client +"; + + my $row = $self->dbh_selectrow_hashref($query) ; + + $row->{nb_bytes} = human_size($row->{nb_bytes}); + + $row->{db_size} = '???'; + $row->{label} = $label; + + $self->display($row, "general.tpl"); +} + +sub get_param +{ + my ($self, @what) = @_ ; + my %elt = map { $_ => 1 } @what; + my %ret; + + my $limit = ''; + + if ($elt{clients}) { + my @clients = CGI::param('client'); + if (@clients) { + $ret{clients} = \@clients; + my $str = $self->dbh_join(@clients); + $limit .= "AND Client.Name IN ($str) "; + } + } + + if ($elt{filesets}) { + my @filesets = CGI::param('fileset'); + if (@filesets) { + $ret{filesets} = \@filesets; + my $str = $self->dbh_join(@filesets); + $limit .= "AND FileSet.FileSet IN ($str) "; + } + } + + if ($elt{mediatypes}) { + my @medias = CGI::param('mediatype'); + if (@medias) { + $ret{mediatypes} = \@medias; + my $str = $self->dbh_join(@medias); + $limit .= "AND Media.MediaType IN ($str) "; + } + } + + if ($elt{client}) { + my $client = CGI::param('client'); + $ret{client} = $client; + $client = $self->dbh_join($client); + $limit .= "AND Client.Name = $client "; + } + + if ($elt{level}) { + my $level = CGI::param('level') || ''; + if ($level =~ /^(\w)$/) { + $ret{level} = $1; + $limit .= "AND Job.Level = '$1' "; + } + } + + if ($elt{jobid}) { + my $jobid = CGI::param('jobid') || ''; + + if ($jobid =~ /^(\d+)$/) { + $ret{jobid} = $1; + $limit .= "AND Job.JobId = '$1' "; + } + } + + if ($elt{status}) { + my $status = CGI::param('status') || ''; + if ($status =~ /^(\w)$/) { + $ret{status} = $1; + $limit .= "AND Job.JobStatus = '$1' "; + } + } + + if ($elt{locations}) { + my @location = CGI::param('location') ; + if (@location) { + $ret{locations} = \@location; + my $str = $self->dbh_join(@location); + $limit .= "AND Location.Location IN ($str) "; + } + } + + if ($elt{pools}) { + my @pool = CGI::param('pool') ; + if (@pool) { + $ret{pools} = \@pool; + my $str = $self->dbh_join(@pool); + $limit .= "AND Pool.Name IN ($str) "; + } + } + + if ($elt{location}) { + my $location = CGI::param('location') || ''; + if ($location) { + $ret{location} = $location; + $location = $self->dbh_quote($location); + $limit .= "AND Location.Location = $location "; + } + } + + if ($elt{pool}) { + my $pool = CGI::param('pool') || ''; + if ($pool) { + $ret{pool} = $pool; + $pool = $self->dbh_quote($pool); + $limit .= "AND Pool.Name = $pool "; + } + } + + if ($elt{jobtype}) { + my $jobtype = CGI::param('jobtype') || ''; + if ($jobtype =~ /^(\w)$/) { + $ret{jobtype} = $1; + $limit .= "AND Job.Type = '$1' "; + } + } + + return ($limit, %ret); +} + +=head1 + + get last backup + +SELECT DISTINCT Job.JobId AS jobid, + Client.Name AS client, + FileSet.FileSet AS fileset, + Job.Name AS jobname, + Level AS level, + StartTime AS starttime, + JobFiles AS jobfiles, + JobBytes AS jobbytes, + VolumeName AS volumename, + JobStatus AS jobstatus, + JobErrors AS joberrors + + FROM Client,Job,JobMedia,Media,FileSet + WHERE Client.ClientId=Job.ClientId + AND Job.FileSetId=FileSet.FileSetId + AND JobMedia.JobId=Job.JobId + AND JobMedia.MediaId=Media.MediaId + $limit + +=cut + +sub display_job +{ + my ($self, %arg) = @_ ; + + $arg{order} = ' Job.JobId DESC '; + + my ($limit, $label) = $self->get_limit(%arg); + my ($where, undef) = $self->get_param('clients', + 'level', + 'filesets', + 'jobtype', + 'jobid', + 'status'); + + my $query=" +SELECT Job.JobId AS jobid, + Client.Name AS client, + FileSet.FileSet AS fileset, + Job.Name AS jobname, + Level AS level, + StartTime AS starttime, + Pool.Name AS poolname, + JobFiles AS jobfiles, + JobBytes AS jobbytes, + JobStatus AS jobstatus, + JobErrors AS joberrors + + FROM Client, + Job LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId) + LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId) + WHERE Client.ClientId=Job.ClientId + $where + $limit +"; + + my $all = $self->dbh_selectall_hashref($query, 'jobid') ; + + foreach (values %$all) { + $_->{jobbytes} = human_size($_->{jobbytes}) ; + } + + $self->display({ Filter => $label, + ID => $cur_id++, + Jobs => + [ + sort { $a->{jobid} <=> $b->{jobid} } + values %$all + ], + }, + "display_job.tpl"); +} + +# display job informations +sub display_job_zoom +{ + my ($self, $jobid) = @_ ; + + $jobid = $self->dbh_quote($jobid); + + my $query=" +SELECT DISTINCT Job.JobId AS jobid, + Client.Name AS client, + Job.Name AS jobname, + FileSet.FileSet AS fileset, + Level AS level, + Pool.Name AS poolname, + StartTime AS starttime, + JobFiles AS jobfiles, + JobBytes AS jobbytes, + JobStatus AS jobstatus, + $self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(EndTime) + - $self->{sql}->{UNIX_TIMESTAMP}(StartTime)) AS duration + + FROM Client, + Job LEFT JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId) + LEFT JOIN Pool ON (Job.PoolId = Pool.PoolId) + WHERE Client.ClientId=Job.ClientId + AND Job.JobId = $jobid +"; + + my $row = $self->dbh_selectrow_hashref($query) ; + + $row->{jobbytes} = human_size($row->{jobbytes}) ; + + # display all volumes associate with this job + $query=" +SELECT Media.VolumeName as volumename +FROM Job,Media,JobMedia +WHERE Job.JobId = $jobid + AND JobMedia.JobId=Job.JobId + AND JobMedia.MediaId=Media.MediaId +"; + + my $all = $self->dbh_selectall_hashref($query, 'volumename'); + + $row->{volumes} = [ values %$all ] ; + + $self->display($row, "display_job_zoom.tpl"); +} + +sub display_media +{ + my ($self) = @_ ; + + my ($where, %elt) = $self->get_param('pool', + 'location'); + + my $query=" +SELECT Media.VolumeName AS volumename, + Media.VolBytes AS volbytes, + Media.VolStatus AS volstatus, + Media.MediaType AS mediatype, + Media.InChanger AS online, + Media.LastWritten AS lastwritten, + Location.Location AS location, + Pool.Name AS poolname, + $self->{sql}->{FROM_UNIXTIME}( + $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten) + + $self->{sql}->{TO_SEC}(Media.VolRetention) + ) AS expire +FROM Pool, Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId) +WHERE Media.PoolId=Pool.PoolId +$where +"; + + my $all = $self->dbh_selectall_hashref($query, 'volumename') ; + foreach (values %$all) { + $_->{volbytes} = human_size($_->{volbytes}) ; + } + + $self->display({ ID => $cur_id++, + Pool => $elt{pool}, + Location => $elt{location}, + Medias => [ values %$all ] + }, + "display_media.tpl"); +} + +sub display_medias +{ + my ($self) = @_ ; + + my $pool = $self->get_form('db_pools'); + + foreach my $name (@{ $pool->{db_pools} }) { + CGI::param('pool', $name->{name}); + $self->display_media(); + } +} + +sub display_media_zoom +{ + my ($self) = @_ ; + + my $medias = $self->get_form('jmedias'); + + unless ($medias->{jmedias}) { + return $self->error("Can't get media selection"); + } + + my $query=" +SELECT InChanger AS online, + VolBytes AS nb_bytes, + VolumeName AS volumename, + VolStatus AS volstatus, + VolMounts AS nb_mounts, + Media.VolUseDuration AS voluseduration, + Media.MaxVolJobs AS maxvoljobs, + Media.MaxVolFiles AS maxvolfiles, + Media.MaxVolBytes AS maxvolbytes, + VolErrors AS nb_errors, + Pool.Name AS poolname, + Location.Location AS location, + Media.Recycle AS recycle, + Media.VolRetention AS volretention, + Media.LastWritten AS lastwritten, + $self->{sql}->{FROM_UNIXTIME}( + $self->{sql}->{UNIX_TIMESTAMP}(Media.LastWritten) + + $self->{sql}->{TO_SEC}(Media.VolRetention) + ) AS expire + FROM Job,Pool, + Media LEFT JOIN Location ON (Media.LocationId = Location.LocationId) + WHERE Pool.PoolId = Media.PoolId + AND VolumeName IN ($medias->{jmedias}) +"; + + my $all = $self->dbh_selectall_hashref($query, 'volumename') ; + + foreach my $media (values %$all) { + $media->{nb_bytes} = human_size($media->{nb_bytes}) ; + $media->{voluseduration} = human_sec($media->{voluseduration}); + $media->{volretention} = human_sec($media->{volretention}); + my $mq = $self->dbh_quote($media->{volumename}); + + $query = " +SELECT DISTINCT Job.JobId AS jobid, + Job.Name AS name, + Job.StartTime AS starttime, + Job.Type AS type, + Job.Level AS level, + Job.JobFiles AS files, + Job.JobBytes AS bytes, + Job.jobstatus AS status + FROM Media,JobMedia,Job + WHERE Media.VolumeName=$mq + AND Media.MediaId=JobMedia.MediaId + AND JobMedia.JobId=Job.JobId +"; + + my $jobs = $self->dbh_selectall_hashref($query, 'jobid') ; + + foreach (values %$jobs) { + $_->{bytes} = human_size($_->{bytes}) ; + } + + $self->display({ jobs => [ values %$jobs ], + %$media }, + "display_media_zoom.tpl"); + } +} + +sub location_edit +{ + my ($self) = @_ ; + + my $loc = $self->get_form('qlocation'); + unless ($loc->{qlocation}) { + return $self->error("Can't get location"); + } + + my $query = " +SELECT Location.Location AS location, + Location.Cost AS cost, + Location.Enabled AS enabled +FROM Location +WHERE Location.Location = $loc->{qlocation} +"; + + my $row = $self->dbh_selectrow_hashref($query); + + $self->display({ ID => $cur_id++, + %$row }, "location_edit.tpl") ; + +} + +sub location_save +{ + my ($self) = @_ ; + + my $arg = $self->get_form(qw/qlocation qnewlocation cost/) ; + unless ($arg->{qlocation}) { + return $self->error("Can't get location"); + } + unless ($arg->{qnewlocation}) { + return $self->error("Can't get new location name"); + } + unless ($arg->{cost}) { + return $self->error("Can't get new cost"); + } + + my $enabled = CGI::param('enabled') || ''; + $enabled = $enabled?1:0; + + my $query = " +UPDATE Location SET Cost = $arg->{cost}, + Location = $arg->{qnewlocation}, + Enabled = $enabled +WHERE Location.Location = $arg->{qlocation} +"; + + $self->dbh_do($query); + + $self->display_location(); +} + +sub location_add +{ + my ($self) = @_ ; + my $arg = $self->get_form(qw/qlocation cost/) ; + + unless ($arg->{qlocation}) { + $self->display({}, "location_add.tpl"); + return 1; + } + unless ($arg->{cost}) { + return $self->error("Can't get new cost"); + } + + my $enabled = CGI::param('enabled') || ''; + $enabled = $enabled?1:0; + + my $query = " +INSERT INTO Location (Location, Cost, Enabled) + VALUES ($arg->{qlocation}, $arg->{cost}, $enabled) +"; + + $self->dbh_do($query); + + $self->display_location(); +} + +sub display_location +{ + my ($self) = @_ ; + + my $query = " +SELECT Location.Location AS location, + Location.Cost AS cost, + Location.Enabled AS enabled, + (SELECT count(Media.MediaId) + FROM Media + WHERE Media.LocationId = Location.LocationId + ) AS volnum +FROM Location +"; + + my $location = $self->dbh_selectall_hashref($query, 'location'); + + $self->display({ ID => $cur_id++, + Locations => [ values %$location ] }, + "display_location.tpl"); +} + +sub update_location +{ + my ($self) = @_ ; + + my $medias = $self->get_selected_media_location(); + unless ($medias) { + return ; + } + + my $arg = $self->get_form('db_locations', 'qnewlocation'); + + $self->display({ email => $self->{info}->{email_media}, + %$arg, + medias => [ values %$medias ], + }, + "update_location.tpl"); +} + +sub do_update_media +{ + my ($self) = @_ ; + + my $media = CGI::param('media'); + unless ($media) { + return $self->error("Can't find media selection"); + } + + $media = $self->dbh_quote($media); + + my $update = ''; + + my $volstatus = CGI::param('volstatus') || ''; + $volstatus = $self->dbh_quote($volstatus); # is checked by db + $update .= " VolStatus=$volstatus, "; + + my $inchanger = CGI::param('inchanger') || ''; + if ($inchanger) { + $update .= " InChanger=1, " ; + my $slot = CGI::param('slot') || ''; + if ($slot =~ /^(\d+)$/) { + $update .= " Slot=$1, "; + } else { + $update .= " Slot=0, "; + } + } else { + $update = " Slot=0, InChanger=0, "; + } + + my $pool = CGI::param('pool') || ''; + $pool = $self->dbh_quote($pool); # is checked by db + $update .= " PoolId=(SELECT PoolId FROM Pool WHERE Name=$pool), "; + + my $volretention = CGI::param('volretention') || ''; + $volretention = from_human_sec($volretention); + unless ($volretention) { + return $self->error("Can't get volume retention"); + } + + $update .= " VolRetention = $volretention, "; + + my $loc = CGI::param('location') || ''; + $loc = $self->dbh_quote($loc); # is checked by db + $update .= " LocationId=(SELECT LocationId FROM Location WHERE Location=$loc), "; + + my $usedu = CGI::param('voluseduration') || '0'; + $usedu = from_human_sec($usedu); + $update .= " VolUseDuration=$usedu, "; + + my $maxj = CGI::param('maxvoljobs') || '0'; + unless ($maxj =~ /^(\d+)$/) { + return $self->error("Can't get max jobs"); + } + $update .= " MaxVolJobs=$1, " ; + + my $maxf = CGI::param('maxvolfiles') || '0'; + unless ($maxj =~ /^(\d+)$/) { + return $self->error("Can't get max files"); + } + $update .= " MaxVolFiles=$1, " ; + + my $maxb = CGI::param('maxvolbytes') || '0'; + unless ($maxb =~ /^(\d+)$/) { + return $self->error("Can't get max bytes"); + } + $update .= " MaxVolBytes=$1 " ; + + my $row=$self->dbh_do("UPDATE Media SET $update WHERE VolumeName=$media"); + + if ($row) { + print "Update Ok\n"; + $self->update_media(); + } +} + +sub update_media +{ + my ($self) = @_ ; + + my $media = $self->get_form('qmedia'); + + unless ($media->{qmedia}) { + return $self->error("Can't get media"); + } + + my $query = " +SELECT Media.Slot AS slot, + Pool.Name AS poolname, + Media.VolStatus AS volstatus, + Media.InChanger AS inchanger, + Location.Location AS location, + Media.VolumeName AS volumename, + Media.MaxVolBytes AS maxvolbytes, + Media.MaxVolJobs AS maxvoljobs, + Media.MaxVolFiles AS maxvolfiles, + Media.VolUseDuration AS voluseduration, + Media.VolRetention AS volretention + +FROM Media INNER JOIN Pool ON (Media.PoolId = Pool.PoolId) + LEFT JOIN Location ON (Media.LocationId = Location.LocationId) + +WHERE Media.VolumeName = $media->{qmedia} +"; + + my $row = $self->dbh_selectrow_hashref($query); + $row->{volretention} = human_sec($row->{volretention}); + $row->{voluseduration} = human_sec($row->{voluseduration}); + + my $elt = $self->get_form(qw/db_pools db_locations/); + + $self->display({ + %$elt, + %$row, + }, + "update_media.tpl"); +} + +sub save_location +{ + my ($self) = @_ ; + + my $medias = $self->get_selected_media(); + + unless ($medias) { + return 0; + } + + my $loc = $self->get_form('qnewlocation'); + unless ($loc->{qnewlocation}) { + return $self->error("Can't get new location"); + } + + my $query = " + UPDATE Media + SET LocationId = (SELECT LocationId + FROM Location + WHERE Location = $loc->{qnewlocation}) + WHERE Media.VolumeName IN ($medias) +"; + + my $nb = $self->dbh_do($query); + + print "$nb media updated"; +} + +sub change_location +{ + my ($self) = @_ ; + + my $medias = $self->get_selected_media_location(); + unless ($medias) { + return $self->error("Can't get media selection"); + } + my $newloc = CGI::param('newlocation'); + + my $user = CGI::param('user') || 'unknow'; + my $comm = CGI::param('comment') || ''; + $comm = $self->dbh_quote("$user: $comm"); + + my $query; + + foreach my $media (keys %$medias) { + $query = " +INSERT LocationLog (Date, Comment, MediaId, LocationId, NewVolStatus) + VALUES( + NOW(), $comm, (SELECT MediaId FROM Media WHERE VolumeName = '$media'), + (SELECT LocationId FROM Location WHERE Location = '$medias->{$media}->{location}'), + (SELECT VolStatus FROM Media WHERE VolumeName = '$media') + ) +"; + + $self->debug($query); + } + + my $q = new CGI; + $q->param('action', 'update_location'); + my $url = $q->url(-full => 1, -query=>1); + + $self->display({ email => $self->{info}->{email_media}, + url => $url, + newlocation => $newloc, + # [ { volumename => 'vol1' }, { volumename => 'vol2' },..] + medias => [ values %$medias ], + }, + "change_location.tpl"); + +} + +sub display_client_stats +{ + my ($self, %arg) = @_ ; + + my $client = $self->dbh_quote($arg{clientname}); + my ($limit, $label) = $self->get_limit(%arg); + + my $query = " +SELECT + count(Job.JobId) AS nb_jobs, + sum(Job.JobBytes) AS nb_bytes, + sum(Job.JobErrors) AS nb_err, + sum(Job.JobFiles) AS nb_files, + Client.Name AS clientname +FROM Job INNER JOIN Client USING (ClientId) +WHERE + Client.Name = $client + $limit +GROUP BY Client.Name +"; + + my $row = $self->dbh_selectrow_hashref($query); + + $row->{ID} = $cur_id++; + $row->{label} = $label; + $row->{nb_bytes} = human_size($row->{nb_bytes}) ; + + $self->display($row, "display_client_stats.tpl"); +} + +# poolname can be undef +sub display_pool +{ + my ($self, $poolname) = @_ ; + +# TODO : afficher les tailles et les dates + + my $query = " +SELECT Pool.Name AS name, + Pool.Recycle AS recycle, + Pool.VolRetention AS volretention, + Pool.VolUseDuration AS voluseduration, + Pool.MaxVolJobs AS maxvoljobs, + Pool.MaxVolFiles AS maxvolfiles, + Pool.MaxVolBytes AS maxvolbytes, + (SELECT count(Media.MediaId) + FROM Media + WHERE Media.PoolId = Pool.PoolId + ) AS volnum + FROM Pool +"; + + my $all = $self->dbh_selectall_hashref($query, 'name') ; + foreach (values %$all) { + $_->{maxvolbytes} = human_size($_->{maxvolbytes}) ; + $_->{volretention} = human_sec($_->{volretention}) ; + $_->{voluseduration} = human_sec($_->{voluseduration}) ; + } + + $self->display({ ID => $cur_id++, + Pools => [ values %$all ]}, + "display_pool.tpl"); +} + +sub display_running_job +{ + my ($self) = @_; + + my $arg = $self->get_form('client', 'jobid'); + + if (!$arg->{client} and $arg->{jobid}) { + + my $query = " +SELECT Client.Name AS name +FROM Job INNER JOIN Client USING (ClientId) +WHERE Job.JobId = $arg->{jobid} +"; + + my $row = $self->dbh_selectrow_hashref($query); + + if ($row) { + $arg->{client} = $row->{name}; + CGI::param('client', $arg->{client}); + } + } + + if ($arg->{client}) { + my $cli = new Bweb::Client(name => $arg->{client}); + $cli->display_running_job($self->{info}, $arg->{jobid}); + if ($arg->{jobid}) { + $self->get_job_log(); + } + } else { + $self->error("Can't get client or jobid"); + } +} + +sub display_running_jobs +{ + my ($self, $display_action) = @_; + + my $query = " +SELECT Job.JobId AS jobid, + Job.Name AS jobname, + Job.Level AS level, + Job.StartTime AS starttime, + Job.JobFiles AS jobfiles, + Job.JobBytes AS jobbytes, + Job.JobStatus AS jobstatus, +$self->{sql}->{SEC_TO_TIME}( $self->{sql}->{UNIX_TIMESTAMP}(NOW()) + - $self->{sql}->{UNIX_TIMESTAMP}(StartTime)) + AS duration, + Client.Name AS clientname +FROM Job INNER JOIN Client USING (ClientId) +WHERE JobStatus IN ('C','R','B','e','D','F','S','m','M','s','j','c','d','t','p') +"; + my $all = $self->dbh_selectall_hashref($query, 'jobid') ; + + $self->display({ ID => $cur_id++, + display_action => $display_action, + Jobs => [ values %$all ]}, + "running_job.tpl") ; +} + +sub eject_media +{ + my ($self) = @_; + my $arg = $self->get_form('jmedias', 'slots', 'ach'); + + unless ($arg->{jmedias}) { + return $self->error("Can't get media selection"); + } + + my $query = " +SELECT Media.VolumeName AS volumename, + Storage.Name AS storage, + Location.Location AS location, + Media.Slot AS slot +FROM Media INNER JOIN Storage ON (Media.StorageId = Storage.StorageId) + LEFT JOIN Location ON (Media.LocationId = Location.LocationId) +WHERE Media.VolumeName IN ($arg->{jmedias}) + AND Media.InChanger = 1 +"; + + my $all = $self->dbh_selectall_hashref($query, 'volumename'); + + my $a = Bweb::Autochanger::get('SDLT-1-2', $self); + + $a->status(); + foreach my $vol (values %$all) { + print "eject $vol->{volumename} from $vol->{storage} : "; + if ($a->send_to_io($vol->{slot})) { + print "ok
"; + } else { + print "err
"; + } + } +} + +sub restore +{ + my ($self) = @_; + + my $arg = $self->get_form('jobid', 'client'); + + print CGI::header('text/brestore'); + print "jobid=$arg->{jobid}\n" if ($arg->{jobid}); + print "client=$arg->{client}\n" if ($arg->{client}); + print "\n"; +} + +# TODO : move this to Bweb::Autochanger ? +# TODO : make this internal to not eject tape ? +use Bconsole; + +sub delete +{ + my ($self) = @_; + my $arg = $self->get_form('jobid'); + + my $b = new Bconsole(pref => $self->{info}); + + if ($arg->{jobid}) { + my $ret = $b->send_cmd("delete jobid=\"$arg->{jobid}\""); + $self->display({ + content => $b->send_cmd("delete jobid=\"$arg->{jobid}\""), + title => "Delete a job ", + name => "delete jobid=$arg->{jobid}", + }, "command.tpl"); + } +} + +sub update_slots +{ + my ($self) = @_; + + my $ach = CGI::param('ach') ; + unless ($ach =~ /^([\w\d\.-]+)$/) { + return $self->error("Bad autochanger name"); + } + + my $b = new Bconsole(pref => $self->{info}); + print "
" . $b->update_slots($ach) . "
"; +} + +sub get_job_log +{ + my ($self) = @_; + + my $arg = $self->get_form('jobid'); + unless ($arg->{jobid}) { + return $self->error("Can't get jobid"); + } + + my $t = CGI::param('time') || ''; + + my $query = " +SELECT Job.Name as name, Client.Name as clientname + FROM Job INNER JOIN Client ON (Job.ClientId = Client.ClientId) + WHERE JobId = $arg->{jobid} +"; + + my $row = $self->dbh_selectrow_hashref($query); + + unless ($row) { + return $self->error("Can't find $arg->{jobid} in catalog"); + } + + + $query = " +SELECT Time AS time, LogText AS log + FROM Log + WHERE JobId = $arg->{jobid} +"; + my $log = $self->dbh_selectall_arrayref($query); + unless ($log) { + return $self->error("Can't get log for jobid $arg->{jobid}"); + } + + if ($t) { + # log contains \n + $logtxt = join("", map { ($_->[0] . ' ' . $_->[1]) } @$log ) ; + } else { + $logtxt = join("", map { $_->[1] } @$log ) ; + } + + $self->display({ lines=> $logtxt, + jobid => $arg->{jobid}, + name => $row->{name}, + client => $row->{clientname}, + }, 'display_log.tpl'); +} + + +sub label_barcodes +{ + my ($self) = @_ ; + + my $arg = $self->get_form('ach', 'slots', 'drive'); + + unless ($arg->{ach}) { + return $self->error("Can't find autochanger name"); + } + + my $slots = ''; + if ($arg->{slots}) { + $slots = join(",", @{ $arg->{slots} }); + } + + my $t = 60*scalar( @{ $arg->{slots} }); + my $b = new Bconsole(pref => $self->{info}, timeout => $t,log_stdout => 1); + print "

This command can take long time, be patient...

"; + print "
" ;
+    $b->label_barcodes(storage => $arg->{ach},
+		       drive => $arg->{drive},
+		       pool  => 'Scratch',
+		       slots => $slots) ;
+    print "
"; +} + +sub purge +{ + my ($self) = @_; + + my @volume = CGI::param('media'); + + my $b = new Bconsole(pref => $self->{info}, timeout => 60); + + $self->display({ + content => $b->purge_volume(@volume), + title => "Purge media", + name => "purge volume=" . join(' volume=', @volume), + }, "command.tpl"); +} + +sub prune +{ + my ($self) = @_; + + my $b = new Bconsole(pref => $self->{info}, timeout => 60); + + my @volume = CGI::param('media'); + $self->display({ + content => $b->prune_volume(@volume), + title => "Prune media", + name => "prune volume=" . join(' volume=', @volume), + }, "command.tpl"); +} + +sub cancel_job +{ + my ($self) = @_; + + my $arg = $self->get_form('jobid'); + unless ($arg->{jobid}) { + return $self->error('Bad jobid'); + } + + my $b = new Bconsole(pref => $self->{info}); + $self->display({ + content => $b->cancel($arg->{jobid}), + title => "Cancel job", + name => "cancel jobid=$arg->{jobid}", + }, "command.tpl"); +} + +sub director_show_sched +{ + my ($self) = @_ ; + + my $arg = $self->get_form('days'); + + my $b = new Bconsole(pref => $self->{info}) ; + + my $ret = $b->director_get_sched( $arg->{days} ); + + $self->display({ + id => $cur_id++, + list => $ret, + }, "scheduled_job.tpl"); +} + +sub enable_disable_job +{ + my ($self, $what) = @_ ; + + my $name = CGI::param('job') || ''; + unless ($name =~ /^[\w\d\.\-\s]+$/) { + return $self->error("Can't find job name"); + } + + my $b = new Bconsole(pref => $self->{info}) ; + + my $cmd; + if ($what) { + $cmd = "enable"; + } else { + $cmd = "disable"; + } + + $self->display({ + content => $b->send_cmd("$cmd job=\"$name\""), + title => "$cmd $name", + name => "$cmd job=\"$name\"", + }, "command.tpl"); +} + +sub run_job_select +{ + my ($self) = @_; + $b = new Bconsole(pref => $self->{info}); + + my $joblist = [ map { { name => $_ } } split(/\r\n/, $b->send_cmd(".job")) ]; + + $self->display({ Jobs => $joblist }, "run_job.tpl"); +} + +sub run_parse_job +{ + my ($self, $ouput) = @_; + + my %arg; + foreach my $l (split(/\r\n/, $ouput)) { + if ($l =~ /(\w+): name=([\w\d\.\s-]+?)(\s+\w+=.+)?$/) { + $arg{$1} = $2; + $l = $3 + if ($3) ; + } + + if (my @l = $l =~ /(\w+)=([\w\d*]+)/g) { + %arg = (%arg, @l); + } + } + + my %lowcase ; + foreach my $k (keys %arg) { + $lowcase{lc($k)} = $arg{$k} ; + } + + return \%lowcase; +} + +sub run_job_mod +{ + my ($self) = @_; + $b = new Bconsole(pref => $self->{info}); + + my $job = CGI::param('job') || ''; + + my $info = $b->send_cmd("show job=\"$job\""); + my $attr = $self->run_parse_job($info); + + my $jobs = [ map {{ name => $_ }} split(/\r\n/, $b->send_cmd(".job")) ]; + + my $pools = [ map { { name => $_ } } split(/\r\n/, $b->send_cmd(".pool")) ]; + my $clients = [ map { { name => $_ } } split(/\r\n/, $b->send_cmd(".client")) ]; + my $filesets= [ map { { name => $_ } } split(/\r\n/, $b->send_cmd(".fileset")) ]; + my $storages= [ map { { name => $_ } } split(/\r\n/, $b->send_cmd(".storage")) ]; + + $self->display({ + jobs => $jobs, + pools => $pools, + clients => $clients, + filesets => $filesets, + storages => $storages, + %$attr, + }, "run_job_mod.tpl"); +} + +sub run_job +{ + my ($self) = @_; + $b = new Bconsole(pref => $self->{info}); + + my $jobs = [ map {{ name => $_ }} split(/\r\n/, $b->send_cmd(".job")) ]; + + $self->display({ + jobs => $jobs, + }, "run_job.tpl"); +} + +sub run_job_now +{ + my ($self) = @_; + $b = new Bconsole(pref => $self->{info}); + + # TODO: check input (don't use pool, level) + + my $arg = $self->get_form('pool', 'level', 'client', 'priority'); + my $job = CGI::param('job') || ''; + my $storage = CGI::param('storage') || ''; + + my $jobid = $b->run(job => $job, + client => $arg->{client}, + priority => $arg->{priority}, + level => $arg->{level}, + storage => $storage, + pool => $arg->{pool}, + ); + + print $jobid, $b->{error}; + + print "
You can follow job execution here "; +} + +1; diff --git a/gui/bweb/script/bweb-postgresql.sql b/gui/bweb/script/bweb-postgresql.sql new file mode 100644 index 0000000000..8bc14bf310 --- /dev/null +++ b/gui/bweb/script/bweb-postgresql.sql @@ -0,0 +1,23 @@ +BEGIN; + +CREATE FUNCTION SEC_TO_TIME(timestamp with time zone) +RETURNS timestamp with time zone AS $$ + select date_trunc('second', $1); +$$ LANGUAGE SQL; + +CREATE FUNCTION SEC_TO_TIME(bigint) +RETURNS interval AS $$ + select date_trunc('second', $1 * interval '1 second'); +$$ LANGUAGE SQL; + +CREATE FUNCTION UNIX_TIMESTAMP(timestamp with time zone) +RETURNS double precision AS $$ + select date_part('epoch', $1); +$$ LANGUAGE SQL; + +CREATE FUNCTION SEC_TO_INT(interval) +RETURNS double precision AS $$ + select extract(epoch from $1); +$$ LANGUAGE SQL; + +COMMIT; \ No newline at end of file diff --git a/gui/bweb/tpl/about.tpl b/gui/bweb/tpl/about.tpl new file mode 100644 index 0000000000..6a9e19a9ff --- /dev/null +++ b/gui/bweb/tpl/about.tpl @@ -0,0 +1,29 @@ +
+
+

About

+
+
+
+    Bweb Copyright (C) 2006 Eric Bollengier
+        All rights reserved.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+    Bacula Copyright 2000-2006 Kern Sibbald (GPL)
+    nrsTable Copyright 2005 New Roads School (GPL)
+    kaiska css Copyright Willy Morin (BWL)
+
+
diff --git a/gui/bweb/tpl/ach_content.tpl b/gui/bweb/tpl/ach_content.tpl new file mode 100644 index 0000000000..da667159ef --- /dev/null +++ b/gui/bweb/tpl/ach_content.tpl @@ -0,0 +1,155 @@ +
+
+

+Autochanger : ( Drives + IMPORT/EXPORT)

+
+
+
+ + + You must run update slot, Autochanger status is different from bacula slots +
+
+ + + + +
+
+ Tools +
+
+ + + + + + +

+ + + +
+
+ + Drives:
+

+
+ Content:
+
+
+
+ + diff --git a/gui/bweb/tpl/begin.tpl b/gui/bweb/tpl/begin.tpl new file mode 100644 index 0000000000..da3f7744a6 --- /dev/null +++ b/gui/bweb/tpl/begin.tpl @@ -0,0 +1,38 @@ + + +Bweb + + + + + + + + + + + + + diff --git a/gui/bweb/tpl/change_location.tpl b/gui/bweb/tpl/change_location.tpl new file mode 100644 index 0000000000..87f5332b7b --- /dev/null +++ b/gui/bweb/tpl/change_location.tpl @@ -0,0 +1,25 @@ +
+ + + +
Move media
+ To:
+ Subject: [BACULA] Move media to
+
+Dear,
+
+Could you move these media to
+Media :
+
    + +
  • () + +
+ +When it's finish, could you update media location ? (you can use this link). +
+Thanks + +
+ diff --git a/gui/bweb/tpl/client_job_status.tpl b/gui/bweb/tpl/client_job_status.tpl new file mode 100644 index 0000000000..0d3939232a --- /dev/null +++ b/gui/bweb/tpl/client_job_status.tpl @@ -0,0 +1,44 @@ +
+
+

+ Running job on +

+
+
+ + + + + + + + + + + + + + + + +
JobName: () +
Processing file:
Speed: B/s
Files Examined:
Bytes:
+ +
+ +
+ + + + +
+ + + + diff --git a/gui/bweb/tpl/client_list.tpl b/gui/bweb/tpl/client_list.tpl new file mode 100644 index 0000000000..4db687ba75 --- /dev/null +++ b/gui/bweb/tpl/client_list.tpl @@ -0,0 +1,65 @@ +
+
+

Clients

+
+
+
+
+
+ Actions   +
+
+ +   +   +   +   +
+ +
+
+ + diff --git a/gui/bweb/tpl/client_status.tpl b/gui/bweb/tpl/client_status.tpl new file mode 100644 index 0000000000..04a5ba296c --- /dev/null +++ b/gui/bweb/tpl/client_status.tpl @@ -0,0 +1,44 @@ +
+
+

+ Running job on +

+
+
+ + + + + + + + + + + + + + + + +
JobName: () +
Processing file:
Speed: B/s
Files Examined:
Bytes:
+ +
+ +
+ + + + +
+ + + + diff --git a/gui/bweb/tpl/command.tpl b/gui/bweb/tpl/command.tpl new file mode 100644 index 0000000000..ce9a9065ca --- /dev/null +++ b/gui/bweb/tpl/command.tpl @@ -0,0 +1,9 @@ +
+
+

:

+
+
+
+
+  
+
diff --git a/gui/bweb/tpl/config_edit.tpl b/gui/bweb/tpl/config_edit.tpl new file mode 100644 index 0000000000..8d2e33bdfc --- /dev/null +++ b/gui/bweb/tpl/config_edit.tpl @@ -0,0 +1,49 @@ +
+
+

Configuration

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SQL Connection
DBI : + +
user : +
password : +
General Options
email_media : +
Bweb Configuration
graph_font : +
template_dir : +
bconsole : +
debug : +
+ +
+
diff --git a/gui/bweb/tpl/config_view.tpl b/gui/bweb/tpl/config_view.tpl new file mode 100644 index 0000000000..37ddc80fe0 --- /dev/null +++ b/gui/bweb/tpl/config_view.tpl @@ -0,0 +1,29 @@ +
+
+

Configuration

+
+
+ + + + + + + + + + + + + +
SQL connection
SQL Connection
DBI :
user :
password :
General Options
email_media :
Bweb Configuration
template_dir :
graph_font :
bconsole :
debug :
+ + info :
+ +
+ +
+ +
diff --git a/gui/bweb/tpl/display_client_job.tpl b/gui/bweb/tpl/display_client_job.tpl new file mode 100644 index 0000000000..f5a6bef96d --- /dev/null +++ b/gui/bweb/tpl/display_client_job.tpl @@ -0,0 +1,65 @@ +
+
+

Last jobs for () +

+
+
+ +
+ + + backup size + + + backup duration + + + backup rate + +
+ + + diff --git a/gui/bweb/tpl/display_client_stats.tpl b/gui/bweb/tpl/display_client_stats.tpl new file mode 100644 index 0000000000..536cf88a7a --- /dev/null +++ b/gui/bweb/tpl/display_client_stats.tpl @@ -0,0 +1,58 @@ +
+
+

Client : ()

+
+
+
+ Not enough data   + Not enough data   + Not enough data   + +
+
+ + diff --git a/gui/bweb/tpl/display_form_job.tpl b/gui/bweb/tpl/display_form_job.tpl new file mode 100644 index 0000000000..3901fbf152 --- /dev/null +++ b/gui/bweb/tpl/display_form_job.tpl @@ -0,0 +1,114 @@ +
+ +
+ Filter   +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+

Level

+ +
+

Status

+ +
+

Age

+ +
+

Number of items

+ +
+

Job Type

+ +
+

Clients

+ +
+ + +
+ + + diff --git a/gui/bweb/tpl/display_job.tpl b/gui/bweb/tpl/display_job.tpl new file mode 100644 index 0000000000..1fc9fb255c --- /dev/null +++ b/gui/bweb/tpl/display_job.tpl @@ -0,0 +1,80 @@ +
+
+

Last Jobs ()

+
+
+
+
+ + diff --git a/gui/bweb/tpl/display_job_zoom.tpl b/gui/bweb/tpl/display_job_zoom.tpl new file mode 100644 index 0000000000..2e298e16f6 --- /dev/null +++ b/gui/bweb/tpl/display_job_zoom.tpl @@ -0,0 +1,72 @@ +
+

Information about job

+
+
+
+
+ + +
+
+ + \ No newline at end of file diff --git a/gui/bweb/tpl/display_location.tpl b/gui/bweb/tpl/display_location.tpl new file mode 100644 index 0000000000..9c22c8394c --- /dev/null +++ b/gui/bweb/tpl/display_location.tpl @@ -0,0 +1,73 @@ +
+
+

Locations

+
+
+
+
+ + + + + +
+
+ + diff --git a/gui/bweb/tpl/display_log.tpl b/gui/bweb/tpl/display_log.tpl new file mode 100644 index 0000000000..251a7b094a --- /dev/null +++ b/gui/bweb/tpl/display_log.tpl @@ -0,0 +1,9 @@ +
+
+

Log : on ()

+
+
+
+
+  
+
diff --git a/gui/bweb/tpl/display_media.tpl b/gui/bweb/tpl/display_media.tpl new file mode 100644 index 0000000000..aa5d4118aa --- /dev/null +++ b/gui/bweb/tpl/display_media.tpl @@ -0,0 +1,88 @@ +
+
+

+ Media +

+
+
+ + +

+Pool : + + +

+
+ +

+Location : +

+
+ +
+
+ + + + + + +
+
+ + diff --git a/gui/bweb/tpl/display_media_zoom.tpl b/gui/bweb/tpl/display_media_zoom.tpl new file mode 100644 index 0000000000..bc24ba1472 --- /dev/null +++ b/gui/bweb/tpl/display_media_zoom.tpl @@ -0,0 +1,121 @@ +
+
+

Media :

+
+
+ Media Infos
+
+ Job List
+
+ Actions
+
+ + + + + + + + + +
+
+ + + + diff --git a/gui/bweb/tpl/display_pool.tpl b/gui/bweb/tpl/display_pool.tpl new file mode 100644 index 0000000000..a657c5f404 --- /dev/null +++ b/gui/bweb/tpl/display_pool.tpl @@ -0,0 +1,67 @@ +
+
+

Pools

+
+
+
+
+ +
+
+ Tips: To modify pool properties, you have to edit your bacula configuration + and reload it. After, you have to run "update pool=mypool" on bconsole. +
+ + diff --git a/gui/bweb/tpl/end.tpl b/gui/bweb/tpl/end.tpl new file mode 100644 index 0000000000..308b1d01b6 --- /dev/null +++ b/gui/bweb/tpl/end.tpl @@ -0,0 +1,2 @@ + + diff --git a/gui/bweb/tpl/error.tpl b/gui/bweb/tpl/error.tpl new file mode 100644 index 0000000000..154b805627 --- /dev/null +++ b/gui/bweb/tpl/error.tpl @@ -0,0 +1,4 @@ +

An error as occured :

+
+
+
\ No newline at end of file diff --git a/gui/bweb/tpl/general.tpl b/gui/bweb/tpl/general.tpl new file mode 100644 index 0000000000..5b6d69928b --- /dev/null +++ b/gui/bweb/tpl/general.tpl @@ -0,0 +1,30 @@ +
+ +
+

+ Informations +

+
+
+ + + + + + + + + + + + + + + + +
Total clients: Total bytes stored: Total media:
Database size: Total Pool: Total Job:
Job failed (): class='joberr' > + +
+
diff --git a/gui/bweb/tpl/graph.tpl b/gui/bweb/tpl/graph.tpl new file mode 100644 index 0000000000..ee957c8f01 --- /dev/null +++ b/gui/bweb/tpl/graph.tpl @@ -0,0 +1,150 @@ +
+
+

Statistics

+
+
+ + + +
+
+
+ Options   +
+
+ + + + + + + + + + + + + + + + + + + + +
+

Level

+ +
+

Status

+ +
+

Age

+ +
+

Size

+ Width:  
+ Height:
+
+

Clients

+ +
+

File Set

+ +

Type

+ +
+

Number of items

+ +

Graph type

+
+ +
+
+ +
+
+ +
+ Current   +
+
+ Nothing to display, Try a bigger date range +
+ +
+
+ + + diff --git a/gui/bweb/tpl/help_extern.tpl b/gui/bweb/tpl/help_extern.tpl new file mode 100644 index 0000000000..87174108a4 --- /dev/null +++ b/gui/bweb/tpl/help_extern.tpl @@ -0,0 +1,46 @@ +
+
+

Help to eject media (part 1/2)

+
+
+This tool will select for you best candidate to eject. You will +be asked for choose inside the selection in the next screen. +
+ + + + + + + + + + + + + + + + +
Pool: +
Media Type: +
Location : +
Number of media
to eject:
+
+
+
diff --git a/gui/bweb/tpl/help_extern_compute.tpl b/gui/bweb/tpl/help_extern_compute.tpl new file mode 100644 index 0000000000..02847883af --- /dev/null +++ b/gui/bweb/tpl/help_extern_compute.tpl @@ -0,0 +1,71 @@ +
+
+

Help to eject media (part 2/2)

+
+
+ Now, you can verify the selection and eject media. +
+
+ + + + + + diff --git a/gui/bweb/tpl/help_intern.tpl b/gui/bweb/tpl/help_intern.tpl new file mode 100644 index 0000000000..3c780194c7 --- /dev/null +++ b/gui/bweb/tpl/help_intern.tpl @@ -0,0 +1,53 @@ +
+
+

Help to load media (part 1/2)

+
+
+This tool will select for you best candidate to load. You will +be asked for choose inside the selection in the next screen. +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
Pool: +
Media Type: +
+ Location : + +
Expired :
Number of media
to load:
+
+ +
diff --git a/gui/bweb/tpl/help_intern_compute.tpl b/gui/bweb/tpl/help_intern_compute.tpl new file mode 100644 index 0000000000..a60241e98b --- /dev/null +++ b/gui/bweb/tpl/help_intern_compute.tpl @@ -0,0 +1,71 @@ +
+
+

Help to load media (part 2/2)

+
+
+ Now, you can verify the selection and load media. +
+
+ + + + + + diff --git a/gui/bweb/tpl/install.tpl b/gui/bweb/tpl/install.tpl new file mode 100644 index 0000000000..749c5d2447 --- /dev/null +++ b/gui/bweb/tpl/install.tpl @@ -0,0 +1,9 @@ +
+
+

Install notes

+
+
+ + + +
diff --git a/gui/bweb/tpl/job_select.tpl b/gui/bweb/tpl/job_select.tpl new file mode 100644 index 0000000000..8a32a27441 --- /dev/null +++ b/gui/bweb/tpl/job_select.tpl @@ -0,0 +1,77 @@ +
+ Find +
+
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Clients: + + + + Level: + + +
+ Status: + + +
+ JobId: + + + + Start Time: + + +
+ Limit: + + +
+ + + +
+ +
diff --git a/gui/bweb/tpl/location_add.tpl b/gui/bweb/tpl/location_add.tpl new file mode 100644 index 0000000000..feba84fb8a --- /dev/null +++ b/gui/bweb/tpl/location_add.tpl @@ -0,0 +1,26 @@ +
+
+

New location

+
+
+
+ + + + + + + + + + +
Location : + +
Cost : +
Enabled : +
+ +
+
diff --git a/gui/bweb/tpl/location_edit.tpl b/gui/bweb/tpl/location_edit.tpl new file mode 100644 index 0000000000..482041b97f --- /dev/null +++ b/gui/bweb/tpl/location_edit.tpl @@ -0,0 +1,27 @@ +
+
+

Location :

+
+
+
+ + + + + + + + + + + +
Location : + +
Cost : +
Enabled : checked > +
+ +
+
diff --git a/gui/bweb/tpl/move_media.tpl b/gui/bweb/tpl/move_media.tpl new file mode 100644 index 0000000000..30f096cf40 --- /dev/null +++ b/gui/bweb/tpl/move_media.tpl @@ -0,0 +1,85 @@ +
+
+

Move media

+
+
+
+
+ + + +
New location: + +
Status: + +
User: + +
Comment: + +
+ +
+
+ + diff --git a/gui/bweb/tpl/run_job.tpl b/gui/bweb/tpl/run_job.tpl new file mode 100644 index 0000000000..cbc8ed6c8e --- /dev/null +++ b/gui/bweb/tpl/run_job.tpl @@ -0,0 +1,32 @@ +
+
+

Defined jobs :

+
+
+
+ + + +
Job Name: + +
+
+ + + +
+
diff --git a/gui/bweb/tpl/run_job_mod.tpl b/gui/bweb/tpl/run_job_mod.tpl new file mode 100644 index 0000000000..5a35398765 --- /dev/null +++ b/gui/bweb/tpl/run_job_mod.tpl @@ -0,0 +1,124 @@ +
+
+

Run job : on

+
+
+
+ + + +
Job Name: + +
Pool: + + +
Client: + + + +
FileSet: + + +
Storage: + + +
Level: + + +
Priority: + + +
+
+ +
+
+ + \ No newline at end of file diff --git a/gui/bweb/tpl/running_job.tpl b/gui/bweb/tpl/running_job.tpl new file mode 100644 index 0000000000..d74af2a6dd --- /dev/null +++ b/gui/bweb/tpl/running_job.tpl @@ -0,0 +1,95 @@ +
+
+

Running Jobs

+
+
+
+
+
+ + +
+ +
+ + diff --git a/gui/bweb/tpl/scheduled_job.tpl b/gui/bweb/tpl/scheduled_job.tpl new file mode 100644 index 0000000000..7dc63d2a5d --- /dev/null +++ b/gui/bweb/tpl/scheduled_job.tpl @@ -0,0 +1,76 @@ +
+
+

Next Jobs

+
+
+
+
+ + + +
+
+ + diff --git a/gui/bweb/tpl/update_location.tpl b/gui/bweb/tpl/update_location.tpl new file mode 100644 index 0000000000..99c3b33920 --- /dev/null +++ b/gui/bweb/tpl/update_location.tpl @@ -0,0 +1,67 @@ +
+
+

Update media location

+
+
+
+
+ New location : + +
+
+ + diff --git a/gui/bweb/tpl/update_media.tpl b/gui/bweb/tpl/update_media.tpl new file mode 100644 index 0000000000..7038f095f2 --- /dev/null +++ b/gui/bweb/tpl/update_media.tpl @@ -0,0 +1,128 @@ +
+
+

Update media

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Volume Name: +
Pool: +
Status: +
Slot: + +
InChanger Flag: + checked> +
Location : +
Retention period: + +
Use duration: + +
Max Jobs: + +
Max Files: + +
Max Bytes: + +
+ + + + +
+
+ + \ No newline at end of file