etsy-deployinator 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.travis.yml +8 -0
- data/Gemfile +0 -1
- data/README.md +47 -10
- data/Rakefile +0 -1
- data/bin/deployinator-tailer.rb +2 -2
- data/deployinator.gemspec +17 -10
- data/lib/deployinator/app.rb +53 -12
- data/lib/deployinator/controller.rb +8 -3
- data/lib/deployinator/helpers/concurrency.rb +70 -0
- data/lib/deployinator/helpers/dsh.rb +15 -5
- data/lib/deployinator/helpers/git.rb +16 -9
- data/lib/deployinator/helpers/version.rb +52 -52
- data/lib/deployinator/helpers.rb +109 -13
- data/lib/deployinator/static/css/maintenance.css +9 -0
- data/lib/deployinator/static/css/style.css +25 -5
- data/lib/deployinator/static/images/maintenance.gif +0 -0
- data/lib/deployinator/static/js/stats_load.js +10 -0
- data/lib/deployinator/templates/exception.mustache +11 -0
- data/lib/deployinator/templates/generic_single_push.mustache +2 -1
- data/lib/deployinator/templates/layout.mustache +5 -4
- data/lib/deployinator/templates/log_table.mustache +18 -5
- data/lib/deployinator/templates/maintenance.mustache +5 -0
- data/lib/deployinator/templates/stats.mustache +180 -0
- data/lib/deployinator/version.rb +1 -1
- data/lib/deployinator/views/log_table.rb +36 -0
- data/lib/deployinator/views/maintenance.rb +15 -0
- data/lib/deployinator/views/stats.rb +96 -0
- data/lib/deployinator.rb +20 -1
- data/test/unit/concurrency_test.rb +72 -0
- data/test/unit/git_test.rb +70 -0
- data/test/unit/helpers_test.rb +61 -2
- data/test/unit/version_test.rb +12 -12
- metadata +129 -27
|
@@ -1,62 +1,62 @@
|
|
|
1
1
|
module Deployinator
|
|
2
2
|
module Helpers
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
module VersionHelpers
|
|
4
|
+
# Public: wrapper function to get the short SHA of a revision. The function
|
|
5
|
+
# checks retrieves the part of the string before the first dash. If the part
|
|
6
|
+
# is a valid default git short rev, i.e. alphanumeric and length 7 it is
|
|
7
|
+
# returned. For an invalid rev, nil is returned.
|
|
8
|
+
#
|
|
9
|
+
# ver - String representing the revision
|
|
10
|
+
#
|
|
11
|
+
# Returns the short SHA consisting of the alphanumerics until the first dash
|
|
12
|
+
# or nil for an invalid version string
|
|
13
|
+
def get_build(ver)
|
|
14
|
+
# return the short sha of the rev
|
|
15
|
+
the_sha = (ver || "")[/^([^-]+)/]
|
|
16
|
+
# check that we have a default git SHA
|
|
17
|
+
val = /^[a-zA-Z0-9]{7,}$/.match the_sha
|
|
18
|
+
val.nil? ? nil : the_sha
|
|
19
|
+
end
|
|
20
20
|
module_function :get_build
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
# Public: function to get the current software version running on a host
|
|
23
|
+
#
|
|
24
|
+
# host - String of the hostname to check
|
|
25
|
+
#
|
|
26
|
+
# Returns the full version of the current software running on the host
|
|
27
|
+
def get_version(host)
|
|
28
|
+
host_url = "https://#{host}/"
|
|
29
|
+
get_version_by_url("#{host_url}version.txt")
|
|
30
|
+
end
|
|
31
31
|
module_function :get_version
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
# Public: function to fetch a version string from a URL. The version string
|
|
34
|
+
# is validated to have a valid format. The function calls a lower level
|
|
35
|
+
# implementation method for actually getting the version.
|
|
36
|
+
#
|
|
37
|
+
# url - String representing where to get the version from
|
|
38
|
+
#
|
|
39
|
+
# Returns the version string or nil if the format is invalid
|
|
40
|
+
def get_version_by_url(url)
|
|
41
|
+
version = curl_get_url(url)
|
|
42
|
+
val = /^[a-zA-Z0-9]{7,}-[0-9]{8}-[0-9]{6}-UTC$/.match version
|
|
43
|
+
val.nil? ? nil : version.chomp
|
|
44
|
+
end
|
|
45
45
|
module_function :get_version_by_url
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
# Public: this helper function wraps the actual call to get the contents of a
|
|
48
|
+
# version file. This helps with reducing code duplication and also stubbing
|
|
49
|
+
# out the actual call for unit testing.
|
|
50
|
+
#
|
|
51
|
+
# url - String representing the complete URL to query
|
|
52
|
+
#
|
|
53
|
+
# Returns the contents of the URL resource
|
|
54
|
+
def curl_get_url(url)
|
|
55
|
+
with_timeout 2, "getting version via curl from #{url}" do
|
|
56
|
+
`curl -s #{url}`
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
59
|
module_function :curl_get_url
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
62
|
end
|
data/lib/deployinator/helpers.rb
CHANGED
|
@@ -75,11 +75,37 @@ module Deployinator
|
|
|
75
75
|
#
|
|
76
76
|
# Returns nothing
|
|
77
77
|
def log_and_stream(output)
|
|
78
|
-
write_file output,
|
|
78
|
+
write_file output, runlog_filename if runlog_filename
|
|
79
79
|
return @block.call(output) unless @block.nil?
|
|
80
80
|
""
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# gives the filename to send runlog to based on whether we are in the main thread or not
|
|
84
|
+
# We do this because we want to be able to use log_and_stream seamlessly in a
|
|
85
|
+
# parallel thread. So, all log_and_stream calls in all but the main thread will
|
|
86
|
+
# log to a seaparate file
|
|
87
|
+
# output - String filename to log to
|
|
88
|
+
def runlog_filename(name=nil)
|
|
89
|
+
if @filename
|
|
90
|
+
if Thread.main == Thread.current
|
|
91
|
+
@filename
|
|
92
|
+
elsif Thread.current[:logfile_name]
|
|
93
|
+
Thread.current[:logfile_name]
|
|
94
|
+
elsif name
|
|
95
|
+
Thread.current[:logfile_name] = runlog_thread_filename(name)
|
|
96
|
+
Thread.current[:logfile_name]
|
|
97
|
+
else
|
|
98
|
+
raise 'Logfile name not defined in thread. Expecting name parameter to be passed in.'
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# gives us the filename to log to in thread
|
|
104
|
+
# output - String filename for thread
|
|
105
|
+
def runlog_thread_filename(name)
|
|
106
|
+
@filename + '-' + name.to_s
|
|
107
|
+
end
|
|
108
|
+
|
|
83
109
|
# Run external command with timing information
|
|
84
110
|
# streams and logs the output of the command as well
|
|
85
111
|
# If the command fails, it is retried some number of times
|
|
@@ -343,7 +369,7 @@ module Deployinator
|
|
|
343
369
|
end
|
|
344
370
|
end
|
|
345
371
|
|
|
346
|
-
# Public: gets the
|
|
372
|
+
# Public: gets the contents from a cache file if it hasn't expired
|
|
347
373
|
#
|
|
348
374
|
# Paramaters:
|
|
349
375
|
# cache_file: path to a cache file
|
|
@@ -370,14 +396,30 @@ module Deployinator
|
|
|
370
396
|
end
|
|
371
397
|
end
|
|
372
398
|
|
|
399
|
+
# Public: writes the supplied contents to the cache file, ensuring that
|
|
400
|
+
# encoding is correct
|
|
401
|
+
#
|
|
402
|
+
# Parameters:
|
|
403
|
+
# cache_file: path to the cache file
|
|
404
|
+
# content: the data to write to the file
|
|
405
|
+
#
|
|
406
|
+
# Returns nothing
|
|
407
|
+
def write_to_cache(cache_file, contents)
|
|
408
|
+
File.open(cache_file, 'w:UTF-8') do |f|
|
|
409
|
+
f.write(contents.force_encoding('UTF-8'))
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
373
413
|
def lock_pushes(stack, who, method)
|
|
374
|
-
log_and_stream("LOCKING #{stack}")
|
|
414
|
+
log_and_stream("LOCKING #{stack}<br>")
|
|
375
415
|
if lock_info = push_lock_info(stack)
|
|
376
|
-
|
|
416
|
+
log_and_stream("Pushes locked by #{lock_info[:who]} - #{lock_info[:method]}<br>")
|
|
417
|
+
return false
|
|
377
418
|
end
|
|
378
419
|
|
|
379
420
|
dt = Time.now.strftime("%m/%d/%Y %H:%M")
|
|
380
421
|
log_string_to_file("#{who}|#{method}|#{dt}", push_lock_path(stack))
|
|
422
|
+
return true
|
|
381
423
|
end
|
|
382
424
|
|
|
383
425
|
def unlock_pushes(stack)
|
|
@@ -424,16 +466,19 @@ module Deployinator
|
|
|
424
466
|
|
|
425
467
|
# Public: wrap a block into a timeout
|
|
426
468
|
#
|
|
427
|
-
# seconds
|
|
428
|
-
# description
|
|
429
|
-
#
|
|
469
|
+
# seconds - timeout in seconds
|
|
470
|
+
# description - optional description for logging (default:"")
|
|
471
|
+
# throw_exception - options param to throw exception back up stack
|
|
472
|
+
# quiet - optional boolean for logging as a big red warning using the stderr div class
|
|
473
|
+
# extra_opts - optional hash to pass along to plugins
|
|
474
|
+
# &block - block to call
|
|
430
475
|
#
|
|
431
476
|
# Example
|
|
432
477
|
# with_timeout(20){system("curl -s http://google.com")}
|
|
433
478
|
# with_timeout 30 do; system("curl -s http://google.com"); end
|
|
434
479
|
#
|
|
435
480
|
# Returns nothing
|
|
436
|
-
def with_timeout(seconds, description=nil,
|
|
481
|
+
def with_timeout(seconds, description=nil, throw_exception=false, quiet=false, extra_opts={}, &block)
|
|
437
482
|
begin
|
|
438
483
|
Timeout.timeout(seconds) do
|
|
439
484
|
yield
|
|
@@ -443,13 +488,22 @@ module Deployinator
|
|
|
443
488
|
info += " for #{description}" unless description.nil?
|
|
444
489
|
# log and stream if log filename is not undefined
|
|
445
490
|
if (/undefined/ =~ @filename).nil?
|
|
446
|
-
|
|
491
|
+
if quiet
|
|
492
|
+
log_and_stream "#{info}<br>"
|
|
493
|
+
else
|
|
494
|
+
log_and_stream "<div class=\"stderr\">#{info}</div>"
|
|
495
|
+
end
|
|
447
496
|
end
|
|
448
497
|
state = {
|
|
449
498
|
:seconds => seconds,
|
|
450
|
-
:info => info
|
|
499
|
+
:info => info,
|
|
500
|
+
:stack => stack,
|
|
501
|
+
:extra_opts => extra_opts
|
|
451
502
|
}
|
|
452
503
|
raise_event(:timeout, state)
|
|
504
|
+
if throw_exception
|
|
505
|
+
raise e
|
|
506
|
+
end
|
|
453
507
|
""
|
|
454
508
|
end
|
|
455
509
|
end
|
|
@@ -492,7 +546,7 @@ module Deployinator
|
|
|
492
546
|
raise "Must have stack" unless options[:stack]
|
|
493
547
|
|
|
494
548
|
options[:env] ||= "PROD"
|
|
495
|
-
options[:nice_env]
|
|
549
|
+
options[:nice_env] ||= nicify_env(options[:env])
|
|
496
550
|
options[:user] ||= @username
|
|
497
551
|
options[:start] = @start_time unless options[:start] || ! @start_time
|
|
498
552
|
|
|
@@ -548,7 +602,7 @@ module Deployinator
|
|
|
548
602
|
# stack should be the last part of the log line from the last pipe to the end
|
|
549
603
|
# modified this to take into account the run_log entry at the end
|
|
550
604
|
unless stacks.empty? && env.empty?
|
|
551
|
-
grep = "| egrep '#{env}.*\\|\(#{stacks.join("|")}\)
|
|
605
|
+
grep = "| egrep '#{env}.*\\|\(#{stacks.join("|")}\)\\|'"
|
|
552
606
|
end
|
|
553
607
|
|
|
554
608
|
# extra grep does another filter to the line, needed to get CONFIG PRODUCTION
|
|
@@ -600,11 +654,53 @@ module Deployinator
|
|
|
600
654
|
log_msg = e.nil? ? msg : "#{msg} (#{e.message})"
|
|
601
655
|
log_and_stream "<div class=\"stderr\">#{log_msg}</div>"
|
|
602
656
|
if !e.nil?
|
|
603
|
-
|
|
657
|
+
begin
|
|
658
|
+
template = open("#{File.dirname(__FILE__)}/templates/exception.mustache").read
|
|
659
|
+
|
|
660
|
+
regex = /(?<file>.*?):(?<line>\d+):.*?`(?<method>.*)'/
|
|
661
|
+
context = e.backtrace.map do |line|
|
|
662
|
+
match = regex.match(line)
|
|
663
|
+
{
|
|
664
|
+
:file => match['file'],
|
|
665
|
+
:line => match['line'],
|
|
666
|
+
:method => match['method']
|
|
667
|
+
}
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
output = Mustache.render(template, {:exceptions => context})
|
|
671
|
+
log_and_stream output
|
|
672
|
+
rescue
|
|
673
|
+
log_and_stream e.backtrace.inspect
|
|
674
|
+
end
|
|
604
675
|
end
|
|
605
676
|
# This is so we have something in the log if/when this fails
|
|
606
677
|
puts log_msg
|
|
607
678
|
end
|
|
608
679
|
|
|
680
|
+
# Public: helper method to check if the current user is part of
|
|
681
|
+
# an group. This method let's you pass in groups for explicitness and
|
|
682
|
+
# testing but will fall back on @group and Deployinator.admin_groups.
|
|
683
|
+
#
|
|
684
|
+
# Param:
|
|
685
|
+
# user_groups - groups a user is part of
|
|
686
|
+
# admin_groups - admin groups
|
|
687
|
+
#
|
|
688
|
+
# Returns true if the current user is part of and admin group and false
|
|
689
|
+
# otherwise
|
|
690
|
+
def is_admin?(user_groups=nil, admin_groups=nil)
|
|
691
|
+
user_groups ||= @groups
|
|
692
|
+
admin_groups ||= Deployinator.admin_groups
|
|
693
|
+
|
|
694
|
+
return false if user_groups.nil?
|
|
695
|
+
return false if admin_groups.nil?
|
|
696
|
+
|
|
697
|
+
intersected_groups = user_groups & admin_groups
|
|
698
|
+
|
|
699
|
+
# if we found an intersection between admin and user groups, the current
|
|
700
|
+
# user is an admin
|
|
701
|
+
return (intersected_groups.length > 0)
|
|
702
|
+
|
|
703
|
+
end
|
|
704
|
+
|
|
609
705
|
end
|
|
610
706
|
end
|
|
@@ -941,19 +941,29 @@ section#main.config{
|
|
|
941
941
|
/*consol*/
|
|
942
942
|
.code {
|
|
943
943
|
background-color: #222;
|
|
944
|
-
color: #
|
|
945
|
-
font-
|
|
944
|
+
color: #ddd;
|
|
945
|
+
font-size: 16px;
|
|
946
946
|
padding: 8px;
|
|
947
|
+
|
|
947
948
|
}
|
|
948
949
|
|
|
949
950
|
.code .command {
|
|
951
|
+
font-family: monospace;
|
|
952
|
+
font-size:14px;
|
|
953
|
+
margin-top:2px;
|
|
950
954
|
border-top: 1px solid #555;
|
|
951
|
-
padding: 20px
|
|
955
|
+
padding: 10px 20px 5px 10px;
|
|
952
956
|
}
|
|
953
957
|
.code .command h4 {
|
|
954
|
-
color: #
|
|
958
|
+
color: #ccc;
|
|
955
959
|
font-weight: normal;
|
|
956
960
|
}
|
|
961
|
+
|
|
962
|
+
.code .command h5 {
|
|
963
|
+
color: #abc;
|
|
964
|
+
font-size: 12px;
|
|
965
|
+
margin-bottom:10px;
|
|
966
|
+
}
|
|
957
967
|
.code .command p {
|
|
958
968
|
margin: 10px;
|
|
959
969
|
}
|
|
@@ -967,6 +977,17 @@ section#main.config{
|
|
|
967
977
|
font-size: 12px;
|
|
968
978
|
white-space: pre;
|
|
969
979
|
}
|
|
980
|
+
.code .success_msg {
|
|
981
|
+
color: #7fbf4d;
|
|
982
|
+
font-size: 12px;
|
|
983
|
+
white-space: pre;
|
|
984
|
+
}
|
|
985
|
+
.code .warning_msg {
|
|
986
|
+
color: #f0ad4e;
|
|
987
|
+
font-size: 12px;
|
|
988
|
+
white-space: pre;
|
|
989
|
+
}
|
|
990
|
+
|
|
970
991
|
.hidden {
|
|
971
992
|
display:none;
|
|
972
993
|
}
|
|
@@ -1135,7 +1156,6 @@ header.web_config {
|
|
|
1135
1156
|
margin-top: 20px;
|
|
1136
1157
|
-webkit-column-width: 220px;
|
|
1137
1158
|
-moz-column-width: 220px;
|
|
1138
|
-
height: 420px;
|
|
1139
1159
|
}
|
|
1140
1160
|
|
|
1141
1161
|
.pinned-stack-list {
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
$(function() {
|
|
2
|
+
$.each(data, function(key, val) {
|
|
3
|
+
$("#choices").append('<h3><input type="checkbox" name="' + key +
|
|
4
|
+
'" checked="checked" id="id' + key + '">' +
|
|
5
|
+
'<label for="id' + key + '">'+ val.label + '</label></h3>');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
$("#choices").find("input").click(drawGraphs);
|
|
9
|
+
drawGraphs();
|
|
10
|
+
})
|
|
@@ -50,8 +50,9 @@
|
|
|
50
50
|
<div class="push-topic">
|
|
51
51
|
<div class="title"></div>
|
|
52
52
|
<span class="log-run">
|
|
53
|
-
<form action="/
|
|
53
|
+
<form action="/log">
|
|
54
54
|
<button class="button small" type="submit">See run logs</button>
|
|
55
|
+
<input type="hidden" name="stack" value="{{stack}}" />
|
|
55
56
|
</form>
|
|
56
57
|
</span>
|
|
57
58
|
</div>
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
4
|
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
|
5
|
-
<title>Deployinator</title>
|
|
5
|
+
<title>Deployinator{{#stack}} - {{stack}}{{/stack}}</title>
|
|
6
6
|
<link rel="shortcut icon" href="/favicon.ico">
|
|
7
7
|
<link rel="stylesheet" href="/static/css/highlight.css" type="text/css" media="screen">
|
|
8
|
-
<link rel="stylesheet" href="/static/css/style.css?v=
|
|
8
|
+
<link rel="stylesheet" href="/static/css/style.css?v=11" type="text/css" media="screen">
|
|
9
9
|
<link rel="stylesheet" href="/static/css/diff_style.css" type="text/css" media="screen">
|
|
10
10
|
<script src="/js/jquery-1.8.3.min.js"></script>
|
|
11
11
|
<script src="/js/jquery-ui-1.8.24.min.js"></script>
|
|
@@ -53,6 +53,7 @@ _ __ / _ _ \___ __ \__ / _ __ \__ / / /__ / __ __ \_ __ `/_ __/_ __
|
|
|
53
53
|
<span class="label">Stack: </span>
|
|
54
54
|
<span>
|
|
55
55
|
<select name='stacks' id='stacks'>
|
|
56
|
+
<option>Choose...</option>
|
|
56
57
|
{{#get_stack_select}}
|
|
57
58
|
<option value="{{stack}}"{{#current}} selected{{/current}}>{{stack}}</option>
|
|
58
59
|
{{/get_stack_select}}
|
|
@@ -99,7 +100,7 @@ _ __ / _ _ \___ __ \__ / _ __ \__ / / /__ / __ __ \_ __ `/_ __/_ __
|
|
|
99
100
|
window.location = '/' + stack;
|
|
100
101
|
});
|
|
101
102
|
|
|
102
|
-
// listen for changes on the watch button
|
|
103
|
+
// listen for changes on the watch button
|
|
103
104
|
$('.watch').click(function () {
|
|
104
105
|
$.ajax({
|
|
105
106
|
url : '/{{stack}}/can-deploy',
|
|
@@ -110,7 +111,7 @@ _ __ / _ _ \___ __ \__ / _ __ \__ / / /__ / __ __ \_ __ `/_ __/_ __
|
|
|
110
111
|
console.log(form);
|
|
111
112
|
if (form.length == 1) {
|
|
112
113
|
stream_log_websocket(form[0]);
|
|
113
|
-
}
|
|
114
|
+
}
|
|
114
115
|
}
|
|
115
116
|
});
|
|
116
117
|
});
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
<tr>
|
|
4
4
|
<th>
|
|
5
5
|
{{# prev_page }}
|
|
6
|
-
|
|
6
|
+
<form action="/log">
|
|
7
|
+
{{#prev_page_params}}
|
|
8
|
+
<input type="hidden" name="{{name}}" value="{{value}}" />
|
|
9
|
+
{{/prev_page_params}}
|
|
10
|
+
<button class="button small">Prev</button>
|
|
11
|
+
</form>
|
|
7
12
|
{{/ prev_page }}
|
|
8
13
|
</th>
|
|
9
14
|
<th></th>
|
|
@@ -11,9 +16,13 @@
|
|
|
11
16
|
<th></th>
|
|
12
17
|
<th></th>
|
|
13
18
|
<th></th>
|
|
14
|
-
<th></th>
|
|
15
19
|
<th>
|
|
16
|
-
<
|
|
20
|
+
<form action="/log">
|
|
21
|
+
{{#next_page_params}}
|
|
22
|
+
<input type="hidden" name="{{name}}" value="{{value}}" />
|
|
23
|
+
{{/next_page_params}}
|
|
24
|
+
<button class="button small">Next</button>
|
|
25
|
+
</form>
|
|
17
26
|
</th>
|
|
18
27
|
</tr>
|
|
19
28
|
<tr>
|
|
@@ -67,7 +76,9 @@
|
|
|
67
76
|
<th>
|
|
68
77
|
{{# prev_page }}
|
|
69
78
|
<form action="/log">
|
|
70
|
-
|
|
79
|
+
{{#prev_page_params}}
|
|
80
|
+
<input type="hidden" name="{{name}}" value="{{value}}" />
|
|
81
|
+
{{/prev_page_params}}
|
|
71
82
|
<button class="button small">Prev</button>
|
|
72
83
|
</form>
|
|
73
84
|
{{/ prev_page }}
|
|
@@ -79,7 +90,9 @@
|
|
|
79
90
|
<th></th>
|
|
80
91
|
<th>
|
|
81
92
|
<form action="/log">
|
|
82
|
-
|
|
93
|
+
{{#next_page_params}}
|
|
94
|
+
<input type="hidden" name="{{name}}" value="{{value}}" />
|
|
95
|
+
{{/next_page_params}}
|
|
83
96
|
<button class="button small">Next</button>
|
|
84
97
|
</form>
|
|
85
98
|
|