Group
Extension

Log-Saftpresse/lib/Log/Saftpresse/CountersOutput/Html.pm

package Log::Saftpresse::CountersOutput::Html;

use Moose;

# ABSTRACT: plugin to output counters in HTML report
our $VERSION = '1.6'; # VERSION

extends 'Log::Saftpresse::CountersOutput';

use Log::Saftpresse::Utils qw( adj_int_units get_smh);

use JSON;
use Time::Piece;
use Template;

use Template::Stash;

$Template::Stash::LIST_OPS->{ type } = sub { return 'list'; };
$Template::Stash::SCALAR_OPS->{ type } = sub { return 'scalar'; };
$Template::Stash::HASH_OPS->{ type } = sub { return 'hash'; };

sub version {
	my $version;
  {
    ## no critic
		no strict 'vars'; # is only declared in build
		$version = defined $VERSION ? $VERSION : '(git checkout)';
	}
	return( $version );
}

sub tt {
	my $self = shift;
	if( ! defined $self->{_tt} ) {
		my $tt = Template->new(
			ABSOLUTE => 1,
			EVAL_PERL => 1,
		);

 		my $code = $self->template_content;
		# create a parsed object of our main template
		my $doc = $tt->template( \$code );
		my $blocks = $doc->blocks();
		my $ctx = $tt->context;

		# copy all defined blocks over to the global context
		foreach my $block ( keys %$blocks ) {
			$ctx->define_block( $block, $blocks->{$block} );
		}

		$self->{_tt} = $tt;
	}
	return( $self->{_tt} );
}

sub json {
	my $self = shift;
	if( ! defined $self->{_json} ) {
		$self->{_json} = JSON->new->pretty->utf8;
	}
	return( $self->{_json} );
}

sub template_content {
	my $self = shift;
	my $c = '';
	my $h;

	if( defined $self->{'_template_content'}) {
		return( $self->{'_template_content'} );
	}

	if( defined $self->{'template_file'} ) {
		$h = IO::File->new($self->{'template_file'}, 'r')
			or die('error opening output template: '.$!);
	} else {
		$h = IO::Handle->new_from_fd(*DATA,'r')
			or die('error reading default template from __DATA__: '.$!);
	}
	while ( my $line = $h->getline ) {
		$c .= $line;
	}
	$h->close;
	return( $self->{'_template_content'} = $c );
}

sub process {
	my ( $self, $block, %vars ) = @_;
	my $buf;
	$vars{'self'} = $self;
	my $tt = $self->tt;

	# create a wrapper script and execute it
	my $eval = "[% INCLUDE $block -%]\n";
	$tt->process( \$eval, \%vars, \$buf)
		or die( $tt->error );

	return $buf;
}

sub title {
	my $self = shift;
	return( "Postfix log summaries generated on ".Time::Piece->new->ymd );
}

sub output {
	my ( $self, $cnt ) = @_;

	print $self->process('header');
	
	$self->print_totals( $cnt );

	if( defined $cnt->{'PostfixSmtpdStats'} ) {
		$self->print_smtpd_stats( $cnt->{'PostfixSmtpdStats'} );
	}
	
	# TODO: fix problem report output
	#$self->print_problems_reports( $cnt );

	$self->print_traffic_summaries( $cnt );

	if( defined $self->{'top_domains_cnt'}
			&& $self->{'top_domains_cnt'} != 0 ) {
		$self->print_domain_summaries( $cnt );
	}

	if( defined $cnt->{'PostfixSmtpdStats'} ) {
		$self->print_smtpd_summaries( $cnt );
	}

	$self->print_user_summaries( $cnt );

	# TODO: restore Message detail of pflogsumm?

	if( defined $cnt->{'TlsStatistics'} ) {
		$self->print_tls_stats( $cnt );
	}
	if( defined $cnt->{'PostfixGeoStats'} ) {
		$self->print_geo_stats( $cnt->{'PostfixGeoStats'} );
	}

	print $self->process('footer');

	return;
}

