lightningff 0.0.2 → 0.0.4
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/README.md +59 -22
- data/app/assets/images/lightning/favicon.png +0 -0
- data/app/assets/stylesheets/lightning/application.css +12 -3
- data/app/controllers/lightning/feature_opt_ins_controller.rb +12 -7
- data/app/controllers/lightning/features_controller.rb +2 -10
- data/app/helpers/lightning/flaggable.rb +5 -1
- data/app/models/lightning/feature_opt_in.rb +9 -0
- data/app/views/layouts/lightning/application.html.erb +2 -2
- data/app/views/lightning/_header.html.erb +14 -0
- data/app/views/lightning/features/_form.html.erb +43 -31
- data/app/views/lightning/features/edit.html.erb +13 -5
- data/app/views/lightning/features/index.html.erb +30 -27
- data/app/views/lightning/features/new.html.erb +13 -4
- data/app/views/lightning/features/show.html.erb +93 -33
- data/db/migrate/20210513140212_create_feature_flag_framework.rb +2 -1
- data/lib/lightning.rb +1 -1
- data/lib/lightning/api.rb +6 -13
- data/lib/lightning/engine.rb +1 -1
- data/lib/lightning/version.rb +1 -1
- data/lib/tasks/lightning_tasks.rake +4 -8
- metadata +6 -12
- data/app/views/lightning/feature_opt_ins/_feature_opt_in.html.erb +0 -3
- data/app/views/lightning/feature_opt_ins/_form.html.erb +0 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d5d292f26ff307055c719d05e47726617b98ad9fe3d1c3c88a2c5842064cda4
|
4
|
+
data.tar.gz: a80724414799257c295c954b3ae6d7151764203636e8edbad59ea0f9ad27eb98
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dfdcf7ba3d009b2bc2b25bbdb6d35759448873edb39cf177fbbfa1a8bfc8ce359772ba4e5ed4aaceadbe38bf87804a8c6cf7664e926616a0df61f7c18d3458ab
|
7
|
+
data.tar.gz: 7073968c53537f49b35ee82f6f289f770d8ee36468a6186cdb0ac5b066f79d64f1f936cd1904def43838e861826e76397a9807854c572ac166b0bebbf411e4c3
|
data/README.md
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
# ⚡️ Lightning
|
1
|
+
# ⚡️ Lightning
|
2
|
+
|
3
|
+
 [](https://badge.fury.io/rb/lightningff)
|
4
|
+
|
2
5
|
An end-to-end feature flagging system that can be setup in <1 minute.
|
3
6
|
|
4
7
|
Lightning is a rails gem you can install into your Rails application to get both console and UI access to manage feature flags. Lightning saves you time to avoid building an in-house solution.
|
@@ -11,6 +14,19 @@ gem 'lightningff', require: 'lightning'
|
|
11
14
|
```
|
12
15
|
and run `bundle install`. _Note: You might need to run `bundle update` to resolve any incompatible issues with rails._
|
13
16
|
|
17
|
+
#### Script Installation
|
18
|
+
|
19
|
+
Run the following script to install the entire feature flag functionality without manual setup for your models (i.e. User, Workspace)
|
20
|
+
```ruby
|
21
|
+
rails lightning:install User Workspace
|
22
|
+
```
|
23
|
+
If you want more control over the setup, run the command below and check out [Manual Installation](#manual-installation) and skip the migrations part
|
24
|
+
```ruby
|
25
|
+
rails lightning:install
|
26
|
+
```
|
27
|
+
|
28
|
+
#### Manual Installation
|
29
|
+
|
14
30
|
Set up feature flag migrations by running the following lines
|
15
31
|
```bash
|
16
32
|
bin/rails lightning:install:migrations
|
@@ -28,9 +44,25 @@ class User < ApplicationRecord
|
|
28
44
|
end
|
29
45
|
```
|
30
46
|
|
31
|
-
|
47
|
+
## Usage
|
32
48
|
|
33
|
-
|
49
|
+
```ruby
|
50
|
+
user = User.create(name: 'Dummy user')
|
51
|
+
|
52
|
+
# Check if feature is enabled for entity
|
53
|
+
Lightning.enabled?(user, 'homepage_v2')
|
54
|
+
|
55
|
+
# Opt in an entity
|
56
|
+
Lightning.opt_in('homepage_v2', user)
|
57
|
+
|
58
|
+
# Opt out an entity
|
59
|
+
Lightning.opt_out('homepage_v2', user)
|
60
|
+
|
61
|
+
# List entities opted in to a feature
|
62
|
+
Lightning.opt_ins('hompage_v2')
|
63
|
+
```
|
64
|
+
|
65
|
+
## UI setup
|
34
66
|
|
35
67
|
Mount engine in `routes.rb` file
|
36
68
|
```ruby
|
@@ -39,31 +71,36 @@ Rails.application.routes.draw do
|
|
39
71
|
end
|
40
72
|
```
|
41
73
|
|
42
|
-
##
|
74
|
+
## API
|
75
|
+
|
76
|
+
**Create feature (state is disabled)**
|
43
77
|
|
44
78
|
```ruby
|
45
|
-
### Feature Management
|
46
|
-
# Create feature (state is disabled)
|
47
79
|
Lightning.create!('homepage_v2', 'New homepage with better logo')
|
48
|
-
|
80
|
+
```
|
81
|
+
|
82
|
+
**Find a feature by key**
|
83
|
+
|
84
|
+
```ruby
|
49
85
|
Lightning.get('homepage_v2')
|
50
|
-
|
86
|
+
```
|
87
|
+
|
88
|
+
**List all features**
|
89
|
+
|
90
|
+
```ruby
|
51
91
|
Lightning.list
|
52
|
-
|
53
|
-
Lightning.update('homepage_v2', {state: 'enabled_per_entity', description: 'Homepage with new nav'})
|
54
|
-
# Delete feature
|
55
|
-
Lightning.delete('homepage_v2')
|
92
|
+
```
|
56
93
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
Lightning.
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
Lightning.
|
94
|
+
**Update feature state/description**
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
Lightning.update('homepage_v2', { state: 'enabled_per_entity', description: 'Homepage with new nav' })
|
98
|
+
```
|
99
|
+
|
100
|
+
**Delete feature**
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
Lightning.delete('homepage_v2')
|
67
104
|
```
|
68
105
|
|
69
106
|
## Advanced Configuration
|
Binary file
|
@@ -19,19 +19,28 @@ html {
|
|
19
19
|
}
|
20
20
|
|
21
21
|
.fw-monospace {
|
22
|
-
font-family: monospace;
|
22
|
+
font-family: "Courier New", Courier, monospace;
|
23
23
|
}
|
24
24
|
|
25
|
-
.text-underline-on-hover {
|
25
|
+
.text-decoration-underline-on-hover {
|
26
26
|
text-decoration: none;
|
27
27
|
}
|
28
28
|
|
29
|
-
.text-underline-on-hover:hover {
|
29
|
+
.text-decoration-underline-on-hover:hover {
|
30
30
|
text-decoration: underline;
|
31
31
|
}
|
32
32
|
|
33
|
+
.fs-7 {
|
34
|
+
font-size: 14px;
|
35
|
+
}
|
36
|
+
|
37
|
+
.fs-8 {
|
38
|
+
font-size: 12px;
|
39
|
+
}
|
40
|
+
|
33
41
|
.lightning-logo {
|
34
42
|
font-size: 18px;
|
35
43
|
font-weight: bolder;
|
36
44
|
letter-spacing: -0.5px;
|
45
|
+
text-decoration: none;
|
37
46
|
}
|
@@ -5,14 +5,18 @@ module Lightning
|
|
5
5
|
|
6
6
|
def create
|
7
7
|
@feature = Feature.find(params[:feature_id])
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
flash[:notice] = "FAILED TO SAVE PERMISSIONS! INVALID ENTITY ID!"
|
8
|
+
entity_ids = params[:feature_opt_in][:entity_id].split(",")
|
9
|
+
entity_class = params[:feature_opt_in][:entity_type].constantize
|
10
|
+
|
11
|
+
entity_ids.each do |entity_id|
|
12
|
+
entity = entity_class.find(entity_id)
|
13
|
+
@feature.feature_opt_ins.find_or_create_by!(entity_id: entity.id, entity_type: entity_class.to_s)
|
15
14
|
end
|
15
|
+
|
16
|
+
flash[:notice] = "Permissions has been created!"
|
17
|
+
redirect_to @feature
|
18
|
+
rescue => e
|
19
|
+
flash[:notice] = "Failed to opt in some entities: #{e.message}"
|
16
20
|
redirect_to @feature
|
17
21
|
end
|
18
22
|
|
@@ -24,6 +28,7 @@ module Lightning
|
|
24
28
|
end
|
25
29
|
|
26
30
|
private
|
31
|
+
|
27
32
|
def feature_opt_ins_params
|
28
33
|
params.require(:feature_opt_in).permit(:entity_id, :entity_type)
|
29
34
|
end
|
@@ -4,26 +4,21 @@ module Lightning
|
|
4
4
|
class FeaturesController < ApplicationController
|
5
5
|
before_action :set_feature, only: [:show, :edit, :update, :destroy]
|
6
6
|
|
7
|
-
# GET /features
|
8
7
|
def index
|
9
8
|
@features = Feature.all
|
10
9
|
end
|
11
|
-
|
12
|
-
# GET /features/1
|
10
|
+
|
13
11
|
def show
|
14
12
|
@flaggable_entities = Lightning.flaggable_entities
|
15
13
|
end
|
16
14
|
|
17
|
-
# GET /features/new
|
18
15
|
def new
|
19
16
|
@feature = Feature.new
|
20
17
|
end
|
21
18
|
|
22
|
-
# GET /feature/1/edit
|
23
19
|
def edit
|
24
20
|
end
|
25
21
|
|
26
|
-
# POST /features
|
27
22
|
def create
|
28
23
|
@feature = Feature.new(feature_params)
|
29
24
|
|
@@ -34,7 +29,6 @@ module Lightning
|
|
34
29
|
end
|
35
30
|
end
|
36
31
|
|
37
|
-
# PATCH/PUT /features/1
|
38
32
|
def update
|
39
33
|
if @feature.update(feature_params)
|
40
34
|
redirect_to @feature, notice: 'Feature was successfully updated.'
|
@@ -43,19 +37,17 @@ module Lightning
|
|
43
37
|
end
|
44
38
|
end
|
45
39
|
|
46
|
-
# DELETE /features/1
|
47
40
|
def destroy
|
48
41
|
@feature.destroy
|
49
42
|
redirect_to features_url, notice: 'Feature was successfully destroyed.'
|
50
43
|
end
|
51
44
|
|
52
45
|
private
|
53
|
-
|
46
|
+
|
54
47
|
def set_feature
|
55
48
|
@feature = Feature.find(params[:id])
|
56
49
|
end
|
57
50
|
|
58
|
-
# Only allow a list of trusted parameters through.
|
59
51
|
def feature_params
|
60
52
|
params.require(:feature).permit(:key, :description, :state)
|
61
53
|
end
|
@@ -7,7 +7,16 @@ module Lightning
|
|
7
7
|
belongs_to :entity, polymorphic: true
|
8
8
|
before_validation :set_entity
|
9
9
|
|
10
|
+
def entity_name
|
11
|
+
if entity.respond_to?(:lightning_display)
|
12
|
+
entity.lightning_display
|
13
|
+
else
|
14
|
+
entity.id
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
10
18
|
private
|
19
|
+
|
11
20
|
def set_entity
|
12
21
|
self.entity = self.entity_type.constantize.find_by_id(self.entity_id)
|
13
22
|
end
|
@@ -4,11 +4,11 @@
|
|
4
4
|
<title>Lightning — Feature Flags</title>
|
5
5
|
<%= csrf_meta_tags %>
|
6
6
|
<%= csp_meta_tag %>
|
7
|
-
|
8
7
|
<%= stylesheet_link_tag "lightning/application", media: "all" %>
|
8
|
+
|
9
9
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
|
10
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
|
10
11
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"></script>
|
11
|
-
|
12
12
|
</head>
|
13
13
|
|
14
14
|
<body>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<div class="bg-light border-bottom p-3 mb-3">
|
2
|
+
<div class="container d-flex justify-content-between align-items-center">
|
3
|
+
<%= link_to '⚡️ Lightning', features_path, class: 'lightning-logo text-reset' %>
|
4
|
+
|
5
|
+
<div class="d-flex align-items-center gap-3">
|
6
|
+
<%= link_to "https://github.com/LightningFF/lightning", class: "text-decoration-underline-on-hover text-muted fs-5", target: "_blank" do %>
|
7
|
+
<i class="bi-github"></i>
|
8
|
+
<% end %>
|
9
|
+
<% if show_new_feature_button %>
|
10
|
+
<%= link_to 'New Feature', new_feature_path, class: "btn btn-primary btn-sm" %>
|
11
|
+
<% end %>
|
12
|
+
</div>
|
13
|
+
</div>
|
14
|
+
</div>
|
@@ -1,37 +1,49 @@
|
|
1
|
-
<%= form_with(model: feature) do |form| %>
|
2
|
-
|
3
|
-
|
4
|
-
<
|
1
|
+
<%= form_with(model: feature, class: "d-grid") do |form| %>
|
2
|
+
<div class="col-4">
|
3
|
+
<% if feature.errors.any? %>
|
4
|
+
<div id="error_explanation">
|
5
|
+
<h2><%= pluralize(feature.errors.count, "error") %> prohibited this feature from being saved:</h2>
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
<div class="field">
|
15
|
-
<%= form.label :key %>
|
16
|
-
<%= form.text_field :key %>
|
17
|
-
</div>
|
7
|
+
<ul>
|
8
|
+
<% feature.errors.each do |error| %>
|
9
|
+
<li><%= error.full_message %></li>
|
10
|
+
<% end %>
|
11
|
+
</ul>
|
12
|
+
</div>
|
13
|
+
<% end %>
|
18
14
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
15
|
+
<% if @feature.new_record? %>
|
16
|
+
<div class="mb-2">
|
17
|
+
<%= form.label :key, "Name", class: "form-label" %>
|
18
|
+
<%= form.text_field :key, class: "form-control" %>
|
19
|
+
</div>
|
20
|
+
<% end %>
|
23
21
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
22
|
+
<div class="mb-2">
|
23
|
+
<%= form.label :description, class: "form-label" %>
|
24
|
+
<%= form.text_area :description, class: "form-control" %>
|
25
|
+
</div>
|
28
26
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
<div class="mb-2">
|
28
|
+
<% if @feature.new_record? %>
|
29
|
+
<span class="text-muted">After saving your feature flag, you can enable it.</span>
|
30
|
+
<% else %>
|
31
|
+
<%= form.label :state, class: "form-label" %>
|
32
|
+
<%= form.select :state, [
|
33
|
+
["Globally disabled", "disabled"],
|
34
|
+
["Globally enabled (regardless of opt-ins)", "enabled_globally"],
|
35
|
+
["Enabled only for opt-ins", "enabled_per_entity"],
|
36
|
+
], {}, { class: "form-select" } %>
|
37
|
+
<% end %>
|
38
|
+
</div>
|
33
39
|
|
34
|
-
|
35
|
-
|
40
|
+
<div>
|
41
|
+
<%= form.submit 'Save', class: "btn btn-primary" %>
|
42
|
+
<% if @feature.new_record? %>
|
43
|
+
<%= link_to 'Cancel', features_path, class: "btn btn-light" %>
|
44
|
+
<% else %>
|
45
|
+
<%= link_to 'Cancel', feature_path(feature), class: "btn btn-light" %>
|
46
|
+
<% end%>
|
47
|
+
</div>
|
36
48
|
</div>
|
37
|
-
<% end %>
|
49
|
+
<% end %>
|
@@ -1,7 +1,15 @@
|
|
1
|
-
|
1
|
+
<%= render partial: "lightning/header", locals: { show_new_feature_button: false } %>
|
2
2
|
|
3
|
-
|
4
|
-
<
|
3
|
+
<div class="container">
|
4
|
+
<div class="d-grid align-items-center gap-1 d-md-block border-bottom pb-2 mb-4">
|
5
|
+
<%= link_to feature_path(@feature), class: 'btn btn-light btn-sm' do %>
|
6
|
+
<i class="bi-arrow-left-short"></i> <%= @feature.key %>
|
7
|
+
<% end %>
|
8
|
+
</div>
|
5
9
|
|
6
|
-
|
7
|
-
<%=
|
10
|
+
<h1 class="fw-monospace fw-bold">
|
11
|
+
<%= @feature.key %>
|
12
|
+
</h1>
|
13
|
+
|
14
|
+
<%= render 'form', feature: @feature %>
|
15
|
+
</div>
|
@@ -1,31 +1,34 @@
|
|
1
|
+
<%= render partial: "lightning/header", locals: { show_new_feature_button: true } %>
|
2
|
+
|
1
3
|
<div class="container">
|
2
4
|
<p id="notice"><%= notice %></p>
|
3
5
|
|
4
|
-
|
5
|
-
<
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
6
|
+
<%if @features.empty? %>
|
7
|
+
<p class="lead">
|
8
|
+
⛳️ No features created yet.
|
9
|
+
</p>
|
10
|
+
<% else %>
|
11
|
+
<table class="table">
|
12
|
+
<tbody>
|
13
|
+
<% @features.each do |feature| %>
|
14
|
+
<tr>
|
15
|
+
<td>
|
16
|
+
<% if feature.enabled_per_entity? %>
|
17
|
+
<span class="badge bg-primary"><%= pluralize feature.feature_opt_ins.count, "opt in" %></span>
|
18
|
+
<% elsif feature.enabled_globally? %>
|
19
|
+
<span class="badge bg-success">On</span>
|
20
|
+
<% else %>
|
21
|
+
<span class="badge bg-secondary">Off</span>
|
22
|
+
<% end %>
|
23
|
+
</td>
|
24
|
+
<td>
|
25
|
+
<%= link_to feature.key, feature, class: "fw-monospace fw-bold text-decoration-underline-on-hover text-reset" %>
|
26
|
+
</td>
|
27
|
+
<td><%= feature.description %></td>
|
28
|
+
<td><%= feature.created_at.to_formatted_s(:short) %></td>
|
29
|
+
</tr>
|
30
|
+
<% end %>
|
31
|
+
</tbody>
|
32
|
+
</table>
|
33
|
+
<% end %>
|
31
34
|
</div>
|
@@ -1,6 +1,15 @@
|
|
1
|
-
|
1
|
+
<%= render partial: "lightning/header", locals: { show_new_feature_button: false } %>
|
2
2
|
|
3
|
-
|
4
|
-
<
|
3
|
+
<div class="container">
|
4
|
+
<div class="d-grid align-items-center gap-1 d-md-block border-bottom pb-2 mb-4">
|
5
|
+
<%= link_to features_path, class: 'btn btn-light btn-sm' do %>
|
6
|
+
<i class="bi-arrow-left-short"></i> All features
|
7
|
+
<% end %>
|
8
|
+
</div>
|
5
9
|
|
6
|
-
|
10
|
+
<h3 class="fw-bold">
|
11
|
+
New Feature
|
12
|
+
</h3>
|
13
|
+
|
14
|
+
<%= render 'form', feature: @feature %>
|
15
|
+
</div>
|
@@ -1,39 +1,99 @@
|
|
1
|
-
|
1
|
+
<%= render partial: "lightning/header", locals: { show_new_feature_button: false } %>
|
2
2
|
|
3
|
-
<
|
4
|
-
<
|
3
|
+
<div class="container">
|
4
|
+
<p id="notice"><%= notice %></p>
|
5
5
|
|
6
|
-
<
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
<div class="d-grid align-items-center gap-1 d-md-block border-bottom pb-2 mb-4">
|
7
|
+
<%= link_to features_path, class: 'btn btn-light btn-sm' do %>
|
8
|
+
<i class="bi-arrow-left-short"></i> All features
|
9
|
+
<% end %>
|
10
|
+
<span class="text-light px-2">|</span>
|
11
|
+
<%= link_to 'Edit', edit_feature_path(@feature), class: "btn btn-light btn-sm" %>
|
12
|
+
<div class="btn-group">
|
13
|
+
<button type="button" class="btn btn-light btn-sm dropdown-toggle ms-2" data-bs-toggle="dropdown" aria-expanded="false">
|
14
|
+
More
|
15
|
+
</button>
|
16
|
+
<ul class="dropdown-menu">
|
17
|
+
<li>
|
18
|
+
<%= button_to 'Delete', feature_path(@feature), class: "dropdown-item", method: :delete, data: { confirm: 'Are you sure?' } %>
|
19
|
+
</li>
|
20
|
+
</ul>
|
21
|
+
</div>
|
22
|
+
</div>
|
10
23
|
|
11
|
-
<
|
12
|
-
|
13
|
-
|
14
|
-
|
24
|
+
<h1 class="fw-monospace fw-bold mb-1">
|
25
|
+
<%= @feature.key %>
|
26
|
+
</h1>
|
27
|
+
<div class="fs-6 mb-2">
|
28
|
+
<%= @feature.description %>
|
29
|
+
</div>
|
30
|
+
<div class="d-flex align-items-baseline gap-2">
|
31
|
+
<div class="fs-5">
|
32
|
+
<% if @feature.enabled_per_entity? %>
|
33
|
+
<span class="badge bg-primary">
|
34
|
+
<%= pluralize @feature.feature_opt_ins.count, "opt in" %>
|
35
|
+
</span>
|
36
|
+
<% elsif @feature.enabled_globally? %>
|
37
|
+
<span class="badge bg-success">Globally enabled</span>
|
38
|
+
<% else %>
|
39
|
+
<span class="badge bg-secondary">Off</span>
|
40
|
+
<% end %>
|
41
|
+
</div>
|
42
|
+
<div class="text-muted">•</div>
|
43
|
+
<div class="text-muted fs-8">
|
44
|
+
Created <%= @feature.created_at.to_formatted_s(:short) %>
|
45
|
+
</div>
|
46
|
+
<div class="text-muted">•</div>
|
47
|
+
<div class="text-muted fs-8">
|
48
|
+
Last updated <%= @feature.updated_at.to_formatted_s(:short) %>
|
49
|
+
</div>
|
50
|
+
</div>
|
15
51
|
|
16
|
-
<
|
17
|
-
|
18
|
-
|
19
|
-
</
|
52
|
+
<div class="mt-5">
|
53
|
+
<%= form_with model: [@feature, @feature.feature_opt_ins.build] do |form| %>
|
54
|
+
<div>
|
55
|
+
<label class="form-label">Enable feature for entities</label>
|
56
|
+
</div>
|
57
|
+
<div class="row g-3">
|
58
|
+
<div class="col-auto">
|
59
|
+
<%=
|
60
|
+
form.select :entity_type,
|
61
|
+
options_for_select(@flaggable_entities.map { |entity| [ entity.to_s, entity.to_s] }),
|
62
|
+
{},
|
63
|
+
{ class: "form-select" }
|
64
|
+
%>
|
65
|
+
</div>
|
66
|
+
<div class="col-4">
|
67
|
+
<%= form.text_field :entity_id, placeholder: 'comma separated entity ids', class: 'form-control' %>
|
68
|
+
</div>
|
69
|
+
<div class="col-auto">
|
70
|
+
<%= form.submit 'Save', class: "btn btn-primary" %>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
<% end %>
|
74
|
+
</div>
|
20
75
|
|
76
|
+
<div class="mt-5">
|
77
|
+
<h3>
|
78
|
+
Opt Ins
|
79
|
+
<small class="text-muted">(<%= @feature.feature_opt_ins.count %>)</small>
|
80
|
+
</h3>
|
21
81
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
82
|
+
<% if @feature.feature_opt_ins.reject(&:new_record?).empty? %>
|
83
|
+
<p class="lead text-muted">👌 No entities opted in yet.</p>
|
84
|
+
<% else %>
|
85
|
+
<table class="table table-sm">
|
86
|
+
<% @feature.feature_opt_ins.each do |feature_opt_in| %>
|
87
|
+
<% next if feature_opt_in.new_record? %>
|
88
|
+
<tbody>
|
89
|
+
<tr>
|
90
|
+
<td><code><%= feature_opt_in.entity.class %></code><td>
|
91
|
+
<td><%= feature_opt_in.entity_name %></td>
|
92
|
+
<td><%= button_to 'Opt out', [feature_opt_in.feature, feature_opt_in], class: "btn btn-light btn-sm", method: :delete, data: { confirm: 'Are you sure?' } %></td>
|
93
|
+
</tr>
|
94
|
+
</tbody>
|
95
|
+
<% end %>
|
96
|
+
</table>
|
97
|
+
<% end %>
|
98
|
+
</div>
|
99
|
+
</div>
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class CreateFeatureFlagFramework < ActiveRecord::Migration[
|
1
|
+
class CreateFeatureFlagFramework < ActiveRecord::Migration[5.0]
|
2
2
|
def change
|
3
3
|
create_table :lightning_features do |t|
|
4
4
|
t.string :key, index: { unique: true }, null: false
|
@@ -17,5 +17,6 @@ class CreateFeatureFlagFramework < ActiveRecord::Migration[6.1]
|
|
17
17
|
end
|
18
18
|
|
19
19
|
add_index :lightning_feature_opt_ins, [:entity_id, :entity_type]
|
20
|
+
add_index :lightning_feature_opt_ins, [:feature_id, :entity_id, :entity_type], unique: true, name: 'opt_in_index'
|
20
21
|
end
|
21
22
|
end
|
data/lib/lightning.rb
CHANGED
@@ -12,6 +12,6 @@ module Lightning
|
|
12
12
|
end
|
13
13
|
|
14
14
|
class << self
|
15
|
-
delegate :create!, :get, :list, :update, :delete, :
|
15
|
+
delegate :create!, :get, :list, :update, :delete, :opt_ins, :opt_in, :opt_out, :enabled?, to: Api
|
16
16
|
end
|
17
17
|
end
|
data/lib/lightning/api.rb
CHANGED
@@ -28,29 +28,22 @@ module Lightning
|
|
28
28
|
get(key).destroy
|
29
29
|
end
|
30
30
|
|
31
|
-
def self.
|
31
|
+
def self.opt_ins(key)
|
32
32
|
get(key).feature_opt_ins.all.map(&:entity)
|
33
33
|
end
|
34
34
|
|
35
|
-
def self.
|
36
|
-
|
37
|
-
permissioned_entity.entity_id = entity.id
|
38
|
-
permissioned_entity.entity_type = entity.class.to_s
|
39
|
-
permissioned_entity.save!
|
35
|
+
def self.opt_in(key, entity)
|
36
|
+
get(key).feature_opt_ins.find_or_create_by!(entity_id: entity.id, entity_type: entity.class.to_s)
|
40
37
|
end
|
41
38
|
|
42
|
-
def self.
|
39
|
+
def self.opt_out(key, entity)
|
43
40
|
get(key).feature_opt_ins.find_by(entity_id: entity.id, entity_type: entity.class.to_s)&.destroy
|
44
41
|
rescue ActiveRecord::RecordNotFound
|
45
42
|
raise ::Lightning::Errors::EntityNotFound, "Could not find entity with id #{entity.id} and type #{entity.class.to_s}"
|
46
43
|
end
|
47
44
|
|
48
|
-
def self.enabled?(
|
49
|
-
|
50
|
-
joined_table
|
51
|
-
.where(key: feature_key, state: :enabled_per_entity, feature_opt_ins: { entity_id: entity.id, entity_type: entity.class.to_s })
|
52
|
-
.or(joined_table.where(key: feature_key, state: :enabled_globally))
|
53
|
-
.exists?
|
45
|
+
def self.enabled?(feature_key, entity)
|
46
|
+
Feature.where(key: feature_key).left_outer_joins(:feature_opt_ins).where("state = ? OR (state = ? AND lightning_feature_opt_ins.entity_id = ? AND lightning_feature_opt_ins.entity_type = ?)", Feature.states[:enabled_globally], Feature.states[:enabled_per_entity], entity.id, entity.class.to_s).exists?
|
54
47
|
end
|
55
48
|
end
|
56
49
|
end
|
data/lib/lightning/engine.rb
CHANGED
@@ -3,7 +3,7 @@ module Lightning
|
|
3
3
|
isolate_namespace Lightning
|
4
4
|
|
5
5
|
initializer "lightning.assets.precompile" do |app|
|
6
|
-
app.config.assets.precompile += %w(
|
6
|
+
app.config.assets.precompile += %w(lightning/application.css lightning/favicon.png)
|
7
7
|
end
|
8
8
|
|
9
9
|
config.generators do |g|
|
data/lib/lightning/version.rb
CHANGED
@@ -3,15 +3,13 @@ namespace :lightning do
|
|
3
3
|
task :install do
|
4
4
|
_, *args = ARGV
|
5
5
|
|
6
|
-
|
7
|
-
# Task goes here
|
8
|
-
puts 'Copying migrations from engines...'
|
6
|
+
puts '️⚡️ Copying migrations'
|
9
7
|
`bin/rails lightning:install:migrations`
|
10
8
|
|
11
|
-
puts 'Running
|
9
|
+
puts '⚡️ Running migrations'
|
12
10
|
`bin/rails db:migrate SCOPE=lightning`
|
13
11
|
|
14
|
-
puts 'Creating
|
12
|
+
puts '⚡️ Creating lightning initializer'
|
15
13
|
`echo "Lightning.flaggable_entities = [#{args.map{ |a| '\"' + a + '\"'}.join(", ")}]" > config/initializers/lightning.rb`
|
16
14
|
|
17
15
|
args.each do |model|
|
@@ -43,9 +41,7 @@ namespace :lightning do
|
|
43
41
|
end
|
44
42
|
|
45
43
|
|
46
|
-
puts '
|
47
|
-
|
48
|
-
# Don't run the tasks in the arguments
|
44
|
+
puts '⚡️ Lightning setup complete'
|
49
45
|
exit
|
50
46
|
end
|
51
47
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lightningff
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ruthwick Pathireddy
|
@@ -9,28 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-
|
12
|
+
date: 2021-06-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
|
-
- - "~>"
|
19
|
-
- !ruby/object:Gem::Version
|
20
|
-
version: 6.1.3
|
21
18
|
- - ">="
|
22
19
|
- !ruby/object:Gem::Version
|
23
|
-
version:
|
20
|
+
version: 5.0.0
|
24
21
|
type: :runtime
|
25
22
|
prerelease: false
|
26
23
|
version_requirements: !ruby/object:Gem::Requirement
|
27
24
|
requirements:
|
28
|
-
- - "~>"
|
29
|
-
- !ruby/object:Gem::Version
|
30
|
-
version: 6.1.3
|
31
25
|
- - ">="
|
32
26
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
27
|
+
version: 5.0.0
|
34
28
|
description: With Lightning, you can set up an end-to-end highly customizable feature
|
35
29
|
flagging system in <1 minute. It provides console and UI support for feature flag
|
36
30
|
management.
|
@@ -45,6 +39,7 @@ files:
|
|
45
39
|
- README.md
|
46
40
|
- Rakefile
|
47
41
|
- app/assets/config/lightning_manifest.js
|
42
|
+
- app/assets/images/lightning/favicon.png
|
48
43
|
- app/assets/stylesheets/lightning/application.css
|
49
44
|
- app/controllers/lightning/application_controller.rb
|
50
45
|
- app/controllers/lightning/feature_opt_ins_controller.rb
|
@@ -57,8 +52,7 @@ files:
|
|
57
52
|
- app/models/lightning/feature.rb
|
58
53
|
- app/models/lightning/feature_opt_in.rb
|
59
54
|
- app/views/layouts/lightning/application.html.erb
|
60
|
-
- app/views/lightning/
|
61
|
-
- app/views/lightning/feature_opt_ins/_form.html.erb
|
55
|
+
- app/views/lightning/_header.html.erb
|
62
56
|
- app/views/lightning/features/_form.html.erb
|
63
57
|
- app/views/lightning/features/edit.html.erb
|
64
58
|
- app/views/lightning/features/index.html.erb
|
@@ -1,12 +0,0 @@
|
|
1
|
-
<h3>Permission Entities</h3>
|
2
|
-
<%= form_with model: [@feature, @feature.feature_opt_ins.build] do |form| %>
|
3
|
-
<p>
|
4
|
-
<%= form.label :entity_type, 'Select Entity Type' %><br>
|
5
|
-
<%= form.select :entity_type, options_for_select(@flaggable_entities.map {|u| [ u.to_s, u.to_s] }) %>
|
6
|
-
</p>
|
7
|
-
<p>
|
8
|
-
<%= form.label :entity_id, 'Entity Id' %><br>
|
9
|
-
<%= form.text_field :entity_id %>
|
10
|
-
</p>
|
11
|
-
<%= form.submit 'Add entity to feature' %>
|
12
|
-
<% end %>
|