rollout-ui 0.8.1 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d9fab3c48e549993d1e00275c95b7346a51a99751e0385c8602bdadef24ba3f
4
- data.tar.gz: 52ff38014c4a74f4a53261edc266b04da7cd3b5f02a31a167c2971b30f3abf23
3
+ metadata.gz: 4206916ef324d7cfb3884b9a3c49529ce13c1775c48d3de796ae2cea639355b7
4
+ data.tar.gz: 9e0702e530a5dc7b9e22b5201e7a6165395573b3f4154236823cfc1dfe91a18c
5
5
  SHA512:
6
- metadata.gz: b298705af03edb6097e9bf8a1fa7ca5a4f94e793f18a619410d9600759d3d5781ec608a92123f97bd29531f983c1068d26669313d397dd279cd872ebe16b2a32
7
- data.tar.gz: 3ac6cea06516db2ed27b865dc0d16a86eea1f567c3d0813560392f8d834971d009ec80829fa8e9a415ef8855cc5fae5612218c4711c37cda1bb493fa0201f2fc
6
+ metadata.gz: 2023c3c6b08fe1d9ea02b976b1b8feacf96b64175c69fc9fcb673343a603afd3fb24674ed7adb852006b7f14b7da24dcd0a0fb4c3433ac24bdeb2bf6d303f0ce
7
+ data.tar.gz: 1c881df9faa5d1f7ae3bafe43e09491bd37f0b6b5d38b21190ad826f4741a776f58d22a5cf9d8a90f79fbd38242e3eb98f647d34389ef799f11131432a9149ae
@@ -8,6 +8,9 @@ on:
8
8
  jobs:
9
9
  release:
10
10
  runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
13
+ id-token: write
11
14
  services:
12
15
  redis:
13
16
  image: redis:7-alpine
@@ -21,12 +24,16 @@ jobs:
21
24
  steps:
22
25
  - name: Checkout code
23
26
  uses: actions/checkout@v4
27
+ with:
28
+ persist-credentials: false
24
29
  - name: Setup Ruby
25
30
  uses: ruby/setup-ruby@v1
26
31
  with:
27
32
  bundler-cache: true
28
33
  - name: Run tests
29
- run: bundle exec rspec
34
+ run: bundle exec rspec
35
+ - name: Release to RubyGems
36
+ uses: rubygems/release-gem@v1
30
37
  - name: Create GitHub Release
31
38
  uses: softprops/action-gh-release@v2
32
39
  with:
@@ -34,13 +41,4 @@ jobs:
34
41
  name: ${{ github.ref_name }}
35
42
  generate_release_notes: true
36
43
  draft: false
37
- prerelease: false
38
- - name: Set up RubyGems credentials
39
- env:
40
- RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
41
- run: |
42
- mkdir -p ~/.gem
43
- echo ":rubygems_api_key: $RUBYGEMS_API_KEY" > ~/.gem/credentials
44
- chmod 0600 ~/.gem/credentials
45
- - name: Release to RubyGems
46
- run: bundle exec rake release
44
+ prerelease: false
data/README.md CHANGED
@@ -12,9 +12,13 @@ you can just mount as a Rack app and it will just work.
12
12
  Add it to your application's Gemfile:
13
13
 
14
14
  ```ruby
15
- gem 'rollout-ui'
15
+ gem "rollout", "~> 3.1"
16
+ gem "rollout-redis-adapter", "~> 0.1"
17
+ gem "rollout-ui"
16
18
  ```
17
19
 
20
+ `rollout-ui` 0.9+ requires Rollout 3.1 and does not support Rollout 2.x. The UI is backend-neutral: applications using Redis should add `rollout-redis-adapter` and pass a configured Rollout instance into the UI.
21
+
18
22
  Mount it
19
23
 
20
24
  ```ruby
@@ -73,7 +77,15 @@ To get the most out of **rollout-ui**, we recommend you to turn on logging
73
77
  on your rollout instance to see history of changes in the UI.
74
78
 
75
79
  ```ruby