sub print_user_summaries {
	my ( $self, $cnt ) = @_;
	my $delivered = $cnt->{'PostfixDelivered'};

	my @tables = (
		[ "Senders by message count" => 'sender',
			0, 'recieved', 'by_sender' ],
		[ "Recipients by message count" => 'recipient',
			0, 'sent', 'by_rcpt' ],
		[ "Senders by message size" => 'sender',
			0, 'recieved', 'size', 'by_sender' ],
		[ "Recipients by message size" => 'recipient',
			0, 'sent', 'size', 'by_rcpt' ],
	);

	foreach my $table ( @tables ) {
		my ( $title, $legend, $total, @node ) = @$table;
		my $values = $delivered->get_node(@node);
		if( ! defined $values ) { next; }
		print $self->hash_top_values( $values,
			title => $title,
			total => $total,
			legend => $legend,
			unit => $title =~ /size$/ ? 'byte' : 'count',
		);
	}

	return;
}

sub print_totals {
	my ( $self, $cnt ) = @_;
	my $reject_cnt = $cnt->{'PostfixRejects'};
	my $recieved_cnt = $cnt->{'PostfixRecieved'};
	my $delivered_cnt = $cnt->{'PostfixDelivered'};
	my $smtpdConnCnt = 0;

	# PostfixRejects
	my $msgsRjctd = $reject_cnt->get_value_or_zero('total', 'reject');
	my $msgsDscrdd = $reject_cnt->get_value_or_zero('total', 'discard');
	my $msgsWrnd = $reject_cnt->get_value_or_zero('total', 'warning');
	my $msgsHld = $reject_cnt->get_value_or_zero('total', 'hold');

	# PostfixRecieved
	my $msgsRcvd = $recieved_cnt->get_value_or_zero('total');

	my $msgsDlvrd = $delivered_cnt->get_value_or_zero('sent', 'total');
	my $msgsDfrd = $delivered_cnt->get_value_or_zero('deferred', 'total');
	my $msgsFwdd = $delivered_cnt->get_value_or_zero('forwarded');
	my $msgsBncd = $delivered_cnt->get_value_or_zero('bounced', 'total');

	my $sizeRcvd = $delivered_cnt->get_value_or_zero('recieved', 'size', 'total');
	my $sizeDlvrd = $delivered_cnt->get_value_or_zero('sent', 'size', 'total');

	my $sendgUserCnt = $delivered_cnt->get_key_count('recieved', 'by_sender');
	my $sendgDomCnt = $delivered_cnt->get_key_count('recieved', 'by_domain'); 
	my $recipUserCnt =$delivered_cnt->get_key_count('sent', 'by_rcpt');
	my $recipDomCnt = $delivered_cnt->get_key_count('sent', 'by_domain');

	my $msgsTotal = $msgsDlvrd + $msgsRjctd + $msgsDscrdd;

	print $self->headline(1, 'Grand Totals');

	print $self->key_value_table( "Messages", [
		[ 'Received', $msgsRcvd ],
		[ 'Delivered', $msgsDlvrd ],
		[ 'Forwarded', $msgsFwdd ],
		[ 'Deferred', $msgsDfrd ],
	] );

	print $self->key_value_table( "Rejects", [
		[ 'Bounced', $msgsBncd ],
		[ 'Rejected', $msgsRjctd, 'count', $msgsTotal ],
		[ 'Rejected Warnings', $msgsWrnd ],
		[ 'Held', $msgsHld ],
		[ 'discarded', $msgsDscrdd, 'count', $msgsTotal ],
	] );

	print $self->key_value_table( "Traffic Volume", [
		[ 'Bytes recieved', $sizeRcvd, 'byte' ],
		[ 'Bytes delivered', $sizeDlvrd, 'byte' ],
		[ 'Senders', $sendgUserCnt ],
		[ 'Sending hosts/domains', $sendgDomCnt ],
		[ 'Recipients', $recipUserCnt ],
		[ 'Recipients hosts/domains', $recipDomCnt ],
	] );

	return;
}

sub print_smtpd_stats {
	my ( $self, $cnt ) = @_;
	my $connections = $cnt->get_value_or_zero('total');
	my $hosts_domains = int(keys %{$cnt->get_node('per_domain')});
	my $avg_conn_time = $connections > 0 ?
		($cnt->get_value_or_zero('busy', 'total')
			/ $connections ) + .5 : 0;
	my $total_conn_time = $cnt->get_value_or_zero('busy', 'total');

	print $self->headline(1, 'Smtpd Statistics');

	print $self->key_value_table( "Connections", [
		[ 'Connections', $connections ],
		[ 'Hosts/domains', $hosts_domains ],
		[ 'Avg. connect time', $avg_conn_time ],
		[ 'total connect time', $total_conn_time, 'interval' ],
	] );
	return;
}

