flipside 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -1
- data/README.md +53 -5
- data/lib/flipside/feature_presenter.rb +9 -1
- data/lib/flipside/public/index.js +9 -0
- data/lib/flipside/public/modal_controller.js +20 -0
- data/lib/flipside/public/search_controller.js +110 -0
- data/lib/flipside/public/toggle_controller.js +29 -0
- data/lib/flipside/version.rb +1 -1
- data/lib/flipside/views/_feature_item.erb +7 -3
- data/lib/flipside/views/index.erb +1 -1
- data/lib/flipside.rb +3 -2
- metadata +12 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 982b83a0163d681b2b82a9d2e6d8315044e28b74c7fa437f0b9b267f2e409a90
|
4
|
+
data.tar.gz: fc0cabac07632fe04647b28af3cecf2fc5cd8400ca5c33107831812f0551ad65
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a076c81b783ed62ae7975373048c5a0cb4b3575971840c7f190ba4dc2a2cbd9ef9d3a5524e1b4375d0d8712f77317fea4f0a9dbd4d4547290c1a75483d8739da
|
7
|
+
data.tar.gz: 887476b87ce17f6069d9b6dfef90e693399e43b16877aae67cc636aa0008b288c6d49ef19e66ecf76ad428bc75df04ce7878f00ab0d8cecb18e29321c4b390a5
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,12 @@
|
|
1
|
-
## [
|
1
|
+
## [0.2.0] - 2025-01-29
|
2
|
+
|
3
|
+
- Support checking multiple objects at once
|
4
|
+
- Show hover text for feature statuses
|
5
|
+
- Support feature names with spaces
|
6
|
+
|
7
|
+
## [0.1.1] - 2024-11-22
|
8
|
+
|
9
|
+
- Fix missing js files for web UI
|
2
10
|
|
3
11
|
## [0.1.0] - 2024-11-22
|
4
12
|
|
data/README.md
CHANGED
@@ -35,12 +35,51 @@ This will create a migration file. Run the migration, to add the flipside tables
|
|
35
35
|
|
36
36
|
1. Defining Features
|
37
37
|
|
38
|
-
|
38
|
+
Features are created by running this (in a console or from code):
|
39
39
|
```ruby
|
40
|
-
Flipside::Feature.create(
|
40
|
+
Flipside::Feature.create(
|
41
|
+
name: "MyFeature",
|
42
|
+
description: "Some optional description about what this feature do"
|
43
|
+
)
|
41
44
|
```
|
42
45
|
|
43
46
|
By default features are turned off. If you would like it turned on from the beginning you could pass in `enabled: true`.
|
47
|
+
```ruby
|
48
|
+
Flipside::Feature.create(name: "MyFeature", enabled: true)
|
49
|
+
```
|
50
|
+
|
51
|
+
Features can be active during a given period. Set `activated_at` and/or `deactivated_at` to define this period.
|
52
|
+
Note: A feature is always disabled outside of the active period.
|
53
|
+
```ruby
|
54
|
+
Flipside::Feature.create(
|
55
|
+
name: "MyFeature",
|
56
|
+
activated_at: 1.week.from_now,
|
57
|
+
deactivated_at: 2.weeks.from_now
|
58
|
+
)
|
59
|
+
```
|
60
|
+
|
61
|
+
Features can be enabled for a certain record, typically a certain record or organization. This records are called entities. To enable a feature for a given record use.
|
62
|
+
```ruby
|
63
|
+
user = User.first
|
64
|
+
Flipside.enabled? user # => false
|
65
|
+
Flipside.add_entity(name: "MyFeature", user)
|
66
|
+
Flipside.enabled? user # => true
|
67
|
+
```
|
68
|
+
|
69
|
+
Features can be enabled for records responding true to a certain method. This is called a "role". Given that User records have an admin? method. A feature can then be enabled
|
70
|
+
for all users how are admins.
|
71
|
+
```ruby
|
72
|
+
user1 = User.new(admin: false)
|
73
|
+
user2 = User.new(admin: true)
|
74
|
+
Flipside.add_role(
|
75
|
+
name: "MyFeature",
|
76
|
+
class_name: "User",
|
77
|
+
method_name: :admin?
|
78
|
+
)
|
79
|
+
Flipside.enabled? user1 # => false
|
80
|
+
Flipside.enabled? user2 # => true
|
81
|
+
```
|
82
|
+
|
44
83
|
|
45
84
|
2. Checking Feature Status
|
46
85
|
|
@@ -54,14 +93,23 @@ Flipside.enabled? "MyFeature"
|
|
54
93
|
|
55
94
|
#### For a Specific Record
|
56
95
|
|
57
|
-
Check if a feature is enabled for a specific record (e.g. a user
|
96
|
+
Check if a feature is enabled for a specific record (e.g. a user):
|
58
97
|
|
59
98
|
```ruby
|
60
99
|
Flipside.enabled? "MyFeature", user
|
61
100
|
```
|
62
101
|
|
63
|
-
##
|
64
|
-
|
102
|
+
## UI
|
103
|
+
Flipside comes with a sinatra web ui to mange feature flags. To mount this sinatra app in Rails add the following to your routes.rb file.
|
104
|
+
```ruby
|
105
|
+
mount Flipside::Web, at: '/flipside'
|
106
|
+
```
|
107
|
+
|
108
|
+
Note: you also probably want to wrap this inside a constraints block to provide some authentication.
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
### Configuration
|
65
113
|
|
66
114
|
## Development
|
67
115
|
|
@@ -12,7 +12,7 @@ class FeaturePresenter
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def href
|
15
|
-
File.join(base_path, "feature", name)
|
15
|
+
File.join(base_path, "feature", ERB::Util.url_encode(name))
|
16
16
|
end
|
17
17
|
|
18
18
|
def toggle_path
|
@@ -87,6 +87,14 @@ class FeaturePresenter
|
|
87
87
|
feature.deactivated_at <= Time.now + period
|
88
88
|
end
|
89
89
|
|
90
|
+
def status_title
|
91
|
+
if activates_soon?
|
92
|
+
"Activates at: #{activated_at}"
|
93
|
+
elsif deactivates_soon?
|
94
|
+
"deactivates at: #{deactivated_at}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
90
98
|
def entity_count_str
|
91
99
|
count = feature.entities.count
|
92
100
|
if count == 1
|
@@ -0,0 +1,9 @@
|
|
1
|
+
import { Application } from "@hotwired/stimulus"
|
2
|
+
import ToggleController from "toggle_controller"
|
3
|
+
import SearchController from "search_controller"
|
4
|
+
import ModalController from "modal_controller"
|
5
|
+
|
6
|
+
const application = Application.start()
|
7
|
+
application.register("toggle", ToggleController)
|
8
|
+
application.register("search", SearchController)
|
9
|
+
application.register("modal", ModalController)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["dialog"];
|
5
|
+
|
6
|
+
open() {
|
7
|
+
if (this.hasDialogTarget) {
|
8
|
+
this.dialogTarget.classList.remove("hidden");
|
9
|
+
this.dialogTarget.showModal()
|
10
|
+
}
|
11
|
+
}
|
12
|
+
|
13
|
+
close() {
|
14
|
+
if (this.hasDialogTarget) {
|
15
|
+
this.dialogTarget.classList.add("hidden");
|
16
|
+
this.dialogTarget.close()
|
17
|
+
}
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
@@ -0,0 +1,110 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class SearchController extends Controller {
|
4
|
+
static targets = ["input", "value", "param", "results", "addButton"]
|
5
|
+
static values = {url: String}
|
6
|
+
|
7
|
+
timer = null
|
8
|
+
|
9
|
+
async search(event) {
|
10
|
+
clearTimeout(this.timer)
|
11
|
+
this.timer = setTimeout(this.fetchResults.bind(this), 400)
|
12
|
+
}
|
13
|
+
|
14
|
+
async fetchResults() {
|
15
|
+
this.clearResults()
|
16
|
+
try {
|
17
|
+
const url = this.getUrl()
|
18
|
+
if (!url) return
|
19
|
+
|
20
|
+
const response = await fetch(url)
|
21
|
+
if (!response.ok) {
|
22
|
+
console.error("Failed to fetch search results")
|
23
|
+
return
|
24
|
+
}
|
25
|
+
|
26
|
+
const html = await response.text()
|
27
|
+
this.updateResults(html)
|
28
|
+
} catch (error) {
|
29
|
+
console.error("Error fetching search results:", error)
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
select(event) {
|
34
|
+
// If the event target has no value, then we assume we didn't find any results
|
35
|
+
if (!event.target.value) {
|
36
|
+
this.clearResults()
|
37
|
+
if (this.hasAddButtonTarget) this.disableAddButton()
|
38
|
+
return
|
39
|
+
}
|
40
|
+
|
41
|
+
if (this.hasInputTarget) {
|
42
|
+
this.inputTarget.value = event.target.textContent.trim()
|
43
|
+
}
|
44
|
+
|
45
|
+
if (this.hasValueTarget) {
|
46
|
+
this.valueTarget.value = event.target.value
|
47
|
+
if (this.hasAddButtonTarget) this.enabledAddButton()
|
48
|
+
}
|
49
|
+
|
50
|
+
this.clearResults()
|
51
|
+
}
|
52
|
+
|
53
|
+
getUrl() {
|
54
|
+
if (!this.hasInputTarget) return
|
55
|
+
|
56
|
+
const query = this.inputTarget.value.trim()
|
57
|
+
if (query.length === 0) return
|
58
|
+
|
59
|
+
const params = new URLSearchParams()
|
60
|
+
params.append("q", encodeURIComponent(query))
|
61
|
+
|
62
|
+
this.paramTargets.forEach(paramTarget => {
|
63
|
+
const key = paramTarget.dataset.searchParam
|
64
|
+
const value = paramTarget.value
|
65
|
+
params.append(key, value)
|
66
|
+
})
|
67
|
+
|
68
|
+
return `${this.urlValue}?${params.toString()}`
|
69
|
+
}
|
70
|
+
|
71
|
+
updateResults(html) {
|
72
|
+
if (!this.hasResultsTarget) return
|
73
|
+
|
74
|
+
this.resultsTarget.innerHTML = html
|
75
|
+
this.resultsTarget.classList.add("block")
|
76
|
+
}
|
77
|
+
|
78
|
+
clearResults() {
|
79
|
+
if (this.hasResultsTarget) {
|
80
|
+
this.resultsTarget.innerHTML = ""
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
clearAll() {
|
85
|
+
if (this.hasInputTarget) {
|
86
|
+
this.inputTarget.value = ""
|
87
|
+
}
|
88
|
+
|
89
|
+
if (this.hasResultsTarget) {
|
90
|
+
this.resultsTarget.innerHTML = ""
|
91
|
+
}
|
92
|
+
|
93
|
+
if (this.hasAddButtonTarget) this.disableAddButton()
|
94
|
+
}
|
95
|
+
|
96
|
+
enabledAddButton() {
|
97
|
+
this.addButtonTarget.classList.remove("bg-gray-600")
|
98
|
+
this.addButtonTarget.classList.add("bg-gray-300")
|
99
|
+
this.addButtonTarget.classList.add("hover:bg-gray-400")
|
100
|
+
this.addButtonTarget.disabled = false
|
101
|
+
}
|
102
|
+
|
103
|
+
disableAddButton() {
|
104
|
+
this.addButtonTarget.classList.add("bg-gray-600")
|
105
|
+
this.addButtonTarget.classList.remove("bg-gray-300")
|
106
|
+
this.addButtonTarget.classList.remove("hover:bg-gray-400")
|
107
|
+
this.addButtonTarget.disabled = true
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class ToggleController extends Controller {
|
4
|
+
static targets = ["switch"]
|
5
|
+
static values = {
|
6
|
+
url: String,
|
7
|
+
enabled: Boolean
|
8
|
+
}
|
9
|
+
|
10
|
+
async switch(event) {
|
11
|
+
try {
|
12
|
+
const data = {enable: !this.enabledValue}
|
13
|
+
const response = await fetch(this.urlValue, {
|
14
|
+
method: "PUT",
|
15
|
+
headers: {"Content-Type": "application/json"},
|
16
|
+
body: JSON.stringify(data)
|
17
|
+
})
|
18
|
+
|
19
|
+
if (response.ok) {
|
20
|
+
location.reload()
|
21
|
+
} else {
|
22
|
+
const text = await response.text()
|
23
|
+
console.error("Failed to update:", text)
|
24
|
+
}
|
25
|
+
} catch (error) {
|
26
|
+
console.error("Error during the PUT request:", error)
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
data/lib/flipside/version.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
<div class="p-4 grid grid-cols-12 gap-4 cursor-pointer text-slate-300">
|
2
2
|
<div class="col-span-3 text-left text-xl"><%= feature.name %></div>
|
3
|
-
<div class="col-span-
|
4
|
-
<div class="col-span-
|
5
|
-
<span
|
3
|
+
<div class="col-span-7 text-center text-lg"><%= feature.description&.capitalize %></div>
|
4
|
+
<div class="col-span-1 text-right text-lg font-normal">
|
5
|
+
<span
|
6
|
+
class="inline-block py-1 px-2 min-w-20 text-center rounded-lg <%= feature.status_color %>"
|
7
|
+
title="<%= feature.status_title %>">
|
8
|
+
<%= feature.status %>
|
9
|
+
</span>
|
6
10
|
</div>
|
7
11
|
<div class="text-right">
|
8
12
|
<%= erb :_toggle_button, locals: {feature:} %>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
<div class="mt-12 w-3/4 m-auto bg-gray-800 text-slate-400 p-8 pb-24 rounded-lg shadow-lg">
|
2
2
|
<h1 class="text-3xl text-center font-bold mb-6">Flipside feature flags</h1>
|
3
3
|
|
4
|
-
<div class="mt-12
|
4
|
+
<div class="mt-12 m-auto">
|
5
5
|
<ul>
|
6
6
|
<% features.each do |feature| %>
|
7
7
|
<li class="border-b border-slate-500 hover:font-semibold">
|
data/lib/flipside.rb
CHANGED
@@ -19,11 +19,12 @@ module Flipside
|
|
19
19
|
end
|
20
20
|
|
21
21
|
class << self
|
22
|
-
def enabled?(name,
|
22
|
+
def enabled?(name, *objects)
|
23
23
|
feature = find_by(name:)
|
24
24
|
return false unless feature
|
25
25
|
|
26
|
-
|
26
|
+
objects << nil if objects.empty?
|
27
|
+
objects.any? { |object| feature.enabled? object }
|
27
28
|
end
|
28
29
|
|
29
30
|
def enable!(name)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flipside
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sammy Henningsson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-01-
|
11
|
+
date: 2025-01-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -82,7 +82,7 @@ dependencies:
|
|
82
82
|
version: '2.1'
|
83
83
|
description: Create simple feature toggles.
|
84
84
|
email:
|
85
|
-
- sammy.henningsson@
|
85
|
+
- sammy.henningsson@hey.com
|
86
86
|
executables: []
|
87
87
|
extensions: []
|
88
88
|
extra_rdoc_files: []
|
@@ -104,6 +104,10 @@ files:
|
|
104
104
|
- lib/flipside/config/registered_role.rb
|
105
105
|
- lib/flipside/config/roles.rb
|
106
106
|
- lib/flipside/feature_presenter.rb
|
107
|
+
- lib/flipside/public/index.js
|
108
|
+
- lib/flipside/public/modal_controller.js
|
109
|
+
- lib/flipside/public/search_controller.js
|
110
|
+
- lib/flipside/public/toggle_controller.js
|
107
111
|
- lib/flipside/search_result.rb
|
108
112
|
- lib/flipside/version.rb
|
109
113
|
- lib/flipside/views/_datetime_modal.erb
|
@@ -119,10 +123,13 @@ files:
|
|
119
123
|
- lib/flipside/web.rb
|
120
124
|
- lib/generators/flipside/install/install_generator.rb
|
121
125
|
- lib/generators/flipside/install/templates/20241122_create_flipside_migration.rb
|
122
|
-
homepage:
|
126
|
+
homepage: https://github.com/sammyhenningsson/flipside
|
123
127
|
licenses:
|
124
128
|
- MIT
|
125
|
-
metadata:
|
129
|
+
metadata:
|
130
|
+
homepage_uri: https://github.com/sammyhenningsson/flipside
|
131
|
+
source_code_uri: https://github.com/sammyhenningsson/flipside
|
132
|
+
changelog_uri: https://github.com/sammyhenningsson/flipside/blob/main/CHANGELOG.md
|
126
133
|
post_install_message:
|
127
134
|
rdoc_options: []
|
128
135
|
require_paths:
|