rollout-ui 0.8.0 → 0.8.2

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: abb4b1f85851c0127d87fcb8c428fd704d40b1e4c7b976ce8474d90eb449ab2e
4
- data.tar.gz: e9bf31f2540a4811fd5aa4df4b9eb19a0cb77c88955a72d34694703359adc222
3
+ metadata.gz: 20b692482ef246dfbd60db5494d0730866913989d951f003aed72678233747bc
4
+ data.tar.gz: 4119e43228ccd69b654ec48e9b8784979931eb054cc43fc93816e1e0cc5a0fea
5
5
  SHA512:
6
- metadata.gz: 9b7ca3dae1414eced5038c1e3bdaf73e2e6f870f2164fab88cad497d98d42fcf863c70c286df9f7b9058cd36020f3fcf4106e50c6d44ed0c997f9410740271d5
7
- data.tar.gz: 0b3ce4474df18ec347fff573d6a112c848cbe4d6c0e678ec773206385eecb0f45ef116a0fde1f02760c071912e93db42a56a47b528f502e59149858aa88e7e42
6
+ metadata.gz: ce849886243dd7fbbad302880b793bd0ff9bac4619e2ffe38f5b1c429ac2e20b62d103a47adad0b34ee873777e61bb60aa793d2c0d173e27de8686a609dfa1ca
7
+ data.tar.gz: 2fc5156404d3a12b5d7d5ee84925c092a1838d78d9f4b05ba81ec38b831ff018620fd61c188fb601186d4574b4a8b25c3696ade1928f45bf6ef024711950b606
@@ -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
@@ -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.0"
3
+ VERSION = "0.8.2"
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
 
data/rollout-ui.gemspec CHANGED
@@ -25,7 +25,6 @@ Gem::Specification.new do |spec|
25
25
  spec.add_dependency 'rollout', '~> 2.6'
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
 
30
29
  spec.add_development_dependency 'bundler', '>= 1.17'
31
30
  spec.add_development_dependency 'rake'
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.0
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - FetLife
@@ -63,26 +63,6 @@ dependencies:
63
63
  - - "<"
64
64
  - !ruby/object:Gem::Version
65
65
  version: '5.0'
66
- - !ruby/object:Gem::Dependency
67
- name: slim
68
- requirement: !ruby/object:Gem::Requirement
69
- requirements:
70
- - - ">="
71
- - !ruby/object:Gem::Version
72
- version: '3.0'
73
- - - "<"
74
- - !ruby/object:Gem::Version
75
- version: '6.0'
76
- type: :runtime
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '3.0'
83
- - - "<"
84
- - !ruby/object:Gem::Version
85
- version: '6.0'
86
66
  - !ruby/object:Gem::Dependency
87
67
  name: bundler
88
68
  requirement: !ruby/object:Gem::Requirement
@@ -221,11 +201,11 @@ files:
221
201
  - lib/rollout/ui/helpers.rb
222
202
  - lib/rollout/ui/public/css/tailwind.min.css
223
203
  - 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
204
+ - lib/rollout/ui/views/features/index.erb
205
+ - lib/rollout/ui/views/features/new.erb
206
+ - lib/rollout/ui/views/features/partials/event_log.erb
207
+ - lib/rollout/ui/views/features/show.erb
208
+ - lib/rollout/ui/views/layout.erb
229
209
  - lib/rollout/ui/web.rb
230
210
  - rollout-ui.gemspec
231
211
  - 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,64 +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-gray-200 { border-color: #404040; }
21
- | .dark .hover\:bg-gray-200:hover { background-color: #3a3a3a; }
22
- | .dark .text-blue-600 { color: #60a5fa; }
23
- | .dark .hover\:text-blue-700:hover { color: #93c5fd; }
24
-
25
- body
26
- / Addresses https://stackoverflow.com/questions/21147149/flash-of-unstyled-content-fouc-in-firefox-only-is-ff-slow-renderer
27
- script 0
28
-
29
- script
30
- | (function() {
31
- | const theme = localStorage.getItem('theme') || 'system';
32
- | function applyTheme(t) {
33
- | const root = document.documentElement;
34
- | if (t === 'dark' || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
35
- | root.classList.add('dark');
36
- | } else {
37
- | root.classList.remove('dark');
38
- | }
39
- | }
40
- | applyTheme(theme);
41
- | window.addEventListener('DOMContentLoaded', function() {
42
- | const btn = document.getElementById('theme-toggle');
43
- | btn.textContent = theme;
44
- | btn.addEventListener('click', function() {
45
- | const current = localStorage.getItem('theme') || 'system';
46
- | const next = current === 'light' ? 'dark' : current === 'dark' ? 'system' : 'light';
47
- | localStorage.setItem('theme', next);
48
- | applyTheme(next);
49
- | btn.textContent = next;
50
- | });
51
- | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
52
- | if (localStorage.getItem('theme') === 'system') applyTheme('system');
53
- | });
54
- | });
55
- | })();
56
-
57
- .container.mx-auto.font-mono.text-gray-700.px-5.pb-20
58
- .flex.items-center.pt-20
59
- h1.font-light.text-4xl.text-gray-600.flex-auto
60
- a href=index_path class='hover:text-blue-700'
61
- ' Rollout
62
- 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
63
-
64
- == yield