sub print_smtpd_summaries {
	my ( $self, $cnt ) = @_;
	my $smtpd_stats = $cnt->{'PostfixSmtpdStats'};
	my $params = {
		'day' => [ 'Per-Day', 'per_day', 'string' ],
		'hour' => [ 'Per-Hour', 'per_hr', 'decimal' ],
		'domain' => [ 'Per-Domain', 'per_domain', [ 'connections', 'decimal', 20 ] ],
	};

	foreach my $table ( 'day', 'hour', 'domain' ) {
		my ( $title, $key, $sort ) = @{$params->{ $table }};
		print $self->headline(1, "$title SMTPD Connection Summary");
		print $self->statistics_from_hashes(
			legend => $table,
			sort => $sort,
			rows => [
				[ 'connections', $smtpd_stats->get_node($key) ],
				[ 'time conn.', $smtpd_stats->get_node('busy', $key) ],
				[ 'avg./conn.', $self->hash_calc_avg( 2,
						$smtpd_stats->get_node('busy', $key),
						$smtpd_stats->get_node($key),
					), ],
				[ 'max. time', $smtpd_stats->get_node('busy', 'max_'.$key ), ],
			],
		);
	}
	return;
}

sub print_domain_summaries {
	my ( $self, $cnt ) = @_;
	my $top_cnt = $self->{'top_domains_cnt'};
	$top_cnt = defined $top_cnt && $top_cnt >= 0 ?
		$self->{'top_domains_cnt'} : 20;
	my $delivered = $cnt->{'PostfixDelivered'};

	foreach my $table ( 'sent', 'recieved' ) {
		print $self->headline(1, "Host/Domain Summary: Message Delivery (top $top_cnt $table)");
		print $self->statistics_from_hashes(
			legend => 'host/domain',
			sort => [ 'sent cnt', 'decimal', $top_cnt ],
			rows => [
				[ 'sent cnt', $delivered->get_node($table, 'by_domain') ],
				[ 'bytes', $delivered->get_node($table, 'size', 'by_domain') ],
				$table eq 'sent' ? (
					# TODO
					#[ 'defers', $delivered->get_node('busy', 'per_day') ],
					[ 'avg delay', $self->hash_calc_avg( 2,
							$delivered->get_node($table, 'delay', 'by_domain'),
							$delivered->get_node($table, 'by_domain'),
						), ],
					[ 'max. delay', $delivered->get_node($table, 'max_delay', 'by_domain'), ],
				) : (),
			],
		);
	}

	return;
}
sub print_geo_stats {
	my ( $self, $cnt ) = @_;
	my $client = $cnt->get_node('client');
	if( defined $client ) {
    		print $self->hash_top_values(
			$client,
			title => 'Client Countries',
			count => 0,
			legend => 'Country',
		);
	}
	return;
}

