gdk-toogle 0.7.0 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc23503c85ec958b0051f3dd65ae20430ef7260df40441c6bc887b5caa1b3502
4
- data.tar.gz: f88f8017af1429e311e216cab810197c4d4b5d9feafa327633dda26e84a70739
3
+ metadata.gz: 126d3207f78acad2e5a2af3959117efd93a9a6b210187afafb4b4b69e1db33b9
4
+ data.tar.gz: 26b6411779359a408db3f218e3e9d71d5f33538277c9cf276183c8756c63ede8
5
5
  SHA512:
6
- metadata.gz: b99d1e496643408bfaee5ff88409e5136fb89fa895b36b8f94ee08210a4d9ae03fe578d4699e5552672fceb345bb9d83c82ee557147348057d712b73e7803ce5
7
- data.tar.gz: d24eaa1a2864ab9734d242843f7fc5cda999946fe361fc13d3a023d7c312d9dded7fa78c51a02094a231c171a5e4f92bb972bb1474e254fe24e5c62cd3e70c27
6
+ metadata.gz: 19cd5ab3ca2ecf53b0f4712ada95810c468a37e963d70cf726081dacf6797ce2cf63d9cc193c79d924a658e7d5effcb4042f1e5cc0f0210a11b4cc077a45b04a
7
+ data.tar.gz: 9f88ab74e9ed65a6ccdc031378d59b3310b52f2cdd0a8703d58f837cf7c357f9f2679b754fb525c99c1e7d934e81ef10e1c775f627ecad8c13da108b6aece209
data/README.md CHANGED
@@ -8,14 +8,7 @@ This gem was written specifically for the GitLab codebase. It won't work in any
8
8
 
9
9
  ### Usage within the GitLab Development Kit
10
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.
11
+ The UI is availabe at http://gdk.test:3000/rails/features.
19
12
 
20
13
  ## Contributing
21
14
 
@@ -29,6 +22,23 @@ The system specs in `spec/system` are configured to use Firefox as headless brow
29
22
 
30
23
  **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
24
 