76
- $rollout = Rollout.new(Redis.current, logging: { history_length: 100, global: true })
80
+ require "redis"
81
+ require "rollout"
82
+ require "rollout/adapters/redis"
83
+
84
+ $redis = Redis.new
85
+ $rollout = Rollout.new(
86
+ adapter: Rollout::Adapters::Redis.new($redis),
87
+ logging: { history_length: 100, global: true },
88
+ )
77
89
  ```
78
90
 
79
91
  To also see who updated states of your rollouts, you can configure `actor` and
data/config.ru CHANGED
@@ -2,13 +2,17 @@
2
2
 
3
3
  require_relative 'lib/rollout/ui'
4
4
  require 'redis'
5
+ require 'rollout/adapters/redis'
5
6
 
6
7
  redis_host = ENV.fetch('REDIS_HOST', 'localhost')
7
8
  redis_port = ENV.fetch('REDIS_PORT', '6379')
8
9
  redis_db = ENV.fetch('REDIS_DB', '10')
9
10
 
10
- redis = Redis.new(host: redis_host, port: redis_port, db: redis_db)
11
- rollout = Rollout.new(redis, logging: { history_length: 100, global: true })
11
+ redis = ::Redis.new(host: redis_host, port: redis_port, db: redis_db)
12
+ rollout = Rollout.new(
13
+ adapter: Rollout::Adapters::Redis.new(redis),
14
+ logging: { history_length: 100, global: true },
15
+ )
12
16
 
13
17
  %i[employees developers subscribers].each do |group|
14
18
  rollout.define_group(group) { }
@@ -109,8 +109,8 @@ module Rollout::UI
109
109
  }
110
110
  end
111
111
 
112
- def sanitized_name(feature_name)
113
- Rack::Utils.escape_html(feature_name)
112
+ def h(value)
113
+ Rack::Utils.escape_html(value)
114
114
  end
115
115
  end
116
116
  end
@@ -1,5 +1,5 @@
1
1
  class Rollout
2
2
  module UI
3
- VERSION = "0.8.1"
3
+ VERSION = "0.9.0"
4
4
  end
5
5
  end
@@ -0,0 +1,75 @@
1
+ <h2 class="font-semibold text-xl text-gray-500 pt-12 flex items-center">
2
+ <div class="flex-auto">Features<span class="text-sm p-1 bg-gray-100 ml-1 rounded-sm font-bold leading-none"><%= h(@features.count) %></span></div>
3
+ <form class="flex-none" action="<%= h(new_feature_path) %>" method="GET">
4
+ <button class="text-sm text-gray-600 p-3 bg-gray-100 ml-1 rounded-sm font-bold leading-none transition-colors duration-150 hover:bg-gray-200" type="submit">Add New Feature </button>
5
+ </form>
6
+ </h2>
7
+
8
+ <div class="w-8 h-1 bg-gray-300 my-10"></div>
9
+
10
+ <div class="overflow-auto scrolling-touch">
11
+ <table class="text-sm w-100 text-left w-full">
12
+ <thead class="font-semibold text-gray-500 border-b border-gray-200">
13
+ <th class="pb-2 pr-3">Name</th>
14
+ <th class="pb-2 pr-3">Percentage</th>
15
+ <th class="pb-2 pr-3">Groups</th>
16
+ <th class="pb-2 pr-3">Users</th>
17
+ <% if @rollout.respond_to?(:logging) %>
18
+ <th class="pb-2 pr-3">Updated</th>
19
+ <% end %>
20
+ <th class="pl-2"></th>
21
+ </thead>
22
+
23
+ <tbody class="text-gray-600">
24
+ <% @features.each do |feature_name| %>
25
+ <% feature = @rollout.get(feature_name) %>
26
+ <tr class="border-b border-gray-200">
27
+ <td class="py-2">
28
+ <a class="text-blue-600 hover:text-blue-700 hover:underline" href="<%= h(feature_path(feature.name)) %>"><%= h(feature_name) %></a>
29
+ <div class="text-gray-500 text-xs"><%= h(feature.data['description']) %></div>
30
+ </td>
31
+ <td class="py-2 whitespace-no-wrap">
32
+ <% if feature.percentage == 0 %>
33
+ <span class="text-gray-400"><%= h(feature.percentage) %>%</span>
34
+ <% else %>
35
+ <%= h(feature.percentage) %>%
36
+ <% end %>
37
+ </td>
38
+ <td class="py-2 whitespace-no-wrap"><%= h(feature.groups.join(', ')) %></td>
39
+ <td class="py-2 whitespace-no-wrap">
40
+ <% if feature.users.count == 0 %>
41
+ <span class="text-gray-400"><%= h(feature.users.count) %></span>
42
+ <% else %>
43
+ <%= h(feature.users.count) %>
44
+ <% end %>
45
+ </td>
46
+ <% if @rollout.respond_to?(:logging) %>
47
+ <td class="py-2 whitespace-no-wrap">
48
+ <% updated_at = @rollout.logging.updated_at(feature_name) %>
49
+ <% if updated_at.nil? %>
50
+ <span class="text-gray-400">Never</span>
51
+ <% else %>
52
+ <span title="<%= h(updated_at.strftime(Rollout::UI.config.timestamp_format)) %>"><%= h(time_ago(updated_at)) %></span>
53
+ <% end %>
54
+ </td>
55
+ <% end %>
56
+ <td class="flex items-center py-2 justify-end whitespace-no-wrap pl-3">
57
+ <form action="<%= h(activate_percentage_feature_path(feature_name, 100)) %>" method="POST">
58
+ <button class="p-3 bg-gray-100 ml-1 rounded-sm font-bold leading-none transition-colors duration-150 hover:bg-gray-200" type="submit" onclick="return confirm(<%= h("Are you sure you want activate #{feature_name} to 100%?".to_json) %>)">100% </button>
59
+ </form>
60
+ <form action="<%= h(activate_percentage_feature_path(feature_name, 0)) %>" method="POST">
61
+ <button class="p-3 bg-gray-100 ml-1 rounded-sm font-bold leading-none transition-colors duration-150 hover:bg-gray-200" type="submit" onclick="return confirm(<%= h("Are you sure you want activate #{feature_name} to 0%?".to_json) %>)">0% </button>
62
+ </form>
63
+ <form action="<%= h(delete_feature_path(feature_name)) %>" method="POST">
64
+ <button class="p-3 bg-gray-100 ml-1 rounded-sm font-bold leading-none transition-colors duration-150 hover:bg-gray-200" type="submit" onclick="return confirm(<%= h("Are you sure you want to delete #{feature_name}?".to_json) %>)">Delete </button>
65
+ </form>
66
+ </td>
67
+ </tr>
68
+ <% end %>
69
+ </tbody>
70
+ </table>
71
+ </div>
72
+
73
+ <% global_history_events = @rollout.respond_to?(:logging) ? @rollout.logging.global_events.reverse : [] %>
74
+
75
+ <%= erb :"features/partials/event_log", layout: false, locals: { events: global_history_events, show_feature: true, show_header: true, existing_features: @features } %>
@@ -0,0 +1,16 @@
1
+ <a class="text-sm text-blue-600 hover:text-blue-700 hover:underline" href="<%= h(index_path) %>">&larr; back to overview</a>
2
+
3
+ <h2 class="font-semibold text-xl text-gray-500 pt-12">New Feature </h2>
4
+
5
+ <div class="w-8 h-1 bg-gray-300 my-10"></div>
6
+
7
+ <form class="p-6 bg-gray-100 max-w-lg w-full text-sm rounded-sm" action="<%= h(new_feature_path) %>" method="POST">
8
+ <div class="mb-5">
9
+ <label class="block text-gray-500 mb-2" for="name">Name</label>
10
+ <input class="appearance-none border rounded-sm w-full py-2 px-4 text-gray-600 leading-relaxed bg-white hover:border-gray-500" name="name" id="name">
11
+ </div>
12
+
13
+ <div class="flex items-center justify-end">
14
+ <button class="py-4 px-5 bg-gray-700 text-gray-200 rounded-sm font-bold leading-none transition-colors duration-200 hover:bg-gray-800" type="submit">Create</button>
15
+ </div>
16
+ </form>
@@ -0,0 +1,65 @@
1
+ <% show_feature ||= false %>
2
+ <% show_header ||= false %>
3
+ <% existing_features = existing_features.map(&:to_s) if show_feature %>
4
+
5
+ <% if events.any? %>
6
+ <h2 class="font-semibold text-xl text-gray-500 pt-20 flex items-center">History</h2>
7
+ <div class="w-8 h-1 bg-gray-300 my-10"></div>
8
+
9
+ <div class="overflow-auto scrolling-touch">
10
+ <table class="text-sm w-100 text-left w-full">
11
+ <% if show_header %>
12
+ <thead class="font-semibold text-gray-600 border-b border-gray-200">
13
+ <% if show_feature %>
14
+ <th class="pb-2 pr-3 whitespace-nowrap">Feature</th>
15
+ <% end %>
16
+ <th class="pb-2 pr-3 w-full">Action</th>
17
+ <th class="pl-2 whitespace-nowrap" style="min-width: 160px;"></th>
18
+ </thead>
19
+ <% end %>
20
+
21
+ <tbody class="text-gray-600 border-t border-gray-200">
22
+ <% events.each do |event| %>
23
+ <tr class="border-b border-gray-200">
24
+ <% if show_feature %>
25
+ <td class="py-2 pr-3 whitespace-nowrap">
26
+ <% if existing_features.include?(event.feature.to_s) %>
27
+ <a class="text-blue-600 hover:text-blue-700 hover:underline" href="<%= h(feature_path(event.feature)) %>"><%= h(event.feature) %></a>
28
+ <% else %>
29
+ <%= h(event.feature) %>
30
+ <% end %>
31
+ </td>
32
+ <% end %>
33
+ <td class="py-2 pr-3 break-words w-full">
34
+ <% if event.name == "update" %>
35
+ <% if event.context && event.context[:actor] %>
36
+ <% if config.defined?(:actor_url) %>
37
+ <a class="underline" href="<%= h(config.get(:actor_url, event.context[:actor])) %>" target="_blank"><%= h(event.context[:actor]) %></a>
38
+ <% else %>
39
+ <%= h(event.context[:actor]) %>
40
+ <% end %>
41
+ <% else %>
42
+ unidentified user
43
+ <% end %>
44
+ <%
45
+ changes = event.data.fetch(:before).keys.reject { |key| key.to_s == 'data.updated_at' }.map do |key|
46
+ before = format_change_value(event.data.fetch(:before).fetch(key))
47
+ after = format_change_value(event.data.fetch(:after).fetch(key))
48
+ "#{format_change_key(key)} from #{before} to #{after}"
49
+ end
50
+ %>
51
+ changed <%= h(changes.empty? ? 'nothing!' : changes.join(', ')) %>
52
+ <% else %>
53
+ <%= h(event.data) %>
54
+ <% end %>
55
+ </td>
56
+
57
+ <td class="py-2 text-right whitespace-nowrap pl-3" style="min-width: 160px;">
58
+ <span title="<%= h(event.created_at.strftime(Rollout::UI.config.timestamp_format)) %>"><%= h(time_ago(event.created_at)) %></span>
59
+ </td>
60
+ </tr>
61
+ <% end %>
62
+ </tbody>
63
+ </table>
64
+ </div>
65
+ <% end %>
@@ -0,0 +1,52 @@
1
+ <a class="text-sm text-blue-600 hover:text-blue-700 hover:underline" href="<%= h(index_path) %>">&larr; back to overview </a>
2
+
3
+ <% if params[:error] %>
4
+ <pre class="text-red-500"><%= h(params[:error]) %></pre>
5
+ <% end %>
6
+ <h2 class="font-semibold text-xl text-gray-500 pt-12"><%= h(@feature.name) %></h2>
7
+
8
+ <div class="w-8 h-1 bg-gray-300 my-10"></div>
9
+ <main class="p-6 bg-gray-100 max-w-lg w-full text-sm rounded-sm">
10
+ <form id="updateFormSubmit" action="<%= h(feature_path(@feature.name)) %>" method="POST">
11
+ <input type="hidden" name="last_updated_at" value="<%= h(@feature.data['updated_at']) %>">
12
+ <div class="mb-5">
13
+ <label class="block text-gray-500 mb-2" for="description">Description</label>
14
+ <input class="appearance-none border rounded-sm w-full py-2 px-4 text-gray-600 leading-relaxed bg-white hover:border-gray-500" name="description" id="description" value="<%= h(@feature.data['description']) %>">
15
+ </div>
16
+
17
+ <div class="mb-5">
18
+ <label class="block text-gray-500 mb-2" for="groups">Groups<span class="ml-1 text-gray-400">(multi-select)</span></label>
19
+ <select class="block appearance-none w-full bg-white border border-gray-300 px-4 py-3 rounded-sm leading-relaxed" name="groups[]" id="groups" multiple size="<%= h(@rollout.groups.count + 1) %>">
20
+ <option class="py-1 px-1" value=""<% if @feature.groups.count == 0 %> selected<% end %>>(none)</option>
21
+ <% @rollout.groups.each do |group| %>
22
+ <option class="py-1 px-1" value="<%= h(group) %>"<% if @feature.groups.include?(group) %> selected<% end %>><%= h(group) %></option>
23
+ <% end %>
24
+ </select>
25
+ </div>
26
+
27
+ <div class="mb-5">
28
+ <label class="block text-gray-500 mb-2" for="percentage">Percentage</label>
29
+ <input class="appearance-none border rounded-sm w-full py-2 px-4 text-gray-600 leading-relaxed bg-white hover:border-gray-500" name="percentage" id="percentage" value="<%= h(@feature.percentage) %>" type="number" step="0.1">
30
+ </div>
31
+
32
+ <div class="mb-5">
33
+ <label class="block text-gray-500 mb-2">Users</label>
34
+ <% if @feature.users.count > 150 %>
35
+ <div class="appearance-none border rounded-sm w-full py-2 px-4 text-gray-600 leading-relaxed bg-gray-100"><%= h(@feature.users.count) %></div>
36
+ <% else %>
37
+ <textarea class="appearance-none border rounded-sm w-full py-2 px-4 text-gray-600 leading-relaxed bg-white hover:border-gray-500" name="users" id="users" value="<%= h(@feature.users.join(', ')) %>" rows="2"><%= h(@feature.users.join(', ')) %></textarea>
38
+ <% end %>
39
+ </div>
40
+ </form>
41
+
42
+ <div class="flex items-center justify-end">
43
+ <form action="<%= h(delete_feature_path(@feature.name)) %>" method="POST">
44
+ <button class="mr-5 text-gray-600 hover:underline" type="submit" onclick="return confirm(<%= h("Are you sure you want to delete #{@feature.name}?".to_json) %>)">Delete</button>
45
+ </form>
46
+ <button class="py-4 px-5 bg-gray-700 text-gray-200 rounded-sm font-bold leading-none transition-colors duration-200 hover:bg-gray-800" type="submit" form="updateFormSubmit">Update</button>
47
+ </div>
48
+ </main>
49
+
50
+ <% history_events = @rollout.respond_to?(:logging) ? @rollout.logging.events(@feature.name).reverse : [] %>
51
+
52
+ <%= erb :"features/partials/event_log", layout: false, locals: { events: history_events } %>
@@ -0,0 +1,71 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Rollout UI</title>
5
+ <meta name="viewport" content="width=device-width initial-scale=1">
6
+ <meta name="color-scheme" content="light dark">
7
+ <link rel="stylesheet" href="<%= h(stylesheet_path('tailwind.min')) %>">
8
+ <style>
9
+ :root { color-scheme: light; }
10
+ :root.dark { color-scheme: dark; }
11
+ body { transition: background-color 0.2s, color 0.2s; }
12
+ .dark { background-color: #1a1a1a; color: #e5e5e5; }
13
+ .dark .text-gray-700 { color: #d1d1d1; }
14
+ .dark .text-gray-600 { color: #b8b8b8; }
15
+ .dark .text-gray-500 { color: #9ca3af; }
16
+ .dark .text-gray-400 { color: #6b7280; }
17
+ .dark .bg-gray-100 { background-color: #2a2a2a; }
18
+ .dark .bg-gray-200 { background-color: #333333; }
19
+ .dark .bg-gray-300 { background-color: #404040; }
20
+ .dark .border { border-color: #555555; }
21
+ .dark .border-gray-200 { border-color: #404040; }
22
+ .dark .hover\:bg-gray-200:hover { background-color: #3a3a3a; }
23
+ .dark .text-blue-600 { color: #60a5fa; }
24
+ .dark .hover\:text-blue-700:hover { color: #93c5fd; }
25
+ .dark .bg-white { background-color: #333333; }
26
+ .dark .border-gray-300 { border-color: #555555; }
27
+ .dark .hover\:bg-gray-800:hover { background-color: #4a4a4a; }
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <!-- Addresses https://stackoverflow.com/questions/21147149/flash-of-unstyled-content-fouc-in-firefox-only-is-ff-slow-renderer -->
32
+ <script>0</script>
33
+ <script>
34
+ (function() {
35
+ const theme = localStorage.getItem('theme') || 'system';
36
+ function applyTheme(t) {
37
+ const root = document.documentElement;
38
+ if (t === 'dark' || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
39
+ root.classList.add('dark');
40
+ } else {
41
+ root.classList.remove('dark');
42
+ }
43
+ }
44
+ applyTheme(theme);
45
+ window.addEventListener('DOMContentLoaded', function() {
46
+ const btn = document.getElementById('theme-toggle');
47
+ btn.textContent = theme;
48
+ btn.addEventListener('click', function() {
49
+ const current = localStorage.getItem('theme') || 'system';
50
+ const next = current === 'light' ? 'dark' : current === 'dark' ? 'system' : 'light';
51
+ localStorage.setItem('theme', next);
52
+ applyTheme(next);
53
+ btn.textContent = next;
54
+ });
55
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
56
+ if (localStorage.getItem('theme') === 'system') applyTheme('system');
57
+ });
58
+ });
59
+ })();
60
+ </script>
61
+ <div class="container mx-auto font-mono text-gray-700 px-5 pb-20">
62
+ <div class="flex items-center pt-20">
63
+ <h1 class="font-light text-4xl text-gray-600 flex-auto">
64
+ <a href="<%= h(index_path) %>" class="hover:text-blue-700">Rollout </a>
65
+ </h1>
66
+ <button id="theme-toggle" class="text-sm text-gray-600 p-2 bg-gray-100 rounded-sm transition-colors duration-150 hover:bg-gray-200">system</button>
67
+ </div>
68
+ <%= yield %>
69
+ </div>
70
+ </body>
71
+ </html>
@@ -23,12 +23,12 @@ module Rollout::UI
23
23
  end
24
24
  )
25
25
  else
26
- slim :'features/index'
26
+ erb :'features/index'
27
27
  end
28
28
  end
29
29
 
30
30
  get '/features/new' do
31
- slim :'features/new'
31
+ erb :'features/new'
32
32
  end
33
33
 
34
34
  post '/features/new' do
@@ -42,7 +42,7 @@ module Rollout::UI
42
42
  if json_request?
43
43
  json(feature_to_hash(@feature))
44
44
  else
45
- slim :'features/show'
45
+ erb :'features/show'
46
46
  end
47
47
  end
48
48
 
@@ -60,8 +60,8 @@ module Rollout::UI
60
60
  if params[:users]
61
61
  feature.users = params[:users].split(',').map(&:strip).uniq.sort
62
62
  end
63
- feature.data.update(description: params[:description])
64
- feature.data.update(updated_at: Time.now.to_i)
63
+ feature.data["description"] = params[:description]
64
+ feature.data["updated_at"] = Time.now.to_i
65
65
  end
66
66
  end
67
67
 
@@ -75,7 +75,7 @@ module Rollout::UI
75
75
  with_rollout_context(rollout, actor: actor) do
76
76
  rollout.with_feature(params[:feature_name]) do |feature|
77
77
  feature.percentage = params[:percentage].to_f.clamp(0.0, 100.0)
78
- feature.data.update(updated_at: Time.now.to_i)
78
+ feature.data["updated_at"] = Time.now.to_i
79
79
  end
80
80
  end
81
81
 
data/rollout-ui.gemspec CHANGED
@@ -22,11 +22,12 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ['lib']
24
24
 
25
- spec.add_dependency 'rollout', '~> 2.6'
25
+ spec.add_dependency 'rollout', '>= 3.1', '< 4'
26
26
  spec.add_dependency 'sinatra', ['>= 2.0', '< 5.0']
27
27
  spec.add_dependency 'sinatra-contrib', ['>= 2.0', '< 5.0']
28
- spec.add_dependency 'slim', ['>= 3.0', '< 6.0']
29
28
 
29
+ spec.add_development_dependency 'rollout-redis-adapter', '~> 0.1'
30
+ spec.add_development_dependency 'redis-namespace'
30
31
  spec.add_development_dependency 'bundler', '>= 1.17'
31
32
  spec.add_development_dependency 'rake'
32
33
  spec.add_development_dependency 'rspec', '~> 3.13'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rollout-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - FetLife
@@ -13,16 +13,22 @@ dependencies:
13
13
  name: rollout
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.1'
19
+ - - "<"
17
20
  - !ruby/object:Gem::Version
18
- version: '2.6'
21
+ version: '4'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
- - - "~>"
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '3.1'
29
+ - - "<"
24
30
  - !ruby/object:Gem::Version
25
- version: '2.6'
31
+ version: '4'
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: sinatra
28
34
  requirement: !ruby/object:Gem::Requirement
@@ -64,25 +70,33 @@ dependencies:
64
70
  - !ruby/object:Gem::Version
65
71
  version: '5.0'
66
72
  - !ruby/object:Gem::Dependency
67
- name: slim
73
+ name: rollout-redis-adapter
68
74
  requirement: !ruby/object:Gem::Requirement
69
75
  requirements:
70
- - - ">="
71
- - !ruby/object:Gem::Version
72
- version: '3.0'
73
- - - "<"
76
+ - - "~>"
74
77
  - !ruby/object:Gem::Version
75
- version: '6.0'
76
- type: :runtime
78
+ version: '0.1'
79
+ type: :development
77
80
  prerelease: false
78
81
  version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: '0.1'
86
+ - !ruby/object:Gem::Dependency
87
+ name: redis-namespace
88
+ requirement: !ruby/object:Gem::Requirement
79
89
  requirements:
80
90
  - - ">="
81
91
  - !ruby/object:Gem::Version
82
- version: '3.0'
83
- - - "<"
92
+ version: '0'
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
84
98
  - !ruby/object:Gem::Version
85
- version: '6.0'
99
+ version: '0'
86
100
  - !ruby/object:Gem::Dependency
87
101
  name: bundler
88
102
  requirement: !ruby/object:Gem::Requirement
@@ -221,11 +235,11 @@ files:
221
235
  - lib/rollout/ui/helpers.rb
222
236
  - lib/rollout/ui/public/css/tailwind.min.css
223
237
  - lib/rollout/ui/version.rb
224
- - lib/rollout/ui/views/features/index.slim
225
- - lib/rollout/ui/views/features/new.slim
226
- - lib/rollout/ui/views/features/partials/event_log.slim
227
- - lib/rollout/ui/views/features/show.slim
228
- - lib/rollout/ui/views/layout.slim
238
+ - lib/rollout/ui/views/features/index.erb
239
+ - lib/rollout/ui/views/features/new.erb
240
+ - lib/rollout/ui/views/features/partials/event_log.erb
241
+ - lib/rollout/ui/views/features/show.erb
242
+ - lib/rollout/ui/views/layout.erb
229
243
  - lib/rollout/ui/web.rb
230
244
  - rollout-ui.gemspec
231
245
  - screenshot_index.png
@@ -1,63 +0,0 @@
1
- h2.font-semibold.text-xl.text-gray-500.pt-12.flex.items-center
2
- .flex-auto
3
- | Features
4
- span.text-sm.p-1.bg-gray-100.ml-1.rounded-sm.font-bold.leading-none #{@features.count}
5
-
6
- form.flex-none action=new_feature_path method='GET'
7
- button.text-sm.text-gray-600.p-3.bg-gray-100.ml-1.rounded-sm.font-bold.leading-none.transition-colors.duration-150(class='hover:bg-gray-200' type='submit')
8
- ' Add New Feature
9
-
10
- .w-8.h-1.bg-gray-300.my-10
11
-
12
- .overflow-auto.scrolling-touch
13
- table.text-sm.w-100.text-left.w-full
14
- thead.font-semibold.text-gray-500.border-b.border-gray-200
15
- th.pb-2.pr-3 Name
16
- th.pb-2.pr-3 Percentage
17
- th.pb-2.pr-3 Groups
18
- th.pb-2.pr-3 Users
19
- - if @rollout.respond_to?(:logging)
20
- th.pb-2.pr-3 Updated
21
- th.pl-2
22
-
23
- tbody.text-gray-600
24
- - @features.each do |feature_name|
25
- - feature = @rollout.get(feature_name)
26
- tr.border-b.border-gray-200
27
- td.py-2
28
- a.text-blue-600(href=feature_path(feature.name) class='hover:text-blue-700 hover:underline')
29
- = feature_name
30
- div.text-gray-500.text-xs = feature.data['description']
31
- td.py-2.whitespace-no-wrap
32
- - if feature.percentage == 0
33
- span.text-gray-400 #{feature.percentage}%
34
- - else
35
- | #{feature.percentage}%
36
- td.py-2.whitespace-no-wrap
37
- = feature.groups.join(', ')
38
- td.py-2.whitespace-no-wrap
39
- - if feature.users.count == 0
40
- span.text-gray-400 = feature.users.count
41
- - else
42
- = feature.users.count
43
- - if @rollout.respond_to?(:logging)
44
- td.py-2.whitespace-no-wrap
45
- - updated_at = @rollout.logging.updated_at(feature_name)
46
- - if updated_at.nil?
47
- span.text-gray-400 Never
48
- - else
49
- span title=updated_at.strftime(Rollout::UI.config.timestamp_format) = time_ago(updated_at)
50
- td.flex.items-center.py-2.justify-end.whitespace-no-wrap.pl-3
51
- form action=activate_percentage_feature_path(feature_name, 100) method='POST'
52
- button.p-3.bg-gray-100.ml-1.rounded-sm.font-bold.leading-none.transition-colors.duration-150(class='hover:bg-gray-200' type='submit' onclick="return confirm('Are you sure you want activate #{sanitized_name(feature_name)} to 100%?')")
53
- ' 100%
54
- form action=activate_percentage_feature_path(feature_name, 0) method='POST'
55
- button.p-3.bg-gray-100.ml-1.rounded-sm.font-bold.leading-none.transition-colors.duration-150(class='hover:bg-gray-200' type='submit' onclick="return confirm('Are you sure you want activate #{sanitized_name(feature_name)} to 0%?')")
56
- ' 0%
57
- form action=delete_feature_path(feature_name) method='POST'
58
- button.p-3.bg-gray-100.ml-1.rounded-sm.font-bold.leading-none.transition-colors.duration-150(class='hover:bg-gray-200' type='submit' onclick="return confirm('Are you sure you want to delete #{sanitized_name(feature_name)}?')")
59
- ' Delete
60
-
61
- - global_history_events = @rollout.respond_to?(:logging) ? @rollout.logging.global_events.reverse : []
62
-
63
- == slim :"features/partials/event_log", locals: { events: global_history_events, show_feature: true, show_header: true }
@@ -1,14 +0,0 @@
1
- a.text-sm.text-blue-600(href=index_path class='hover:text-blue-700 hover:underline') &larr; back to overview
2
-
3
- h2.font-semibold.text-xl.text-gray-500.pt-12
4
- ' New Feature
5
-
6
- .w-8.h-1.bg-gray-300.my-10
7
-
8
- form.p-6.bg-gray-100.max-w-lg.w-full.text-sm.rounded-sm action=new_feature_path method='POST'
9
- .mb-5
10
- label.block.text-gray-500.mb-2(for='name') Name
11
- input.appearance-none.border.rounded-sm.w-full.py-2.px-4.text-gray-600.leading-relaxed.bg-white(name='name' id='name' class='hover:border-gray-500')
12
-
13
- .flex.items-center.justify-end
14
- button.py-4.px-5.bg-gray-700.text-gray-200.rounded-sm.font-bold.leading-none.transition-colors.duration-200(class='hover:bg-gray-800' type='submit') Create
@@ -1,49 +0,0 @@
1
- - show_feature ||= false
2
- - show_header ||= false
3
-
4
- - if events.any?
5
- h2.font-semibold.text-xl.text-gray-500.pt-20.flex.items-center
6
- | History
7
- .w-8.h-1.bg-gray-300.my-10
8
-
9
- .overflow-auto.scrolling-touch
10
- table.text-sm.w-100.text-left.w-full
11
- - if show_header
12
- thead.font-semibold.text-gray-600.border-b.border-gray-200
13
- - if show_feature
14
- th.pb-2.pr-3.whitespace-nowrap Feature
15
- th.pb-2.pr-3.w-full Action
16
- th.pl-2.whitespace-nowrap style="min-width: 160px;"
17
-
18
- tbody.text-gray-600.border-t.border-gray-200
19
- - events.each do |event|
20
- tr.border-b.border-gray-200
21
- - if show_feature
22
- td.py-2.pr-3.whitespace-nowrap
23
- a.text-blue-600(href=feature_path(event.feature) class='hover:text-blue-700 hover:underline')
24
- = event.feature
25
- td.py-2.pr-3.break-words.w-full
26
- - if event.name == "update"
27
- - if event.context && event.context[:actor]
28
- - if config.defined?(:actor_url)
29
- a.underline> href=config.get(:actor_url, event.context[:actor]) target='_blank'
30
- = event.context[:actor]
31
- - else
32
- ' #{event.context[:actor]}
33
- - else
34
- ' unidentified user
35
- - changes = event.data.fetch(:before).keys.reject { |key| key.to_s == 'data.updated_at' }.map do |key|
36
- - before = format_change_value(event.data.fetch(:before).fetch(key))
37
- - after = format_change_value(event.data.fetch(:after).fetch(key))
38
- - "#{format_change_key(key)} from #{before} to #{after}"
39
-
40
- ' changed
41
- - unless changes.empty?
42
- = changes.join(', ')
43
- - else
44
- ' nothing!
45
- - else
46
- = event.data
47
-
48
- td.py-2.text-right.whitespace-nowrap.pl-3 style="min-width: 160px;"
49
- span title=event.created_at.strftime(Rollout::UI.config.timestamp_format) = time_ago(event.created_at)
@@ -1,77 +0,0 @@
1
- a.text-sm.text-blue-600(href=index_path class='hover:text-blue-700 hover:underline')
2
- ' &larr; back to overview
3
-
4
- - if params[:error]
5
- pre.text-red-500= params[:error]
6
- h2.font-semibold.text-xl.text-gray-500.pt-12
7
- = @feature.name
8
-
9
- .w-8.h-1.bg-gray-300.my-10
10
- main.p-6.bg-gray-100.max-w-lg.w-full.text-sm.rounded-sm
11
- form#updateFormSubmit action=feature_path(@feature.name) method='POST'
12
- input type='hidden' name='last_updated_at' value=(@feature.data['updated_at'])
13
- .mb-5
14
- label.block.text-gray-500.mb-2(for='description') Description
15
- input.appearance-none.border.rounded-sm.w-full.py-2.px-4.text-gray-600.leading-relaxed.bg-white(
16
- name='description'
17
- id='description'
18
- value=@feature.data['description']
19
- class='hover:border-gray-500'
20
- )
21
-
22
- .mb-5
23
- label.block.text-gray-500.mb-2(for='groups')
24
- | Groups
25
- span.ml-1.text-gray-400
26
- | (multi-select)
27
- select.block.appearance-none.w-full.bg-white.border.border-gray-300.px-4.py-3.rounded-sm.leading-relaxed(name="groups[]" id='groups' multiple=true size=(@rollout.groups.count + 1))
28
- option.py-1.px-1(value='' selected=(@feature.groups.count == 0))
29
- = '(none)'
30
- - @rollout.groups.each do |group|
31
- option.py-1.px-1(
32
- value=group
33
- selected=@feature.groups.include?(group)
34
- )
35
- = group
36
-
37
- .mb-5
38
- label.block.text-gray-500.mb-2(for='percentage') Percentage
39
- input.appearance-none.border.rounded-sm.w-full.py-2.px-4.text-gray-600.leading-relaxed.bg-white(
40
- name='percentage'
41
- id='percentage'
42
- value=@feature.percentage
43
- class='hover:border-gray-500'
44
- type='number'
45
- step='0.1'
46
- )
47
-
48
- .mb-5
49
- label.block.text-gray-500.mb-2 Users
50
-
51
- - if @feature.users.count > 150
52
- .appearance-none.border.rounded-sm.w-full.py-2.px-4.text-gray-600.leading-relaxed.bg-gray-100
53
- = @feature.users.count
54
- - else
55
- textarea.appearance-none.border.rounded-sm.w-full.py-2.px-4.text-gray-600.leading-relaxed.bg-white(
56
- name='users'
57
- id='users'
58
- value=@feature.users.join(', ')
59
- class='hover:border-gray-500'
60
- rows='2'
61
- )
62
- = @feature.users.join(', ')
63
-
64
- .flex.items-center.justify-end
65
- form action=delete_feature_path(@feature.name) method='POST'
66
- button.mr-5.text-gray-600(class='hover:underline' type='submit' onclick="return confirm('Are you sure you want to delete #{sanitized_name(@feature.name)}?')")
67
- | Delete
68
- button.py-4.px-5.bg-gray-700.text-gray-200.rounded-sm.font-bold.leading-none.transition-colors.duration-200(
69
- type='submit'
70
- class='hover:bg-gray-800'
71
- form='updateFormSubmit'
72
- )
73
- | Update
74
-
75
- - history_events = @rollout.respond_to?(:logging) ? @rollout.logging.events(@feature.name).reverse : []
76
-
77
- == slim :"features/partials/event_log", locals: { events: history_events }
@@ -1,68 +0,0 @@
1
- doctype html
2
- html
3
- head
4
- title Rollout UI
5
- meta name='viewport' content='width=device-width initial-scale=1'
6
- meta name='color-scheme' content='light dark'
7
- link rel='stylesheet' href=stylesheet_path('tailwind.min')
8
- style
9
- | :root { color-scheme: light; }
10
- | :root.dark { color-scheme: dark; }
11
- | body { transition: background-color 0.2s, color 0.2s; }
12
- | .dark { background-color: #1a1a1a; color: #e5e5e5; }
13
- | .dark .text-gray-700 { color: #d1d1d1; }
14
- | .dark .text-gray-600 { color: #b8b8b8; }
15
- | .dark .text-gray-500 { color: #9ca3af; }
16
- | .dark .text-gray-400 { color: #6b7280; }
17
- | .dark .bg-gray-100 { background-color: #2a2a2a; }
18
- | .dark .bg-gray-200 { background-color: #333333; }
19
- | .dark .bg-gray-300 { background-color: #404040; }
20
- | .dark .border { border-color: #555555; }
21
- | .dark .border-gray-200 { border-color: #404040; }
22
- | .dark .hover\:bg-gray-200:hover { background-color: #3a3a3a; }
23
- | .dark .text-blue-600 { color: #60a5fa; }
24
- | .dark .hover\:text-blue-700:hover { color: #93c5fd; }
25
- | .dark .bg-white { background-color: #333333; }
26
- | .dark .border-gray-300 { border-color: #555555; }
27
- | .dark .hover\:bg-gray-800:hover { background-color: #4a4a4a; }
28
-
29
- body
30
- / Addresses https://stackoverflow.com/questions/21147149/flash-of-unstyled-content-fouc-in-firefox-only-is-ff-slow-renderer
31
- script 0
32
-
33
- script
34
- | (function() {
35
- | const theme = localStorage.getItem('theme') || 'system';
36
- | function applyTheme(t) {
37
- | const root = document.documentElement;
38
- | if (t === 'dark' || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
39
- | root.classList.add('dark');
40
- | } else {
41
- | root.classList.remove('dark');
42
- | }
43
- | }
44
- | applyTheme(theme);
45
- | window.addEventListener('DOMContentLoaded', function() {
46
- | const btn = document.getElementById('theme-toggle');
47
- | btn.textContent = theme;
48
- | btn.addEventListener('click', function() {
49
- | const current = localStorage.getItem('theme') || 'system';
50
- | const next = current === 'light' ? 'dark' : current === 'dark' ? 'system' : 'light';
51
- | localStorage.setItem('theme', next);
52
- | applyTheme(next);
53
- | btn.textContent = next;
54
- | });
55
- | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
56
- | if (localStorage.getItem('theme') === 'system') applyTheme('system');
57
- | });
58
- | });
59
- | })();
60
-
61
- .container.mx-auto.font-mono.text-gray-700.px-5.pb-20
62
- .flex.items-center.pt-20
63
- h1.font-light.text-4xl.text-gray-600.flex-auto
64
- a href=index_path class='hover:text-blue-700'
65
- ' Rollout
66
- button#theme-toggle.text-sm.text-gray-600.p-2.bg-gray-100.rounded-sm.transition-colors.duration-150(class='hover:bg-gray-200') system
67
-
68
- == yield