sub print_tls_stats {
	my ( $self, $cnt ) = @_;
	my $tls_cnt = $cnt->{'TlsStatistics'};
	my $smtpd_cnt = $cnt->{'PostfixSmtpdStats'};
	my $recieved_cnt = $cnt->{'PostfixRecieved'};
	my $delivered_cnt = $cnt->{'PostfixDelivered'};
	my $smtpdConnCnt;

	if( defined $smtpd_cnt ) {
		$smtpdConnCnt = $smtpd_cnt->get_value_or_zero('total');
	}
	my $msgs_rcvd = $recieved_cnt->get_value_or_zero('total');
	my $msgs_sent = $delivered_cnt->get_value_or_zero('sent', 'total');

	print $self->headline(1, "TLS Statistics");

	print $self->key_value_table( "Total", [
		[ 'Incoming TLS connections',
			$tls_cnt->get('smtpd', 'connections', 'total'),
			'count', $smtpdConnCnt ],
		[ 'Incoming TLS messages',
			$tls_cnt->get('smtpd', 'messages', 'total'),
			'count', $msgs_rcvd ],
		[ 'Outgoing TLS connections',
			$tls_cnt->get('smtp', 'connections', 'total'),
			'count', $smtpdConnCnt ],
		[ 'Outgoing TLS messages',
			$tls_cnt->get('smtp', 'messages', 'total'),
			'count', $msgs_sent ],
	] );

	my @tls_statistics = (
		[ "Incoming TLS trust-level" => 'trust-level',
			$smtpdConnCnt, 'smtpd', 'connections', 'level' ],
		[ "Outgoing TLS trust-level" => 'trust-level',
			0, 'smtp', 'connections', 'level' ],
		[ "Incoming TLS Protocol Version" => 'protocol version',
			$smtpdConnCnt, 'smtpd', 'connections', 'protocol' ],
		[ "Outgoing TLS Protocol Version" => 'protocol version',
			0, 'smtp', 'connections', 'protocol' ],
		[ "Incoming TLS key length" => 'key length',
			$smtpdConnCnt, 'smtpd', 'connections', 'keylen' ],
		[ "Outgoing TLS key length" => 'key length',
			0, 'smtp', 'connections', 'keylen' ],
		[ "Incoming TLS Ciphers" => 'cipher',
			$smtpdConnCnt, 'smtpd', 'connections', 'cipher' ],
		[ "Outgoing TLS Ciphers" => 'cipher',
			0, 'smtp', 'connections', 'cipher' ],
	);

	foreach my $tls_stat ( @tls_statistics ) {
		my ( $title, $legend, $total, @node ) = @$tls_stat;
		my $values = $tls_cnt->get_node(@node);
		if( ! defined $values ) { next; }
		print $self->hash_top_values( $values,
			title => $title,
			total => $total,
			legend => $legend,
		);
	}
}

sub print_problems_reports {
	my ( $self, $cnt ) = @_;

	my $delivered_cnt = $cnt->{'PostfixDelivered'};
	my $reject_cnt = $cnt->{'PostfixRejects'};

	if($self->{'deferral_detail'} != 0) {
		print $self->nested_top_values(
			$delivered_cnt->get_node('deferred'),
			title => "message deferral detail",
			count => $self->{'deferral_detail'} );
	}
	if($self->{'bounce_detail'} != 0) {
		print $self->nested_top_values(
			$delivered_cnt->get_node('bounced'),
			title => "message bounce detail (by relay)",
			count => $self->{'bounce_detail'} );
	}
	if($self->{'reject_detail'} != 0) {
		foreach my $key ( 'reject', 'warning', 'hold', 'discard') {
			print $self->nested_top_values(
				$reject_cnt->get_node($key),
				title => "message $key detail",
				count => $self->{'reject_detail'} );
		}
	}

	if( my $smtp_cnt = $cnt->{'PostfixSmtp'} ) {
		my $messages = $smtp_cnt->get_node('messages');
		if( defined $messages ) {
			print $self->nested_top_values($messages,
				title => "smtp delivery failures",
				count => $self->{'smtp_detail'} );
		}
	}
	if( my $msg_cnt =  $cnt->{'PostfixMessages'} ) {
		if($self->{'smtpd_warn_detail'} != 0) {
			print $self->nested_top_values(
				$msg_cnt->get_node('warning'),
				title => "Warnings",
				count => $self->{'smtpd_warn_detail'} );
		}
		print $self->nested_top_values(
			$msg_cnt->get_node('fatal'),
			title => "Fatal Errors" );
		print $self->nested_top_values(
			$msg_cnt->get_node('panic'),
			title => "Panics" );
		print $self->hash_top_values($msg_cnt->get_node('master'),
			title => "Master daemon messages",
			legend => 'Message',
		);
	}
}

