active_endpoint 0.2.0
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 +7 -0
- data/.gitignore +55 -0
- data/.rspec +2 -0
- data/.rubocop.yml +60 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +25 -0
- data/Gemfile.lock +140 -0
- data/LICENSE +21 -0
- data/README.md +233 -0
- data/Rakefile +6 -0
- data/active_endpoint.gemspec +28 -0
- data/app/assets/config/active_endpoint_manifest.js +2 -0
- data/app/assets/javascripts/active_endpoint/application.js +2 -0
- data/app/assets/stylesheets/active_endpoint/application.css +10 -0
- data/app/controllers/active_endpoint/application_controller.rb +5 -0
- data/app/controllers/active_endpoint/dashboard_controller.rb +7 -0
- data/app/controllers/active_endpoint/probes_controller.rb +22 -0
- data/app/controllers/active_endpoint/unregistred_probes_controller.rb +14 -0
- data/app/helpers/active_endpoint/application_helper.rb +105 -0
- data/app/models/active_endpoint/application_record.rb +5 -0
- data/app/models/active_endpoint/probe.rb +52 -0
- data/app/models/active_endpoint/unregistred_probe.rb +3 -0
- data/app/views/active_endpoint/application/_flashes.html.erb +5 -0
- data/app/views/active_endpoint/application/_header.html.erb +18 -0
- data/app/views/active_endpoint/dashboard/_blacklist.html.erb +57 -0
- data/app/views/active_endpoint/dashboard/_constraints.html.erb +117 -0
- data/app/views/active_endpoint/dashboard/_settings.html.erb +13 -0
- data/app/views/active_endpoint/dashboard/_tags.html.erb +20 -0
- data/app/views/active_endpoint/dashboard/index.html.erb +8 -0
- data/app/views/active_endpoint/probes/index.html.erb +28 -0
- data/app/views/active_endpoint/probes/show.html.erb +43 -0
- data/app/views/active_endpoint/probes/show_response.html.erb +9 -0
- data/app/views/active_endpoint/unregistred_probes/index.html.erb +28 -0
- data/app/views/layouts/active_endpoint/application.html.erb +39 -0
- data/bin/console +14 -0
- data/bin/rails +13 -0
- data/bin/setup +8 -0
- data/config/routes.rb +8 -0
- data/lib/active_endpoint.rb +60 -0
- data/lib/active_endpoint/concerns/configurable.rb +29 -0
- data/lib/active_endpoint/concerns/constraintable.rb +51 -0
- data/lib/active_endpoint/concerns/optionable.rb +41 -0
- data/lib/active_endpoint/concerns/rails_routable.rb +49 -0
- data/lib/active_endpoint/engine.rb +15 -0
- data/lib/active_endpoint/extentions/active_record.rb +30 -0
- data/lib/active_endpoint/logger.rb +28 -0
- data/lib/active_endpoint/proxy.rb +64 -0
- data/lib/active_endpoint/rails/middleware.rb +17 -0
- data/lib/active_endpoint/rails/railtie.rb +13 -0
- data/lib/active_endpoint/request.rb +79 -0
- data/lib/active_endpoint/response.rb +17 -0
- data/lib/active_endpoint/routes/blacklist.rb +67 -0
- data/lib/active_endpoint/routes/cache/proxy.rb +22 -0
- data/lib/active_endpoint/routes/cache/proxy/redis_store_proxy.rb +41 -0
- data/lib/active_endpoint/routes/cache/store.rb +73 -0
- data/lib/active_endpoint/routes/constraint_rule.rb +38 -0
- data/lib/active_endpoint/routes/constraints.rb +81 -0
- data/lib/active_endpoint/routes/matcher.rb +51 -0
- data/lib/active_endpoint/routes/momento.rb +81 -0
- data/lib/active_endpoint/storage.rb +112 -0
- data/lib/active_endpoint/tags.rb +15 -0
- data/lib/active_endpoint/version.rb +3 -0
- data/lib/generators/active_endpoint/install_generator.rb +35 -0
- data/lib/generators/templates/active_endpoint.rb +40 -0
- data/lib/generators/templates/migration.erb +30 -0
- metadata +109 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'active_endpoint/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.version = ActiveEndpoint::VERSION
|
|
8
|
+
|
|
9
|
+
spec.name = 'active_endpoint'
|
|
10
|
+
spec.authors = ['Marat Khusnetdinov']
|
|
11
|
+
spec.email = ['marat@khusnetdinov.ru']
|
|
12
|
+
|
|
13
|
+
spec.summary = 'Your request tracking tool for rails applications'
|
|
14
|
+
spec.description = 'ActiveEndpoint is middleware for Rails applications that collects and analyses requests and responses per request for endpoint. It works with minimal impact on application\'s response time.'
|
|
15
|
+
spec.homepage = 'https://github.com/khusnetdinov/active_endpoint'
|
|
16
|
+
spec.license = 'MIT'
|
|
17
|
+
|
|
18
|
+
spec.homepage = 'https://github.com/khusnetdinov/active_endpoint'
|
|
19
|
+
spec.summary = 'Summary of ActiveEndpoint.'
|
|
20
|
+
spec.description = 'Description of ActiveEndpoint.'
|
|
21
|
+
|
|
22
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
23
|
+
f.match(%r{^(test|spec|features)/})
|
|
24
|
+
end
|
|
25
|
+
spec.bindir = 'exe'
|
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require_dependency 'active_endpoint/application_controller'
|
|
2
|
+
|
|
3
|
+
module ActiveEndpoint
|
|
4
|
+
class ProbesController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@probes_group = ActiveEndpoint::Probe.group_by_endpoint
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
@probes = ActiveEndpoint::Probe.where(endpoint: params[:id])
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show_response
|
|
14
|
+
@probe = ActiveEndpoint::Probe.find(params[:probe_id])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def destroy
|
|
18
|
+
ActiveEndpoint::Probe.find(params[:id]).destroy
|
|
19
|
+
redirect_to :back
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require_dependency 'active_endpoint/application_controller'
|
|
2
|
+
|
|
3
|
+
module ActiveEndpoint
|
|
4
|
+
class UnregistredProbesController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@probes = ActiveEndpoint::UnregistredProbe.all.select(:id, :uuid, :started_at, :path, :query_string)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def destroy
|
|
10
|
+
ActiveEndpoint::UnregistredProbe.find(params[:id]).destroy
|
|
11
|
+
redirect_to action: :index
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module ActiveEndpoint
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
SETTINGS = [
|
|
4
|
+
:cache_store_client,
|
|
5
|
+
:cache_prefix,
|
|
6
|
+
:constraint_limit,
|
|
7
|
+
:constraint_period,
|
|
8
|
+
:logger,
|
|
9
|
+
:log_probe_info,
|
|
10
|
+
:log_debug_info,
|
|
11
|
+
:storage_limit,
|
|
12
|
+
:storage_period,
|
|
13
|
+
:storage_keep_periods
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
TAGS_COLORS = [
|
|
17
|
+
'label-info',
|
|
18
|
+
'label-success',
|
|
19
|
+
'label-warning',
|
|
20
|
+
'label-danger',
|
|
21
|
+
'label-default'
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
def settings
|
|
25
|
+
SETTINGS
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def tags
|
|
29
|
+
tags = []
|
|
30
|
+
ActiveEndpoint.tags.definition.to_a.each_with_index do |definition, index|
|
|
31
|
+
tags << [
|
|
32
|
+
definition.first,
|
|
33
|
+
tag_label_by(index),
|
|
34
|
+
definition.last,
|
|
35
|
+
"ActiveEndpoint::Probe.tagged_as(:#{definition.first})"
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
tags
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def label_for(tag)
|
|
42
|
+
TAGS_COLORS[ActiveEndpoint.tags.definition.keys.index(tag.to_sym)]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def endpoints
|
|
46
|
+
ActiveEndpoint.blacklist.endpoints
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resources
|
|
50
|
+
ActiveEndpoint.blacklist.resources
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def actions
|
|
54
|
+
ActiveEndpoint.blacklist.actions
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def scopes
|
|
58
|
+
ActiveEndpoint.blacklist.scopes
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def endpoints_constraints
|
|
62
|
+
ActiveEndpoint.constraints.endpoints.map do |endpoint, constraints|
|
|
63
|
+
constraints_for_html(endpoint, constraints)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def resources_constraints
|
|
68
|
+
ActiveEndpoint.constraints.resources.map do |endpoint, constraints|
|
|
69
|
+
constraints_for_html(endpoint, constraints)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def actions_constraints
|
|
74
|
+
ActiveEndpoint.constraints.actions.map do |endpoint, constraints|
|
|
75
|
+
constraints_for_html(endpoint, constraints)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def scopes_constraints
|
|
80
|
+
ActiveEndpoint.constraints.scopes.map do |endpoint, constraints|
|
|
81
|
+
constraints_for_html(endpoint, constraints)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def tag_label_by(index)
|
|
88
|
+
if TAGS_COLORS.length > index
|
|
89
|
+
TAGS_COLORS[index]
|
|
90
|
+
else
|
|
91
|
+
TAGS_COLORS.last
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def constraints_for_html(endpoint, constraints)
|
|
96
|
+
rule = constraints[:rule]
|
|
97
|
+
storage = constraints[:storage]
|
|
98
|
+
|
|
99
|
+
rules = "#{rule[:limit]} per #{distance_of_time_in_words(rule[:period])}"
|
|
100
|
+
storages = "#{storage[:limit]} per #{distance_of_time_in_words(storage[:period])}"
|
|
101
|
+
|
|
102
|
+
[endpoint, rules, storages]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module ActiveEndpoint
|
|
2
|
+
class Probe < ApplicationRecord
|
|
3
|
+
validates :uuid, :started_at, :duration, :endpoint, :ip, :path,
|
|
4
|
+
:request_method, :url, presence: true
|
|
5
|
+
|
|
6
|
+
validates :finished_at, presence: true, unless: :skip_validation?
|
|
7
|
+
|
|
8
|
+
scope :unregistred, ->() { where(endpoint: :unregistred) }
|
|
9
|
+
scope :registred, ->() { where.not(endpoint: :unregistred) }
|
|
10
|
+
scope :probe, ->(endpoint) { where(endpoint: endpoint) }
|
|
11
|
+
scope :taken_before, ->(period) { where('created_at < ?', period) }
|
|
12
|
+
scope :group_by_endpoint, ->() {
|
|
13
|
+
execute_statement("
|
|
14
|
+
select count(*) as amount, endpoint, request_method as method, avg(duration) as duration
|
|
15
|
+
from active_endpoint_probes group by endpoint, request_method
|
|
16
|
+
having endpoint != 'unregistred'
|
|
17
|
+
")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def tag
|
|
21
|
+
definitions = ActiveEndpoint.tags.definition
|
|
22
|
+
methods = ActiveEndpoint::Extentions::ActiveRecord::METHODS
|
|
23
|
+
|
|
24
|
+
definitions.each do |tag_name, operators|
|
|
25
|
+
last_operation_index = operators.length - 1
|
|
26
|
+
|
|
27
|
+
exec_operator = ''
|
|
28
|
+
operators.each_with_index do |(key, value), index|
|
|
29
|
+
exec_operator << "#{duration * 1000} #{methods[key]} #{value}"
|
|
30
|
+
exec_operator << (index == last_operation_index ? '' : ' && ')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
return tag_name if eval(exec_operator)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def skip_validation?
|
|
40
|
+
type == 'ActiveEndpoint::UnregistredProbe'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.execute_statement(sql)
|
|
44
|
+
results = ActiveRecord::Base.connection.execute(sql)
|
|
45
|
+
if results.present?
|
|
46
|
+
return results.map(&:deep_symbolize_keys)
|
|
47
|
+
else
|
|
48
|
+
return nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<nav class="navbar-default navbar-toggleable-md navbar-light bg-faded">
|
|
2
|
+
<div class="container">
|
|
3
|
+
<button class="navbar-toggler navbar-toggler-right" style="top: 16px;" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
|
4
|
+
<span class="navbar-toggler-icon"></span>
|
|
5
|
+
</button>
|
|
6
|
+
<%= link_to 'ActiveEndpoint', active_endpoint.root_path, class: 'navbar-brand'%>
|
|
7
|
+
<div class="collapse navbar-collapse" id="navbarNav">
|
|
8
|
+
<ul class="nav navbar-nav">
|
|
9
|
+
<li class="<%= controller_name == 'probes' ? 'active' : '' %>">
|
|
10
|
+
<%= link_to 'Probes', active_endpoint.probes_path, class: '' %>
|
|
11
|
+
</li>
|
|
12
|
+
<li class="<%= controller_name == 'unregistred_probes' ? 'active' : '' %>">
|
|
13
|
+
<%= link_to 'Unregistred Probes', active_endpoint.unregistred_probes_path, class: '' %>
|
|
14
|
+
</li>
|
|
15
|
+
</ul>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</nav>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<div class="card-header">Blacklist</div>
|
|
3
|
+
<ul class="list-group">
|
|
4
|
+
<li class="list-group-item">
|
|
5
|
+
<div class="row">
|
|
6
|
+
<div class="col">
|
|
7
|
+
<table class="table">
|
|
8
|
+
<thead class="thead-default">
|
|
9
|
+
<tr><th>Endpoints</th></tr>
|
|
10
|
+
</thead>
|
|
11
|
+
<tbody>
|
|
12
|
+
<%- endpoints.each do |endpoint| %>
|
|
13
|
+
<tr><td><%= endpoint %></td></tr>
|
|
14
|
+
<% end %>
|
|
15
|
+
</tbody>
|
|
16
|
+
</table>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="col">
|
|
19
|
+
<table class="table">
|
|
20
|
+
<thead class="thead-default">
|
|
21
|
+
<tr><th>Resources</th></tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
<%- resources.each do |resource| %>
|
|
25
|
+
<tr><td><%= resource %></td></tr>
|
|
26
|
+
<% end %>
|
|
27
|
+
</tbody>
|
|
28
|
+
</table>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="col">
|
|
31
|
+
<table class="table">
|
|
32
|
+
<thead class="thead-default">
|
|
33
|
+
<tr><th>Actions</th></tr>
|
|
34
|
+
</thead>
|
|
35
|
+
<tbody>
|
|
36
|
+
<%- actions.each do |actions| %>
|
|
37
|
+
<tr><td><%= actions %></td></tr>
|
|
38
|
+
<% end %>
|
|
39
|
+
</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="col">
|
|
43
|
+
<table class="table">
|
|
44
|
+
<thead class="thead-default">
|
|
45
|
+
<tr><th>Scope</th></tr>
|
|
46
|
+
</thead>
|
|
47
|
+
<tbody>
|
|
48
|
+
<%- scopes.each do |scopes| %>
|
|
49
|
+
<tr><td><%= scopes %></td></tr>
|
|
50
|
+
<% end %>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</li>
|
|
56
|
+
</ul>
|
|
57
|
+
</div>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<div class="card-header">Constraints</div>
|
|
3
|
+
<ul class="list-group">
|
|
4
|
+
<li class="list-group-item">
|
|
5
|
+
<div class="row">
|
|
6
|
+
<div class="col">
|
|
7
|
+
<table class="table">
|
|
8
|
+
<thead class="thead-default">
|
|
9
|
+
<tr>
|
|
10
|
+
<th>Endpoints</th>
|
|
11
|
+
<th>Request limits</th>
|
|
12
|
+
<th>Storage limits</th>
|
|
13
|
+
</tr>
|
|
14
|
+
</thead>
|
|
15
|
+
<tbody>
|
|
16
|
+
<%- if endpoints_constraints.any? %>
|
|
17
|
+
<%- endpoints_constraints.each do |endpoint, request, storage| %>
|
|
18
|
+
<tr>
|
|
19
|
+
<td style="width: 33%;"><%= endpoint %></td>
|
|
20
|
+
<td style="width: 33%;"><%= request %></td>
|
|
21
|
+
<td style="width: 33%;"><%= storage %></td>
|
|
22
|
+
</tr>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% else %>
|
|
25
|
+
<tr><td colspan="3" style="text-align: center;">No Constraints</tr>
|
|
26
|
+
<% end %>
|
|
27
|
+
</tbody>
|
|
28
|
+
</table>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</li>
|
|
32
|
+
<li class="list-group-item">
|
|
33
|
+
<div class="row">
|
|
34
|
+
<div class="col">
|
|
35
|
+
<table class="table">
|
|
36
|
+
<thead class="thead-default">
|
|
37
|
+
<tr>
|
|
38
|
+
<th>Resources</th>
|
|
39
|
+
<th>Request limits</th>
|
|
40
|
+
<th>Storage limits</th>
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
<% if resources_constraints.any? %>
|
|
45
|
+
<%- resources_constraints.each do |endpoint, request, storage| %>
|
|
46
|
+
<tr>
|
|
47
|
+
<td style="width: 33%;"><%= endpoint %></td>
|
|
48
|
+
<td style="width: 33%;"><%= request %></td>
|
|
49
|
+
<td style="width: 33%;"><%= storage %></td>
|
|
50
|
+
</tr>
|
|
51
|
+
<% end %>
|
|
52
|
+
<% else %>
|
|
53
|
+
<tr><td colspan="3" style="text-align: center;">No Constraints</tr>
|
|
54
|
+
<% end %>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</li>
|
|
60
|
+
<li class="list-group-item">
|
|
61
|
+
<div class="row">
|
|
62
|
+
<div class="col">
|
|
63
|
+
<table class="table">
|
|
64
|
+
<thead class="thead-default">
|
|
65
|
+
<tr>
|
|
66
|
+
<th>Actions</th>
|
|
67
|
+
<th>Request limits</th>
|
|
68
|
+
<th>Storage limits</th>
|
|
69
|
+
</tr>
|
|
70
|
+
</thead>
|
|
71
|
+
<tbody>
|
|
72
|
+
<% if actions_constraints.any? %>
|
|
73
|
+
<%- actions_constraints.each do |endpoint, request, storage| %>
|
|
74
|
+
<tr>
|
|
75
|
+
<td style="width: 33%;"><%= endpoint %></td>
|
|
76
|
+
<td style="width: 33%;"><%= request %></td>
|
|
77
|
+
<td style="width: 33%;"> <%= storage %></td>
|
|
78
|
+
</tr>
|
|
79
|
+
<% end %>
|
|
80
|
+
<% else %>
|
|
81
|
+
<tr><td colspan="3" style="text-align: center;">No Constraints</tr>
|
|
82
|
+
<% end %>
|
|
83
|
+
</tbody>
|
|
84
|
+
</table>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</li>
|
|
88
|
+
<li class="list-group-item">
|
|
89
|
+
<div class="row">
|
|
90
|
+
<div class="col">
|
|
91
|
+
<table class="table">
|
|
92
|
+
<thead class="thead-default">
|
|
93
|
+
<tr>
|
|
94
|
+
<th>Scopes</th>
|
|
95
|
+
<th>Request limits</th>
|
|
96
|
+
<th>Storage limits</th>
|
|
97
|
+
</tr>
|
|
98
|
+
</thead>
|
|
99
|
+
<tbody>
|
|
100
|
+
<% if scopes_constraints.any? %>
|
|
101
|
+
<%- scopes_constraints.each do |endpoint, request, storage| %>
|
|
102
|
+
<tr>
|
|
103
|
+
<td style="width: 33%;"><%= endpoint %></td>
|
|
104
|
+
<td style="width: 33%;"><%= request %></td>
|
|
105
|
+
<td style="width: 33%;"><%= storage %></td>
|
|
106
|
+
</tr>
|
|
107
|
+
<% end %>
|
|
108
|
+
<% else %>
|
|
109
|
+
<tr><td colspan="3" style="text-align: center;">No Constraints</tr>
|
|
110
|
+
<% end %>
|
|
111
|
+
</tbody>
|
|
112
|
+
</table>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</li>
|
|
116
|
+
</ul>
|
|
117
|
+
</div>
|