flipside 0.2.1 → 0.2.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/CHANGELOG.md +6 -0
- data/README.md +137 -33
- data/lib/flipside/config/settings.rb +10 -0
- data/lib/flipside/public/index.js +2 -0
- data/lib/flipside/public/inline_edit_controller.js +25 -0
- data/lib/flipside/version.rb +1 -1
- data/lib/flipside/views/layout.erb +2 -1
- data/lib/flipside/views/show.erb +27 -4
- data/lib/flipside/web.rb +6 -0
- data/lib/flipside.rb +10 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c0a3e61bb314f51e06511146b141050b6ca6da1bc7caefa8307f854e19deb78
|
4
|
+
data.tar.gz: f9465ceb62faf692a5a19addcd5d38251d76712d9ee39b3c634fa9b78be745f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f779f75b475ae4b140a65dd6b1da62b9c77f8593000475ee0dec0a5bf7ad4d3170cd1af307d83927312d8c1ee389285d3bf001476a7190c4dff7392cdc7f4f0f
|
7
|
+
data.tar.gz: 3606e255c03e504d704f147d106783cb920d62c29b8a163b2869bdb50a8f204e4d8c45b0ba524683fe1fc6617b27e23da7b9ad55246df1d12f3e139c1872585d
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -33,41 +33,72 @@ This will create a migration file. Run the migration, to add the flipside tables
|
|
33
33
|
|
34
34
|
## Usage
|
35
35
|
|
36
|
-
|
36
|
+
### Defining Features
|
37
37
|
|
38
38
|
Features are created by running this (in a console or from code):
|
39
39
|
```ruby
|
40
|
-
Flipside
|
40
|
+
Flipside.create(
|
41
41
|
name: "MyFeature",
|
42
42
|
description: "Some optional description about what this feature do"
|
43
43
|
)
|
44
44
|
```
|
45
45
|
|
46
|
-
By default features are turned off. If
|
46
|
+
By default features are turned off. If we would like it turned on from the get go we could pass in `enabled: true`.
|
47
47
|
```ruby
|
48
|
-
Flipside
|
48
|
+
Flipside.create(name: "MyFeature", enabled: true)
|
49
49
|
```
|
50
50
|
|
51
51
|
Features can be active during a given period. Set `activated_at` and/or `deactivated_at` to define this period.
|
52
52
|
Note: A feature is always disabled outside of the active period.
|
53
|
+
Note: A nil value means that its active. I.e. `activated_at = nil` means active from the start, `deactivated_at = nil` means never deactivates.
|
53
54
|
```ruby
|
54
|
-
Flipside
|
55
|
+
Flipside.create(
|
55
56
|
name: "MyFeature",
|
56
57
|
activated_at: 1.week.from_now,
|
57
58
|
deactivated_at: 2.weeks.from_now
|
58
59
|
)
|
59
60
|
```
|
60
61
|
|
61
|
-
|
62
|
+
### Checking Feature Status
|
63
|
+
|
64
|
+
#### Globally
|
65
|
+
|
66
|
+
Check if a feature is enabled globally:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
Flipside.enabled? "MyFeature"
|
70
|
+
```
|
71
|
+
|
72
|
+
We can also check if a feature is disabled:
|
73
|
+
```ruby
|
74
|
+
Flipside.disabled? "MyFeature"
|
75
|
+
```
|
76
|
+
|
77
|
+
#### For a Specific Record
|
78
|
+
|
79
|
+
Check if a feature is enabled for a specific record (e.g. a user):
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
Flipside.enabled? "MyFeature", user
|
83
|
+
```
|
84
|
+
|
85
|
+
We can also check multiple records. `.enabled?` will return `true` if any of them have the feature enabled:
|
86
|
+
```ruby
|
87
|
+
Flipside.enabled? "MyFeature", user, company, location
|
88
|
+
```
|
89
|
+
|
90
|
+
### Enabling features for specific records
|
91
|
+
|
92
|
+
Features can be enabled for a certain record, typically a certain user or organization. These records are called entities. To enable a feature for a given record use `.add_entity`:
|
62
93
|
```ruby
|
63
94
|
user = User.first
|
64
|
-
Flipside.enabled? user # => false
|
95
|
+
Flipside.enabled? "MyFeature", user # => false
|
65
96
|
Flipside.add_entity(name: "MyFeature", user)
|
66
|
-
Flipside.enabled? user # => true
|
97
|
+
Flipside.enabled? "MyFeature", user # => true
|
67
98
|
```
|
68
99
|
|
69
100
|
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
|
101
|
+
for all users who are admins, using the `.add_role` method:
|
71
102
|
```ruby
|
72
103
|
user1 = User.new(admin: false)
|
73
104
|
user2 = User.new(admin: true)
|
@@ -76,43 +107,69 @@ Flipside.add_role(
|
|
76
107
|
class_name: "User",
|
77
108
|
method_name: :admin?
|
78
109
|
)
|
79
|
-
Flipside.enabled? user1 # => false
|
80
|
-
Flipside.enabled? user2 # => true
|
110
|
+
Flipside.enabled? "MyFeature", user1 # => false
|
111
|
+
Flipside.enabled? "MyFeature", user2 # => true
|
112
|
+
Flipside.enabled? "MyFeature", user1, user2 # => true, enabled for at least one.
|
81
113
|
```
|
82
114
|
|
83
115
|
|
84
|
-
|
116
|
+
## UI
|
117
|
+
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.
|
118
|
+
```ruby
|
119
|
+
mount Flipside::Web, at: '/flipside'
|
120
|
+
```
|
121
|
+
Note: you probably want to wrap this inside a constraints block to provide some authentication.
|
85
122
|
|
86
|
-
|
123
|
+