sub print_traffic_summaries {
	my ( $self, $cnt ) = @_;
	my $params = {
		'day' => [ 'Per-Day', 'per_day', 'string' ],
		'hour' => [ 'Per-Hour', 'per_hr', 'decimal' ],
	};

	foreach my $table ('day', 'hour') {
		my ( $title, $key, $sort ) = @{$params->{ $table }};
		print $self->headline(1, 'Traffic Summary ('.$title.')');
		print $self->statistics_from_hashes(
			legend => $table,
			sort => $sort,
			chart => 1,
			rows => [
				[ 'recieved', $cnt->{'PostfixRecieved'}->get_node($key) ],
				[ 'delivered', $cnt->{'PostfixDelivered'}->get_node('sent', $key) ],
				[ 'deffered', $cnt->{'PostfixDelivered'}->get_node('deferred', $key), ],
				[ 'bounced', $cnt->{'PostfixDelivered'}->get_node('bounced', $key), ],
				[ 'rejected', $cnt->{'PostfixRejects'}->get_node($key) ],
			],
		);
	}

	return;
}

sub hash_calc_avg {
	my ( $self, $precision, $total, $count ) = @_;
	my %avg;
	my %uniq = map { $_ => 1 } ( keys %$total, keys %$count );
	my @keys = keys %uniq;
	foreach my $key ( @keys ) {
		my $value;
		if( defined $total->{$key} && $total->{$key} > 0
				&& defined $count->{$key} && $count->{$key} > 0 ) { 
			$value = $total->{$key} / $count->{$key};
		}
		if( defined $total->{$key} && $total->{$key} eq 0 ) {
			$value = 0;
		}
		if( defined $value ) {
			$avg{$key} = sprintf('%.'.$precision.'f', $value);
		} else {
			$avg{$key} = undef;
		}
	}
	return \%avg;
}

sub statistics_from_hashes {
	my ( $self, %params ) = @_;
	my @rows = @{$params{'rows'}};
	my @head = map { $_->[0] } @rows;
	my @hashes = map { $_->[1] } @rows;
	my @yaxis;
	my ( @series, @labeled_rows );

	if( ref($params{'sort'}) eq 'ARRAY' ) { # sort by a column value
		my ( $sortby, $alg, $limit ) = @{$params{'sort'}};
		my ( $row ) = grep { $_->[0] eq $sortby } @rows;
		$row = $row->[1];
		if( ! defined $row ) { die('cant find row '.$sortby.' for sorting'); }
		if( $alg eq 'decimal' ) {
			@yaxis = sort { $row->{$b} <=> $row->{$a} } keys %$row;
		} else { # string
			@yaxis = sort { $row->{$b} cmp $row->{$a} } keys %$row;
		}
		if( $limit > 0 && scalar @yaxis > $limit ) { @yaxis = @yaxis[0 .. ($limit-1) ] };
	} else { # simple sort by key
		my @all_keys = map { keys %$_ } @hashes;
		my %uniq = map { $_ => 1 } @all_keys;
		if( $params{'sort'} eq 'decimal' ) {
			@yaxis = sort { $a <=> $b } keys %uniq;
		} else { # string
			@yaxis = sort { $a cmp $b } keys %uniq;
		}
	}

	foreach my $row ( @yaxis ) {
		push(@labeled_rows, [ $row, map { $_->{$row} } @hashes ] );
	}

	foreach my $row ( @rows ) {
		my $name = $row->[0];
		my $values = $row->[1];
		push(@series, {
			label => $name,
			data => [ map { [
				$_ =~ /\d{4}-\d{2}-\d{2}/ ? 
					Time::Piece->strptime($_, '%Y-%m-%d')->epoch
					 : $_,
				defined $values->{$_} ? $values->{$_} : 0
			] } @yaxis ],
		} );
	}

	my %options = (
		legend => $params{'legend'},
		head => \@head,
		labeled_rows => \@labeled_rows,
		series => \@series,
		yaxis => \@yaxis,
		hashes => \@hashes,
	);

	my $output = '';
	if( defined $params{'chart'} && $params{'chart'} ) {
		$output .= $self->statistics_chart( %options );
	}
	$output .= $self->statistics_table( %options );
	return $output;
}

sub statistics_table {
	my $self = shift;
	return $self->process('statistics_table', @_ );
}

sub statistics_chart {
	my $self = shift;
	return $self->process('statistics_chart', @_ );
}

sub get_element_id {
	my $self = shift;
	if( ! defined $self->{_cur_element_id} ) {
		$self->{_cur_element_id} = 1;
	}
	return $self->{_cur_element_id}++;
}