25
+ ## Tech stack
26
+
27
+ One design goal of this little Rails engine was to keep it free of any dependencies, that GitLab uses today.
28
+ Because, tomorrow we might decide to ditch one or the other tech we use today, and Toogle shouldn't break just because GitLab removes `sassc`, as an example (that actually happened during development).
29
+
30
+ That's why Toogle only depends on the bare basics: `rails`, `sprockets` and `haml`.
31
+
32
+ ### Javascript
33
+
34
+ Toogle uses [Alpine.js](https://alpinejs.dev/) from a `<script>` tag so it does not use or depend on our Vue.js usage or any bundler. Alpine is very similar to Vue, so it's really easy to pick up. Give it a try!
35
+
36
+ ### CSS
37
+
38
+ Toogle uses native unprocessed CSS to not depend on any specific preprocessor. Modern CSS is awesome, and all modern browsers support things like nested selectors today.
39
+
40
+ For theming and dark mode, it uses CSS variables and HSL colors, which makes it super easy to switch from light to dark by just changing the variable that makes up the _Lightness_. Check it out!
41
+
32
42
  ## License
33
43
 
34
44
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1 +1,2 @@
1
1
  //= link toogle/application.css
2
+ //= link toogle/application.js
@@ -0,0 +1,117 @@
1
+ document.addEventListener("alpine:init", () => {
2
+ Alpine.data("features", (indexUrl) => ({
3
+ features: [],
4
+ indexUrl,
5
+
6
+ init() {
7
+ fetch(this.indexUrl, {
8
+ method: "GET",
9
+ headers: {
10
+ Accept: "application/json",
11
+ },
12
+ })
13
+ .then((response) => response.json())
14
+ .then((data) => {
15
+ this.features = data;
16
+ })
17
+ .catch((error) => console.error("Error fetching data:", error));
18
+ },
19
+
20
+ toggleFeature(feature) {
21
+ const newState = feature.state === "enabled" ? "disabled" : "enabled";
22
+ fetch(`${this.indexUrl}/${feature.name}`, {
23
+ method: "PUT",
24
+ headers: {
25
+ Accept: "application/json",
26
+ "Content-Type": "application/json",
27
+ },
28
+ body: JSON.stringify({
29
+ state: newState,
30
+ }),
31
+ }).then(() => {
32
+ this.features = this.features.map((f) => {
33
+ return f === feature ? { ...f, state: newState } : f;
34
+ });
35
+ });
36
+ },
37
+
38
+ deleteFeature(featureName) {
39
+ const csrfToken = document.head.querySelector(
40
+ "meta[name=csrf-token]"
41
+ )?.content;
42
+
43
+ fetch(`${this.indexUrl}/${featureName}`, {
44
+ method: "DELETE",
45
+ headers: {
46
+ Accept: "application/json",
47
+ "X-CSRF-Token": csrfToken,
48
+ },
49
+ }).then(() => {
50
+ this.features = this.features.filter((f) => f.name !== featureName);
51
+ });
52
+ },
53
+ }));
54
+
55
+ Alpine.data("toggle", (featureName, isChecked, indexUrl) => ({
56
+ name: featureName,
57
+ checked: isChecked,
58
+ indexUrl,
59
+
60
+ input: {
61
+ ["@change"]() {
62
+ const csrfToken = document.head.querySelector(
63
+ "meta[name=csrf-token]"
64
+ )?.content;
65
+
66
+ fetch(`${this.indexUrl}/${this.name}`, {
67
+ method: "PUT",
68
+ headers: {
69
+ "Content-Type": "application/json",
70
+ "X-CSRF-Token": csrfToken,
71
+ },
72
+ body: JSON.stringify({
73
+ state: this.checked ? "disabled" : "enabled",
74
+ }),
75
+ }).then(() => {
76
+ window.location = this.indexUrl;
77
+ });
78
+ },
79
+ },
80
+ }));
81
+
82
+ Alpine.data("darkModeSwitcher", () => ({
83
+ isDark: undefined,
84
+
85
+ init() {
86
+ const cookieValue = this.getCookieValue();
87
+ if (cookieValue) {
88
+ this.isDark = cookieValue === "true";
89
+ } else {
90
+ this.isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
91
+ }
92
+ },
93
+
94
+ getCookieValue() {
95
+ return (
96
+ document.cookie
97
+ .match("(^|;)\\s*" + "toogle-dark" + "\\s*=\\s*([^;]+)")
98
+ ?.pop() || ""
99
+ );
100
+ },
101
+
102
+ button: {
103
+ ["@click"]() {
104
+ this.isDark = !this.isDark;
105
+ document.cookie = `toogle-dark=${this.isDark}; SameSite=Strict`;
106
+ if (this.isDark) {
107
+ document.documentElement.classList.remove("light-mode");
108
+ } else {
109
+ document.documentElement.classList.add("light-mode");
110
+ }
111
+ },
112
+ ["x-bind:title"]() {
113
+ return `Switch to ${this.isDark ? "Light Mode" : "Dark Mode"}`;
114
+ },
115
+ },
116
+ }));
117
+ });
@@ -8,8 +8,6 @@
8
8
  @import "components/scrollbox";
9
9
  @import "components/toggle";
10
10
 
11
- @import "dark-mode";
12
-
13
11
  [x-cloak] {
14
12
  display: none !important;
15
13
  }
@@ -1,6 +1,11 @@
1
1
  .card {
2
- background-color: #f0f0f0;
2
+ background-color: hsl(
3
+ var(--hue-primary),
4
+ var(--saturation-bg),
5
+ var(--lightness-bg1)
6
+ );
7
+ box-shadow: 0 10px 16px var(--color-shadow);
3
8
  border-radius: 5px;
4
- padding: 2rem;
5
- box-shadow: 0 0.5rem 1rem #888;
9
+ padding: 1rem;
10
+ max-width: 100%;
6
11
  }
@@ -1,13 +1,26 @@
1
1
  .scrollbox {
2
2
  max-height: 20ch;
3
3
  overflow: auto;
4
- padding: 1em 0;
4
+ padding: 1rem 0;
5
5
  word-break: break-all;
6
6
 
7
- background-color: white;
8
- border: 2px solid #444;
7
+ background-color: hsl(
8
+ var(--hue-primary),
9
+ var(--saturation-bg),
10
+ var(--lightness-bg2)
11
+ );
12
+ border: 2px solid
13
+ hsl(var(--hue-primary), var(--saturation-bg), var(--lightness-bg1));
9
14
 
10
- li:hover {
11
- background-color: rgba(153, 153, 153, 0.1);
15
+ border-style: ridge;
16
+
17
+ box-shadow: 0 5px 10px var(--color-shadow);
18
+
19
+ li {
20
+ padding: 0 1rem;
21
+
22
+ &:hover {
23
+ background-color: hsl(var(--hue-primary), 60%, var(--lightness-bg1));
24
+ }
12
25
  }
13
26
  }
@@ -4,6 +4,7 @@
4
4
  width: 60px;
5
5
  height: 34px;
6
6
  margin: 4px;
7
+ flex-shrink: 0;
7
8
 
8
9
  input {
9
10
  opacity: 0;
@@ -1,8 +1,11 @@
1
- html {
2
- --page-bg: #ddd;
1
+ * {
2
+ box-sizing: border-box;
3
+ }
3
4
 
4
- background-color: var(--page-bg);
5
+ html {
6
+ color: var(--color-text);
5
7
  transition: 0.2s;
8
+ background-color: var(--color-page-bg);
6
9
  }
7
10
 
8
11
  body {
@@ -11,7 +14,6 @@ body {
11
14
  > header {
12
15
  position: sticky;
13
16
  top: 0;
14
- background-color: var(--page-bg);
15
17
  transition: 0.2s;
16
18
  }
17
19
  }
@@ -27,34 +29,58 @@ p {
27
29
  }
28
30
 
29
31
  a {
30
- color: #222;
32
+ color: var(--color-primary);
31
33
  font-weight: bold;
32
34
  text-decoration: none;
33
- padding: 2px;
34
35
  border-radius: 2px;
35
36
  transition: outline 0.2s;
36
37
 
37
38
  &:focus {
38
- outline: 4px solid var(--color-primary);
39
+ outline: 2px solid var(--color-primary);
39
40
  }
40
41
  }
41
42
 
43
+ footer a {
44
+ color: var(--color-text);
45
+ }
46
+
42
47
  input[type="submit"]:not(.small) {
43
48
  font-size: large;
44
49
  padding: 0.25rem;
45
50
  }
46
51
 
47
- input[type="search"] {
52
+ input[type="search"],
53
+ input[type="text"] {
48
54
  margin: 0.25rem 0;
49
55
  padding: 0.75rem;
50
56
  font-family: monospace;
51
57
  border-radius: 0.5rem;
52
58
 
59
+ &:not(:read-only):focus {
60
+ outline: 2px solid var(--color-primary);
61
+ }
62
+
63
+ &:read-only:focus {
64
+ outline: none;
65
+ }
66
+
53
67
  &.large {
54
68
  font-size: large;
55
69
  }
56
70
  }
57
71
 
72
+ input[type="search"],
73
+ input[type="text"] {
74
+ color: hsl(var(--hue-primary), var(--saturation-bg), var(--lightness-fg));
75
+ background-color: hsl(
76
+ var(--hue-primary),
77
+ var(--saturation-bg),
78
+ var(--lightness-bg2)
79
+ );
80
+ border: none;
81
+ box-shadow: inset 1px 1px 2px var(--color-shadow);
82
+ }
83
+
58
84
  hr {
59
85
  margin: 1.5rem 0;
60
86
  }
@@ -64,6 +90,7 @@ code {
64
90
  }
65
91
 
66
92
  button {
93
+ color: var(--color-text);
67
94
  cursor: pointer;
68
95
  background-color: transparent;
69
96
  border: none;
@@ -84,13 +111,9 @@ button {
84
111
  }
85
112
 
86
113
  &:focus {
87
- outline: 4px solid var(--color-primary);
114
+ outline: 2px solid var(--color-primary);
88
115
  opacity: 1 !important;
89
116
  }
90
-
91
- > svg + span {
92
- margin-left: 0.25em;
93
- }
94
117
  }
95
118
 
96
119
  ul {
@@ -108,6 +131,7 @@ ul {
108
131
 
109
132
  dialog {
110
133
  background-color: transparent;
134
+ color: var(--color-text);
111
135
  border: none;
112
136
  outline: none;
113
137
 
@@ -12,7 +12,6 @@ body {
12
12
  gap: 1rem;
13
13
 
14
14
  padding: 1rem;
15
- z-index: 2;
16
15
 
17
16
  a,
18
17
  span {
@@ -25,13 +24,30 @@ body {
25
24
 
26
25
  > main {
27
26
  display: flex;
28
- flex-direction: column-reverse;
29
- width: 100ch;
30
- max-width: 95%;
31
- margin: auto;
27
+ flex-direction: column;
28
+ align-items: center;
29
+ justify-content: center;
32
30
  gap: 3rem;
33
- padding: 2rem 0;
31
+
32
+ padding: 0 1rem;
34
33
  flex-grow: 1;
34
+
35
+ &.with-sticky-search {
36
+ margin-top: 4rem;
37
+
38
+ .sticky-search {
39
+ align-self: center;
40
+ position: sticky;
41
+ width: min(100%, 80ch);
42
+ top: 0;
43
+ background-image: linear-gradient(
44
+ var(--color-page-bg) 90%,
45
+ transparent 100%
46
+ );
47
+ padding-bottom: 0.5rem;
48
+ z-index: 2;
49
+ }
50
+ }
35
51
  }
36
52
 
37
53
  > footer {
@@ -64,6 +80,16 @@ li.feature-toggle {
64
80
  padding: 0.5rem;
65
81
  }
66
82
 
83
+ .more-info {
84
+ padding-left: 68px; /* toggle width */
85
+ }
86
+
87
+ .metadata {
88
+ display: grid;
89
+ grid-template-columns: auto 1fr;
90
+ gap: 0.5rem;
91
+ }
92
+
67
93
  &:hover {
68
94
  background-color: rgba(153, 153, 153, 0.1);
69
95
 
@@ -35,3 +35,8 @@
35
35
  .w-100 {
36
36
  width: 100%;
37
37
  }
38
+
39
+ .ellipsis {
40
+ overflow-x: hidden;
41
+ text-overflow: ellipsis;
42
+ }
@@ -1,3 +1,28 @@
1
- :root {
2
- --color-primary: #fca326;
1
+ html {
2
+ --hue-primary: 35;
3
+ --saturation-bg: 5%;
4
+ --lightness-bg0: 80%;
5
+ --lightness-bg1: 88%;
6
+ --lightness-bg2: 96%;
7
+ --lightness-fg: 10%;
8
+ --lightness-shadow: 50%;
9
+
10
+ --color-primary: hsl(var(--hue-primary), 90%, 50%);
11
+ --color-text: hsl(var(--hue-primary), 0%, var(--lightness-fg));
12
+ --color-shadow: hsl(0, var(--saturation-bg), var(--lightness-shadow));
13
+ --color-page-bg: hsl(
14
+ var(--hue-primary),
15
+ var(--saturation-bg),
16
+ var(--lightness-bg0)
17
+ );
18
+ }
19
+
20
+ @media (prefers-color-scheme: dark) {
21
+ html:not(.light-mode) {
22
+ --lightness-bg0: 8%;
23
+ --lightness-bg1: 16%;
24
+ --lightness-bg2: 24%;
25
+ --lightness-fg: 90%;
26
+ --lightness-shadow: 4%;
27
+ }
3
28
  }
@@ -2,6 +2,11 @@ module Toogle
2
2
  class FeaturesController < ApplicationController
3
3
  def index
4
4
  @features = Toogle::Feature.all
5
+
6
+ respond_to do |format|
7
+ format.html
8
+ format.json { render json: @features }
9
+ end
5
10
  end
6
11
 
7
12
  def show
@@ -35,7 +40,16 @@ module Toogle
35
40
 
36
41
  def destroy
37
42
  ::Feature.remove(params[:id])
38
- redirect_to features_url, status: :see_other
43
+
44
+ respond_to do |format|
45
+ format.html do
46
+ redirect_to features_url, status: :see_other
47
+ end
48
+
49
+ format.json do
50
+ head :ok
51
+ end
52
+ end
39
53
  end
40
54
  end
41
55
  end
@@ -6,7 +6,13 @@ module Toogle
6
6
 
7
7
  def self.all
8
8
  ::Feature::Definition.definitions.map do |definition|
9
- new(name: definition[0].to_s, default_enabled: definition[1].default_enabled)
9
+ new(
10
+ name: definition[0].to_s,
11
+ default_enabled: definition[1].default_enabled,
12
+ milestone: definition[1].milestone,
13
+ introduced_by_url: definition[1].introduced_by_url,
14
+ rollout_issue_url: definition[1].rollout_issue_url,
15
+ )
10
16
  end
11
17
  end
12
18
 
@@ -19,9 +25,12 @@ module Toogle
19
25
  all.find { |definition| definition.name == name }
20
26
  end
21
27
 
22
- def initialize(name:, default_enabled:)
28
+ def initialize(name:, default_enabled:, milestone: nil, introduced_by_url: nil, rollout_issue_url: nil)
23
29
  @name = name
24
30
  @default_enabled = default_enabled
31
+ @milestone = milestone
32
+ @introduced_by_url = introduced_by_url
33
+ @rollout_issue_url = rollout_issue_url
25
34
  end
26
35
  end
27
36
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Toogle
4
4
  class Feature
5
- attr_accessor :name, :state
5
+ attr_accessor :name, :state, :definition
6
6
 
7
7
  def self.all
8
8
  definitions = Definition.all
@@ -11,21 +11,22 @@ module Toogle
11
11
  feature_class: ::Feature::FlipperFeature,
12
12
  gate_class: ::Feature::FlipperGate
13
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
14
+ feature_definition = Definition.find(feature_name)
15
+ feature_state = if feature_definition
16
16
  feature_values[:boolean] ? :enabled: :disabled
17
17
  else
18
18
  # This usually happens when switching back from an unmerged feature
19
19
  # branch that introduces a new flag, or when a flag got deleted.
20
20
  :unknown
21
21
  end
22
- new(name: feature_name, state: feature_state)
22
+ new(name: feature_name, state: feature_state, definition: feature_definition)
23
23
  end
24
24
  end
25
25
 
26
- def initialize(name:, state:)
26
+ def initialize(name:, state:, definition:)
27
27
  @name = name
28
28
  @state = state
29
+ @definition = definition
29
30
  end
30
31
  end
31
32
  end
@@ -7,19 +7,15 @@
7
7
  = csrf_meta_tags
8
8
  = csp_meta_tag
9
9
  = stylesheet_link_tag "toogle/application", media: "all"
10
+ = javascript_include_tag 'toogle/application'
10
11
  %script(defer src="https://cdn.jsdelivr.net/npm/@alpinejs/anchor@3.x.x/dist/cdn.min.js")
11
12
  %script(defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js")
13
+
12
14
  %body
13
15
  %header
14
16
  %nav.d-flex.gap
15
17
  = link_to main_app.root_path do
16
18
  = 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
19
 
24
20
  %button(x-data="darkModeSwitcher" x-bind="button")
25
21
  %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,7 +33,8 @@
37
33
  -# 🌙
38
34
  %path(d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z")
39
35
 
40
- %main
36
+ %main.with-sticky-search
37
+ = yield
41
38
  - if notice.present?
42
39
  #notice.d-flex.row.center.gap
43
40
  %svg(width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
@@ -45,9 +42,7 @@
45
42
  %line(x1="12" y1="16" x2="12" y2="12")
46
43
  %line(x1="12" y1="8" x2="12.01" y2="8")
47
44
  %span.grow= notice
48
- = yield
45
+
49
46
  %footer
50
47
  %a(href="https://gitlab.com/thutterer/toogle" target="_blank")
51
48
  = Toogle::VERSION
52
-
53
- = render "alpine_components"
@@ -3,7 +3,7 @@
3
3
  - name = definition.name
4
4
  - enabled = definition.default_enabled
5
5
  %li.d-flex.center.row.nowrap(x-show="$el.textContent.includes(query) || query == ''")
6
- %label.toggle(x-data="toggle('#{name}', #{enabled})")
6
+ %label.toggle(x-data="toggle('#{name}', #{enabled}, '#{features_url}')")
7
7
  %input(type="checkbox" x-bind="input" x-model="checked"){checked: enabled}
8
8
  %span.handle.round(:title="checked ? 'Enabled by default. Click to disable.' : 'Disabled by default. Click to enable.'")
9
9
  %code.grow= name
@@ -2,7 +2,7 @@
2
2
  .card{"x-on:click.outside": "$refs.dialog.close()"}
3
3
  %header
4
4
  %h2
5
- %label.toggle(x-data="toggle(feature, isAlreadyEnabled)")
5
+ %label.toggle(x-data="toggle(feature, isAlreadyEnabled, '#{features_url}')")
6
6
  %input(type="checkbox" x-bind="input" x-model="checked")
7
7
  %span.handle.round
8
8
 
@@ -1,46 +1,17 @@
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"}
1
+ .sticky-search
2
+ .d-flex.col{
3
+ "x-data": "{query: '', show: false}",
4
+ "@click.outside": "show = false",
5
+ "@keyup.esc": "show = false"}
6
+ %input.large.w-100#search{
7
+ "type": "search",
8
+ "placeholder": "Search and toggle feature flags",
9
+ "x-model.debounce.400ms": "query",
10
+ "x-ref": "search",
11
+ "x-on:input": "show = true",
12
+ "x-on:focus": "show = true"}
13
+ %template(x-if="show")
42
14
  .d-flex.col.stretch{
43
- "x-show": "show",
44
15
  "x-anchor.bottom-start.offset.5" => "$refs.search",
45
16
  "x-transition": nil}
46
17
  .grow{
@@ -48,4 +19,58 @@
48
19
  "x-data": "{}",
49
20
  "x-init": "$el.innerHTML = await (await fetch($el.dataset.url)).text()"}
50
21
 
22
+ .card
23
+ %h3 My feature flags
24
+ %small These features already use custom settings in this GDK.
25
+
26
+ %ul(x-data="features('#{features_url}')")
27
+ %template(x-for="feature in features" :key="feature.name")
28
+ %li.feature-toggle(x-data="{ showMore: false }"){"@click.outside": "showMore = false"}
29
+ .d-flex.row.nowrap
30
+ %template(x-if="feature.state === 'unknown'")
31
+ %label.toggle(title="This feature does not exist on the current branch.")
32
+ %input(type="checkbox" disabled :name="feature.name")
33
+ %span.handle.round
34
+ %template(x-if="feature.state !== 'unknown'")
35
+ %label.toggle
36
+ %input(type="checkbox" x-on:change="toggleFeature(feature)" :checked="feature.state === 'enabled'" :name="feature.name")
37
+ %span.handle.round
38
+
39
+ %code.grow.ellipsis(x-text="feature.name")
40
+
41
+ %button(title="Show more" x-on:click="showMore = !showMore")
42
+ %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")
43
+ %circle(cx="12" cy="12" r="1")
44
+ %circle(cx="12" cy="5" r="1")
45
+ %circle(cx="12" cy="19" r="1")
46
+
47
+ %button(title="Forget this setting and use default" x-on:click="deleteFeature(feature.name)")
48
+ %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")
49
+ %line(x1="18" y1="6" x2="6" y2="18")
50
+ %line(x1="6" y1="6" x2="18" y2="18")
51
+
52
+ .d-flex.col.more-info{ "x-show": "showMore" }
53
+ .metadata
54
+ %strong Milestone
55
+ %span(x-text="feature.definition.milestone || '-'")
56
+ %strong Introduced by
57
+ %a(x-show="feature.definition.introduced_by_url"
58
+ x-text="`!${feature.definition.introduced_by_url?.split('/').at(-1)}`"
59
+ :href="feature.definition.introduced_by_url"
60
+ target="_blank")
61
+ %span(x-show="!feature.definition.introduced_by_url") -
62
+ %strong Rollout issue
63
+ %a(x-show="feature.definition.rollout_issue_url"
64
+ x-text="`#${feature.definition.rollout_issue_url?.split('/').at(-1)}`"
65
+ :href="feature.definition.rollout_issue_url"
66
+ target="_blank")
67
+ %span(x-show="!feature.definition.rollout_issue_url") -
68
+
69
+ %hr
70
+ %small Share this URL with MR reviewers.
71
+ .d-flex.row
72
+ %input.grow(x-ref="copyText" :value="`${indexUrl}${feature.name}`" type="text" readonly="readonly")
73
+ %button(x-on:click="$refs.copyText.select(); document.execCommand('copy');") Copy
74
+ %p(x-show="!features.length") No active feature flags in this GDK yet.
75
+
51
76
  = render "dialog"
@@ -1,3 +1,3 @@
1
1
  module Toogle
2
- VERSION = "0.7.0"
2
+ VERSION = "0.9.5"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gdk-toogle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Hutterer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-13 00:00:00.000000000 Z
11
+ date: 2024-05-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -92,11 +92,11 @@ files:
92
92
  - README.md
93
93
  - Rakefile
94
94
  - app/assets/config/toogle_manifest.js
95
+ - app/assets/javascript/toogle/application.js
95
96
  - app/assets/stylesheets/toogle/application.css
96
97
  - app/assets/stylesheets/toogle/components/card.css
97
98
  - app/assets/stylesheets/toogle/components/scrollbox.css
98
99
  - app/assets/stylesheets/toogle/components/toggle.css
99
- - app/assets/stylesheets/toogle/dark-mode.css
100
100
  - app/assets/stylesheets/toogle/elements.css
101
101
  - app/assets/stylesheets/toogle/layout.css
102
102
  - app/assets/stylesheets/toogle/utilities.css
@@ -107,10 +107,8 @@ files:
107
107
  - app/models/toogle/definition.rb
108
108
  - app/models/toogle/feature.rb
109
109
  - app/views/layouts/toogle/application.html.haml
110
- - app/views/toogle/application/_alpine_components.html.haml
111
110
  - app/views/toogle/definitions/index.html.haml
112
111
  - app/views/toogle/features/_dialog.html.haml
113
- - app/views/toogle/features/_remove.html.haml
114
112
  - app/views/toogle/features/_toggle.html.haml
115
113
  - app/views/toogle/features/index.html.haml
116
114
  - config/routes.rb
@@ -1,25 +0,0 @@
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
- }
@@ -1,56 +0,0 @@
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
- });
@@ -1,4 +0,0 @@
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")