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 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: []