sub headline {
	my ( $self, $level, $title ) = @_;
	if( ! defined $self->{'_headlines'} ) {
		$self->{'_headlines'} = [];
	}
	my $cur = $self->{'_headlines'};
	my $cur_level = 1;
	my $id = 'title-'.$self->get_element_id;
	while( $cur_level < $level ) {
		if( scalar(@$cur) == 0 || ref($cur->[-1]) ne 'ARRAY' ) {
			push(@$cur, []);
		}
		$cur = $cur->[-1];
		$cur_level++;
	}
	my %headline = (
		title => $title,
		id => $id,
		level => $level,
	);
	push(@$cur, \%headline );
	return $self->process('headline', %headline );
}

sub navigation {
	my ( $self, $depth ) = @_;
	return $self->process('navigation',
		nav => $self->{_headlines},
		depth => $depth,
	);
	return;
}

sub nested_top_values {
	my ( $self, $hash ) = ( shift, shift );
	my %args = (
		'count' => 0,
		'unit' => 'count',
		'legend' => '',
		@_,
	);
	if( ! defined $hash) { return ''; }
	
	return $self->process('nested_values_table',
		%args,
		'data' => $hash,
	);
}

sub hash_top_values {
	my ( $self, $hash ) = ( shift, shift );
	my %args = (
		'count' => 0,
		'unit' => 'count',
		'legend' => '',
		'total' => 0,
		@_,
	);
	if( ! defined $hash) { return ''; }

	my @data = sort { $b->[0] <=> $a->[0] || $b->[1] cmp $a->[1] }
		map { [ $hash->{$_} => $_ ] } keys %$hash;

	return $self->process('top_values_table',
		%args,
		'data' => \@data,
	);
}

sub key_value_table {
	my ( $self, $name, $data ) = @_;
	return $self->process('key_value_table',
		'name' => $name,
		'data' => $data,
	);
}


1;

=pod

=encoding UTF-8

=head1 NAME

Log::Saftpresse::CountersOutput::Html - plugin to output counters in HTML report

=head1 VERSION

version 1.6

=head1 AUTHOR

Markus Benning <ich@markusbenning.de>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 1998 by James S. Seymour, 2015 by Markus Benning.

This is free software, licensed under:

  The GNU General Public License, Version 2, June 1991

=cut

__DATA__
[% BLOCK header -%]
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="[% self.title %]">

    <title>[% self.title %]</title>

    <link rel="stylesheet" href="https://markusbenning.de/js/bootstrap/css/bootstrap.min.css" />
    <link rel="stylesheet" href="https://markusbenning.de/js/bootstrap/css/bootstrap-theme.min.css" />
    <script src="https://markusbenning.de/js/jquery.min.js"></script>
    <script src="https://markusbenning.de/js/bootstrap/js/bootstrap.min.js"></script>
    <script src="https://markusbenning.de/js/numeral.min.js"></script>
    <script src="https://markusbenning.de/js/flot/jquery.flot.min.js"></script>
    <script>
    $( document ).ready(function() {
      $("span.unit-count").each( function( index ) {
      	$( this ).html( numeral( $(this).text() ).format('0,0') );
      });
      $("span.unit-byte").each(function( index ) {
      	$( this ).html( numeral( $(this).text() ).format('0 b') );
      });
      $("span.unit-percent").each(function( index ) {
      	$( this ).html( numeral( $(this).text() ).format('0.00%') );
      });
      $("span.unit-interval").each(function( index ) {
      	$( this ).html( numeral( $(this).text() ).format('00:00:00') );
      });
    });
    </script>
  </head>

  <body>

    <nav class="navbar navbar-inverse">
      <div class="container-fluid">
        <div class="navbar-header">
          <a class="navbar-brand" href="#">Postfix Statistics</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
          <ul class="nav navbar-nav navbar-right">
            <li><a href="https://markusbenning.de/">saftpresse</a></li>
          </ul>
        </div>
      </div>
    </nav>

    <div class="container-fluid">
      <div class="row">
        <div class="col-md-10 col-md-push-2 main">
	  <h1>[% self.title %]</h1>
	  <p class="lead">generated by saftpresse [% self.version %] log file analyzer</p>
[% END -%]

[% BLOCK footer %]
        </div>
        [% self.navigation(2) %]
      </div>
    </div> <!-- /container -->


  </body>
