gdk-toogle 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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,6 @@
1
+ .card {
2
+ background-color: #f0f0f0;
3
+ border-radius: 5px;
4
+ padding: 2rem;
5
+ box-shadow: 0 0.5rem 1rem #888;
6
+ }
@@ -0,0 +1,13 @@
1
+ .scrollbox {
2
+ max-height: 20ch;
3
+ overflow: auto;
4
+ padding: 1em 0;
5
+ word-break: break-all;
6
+
7
+ background-color: white;
8
+ border: 2px solid #444;
9
+
10
+ li:hover {
11
+ background-color: rgba(153, 153, 153, 0.1);
12
+ }
13
+ }
@@ -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,3 @@
1
+ :root {
2
+ --color-primary: #fca326;
3
+ }
@@ -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,10 @@
1
+ module Toogle
2
+ class DefinitionsController < ApplicationController
3
+ # This controller is only used to fetch the long list of feature definitions async.
4
+ layout false
5
+
6
+ def index
7
+ @definitions = Toogle::Definition.unchanged
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,6 @@
1
+ Toogle::Engine.routes.draw do
2
+ resources :definitions, only: %i[index]
3
+
4
+ # Keep this line last as it matches any URL
5
+ resources :features, only: %i[index show update destroy], path: "/"
6
+ end
@@ -0,0 +1,11 @@
1
+ module Toogle
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Toogle
4
+
5
+ config.generators do |g|
6
+ g.test_framework :rspec
7
+ g.assets false
8
+ g.helper false
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Toogle
2
+ VERSION = "0.7.0"
3
+ end
data/lib/toogle.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "toogle/version"
2
+ require "toogle/engine"
3
+
4
+ module Toogle
5
+ end
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: []