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 +4 -4
- data/.github/workflows/release.yml +9 -11
- data/lib/rollout/ui/helpers.rb +2 -2
- data/lib/rollout/ui/version.rb +1 -1
- data/lib/rollout/ui/views/features/index.erb +75 -0
- data/lib/rollout/ui/views/features/new.erb +16 -0
- data/lib/rollout/ui/views/features/partials/event_log.erb +65 -0
- data/lib/rollout/ui/views/features/show.erb +52 -0
- data/lib/rollout/ui/views/layout.erb +71 -0
- data/lib/rollout/ui/web.rb +3 -3
- data/rollout-ui.gemspec +0 -1
- metadata +6 -26
- data/lib/rollout/ui/views/features/index.slim +0 -63
- data/lib/rollout/ui/views/features/new.slim +0 -14
- data/lib/rollout/ui/views/features/partials/event_log.slim +0 -49
- data/lib/rollout/ui/views/features/show.slim +0 -77
- data/lib/rollout/ui/views/layout.slim +0 -64
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20b692482ef246dfbd60db5494d0730866913989d951f003aed72678233747bc
|
|
4
|
+
data.tar.gz: 4119e43228ccd69b654ec48e9b8784979931eb054cc43fc93816e1e0cc5a0fea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/rollout/ui/helpers.rb
CHANGED
data/lib/rollout/ui/version.rb
CHANGED
|
@@ -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) %>">← 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) %>">← 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>
|
data/lib/rollout/ui/web.rb
CHANGED
|
@@ -23,12 +23,12 @@ module Rollout::UI
|
|
|
23
23
|
end
|
|
24
24
|
)
|
|
25
25
|
else
|
|
26
|
-
|
|
26
|
+
erb :'features/index'
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
get '/features/new' do
|
|
31
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
225
|
-
- lib/rollout/ui/views/features/new.
|
|
226
|
-
- lib/rollout/ui/views/features/partials/event_log.
|
|
227
|
-
- lib/rollout/ui/views/features/show.
|
|
228
|
-
- lib/rollout/ui/views/layout.
|
|
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') ← 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
|
-
' ← 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
|