</html>
[% END %]

[% BLOCK navigation %]
<div class="col-md-2 col-md-pull-10 sidebar">
  <ul class="nav nav-sidebar nav-stacked">
    [% INCLUDE nav_element nav=nav level=1 depth=depth %]
  </ul>
</div>
[% END %]

[% BLOCK nav_element %]
[% FOREACH element = nav -%]
[% IF element.type == 'list' -%]
    [% IF level < depth -%]
    <li><ul class="nav">
    [% INCLUDE nav_element nav=element level=level+1 depth=depth %]
    </ul></li>
    [% END %]
[% ELSE -%]
    <li><a href="#[% element.id %]">[% element.title %]</a></li>
[% END -%]
[% END -%]
[% END %]

[% BLOCK headline %]
<h[% level + 1 %] id="[% id %]">[% title %]</h[% level + 1 %]>
[% END %]

[% BLOCK key_value_table %]
[% self.headline( 2, name ) %]
<table class="table table-striped table-hover">
<thead>
  <tr>
    <th class="col-md-3">Key</th>
    <th>Value</th>
  </tr>
</thead>
<tbody>
[% FOREACH row = data -%]
  <tr>
    <td>[% row.shift %]</td>
    <td>[% INCLUDE format_unit format=row %]</td>
  </tr>
[% END -%]
</tbody>
</table>
[% END %]

[% BLOCK format_unit %]
[% value = format.0; type = format.1 -%]
[% IF ! type ; type = 'count' ; END -%]
[% IF format.2 -%]
[% total = format.2 ; percent = value / total; -%]
<span class="unit-[% type %]">[% value %]</span>
(<span class="unit-percent">[% percent %]</span> of <span class="unit-[% type %]">[% total %]</span>)
[% ELSE -%]
<span class="unit-[% type %]">[% value %]</span>
[% END -%]
[% END %]

[% BLOCK top_values_table %]
[% IF title ; self.headline( 2, title ) ; END -%]
<table class="table table-striped table-hover[% IF compact %] table-condensed[% END %]">
<thead>
  <tr>
    <th class="col-md-3">Count</th>
    <th>[% legend %]</th>
  </tr>
</thead>
<tbody>
[% FOREACH row = data -%]
  <tr>
    <td>[% INCLUDE format_unit format=[ row.0, unit, total ] %]</td>
    <td>[% row.1 %]</td>
  </tr>
[% END -%]
</tbody>
</table>
[% END %]

[% BLOCK nested_values_table %]
[% self.headline( 2, title ) %]
[% FOREACH section = data -%]
[% IF title ; self.headline( 3, section.key ) ; END %]
<div class="panel-group">
[% FOREACH panel = section.value -%]
<div class="panel panel-default">
<div class="panel-heading">[% panel.key %]</div>
<div class="panel-body">
[% IF panel.value.values.0.type == 'scalar' -%]
[% self.hash_top_values(panel.value, 'title', '', 'compact', 1) -%]
[% ELSE -%]
[% INCLUDE nested_values_table data=panel.value title=undef -%]
[% END -%]
</div>
</div>
[% END %]
</div>
[% END %]
[% END %]

[% BLOCK statistics_table %]
<table class="table table-striped table-hover table-condensed">
  <thead>
    <tr>
    <th>[% legend %]</th>
    [% FOREACH th = head -%]
    <th>[% th %]</th>
    [% END -%]
    </tr>
  </thead>
  <tbody>
  [% FOREACH row = labeled_rows -%]
  <tr>
    [% FOREACH td = row -%]
    <td>[% td != '' ? td : '-' %]</td>
    [% END -%]
  </tr>
  [% END -%]
  </tbody>
</table>
[% END %]

[% BLOCK statistics_chart %]
[% chartid = 'chart-' _ self.get_element_id -%]
<div id="[% chartid %]" style="width:100%;height:300px"></div>
<script>
$( document ).ready(function() {
	var data = [% self.json.encode( series ) %];
	var options = {
		series: {
			stack: 1,
			lines: {
				show: true,
			},
			points: {
				show: true,
			},
			grid: {
				hoverable: true,
				clickable: true
			}
		}
	};
	$("#[% chartid %]").plot(data, options);
});
</script>
[% END %]


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.