gdk-toogle 0.7.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +34 -0
- data/Rakefile +16 -0
- data/app/assets/config/toogle_manifest.js +1 -0
- data/app/assets/stylesheets/toogle/application.css +22 -0
- data/app/assets/stylesheets/toogle/components/card.css +6 -0
- data/app/assets/stylesheets/toogle/components/scrollbox.css +13 -0
- data/app/assets/stylesheets/toogle/components/toggle.css +70 -0
- data/app/assets/stylesheets/toogle/dark-mode.css +25 -0
- data/app/assets/stylesheets/toogle/elements.css +121 -0
- data/app/assets/stylesheets/toogle/layout.css +74 -0
- data/app/assets/stylesheets/toogle/utilities.css +37 -0
- data/app/assets/stylesheets/toogle/variables.css +3 -0
- data/app/controllers/toogle/application_controller.rb +20 -0
- data/app/controllers/toogle/definitions_controller.rb +10 -0
- data/app/controllers/toogle/features_controller.rb +41 -0
- data/app/models/toogle/definition.rb +27 -0
- data/app/models/toogle/feature.rb +31 -0
- data/app/views/layouts/toogle/application.html.haml +53 -0
- data/app/views/toogle/application/_alpine_components.html.haml +56 -0
- data/app/views/toogle/definitions/index.html.haml +9 -0
- data/app/views/toogle/features/_dialog.html.haml +18 -0
- data/app/views/toogle/features/_remove.html.haml +4 -0
- data/app/views/toogle/features/_toggle.html.haml +8 -0
- data/app/views/toogle/features/index.html.haml +51 -0
- data/config/routes.rb +6 -0
- data/lib/toogle/engine.rb +11 -0
- data/lib/toogle/version.rb +3 -0
- data/lib/toogle.rb +5 -0
- metadata +147 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fc23503c85ec958b0051f3dd65ae20430ef7260df40441c6bc887b5caa1b3502
|
4
|
+
data.tar.gz: f88f8017af1429e311e216cab810197c4d4b5d9feafa327633dda26e84a70739
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b99d1e496643408bfaee5ff88409e5136fb89fa895b36b8f94ee08210a4d9ae03fe578d4699e5552672fceb345bb9d83c82ee557147348057d712b73e7803ce5
|
7
|
+
data.tar.gz: d24eaa1a2864ab9734d242843f7fc5cda999946fe361fc13d3a023d7c312d9dded7fa78c51a02094a231c171a5e4f92bb972bb1474e254fe24e5c62cd3e70c27
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2023 Thomas Hutterer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Toogle
|
2
|
+
|
3
|
+
A Rails engine web-UI to quickly toggle feature flags in GDK.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
This gem was written specifically for the GitLab codebase. It won't work in any other Rails app.
|
8
|
+
|
9
|
+
### Usage within the GitLab Development Kit
|
10
|
+
|
11
|
+
This gem is still an experiment and not yet part of our `Gemfile`. So for now, to try out this gem, take the following steps:
|
12
|
+
|
13
|
+
- `git clone` this repository to your machine.
|
14
|
+
- Add `gem 'toogle', path: 'local_toogle_path_here'` to the Gemfile and `bundle`.
|
15
|
+
- Add `mount Toogle::Engine, at: '/rails/toogle'` to `config/routes/development.rb`.
|
16
|
+
- Restart the rails server to pick up the new route: `gdk restart rails-web`
|
17
|
+
|
18
|
+
The UI is now availabe at http://gdk.test:3000/rails/toogle.
|
19
|
+
|
20
|
+
## Contributing
|
21
|
+
|
22
|
+
Everyone can contribute.
|
23
|
+
|
24
|
+
### How to run tests
|
25
|
+
|
26
|
+
This is the default rake task. Just run `rake` with no arguments.
|
27
|
+
|
28
|
+
The system specs in `spec/system` are configured to use Firefox as headless browser.
|
29
|
+
|
30
|
+
**Tip:** If you don't have Firefox installed on your machine, or if you are on Ubuntu and have Firefox installed only via Snap, you can switch to Chrome by changing the `driven_by` to `:selenium_chrome_headless` in `spec/rails_helper.rb`.
|
31
|
+
|
32
|
+
## License
|
33
|
+
|
34
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
3
|
+
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
4
|
+
load "rails/tasks/engine.rake"
|
5
|
+
|
6
|
+
load "rails/tasks/statistics.rake"
|
7
|
+
|
8
|
+
require "bundler/gem_tasks"
|
9
|
+
|
10
|
+
require "rspec/core"
|
11
|
+
require "rspec/core/rake_task"
|
12
|
+
|
13
|
+
desc "Run all specs in spec directory"
|
14
|
+
RSpec::Core::RakeTask.new(:spec)
|
15
|
+
|
16
|
+
task default: :spec
|
@@ -0,0 +1 @@
|
|
1
|
+
//= link toogle/application.css
|
@@ -0,0 +1,22 @@
|
|
1
|
+
@import "variables";
|
2
|
+
|
3
|
+
@import "elements";
|
4
|
+
@import "layout";
|
5
|
+
@import "utilities";
|
6
|
+
|
7
|
+
@import "components/card";
|
8
|
+
@import "components/scrollbox";
|
9
|
+
@import "components/toggle";
|
10
|
+
|
11
|
+
@import "dark-mode";
|
12
|
+
|
13
|
+
[x-cloak] {
|
14
|
+
display: none !important;
|
15
|
+
}
|
16
|
+
|
17
|
+
#notice {
|
18
|
+
background-color: #fc6d26;
|
19
|
+
color: white;
|
20
|
+
padding: 0.5rem;
|
21
|
+
border-radius: 5px;
|
22
|
+
}
|
@@ -0,0 +1,70 @@
|
|
1
|
+
.toggle {
|
2
|
+
position: relative;
|
3
|
+
display: inline-block;
|
4
|
+
width: 60px;
|
5
|
+
height: 34px;
|
6
|
+
margin: 4px;
|
7
|
+
|
8
|
+
input {
|
9
|
+
opacity: 0;
|
10
|
+
width: 0;
|
11
|
+
height: 0;
|
12
|
+
}
|
13
|
+
|
14
|
+
.handle {
|
15
|
+
position: absolute;
|
16
|
+
cursor: pointer;
|
17
|
+
top: 0;
|
18
|
+
left: 0;
|
19
|
+
right: 0;
|
20
|
+
bottom: 0;
|
21
|
+
background-color: #ccc;
|
22
|
+
transition: 0.2s;
|
23
|
+
|
24
|
+
&:before {
|
25
|
+
position: absolute;
|
26
|
+
content: "";
|
27
|
+
height: 26px;
|
28
|
+
width: 26px;
|
29
|
+
left: 4px;
|
30
|
+
bottom: 4px;
|
31
|
+
background-color: white;
|
32
|
+
transition: 0.2s;
|
33
|
+
}
|
34
|
+
|
35
|
+
&.round {
|
36
|
+
border-radius: 34px;
|
37
|
+
}
|
38
|
+
|
39
|
+
&.round:before {
|
40
|
+
border-radius: 50%;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
input:checked + .handle {
|
45
|
+
background-color: var(--color-primary);
|
46
|
+
}
|
47
|
+
|
48
|
+
input:focus + .handle {
|
49
|
+
outline: 4px solid var(--color-primary);
|
50
|
+
}
|
51
|
+
|
52
|
+
input:checked + .handle:before {
|
53
|
+
-webkit-transform: translateX(26px);
|
54
|
+
-ms-transform: translateX(26px);
|
55
|
+
transform: translateX(26px);
|
56
|
+
}
|
57
|
+
|
58
|
+
input:disabled + .handle:before {
|
59
|
+
background-color: #ddd;
|
60
|
+
content: "N/A";
|
61
|
+
text-align: center;
|
62
|
+
color: white;
|
63
|
+
font-size: x-small;
|
64
|
+
line-height: 26px;
|
65
|
+
}
|
66
|
+
|
67
|
+
input:disabled + .handle {
|
68
|
+
cursor: not-allowed;
|
69
|
+
}
|
70
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
@media (prefers-color-scheme: dark) {
|
2
|
+
html:not(.light-mode) {
|
3
|
+
--page-bg: #181822;
|
4
|
+
|
5
|
+
color: #eee;
|
6
|
+
|
7
|
+
a {
|
8
|
+
color: #ddd;
|
9
|
+
}
|
10
|
+
|
11
|
+
button,
|
12
|
+
dialog {
|
13
|
+
color: #eee;
|
14
|
+
}
|
15
|
+
|
16
|
+
.card {
|
17
|
+
background-color: #282833;
|
18
|
+
box-shadow: 0 0.5rem 1rem #111;
|
19
|
+
}
|
20
|
+
|
21
|
+
.scrollbox {
|
22
|
+
background-color: #444;
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
@@ -0,0 +1,121 @@
|
|
1
|
+
html {
|
2
|
+
--page-bg: #ddd;
|
3
|
+
|
4
|
+
background-color: var(--page-bg);
|
5
|
+
transition: 0.2s;
|
6
|
+
}
|
7
|
+
|
8
|
+
body {
|
9
|
+
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
10
|
+
|
11
|
+
> header {
|
12
|
+
position: sticky;
|
13
|
+
top: 0;
|
14
|
+
background-color: var(--page-bg);
|
15
|
+
transition: 0.2s;
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
h1,
|
20
|
+
h2,
|
21
|
+
h3,
|
22
|
+
h4,
|
23
|
+
h5,
|
24
|
+
h6,
|
25
|
+
p {
|
26
|
+
margin: 0;
|
27
|
+
}
|
28
|
+
|
29
|
+
a {
|
30
|
+
color: #222;
|
31
|
+
font-weight: bold;
|
32
|
+
text-decoration: none;
|
33
|
+
padding: 2px;
|
34
|
+
border-radius: 2px;
|
35
|
+
transition: outline 0.2s;
|
36
|
+
|
37
|
+
&:focus {
|
38
|
+
outline: 4px solid var(--color-primary);
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
input[type="submit"]:not(.small) {
|
43
|
+
font-size: large;
|
44
|
+
padding: 0.25rem;
|
45
|
+
}
|
46
|
+
|
47
|
+
input[type="search"] {
|
48
|
+
margin: 0.25rem 0;
|
49
|
+
padding: 0.75rem;
|
50
|
+
font-family: monospace;
|
51
|
+
border-radius: 0.5rem;
|
52
|
+
|
53
|
+
&.large {
|
54
|
+
font-size: large;
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
hr {
|
59
|
+
margin: 1.5rem 0;
|
60
|
+
}
|
61
|
+
|
62
|
+
code {
|
63
|
+
font-size: 1.25em;
|
64
|
+
}
|
65
|
+
|
66
|
+
button {
|
67
|
+
cursor: pointer;
|
68
|
+
background-color: transparent;
|
69
|
+
border: none;
|
70
|
+
border-radius: 0.125rem;
|
71
|
+
padding: 0.5rem;
|
72
|
+
min-width: 1rem;
|
73
|
+
|
74
|
+
display: flex;
|
75
|
+
align-items: center;
|
76
|
+
justify-content: center;
|
77
|
+
|
78
|
+
font-size: large;
|
79
|
+
|
80
|
+
&:hover,
|
81
|
+
&:focus {
|
82
|
+
background-color: var(--color-primary);
|
83
|
+
color: white;
|
84
|
+
}
|
85
|
+
|
86
|
+
&:focus {
|
87
|
+
outline: 4px solid var(--color-primary);
|
88
|
+
opacity: 1 !important;
|
89
|
+
}
|
90
|
+
|
91
|
+
> svg + span {
|
92
|
+
margin-left: 0.25em;
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
ul {
|
97
|
+
margin: 0;
|
98
|
+
padding: 0;
|
99
|
+
|
100
|
+
&:not(:first-child) {
|
101
|
+
margin-top: 1rem;
|
102
|
+
}
|
103
|
+
|
104
|
+
li {
|
105
|
+
list-style-type: none;
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
dialog {
|
110
|
+
background-color: transparent;
|
111
|
+
border: none;
|
112
|
+
outline: none;
|
113
|
+
|
114
|
+
&::backdrop {
|
115
|
+
background-color: rgba(0, 0, 0, 0.5);
|
116
|
+
}
|
117
|
+
|
118
|
+
> .card {
|
119
|
+
margin: 0;
|
120
|
+
}
|
121
|
+
}
|
@@ -0,0 +1,74 @@
|
|
1
|
+
body {
|
2
|
+
display: flex;
|
3
|
+
flex-direction: column;
|
4
|
+
min-height: 100vh;
|
5
|
+
margin: 0;
|
6
|
+
|
7
|
+
> header {
|
8
|
+
display: flex;
|
9
|
+
flex-direction: row;
|
10
|
+
justify-content: space-between;
|
11
|
+
align-items: center;
|
12
|
+
gap: 1rem;
|
13
|
+
|
14
|
+
padding: 1rem;
|
15
|
+
z-index: 2;
|
16
|
+
|
17
|
+
a,
|
18
|
+
span {
|
19
|
+
display: flex;
|
20
|
+
align-items: center;
|
21
|
+
gap: 0.5rem;
|
22
|
+
font-weight: bold;
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
> main {
|
27
|
+
display: flex;
|
28
|
+
flex-direction: column-reverse;
|
29
|
+
width: 100ch;
|
30
|
+
max-width: 95%;
|
31
|
+
margin: auto;
|
32
|
+
gap: 3rem;
|
33
|
+
padding: 2rem 0;
|
34
|
+
flex-grow: 1;
|
35
|
+
}
|
36
|
+
|
37
|
+
> footer {
|
38
|
+
display: flex;
|
39
|
+
justify-content: right;
|
40
|
+
align-items: center;
|
41
|
+
gap: 2em;
|
42
|
+
padding: 1rem;
|
43
|
+
font-size: smaller;
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
li.feature-toggle {
|
48
|
+
display: flex;
|
49
|
+
flex-direction: column;
|
50
|
+
padding: 0.125rem;
|
51
|
+
border-radius: 2px;
|
52
|
+
|
53
|
+
.row {
|
54
|
+
align-items: center;
|
55
|
+
flex-grow: 1;
|
56
|
+
|
57
|
+
button {
|
58
|
+
opacity: 0;
|
59
|
+
transition: 0.2s;
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
> .col {
|
64
|
+
padding: 0.5rem;
|
65
|
+
}
|
66
|
+
|
67
|
+
&:hover {
|
68
|
+
background-color: rgba(153, 153, 153, 0.1);
|
69
|
+
|
70
|
+
button {
|
71
|
+
opacity: 1;
|
72
|
+
}
|
73
|
+
}
|
74
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
.d-flex {
|
2
|
+
display: flex;
|
3
|
+
flex-wrap: wrap;
|
4
|
+
|
5
|
+
&.center {
|
6
|
+
justify-content: center;
|
7
|
+
align-items: center;
|
8
|
+
}
|
9
|
+
|
10
|
+
&.row {
|
11
|
+
flex-direction: row;
|
12
|
+
}
|
13
|
+
|
14
|
+
&.col {
|
15
|
+
flex-direction: column;
|
16
|
+
}
|
17
|
+
|
18
|
+
&.nowrap {
|
19
|
+
flex-wrap: nowrap;
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
.grow {
|
24
|
+
flex-grow: 1;
|
25
|
+
}
|
26
|
+
|
27
|
+
.stretch {
|
28
|
+
align-self: stretch;
|
29
|
+
}
|
30
|
+
|
31
|
+
.gap {
|
32
|
+
gap: 1em;
|
33
|
+
}
|
34
|
+
|
35
|
+
.w-100 {
|
36
|
+
width: 100%;
|
37
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Toogle
|
2
|
+
class ApplicationController < ActionController::Base
|
3
|
+
# In order for Alpine to be able to execute plain strings from HTML attributes as JavaScript expressions,
|
4
|
+
# for example x-on:click="console.log()", it needs to rely on utilities that violate the "unsafe-eval"
|
5
|
+
# content security policy.
|
6
|
+
content_security_policy false, if: -> { Rails.env.development? }
|
7
|
+
|
8
|
+
# Prevent all browser caching. Otherwise a cached version of a page might show the wrong toggle state.
|
9
|
+
# Also, it really doesn't matter on localhost :)
|
10
|
+
before_action :set_cache_headers
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def set_cache_headers
|
15
|
+
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
|
16
|
+
response.headers["Pragma"] = "no-cache"
|
17
|
+
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Toogle
|
2
|
+
class FeaturesController < ApplicationController
|
3
|
+
def index
|
4
|
+
@features = Toogle::Feature.all
|
5
|
+
end
|
6
|
+
|
7
|
+
def show
|
8
|
+
@definition = Toogle::Definition.find(params[:id])
|
9
|
+
if @definition.nil?
|
10
|
+
flash[:notice] = "No feature defintion with name \"#{params[:id]}\" found."
|
11
|
+
end
|
12
|
+
index
|
13
|
+
render "index"
|
14
|
+
end
|
15
|
+
|
16
|
+
def update
|
17
|
+
id = params[:id].to_sym
|
18
|
+
|
19
|
+
if params[:state] == "enabled"
|
20
|
+
::Feature.enable(id)
|
21
|
+
elsif params[:state] == "disabled"
|
22
|
+
::Feature.disable(id)
|
23
|
+
end
|
24
|
+
|
25
|
+
respond_to do |format|
|
26
|
+
format.html do
|
27
|
+
redirect_to features_url
|
28
|
+
end
|
29
|
+
|
30
|
+
format.json do
|
31
|
+
head :ok
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def destroy
|
37
|
+
::Feature.remove(params[:id])
|
38
|
+
redirect_to features_url, status: :see_other
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# A simple wrapper around GitLab's Feature:Definition class for easier use and testing.
|
2
|
+
|
3
|
+
module Toogle
|
4
|
+
class Definition
|
5
|
+
attr_accessor :name, :default_enabled
|
6
|
+
|
7
|
+
def self.all
|
8
|
+
::Feature::Definition.definitions.map do |definition|
|
9
|
+
new(name: definition[0].to_s, default_enabled: definition[1].default_enabled)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.unchanged
|
14
|
+
changed_features = Toogle::Feature.all.map(&:name)
|
15
|
+
all.reject { |definition| changed_features.include?(definition.name) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.find(name)
|
19
|
+
all.find { |definition| definition.name == name }
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(name:, default_enabled:)
|
23
|
+
@name = name
|
24
|
+
@default_enabled = default_enabled
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# A simple wrapper around GitLab's Feature class for easier use and testing.
|
2
|
+
|
3
|
+
module Toogle
|
4
|
+
class Feature
|
5
|
+
attr_accessor :name, :state
|
6
|
+
|
7
|
+
def self.all
|
8
|
+
definitions = Definition.all
|
9
|
+
|
10
|
+
Flipper::Adapters::ActiveRecord.new(
|
11
|
+
feature_class: ::Feature::FlipperFeature,
|
12
|
+
gate_class: ::Feature::FlipperGate
|
13
|
+
).get_all.map do |feature_name, feature_values|
|
14
|
+
feature_exists_on_current_branch = definitions.any?{ |d| d.name == feature_name }
|
15
|
+
feature_state = if feature_exists_on_current_branch
|
16
|
+
feature_values[:boolean] ? :enabled: :disabled
|
17
|
+
else
|
18
|
+
# This usually happens when switching back from an unmerged feature
|
19
|
+
# branch that introduces a new flag, or when a flag got deleted.
|
20
|
+
:unknown
|
21
|
+
end
|
22
|
+
new(name: feature_name, state: feature_state)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(name:, state:)
|
27
|
+
@name = name
|
28
|
+
@state = state
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
!!!
|
2
|
+
%html{ class: ("light-mode" if request.cookies["toogle-dark"] == "false") }
|
3
|
+
%head
|
4
|
+
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
|
5
|
+
%meta(name="viewport" content="width=device-width, initial-scale=1.0")
|
6
|
+
%title Feature flags
|
7
|
+
= csrf_meta_tags
|
8
|
+
= csp_meta_tag
|
9
|
+
= stylesheet_link_tag "toogle/application", media: "all"
|
10
|
+
%script(defer src="https://cdn.jsdelivr.net/npm/@alpinejs/anchor@3.x.x/dist/cdn.min.js")
|
11
|
+
%script(defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js")
|
12
|
+
%body
|
13
|
+
%header
|
14
|
+
%nav.d-flex.gap
|
15
|
+
= link_to main_app.root_path do
|
16
|
+
= render partial: "shared/logo", formats: :svg
|
17
|
+
%span ›
|
18
|
+
= link_to_unless_current "Feature flags", features_path do
|
19
|
+
%span Feature flags
|
20
|
+
- if @feature
|
21
|
+
%span ›
|
22
|
+
%span= @feature.name
|
23
|
+
|
24
|
+
%button(x-data="darkModeSwitcher" x-bind="button")
|
25
|
+
%svg(x-show="isDark" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
|
26
|
+
-# 🔆
|
27
|
+
%circle(cx="12" cy="12" r="5")
|
28
|
+
%line(x1="12" y1="1" x2="12" y2="3")
|
29
|
+
%line(x1="12" y1="21" x2="12" y2="23")
|
30
|
+
%line(x1="4.22" y1="4.22" x2="5.64" y2="5.64")
|
31
|
+
%line(x1="18.36" y1="18.36" x2="19.78" y2="19.78")
|
32
|
+
%line(x1="1" y1="12" x2="3" y2="12")
|
33
|
+
%line(x1="21" y1="12" x2="23" y2="12")
|
34
|
+
%line(x1="4.22" y1="19.78" x2="5.64" y2="18.36")
|
35
|
+
%line(x1="18.36" y1="5.64" x2="19.78" y2="4.22")
|
36
|
+
%svg(x-show="!isDark" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
|
37
|
+
-# 🌙
|
38
|
+
%path(d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z")
|
39
|
+
|
40
|
+
%main
|
41
|
+
- if notice.present?
|
42
|
+
#notice.d-flex.row.center.gap
|
43
|
+
%svg(width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
|
44
|
+
%circle(cx="12" cy="12" r="10")
|
45
|
+
%line(x1="12" y1="16" x2="12" y2="12")
|
46
|
+
%line(x1="12" y1="8" x2="12.01" y2="8")
|
47
|
+
%span.grow= notice
|
48
|
+
= yield
|
49
|
+
%footer
|
50
|
+
%a(href="https://gitlab.com/thutterer/toogle" target="_blank")
|
51
|
+
= Toogle::VERSION
|
52
|
+
|
53
|
+
= render "alpine_components"
|
@@ -0,0 +1,56 @@
|
|
1
|
+
:javascript
|
2
|
+
document.addEventListener("alpine:init", () => {
|
3
|
+
Alpine.data("toggle", (featureName, isChecked) => ({
|
4
|
+
name: featureName,
|
5
|
+
checked: isChecked,
|
6
|
+
|
7
|
+
input: {
|
8
|
+
['@change']() {
|
9
|
+
fetch(`./${this.name}.json`, {
|
10
|
+
method: "PUT",
|
11
|
+
headers: {
|
12
|
+
'Content-Type': 'application/json'
|
13
|
+
},
|
14
|
+
body: JSON.stringify({state: this.checked ? 'disabled' : 'enabled'})
|
15
|
+
}).then(() => {
|
16
|
+
// Always reload the page after change as a lazy way to move newly
|
17
|
+
// enabled feature flags into the top section.
|
18
|
+
// TODO: Either make this a classic form element or go full Alpine.
|
19
|
+
window.location = '#{features_url}'
|
20
|
+
})
|
21
|
+
},
|
22
|
+
},
|
23
|
+
}));
|
24
|
+
|
25
|
+
Alpine.data("darkModeSwitcher", () => ({
|
26
|
+
isDark: undefined,
|
27
|
+
|
28
|
+
init() {
|
29
|
+
const cookieValue = this.getCookieValue()
|
30
|
+
if (cookieValue) {
|
31
|
+
this.isDark = cookieValue === "true"
|
32
|
+
} else {
|
33
|
+
this.isDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
34
|
+
}
|
35
|
+
},
|
36
|
+
|
37
|
+
getCookieValue() {
|
38
|
+
return document.cookie.match('(^|;)\\s*' + 'toogle-dark' + '\\s*=\\s*([^;]+)')?.pop() || ''
|
39
|
+
},
|
40
|
+
|
41
|
+
button: {
|
42
|
+
['@click']() {
|
43
|
+
this.isDark = !this.isDark
|
44
|
+
document.cookie = `toogle-dark=${this.isDark}; SameSite=Strict`
|
45
|
+
if(this.isDark) {
|
46
|
+
document.documentElement.classList.remove('light-mode');
|
47
|
+
} else {
|
48
|
+
document.documentElement.classList.add('light-mode');
|
49
|
+
}
|
50
|
+
},
|
51
|
+
['x-bind:title']() {
|
52
|
+
return `Switch to ${this.isDark ? 'Light Mode' : 'Dark Mode'}`
|
53
|
+
}
|
54
|
+
},
|
55
|
+
}));
|
56
|
+
});
|
@@ -0,0 +1,9 @@
|
|
1
|
+
%ul.scrollbox
|
2
|
+
- @definitions.each do |definition|
|
3
|
+
- name = definition.name
|
4
|
+
- enabled = definition.default_enabled
|
5
|
+
%li.d-flex.center.row.nowrap(x-show="$el.textContent.includes(query) || query == ''")
|
6
|
+
%label.toggle(x-data="toggle('#{name}', #{enabled})")
|
7
|
+
%input(type="checkbox" x-bind="input" x-model="checked"){checked: enabled}
|
8
|
+
%span.handle.round(:title="checked ? 'Enabled by default. Click to disable.' : 'Disabled by default. Click to enable.'")
|
9
|
+
%code.grow= name
|
@@ -0,0 +1,18 @@
|
|
1
|
+
%dialog(x-ref="dialog" x-data="{enable: #{@definition.present?}, feature: '#{@definition&.name}', isAlreadyEnabled: #{@definition && @features.select {|f| f.state == :enabled }.map(&:name).include?(@definition.name) || false}}" x-init="if(enable) $refs.dialog.showModal()")
|
2
|
+
.card{"x-on:click.outside": "$refs.dialog.close()"}
|
3
|
+
%header
|
4
|
+
%h2
|
5
|
+
%label.toggle(x-data="toggle(feature, isAlreadyEnabled)")
|
6
|
+
%input(type="checkbox" x-bind="input" x-model="checked")
|
7
|
+
%span.handle.round
|
8
|
+
|
9
|
+
%code(x-text="feature")
|
10
|
+
%template(x-if="isAlreadyEnabled")
|
11
|
+
%main
|
12
|
+
%p
|
13
|
+
This feature is already enabled.
|
14
|
+
Click the toggle to disable it.
|
15
|
+
%template(x-if="!isAlreadyEnabled")
|
16
|
+
%main
|
17
|
+
%p
|
18
|
+
Click the toggle to enable this feature.
|
@@ -0,0 +1,4 @@
|
|
1
|
+
= button_to feature_path(feature.name), method: :delete, title: "Forget this setting and use default" do
|
2
|
+
%svg(xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
|
3
|
+
%line(x1="18" y1="6" x2="6" y2="18")
|
4
|
+
%line(x1="6" y1="6" x2="18" y2="18")
|
@@ -0,0 +1,8 @@
|
|
1
|
+
- if feature.state == :unknown
|
2
|
+
%label.toggle(title="This feature does not exist on the current branch.")
|
3
|
+
%input(type="checkbox" disabled){name: feature.name}
|
4
|
+
%span.handle.round
|
5
|
+
- else
|
6
|
+
%label.toggle(x-data="toggle('#{feature.name}', #{feature.state == :enabled})")
|
7
|
+
%input(type="checkbox" x-bind="input" x-model="checked"){checked: feature.state == :enabled, name: feature.name}
|
8
|
+
%span.handle.round(:title="checked ? 'Enabled. Click to disable.' : 'Disabled. Click to enable.'")
|
@@ -0,0 +1,51 @@
|
|
1
|
+
.grow
|
2
|
+
.card
|
3
|
+
%h3 My feature flags
|
4
|
+
%small These features already use custom settings in this GDK.
|
5
|
+
|
6
|
+
- if @features.any?
|
7
|
+
%ul
|
8
|
+
- @features.each do |feature|
|
9
|
+
%li.feature-toggle(x-data="{ showMore: false }")
|
10
|
+
.d-flex.row
|
11
|
+
= render "toggle", feature: feature
|
12
|
+
|
13
|
+
%code.grow= feature.name
|
14
|
+
|
15
|
+
%button(title="Show Share URL" x-on:click="showMore = !showMore")
|
16
|
+
%svg(xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
|
17
|
+
%path(d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8")
|
18
|
+
%polyline(points="16 6 12 2 8 6")
|
19
|
+
%line(x1="12" y1="2" x2="12" y2="15")
|
20
|
+
|
21
|
+
= render "remove", feature: feature
|
22
|
+
|
23
|
+
.d-flex.col(x-show="showMore" x-data="{text: '#{request.url}#{feature.name}'}"){'@click.outside' => 'showMore = false'}
|
24
|
+
%small Share this URL with MR reviewers.
|
25
|
+
.d-flex.row
|
26
|
+
%input.grow(x-ref="copyText" x-model="text" type="text" readonly="readonly")
|
27
|
+
%button(x-on:click="$refs.copyText.select(); document.execCommand('copy');") Copy
|
28
|
+
- else
|
29
|
+
%p No active feature flags in this GDK yet.
|
30
|
+
|
31
|
+
.grow.d-flex
|
32
|
+
.card.grow(style="align-self: center;")
|
33
|
+
%h3 Search and toggle feature flags
|
34
|
+
.d-flex.col{
|
35
|
+
"x-data": "{query: '', show: false}",
|
36
|
+
"@click.outside": 'show = false'}
|
37
|
+
%input.large.w-100#search{
|
38
|
+
"type": "search",
|
39
|
+
"x-model.debounce.400ms": "query",
|
40
|
+
"x-ref": "search",
|
41
|
+
"x-on:focus": "show = true"}
|
42
|
+
.d-flex.col.stretch{
|
43
|
+
"x-show": "show",
|
44
|
+
"x-anchor.bottom-start.offset.5" => "$refs.search",
|
45
|
+
"x-transition": nil}
|
46
|
+
.grow{
|
47
|
+
"data-url": definitions_path,
|
48
|
+
"x-data": "{}",
|
49
|
+
"x-init": "$el.innerHTML = await (await fetch($el.dataset.url)).text()"}
|
50
|
+
|
51
|
+
= render "dialog"
|
data/config/routes.rb
ADDED
data/lib/toogle.rb
ADDED
metadata
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gdk-toogle
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.7.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Thomas Hutterer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-03-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 7.0.4.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 7.0.4.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: haml
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec-rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: capybara
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: selenium-webdriver
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Toogle is a Rails engine to toggle feature flags in GitLab development
|
84
|
+
environments.
|
85
|
+
email:
|
86
|
+
- thutterer@gitlab.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- MIT-LICENSE
|
92
|
+
- README.md
|
93
|
+
- Rakefile
|
94
|
+
- app/assets/config/toogle_manifest.js
|
95
|
+
- app/assets/stylesheets/toogle/application.css
|
96
|
+
- app/assets/stylesheets/toogle/components/card.css
|
97
|
+
- app/assets/stylesheets/toogle/components/scrollbox.css
|
98
|
+
- app/assets/stylesheets/toogle/components/toggle.css
|
99
|
+
- app/assets/stylesheets/toogle/dark-mode.css
|
100
|
+
- app/assets/stylesheets/toogle/elements.css
|
101
|
+
- app/assets/stylesheets/toogle/layout.css
|
102
|
+
- app/assets/stylesheets/toogle/utilities.css
|
103
|
+
- app/assets/stylesheets/toogle/variables.css
|
104
|
+
- app/controllers/toogle/application_controller.rb
|
105
|
+
- app/controllers/toogle/definitions_controller.rb
|
106
|
+
- app/controllers/toogle/features_controller.rb
|
107
|
+
- app/models/toogle/definition.rb
|
108
|
+
- app/models/toogle/feature.rb
|
109
|
+
- app/views/layouts/toogle/application.html.haml
|
110
|
+
- app/views/toogle/application/_alpine_components.html.haml
|
111
|
+
- app/views/toogle/definitions/index.html.haml
|
112
|
+
- app/views/toogle/features/_dialog.html.haml
|
113
|
+
- app/views/toogle/features/_remove.html.haml
|
114
|
+
- app/views/toogle/features/_toggle.html.haml
|
115
|
+
- app/views/toogle/features/index.html.haml
|
116
|
+
- config/routes.rb
|
117
|
+
- lib/toogle.rb
|
118
|
+
- lib/toogle/engine.rb
|
119
|
+
- lib/toogle/version.rb
|
120
|
+
homepage: https://gitlab.com/thutterer/toogle
|
121
|
+
licenses:
|
122
|
+
- MIT
|
123
|
+
metadata:
|
124
|
+
allowed_push_host: https://rubygems.org
|
125
|
+
homepage_uri: https://gitlab.com/thutterer/toogle
|
126
|
+
source_code_uri: https://gitlab.com/thutterer/toogle
|
127
|
+
changelog_uri: https://gitlab.com/thutterer/toogle
|
128
|
+
post_install_message:
|
129
|
+
rdoc_options: []
|
130
|
+
require_paths:
|
131
|
+
- lib
|
132
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
requirements: []
|
143
|
+
rubygems_version: 3.4.10
|
144
|
+
signing_key:
|
145
|
+
specification_version: 4
|
146
|
+
summary: Toogle toggles feature flags
|
147
|
+
test_files: []
|