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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10dcbfd4da7a00053fc1e9486c54312a0010096deeec169368e251dd173a6c41
4
- data.tar.gz: 0327eac1507849ce0ae681a37af3ca5b09af9d642331619879cfd2e876f97462
3
+ metadata.gz: 982b83a0163d681b2b82a9d2e6d8315044e28b74c7fa437f0b9b267f2e409a90
4
+ data.tar.gz: fc0cabac07632fe04647b28af3cecf2fc5cd8400ca5c33107831812f0551ad65
5
5
  SHA512:
6
- metadata.gz: 1380f6f03f37f95a4f567ea840534e371a963fa1380bde5424981651d558761c642f67ba9cbc9bc14fac90ee0dc9393020a3bb13ca809b0e5206cea7c542ffe9
7
- data.tar.gz: f9827baa63d88ec20224c8e5bb356af5ac03271714a0db706359e53b157a28e03108a2be5c729fd83e58a39d443dc548b4e7bdcdebe591fce706b7839bae8d55
6
+ metadata.gz: a076c81b783ed62ae7975373048c5a0cb4b3575971840c7f190ba4dc2a2cbd9ef9d3a5524e1b4375d0d8712f77317fea4f0a9dbd4d4547290c1a75483d8739da
7
+ data.tar.gz: 887476b87ce17f6069d9b6dfef90e693399e43b16877aae67cc636aa0008b288c6d49ef19e66ecf76ad428bc75df04ce7878f00ab0d8cecb18e29321c4b390a5
data/CHANGELOG.md CHANGED
@@ -1,4 +1,12 @@
1
- ## [Unreleased]
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
- Feature are created by running this (in a console or from code):
38
+ Features are created by running this (in a console or from code):
39
39
  ```ruby
40
- Flipside::Feature.create(name: "MyFeature", description: "Some optional description about what this feature do")
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, an organization or a user responding `true` to `user.admin?`):
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
- ## Configuration
64
- TODO
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
+ }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flipside
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -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 text-left text-lg"><%= feature.description&.capitalize %></div>
4
- <div class="col-span-4 text-right text-lg font-normal">
5
- <span class="inline-block py-1 px-2 min-w-20 text-center rounded-lg <%= feature.status_color %>"><%= feature.status %></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 w-3/4 m-auto">
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, object = nil)
22
+ def enabled?(name, *objects)
23
23
  feature = find_by(name:)
24
24
  return false unless feature
25
25
 
26
- feature.enabled? object
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.1.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-25 00:00:00.000000000 Z
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@apoex.se
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: