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 +4 -4
- data/README.md +18 -8
- data/app/assets/config/toogle_manifest.js +1 -0
- data/app/assets/javascript/toogle/application.js +117 -0
- data/app/assets/stylesheets/toogle/application.css +0 -2
- data/app/assets/stylesheets/toogle/components/card.css +8 -3
- data/app/assets/stylesheets/toogle/components/scrollbox.css +18 -5
- data/app/assets/stylesheets/toogle/components/toggle.css +1 -0
- data/app/assets/stylesheets/toogle/elements.css +37 -13
- data/app/assets/stylesheets/toogle/layout.css +32 -6
- data/app/assets/stylesheets/toogle/utilities.css +5 -0
- data/app/assets/stylesheets/toogle/variables.css +27 -2
- data/app/controllers/toogle/features_controller.rb +15 -1
- data/app/models/toogle/definition.rb +11 -2
- data/app/models/toogle/feature.rb +6 -5
- data/app/views/layouts/toogle/application.html.haml +5 -10
- data/app/views/toogle/definitions/index.html.haml +1 -1
- data/app/views/toogle/features/_dialog.html.haml +1 -1
- data/app/views/toogle/features/index.html.haml +67 -42
- data/lib/toogle/version.rb +1 -1
- metadata +3 -5
- data/app/assets/stylesheets/toogle/dark-mode.css +0 -25
- data/app/views/toogle/application/_alpine_components.html.haml +0 -56
- data/app/views/toogle/features/_remove.html.haml +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 126d3207f78acad2e5a2af3959117efd93a9a6b210187afafb4b4b69e1db33b9
|
4
|
+
data.tar.gz: 26b6411779359a408db3f218e3e9d71d5f33538277c9cf276183c8756c63ede8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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).
|
@@ -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
|
+
});
|
@@ -1,6 +1,11 @@
|
|
1
1
|
.card {
|
2
|
-
background-color:
|
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:
|
5
|
-
|
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:
|
4
|
+
padding: 1rem 0;
|
5
5
|
word-break: break-all;
|
6
6
|
|
7
|
-
background-color:
|
8
|
-
|
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
|
-
|
11
|
-
|
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
|
}
|
@@ -1,8 +1,11 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
* {
|
2
|
+
box-sizing: border-box;
|
3
|
+
}
|
3
4
|
|
4
|
-
|
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:
|
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:
|
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:
|
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
|
29
|
-
|
30
|
-
|
31
|
-
margin: auto;
|
27
|
+
flex-direction: column;
|
28
|
+
align-items: center;
|
29
|
+
justify-content: center;
|
32
30
|
gap: 3rem;
|
33
|
-
|
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
|
|
@@ -1,3 +1,28 @@
|
|
1
|
-
|
2
|
-
--
|
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
|
-
|
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(
|
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
|
-
|
15
|
-
feature_state = if
|
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
|
-
|
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
|
-
.
|
2
|
-
.
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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"
|
data/lib/toogle/version.rb
CHANGED
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.
|
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-
|
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")
|