|
87
124
|
|
88
|
-
Check if a feature is enabled globally:
|
89
125
|
|
90
|
-
|
91
|
-
Flipside.enabled? "MyFeature"
|
92
|
-
```
|
126
|
+
### Configuration
|
93
127
|
|
94
|
-
|
128
|
+
The Flipside UI can be configured by calling some class methods on `Flipside` (see below).
|
95
129
|
|
96
|
-
|
130
|
+
`ui_back_path` is used to set a path to return to from the Flipside UI. If this is set, then the UI shows a "Back" button,
|
131
|
+
targeting this path/url. By default no back button is shown.
|
132
|
+
|
133
|
+
|
134
|
+
If `create_missing_features` is set to true, then features will automatically be created, whenever a check for an unknown feature is done.
|
135
|
+
This can be a convenient way of makes features "show up" in the Flipside UI. However, the description will not reveal much about what this features does.
|
136
|
+
(it will simply point to the place in the code where this check was done). So these features should be manually updated with a better description.
|
137
|
+
By default, features are not added and code like `Flipside.enabled? "Some unknown feature"` will simply return `false`.
|
97
138
|
|
139
|
+
`default_object` can be used to avoid the need to always pass in a record when checking if a feature is enabled. For example, say that we
|
140
|
+
always want to check if a feature is enabled for the currently logged in user (`Current.user`). Then we might add this configuration:
|
98
141
|
```ruby
|
99
|
-
Flipside.
|
142
|
+
Flipside.default_object = -> { Current.user }
|
100
143
|
```
|
101
|
-
|
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.
|
144
|
+
Now we can check if a feature is enabled without needing to pass in `Current.user`:
|
104
145
|
```ruby
|
105
|
-
|
146
|
+
# With `default_object` set then all we need is this
|
147
|
+
Flipside.enabled? :some_feature
|
148
|
+
|
149
|
+
# Which will be the same as
|
150
|
+
Flipside.enabled? :some_feature, Current.user
|
151
|
+
|
152
|
+
# Note: if we do pass in an argument, then `default_object ` will not be used:
|
153
|
+
Flipside.enabled? :some_feature, Current.company # check current company instead of user.
|
106
154
|
```
|
107
|
-
Note: you probably want to wrap this inside a constraints block to provide some authentication.
|
108
155
|
|
109
|
-

|
110
156
|
|
157
|
+
Typically this configuration should be declared in an initializer file.
|
111
158
|
|
112
|
-
|
159
|
+
```ruby
|
160
|
+
# config/initializers/flipside.rb
|
161
|
+
require 'flipside'
|
113
162
|
|
114
|
-
|
115
|
-
|
163
|
+
Flipside.ui_back_path = "/"
|
164
|
+
Flipside.create_missing_features = true
|
165
|
+
Flipside.default_object = -> { Current.user }
|
166
|
+
```
|
167
|
+
|
168
|
+
#### Entities
|
169
|
+
|
170
|
+
Entities can be added to a feature by searching for records.
|
171
|
+
|
172
|
+

|
116
173
|
|
117
174
|
To make this work, some configuration is required. Use the class method `Flipside.register_entity` for this.
|
118
175
|
```ruby
|
@@ -123,12 +180,59 @@ Flipside.register_entity(
|
|
123
180
|
identified_by: :id
|
124
181
|
)
|
125
182
|
```
|
126
|
-
Typically this should be configured in an initializer file.
|
127
183
|
|
128
|
-
The `.register_entity` method should be called once for each class that may be used as feature
|
129
|
-
The `search_by` keyword argument, which may be a
|
184
|
+
The `.register_entity` method should be called once for each class that may be used as a feature enabler.
|
185
|
+
The `search_by` keyword argument, which may be a `Symbol` or a `Proc`, dictates how records are found from searching in the ui.
|
186
|
+
When a `Symbol` is given, e.g. `:name`, then entities with an exact match on the corresponding attribute are returned. I.e. `User.where(name: query)`.
|
187
|
+
When a `Proc` is given, then this `Proc` is called with the search string and is expected to return an object responding to `to_a` (e.g. an AR collection).
|
188
|
+
This gives us the flexibility to decide how to search for entities. For example, to search for users with matching first name or last name or an email
|
189
|
+
starting with _query_, something like this could be used.
|
190
|
+
```ruby
|
191
|
+
Flipside.register_entity(
|
192
|
+
class_name: "User",
|
193
|
+
search_by: ->(str) { User.where("lower(first_name) = :name OR lower(last_name) = :name or email LIKE :str", name: str.downcase, str: "#{str}%") },
|
194
|
+
)
|
195
|
+
|
196
|
+
```
|
130
197
|
|
131
|
-
|
198
|
+
The `identified_by` keyword argument, sets the column used as primary key for the corresponding table. This defaults to `:id` and typically does need to be change.
|
199
|
+
Currently composite keys are not supported.
|
200
|
+
|
201
|
+
The `display_as` keyword argument, is used to configure how these entities show up in the combobox. When set to a `Symbol`, then this value is sent to the corresponding entity.
|
202
|
+
For example, given the following setup. Users will be displayed with first name and last name:
|
203
|
+
```ruby
|
204
|
+
class User < ApplicationRecord
|
205
|
+
def name
|
206
|
+
[first_name, last_name].compact.map(&:capitalize).join(" ")
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
Flipside.register_entity(
|
211
|
+
class_name: "User",
|
212
|
+
display_as: :name,
|
213
|
+
)
|
214
|
+
```
|
215
|
+
|
216
|
+
When a `Proc` is given, then it is expected to take an entity as input and return a string used for displaying the entity. The config above could then instead be done using:
|
217
|
+
```ruby
|
218
|
+
Flipside.register_entity(
|
219
|
+
class_name: "User",
|
220
|
+
display_as: ->(user) { [user.first_name, user.last_name].compact.map(&:capitalize).join(" ") }
|
221
|
+
)
|
222
|
+
```
|
223
|
+
|
224
|
+
#### Roles
|
225
|
+
|
226
|
+
Features can be enabled for certain roles, by searching for roles (by method name).
|
227
|
+
|
228
|
+

|
229
|
+
|
230
|
+
This is configured by calling the class method `Flipside.register_role` for each role to be added.
|
231
|
+
Note a role consists of a class and a corresponding instance method.
|
232
|
+
```ruby
|
233
|
+
Flipside.register_role(class_name: "User", method_name: :admin?)
|
234
|
+
Flipside.register_role(class_name: "User", method_name: :awesome?)
|
235
|
+
```
|
132
236
|
|
133
237
|
## Development
|
134
238
|
|
@@ -2,6 +2,16 @@ module Flipside
|
|
2
2
|
module Config
|
3
3
|
module Settings
|
4
4
|
attr_accessor :ui_back_path, :create_missing_features
|
5
|
+
attr_writer :default_object
|
6
|
+
|
7
|
+
def default_object
|
8
|
+
case @default_object
|
9
|
+
when Proc
|
10
|
+
@default_object.call
|
11
|
+
else
|
12
|
+
@default_object
|
13
|
+
end
|
14
|
+
end
|
5
15
|
end
|
6
16
|
end
|
7
17
|
end
|
@@ -2,8 +2,10 @@ import { Application } from "@hotwired/stimulus"
|
|
2
2
|
import ToggleController from "toggle_controller"
|
3
3
|
import SearchController from "search_controller"
|
4
4
|
import ModalController from "modal_controller"
|
5
|
+
import InlineEditController from "inline_edit_controller"
|
5
6
|
|
6
7
|
const application = Application.start()
|
7
8
|
application.register("toggle", ToggleController)
|
8
9
|
application.register("search", SearchController)
|
9
10
|
application.register("modal", ModalController)
|
11
|
+
application.register("inline-edit", InlineEditController)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class InlineEditController extends Controller {
|
4
|
+
static targets = ["read", "edit"];
|
5
|
+
|
6
|
+
edit() {
|
7
|
+
this.readTargets.forEach(element => {
|
8
|
+
element.classList.add("hidden")
|
9
|
+
})
|
10
|
+
|
11
|
+
this.editTargets.forEach(element => {
|
12
|
+
element.classList.remove("hidden")
|
13
|
+
})
|
14
|
+
}
|
15
|
+
|
16
|
+
cancel() {
|
17
|
+
this.editTargets.forEach(element => {
|
18
|
+
element.classList.add("hidden")
|
19
|
+
})
|
20
|
+
this.readTargets.forEach(element => {
|
21
|
+
element.classList.remove("hidden")
|
22
|
+
})
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
data/lib/flipside/version.rb
CHANGED
@@ -10,7 +10,8 @@
|
|
10
10
|
"@hotwired/stimulus": "https://cdn.jsdelivr.net/npm/@hotwired/stimulus/dist/stimulus.js",
|
11
11
|
"toggle_controller": "/flipside/toggle_controller.js",
|
12
12
|
"search_controller": "/flipside/search_controller.js",
|
13
|
-
"modal_controller": "/flipside/modal_controller.js"
|
13
|
+
"modal_controller": "/flipside/modal_controller.js",
|
14
|
+
"inline_edit_controller": "/flipside/inline_edit_controller.js"
|
14
15
|
}
|
15
16
|
}
|
16
17
|
</script>
|
data/lib/flipside/views/show.erb
CHANGED
@@ -8,9 +8,31 @@
|
|
8
8
|
<span class="font-semibold">Name:</span>
|
9
9
|
<span class="font-bold text-lg text-slate-300"><%= feature.name %></span>
|
10
10
|
</div>
|
11
|
-
|
12
|
-
<
|
13
|
-
<
|
11
|
+
<div class="mt-2 flex justify-between items-center text-center" data-controller="inline-edit">
|
12
|
+
<div class="font-semibold">Description:</div>
|
13
|
+
<div data-inline-edit-target="read"><%= feature.description %></div>
|
14
|
+
<div class="ml-2 " data-inline-edit-target="read">
|
15
|
+
<button
|
16
|
+
class="px-2 py-1 rounded-lg bg-blue-900 text-xl text-slate-400 border border-blue-950 hover:bg-blue-800"
|
17
|
+
data-action="inline-edit#edit" >
|
18
|
+
Edit
|
19
|
+
</button>
|
20
|
+
</div>
|
21
|
+
<form action="<%= feature.href %>" method="post" class="hidden ml-4 grow flex gap-2" data-inline-edit-target="edit">
|
22
|
+
<input type="hidden" name="_method" value="put" />
|
23
|
+
<input
|
24
|
+
type="text"
|
25
|
+
value="<%= feature.description %>"
|
26
|
+
name="description"
|
27
|
+
class="grow border text-slate-600 border-slate-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
28
|
+
<button
|
29
|
+
type="button"
|
30
|
+
class="p-2 bg-slate-600 text-slate-300 font-semibold rounded hover:bg-slate-500"
|
31
|
+
data-action="inline-edit#cancel" >
|
32
|
+
Cancel
|
33
|
+
</button>
|
34
|
+
<button type="submit" class="p-2 bg-blue-900 font-semibold rounded hover:bg-blue-800">Update</button>
|
35
|
+
</form>
|
14
36
|
</div>
|
15
37
|
</div>
|
16
38
|
<div class="mt-2 flex justify-between items-center">
|
@@ -39,7 +61,8 @@
|
|
39
61
|
Edit
|
40
62
|
</a>
|
41
63
|
</span>
|
42
|
-
</div
|
64
|
+
</div>
|
65
|
+
<div class="mt-2 grid grid-cols-3 items-center">
|
43
66
|
<span class="font-semibold">Roles:</span>
|
44
67
|
<span class="text-center"><%= feature.role_count_str %></span>
|
45
68
|
<span class="text-right text-xl text-slate-400">
|
data/lib/flipside/web.rb
CHANGED
@@ -20,6 +20,12 @@ module Flipside
|
|
20
20
|
erb :show, locals: {feature: FeaturePresenter.new(feature, base_path)}
|
21
21
|
end
|
22
22
|
|
23
|
+
put "/feature/:name" do
|
24
|
+
feature.update(params.slice("description"))
|
25
|
+
|
26
|
+
redirect to(request.path_info), 303
|
27
|
+
end
|
28
|
+
|
23
29
|
put "/feature/:name/toggle" do
|
24
30
|
content_type :text
|
25
31
|
|
data/lib/flipside.rb
CHANGED
@@ -25,10 +25,14 @@ module Flipside
|
|
25
25
|
feature = find_by(name:)
|
26
26
|
return false unless feature
|
27
27
|
|
28
|
-
objects
|
28
|
+
objects = [default_object] if objects.empty?
|
29
29
|
objects.any? { |object| feature.enabled? object }
|
30
30
|
end
|
31
31
|
|
32
|
+
def disabled?(...)
|
33
|
+
!enabled?(...)
|
34
|
+
end
|
35
|
+
|
32
36
|
def enable!(name)
|
33
37
|
feature = find_by!(name:)
|
34
38
|
feature.update(enabled: true)
|
@@ -64,11 +68,15 @@ module Flipside
|
|
64
68
|
find_by(name:) || raise(NoSuchFeauture.new(name))
|
65
69
|
end
|
66
70
|
|
71
|
+
def create(name:, description: nil)
|
72
|
+
Feature.create(name: name, description: description)
|
73
|
+
end
|
74
|
+
|
67
75
|
def create_missing(name)
|
68
76
|
trace = caller.find { |trace| !trace.start_with? __FILE__ }
|
69
77
|
source, line, _ = trace.split(":")
|
70
78
|
source = [source, line].join(":") if line.match?(/\d+/)
|
71
|
-
|
79
|
+
create(name:, description: "Created from #{source}")
|
72
80
|
end
|
73
81
|
end
|
74
82
|
end
|
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.2.
|
4
|
+
version: 0.2.2
|
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-
|
11
|
+
date: 2025-04-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -106,6 +106,7 @@ files:
|
|
106
106
|
- lib/flipside/config/settings.rb
|
107
107
|
- lib/flipside/feature_presenter.rb
|
108
108
|
- lib/flipside/public/index.js
|
109
|
+
- lib/flipside/public/inline_edit_controller.js
|
109
110
|
- lib/flipside/public/modal_controller.js
|
110
111
|
- lib/flipside/public/search_controller.js
|
111
112
|
- lib/flipside/public/toggle_controller.js
|