stoplight 4.1.0 → 5.0.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/README.md +289 -350
- data/lib/stoplight/admin/actions/action.rb +24 -0
- data/lib/stoplight/admin/actions/lock.rb +23 -0
- data/lib/stoplight/admin/actions/lock_all_green.rb +18 -0
- data/lib/stoplight/admin/actions/lock_green.rb +23 -0
- data/lib/stoplight/admin/actions/lock_red.rb +23 -0
- data/lib/stoplight/admin/actions/stats.rb +27 -0
- data/lib/stoplight/admin/actions/unlock.rb +23 -0
- data/lib/stoplight/admin/dependencies.rb +50 -0
- data/lib/stoplight/admin/helpers.rb +27 -0
- data/lib/stoplight/admin/lights_repository/light.rb +155 -0
- data/lib/stoplight/admin/lights_repository.rb +74 -0
- data/lib/stoplight/admin/lights_stats.rb +77 -0
- data/lib/stoplight/admin/views/_card.erb +120 -0
- data/lib/stoplight/admin/views/index.erb +36 -0
- data/lib/stoplight/admin/views/layout.erb +66 -0
- data/lib/stoplight/admin.rb +68 -0
- data/lib/stoplight/color.rb +3 -3
- data/lib/stoplight/config/config_provider.rb +62 -0
- data/lib/stoplight/config/library_default_config.rb +29 -0
- data/lib/stoplight/config/user_default_config.rb +83 -0
- data/lib/stoplight/data_store/base.rb +59 -33
- data/lib/stoplight/data_store/fail_safe.rb +105 -0
- data/lib/stoplight/data_store/memory.rb +257 -50
- data/lib/stoplight/data_store/redis/get_metadata.lua +38 -0
- data/lib/stoplight/data_store/redis/lua.rb +23 -0
- data/lib/stoplight/data_store/redis/record_failure.lua +36 -0
- data/lib/stoplight/data_store/redis/record_success.lua +35 -0
- data/lib/stoplight/data_store/redis/transition_to_green.lua +10 -0
- data/lib/stoplight/data_store/redis/transition_to_red.lua +10 -0
- data/lib/stoplight/data_store/redis/transition_to_yellow.lua +9 -0
- data/lib/stoplight/data_store/redis.rb +345 -106
- data/lib/stoplight/default.rb +11 -9
- data/lib/stoplight/error.rb +1 -13
- data/lib/stoplight/failure.rb +14 -13
- data/lib/stoplight/light/config.rb +118 -0
- data/lib/stoplight/light/configuration_builder_interface.rb +128 -0
- data/lib/stoplight/light/green_run_strategy.rb +53 -0
- data/lib/stoplight/light/red_run_strategy.rb +26 -0
- data/lib/stoplight/light/run_strategy.rb +30 -0
- data/lib/stoplight/light/yellow_run_strategy.rb +78 -0
- data/lib/stoplight/light.rb +164 -84
- data/lib/stoplight/metadata.rb +71 -0
- data/lib/stoplight/notifier/base.rb +14 -7
- data/lib/stoplight/notifier/fail_safe.rb +67 -0
- data/lib/stoplight/notifier/generic.rb +54 -5
- data/lib/stoplight/rspec/generic_notifier.rb +11 -12
- data/lib/stoplight/rspec.rb +1 -1
- data/lib/stoplight/state.rb +3 -3
- data/lib/stoplight/traffic_control/base.rb +35 -0
- data/lib/stoplight/traffic_control/consecutive_failures.rb +43 -0
- data/lib/stoplight/traffic_recovery/base.rb +51 -0
- data/lib/stoplight/traffic_recovery/single_success.rb +35 -0
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight.rb +111 -51
- metadata +49 -98
- data/lib/stoplight/builder.rb +0 -70
- data/lib/stoplight/circuit_breaker.rb +0 -102
- data/lib/stoplight/configurable.rb +0 -95
- data/lib/stoplight/configuration.rb +0 -126
- data/lib/stoplight/light/deprecated.rb +0 -44
- data/lib/stoplight/light/lockable.rb +0 -45
- data/lib/stoplight/light/runnable.rb +0 -127
- data/lib/stoplight/notifier.rb +0 -6
- data/spec/spec_helper.rb +0 -22
- data/spec/stoplight/builder_spec.rb +0 -165
- data/spec/stoplight/circuit_breaker_spec.rb +0 -43
- data/spec/stoplight/color_spec.rb +0 -39
- data/spec/stoplight/configurable_spec.rb +0 -25
- data/spec/stoplight/data_store/base_spec.rb +0 -71
- data/spec/stoplight/data_store/memory_spec.rb +0 -22
- data/spec/stoplight/data_store/redis_spec.rb +0 -45
- data/spec/stoplight/data_store_spec.rb +0 -9
- data/spec/stoplight/default_spec.rb +0 -80
- data/spec/stoplight/error_spec.rb +0 -39
- data/spec/stoplight/failure_spec.rb +0 -108
- data/spec/stoplight/light/lockable_spec.rb +0 -93
- data/spec/stoplight/light/runnable_spec.rb +0 -38
- data/spec/stoplight/light_spec.rb +0 -156
- data/spec/stoplight/notifier/base_spec.rb +0 -18
- data/spec/stoplight/notifier/generic_spec.rb +0 -50
- data/spec/stoplight/notifier/io_spec.rb +0 -41
- data/spec/stoplight/notifier/logger_spec.rb +0 -75
- data/spec/stoplight/notifier_spec.rb +0 -9
- data/spec/stoplight/state_spec.rb +0 -39
- data/spec/stoplight/version_spec.rb +0 -9
- data/spec/stoplight_spec.rb +0 -32
- data/spec/support/configurable.rb +0 -69
- data/spec/support/data_store/base/clear_failures.rb +0 -18
- data/spec/support/data_store/base/clear_state.rb +0 -20
- data/spec/support/data_store/base/get_all.rb +0 -44
- data/spec/support/data_store/base/get_failures.rb +0 -30
- data/spec/support/data_store/base/get_state.rb +0 -7
- data/spec/support/data_store/base/names.rb +0 -29
- data/spec/support/data_store/base/record_failures.rb +0 -70
- data/spec/support/data_store/base/set_state.rb +0 -15
- data/spec/support/data_store/base/with_notification_lock.rb +0 -27
- data/spec/support/data_store/base.rb +0 -21
- data/spec/support/database_cleaner.rb +0 -26
- data/spec/support/exception_helpers.rb +0 -9
- data/spec/support/light/runnable/color.rb +0 -79
- data/spec/support/light/runnable/run.rb +0 -247
- data/spec/support/light/runnable/state.rb +0 -31
- data/spec/support/light/runnable.rb +0 -5
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Admin
|
5
|
+
module Actions
|
6
|
+
# @abstract
|
7
|
+
class Action
|
8
|
+
# @!attribute lights_repository
|
9
|
+
# @return [Stoplight::Admin::LightsRepository]
|
10
|
+
attr_reader :lights_repository
|
11
|
+
private :lights_repository
|
12
|
+
|
13
|
+
# @return lights_repository [Stoplight::Admin::LightsRepository]
|
14
|
+
def initialize(lights_repository:)
|
15
|
+
@lights_repository = lights_repository
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(params)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Admin
|
5
|
+
module Actions
|
6
|
+
# This action locks light
|
7
|
+
class Lock < Action
|
8
|
+
# @param params [Hash] query parameters
|
9
|
+
# @return [void]
|
10
|
+
def call(params)
|
11
|
+
light_names(params).each do |name|
|
12
|
+
lights_repository.lock(name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private def light_names(params)
|
17
|
+
Array(params[:names])
|
18
|
+
.map { |name| CGI.unescape(name) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Admin
|
5
|
+
module Actions
|
6
|
+
# This action locks all lights green
|
7
|
+
class LockAllGreen < Action
|
8
|
+
# @return [void]
|
9
|
+
def call(*)
|
10
|
+
lights_repository
|
11
|
+
.with_color(RED, YELLOW)
|
12
|
+
.map(&:name)
|
13
|
+
.each { |name| lights_repository.lock(name, GREEN) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Admin
|
5
|
+
module Actions
|
6
|
+
# This action locks light with the specific name green
|
7
|
+
class LockGreen < Action
|
8
|
+
# @param params [Hash] query parameters
|
9
|
+
# @return [void]
|
10
|
+
def call(params)
|
11
|
+
light_names(params).each do |name|
|
12
|
+
lights_repository.lock(name, GREEN)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private def light_names(params)
|
17
|
+
Array(params[:names])
|
18
|
+
.map { |name| CGI.unescape(name) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Admin
|
5
|
+
module Actions
|
6
|
+
# This action locks light with the specific name red
|
7
|
+
class LockRed < Action
|
8
|
+
# @param params [Hash] query parameters
|
9
|
+
# @return [void]
|
10
|
+
def call(params)
|
11
|
+
light_names(params).each do |name|
|
12
|
+
lights_repository.lock(name, RED)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private def light_names(params)
|
17
|
+
Array(params[:names])
|
18
|
+
.map { |name| CGI.unescape(name) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Admin
|
5
|
+
module Actions
|
6
|
+
class Stats < Action
|
7
|
+
# @!attribute lights_stats
|
8
|
+
# @return [Class<Stoplight::Admin::LightsStats>]
|
9
|
+
attr_reader :lights_stats
|
10
|
+
private :lights_stats
|
11
|
+
|
12
|
+
# @param lights_stats [Class<Stoplight::Admin::LightsStats>]
|
13
|
+
def initialize(lights_stats:, **deps)
|
14
|
+
super(**deps)
|
15
|
+
@lights_stats = lights_stats
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [(Stoplight::Admin::LightsRepository::Light)]
|
19
|
+
def call(*)
|
20
|
+
lights = lights_repository.all
|
21
|
+
stats = lights_stats.call(lights)
|
22
|
+
[lights, stats]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Admin
|
5
|
+
module Actions
|
6
|
+
# This action unlocks light
|
7
|
+
class Unlock < Action
|
8
|
+
# @param params [Hash] query parameters
|
9
|
+
# @return [void]
|
10
|
+
def call(params)
|
11
|
+
light_names(params).each do |name|
|
12
|
+
lights_repository.unlock(name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private def light_names(params)
|
17
|
+
Array(params[:names])
|
18
|
+
.map { |name| CGI.unescape(name) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Admin
|
5
|
+
class Dependencies
|
6
|
+
# @!attribute data_store
|
7
|
+
# @return [Stoplight::DataStore::Base]
|
8
|
+
attr_reader :data_store
|
9
|
+
private :data_store
|
10
|
+
|
11
|
+
# @param data_store [Stoplight::DataStore::Base]
|
12
|
+
def initialize(data_store:)
|
13
|
+
@data_store = data_store
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Stoplight::Admin::LightsRepository]
|
17
|
+
def lights_repository
|
18
|
+
Stoplight::Admin::LightsRepository.new(data_store: data_store)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Stoplight::Admin::Actions::Stats]
|
22
|
+
def stats_action
|
23
|
+
Stoplight::Admin::Actions::Stats.new(
|
24
|
+
lights_repository: lights_repository,
|
25
|
+
lights_stats: Stoplight::Admin::LightsStats
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Stoplight::Admin::Actions::Unlock]
|
30
|
+
def unlock_action
|
31
|
+
Stoplight::Admin::Actions::Unlock.new(lights_repository: lights_repository)
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Stoplight::Admin::Actions::LockGreen]
|
35
|
+
def green_action
|
36
|
+
Stoplight::Admin::Actions::LockGreen.new(lights_repository: lights_repository)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Stoplight::Admin::Actions::LockRed]
|
40
|
+
def red_action
|
41
|
+
Stoplight::Admin::Actions::LockRed.new(lights_repository: lights_repository)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Stoplight::Admin::Actions::LockAllGreen]
|
45
|
+
def green_all_action
|
46
|
+
Stoplight::Admin::Actions::LockAllGreen.new(lights_repository: lights_repository)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Admin
|
5
|
+
module Helpers
|
6
|
+
COLORS = [
|
7
|
+
GREEN = Stoplight::Color::GREEN,
|
8
|
+
YELLOW = Stoplight::Color::YELLOW,
|
9
|
+
RED = Stoplight::Color::RED
|
10
|
+
].freeze
|
11
|
+
|
12
|
+
# @return [Stoplight::Admin::Dependencies]
|
13
|
+
def dependencies
|
14
|
+
Dependencies.new(data_store:)
|
15
|
+
end
|
16
|
+
|
17
|
+
private def data_store
|
18
|
+
settings.data_store.tap do |data_store|
|
19
|
+
if data_store.is_a?(Stoplight::DataStore::Memory)
|
20
|
+
raise "Stoplight Admin requires a persistent data store, but the current data store is Memory. " \
|
21
|
+
"Please configure a different data store in your Stoplight configuration."
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module Stoplight
|
6
|
+
class Admin
|
7
|
+
class LightsRepository
|
8
|
+
class Light
|
9
|
+
COLORS = [
|
10
|
+
GREEN = Stoplight::Color::GREEN,
|
11
|
+
YELLOW = Stoplight::Color::YELLOW,
|
12
|
+
RED = Stoplight::Color::RED
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
# @!attribute id
|
16
|
+
# @return [String]
|
17
|
+
attr_reader :id
|
18
|
+
|
19
|
+
# @!attribute name
|
20
|
+
# @return [String]
|
21
|
+
attr_reader :name
|
22
|
+
|
23
|
+
# @!attribute color
|
24
|
+
# @return [String]
|
25
|
+
attr_reader :color
|
26
|
+
|
27
|
+
# @!attribute state
|
28
|
+
# @return [String]
|
29
|
+
attr_reader :state
|
30
|
+
|
31
|
+
# @!attribute failures
|
32
|
+
# @return [<Stoplight::Failure>]
|
33
|
+
attr_reader :failures
|
34
|
+
|
35
|
+
# @param name [String]
|
36
|
+
# @param color [String]
|
37
|
+
# @param state [String]
|
38
|
+
# @param failures [<Stoplight::Failure>]
|
39
|
+
def initialize(name:, color:, state:, failures:)
|
40
|
+
@id = SecureRandom.uuid
|
41
|
+
@name = name
|
42
|
+
@color = color
|
43
|
+
@state = state
|
44
|
+
@failures = failures
|
45
|
+
end
|
46
|
+
|
47
|
+
def latest_failure
|
48
|
+
failures.first
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Boolean]
|
52
|
+
def locked?
|
53
|
+
!unlocked?
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Boolean]
|
57
|
+
def unlocked?
|
58
|
+
state == Stoplight::State::UNLOCKED
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Hash]
|
62
|
+
def as_json
|
63
|
+
{
|
64
|
+
name: name,
|
65
|
+
color: color,
|
66
|
+
failures: failures,
|
67
|
+
locked: locked?
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Array]
|
72
|
+
def default_sort_key
|
73
|
+
[-COLORS.index(color), name]
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [String, nil]
|
77
|
+
def last_check_in_words
|
78
|
+
last_error_time = latest_failure&.time
|
79
|
+
return unless last_error_time
|
80
|
+
|
81
|
+
time_difference = Time.now - last_error_time
|
82
|
+
if time_difference < 1
|
83
|
+
"just now"
|
84
|
+
elsif time_difference < 60
|
85
|
+
"#{time_difference.to_i}s ago"
|
86
|
+
elsif time_difference < 3600
|
87
|
+
"#{(time_difference / 60).to_i}m ago"
|
88
|
+
else
|
89
|
+
"#{(time_difference / 3600).to_i}h ago"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# @return [String]
|
94
|
+
def description_title
|
95
|
+
case color
|
96
|
+
when RED
|
97
|
+
if locked? && failures.empty?
|
98
|
+
"Locked Open"
|
99
|
+
else
|
100
|
+
"Last Error"
|
101
|
+
end
|
102
|
+
when Stoplight::Color::YELLOW
|
103
|
+
"Testing Recovery"
|
104
|
+
when GREEN
|
105
|
+
if locked?
|
106
|
+
"Forced Healthy"
|
107
|
+
else
|
108
|
+
"Healthy"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return [String]
|
114
|
+
def description_message
|
115
|
+
case color
|
116
|
+
when RED
|
117
|
+
if locked? && failures.empty?
|
118
|
+
"Circuit manually locked open"
|
119
|
+
else
|
120
|
+
"#{latest_failure.error_class}: #{latest_failure.error_message}"
|
121
|
+
end
|
122
|
+
when Stoplight::Color::YELLOW
|
123
|
+
"#{latest_failure.error_class}: #{latest_failure.error_message}"
|
124
|
+
when GREEN
|
125
|
+
if locked?
|
126
|
+
"Circuit manually locked closed"
|
127
|
+
else
|
128
|
+
"No recent errors"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [String]
|
134
|
+
def description_comment
|
135
|
+
case color
|
136
|
+
when RED
|
137
|
+
if locked?
|
138
|
+
"Override active - all requests blocked"
|
139
|
+
else
|
140
|
+
"Will attempt recovery after cooling period"
|
141
|
+
end
|
142
|
+
when YELLOW
|
143
|
+
"Allowing limited test traffic (0 of 1 requests)"
|
144
|
+
when GREEN
|
145
|
+
if locked?
|
146
|
+
"Override active - all requests processed"
|
147
|
+
else
|
148
|
+
"Operating normally"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Admin
|
5
|
+
class LightsRepository
|
6
|
+
# @!attribute data_store
|
7
|
+
# @return [Stoplight::DataStore::Base]
|
8
|
+
attr_reader :data_store
|
9
|
+
private :data_store
|
10
|
+
|
11
|
+
# @param data_store [Stoplight::DataStore::Base]
|
12
|
+
def initialize(data_store:)
|
13
|
+
@data_store = data_store
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [<Stoplight::Admin::LightsRepository::Light>]
|
17
|
+
def all
|
18
|
+
data_store
|
19
|
+
.names
|
20
|
+
.map { |name| load_light(name) }
|
21
|
+
.sort_by(&:default_sort_key)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param colors <String>] colors name
|
25
|
+
# @return [<Stoplight::Admin::LightsRepository::Light>] lights with the requested colors
|
26
|
+
#
|
27
|
+
def with_color(*colors)
|
28
|
+
requested_colors = Array(colors)
|
29
|
+
|
30
|
+
all.select do |light|
|
31
|
+
requested_colors.include?(light.color)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param name [String] locks light by its name
|
36
|
+
# @param color [String, nil] locks to this color. When nil is given, locks to the current
|
37
|
+
# color
|
38
|
+
# @return [void]
|
39
|
+
def lock(name, color = nil)
|
40
|
+
light = build_light(name)
|
41
|
+
|
42
|
+
case color || light.color
|
43
|
+
when Stoplight::Color::GREEN
|
44
|
+
light.lock(Stoplight::Color::GREEN)
|
45
|
+
else
|
46
|
+
light.lock(Stoplight::Color::RED)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param name [String] unlocks light by its name
|
51
|
+
# @return [void]
|
52
|
+
def unlock(name)
|
53
|
+
build_light(name).unlock
|
54
|
+
end
|
55
|
+
|
56
|
+
private def load_light(name)
|
57
|
+
light = build_light(name)
|
58
|
+
# failures, state
|
59
|
+
metadata = data_store.get_metadata(light.config)
|
60
|
+
|
61
|
+
Light.new(
|
62
|
+
name: name,
|
63
|
+
color: light.color,
|
64
|
+
state: metadata.locked_state,
|
65
|
+
failures: [metadata.last_error].compact
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
private def build_light(name)
|
70
|
+
Stoplight(name, data_store: data_store)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Admin
|
5
|
+
class LightsStats
|
6
|
+
EMPTY_STATS = {
|
7
|
+
count_red: 0, count_yellow: 0, count_green: 0,
|
8
|
+
percent_red: 0, percent_yellow: 0, percent_green: 0
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
# @!attribute lights
|
12
|
+
# @return [<Stoplight::Admin::LightsRepository::Light>]
|
13
|
+
attr_reader :lights
|
14
|
+
private :lights
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def call(lights)
|
18
|
+
new(lights).call
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param lights [<Stoplight::Admin::LightsRepository::Light>]
|
23
|
+
def initialize(lights)
|
24
|
+
@lights = lights
|
25
|
+
end
|
26
|
+
|
27
|
+
def call
|
28
|
+
return EMPTY_STATS if size.zero?
|
29
|
+
|
30
|
+
EMPTY_STATS.merge(
|
31
|
+
count_red: count_red,
|
32
|
+
count_yellow: count_yellow,
|
33
|
+
count_green: count_green,
|
34
|
+
percent_red: percent_red,
|
35
|
+
percent_yellow: percent_yellow,
|
36
|
+
percent_green: percent_green
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
private def count_red
|
41
|
+
count_lights(RED)
|
42
|
+
end
|
43
|
+
|
44
|
+
private def percent_red
|
45
|
+
percent_lights(RED)
|
46
|
+
end
|
47
|
+
|
48
|
+
private def count_green
|
49
|
+
count_lights(GREEN)
|
50
|
+
end
|
51
|
+
|
52
|
+
private def percent_green
|
53
|
+
percent_lights(GREEN)
|
54
|
+
end
|
55
|
+
|
56
|
+
private def count_yellow
|
57
|
+
count_lights(YELLOW)
|
58
|
+
end
|
59
|
+
|
60
|
+
private def percent_yellow
|
61
|
+
percent_lights(YELLOW)
|
62
|
+
end
|
63
|
+
|
64
|
+
private def count_lights(color)
|
65
|
+
lights.count { |l| l.color == color }
|
66
|
+
end
|
67
|
+
|
68
|
+
private def percent_lights(color)
|
69
|
+
(100 * count_lights(color).fdiv(size)).ceil
|
70
|
+
end
|
71
|
+
|
72
|
+
private def size
|
73
|
+
lights.size
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
<div class="max-w-xl mr-5 p-6 border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
2
|
+
<div class="flex items-center">
|
3
|
+
<% light_name = ERB::Util.html_escape(light.name) %>
|
4
|
+
|
5
|
+
<div class="relative">
|
6
|
+
<div class="relative inline-flex items-center justify-center w-10 h-10 overflow-hidden bg-<%= color %>-100 rounded-full dark:bg-<%= color %>-600">
|
7
|
+
<span class="font-medium text-<%= color %>-600 dark:text-<%= color %>-300"><%= color[0].upcase %></span>
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<div class="absolute inline-flex w-4 h-4 bg-white dark:bg-gray-800 border-2 border-white rounded-full bottom-0 right-0 dark:border-gray-900">
|
11
|
+
<svg class="w-3 h-3 text-<%=color %>-900 dark:text-<%=color %>-200" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
12
|
+
<% if light.locked? %>
|
13
|
+
<circle cx="12" cy="16" r="1"/>
|
14
|
+
<rect x="3" y="10" width="18" height="12" rx="2"/>
|
15
|
+
<path d="M7 10V7a5 5 0 0 1 10 0v3"/>
|
16
|
+
<% else %>
|
17
|
+
<circle cx="12" cy="16" r="1"/>
|
18
|
+
<rect width="18" height="12" x="3" y="10" rx="2"/>
|
19
|
+
<path d="M7 10V7a5 5 0 0 1 9.33-2.5"/>
|
20
|
+
<% end %>
|
21
|
+
</svg>
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<div class="flex-1 min-w-0 ms-4">
|
26
|
+
<p class="text-sm font-medium text-gray-900 truncate dark:text-white">
|
27
|
+
<%= light.name %>
|
28
|
+
</p>
|
29
|
+
<span class="flex items-center text-sm font-medium text-gray-500 dark:text-gray-400 truncate me-3">
|
30
|
+
<span class="flex w-2.5 h-2.5 bg-<%= color %>-600 rounded-full me-1.5 shrink-0"></span>
|
31
|
+
<% if light.color == "red" %>
|
32
|
+
Open
|
33
|
+
<% elsif light.color == "yellow" %>
|
34
|
+
Half-Open
|
35
|
+
<% else %>
|
36
|
+
Closed
|
37
|
+
<% end %>
|
38
|
+
|
39
|
+
<% if light.color == "yellow" %>
|
40
|
+
(Recovering)
|
41
|
+
<% elsif light.locked? %>
|
42
|
+
(Locked)
|
43
|
+
<% end %>
|
44
|
+
</span>
|
45
|
+
</div>
|
46
|
+
|
47
|
+
<button id="dropdownMenuIconHorizontalButton" data-dropdown-toggle="dropdownDotsHorizontal-<%= light.id %>" class="inline-flex items-center p-2 text-sm font-medium text-center text-gray-900 bg-white rounded-lg hover:bg-gray-100 focus:ring-4 focus:outline-none dark:text-white focus:ring-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-600" type="button">
|
48
|
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 3">
|
49
|
+
<path d="M2 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm6.041 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM14 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Z"/>
|
50
|
+
</svg>
|
51
|
+
</button>
|
52
|
+
|
53
|
+
<!-- Dropdown menu -->
|
54
|
+
<div id="dropdownDotsHorizontal-<%= light.id %>" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600">
|
55
|
+
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownMenuIconHorizontalButton">
|
56
|
+
<li>
|
57
|
+
<a href="<%= url("/unlock?names=#{light_name}") %>" data-turbo-method="post" class="flex items-center py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
|
58
|
+
<svg class="flex w-4 h-4 me-1.5 shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
59
|
+
<circle cx="12" cy="16" r="1"/>
|
60
|
+
<rect width="18" height="12" x="3" y="10" rx="2"/>
|
61
|
+
<path d="M7 10V7a5 5 0 0 1 9.33-2.5"/>
|
62
|
+
</svg>
|
63
|
+
Unlock
|
64
|
+
</a>
|
65
|
+
</li>
|
66
|
+
<li>
|
67
|
+
<a href="<%= url("/red?names=#{light_name}") %>" data-turbo-method="post" class="flex items-center py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
|
68
|
+
<svg class="flex w-4 h-4 me-1.5 text-red-600 shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
69
|
+
<circle cx="12" cy="16" r="1"/>
|
70
|
+
<rect x="3" y="10" width="18" height="12" rx="2"/>
|
71
|
+
<path d="M7 10V7a5 5 0 0 1 10 0v3"/>
|
72
|
+
</svg>
|
73
|
+
Lock Red
|
74
|
+
</a>
|
75
|
+
</li>
|
76
|
+
<li>
|
77
|
+
<a href="<%= url("/green?names=#{light_name}") %>" data-turbo-method="post" class="flex items-center py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
|
78
|
+
<svg class="flex w-4 h-4 me-1.5 text-green-600 shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
79
|
+
<circle cx="12" cy="16" r="1"/>
|
80
|
+
<rect x="3" y="10" width="18" height="12" rx="2"/>
|
81
|
+
<path d="M7 10V7a5 5 0 0 1 10 0v3"/>
|
82
|
+
</svg>
|
83
|
+
Lock Green
|
84
|
+
</a>
|
85
|
+
</li>
|
86
|
+
</ul>
|
87
|
+
</div>
|
88
|
+
</div>
|
89
|
+
|
90
|
+
<div class="p-4 my-4 text-sm text-<%= color %>-800 rounded-lg bg-<%= color %>-50 dark:bg-gray-800 dark:text-<%= color %>-400">
|
91
|
+
<div class="flex justify-between items-center">
|
92
|
+
<div class="pr-4">
|
93
|
+
<p class="font-semibold"><%= light.description_title %></p>
|
94
|
+
<p class="font-medium"><%= light.description_message %></p>
|
95
|
+
<p><%= light.description_comment %></p>
|
96
|
+
</div>
|
97
|
+
<% if light.latest_failure %>
|
98
|
+
<div class="whitespace-nowrap">
|
99
|
+
<%= Time.at(light.latest_failure.time).strftime("%T") %>
|
100
|
+
</div>
|
101
|
+
<% end %>
|
102
|
+
</div>
|
103
|
+
</div>
|
104
|
+
|
105
|
+
<!-- Stats Row -->
|
106
|
+
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-3">
|
107
|
+
<div>
|
108
|
+
<span class="font-medium">Failures:</span> <%= light.failures.count %>
|
109
|
+
</div>
|
110
|
+
|
111
|
+
<% light.last_check_in_words.then do |last_check| %>
|
112
|
+
<% if last_check %>
|
113
|
+
<div>
|
114
|
+
<span class="font-medium">Last Check:</span> <%= light.last_check_in_words %>
|
115
|
+
</div>
|
116
|
+
<% end %>
|
117
|
+
<% end %>
|
118
|
+
</div>
|
119
|
+
</div>
|
120
|
+
|