"
85 | ]
86 | }
87 |
--------------------------------------------------------------------------------
/t/HRForecast/Calculator/02_calculate.t:
--------------------------------------------------------------------------------
1 | use strict;
2 | use warnings;
3 | use Test::More;
4 | use Test::MockObject;
5 | use Time::Piece;
6 | use Time::Seconds;
7 | use HRForecast;
8 | use HRForecast::Calculator;
9 | use Data::Dumper;
10 |
11 | my $calculator = HRForecast::Calculator->new();
12 |
13 | subtest 'calculate(identity)' => sub {
14 | my $from = Time::Piece->strptime('2015-10-30', '%Y-%m-%d');
15 | my $to = Time::Piece->strptime('2015-11-03', '%Y-%m-%d');
16 |
17 | my @rows_of_get_data = ();
18 | for (my $i = 0; $i < 5; $i++) {
19 | push @rows_of_get_data, {metrics_id=>1, datetime=>$from + ONE_DAY * $i, number=>10};
20 | push @rows_of_get_data, {metrics_id=>2, datetime=>$from + ONE_DAY * $i, number=>20};
21 | }
22 |
23 | my $data = Test::MockObject->new;
24 | $data->mock('get_data' => sub {
25 | my ($self, $id, $from, $to) = @_;
26 | is(123, $id);
27 | is('2015-10-30', $from->ymd);
28 | is('2015-11-03', $to->ymd);
29 | return \@rows_of_get_data;
30 | });
31 |
32 | my $expected = \@rows_of_get_data;
33 | my $actual = $calculator->calculate($data, 123, $from, $to, '');
34 | is_deeply($actual, $expected);
35 | };
36 |
37 | subtest 'calculate(runningtotal_by_month)' => sub {
38 | my $from = Time::Piece->strptime('2015-10-30', '%Y-%m-%d');
39 | my $to = Time::Piece->strptime('2015-11-03', '%Y-%m-%d');
40 |
41 | my @rows_of_get_data = ();
42 | for (my $i = -31; $i < 5; $i++) {
43 | push @rows_of_get_data, {metrics_id=>1, datetime=>$from + ONE_DAY * $i, number=>10};
44 | push @rows_of_get_data, {metrics_id=>2, datetime=>$from + ONE_DAY * $i, number=>20};
45 | }
46 |
47 | my $data = Test::MockObject->new;
48 | $data->mock('get_data' => sub {
49 | my ($self, $id, $from, $to) = @_;
50 | is(123, $id);
51 | is('2015-09-29', $from->ymd);
52 | is('2015-11-03', $to->ymd);
53 | return \@rows_of_get_data;
54 | });
55 |
56 | my $expected = [
57 | {metrics_id=>1, datetime=>Time::Piece->strptime('2015-10-30', '%Y-%m-%d'), number=> 300},
58 | {metrics_id=>2, datetime=>Time::Piece->strptime('2015-10-30', '%Y-%m-%d'), number=> 600},
59 | {metrics_id=>1, datetime=>Time::Piece->strptime('2015-10-31', '%Y-%m-%d'), number=> 310},
60 | {metrics_id=>2, datetime=>Time::Piece->strptime('2015-10-31', '%Y-%m-%d'), number=> 620},
61 | {metrics_id=>1, datetime=>Time::Piece->strptime('2015-11-01', '%Y-%m-%d'), number=> 10},
62 | {metrics_id=>2, datetime=>Time::Piece->strptime('2015-11-01', '%Y-%m-%d'), number=> 20},
63 | {metrics_id=>1, datetime=>Time::Piece->strptime('2015-11-02', '%Y-%m-%d'), number=> 20},
64 | {metrics_id=>2, datetime=>Time::Piece->strptime('2015-11-02', '%Y-%m-%d'), number=> 40},
65 | {metrics_id=>1, datetime=>Time::Piece->strptime('2015-11-03', '%Y-%m-%d'), number=> 30},
66 | {metrics_id=>2, datetime=>Time::Piece->strptime('2015-11-03', '%Y-%m-%d'), number=> 60},
67 | ];
68 |
69 | my $actual = $calculator->calculate($data, 123, $from, $to, 'runningtotal_by_month');
70 | is_deeply($actual, $expected);
71 | };
72 |
73 | done_testing;
74 |
75 |
76 |
--------------------------------------------------------------------------------
/views/edit.tx:
--------------------------------------------------------------------------------
1 | : cascade base
2 |
3 | : around additonal_meta -> {
4 |
5 | : }
6 |
7 | : around content -> {
8 | グラフ設定変更
9 |
10 | : block form | fillinform( $stash.metrics ) -> {
11 |
90 | : } #fillin
91 |
92 | : }
93 |
--------------------------------------------------------------------------------
/views/view_complex.tx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
36 |
37 |
38 |
42 |
43 |
44 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/public/js/jquery.cookie.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * jQuery Cookie Plugin v1.4.0
3 | * https://github.com/carhartl/jquery-cookie
4 | *
5 | * Copyright 2013 Klaus Hartl
6 | * Released under the MIT license
7 | */
8 | (function (factory) {
9 | if (typeof define === 'function' && define.amd) {
10 | // AMD. Register as anonymous module.
11 | define(['jquery'], factory);
12 | } else {
13 | // Browser globals.
14 | factory(jQuery);
15 | }
16 | }(function ($) {
17 |
18 | var pluses = /\+/g;
19 |
20 | function encode(s) {
21 | return config.raw ? s : encodeURIComponent(s);
22 | }
23 |
24 | function decode(s) {
25 | return config.raw ? s : decodeURIComponent(s);
26 | }
27 |
28 | function stringifyCookieValue(value) {
29 | return encode(config.json ? JSON.stringify(value) : String(value));
30 | }
31 |
32 | function parseCookieValue(s) {
33 | if (s.indexOf('"') === 0) {
34 | // This is a quoted cookie as according to RFC2068, unescape...
35 | s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
36 | }
37 |
38 | try {
39 | // Replace server-side written pluses with spaces.
40 | // If we can't decode the cookie, ignore it, it's unusable.
41 | // If we can't parse the cookie, ignore it, it's unusable.
42 | s = decodeURIComponent(s.replace(pluses, ' '));
43 | return config.json ? JSON.parse(s) : s;
44 | } catch(e) {}
45 | }
46 |
47 | function read(s, converter) {
48 | var value = config.raw ? s : parseCookieValue(s);
49 | return $.isFunction(converter) ? converter(value) : value;
50 | }
51 |
52 | var config = $.cookie = function (key, value, options) {
53 |
54 | // Write
55 | if (value !== undefined && !$.isFunction(value)) {
56 | options = $.extend({}, config.defaults, options);
57 |
58 | if (typeof options.expires === 'number') {
59 | var days = options.expires, t = options.expires = new Date();
60 | t.setDate(t.getDate() + days);
61 | }
62 |
63 | return (document.cookie = [
64 | encode(key), '=', stringifyCookieValue(value),
65 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
66 | options.path ? '; path=' + options.path : '',
67 | options.domain ? '; domain=' + options.domain : '',
68 | options.secure ? '; secure' : ''
69 | ].join(''));
70 | }
71 |
72 | // Read
73 |
74 | var result = key ? undefined : {};
75 |
76 | // To prevent the for loop in the first place assign an empty array
77 | // in case there are no cookies at all. Also prevents odd result when
78 | // calling $.cookie().
79 | var cookies = document.cookie ? document.cookie.split('; ') : [];
80 |
81 | for (var i = 0, l = cookies.length; i < l; i++) {
82 | var parts = cookies[i].split('=');
83 | var name = decode(parts.shift());
84 | var cookie = parts.join('=');
85 |
86 | if (key && key === name) {
87 | // If second argument (value) is a function it's a converter...
88 | result = read(cookie, value);
89 | break;
90 | }
91 |
92 | // Prevent storing a cookie that we couldn't decode.
93 | if (!key && (cookie = read(cookie)) !== undefined) {
94 | result[name] = cookie;
95 | }
96 | }
97 |
98 | return result;
99 | };
100 |
101 | config.defaults = {};
102 |
103 | $.removeCookie = function (key, options) {
104 | if ($.cookie(key) === undefined) {
105 | return false;
106 | }
107 |
108 | // Must not alter options, thus extending a fresh object...
109 | $.cookie(key, '', $.extend({}, options, { expires: -1 }));
110 | return !$.cookie(key);
111 | };
112 |
113 | }));
114 |
--------------------------------------------------------------------------------
/views/view.tx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
36 |
37 |
38 |
42 |
43 |
44 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/lib/HRForecast/Calculator.pm:
--------------------------------------------------------------------------------
1 | package HRForecast::Calculator;
2 |
3 | use strict;
4 | use warnings;
5 | use utf8;
6 | use HTTP::Date;
7 | use Time::Piece;
8 | use Time::Seconds;
9 | use Data::Dumper;
10 |
11 | use constant CALCULATIONS => [
12 | {function=>'', name=>'———', minus_days_of_from => 0},
13 | {function=>'runningtotal', name=>'累計', minus_days_of_from => 0},
14 | {function=>'runningtotal_by_month', name=>'累計(月別)', minus_days_of_from => 31},
15 | {function=>'difference', name=>'差分', minus_days_of_from => 1},
16 | {function=>'difference_plus', name=>'差分(増加のみ)', minus_days_of_from => 1},
17 | ];
18 |
19 | sub new {
20 | my $class = shift;
21 | bless {}, $class;
22 | }
23 |
24 | sub calculate {
25 | my ($self, $data, $id, $from, $to, $calculation) = @_;
26 |
27 | my $minus_days_of_from = $self->find_minus_days_of_from($calculation);
28 | my $from_extended = $from - ONE_DAY * $minus_days_of_from;
29 |
30 | my ($rows, $opt) = $data->get_data($id, $from_extended, $to);
31 |
32 | my $function_name = 'calculation_' . $calculation;
33 | my $rows_calculated = $self->$function_name($rows);
34 | my $rows_filtered = $self->filter_by_from($rows_calculated, $from);
35 |
36 | return $rows_filtered;
37 | }
38 |
39 | sub find_minus_days_of_from {
40 | my ($self, $calculation) = @_;
41 |
42 | foreach my $c ( @{CALCULATIONS()} ) {
43 | return $c->{minus_days_of_from} if $c->{function} eq $calculation;
44 | }
45 |
46 | return 0;
47 | }
48 |
49 | sub filter_by_from {
50 | my ($self, $rows, $from) = @_;
51 |
52 | my @rows_filtered = grep { $_->{datetime} >= $from } @$rows;
53 | return \@rows_filtered;
54 | }
55 |
56 | sub calculation_ {
57 | my ($self, $rows) = @_;
58 |
59 | return $rows;
60 | }
61 |
62 | sub calculation_runningtotal {
63 | my ($self, $rows) = @_;
64 |
65 | return $self->calculation_runningtotal_by($rows, sub {});
66 | }
67 |
68 | sub calculation_runningtotal_by_month {
69 | my ($self, $rows) = @_;
70 |
71 | return $self->calculation_runningtotal_by($rows, sub {
72 | my ($datetime, $last_datetime) = @_;
73 | my $month = $datetime->strftime("%Y/%m");
74 | my $last_month = $last_datetime->strftime("%Y/%m");
75 | $month ne $last_month;
76 | });
77 | }
78 |
79 | sub calculation_runningtotal_by {
80 | my ($self, $rows, $by_function) = @_;
81 |
82 | my @calculated_rows;
83 | my %number;
84 | my %last_datetime;
85 | my $metrics_id;
86 | foreach my $row ( @$rows ) {
87 | $metrics_id = $row->{metrics_id};
88 | $number{$metrics_id} ||= 0;
89 | my $datetime = $row->{datetime};
90 | if ((exists $last_datetime{$metrics_id}) and $by_function->($datetime, $last_datetime{$metrics_id})) {
91 | $number{$metrics_id} = 0;
92 | }
93 | $number{$metrics_id} += $row->{number};
94 | push @calculated_rows, {
95 | metrics_id => $row->{metrics_id},
96 | datetime => $row->{datetime},
97 | number => $number{$metrics_id}
98 | };
99 | $last_datetime{$metrics_id} = $datetime;
100 | }
101 |
102 | return \@calculated_rows;
103 | }
104 |
105 | sub calculation_difference {
106 | my ($self, $rows) = @_;
107 |
108 | my @calculated_rows;
109 | my $number;
110 | my %last_number;
111 | my $metrics_id;
112 | foreach my $row ( @$rows ) {
113 | $number = $row->{number};
114 | $metrics_id = $row->{metrics_id};
115 | push @calculated_rows, {
116 | metrics_id => $row->{metrics_id},
117 | datetime => $row->{datetime},
118 | number => exists($last_number{$metrics_id}) ? $number - $last_number{$metrics_id} : 0
119 | };
120 | $last_number{$metrics_id} = $number;
121 | }
122 |
123 | return \@calculated_rows;
124 | }
125 |
126 | sub calculation_difference_plus {
127 | my ($self, $rows) = @_;
128 |
129 | $rows = $self->calculation_difference($rows);
130 |
131 | foreach my $row ( @$rows ) {
132 | if ($row->{number} < 0) {
133 | $row->{number} = 0;
134 | }
135 | }
136 |
137 | return $rows;
138 | }
139 |
140 | 1;
141 |
142 |
--------------------------------------------------------------------------------
/views/docs.tx:
--------------------------------------------------------------------------------
1 | : cascade base
2 |
3 | : around additonal_meta -> {
4 |
5 | : }
6 |
7 | : around content -> {
8 | ドキュメント的な。
9 |
10 |
11 |
12 | グラフの登録方法
13 |
14 | 以下の URL を POST メソッドで叩いてください。
15 |
16 |
17 | <: $c.req.uri_for('/') :>api/:service_name/:section_name/:graph_name
18 |
19 |
20 | HRForecast は、多数のサービスで利用可能な共通 Web Graph API を目標として作られています。 URL 中の各名前に関しては下の表を参考にしてください。
21 |
22 |
23 |
24 | 例中の名前
25 | 役割
26 | 具体例を , 区切りで
27 |
28 |
29 | :service_name
30 | グラフを取りたいサービスの名前
31 | hatenablog, ficia, loctouch, ninjyatoriai
32 |
33 |
34 | :section_name
35 | そのサービスの中での、グラフを取る対象が属してる機能やシステム名
36 | entry, user, spot, items
37 |
38 |
39 | :graph_name
40 | 具体的に何のグラフか
41 | total_entry, kakin_user, muryo_user, syuriken_no_ureta_kazu
42 |
43 |
44 |
45 | もし、忍者取り合いっていうサービスのアイテムの中の手裏剣が売りたい数だったら
46 |
47 |
48 | <: $c.req.uri_for('/') :>api/ninjyatoriai/items/syuriken_no_ureta_kazu
49 |
50 |
51 | に対して POST します。
52 |
53 | また、 POST する時には以下のパラメータをつけます。
54 |
55 |
56 |
57 | パラメータ
58 | 説明
59 | 必須/オプション
60 |
61 |
62 | number
63 | グラフに与える数値
64 | 必須
65 |
66 |
67 | datetime
68 | 数値に関する日付
69 | サポートするフォーマット
70 |
71 | "Wed, 09 Feb 1994 22:23:32 GMT" -- HTTP format
72 | "Thu Feb 3 17:03:55 GMT 1994" -- ctime(3) format
73 | "Thu Feb 3 00:00:00 1994", -- ANSI C asctime() format
74 | "Tuesday, 08-Feb-94 14:15:29 GMT" -- old rfc850 HTTP format
75 | "Tuesday, 08-Feb-1994 14:15:29 GMT" -- broken rfc850 HTTP format
76 | "03/Feb/1994:17:03:55 -0700" -- common logfile format
77 | "09 Feb 1994 22:23:32 GMT" -- HTTP format (no weekday)
78 | "08-Feb-94 14:15:29 GMT" -- rfc850 format (no weekday)
79 | "08-Feb-1994 14:15:29 GMT" -- broken rfc850 format (no weekday)
80 | "1994-02-03 14:15:29 -0100" -- ISO 8601 format
81 | "1994-02-03 14:15:29" -- zone is optional
82 | "1994-02-03" -- only date
83 | "1994-02-03T14:15:29" -- Use T as separator
84 | "19940203T141529Z" -- ISO 8601 compact format
85 | "19940203" -- only date
86 |
87 |
88 | ただし、1時間未満は切り捨てられます。
89 | 例) 1994-02-03T14:15:29 => 1994-02-03T14:00:00
90 | 必須
91 |
92 |
93 |
94 | LWP::UserAgent を使うと以下の様になります。
95 |
96 |
97 | my $ua = LWP::UserAgent->new;
98 | $ua->post('<: $c.req.uri_for('/') :>api/ninjyatoriai/items/syuriken_no_ureta_kazu', {
99 | number => 10,
100 | datetime => scalar localtime(),
101 | });
102 |
103 |
104 | curl を使うと以下の様になります。
105 |
106 |
107 | $ curl -F number=10 -F datetime=20120206T09:41:31 <: $c.req.uri_for('/') :>api/ninjyatoriai/items/syuriken_no_ureta_kazu
108 |
109 |
110 |
111 |
112 | グラフの表示オプション
113 |
114 | たぶんiframeで呼び出し可能です
115 |
116 |
117 | <iframe src="<: $c.req.uri_for('/') :>ifr/:service_name/:section_name/:graph_name" width="425" height="355" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe>
118 |
119 | <iframe src="<: $c.req.uri_for('/') :>ifr_complex/:service_name/:section_name/:graph_name" width="425" height="355" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe> (複合グラフの場合)
120 |
121 |
122 | srcのURIパラメータとして、以下が利用できます
123 |
124 |
125 |
126 | パラメータ
127 | 説明
128 | 必須/オプション
129 |
130 |
131 |
132 | t
133 | グラフの表示範囲
134 | w => 1週間
135 | m => 1ヶ月
136 | y => 1年
137 | c => 指定
138 |
139 | オプション。デフォルトは「m」
140 |
141 |
142 |
143 | from/to
144 | グラフの表示範囲を「c」にした場合の日付
145 | データ登録APIと同じ日付フォーマットが利用できる
146 |
147 | オプション
148 |
149 |
150 |
151 | calculation
152 | グラフの表示形式
153 | 無指定 => そのまま表示
154 | runningtotal => 累計表示
155 | runningtotal_by_month => 月別累計表示
156 | difference => 差分表示
157 | difference_plus => 増加分のみの差分表示
158 |
159 | オプション
160 |
161 |
162 |
163 |
164 | : }
165 |
--------------------------------------------------------------------------------
/views/base.tx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | : block additonal_meta -> { }
6 |
7 |
8 |
9 | HRForecast
10 |
38 |
39 |
40 |
41 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | : for $c.stash.services -> $service {
69 |
81 | : }
82 |
83 |
84 |
85 |
86 |
87 | : block content -> { }
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/views/ifr_complex.tx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 | HRForecast
15 |
16 |
17 |
18 | : if $valid.valid('graphheader') != 0 {
19 |
20 |
21 |
22 |
47 | : }
48 |
49 |
50 |
51 | : if $valid.valid('graphlabel') != 0 {
52 |
63 | : }
64 |
65 |
66 |
67 |
68 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/views/ifr.tx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 | HRForecast
15 |
16 |
17 |
18 | : if $valid.valid('graphheader') != 0 {
19 |
20 |
21 |
22 |
47 | : }
48 |
49 |
50 |
51 | : if $valid.valid('graphlabel') != 0 {
52 |
61 | : }
62 |
63 |
64 |
65 |
66 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/views/add_complex.tx:
--------------------------------------------------------------------------------
1 | : cascade base
2 |
3 | : around additonal_meta -> {
4 |
5 | : }
6 |
7 | : around content -> {
8 | 複合グラフ追加
9 |
10 |
159 |
160 | : }
161 |
162 |
--------------------------------------------------------------------------------
/t/HRForecast/Calculator/03_calculation.t:
--------------------------------------------------------------------------------
1 | use strict;
2 | use warnings;
3 | use Test::More;
4 | use HRForecast;
5 | use HRForecast::Calculator;
6 |
7 | my $calculator = HRForecast::Calculator->new();
8 |
9 | my $input_rows = [
10 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-10-30', '%Y-%m-%d'), number => 1},
11 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-10-31', '%Y-%m-%d'), number => 10},
12 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-01', '%Y-%m-%d'), number => 100},
13 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-02', '%Y-%m-%d'), number => 10},
14 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-03', '%Y-%m-%d'), number => 1},
15 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-10-30', '%Y-%m-%d'), number => 2},
16 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-10-31', '%Y-%m-%d'), number => 20},
17 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-01', '%Y-%m-%d'), number => 200},
18 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-02', '%Y-%m-%d'), number => 20},
19 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-03', '%Y-%m-%d'), number => 2},
20 | ];
21 |
22 | subtest 'calculation=' => sub {
23 | my $expected = $input_rows;
24 |
25 | my $actual = $calculator->calculation_($input_rows);
26 | is_deeply($actual, $expected);
27 | };
28 |
29 | subtest 'calculation=runningtotal' => sub {
30 | my $expected = [
31 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-10-30', '%Y-%m-%d'), number => 1},
32 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-10-31', '%Y-%m-%d'), number => 11},
33 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-01', '%Y-%m-%d'), number => 111},
34 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-02', '%Y-%m-%d'), number => 121},
35 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-03', '%Y-%m-%d'), number => 122},
36 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-10-30', '%Y-%m-%d'), number => 2},
37 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-10-31', '%Y-%m-%d'), number => 22},
38 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-01', '%Y-%m-%d'), number => 222},
39 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-02', '%Y-%m-%d'), number => 242},
40 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-03', '%Y-%m-%d'), number => 244},
41 | ];
42 |
43 | my $actual = $calculator->calculation_runningtotal($input_rows);
44 | is_deeply($actual, $expected);
45 | };
46 |
47 | subtest 'calculation=runningtotal_by_month' => sub {
48 | my $expected = [
49 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-10-30', '%Y-%m-%d'), number => 1},
50 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-10-31', '%Y-%m-%d'), number => 11},
51 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-01', '%Y-%m-%d'), number => 100}, #月が変わったのでリセット
52 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-02', '%Y-%m-%d'), number => 110},
53 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-03', '%Y-%m-%d'), number => 111},
54 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-10-30', '%Y-%m-%d'), number => 2},
55 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-10-31', '%Y-%m-%d'), number => 22},
56 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-01', '%Y-%m-%d'), number => 200}, #月が変わったのでリセット
57 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-02', '%Y-%m-%d'), number => 220},
58 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-03', '%Y-%m-%d'), number => 222},
59 | ];
60 |
61 | my $actual = $calculator->calculation_runningtotal_by_month($input_rows);
62 | is_deeply($actual, $expected);
63 | };
64 |
65 | subtest 'calculation=difference' => sub {
66 | my $expected = [
67 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-10-30', '%Y-%m-%d'), number => 0},
68 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-10-31', '%Y-%m-%d'), number => 9},
69 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-01', '%Y-%m-%d'), number => 90},
70 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-02', '%Y-%m-%d'), number => -90},
71 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-03', '%Y-%m-%d'), number => -9},
72 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-10-30', '%Y-%m-%d'), number => 0},
73 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-10-31', '%Y-%m-%d'), number => 18},
74 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-01', '%Y-%m-%d'), number => 180},
75 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-02', '%Y-%m-%d'), number =>-180},
76 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-03', '%Y-%m-%d'), number => -18},
77 | ];
78 |
79 | my $actual = $calculator->calculation_difference($input_rows);
80 | is_deeply($actual, $expected);
81 | };
82 |
83 | subtest 'calculation=difference_plus' => sub {
84 | my $expected = [
85 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-10-30', '%Y-%m-%d'), number => 0},
86 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-10-31', '%Y-%m-%d'), number => 9},
87 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-01', '%Y-%m-%d'), number => 90},
88 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-02', '%Y-%m-%d'), number => 0},
89 | {metrics_id => 1 , datetime => Time::Piece->strptime('2015-11-03', '%Y-%m-%d'), number => 0},
90 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-10-30', '%Y-%m-%d'), number => 0},
91 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-10-31', '%Y-%m-%d'), number => 18},
92 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-01', '%Y-%m-%d'), number => 180},
93 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-02', '%Y-%m-%d'), number => 0},
94 | {metrics_id => 2 , datetime => Time::Piece->strptime('2015-11-03', '%Y-%m-%d'), number => 0},
95 | ];
96 |
97 | my $actual = $calculator->calculation_difference_plus($input_rows);
98 | is_deeply($actual, $expected);
99 | };
100 |
101 | done_testing;
102 |
103 |
104 |
--------------------------------------------------------------------------------
/views/edit_complex.tx:
--------------------------------------------------------------------------------
1 | : cascade base
2 |
3 | : around additonal_meta -> {
4 |
5 | : }
6 |
7 | : around content -> {
8 | 複合グラフ編集
9 |
10 |
170 |
171 | : }
172 |
--------------------------------------------------------------------------------
/lib/HRForecast/Data.pm:
--------------------------------------------------------------------------------
1 | package HRForecast::Data;
2 |
3 |
4 | use strict;
5 | use warnings;
6 | use utf8;
7 | use Time::Piece;
8 | use Time::Piece::MySQL;
9 | use JSON qw//;
10 | use Log::Minimal;
11 | use DBIx::Sunny;
12 | use Scope::Container::DBI;
13 | use List::MoreUtils qw/uniq/;
14 | use List::Util qw/first/;
15 |
16 | my $JSON = JSON->new()->ascii(1);
17 | sub encode_json {
18 | $JSON->encode(shift);
19 | }
20 |
21 | sub new {
22 | my $class = shift;
23 | bless {}, $class;
24 | }
25 |
26 | sub dbh {
27 | my $self = shift;
28 | local $Scope::Container::DBI::DBI_CLASS = 'DBIx::Sunny';
29 | Scope::Container::DBI->connect(
30 | HRForecast->config->{dsn},
31 | HRForecast->config->{username},
32 | HRForecast->config->{password}
33 | );
34 | }
35 |
36 | sub round_interval {
37 | HRForecast->config->{round_interval} || 3600;
38 | }
39 |
40 | sub inflate_row {
41 | my ($self, $row) = @_;
42 | $row->{created_at} = Time::Piece->from_mysql_datetime($row->{created_at});
43 | $row->{updated_at} = Time::Piece->from_mysql_timestamp($row->{updated_at});
44 | my $ref = JSON::decode_json($row->{meta}||'{}');
45 | my %result = (
46 | %$ref,
47 | %$row
48 | );
49 | $result{colors} = encode_json([$result{color}]);
50 | \%result
51 | }
52 |
53 | sub inflate_data_row {
54 | my ($self, $row) = @_;
55 | $row->{datetime} = Time::Piece->from_mysql_datetime($row->{datetime});
56 | $row->{updated_at} = Time::Piece->from_mysql_timestamp($row->{updated_at});
57 | my %result = (
58 | %$row
59 | );
60 | \%result
61 | }
62 |
63 | sub inflate_complex_row {
64 | my ($self, $row) = @_;
65 | $row->{created_at} = Time::Piece->from_mysql_datetime($row->{created_at});
66 | $row->{updated_at} = Time::Piece->from_mysql_timestamp($row->{updated_at});
67 | my $ref = JSON::decode_json($row->{meta}||'{}');
68 | $ref->{'path-data'} = [ $ref->{'path-data'} ] if ! ref $ref->{'path-data'};
69 | $ref->{uri} = join ":", @{ $ref->{'path-data'} };
70 | $ref->{complex} = 1;
71 | $ref->{metricses} = [];
72 | for my $metrics_id ( @{ $ref->{'path-data'} } ) {
73 | my $data = $self->get_by_id($metrics_id);
74 | push @{$ref->{metricses}}, $data if $data;
75 | }
76 | $ref->{colors} = encode_json([ map { $_->{color} } @{$ref->{metricses}} ]);
77 | my %result = (
78 | %$ref,
79 | %$row
80 | );
81 | \%result
82 | }
83 |
84 | sub get {
85 | my ($self, $service, $section, $graph) = @_;
86 | my $row = $self->dbh->select_row(
87 | 'SELECT * FROM metrics WHERE service_name = ? AND section_name = ? AND graph_name = ?',
88 | $service, $section, $graph
89 | );
90 | return unless $row;
91 | $self->inflate_row($row);
92 | }
93 |
94 | sub get_by_id {
95 | my ($self, $id) = @_;
96 | my $row = $self->dbh->select_row(
97 | 'SELECT * FROM metrics WHERE id = ?',
98 | $id
99 | );
100 | return unless $row;
101 | $self->inflate_row($row);
102 | }
103 |
104 | sub update {
105 | my ($self, $service, $section, $graph, $number, $timestamp ) = @_;
106 | my $dbh = $self->dbh;
107 | $dbh->begin_work;
108 | my $metrics = $self->get($service, $section, $graph);
109 | if ( ! defined $metrics ) {
110 | my @colors = List::Util::shuffle(qw/33 66 99 cc/);
111 | my $color = '#' . join('', splice(@colors,0,3));
112 | my $meta = encode_json({ color => $color });
113 | $dbh->query(
114 | 'INSERT INTO metrics (service_name, section_name, graph_name, meta, created_at)
115 | VALUES (?,?,?,?,NOW())',
116 | $service, $section, $graph, $meta
117 | );
118 | $metrics = $self->get($service, $section, $graph);
119 | }
120 | $dbh->commit;
121 |
122 | my $fixed_timestamp = $timestamp - ($timestamp % $self->round_interval);
123 | $dbh->query(
124 | 'REPLACE data SET metrics_id = ?, datetime = ?, number = ?',
125 | $metrics->{id}, localtime($fixed_timestamp)->mysql_datetime, $number
126 | );
127 |
128 | 1;
129 | }
130 |
131 | sub update_metrics {
132 | my ($self, $id, $args) = @_;
133 | my @update = map { delete $args->{$_} } qw/service_name section_name graph_name sort/;
134 | my $meta = encode_json($args);
135 | my $dbh = $self->dbh;
136 | $dbh->query(
137 | 'UPDATE metrics SET service_name=?, section_name=?, graph_name=?, sort=?, meta=? WHERE id = ?',
138 | @update, $meta, $id
139 | );
140 | return 1;
141 | }
142 |
143 | sub delete_metrics {
144 | my ($self, $id) = @_;
145 | my $dbh = $self->dbh;
146 | $dbh->begin_work;
147 | my $rows = 1;
148 | while ( $rows > 1 ) {
149 | $rows = $dbh->query('DELETE FROM data WHERE metrics_id = ? LIMIT 1000',$id);
150 | }
151 | $dbh->query('DELETE FROM metrics WHERE id =?',$id);
152 | $dbh->commit;
153 | 1;
154 | }
155 |
156 |
157 | sub get_data {
158 | my ($self, $id, $from, $to) = @_;
159 | my @id = ref $id ? @$id : ($id);
160 | my $rows = $self->dbh->select_all(
161 | 'SELECT * FROM data WHERE metrics_id IN (?) AND (datetime BETWEEN ? AND ?) ORDER BY datetime ASC',
162 | \@id, localtime($from)->mysql_datetime, localtime($to)->mysql_datetime
163 | );
164 | my @ret;
165 | for my $row ( @$rows ) {
166 | push @ret, $self->inflate_data_row($row);
167 | }
168 | return \@ret, {
169 | from => Time::Piece->new($from),
170 | to => Time::Piece->new($to),
171 | };
172 | }
173 |
174 |
175 | sub get_services {
176 | my $self = shift;
177 | my $rows = $self->dbh->select_all(
178 | 'SELECT DISTINCT service_name FROM metrics ORDER BY service_name',
179 | );
180 | my $complex_rows = $self->dbh->select_all(
181 | 'SELECT DISTINCT service_name FROM complex ORDER BY service_name',
182 | );
183 | my @names = uniq map { $_->{service_name} } (@$rows,@$complex_rows);
184 | \@names
185 | }
186 |
187 | sub get_sections {
188 | my $self = shift;
189 | my $service_name = shift;
190 | my $rows = $self->dbh->select_all(
191 | 'SELECT DISTINCT section_name FROM metrics WHERE service_name = ? ORDER BY section_name',
192 | $service_name,
193 | );
194 | my $complex_rows = $self->dbh->select_all(
195 | 'SELECT DISTINCT section_name FROM complex WHERE service_name = ? ORDER BY section_name',
196 | $service_name,
197 | );
198 | my @names = uniq map { $_->{section_name} } (@$rows,@$complex_rows);
199 | \@names;
200 | }
201 |
202 |
203 | sub get_metricses {
204 | my $self = shift;
205 | my ($service_name, $section_name) = @_;
206 | my $rows = $self->dbh->select_all(
207 | 'SELECT * FROM metrics WHERE service_name = ? AND section_name = ? ORDER BY sort DESC, graph_name',
208 | $service_name, $section_name
209 | );
210 | my $complex_rows = $self->dbh->select_all(
211 | 'SELECT * FROM complex WHERE service_name = ? AND section_name = ? ORDER BY sort DESC, graph_name',
212 | $service_name, $section_name
213 | );
214 | my @ret;
215 | for my $row ( @$rows ) {
216 | push @ret, $self->inflate_row($row);
217 | }
218 | for my $row ( @$complex_rows ) {
219 | push @ret, $self->inflate_complex_row($row);
220 | }
221 | @ret = sort { $b->{sort} <=> $a->{sort} } @ret;
222 | \@ret;
223 | }
224 |
225 | sub get_all_metrics_name {
226 | my $self = shift;
227 | $self->dbh->select_all(
228 | 'SELECT id,service_name,section_name,graph_name FROM metrics ORDER BY service_name, section_name, sort DESC, graph_name',
229 | );
230 | }
231 |
232 | sub get_complex {
233 | my ($self, $service, $section, $graph) = @_;
234 | my $row = $self->dbh->select_row(
235 | 'SELECT * FROM complex WHERE service_name = ? AND section_name = ? AND graph_name = ?',
236 | $service, $section, $graph
237 | );
238 | return unless $row;
239 | $self->inflate_complex_row($row);
240 | }
241 |
242 | sub get_complex_by_id {
243 | my ($self, $id) = @_;
244 | my $row = $self->dbh->select_row(
245 | 'SELECT * FROM complex WHERE id = ?',
246 | $id
247 | );
248 | return unless $row;
249 | $self->inflate_complex_row($row);
250 | }
251 |
252 | sub create_complex {
253 | my ($self, $service, $section, $graph, $args) = @_;
254 | my @update = map { delete $args->{$_} } qw/sort/;
255 | my $meta = encode_json($args);
256 | $self->dbh->query(
257 | 'INSERT INTO complex (service_name, section_name, graph_name, sort, meta, created_at)
258 | VALUES (?,?,?,?,?,NOW())',
259 | $service, $section, $graph, @update, $meta
260 | );
261 | $self->get_complex($service, $section, $graph);
262 | }
263 |
264 | sub update_complex {
265 | my ($self, $id, $args) = @_;
266 | my @update = map { delete $args->{$_} } qw/service_name section_name graph_name sort/;
267 | my $meta = encode_json($args);
268 | $self->dbh->query(
269 | 'UPDATE complex SET service_name=?, section_name=?, graph_name=?, sort=?, meta=? WHERE id=?',
270 | @update, $meta, $id
271 | );
272 | }
273 |
274 | sub delete_complex {
275 | my ($self, $id) = @_;
276 | $self->dbh->query(
277 | 'DELETE FROM complex WHERE id=?',
278 | $id
279 | );
280 | }
281 |
282 | 1;
283 |
284 |
285 |
--------------------------------------------------------------------------------
/public/css/bootstrap-theme.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.0.3 (http://getbootstrap.com)
3 | * Copyright 2013 Twitter, Inc.
4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0
5 | */
6 |
7 | .btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe0e0e0',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);background-repeat:repeat-x;border-color:#2b669a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff2d6ca2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);background-repeat:repeat-x;border-color:#3e8f3e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff419641',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);background-repeat:repeat-x;border-color:#e38d13;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffeb9316',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);background-repeat:repeat-x;border-color:#b92c28;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc12e2a',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);background-repeat:repeat-x;border-color:#28a4c9;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2aabd2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff3f3f3',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff282828',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)}
--------------------------------------------------------------------------------
/public/js/site.js:
--------------------------------------------------------------------------------
1 | var suffixes = ['', 'k', 'M', 'G', 'T','P'];
2 | function round(num, places) {
3 | var shift = Math.pow(10, places);
4 | return Math.round(num * shift)/shift;
5 | };
6 | function formatDate(date) {
7 | var yyyy = date.getFullYear();
8 | var mm = ('0' + (date.getMonth() + 1)).slice(-2);
9 | var dd = ('0' + date.getDate()).slice(-2);
10 | var hh = ('0' + date.getHours()).slice(-2);
11 | var ii = ('0' + date.getMinutes()).slice(-2);
12 | var ss = ('0' + date.getSeconds()).slice(-2);
13 | var yyyymmdd = yyyy + '/' + mm + '/' + dd;
14 | var hhii = hh + ':' + ii;
15 | return yyyymmdd + ((hhii == '00:00') ? '' : ' ' + hhii);
16 | }
17 | function formatValue(v) {
18 | if (v < 1000) return v;
19 | var magnitude = Math.floor(String(Math.floor(v)).length / 3);
20 | if (magnitude > suffixes.length - 1)
21 | magnitude = suffixes.length - 1;
22 | return String(round(v / Math.pow(10, magnitude * 3), 2)) +
23 | suffixes[magnitude];
24 | }
25 | function addFigure(str) {
26 | var num = new String(str).replace(/,/g, "");
27 | while(num != (num = num.replace(/^(-?\d+)(\d{3})/, "$1,$2")));
28 | return num;
29 | }
30 | function addFigureVal(str) {
31 | return " "+addFigure(str);
32 | }
33 |
34 | function throttle(callback, wait) {
35 | var timer;
36 | return function() {
37 | if (timer) return;
38 | timer = setTimeout(function() {
39 | timer = null;
40 | callback();
41 | }, wait);
42 | };
43 | }
44 | waitForAppear = (function(){
45 | var jobs = {};
46 | var $window = $(window);
47 | var elementHasAppeared = function(window_top, window_bottom, $element) {
48 | var element_middle = $element.offset().top + $element.height() / 2;
49 | return window_top < element_middle && element_middle < window_bottom;
50 | };
51 | $window.scroll(throttle(function() {
52 | var window_top = $window.scrollTop();
53 | var window_bottom = window_top + $window.height();
54 | $.each(jobs, function(key, pair) {
55 | var $element = pair[0];
56 | var callback = pair[1];
57 | if (elementHasAppeared(window_top, window_bottom, $element)) {
58 | callback();
59 | delete jobs[key];
60 | }
61 | });
62 | }, 200));
63 | return function(key, $element, callback) {
64 | var window_top = $window.scrollTop();
65 | var window_bottom = window_top + $window.height();
66 | if (elementHasAppeared(window_top, window_bottom, $element)) {
67 | setTimeout(callback, 0);
68 | return;
69 | }
70 | jobs[key] = [$element, callback];
71 | };
72 | })();
73 | function loadGraphsLater () {
74 | var element = this;
75 | var $element = $(this);
76 | waitForAppear($element.attr('data-csv'), $element, function() {
77 | loadGraphs.apply(element);
78 | });
79 | }
80 | function loadGraphs () {
81 | var gdiv = $(this);
82 | var limit = 8;
83 | var tooltip = $('#tooltip');
84 | if (tooltip.size() == 0) {
85 | tooltip = $('
');
86 | $(document.body).append(tooltip);
87 | }
88 |
89 | $('#'+'label-'+gdiv.data('index')).removeClass('dygraph-closest-legend');
90 | $('#'+'label-'+gdiv.data('index')).removeClass('dygraph-highlighted-legend');
91 | if ( gdiv.data('colors').length > limit ) {
92 | $('#'+'label-'+gdiv.data('index')).addClass('dygraph-closest-legend');
93 | } else if ( gdiv.data('colors').length > 1 ) {
94 | $('#'+'label-'+gdiv.data('index')).addClass('dygraph-highlighted-legend');
95 | }
96 | $('#onmouse-'+gdiv.data('index')).hide();
97 | var g = new Dygraph(
98 | gdiv.context,
99 | gdiv.data('csv'),
100 | {
101 | includeZero: true,
102 | dateWindow: [ Date.parse(gdiv.data('datewindow')[0]),Date.parse(gdiv.data('datewindow')[1]) ],
103 | colors: gdiv.data('colors'),
104 | stackedGraph: gdiv.data('stack') ? true : false,
105 | drawPoints: false,
106 | strokeWidth: 1,
107 | strokeBorderWidth: gdiv.data('colors').length > limit ? 1 : null,
108 | highlightCircleSize: 3,
109 | highlightSeriesBackgroundAlpha: gdiv.data('colors').length > limit ? 0.5 : 1,
110 | highlightSeriesOpts: gdiv.data('colors').length > limit ? {
111 | strokeWidth: 2,
112 | strokeBorderWidth: 1,
113 | highlightCircleSize: 5,
114 | } : {
115 | highlightCircleSize: gdiv.data('colors').length > 1 ? 5 : 3,
116 | },
117 | labelsKMB: true,
118 | labelsDiv: 'onmouse-'+gdiv.data('index'),
119 | labelsSeparateLines: gdiv.data('colors').length > limit ? false : true,
120 | legend: gdiv.data('colors').length > limit ? 'onmouseover' : 'always',
121 | axes: {
122 | x: {
123 | pixelsPerLabel: 28
124 | },
125 | y: {
126 | valueFormatter: addFigureVal
127 | }
128 | },
129 | axisLabelFontSize: 12,
130 | highlightCallback: function(e, x, pts, row, name){
131 | var total = 0;
132 | $('#onmouse-'+gdiv.data('index')).show();
133 | $('#label-'+gdiv.data('index')).hide();
134 | $.each(pts,function(idx,val){
135 | total += val.yval;
136 | });
137 | if ( gdiv.data('stack') ) {
138 | $('#total-'+gdiv.data('index')).html('TOTAL :'+addFigureVal(total));
139 | $('#tooltip .total').text("TOTAL: " + addFigureVal(total));
140 | }
141 | $('#tooltip').show();
142 | $('#tooltip').css({left:e.pageX + 10, top:e.pageY + 10});
143 | $('#tooltip .xval').text(formatDate(new Date(x)) + ':');
144 | for (var i in pts) {
145 | if (pts[i].name == name) {
146 | $('#tooltip .yval').text(name + ': ' + addFigureVal(pts[i].yval));
147 | }
148 | }
149 | },
150 | unhighlightCallback: function(e) {
151 | $('#onmouse-'+gdiv.data('index')).hide();
152 | $('#label-'+gdiv.data('index')).show();
153 | $('#total-'+gdiv.data('index')).html('');
154 | $('#tooltip').hide();
155 | }
156 | }
157 | );
158 | };
159 | function setHxrpost() {
160 | var myform = this;
161 | $(myform).first().prepend('System Error!
');
162 | $(myform).submit(function(){
163 | $(myform).find('.alert-error').hide();
164 | $(myform).find('.validator_message').addClass('hide');
165 | $(myform).find('div.form-group').removeClass('has-error');
166 | $.ajax({
167 | type: 'POST',
168 | url: myform.action,
169 | data: $(myform).serialize(),
170 | success: function(data) {
171 | $(myform).find('.alert-error').hide();
172 | if ( data.error == 0 ) {
173 | location.href = data.location;
174 | }
175 | else {
176 | $.each(data.messages, function (param,message) {
177 | var name = param;
178 | if ( param == 'path-data' ) {
179 | name = 'path-add';
180 | }
181 | var parent = $(myform).find('[name="'+param+'"]').parents('div.form-group').first();
182 | parent.find('.validator_message').text(message).removeClass('hide');
183 | parent.addClass('has-error');
184 | });
185 | }
186 | },
187 | error: function() {
188 | $(myform).find('.alert-error').show();
189 | }
190 | });
191 | return false;
192 | });
193 | };
194 |
195 | function setHxrConfirmBtn() {
196 | var mybtn = this;
197 | var modal = $('');
207 | modal.find('h3').text($(mybtn).text());
208 | modal.find('input[type=submit]').attr('value',$(mybtn).text());
209 | modal.find('.modal-body > p').text( $(mybtn).data('confirm') );
210 | modal.find('form').submit(function(){
211 | $.ajax({
212 | type: 'POST',
213 | url: $(mybtn).data('uri'),
214 | data: modal.find('form').serialize(),
215 | success: function(data) {
216 | modal.find('.alert-error').hide();
217 | if ( data.error == 0 ) {
218 | location.href = data.location;
219 | }
220 | },
221 | error: function() {
222 | modal.find('.alert-error').show();
223 | }
224 | });
225 | return false;
226 | });
227 | $(mybtn).click(function(){
228 | modal.modal({
229 | show: true,
230 | });
231 | });
232 | };
233 |
234 | function addNewRow() {
235 | var metrics = $('select[name="path-add"]#select_metrics');
236 | var option = metrics.find('option:selected');
237 | var label = '/'+option.data('parent')+'/'+option.text().replace(/(^\s+)|(\s+$)/g, "");
238 | var tr = $(' ');
239 | tr.append('⬆ ⬇ ');
240 | tr.append(''+label+' ');
241 | tr.append('✖ ');
242 | tr.appendTo($('table#data-tbl'));
243 |
244 | $('#data-tbl').find('tr:last').addClass('can-table-order');
245 | $('#data-tbl').find('span.table-order-up:last').click(tableOrderUp);
246 | $('#data-tbl').find('span.table-order-down:last').click(tableOrderDown);
247 | $('#data-tbl').find('span.table-order-remove:last').click(tableOrderRemove);
248 |
249 | var myform = $(this).parents('form').first();
250 | setTimeout(function(){tablePreview(myform)},10);
251 |
252 | return false;
253 | };
254 |
255 | function tableOrderUp() {
256 | var btn = this;
257 | var mytr = $(this).parents('tr.can-table-order').first();
258 | if ( mytr ) {
259 | var prevtr = mytr.prev('tr.can-table-order');
260 | mytr.insertBefore(prevtr);
261 | }
262 | var myform = $(this).parents('form').first();
263 | setTimeout(function(){tablePreview(myform)},10);
264 | return false;
265 | }
266 |
267 | function tableOrderDown() {
268 | var btn = this;
269 | var mytr = $(this).parents('tr.can-table-order').first();
270 | if ( mytr ) {
271 | var nexttr = mytr.next('tr.can-table-order');
272 | mytr.insertAfter(nexttr);
273 | }
274 | var myform = $(this).parents('form').first();
275 | setTimeout(function(){tablePreview(myform)},10);
276 | return false;
277 | };
278 |
279 | function tableOrderRemove() {
280 | var btn = this;
281 | var mytr = $(this).parents('tr.can-table-order').first();
282 | var myform = $(this).parents('form').first();
283 | setTimeout(function(){tablePreview(myform)},10);
284 | mytr.detach();
285 | };
286 |
287 | function tablePreview(myform) {
288 | var num = myform.find('input[name="path-data"]').length;
289 | var uri = $('#complex-preview').data('base');
290 | var data = new Array();
291 | myform.find('input[name="path-data"]').each(function(){ data.push($(this).val()) });
292 | uri += data.join(':');
293 | uri += '?stack=' + myform.find('select[name="stack"]').val();
294 | console.log(uri);
295 | $('#complex-preview').attr('src',uri);
296 | };
297 |
298 | function setTablePreview() {
299 | var myform = $(this);
300 | $('#data-tbl').find('span.table-order-up').click(tableOrderUp);
301 | $('#data-tbl').find('span.table-order-down').click(tableOrderDown);
302 | $('#data-tbl').find('span.table-order-remove').click(tableOrderRemove);
303 | tablePreview(myform);
304 | myform.find('select[name="stack"]').change(
305 | function() {
306 | setTimeout(function(){ tablePreview(myform) }, 10)
307 | }
308 | );
309 | };
310 |
311 | $(function() {
312 | $('select#select_service').change();
313 | });
314 |
315 | $(document).on('change', 'select#select_service', function() {
316 | var name0 = $('select#select_service').val();
317 | $('select#select_section option').remove();
318 | var options = $('select#select_section_original option[data-parent="' + name0 + '"]').clone();
319 | $('select#select_section').append(options);
320 | $('select#select_section').change();
321 | });
322 |
323 | $(document).on('change', 'select#select_section', function() {
324 | var name0 = $('select#select_service').val();
325 | var name1 = $('select#select_section').val();
326 | $('select#select_metrics option').remove();
327 | var options = $('select#select_metrics_original option[data-parent="' + name0 + '/' + name1 + '"]').clone();
328 | $('select#select_metrics').append(options);
329 | });
330 |
331 |
--------------------------------------------------------------------------------
/public/js/bootstrap.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.0.3 (http://getbootstrap.com)
3 | * Copyright 2013 Twitter, Inc.
4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0
5 | */
6 |
7 | if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]'),b=!0;if(a.length){var c=this.$element.find("input");"radio"===c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?b=!1:a.find(".active").removeClass("active")),b&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}b&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('
').insertAfter(a(this)).on("click",b),f.trigger(d=a.Event("show.bs.dropdown")),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown"),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=a("[role=menu] li:not(.divider):visible a",f);if(h.length){var i=h.index(h.filter(":focus"));38==b.keyCode&&i>0&&i--,40==b.keyCode&&i ').appendTo(document.body),this.$element.on("click.dismiss.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focus",i="hover"==g?"mouseleave":"blur";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show),void 0):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide),void 0):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this.tip();this.setContent(),this.options.animation&&c.addClass("fade");var d="function"==typeof this.options.placement?this.options.placement.call(this,c[0],this.$element[0]):this.options.placement,e=/\s?auto?\s?/i,f=e.test(d);f&&(d=d.replace(e,"")||"top"),c.detach().css({top:0,left:0,display:"block"}).addClass(d),this.options.container?c.appendTo(this.options.container):c.insertAfter(this.$element);var g=this.getPosition(),h=c[0].offsetWidth,i=c[0].offsetHeight;if(f){var j=this.$element.parent(),k=d,l=document.documentElement.scrollTop||document.body.scrollTop,m="body"==this.options.container?window.innerWidth:j.outerWidth(),n="body"==this.options.container?window.innerHeight:j.outerHeight(),o="body"==this.options.container?0:j.offset().left;d="bottom"==d&&g.top+g.height+i-l>n?"top":"top"==d&&g.top-l-i<0?"bottom":"right"==d&&g.right+h>m?"left":"left"==d&&g.left-h
'}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery);
--------------------------------------------------------------------------------
/lib/HRForecast/Web.pm:
--------------------------------------------------------------------------------
1 | package HRForecast::Web;
2 |
3 | use strict;
4 | use warnings;
5 | use utf8;
6 | use Kossy;
7 | use HTTP::Date;
8 | use Time::Piece;
9 | use HRForecast::Data;
10 | use HRForecast::Calculator;
11 | use Log::Minimal;
12 | use JSON qw//;
13 |
14 | my $JSON = JSON->new()->ascii(1);
15 | sub encode_json {
16 | $JSON->encode(shift);
17 | }
18 |
19 | sub data {
20 | my $self = shift;
21 | $self->{__data} ||= HRForecast::Data->new();
22 | $self->{__data};
23 | }
24 |
25 | sub calc_term {
26 | my $self = shift;
27 | my %args = @_;
28 |
29 | my $term = $args{t};
30 | my $from = $args{from};
31 | my $to = $args{to};
32 | my $offset = $args{offset};
33 | my $period = $args{period};
34 |
35 | if ( $term eq 'w' ) {
36 | $from = time - 86400 * 10;
37 | $to = time;
38 | }
39 | elsif ( $term eq 'm' ) {
40 | $from = time - 86400 * 40;
41 | $to = time;
42 | }
43 | elsif ( $term eq 'y' ) {
44 | $from = time - 86400 * 400;
45 | $to = time;
46 | }
47 | elsif ( $term eq 'range' ) {
48 | $to = time - $offset;
49 | $from = $to - $period;
50 | }
51 | else {
52 | $from = HTTP::Date::str2time($from);
53 | $to = HTTP::Date::str2time($to);
54 | }
55 | $from = localtime($from - ($from % $self->data->round_interval));
56 | $to = localtime($to - ($to % $self->data->round_interval));
57 | return ($from,$to);
58 | }
59 |
60 | filter 'sidebar' => sub {
61 | my $app = shift;
62 | sub {
63 | my ( $self, $c ) = @_;
64 | my $services = $self->data->get_services();
65 | my @services;
66 | for my $service ( @$services ) {
67 | my $sections = $self->data->get_sections($service);
68 | my @sections;
69 | for my $section ( @$sections ) {
70 | push @sections, {
71 | active =>
72 | $c->args->{service_name} && $c->args->{service_name} eq $service &&
73 | $c->args->{section_name} && $c->args->{section_name} eq $section ? 1 : 0,
74 | name => $section
75 | };
76 | }
77 | my $dot_escaped = $service;
78 | $dot_escaped =~ s/\./__2E__/g;
79 | push @services , {
80 | name => $service,
81 | collapse => $c->req->cookies->{'sidebar_collapse_' . $dot_escaped},
82 | sections => \@sections,
83 | };
84 | }
85 | $c->stash->{services} = \@services;
86 | $app->($self,$c);
87 | }
88 | };
89 |
90 |
91 | filter 'get_metrics' => sub {
92 | my $app = shift;
93 | sub {
94 | my ($self, $c) = @_;
95 | my $row = $self->data->get(
96 | $c->args->{service_name}, $c->args->{section_name}, $c->args->{graph_name},
97 | );
98 | $c->halt(404) unless $row;
99 | $c->stash->{metrics} = $row;
100 | $app->($self,$c);
101 | }
102 | };
103 |
104 | filter 'get_complex' => sub {
105 | my $app = shift;
106 | sub {
107 | my ($self, $c) = @_;
108 | my $row = $self->data->get_complex(
109 | $c->args->{service_name}, $c->args->{section_name}, $c->args->{graph_name},
110 | );
111 | $c->halt(404) unless $row;
112 | $c->stash->{metrics} = $row;
113 | $app->($self,$c);
114 | }
115 | };
116 |
117 | filter 'unset_frame_option' => sub {
118 | my $app = shift;
119 | sub {
120 | my ($self, $c) = @_;
121 | $c->res->headers->remove_header('X-Frame-Options');
122 | $app->($self,$c);
123 | }
124 | };
125 |
126 | filter 'display_table' => sub {
127 | my $app = shift;
128 | sub {
129 | my ($self, $c) = @_;
130 | $c->stash->{display_table} = 1;
131 | $app->($self,$c);
132 | }
133 | };
134 |
135 | get '/' => [qw/sidebar/] => sub {
136 | my ( $self, $c ) = @_;
137 | $c->render('index.tx', {});
138 | };
139 |
140 | get '/json' => [qw/sidebar/] => sub {
141 | my ( $self, $c ) = @_;
142 | $c->render_json({
143 | error => 0,
144 | services => $c->stash->{services},
145 | });
146 | };
147 |
148 | get '/docs' => [qw/sidebar/] => sub {
149 | my ( $self, $c ) = @_;
150 | $c->render('docs.tx',{calculations => HRForecast::Calculator::CALCULATIONS});
151 | };
152 |
153 | my $metrics_validator = [
154 | 't' => {
155 | default => 'm',
156 | rule => [
157 | [['CHOICE',qw/w m y c range/],'invalid browse term'],
158 | ],
159 | },
160 | 'from' => {
161 | default => sub { localtime(time-86400*35)->strftime('%Y/%m/%d %T') },
162 | rule => [
163 | [sub{ HTTP::Date::str2time($_[1]) }, 'invalid From datetime'],
164 | ],
165 | },
166 | 'period' => {
167 | default => 0,
168 | rule => [
169 | ['UINT', 'invalid interval'],
170 | ],
171 | },
172 | 'offset' => {
173 | default => 0,
174 | rule => [
175 | ['UINT', 'invalid offset'],
176 | ],
177 | },
178 | 'to' => {
179 | default => sub { localtime()->strftime('%Y/%m/%d %T') },
180 | rule => [
181 | [sub{ HTTP::Date::str2time($_[1]) }, 'invalid To datetime'],
182 | ],
183 | },
184 | 'd' => {
185 | default => 0,
186 | rule => [
187 | [['CHOICE',qw/1 0/],'invalid download flag'],
188 | ],
189 | },
190 | 'stack' => {
191 | default => 0,
192 | rule => [
193 | [['CHOICE',qw/1 0/],'invalid stack flag'],
194 | ],
195 | },
196 | 'graphheader' => {
197 | default => 1,
198 | rule => [
199 | [['CHOICE',qw/1 0/],'invalid graphheader flag'],
200 | ],
201 | },
202 | 'graphlabel' => {
203 | default => 1,
204 | rule => [
205 | [['CHOICE',qw/1 0/],'invalid graphlabel flag'],
206 | ],
207 | },
208 | 'calculation' => {
209 | default => '',
210 | rule => [
211 | [['CHOICE', map { $_->{function} } @{HRForecast::Calculator::CALCULATIONS()} ],'invalid calculation'],
212 | ],
213 | },
214 | ];
215 |
216 | sub _build_metrics_params {
217 | my $result = shift;
218 |
219 | my $term = $result->valid('t');
220 | my @params;
221 | push @params, 't', $term;
222 | if ($term eq 'range') {
223 | push @params, $_ => $result->valid($_) for qw/period offset/;
224 | }
225 | elsif ($term eq 'c') {
226 | push @params, $_ => $result->valid($_) for qw/from to/;
227 | }
228 |
229 | my $calculation = $result->valid('calculation');
230 | if ($calculation && ($calculation ne '')) {
231 | push @params, 'calculation', $calculation;
232 | }
233 |
234 | \@params;
235 | }
236 |
237 | sub create_merge_params {
238 | my $array_ref = shift;
239 |
240 | return sub {
241 | my $hash_ref = shift;
242 | my %params_hash = (@$array_ref, %$hash_ref);
243 |
244 | while (my ($key, $value) = each(%params_hash)){
245 | if ($value eq '') {
246 | delete $params_hash{$key};
247 | }
248 | }
249 |
250 | my @params_array = %params_hash;
251 |
252 | return \@params_array;
253 | }
254 | };
255 |
256 | get '/list/:service_name/:section_name' => [qw/sidebar/] => sub {
257 | my ( $self, $c ) = @_;
258 | my $result = $c->req->validator($metrics_validator);
259 | my $rows = $self->data->get_metricses(
260 | $c->args->{service_name}, $c->args->{section_name}
261 | );
262 | my ($from ,$to) = $self->calc_term( map {($_ => $result->valid($_))} qw/t from to period offset/);
263 | my $metrics_params = _build_metrics_params($result);
264 | $c->render('list.tx',{
265 | metricses => $rows,
266 | valid => $result,
267 | metrics_params => $metrics_params,
268 | date_window => encode_json([$from->strftime('%Y/%m/%d %T'),
269 | $to->strftime('%Y/%m/%d %T')]),
270 | calculations => HRForecast::Calculator::CALCULATIONS,
271 | merge_params => HRForecast::Web::create_merge_params($metrics_params),
272 | });
273 | };
274 |
275 | get '/json/:service_name/:section_name' => sub {
276 | my ( $self, $c ) = @_;
277 | my $rows = $self->data->get_metricses(
278 | $c->args->{service_name}, $c->args->{section_name}
279 | );
280 | $c->render_json({
281 | error => 0,
282 | metricses => $rows
283 | });
284 | };
285 |
286 |
287 | get '/view/:service_name/:section_name/:graph_name' => [qw/sidebar get_metrics/] => sub {
288 | my ( $self, $c ) = @_;
289 | my $result = $c->req->validator($metrics_validator);
290 | my ($from ,$to) = $self->calc_term( map {($_ => $result->valid($_))} qw/t from to period offset/);
291 | my $metrics_params = _build_metrics_params($result);
292 | $c->render('list.tx', {
293 | metricses => [$c->stash->{metrics}],
294 | valid => $result,
295 | metrics_params => $metrics_params,
296 | date_window => encode_json([$from->strftime('%Y/%m/%d %T'),
297 | $to->strftime('%Y/%m/%d %T')]),
298 | calculations => HRForecast::Calculator::CALCULATIONS,
299 | merge_params => HRForecast::Web::create_merge_params($metrics_params),
300 | });
301 | };
302 |
303 | get '/json/:service_name/:section_name/:graph_name' => [qw/get_metrics/] => sub {
304 | my ( $self, $c ) = @_;
305 | $c->render_json({
306 | error => 0,
307 | metricses => [$c->stash->{metrics}],
308 | });
309 | };
310 |
311 |
312 | get '/view_complex/:service_name/:section_name/:graph_name' => [qw/sidebar get_complex/] => sub {
313 | my ( $self, $c ) = @_;
314 | my $result = $c->req->validator($metrics_validator);
315 | my ($from ,$to) = $self->calc_term( map {($_ => $result->valid($_))} qw/t from to period offset/);
316 | my $metrics_params = _build_metrics_params($result);
317 | $c->render('list.tx', {
318 | metricses => [$c->stash->{metrics}],
319 | valid => $result,
320 | metrics_params => $metrics_params,
321 | date_window => encode_json([$from->strftime('%Y/%m/%d %T'),
322 | $to->strftime('%Y/%m/%d %T')]),
323 | calculations => HRForecast::Calculator::CALCULATIONS,
324 | merge_params => HRForecast::Web::create_merge_params($metrics_params),
325 | });
326 | };
327 |
328 | get '/json_complex/:service_name/:section_name/:graph_name' => [qw/get_complex/] => sub {
329 | my ( $self, $c ) = @_;
330 | $c->render_json({
331 | error => 0,
332 | metricses => [$c->stash->{metrics}],
333 | });
334 | };
335 |
336 | get '/ifr/:service_name/:section_name/:graph_name' => [qw/unset_frame_option get_metrics/] => sub {
337 | my ( $self, $c ) = @_;
338 | my $result = $c->req->validator($metrics_validator);
339 | my ($from ,$to) = $self->calc_term( map {($_ => $result->valid($_))} qw/t from to period offset/);
340 | my $metrics_params = _build_metrics_params($result);
341 | $c->render('ifr.tx', {
342 | metrics => $c->stash->{metrics},
343 | valid => $result,
344 | metrics_params => $metrics_params,
345 | date_window => encode_json([$from->strftime('%Y/%m/%d %T'),
346 | $to->strftime('%Y/%m/%d %T')]),
347 | calculations => HRForecast::Calculator::CALCULATIONS,
348 | merge_params => HRForecast::Web::create_merge_params($metrics_params),
349 | });
350 | };
351 |
352 | get '/ifr_complex/:service_name/:section_name/:graph_name' => [qw/unset_frame_option get_complex/] => sub {
353 | my ( $self, $c ) = @_;
354 | my $result = $c->req->validator($metrics_validator);
355 | my ($from ,$to) = $self->calc_term( map {($_ => $result->valid($_))} qw/t from to period offset/);
356 | my $metrics_params = _build_metrics_params($result);
357 | $c->render('ifr_complex.tx', {
358 | metrics => $c->stash->{metrics},
359 | valid => $result,
360 | metrics_params => $metrics_params,
361 | date_window => encode_json([$from->strftime('%Y/%m/%d %T'),
362 | $to->strftime('%Y/%m/%d %T')]),
363 | calculations => HRForecast::Calculator::CALCULATIONS,
364 | merge_params => HRForecast::Web::create_merge_params($metrics_params),
365 | });
366 | };
367 |
368 | get '/ifr/preview/' => [qw/unset_frame_option/] => sub {
369 | my ( $self, $c ) = @_;
370 | $c->render('pifr_dummy.tx');
371 | };
372 |
373 | get '/ifr/preview/:complex' => [qw/unset_frame_option/] => sub {
374 | my ( $self, $c ) = @_;
375 | my $result = $c->req->validator($metrics_validator);
376 | my ($from ,$to) = $self->calc_term( map {($_ => $result->valid($_))} qw/t from to period offset/);
377 |
378 | my @complex = split /:/, $c->args->{complex};
379 | my @colors;
380 | my @metricses;
381 | for my $id ( @complex ) {
382 | my $data = $self->data->get_by_id($id);
383 | push @metricses, $data;
384 | push @colors, $data ? $data->{color} : '#cccccc';
385 | }
386 |
387 | $c->render('pifr.tx', {
388 | metricses => [@metricses],
389 | complex => $c->args->{complex},
390 | valid => $result,
391 | metrics_params => _build_metrics_params($result),
392 | colors => encode_json(\@colors),
393 | date_window => encode_json([$from->strftime('%Y/%m/%d %T'),
394 | $to->strftime('%Y/%m/%d %T')]),
395 | });
396 | };
397 |
398 | get '/edit/:service_name/:section_name/:graph_name' => [qw/sidebar get_metrics/] => sub {
399 | my ( $self, $c ) = @_;
400 | $c->render('edit.tx');
401 | };
402 |
403 | post '/edit/:service_name/:section_name/:graph_name' => [qw/get_metrics/] => sub {
404 | my ( $self, $c ) = @_;
405 | my $check_uniq = sub {
406 | my ($req,$val) = @_;
407 | my $service = $req->param('service_name');
408 | my $section = $req->param('section_name');
409 | my $graph = $req->param('graph_name');
410 | $service = '' if !defined $service;
411 | $section = '' if !defined $section;
412 | $graph = '' if !defined $graph;
413 | my $row = $self->data->get($service,$section,$graph);
414 | return 1 if $row && $row->{id} == $c->stash->{metrics}->{id};
415 | return 1 if !$row;
416 | return;
417 | };
418 | my $result = $c->req->validator([
419 | 'service_name' => {
420 | rule => [
421 | ['NOT_NULL', 'サービス名がありません'],
422 | ],
423 | },
424 | 'section_name' => {
425 | rule => [
426 | ['NOT_NULL', 'セクション名がありません'],
427 | ],
428 | },
429 | 'graph_name' => {
430 | rule => [
431 | ['NOT_NULL', 'グラフ名がありません'],
432 | [$check_uniq,'同じ名前のグラフがあります'],
433 | ],
434 | },
435 | 'description' => {
436 | default => '',
437 | rule => [],
438 | },
439 | 'sort' => {
440 | rule => [
441 | ['NOT_NULL', '値がありません'],
442 | [['CHOICE',0..19], '値が正しくありません'],
443 | ],
444 | },
445 | 'color' => {
446 | rule => [
447 | ['NOT_NULL', '正しくありません'],
448 | [sub{ $_[1] =~ m!^#[0-9A-F]{6}$!i }, '#000000の形式で入力してください'],
449 | ],
450 | },
451 | ]);
452 | if ( $result->has_error ) {
453 | my $res = $c->render_json({
454 | error => 1,
455 | messages => $result->errors
456 | });
457 | return $res;
458 | }
459 |
460 | $self->data->update_metrics(
461 | $c->stash->{metrics}->{id},
462 | $result->valid->as_hashref
463 | );
464 |
465 | my $row = $self->data->get(
466 | $c->args->{service_name}, $c->args->{section_name}, $c->args->{graph_name},
467 | );
468 |
469 | $c->render_json({
470 | error => 0,
471 | metricses => [$row],
472 | location => $c->req->uri_for(
473 | '/list/'.$result->valid('service_name').'/'.$result->valid('section_name'))->as_string,
474 | });
475 | };
476 |
477 | post '/delete/:service_name/:section_name/:graph_name' => [qw/get_metrics/] => sub {
478 | my ( $self, $c ) = @_;
479 | $self->data->delete_metrics(
480 | $c->stash->{metrics}->{id},
481 | );
482 | $c->render_json({
483 | error => 0,
484 | location => $c->req->uri_for(
485 | '/list/'.$c->args->{service_name}.'/'.$c->args->{section_name})->as_string,
486 | });
487 | };
488 |
489 | get '/add_complex' => [qw/sidebar/] => sub {
490 | my ( $self, $c ) = @_;
491 | my $all_metrics_names = $self->data->get_all_metrics_name();
492 | $c->render('add_complex.tx', { all_metrics_names => $all_metrics_names } );
493 | };
494 |
495 | sub check_uniq_complex {
496 | my ($self,$id) = @_;
497 | sub {
498 | my ($req,$val) = @_;
499 | my $service = $req->param('service_name');
500 | my $section = $req->param('section_name');
501 | my $graph = $req->param('graph_name');
502 | $service = '' if !defined $service;
503 | $section = '' if !defined $section;
504 | $graph = '' if !defined $graph;
505 | my $row = $self->data->get_complex($service,$section,$graph);
506 | if ($id) {
507 | return 1 if $row && $row->{id} == $id;
508 | }
509 | return 1 if !$row;
510 | return;
511 | };
512 | }
513 |
514 | post '/add_complex' => sub {
515 | my ( $self, $c ) = @_;
516 | my $result = $c->req->validator([
517 | 'service_name' => {
518 | rule => [
519 | ['NOT_NULL', 'サービス名がありません'],
520 | ],
521 | },
522 | 'section_name' => {
523 | rule => [
524 | ['NOT_NULL', 'セクション名がありません'],
525 | ],
526 | },
527 | 'graph_name' => {
528 | rule => [
529 | ['NOT_NULL', 'グラフ名がありません'],
530 | [$self->check_uniq_complex,'同じ名前のグラフがあります'],
531 | ],
532 | },
533 | 'description' => {
534 | default => '',
535 | rule => [],
536 | },
537 | 'stack' => {
538 | rule => [
539 | ['NOT_NULL', 'スタックの値がありません'],
540 | [['CHOICE',0,1], 'スタックの値が正しくありません'],
541 | ],
542 | },
543 | 'sort' => {
544 | rule => [
545 | ['NOT_NULL', 'ソートの値がありません'],
546 | [['CHOICE',0..19], 'ソートの値が正しくありません'],
547 | ],
548 | },
549 | '@path-data' => {
550 | rule => [
551 | [['@SELECTED_NUM',1,100], 'データは100件までにしてください'],
552 | ['NOT_NULL','データが正しくありません'],
553 | ['NATURAL', 'データが正しくありません'],
554 | ],
555 | },
556 | ]);
557 | if ( $result->has_error ) {
558 | my $res = $c->render_json({
559 | error => 1,
560 | messages => $result->errors
561 | });
562 | return $res;
563 | }
564 |
565 | $self->data->create_complex(
566 | $result->valid('service_name'),$result->valid('section_name'),$result->valid('graph_name'),
567 | $result->valid->mixed
568 | );
569 |
570 | my $row = $self->data->get_complex(
571 | $c->args->{service_name}, $c->args->{section_name}, $c->args->{graph_name},
572 | );
573 |
574 | $c->render_json({
575 | error => 0,
576 | metricses => [$row],
577 | location => $c->req->uri_for('/list/'.$result->valid('service_name').'/'.$result->valid('section_name'))->as_string,
578 | });
579 | };
580 |
581 | get '/edit_complex/:service_name/:section_name/:graph_name' => [qw/sidebar get_complex/] => sub {
582 | my ( $self, $c ) = @_;
583 | my $all_metrics_names = $self->data->get_all_metrics_name();
584 | $c->render('edit_complex.tx', { all_metrics_names => $all_metrics_names } );
585 | };
586 |
587 | post '/edit_complex/:service_name/:section_name/:graph_name' => [qw/sidebar get_complex/] => sub {
588 | my ( $self, $c ) = @_;
589 | my $result = $c->req->validator([
590 | 'service_name' => {
591 | rule => [
592 | ['NOT_NULL', 'サービス名がありません'],
593 | ],
594 | },
595 | 'section_name' => {
596 | rule => [
597 | ['NOT_NULL', 'セクション名がありません'],
598 | ],
599 | },
600 | 'graph_name' => {
601 | rule => [
602 | ['NOT_NULL', 'グラフ名がありません'],
603 | [$self->check_uniq_complex($c->stash->{metrics}->{id}),'同じ名前のグラフがあります'],
604 | ],
605 | },
606 | 'description' => {
607 | default => '',
608 | rule => [],
609 | },
610 | 'stack' => {
611 | rule => [
612 | ['NOT_NULL', 'スタックの値がありません'],
613 | [['CHOICE',0,1], 'スタックの値が正しくありません'],
614 | ],
615 | },
616 | 'sort' => {
617 | rule => [
618 | ['NOT_NULL', 'ソートの値がありません'],
619 | [['CHOICE',0..19], 'ソートの値が正しくありません'],
620 | ],
621 | },
622 | '@path-data' => {
623 | rule => [
624 | [['@SELECTED_NUM',1,100], 'データは100件までにしてください'],
625 | ['NOT_NULL','データが正しくありません'],
626 | ['NATURAL', 'データが正しくありません'],
627 | ],
628 | },
629 | ]);
630 | if ( $result->has_error ) {
631 | my $res = $c->render_json({
632 | error => 1,
633 | messages => $result->errors
634 | });
635 | return $res;
636 | }
637 |
638 | $self->data->update_complex(
639 | $c->stash->{metrics}->{id},
640 | $result->valid->mixed
641 | );
642 |
643 | my $row = $self->data->get_complex(
644 | $c->args->{service_name}, $c->args->{section_name}, $c->args->{graph_name},
645 | );
646 |
647 | $c->render_json({
648 | error => 0,
649 | metricses => [$row],
650 | location => $c->req->uri_for('/list/'.$result->valid('service_name').'/'.$result->valid('section_name'))->as_string,
651 | });
652 | };
653 |
654 |
655 | post '/delete_complex/:service_name/:section_name/:graph_name' => [qw/get_complex/] => sub {
656 | my ( $self, $c ) = @_;
657 | $self->data->delete_complex(
658 | $c->stash->{metrics}->{id},
659 | );
660 | $c->render_json({
661 | error => 0,
662 | location => $c->req->uri_for(
663 | '/list/'.$c->args->{service_name}.'/'.$c->args->{section_name})->as_string,
664 | });
665 | };
666 |
667 | my $display_csv = sub {
668 | my ( $self, $c ) = @_;
669 | my $result = $c->req->validator($metrics_validator);
670 | my ($from ,$to) = $self->calc_term( map {($_ => $result->valid($_))} qw/t from to period offset/);
671 |
672 | my $calculator = HRForecast::Calculator->new();
673 | my $rows = $calculator->calculate($self->data, $c->stash->{metrics}->{id}, $from ,$to, $result->valid('calculation'));
674 |
675 | my @result;
676 | push @result, [
677 | 'Date',
678 | sprintf("/%s/%s/%s",map { $c->stash->{metrics}->{$_} } qw/service_name section_name graph_name/)
679 | ];
680 | foreach my $row ( @$rows ) {
681 | push @result, [
682 | $row->{datetime}->strftime('%Y/%m/%d %T'),
683 | $row->{number}
684 | ];
685 | }
686 |
687 | if ( $c->stash->{display_table} ) {
688 | return $c->render('table.tx', { table => \@result });
689 | }
690 |
691 | if ( $result->valid('d') ) {
692 | $c->res->header('Content-Disposition',
693 | sprintf('attachment; filename="metrics_%s.csv"',$c->stash->{metrics}->{id}));
694 | $c->res->content_type('application/octet-stream');
695 | }
696 | else {
697 | $c->res->content_type('text/plain');
698 | }
699 |
700 | $c->res->body( join "\n", map { join ",", @$_ } @result );
701 | $c->res;
702 | };
703 |
704 | my $display_complex_csv = sub {
705 | my ( $self, $c ) = @_;
706 | my $result = $c->req->validator($metrics_validator);
707 | my ($from ,$to) = $self->calc_term( map {($_ => $result->valid($_))} qw/t from to period offset/);
708 |
709 | my @data;
710 | my @id;
711 | if ( !$c->stash->{metrics} ) {
712 | my @complex = split /:/, $c->args->{complex};
713 | for my $id ( @complex ) {
714 | my $data = $self->data->get_by_id($id);
715 | next unless $data;
716 | push @data, $data;
717 | push @id, $data->{id};
718 | }
719 | }
720 | else {
721 | @data = @{$c->stash->{metrics}->{metricses}};
722 | @id = map { $_->{id} } @data;
723 | }
724 |
725 | my $calculator = HRForecast::Calculator->new();
726 | my $rows = $calculator->calculate($self->data, [ map { $_->{id} } @data ], $from, $to, $result->valid('calculation'));
727 |
728 | my %date_group;
729 | foreach my $row ( @$rows ) {
730 | my $datetime = $row->{datetime}->strftime('%Y%m%d%H%M%S');
731 | $date_group{$datetime} ||= {};
732 | $date_group{$datetime}->{$row->{metrics_id}} = $row->{number};
733 | }
734 |
735 | my @result;
736 | push @result, [
737 | 'Date',
738 | map { '/'.$_->{service_name}.'/'.$_->{section_name}.'/'.$_->{graph_name} } @data
739 | ];
740 |
741 | foreach my $key ( sort keys %date_group ) {
742 | $key =~ m!^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$!;
743 | my $datetime = sprintf "%s/%s/%s %s:%s:%s", $1, $2, $3, $4, $5, $6;
744 |
745 | push @result, [
746 | $datetime,
747 | map { exists $date_group{$key}->{$_} ? $date_group{$key}->{$_} : 0 } @id
748 | ];
749 | }
750 |
751 | if ( $c->stash->{display_table} ) {
752 | return $c->render('table.tx', { table => \@result });
753 | }
754 |
755 |
756 | if ( $result->valid('d') ) {
757 | $c->res->header('Content-Disposition',
758 | sprintf('attachment; filename="metrics_%02d.csv"', int(rand(100)) ));
759 | $c->res->content_type('application/octet-stream');
760 | }
761 | else {
762 | $c->res->content_type('text/plain');
763 | }
764 | $c->res->body( join "\n", map { join ",", @$_ } @result );
765 | $c->res;
766 | };
767 |
768 |
769 | get '/csv/:service_name/:section_name/:graph_name'
770 | => [qw/get_metrics/]
771 | => $display_csv;
772 | get '/table/:service_name/:section_name/:graph_name'
773 | => [qw/get_metrics display_table/]
774 | => $display_csv;
775 |
776 | get '/csv/:complex' => $display_complex_csv;
777 | get '/csv_complex/:service_name/:section_name/:graph_name'
778 | => [qw/get_complex/]
779 | => $display_complex_csv;
780 | get '/table/:complex' => [qw/display_table/] => $display_complex_csv;
781 | get '/table_complex/:service_name/:section_name/:graph_name'
782 | => [qw/get_complex display_table/]
783 | => $display_complex_csv;
784 |
785 |
786 | post '/api/:service_name/:section_name/:graph_name' => sub {
787 | my ( $self, $c ) = @_;
788 | my $result = $c->req->validator([
789 | 'number' => {
790 | rule => [
791 | ['NOT_NULL','number is null'],
792 | ['INT','number is not int']
793 | ],
794 | },
795 | 'datetime' => {
796 | default => sub { HTTP::Date::time2str(time) },
797 | rule => [
798 | [ sub { HTTP::Date::str2time($_[1]) } ,'datetime is not null']
799 | ],
800 | },
801 | ]);
802 |
803 | if ( $result->has_error ) {
804 | my $res = $c->render_json({
805 | error => 1,
806 | messages => $result->messages
807 | });
808 | $res->status(400);
809 | return $res;
810 | }
811 |
812 | my $ret = $self->data->update(
813 | $c->args->{service_name}, $c->args->{section_name}, $c->args->{graph_name},
814 | $result->valid('number'), HTTP::Date::str2time($result->valid('datetime'))
815 | );
816 | my $row = $self->data->get(
817 | $c->args->{service_name}, $c->args->{section_name}, $c->args->{graph_name},
818 | );
819 |
820 | $c->render_json({
821 | error => 0,
822 | metricses => [$row],
823 | });
824 | };
825 |
826 |
827 |
828 |
829 | 1;
830 |
831 |
--------------------------------------------------------------------------------