gdk-toogle 0.9.5 → 1.0.1
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 +4 -4
- data/README.md +60 -7
- data/Rakefile +4 -1
- data/app/assets/javascript/toogle/application.js +184 -49
- data/app/assets/stylesheets/toogle/application.css +1 -3
- data/app/assets/stylesheets/toogle/components/toggle.css +36 -22
- data/app/assets/stylesheets/toogle/elements.css +87 -18
- data/app/assets/stylesheets/toogle/layout.css +47 -18
- data/app/assets/stylesheets/toogle/utilities.css +0 -4
- data/app/assets/stylesheets/toogle/variables.css +38 -7
- data/app/controllers/toogle/application_controller.rb +2 -5
- data/app/controllers/toogle/definitions_controller.rb +1 -4
- data/app/controllers/toogle/features_controller.rb +14 -4
- data/app/models/toogle/definition.rb +9 -8
- data/app/models/toogle/feature.rb +4 -4
- data/app/views/layouts/toogle/application.html.haml +14 -3
- data/app/views/toogle/features/_details_button.html.haml +7 -0
- data/app/views/toogle/features/index.html.haml +90 -63
- data/lib/toogle/version.rb +1 -1
- metadata +4 -11
- data/app/assets/stylesheets/toogle/components/card.css +0 -11
- data/app/assets/stylesheets/toogle/components/scrollbox.css +0 -26
- data/app/views/toogle/definitions/index.html.haml +0 -9
- data/app/views/toogle/features/_dialog.html.haml +0 -18
- data/app/views/toogle/features/_toggle.html.haml +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b7fb4c9b37ce9f5712cc5635ff39bea2b3745179ea554dbe4b941f2a9472cab
|
|
4
|
+
data.tar.gz: 57a6c432b93d5dc0e88c52f46c653ca26338638f99f866c57e56dc5540a21692
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da4f86f837651e78dc12c7c61e2563cb8e099ff5a6a454e00226728906427243649190743cc36b46bcac34967551542afe8da58f5f5a35e5c6fd834412d434e4
|
|
7
|
+
data.tar.gz: 807b565d41e6f131a0811fb7e4d980f1c5a0a5ad5a61c892e81f3c9e6d4c2c614fe269bfadf8087465467deca63d3297080366ad1827b4abd302a09c5fb30e21
|
data/README.md
CHANGED
|
@@ -4,23 +4,76 @@ A Rails engine web-UI to quickly toggle feature flags in GDK.
|
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
|
-
This gem was written specifically for the GitLab codebase. It
|
|
7
|
+
This gem was written specifically for the GitLab codebase. It relies on
|
|
8
|
+
GitLab's `Feature` classes, so mounting it in any other Rails app won't work
|
|
9
|
+
(for developing Toogle itself there is a standalone dummy app, see below).
|
|
8
10
|
|
|
9
11
|
### Usage within the GitLab Development Kit
|
|
10
12
|
|
|
11
|
-
The UI is
|
|
13
|
+
The UI is available at http://gdk.test:3000/rails/features.
|
|
12
14
|
|
|
13
15
|
## Contributing
|
|
14
16
|
|
|
15
|
-
Everyone can contribute.
|
|
17
|
+
Everyone can contribute. Open issues and merge requests at
|
|
18
|
+
https://gitlab.com/thutterer/toogle. Please make sure `rake` (specs and
|
|
19
|
+
linter) passes before submitting.
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
There is an `AGENTS.md` with the conventions and architecture notes that
|
|
22
|
+
keep this codebase simple — worth a read, and keep it accurate when you
|
|
23
|
+
change things.
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
### How to run it standalone
|
|
20
26
|
|
|
21
|
-
The
|
|
27
|
+
The dummy app used for testing includes fake in-memory versions of GitLab's
|
|
28
|
+
`Feature` and Flipper classes (see `spec/dummy/app/models/`), so you can
|
|
29
|
+
develop the engine without a GDK:
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
```sh
|
|
32
|
+
cd spec/dummy
|
|
33
|
+
bin/rails server
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then open http://localhost:3000/toogle. Toggled state lives in memory and
|
|
37
|
+
resets on restart.
|
|
38
|
+
|
|
39
|
+
To test your changes inside a real GDK, point GitLab's `Gemfile` at your
|
|
40
|
+
local checkout:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
gem 'gdk-toogle', path: '~/Code/toogle', require: 'toogle'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### How to run tests and linting
|
|
47
|
+
|
|
48
|
+
Run `rake` to run both the specs and the [standard](https://github.com/standardrb/standard)
|
|
49
|
+
linter. They are also available separately as `rake spec` and `rake standard`
|
|
50
|
+
(use `rake standard:fix` to autoformat).
|
|
51
|
+
|
|
52
|
+
To lint automatically before each commit, enable the bundled git hook once:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
git config core.hooksPath .githooks
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The system specs in `spec/system` use headless Firefox by default. Set
|
|
59
|
+
`CAPYBARA_DRIVER` to any registered Capybara driver to switch, e.g. if you
|
|
60
|
+
don't have Firefox or only a Snap-installed one (incompatible with
|
|
61
|
+
geckodriver):
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
CAPYBARA_DRIVER=selenium_chrome_headless bundle exec rspec
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### How to cut a release
|
|
68
|
+
|
|
69
|
+
1. Bump `Toogle::VERSION` in `lib/toogle/version.rb`.
|
|
70
|
+
2. Add an entry to `CHANGELOG.md`.
|
|
71
|
+
3. Commit, then tag: `git tag vX.Y.Z && git push --tags`.
|
|
72
|
+
4. Build and push the gem: `rake build` then `gem push pkg/gdk-toogle-X.Y.Z.gem`.
|
|
73
|
+
5. Bump the version constraint in GitLab's `Gemfile` to pick it up.
|
|
74
|
+
|
|
75
|
+
Keep Ruby and gem versions at or below what GitLab itself runs (see
|
|
76
|
+
`AGENTS.md`).
|
|
24
77
|
|
|
25
78
|
## Tech stack
|
|
26
79
|
|
data/Rakefile
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
document.addEventListener("alpine:init", () => {
|
|
2
|
-
Alpine.
|
|
2
|
+
Alpine.store("flash", {
|
|
3
|
+
message: "",
|
|
4
|
+
|
|
5
|
+
show(message) {
|
|
6
|
+
this.message = message;
|
|
7
|
+
},
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const showFetchError = (error) => {
|
|
11
|
+
console.error("Error fetching data:", error);
|
|
12
|
+
Alpine.store("flash").show(
|
|
13
|
+
"Could not reach the server. Is your GDK running?"
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const csrfToken = () =>
|
|
18
|
+
document.head.querySelector("meta[name=csrf-token]")?.content;
|
|
19
|
+
|
|
20
|
+
// Cap how many Defaults render at once. This is a search tool — the wanted
|
|
21
|
+
// flag is reachable by typing — so we never build the full ~700+ row DOM.
|
|
22
|
+
const MAX_DEFAULTS_SHOWN = 50;
|
|
23
|
+
|
|
24
|
+
Alpine.data("flags", (featuresUrl, definitionsUrl, shareName = "") => ({
|
|
3
25
|
features: [],
|
|
4
|
-
|
|
26
|
+
definitions: [],
|
|
27
|
+
query: "",
|
|
28
|
+
lastMoved: "",
|
|
29
|
+
moving: "",
|
|
30
|
+
selectedName: "",
|
|
31
|
+
max: MAX_DEFAULTS_SHOWN,
|
|
32
|
+
featuresUrl: featuresUrl.replace(/\/$/, ""),
|
|
33
|
+
definitionsUrl,
|
|
5
34
|
|
|
6
35
|
init() {
|
|
7
|
-
|
|
36
|
+
const featuresLoaded = this.fetchFeatures();
|
|
37
|
+
|
|
38
|
+
const definitionsLoaded = fetch(this.definitionsUrl, {
|
|
8
39
|
method: "GET",
|
|
9
40
|
headers: {
|
|
10
41
|
Accept: "application/json",
|
|
@@ -12,70 +43,174 @@ document.addEventListener("alpine:init", () => {
|
|
|
12
43
|
})
|
|
13
44
|
.then((response) => response.json())
|
|
14
45
|
.then((data) => {
|
|
15
|
-
this.
|
|
46
|
+
this.definitions = data;
|
|
16
47
|
})
|
|
17
|
-
.catch(
|
|
48
|
+
.catch(showFetchError);
|
|
49
|
+
|
|
50
|
+
if (shareName) {
|
|
51
|
+
Promise.all([featuresLoaded, definitionsLoaded]).then(() =>
|
|
52
|
+
this.openDetails(shareName)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
18
55
|
},
|
|
19
56
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
57
|
+
openDetails(name) {
|
|
58
|
+
this.selectedName = name;
|
|
59
|
+
history.replaceState({}, "", `${this.featuresUrl}/${name}`);
|
|
60
|
+
this.$refs.dialog.showModal();
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
closeDetails() {
|
|
64
|
+
this.selectedName = "";
|
|
65
|
+
history.replaceState({}, "", `${this.featuresUrl}/`);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// The dialog's subject, looked up live so it stays in sync after toggles.
|
|
69
|
+
selected() {
|
|
70
|
+
const feature = this.features.find((f) => f.name === this.selectedName);
|
|
71
|
+
if (feature) {
|
|
72
|
+
return {
|
|
73
|
+
state: feature.state,
|
|
74
|
+
definition: feature.definition,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const definition = this.definitions.find(
|
|
79
|
+
(d) => d.name === this.selectedName
|
|
80
|
+
);
|
|
81
|
+
if (definition) {
|
|
82
|
+
return {
|
|
83
|
+
state: definition.default_enabled ? "enabled" : "disabled",
|
|
84
|
+
definition,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
toggleSelected() {
|
|
92
|
+
const feature = this.features.find((f) => f.name === this.selectedName);
|
|
93
|
+
|
|
94
|
+
if (feature) {
|
|
95
|
+
this.toggleFeature(feature);
|
|
96
|
+
} else {
|
|
97
|
+
this.toggleDefault(this.selected().definition);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
fetchFeatures(movedName = "") {
|
|
102
|
+
return fetch(this.featuresUrl, {
|
|
103
|
+
method: "GET",
|
|
24
104
|
headers: {
|
|
25
105
|
Accept: "application/json",
|
|
26
|
-
"Content-Type": "application/json",
|
|
27
106
|
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
107
|
+
})
|
|
108
|
+
.then((response) => response.json())
|
|
109
|
+
.then((data) => {
|
|
110
|
+
if (movedName) {
|
|
111
|
+
this.animated(() => {
|
|
112
|
+
this.features = data;
|
|
113
|
+
}, movedName);
|
|
114
|
+
} else {
|
|
115
|
+
// Not animated: the initial load should not "fly in" rows.
|
|
116
|
+
this.features = data;
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
.catch(showFetchError);
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// Animates the layout change, e.g. a row moving between the sections.
|
|
123
|
+
// Only the moving row gets a view-transition-name (see the view), so the
|
|
124
|
+
// browser snapshots one row, not all ~700 — keeping toggles fast no
|
|
125
|
+
// matter how many flags exist. The moved row then gets a short "shine".
|
|
126
|
+
async animated(applyChange, movedName = "") {
|
|
127
|
+
if (!document.startViewTransition || !movedName) return applyChange();
|
|
128
|
+
|
|
129
|
+
// Name the moving row before the browser snapshots the old state.
|
|
130
|
+
this.moving = movedName;
|
|
131
|
+
await this.$nextTick();
|
|
132
|
+
|
|
133
|
+
const transition = document.startViewTransition(() => {
|
|
134
|
+
applyChange();
|
|
135
|
+
return this.$nextTick();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await transition.finished.catch(() => {});
|
|
139
|
+
this.moving = "";
|
|
140
|
+
this.lastMoved = movedName;
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
if (this.lastMoved === movedName) this.lastMoved = "";
|
|
143
|
+
}, 1200);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
filteredFeatures() {
|
|
147
|
+
return this.features.filter((f) => f.name.includes(this.query.trim()));
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// All definitions matching the query, minus those already customized.
|
|
151
|
+
matchingDefinitions() {
|
|
152
|
+
const customized = this.features.map((f) => f.name);
|
|
153
|
+
return this.definitions.filter(
|
|
154
|
+
(d) =>
|
|
155
|
+
d.name.includes(this.query.trim()) && !customized.includes(d.name)
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Only the rows we actually render (capped); see MAX_DEFAULTS_SHOWN.
|
|
160
|
+
filteredDefinitions() {
|
|
161
|
+
return this.matchingDefinitions().slice(0, this.max);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
defaultsCapped() {
|
|
165
|
+
return this.matchingDefinitions().length > this.max;
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
toggleFeature(feature) {
|
|
169
|
+
const newState = feature.state === "enabled" ? "disabled" : "enabled";
|
|
170
|
+
|
|
171
|
+
this.putState(feature.name, newState).then(() => {
|
|
32
172
|
this.features = this.features.map((f) => {
|
|
33
173
|
return f === feature ? { ...f, state: newState } : f;
|
|
34
174
|
});
|
|
35
175
|
});
|
|
36
176
|
},
|
|
37
177
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
"meta[name=csrf-token]"
|
|
41
|
-
)?.content;
|
|
178
|
+
toggleDefault(definition) {
|
|
179
|
+
const newState = definition.default_enabled ? "disabled" : "enabled";
|
|
42
180
|
|
|
43
|
-
|
|
44
|
-
|
|
181
|
+
this.putState(definition.name, newState).then(() =>
|
|
182
|
+
this.fetchFeatures(definition.name)
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
putState(featureName, state) {
|
|
187
|
+
return fetch(`${this.featuresUrl}/${featureName}`, {
|
|
188
|
+
method: "PUT",
|
|
45
189
|
headers: {
|
|
46
190
|
Accept: "application/json",
|
|
47
|
-
"
|
|
191
|
+
"Content-Type": "application/json",
|
|
192
|
+
"X-CSRF-Token": csrfToken(),
|
|
48
193
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
194
|
+
body: JSON.stringify({ state }),
|
|
195
|
+
}).catch(showFetchError);
|
|
52
196
|
},
|
|
53
|
-
}));
|
|
54
197
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
},
|
|
72
|
-
body: JSON.stringify({
|
|
73
|
-
state: this.checked ? "disabled" : "enabled",
|
|
74
|
-
}),
|
|
75
|
-
}).then(() => {
|
|
76
|
-
window.location = this.indexUrl;
|
|
77
|
-
});
|
|
78
|
-
},
|
|
198
|
+
deleteFeature(featureName) {
|
|
199
|
+
fetch(`${this.featuresUrl}/${featureName}`, {
|
|
200
|
+
method: "DELETE",
|
|
201
|
+
headers: {
|
|
202
|
+
Accept: "application/json",
|
|
203
|
+
"X-CSRF-Token": csrfToken(),
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
.then(() => {
|
|
207
|
+
this.animated(() => {
|
|
208
|
+
this.features = this.features.filter(
|
|
209
|
+
(f) => f.name !== featureName
|
|
210
|
+
);
|
|
211
|
+
}, featureName);
|
|
212
|
+
})
|
|
213
|
+
.catch(showFetchError);
|
|
79
214
|
},
|
|
80
215
|
}));
|
|
81
216
|
|
|
@@ -4,15 +4,13 @@
|
|
|
4
4
|
@import "layout";
|
|
5
5
|
@import "utilities";
|
|
6
6
|
|
|
7
|
-
@import "components/card";
|
|
8
|
-
@import "components/scrollbox";
|
|
9
7
|
@import "components/toggle";
|
|
10
8
|
|
|
11
9
|
[x-cloak] {
|
|
12
10
|
display: none !important;
|
|
13
11
|
}
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
.notice {
|
|
16
14
|
background-color: #fc6d26;
|
|
17
15
|
color: white;
|
|
18
16
|
padding: 0.5rem;
|
|
@@ -15,11 +15,9 @@
|
|
|
15
15
|
.handle {
|
|
16
16
|
position: absolute;
|
|
17
17
|
cursor: pointer;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
bottom: 0;
|
|
22
|
-
background-color: #ccc;
|
|
18
|
+
inset: 0;
|
|
19
|
+
background-color: var(--color-track);
|
|
20
|
+
box-shadow: inset 0 1px 3px var(--color-shadow);
|
|
23
21
|
transition: 0.2s;
|
|
24
22
|
|
|
25
23
|
&:before {
|
|
@@ -29,10 +27,19 @@
|
|
|
29
27
|
width: 26px;
|
|
30
28
|
left: 4px;
|
|
31
29
|
bottom: 4px;
|
|
32
|
-
background-color:
|
|
30
|
+
background-color: var(--color-thumb);
|
|
31
|
+
box-shadow: 0 1px 2px var(--color-shadow);
|
|
33
32
|
transition: 0.2s;
|
|
34
33
|
}
|
|
35
34
|
|
|
35
|
+
&:hover:before {
|
|
36
|
+
filter: brightness(1.1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&:active:before {
|
|
40
|
+
transform: scale(0.92);
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
&.round {
|
|
37
44
|
border-radius: 34px;
|
|
38
45
|
}
|
|
@@ -42,30 +49,37 @@
|
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
input:checked + .handle {
|
|
52
|
+
input:checked + .handle:before {
|
|
46
53
|
background-color: var(--color-primary);
|
|
54
|
+
transform: translateX(26px);
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
input:
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
input:checked + .handle:before {
|
|
54
|
-
-webkit-transform: translateX(26px);
|
|
55
|
-
-ms-transform: translateX(26px);
|
|
56
|
-
transform: translateX(26px);
|
|
57
|
+
input:checked + .handle:active:before {
|
|
58
|
+
transform: translateX(26px) scale(0.92);
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
input:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
text-align: center;
|
|
63
|
-
color: white;
|
|
64
|
-
font-size: x-small;
|
|
65
|
-
line-height: 26px;
|
|
61
|
+
input:focus-visible + .handle {
|
|
62
|
+
outline: 2px solid var(--color-primary);
|
|
63
|
+
outline-offset: 2px;
|
|
66
64
|
}
|
|
67
65
|
|
|
66
|
+
/* Truly disabled (e.g. flag unknown on this branch): flat and washed out
|
|
67
|
+
into the page, nothing raised or sunken that could look clickable. */
|
|
68
68
|
input:disabled + .handle {
|
|
69
69
|
cursor: not-allowed;
|
|
70
|
+
background-color: var(--color-disabled);
|
|
71
|
+
box-shadow: none;
|
|
72
|
+
|
|
73
|
+
&:before,
|
|
74
|
+
&:hover:before {
|
|
75
|
+
content: "N/A";
|
|
76
|
+
text-align: center;
|
|
77
|
+
color: var(--color-text-muted);
|
|
78
|
+
font-size: x-small;
|
|
79
|
+
line-height: 26px;
|
|
80
|
+
background-color: transparent;
|
|
81
|
+
box-shadow: none;
|
|
82
|
+
filter: none;
|
|
83
|
+
}
|
|
70
84
|
}
|
|
71
85
|
}
|
|
@@ -44,11 +44,6 @@ footer a {
|
|
|
44
44
|
color: var(--color-text);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
input[type="submit"]:not(.small) {
|
|
48
|
-
font-size: large;
|
|
49
|
-
padding: 0.25rem;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
47
|
input[type="search"],
|
|
53
48
|
input[type="text"] {
|
|
54
49
|
margin: 0.25rem 0;
|
|
@@ -71,18 +66,26 @@ input[type="text"] {
|
|
|
71
66
|
|
|
72
67
|
input[type="search"],
|
|
73
68
|
input[type="text"] {
|
|
74
|
-
color:
|
|
75
|
-
background-color:
|
|
76
|
-
var(--hue-primary),
|
|
77
|
-
var(--saturation-bg),
|
|
78
|
-
var(--lightness-bg2)
|
|
79
|
-
);
|
|
69
|
+
color: var(--color-text);
|
|
70
|
+
background-color: var(--color-surface);
|
|
80
71
|
border: none;
|
|
81
|
-
box-shadow: inset
|
|
72
|
+
box-shadow: inset 0 1px 3px var(--color-shadow);
|
|
73
|
+
transition: filter 0.15s;
|
|
74
|
+
|
|
75
|
+
&:not(:read-only):hover {
|
|
76
|
+
filter: brightness(1.05);
|
|
77
|
+
}
|
|
82
78
|
}
|
|
83
79
|
|
|
84
80
|
hr {
|
|
81
|
+
width: 100%;
|
|
85
82
|
margin: 1.5rem 0;
|
|
83
|
+
border: none;
|
|
84
|
+
border-top: 1px solid var(--color-track);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
::selection {
|
|
88
|
+
background-color: hsl(var(--hue-primary), 90%, 50%, 0.3);
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
code {
|
|
@@ -94,7 +97,7 @@ button {
|
|
|
94
97
|
cursor: pointer;
|
|
95
98
|
background-color: transparent;
|
|
96
99
|
border: none;
|
|
97
|
-
border-radius: 0.
|
|
100
|
+
border-radius: 0.5rem;
|
|
98
101
|
padding: 0.5rem;
|
|
99
102
|
min-width: 1rem;
|
|
100
103
|
|
|
@@ -103,19 +106,36 @@ button {
|
|
|
103
106
|
justify-content: center;
|
|
104
107
|
|
|
105
108
|
font-size: large;
|
|
109
|
+
transition: 0.15s;
|
|
106
110
|
|
|
107
|
-
&:hover
|
|
108
|
-
&:focus {
|
|
111
|
+
&:hover {
|
|
109
112
|
background-color: var(--color-primary);
|
|
110
113
|
color: white;
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
/* Keyboard focus only: an offset ring, no fill, so a focused button and
|
|
117
|
+
a hovered neighbor stay clearly two separate things. */
|
|
118
|
+
&:focus-visible {
|
|
114
119
|
outline: 2px solid var(--color-primary);
|
|
120
|
+
outline-offset: 2px;
|
|
115
121
|
opacity: 1 !important;
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
.badge {
|
|
126
|
+
font-size: 0.75rem;
|
|
127
|
+
font-weight: bold;
|
|
128
|
+
padding: 0.125rem 0.5rem;
|
|
129
|
+
border-radius: 1rem;
|
|
130
|
+
background-color: var(--color-surface);
|
|
131
|
+
box-shadow: 0 1px 2px var(--color-shadow);
|
|
132
|
+
|
|
133
|
+
&.type {
|
|
134
|
+
background-color: var(--color-primary);
|
|
135
|
+
color: white;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
119
139
|
ul {
|
|
120
140
|
margin: 0;
|
|
121
141
|
padding: 0;
|
|
@@ -135,11 +155,60 @@ dialog {
|
|
|
135
155
|
border: none;
|
|
136
156
|
outline: none;
|
|
137
157
|
|
|
158
|
+
/* Zoom/fade the dialog in and out. Browsers without @starting-style or
|
|
159
|
+
allow-discrete simply show and hide it instantly. */
|
|
160
|
+
opacity: 0;
|
|
161
|
+
transform: scale(0.95);
|
|
162
|
+
transition: opacity 0.2s ease-out, transform 0.2s ease-out,
|
|
163
|
+
display 0.2s allow-discrete, overlay 0.2s allow-discrete;
|
|
164
|
+
|
|
165
|
+
&[open] {
|
|
166
|
+
opacity: 1;
|
|
167
|
+
transform: scale(1);
|
|
168
|
+
|
|
169
|
+
@starting-style {
|
|
170
|
+
opacity: 0;
|
|
171
|
+
transform: scale(0.95);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
138
175
|
&::backdrop {
|
|
176
|
+
background-color: rgba(0, 0, 0, 0);
|
|
177
|
+
transition: background-color 0.2s ease-out,
|
|
178
|
+
display 0.2s allow-discrete, overlay 0.2s allow-discrete;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
&[open]::backdrop {
|
|
139
182
|
background-color: rgba(0, 0, 0, 0.5);
|
|
183
|
+
|
|
184
|
+
@starting-style {
|
|
185
|
+
background-color: rgba(0, 0, 0, 0);
|
|
186
|
+
}
|
|
140
187
|
}
|
|
141
188
|
|
|
142
|
-
> .
|
|
143
|
-
|
|
189
|
+
> .details {
|
|
190
|
+
background-color: var(--color-page-bg);
|
|
191
|
+
border-radius: 0.75rem;
|
|
192
|
+
padding: 1.5rem;
|
|
193
|
+
max-width: 100%;
|
|
194
|
+
|
|
195
|
+
header {
|
|
196
|
+
margin-bottom: 1rem;
|
|
197
|
+
|
|
198
|
+
h2 {
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 0.5rem;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.badges {
|
|
205
|
+
margin-top: 0.5rem;
|
|
206
|
+
gap: 0.5rem;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
main {
|
|
211
|
+
gap: 0.5rem;
|
|
212
|
+
}
|
|
144
213
|
}
|
|
145
214
|
}
|
|
@@ -26,14 +26,13 @@ body {
|
|
|
26
26
|
display: flex;
|
|
27
27
|
flex-direction: column;
|
|
28
28
|
align-items: center;
|
|
29
|
-
justify-content: center;
|
|
30
29
|
gap: 3rem;
|
|
31
30
|
|
|
32
31
|
padding: 0 1rem;
|
|
33
32
|
flex-grow: 1;
|
|
34
33
|
|
|
35
34
|
&.with-sticky-search {
|
|
36
|
-
margin-top:
|
|
35
|
+
margin-top: 2rem;
|
|
37
36
|
|
|
38
37
|
.sticky-search {
|
|
39
38
|
align-self: center;
|
|
@@ -60,6 +59,50 @@ body {
|
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
|
|
62
|
+
.flags {
|
|
63
|
+
width: min(100%, 80ch);
|
|
64
|
+
|
|
65
|
+
section {
|
|
66
|
+
margin-top: 3rem;
|
|
67
|
+
|
|
68
|
+
h3 {
|
|
69
|
+
font-size: 0.875rem;
|
|
70
|
+
font-weight: bold;
|
|
71
|
+
text-transform: uppercase;
|
|
72
|
+
letter-spacing: 0.08em;
|
|
73
|
+
color: var(--color-text-muted);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ul {
|
|
77
|
+
margin-top: 1rem;
|
|
78
|
+
|
|
79
|
+
> p {
|
|
80
|
+
color: var(--color-text-muted);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
li.shine {
|
|
86
|
+
animation: shine-fade 1.2s ease-out;
|
|
87
|
+
border-radius: 2px;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@keyframes shine-fade {
|
|
92
|
+
from {
|
|
93
|
+
background-color: hsl(var(--hue-primary), 10%, 50%, 0.25);
|
|
94
|
+
}
|
|
95
|
+
to {
|
|
96
|
+
background-color: transparent;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.metadata {
|
|
101
|
+
display: grid;
|
|
102
|
+
grid-template-columns: auto 1fr;
|
|
103
|
+
gap: 0.5rem;
|
|
104
|
+
}
|
|
105
|
+
|
|
63
106
|
li.feature-toggle {
|
|
64
107
|
display: flex;
|
|
65
108
|
flex-direction: column;
|
|
@@ -69,29 +112,15 @@ li.feature-toggle {
|
|
|
69
112
|
.row {
|
|
70
113
|
align-items: center;
|
|
71
114
|
flex-grow: 1;
|
|
115
|
+
gap: 0.25rem;
|
|
72
116
|
|
|
73
117
|
button {
|
|
74
118
|
opacity: 0;
|
|
75
|
-
transition: 0.2s;
|
|
76
119
|
}
|
|
77
120
|
}
|
|
78
121
|
|
|
79
|
-
> .col {
|
|
80
|
-
padding: 0.5rem;
|
|
81
|
-
}
|
|
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
|
-
|
|
93
122
|
&:hover {
|
|
94
|
-
background-color:
|
|
123
|
+
background-color: hsl(var(--hue-primary), 10%, 50%, 0.07);
|
|
95
124
|
|
|
96
125
|
button {
|
|
97
126
|
opacity: 1;
|
|
@@ -1,28 +1,59 @@
|
|
|
1
1
|
html {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
|
|
2
4
|
--hue-primary: 35;
|
|
3
5
|
--saturation-bg: 5%;
|
|
4
|
-
--lightness-bg0:
|
|
5
|
-
--lightness-
|
|
6
|
-
--lightness-
|
|
6
|
+
--lightness-bg0: 92%; /* page */
|
|
7
|
+
--lightness-bg2: 97%; /* raised surfaces: inputs, badges */
|
|
8
|
+
--lightness-track: 84%; /* toggle tracks, slightly sunken */
|
|
9
|
+
--lightness-thumb: 99%; /* toggle thumbs (off) */
|
|
10
|
+
--lightness-disabled: 88%; /* disabled toggles, washed out into the page */
|
|
7
11
|
--lightness-fg: 10%;
|
|
8
|
-
--lightness-
|
|
12
|
+
--lightness-muted: 42%;
|
|
13
|
+
--lightness-shadow: 65%;
|
|
9
14
|
|
|
10
15
|
--color-primary: hsl(var(--hue-primary), 90%, 50%);
|
|
11
16
|
--color-text: hsl(var(--hue-primary), 0%, var(--lightness-fg));
|
|
17
|
+
--color-text-muted: hsl(var(--hue-primary), 5%, var(--lightness-muted));
|
|
12
18
|
--color-shadow: hsl(0, var(--saturation-bg), var(--lightness-shadow));
|
|
13
19
|
--color-page-bg: hsl(
|
|
14
20
|
var(--hue-primary),
|
|
15
21
|
var(--saturation-bg),
|
|
16
22
|
var(--lightness-bg0)
|
|
17
23
|
);
|
|
24
|
+
--color-surface: hsl(
|
|
25
|
+
var(--hue-primary),
|
|
26
|
+
var(--saturation-bg),
|
|
27
|
+
var(--lightness-bg2)
|
|
28
|
+
);
|
|
29
|
+
--color-track: hsl(
|
|
30
|
+
var(--hue-primary),
|
|
31
|
+
var(--saturation-bg),
|
|
32
|
+
var(--lightness-track)
|
|
33
|
+
);
|
|
34
|
+
--color-thumb: hsl(
|
|
35
|
+
var(--hue-primary),
|
|
36
|
+
var(--saturation-bg),
|
|
37
|
+
var(--lightness-thumb)
|
|
38
|
+
);
|
|
39
|
+
--color-disabled: hsl(
|
|
40
|
+
var(--hue-primary),
|
|
41
|
+
var(--saturation-bg),
|
|
42
|
+
var(--lightness-disabled)
|
|
43
|
+
);
|
|
18
44
|
}
|
|
19
45
|
|
|
20
46
|
@media (prefers-color-scheme: dark) {
|
|
21
47
|
html:not(.light-mode) {
|
|
48
|
+
color-scheme: dark;
|
|
49
|
+
|
|
22
50
|
--lightness-bg0: 8%;
|
|
23
|
-
--lightness-
|
|
24
|
-
--lightness-
|
|
51
|
+
--lightness-bg2: 16%;
|
|
52
|
+
--lightness-track: 22%;
|
|
53
|
+
--lightness-thumb: 70%;
|
|
54
|
+
--lightness-disabled: 12%;
|
|
25
55
|
--lightness-fg: 90%;
|
|
26
|
-
--lightness-
|
|
56
|
+
--lightness-muted: 60%;
|
|
57
|
+
--lightness-shadow: 3%;
|
|
27
58
|
}
|
|
28
59
|
}
|
|
@@ -5,16 +5,13 @@ module Toogle
|
|
|
5
5
|
# content security policy.
|
|
6
6
|
content_security_policy false, if: -> { Rails.env.development? }
|
|
7
7
|
|
|
8
|
-
#
|
|
9
|
-
# Also, it really doesn't matter on localhost :)
|
|
8
|
+
# Never cache: a stale page could show the wrong toggle state.
|
|
10
9
|
before_action :set_cache_headers
|
|
11
10
|
|
|
12
11
|
private
|
|
13
12
|
|
|
14
13
|
def set_cache_headers
|
|
15
|
-
response.headers["Cache-Control"] = "no-
|
|
16
|
-
response.headers["Pragma"] = "no-cache"
|
|
17
|
-
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
|
|
14
|
+
response.headers["Cache-Control"] = "no-store"
|
|
18
15
|
end
|
|
19
16
|
end
|
|
20
17
|
end
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
module Toogle
|
|
2
2
|
class DefinitionsController < ApplicationController
|
|
3
|
-
# This controller is only used to fetch the long list of feature definitions async.
|
|
4
|
-
layout false
|
|
5
|
-
|
|
6
3
|
def index
|
|
7
|
-
|
|
4
|
+
render json: Toogle::Definition.all
|
|
8
5
|
end
|
|
9
6
|
end
|
|
10
7
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Toogle
|
|
2
2
|
class FeaturesController < ApplicationController
|
|
3
3
|
def index
|
|
4
|
-
|
|
4
|
+
load_features
|
|
5
5
|
|
|
6
6
|
respond_to do |format|
|
|
7
7
|
format.html
|
|
@@ -12,10 +12,14 @@ module Toogle
|
|
|
12
12
|
def show
|
|
13
13
|
@definition = Toogle::Definition.find(params[:id])
|
|
14
14
|
if @definition.nil?
|
|
15
|
-
flash[:notice] = "No feature
|
|
15
|
+
flash.now[:notice] = "No feature definition with name \"#{params[:id]}\" found."
|
|
16
|
+
end
|
|
17
|
+
load_features
|
|
18
|
+
|
|
19
|
+
respond_to do |format|
|
|
20
|
+
format.html { render "index" }
|
|
21
|
+
format.json { render json: @features }
|
|
16
22
|
end
|
|
17
|
-
index
|
|
18
|
-
render "index"
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
def update
|
|
@@ -51,5 +55,11 @@ module Toogle
|
|
|
51
55
|
end
|
|
52
56
|
end
|
|
53
57
|
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def load_features
|
|
62
|
+
@features = Toogle::Feature.all
|
|
63
|
+
end
|
|
54
64
|
end
|
|
55
65
|
end
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Toogle
|
|
4
4
|
class Definition
|
|
5
|
-
|
|
5
|
+
attr_reader :name, :default_enabled, :milestone, :type,
|
|
6
|
+
:feature_issue_url, :introduced_by_url, :rollout_issue_url
|
|
6
7
|
|
|
7
8
|
def self.all
|
|
8
9
|
::Feature::Definition.definitions.map do |definition|
|
|
@@ -10,25 +11,25 @@ module Toogle
|
|
|
10
11
|
name: definition[0].to_s,
|
|
11
12
|
default_enabled: definition[1].default_enabled,
|
|
12
13
|
milestone: definition[1].milestone,
|
|
14
|
+
type: definition[1].type,
|
|
15
|
+
feature_issue_url: definition[1].feature_issue_url,
|
|
13
16
|
introduced_by_url: definition[1].introduced_by_url,
|
|
14
|
-
rollout_issue_url: definition[1].rollout_issue_url
|
|
17
|
+
rollout_issue_url: definition[1].rollout_issue_url
|
|
15
18
|
)
|
|
16
19
|
end
|
|
17
20
|
end
|
|
18
21
|
|
|
19
|
-
def self.unchanged
|
|
20
|
-
changed_features = Toogle::Feature.all.map(&:name)
|
|
21
|
-
all.reject { |definition| changed_features.include?(definition.name) }
|
|
22
|
-
end
|
|
23
|
-
|
|
24
22
|
def self.find(name)
|
|
25
23
|
all.find { |definition| definition.name == name }
|
|
26
24
|
end
|
|
27
25
|
|
|
28
|
-
def initialize(name:, default_enabled:, milestone: nil,
|
|
26
|
+
def initialize(name:, default_enabled:, milestone: nil, type: nil,
|
|
27
|
+
feature_issue_url: nil, introduced_by_url: nil, rollout_issue_url: nil)
|
|
29
28
|
@name = name
|
|
30
29
|
@default_enabled = default_enabled
|
|
31
30
|
@milestone = milestone
|
|
31
|
+
@type = type
|
|
32
|
+
@feature_issue_url = feature_issue_url
|
|
32
33
|
@introduced_by_url = introduced_by_url
|
|
33
34
|
@rollout_issue_url = rollout_issue_url
|
|
34
35
|
end
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Toogle
|
|
4
4
|
class Feature
|
|
5
|
-
|
|
5
|
+
attr_reader :name, :state, :definition
|
|
6
6
|
|
|
7
7
|
def self.all
|
|
8
|
-
|
|
8
|
+
definitions_by_name = Definition.all.index_by(&:name)
|
|
9
9
|
|
|
10
10
|
Flipper::Adapters::ActiveRecord.new(
|
|
11
11
|
feature_class: ::Feature::FlipperFeature,
|
|
12
12
|
gate_class: ::Feature::FlipperGate
|
|
13
13
|
).get_all.map do |feature_name, feature_values|
|
|
14
|
-
feature_definition =
|
|
14
|
+
feature_definition = definitions_by_name[feature_name]
|
|
15
15
|
feature_state = if feature_definition
|
|
16
|
-
feature_values[:boolean] ? :enabled: :disabled
|
|
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.
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
%head
|
|
4
4
|
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
|
|
5
5
|
%meta(name="viewport" content="width=device-width, initial-scale=1.0")
|
|
6
|
+
-# GitLab's masked_referrer_url helper (Snowplow) crashes on referrers
|
|
7
|
+
-# pointing into this engine, because it cannot regenerate engine routes
|
|
8
|
+
-# with the main app's URL helpers. So never send a referrer.
|
|
9
|
+
%meta(name="referrer" content="no-referrer")
|
|
6
10
|
%title Feature flags
|
|
7
11
|
= csrf_meta_tags
|
|
8
12
|
= csp_meta_tag
|
|
9
13
|
= stylesheet_link_tag "toogle/application", media: "all"
|
|
10
14
|
= javascript_include_tag 'toogle/application'
|
|
11
|
-
%script(defer src="https://cdn.jsdelivr.net/npm/@alpinejs/anchor@3.x.x/dist/cdn.min.js")
|
|
12
15
|
%script(defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js")
|
|
13
16
|
|
|
14
17
|
%body
|
|
@@ -34,14 +37,22 @@
|
|
|
34
37
|
%path(d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z")
|
|
35
38
|
|
|
36
39
|
%main.with-sticky-search
|
|
37
|
-
= yield
|
|
38
40
|
- if notice.present?
|
|
39
|
-
|
|
41
|
+
.notice.d-flex.row.center.gap
|
|
40
42
|
%svg(width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
|
|
41
43
|
%circle(cx="12" cy="12" r="10")
|
|
42
44
|
%line(x1="12" y1="16" x2="12" y2="12")
|
|
43
45
|
%line(x1="12" y1="8" x2="12.01" y2="8")
|
|
44
46
|
%span.grow= notice
|
|
47
|
+
-# Client-side errors, e.g. fetch failures when the server is gone.
|
|
48
|
+
.notice.d-flex.row.center.gap(x-data x-show="$store.flash.message" x-cloak)
|
|
49
|
+
%svg(width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
|
|
50
|
+
%circle(cx="12" cy="12" r="10")
|
|
51
|
+
%line(x1="12" y1="16" x2="12" y2="12")
|
|
52
|
+
%line(x1="12" y1="8" x2="12.01" y2="8")
|
|
53
|
+
%span.grow(x-text="$store.flash.message")
|
|
54
|
+
|
|
55
|
+
= yield
|
|
45
56
|
|
|
46
57
|
%footer
|
|
47
58
|
%a(href="https://gitlab.com/thutterer/toogle" target="_blank")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-# The ⋯ button opening the details dialog. `subject` is the Alpine
|
|
2
|
+
-# expression for the flag's name, e.g. "feature.name".
|
|
3
|
+
%button(title="Show details" x-on:click="openDetails(#{subject})")
|
|
4
|
+
%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")
|
|
5
|
+
%circle(cx="12" cy="12" r="1")
|
|
6
|
+
%circle(cx="12" cy="5" r="1")
|
|
7
|
+
%circle(cx="12" cy="19" r="1")
|
|
@@ -1,76 +1,103 @@
|
|
|
1
|
-
.
|
|
2
|
-
.
|
|
3
|
-
"x-data": "{query: '', show: false}",
|
|
4
|
-
"@click.outside": "show = false",
|
|
5
|
-
"@keyup.esc": "show = false"}
|
|
1
|
+
.flags(x-data="flags('#{features_url}', '#{definitions_path}', '#{@definition&.name}')")
|
|
2
|
+
.sticky-search
|
|
6
3
|
%input.large.w-100#search{
|
|
7
4
|
"type": "search",
|
|
8
|
-
"placeholder": "Search
|
|
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")
|
|
14
|
-
.d-flex.col.stretch{
|
|
15
|
-
"x-anchor.bottom-start.offset.5" => "$refs.search",
|
|
16
|
-
"x-transition": nil}
|
|
17
|
-
.grow{
|
|
18
|
-
"data-url": definitions_path,
|
|
19
|
-
"x-data": "{}",
|
|
20
|
-
"x-init": "$el.innerHTML = await (await fetch($el.dataset.url)).text()"}
|
|
5
|
+
"placeholder": "Search feature flags",
|
|
6
|
+
"x-model.debounce.400ms": "query"}
|
|
21
7
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
%small These features already use custom settings in this GDK.
|
|
8
|
+
%section
|
|
9
|
+
%h3 Customized in this GDK
|
|
25
10
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
11
|
+
%ul.pinned
|
|
12
|
+
%template(x-for="feature in filteredFeatures()" :key="feature.name")
|
|
13
|
+
%li.feature-toggle{":style": "feature.name === moving ? `view-transition-name: flag-${feature.name}` : ''", ":class": "{shine: feature.name === lastMoved}"}
|
|
14
|
+
.d-flex.row.nowrap
|
|
15
|
+
%template(x-if="feature.state === 'unknown'")
|
|
16
|
+
%label.toggle(title="This feature does not exist on the current branch.")
|
|
17
|
+
%input(type="checkbox" disabled :name="feature.name")
|
|
18
|
+
%span.handle.round
|
|
19
|
+
%template(x-if="feature.state !== 'unknown'")
|
|
20
|
+
%label.toggle
|
|
21
|
+
%input(type="checkbox" x-on:change="toggleFeature(feature)" :checked="feature.state === 'enabled'" :name="feature.name")
|
|
22
|
+
%span.handle.round
|
|
23
|
+
|
|
24
|
+
%code.grow.ellipsis(x-text="feature.name")
|
|
25
|
+
|
|
26
|
+
= render "details_button", subject: "feature.name"
|
|
27
|
+
|
|
28
|
+
%button(title="Forget this setting and use default" x-on:click="deleteFeature(feature.name)")
|
|
29
|
+
%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")
|
|
30
|
+
%line(x1="18" y1="6" x2="6" y2="18")
|
|
31
|
+
%line(x1="6" y1="6" x2="18" y2="18")
|
|
32
|
+
%p(x-show="!features.length") Nothing custom yet. Toggle any flag below.
|
|
33
|
+
%p(x-show="features.length && !filteredFeatures().length") No matches.
|
|
34
|
+
|
|
35
|
+
%section
|
|
36
|
+
%h3 Defaults from the code
|
|
37
|
+
|
|
38
|
+
%ul.defaults
|
|
39
|
+
%template(x-for="definition in filteredDefinitions()" :key="definition.name")
|
|
40
|
+
%li.feature-toggle{":style": "definition.name === moving ? `view-transition-name: flag-${definition.name}` : ''", ":class": "{shine: definition.name === lastMoved}"}
|
|
41
|
+
.d-flex.row.nowrap
|
|
35
42
|
%label.toggle
|
|
36
|
-
%input(type="checkbox" x-on:change="
|
|
37
|
-
%span.handle.round
|
|
43
|
+
%input(type="checkbox" x-on:change="toggleDefault(definition)" :checked="definition.default_enabled" :name="definition.name")
|
|
44
|
+
%span.handle.round(:title="definition.default_enabled ? 'Enabled by default. Click to disable.' : 'Disabled by default. Click to enable.'")
|
|
45
|
+
|
|
46
|
+
%code.grow.ellipsis(x-text="definition.name")
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
= render "details_button", subject: "definition.name"
|
|
49
|
+
%p(x-show="definitions.length && !matchingDefinitions().length")
|
|
50
|
+
No matches. Are you on the right branch?
|
|
51
|
+
%p(x-show="defaultsCapped()" x-text="`Showing first ${max} of ${matchingDefinitions().length}. Type to narrow down.`")
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
%dialog{"x-ref": "dialog", "@close": "closeDetails()", "@click.self": "$refs.dialog.close()"}
|
|
54
|
+
%template(x-if="selected()")
|
|
55
|
+
.details
|
|
56
|
+
%header
|
|
57
|
+
%h2
|
|
58
|
+
%template(x-if="selected().state === 'unknown'")
|
|
59
|
+
%label.toggle(title="This feature does not exist on the current branch.")
|
|
60
|
+
%input(type="checkbox" disabled)
|
|
61
|
+
%span.handle.round
|
|
62
|
+
%template(x-if="selected().state !== 'unknown'")
|
|
63
|
+
%label.toggle
|
|
64
|
+
%input(type="checkbox" x-on:change="toggleSelected()" :checked="selected().state === 'enabled'")
|
|
65
|
+
%span.handle.round
|
|
46
66
|
|
|
47
|
-
|
|
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")
|
|
67
|
+
%code(x-text="selectedName")
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
%span
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
.d-flex.row.badges(x-show="selected().definition")
|
|
70
|
+
%span.badge.type{"x-show": "selected().definition?.type",
|
|
71
|
+
"x-text": "selected().definition?.type"}
|
|
72
|
+
%span.badge{"x-show": "selected().definition?.milestone",
|
|
73
|
+
"x-text": "`since ${selected().definition?.milestone}`"}
|
|
74
|
+
|
|
75
|
+
%main.d-flex.col
|
|
76
|
+
%template(x-if="selected().definition")
|
|
77
|
+
.metadata
|
|
78
|
+
%strong Feature issue
|
|
79
|
+
%a{"x-show": "selected().definition.feature_issue_url",
|
|
80
|
+
"x-text": "`#${selected().definition.feature_issue_url?.split('/').at(-1)}`",
|
|
81
|
+
":href": "selected().definition.feature_issue_url",
|
|
82
|
+
"target": "_blank"}
|
|
83
|
+
%span(x-show="!selected().definition.feature_issue_url") -
|
|
84
|
+
%strong Introduced by
|
|
85
|
+
%a{"x-show": "selected().definition.introduced_by_url",
|
|
86
|
+
"x-text": "`!${selected().definition.introduced_by_url?.split('/').at(-1)}`",
|
|
87
|
+
":href": "selected().definition.introduced_by_url",
|
|
88
|
+
"target": "_blank"}
|
|
89
|
+
%span(x-show="!selected().definition.introduced_by_url") -
|
|
90
|
+
%strong Rollout issue
|
|
91
|
+
%a{"x-show": "selected().definition.rollout_issue_url",
|
|
92
|
+
"x-text": "`#${selected().definition.rollout_issue_url?.split('/').at(-1)}`",
|
|
93
|
+
":href": "selected().definition.rollout_issue_url",
|
|
94
|
+
"target": "_blank"}
|
|
95
|
+
%span(x-show="!selected().definition.rollout_issue_url") -
|
|
96
|
+
%template(x-if="!selected().definition")
|
|
97
|
+
%p This feature does not exist on the current branch.
|
|
68
98
|
|
|
69
99
|
%hr
|
|
70
100
|
%small Share this URL with MR reviewers.
|
|
71
101
|
.d-flex.row
|
|
72
|
-
%input.grow
|
|
73
|
-
%button(x-on:click="
|
|
74
|
-
%p(x-show="!features.length") No active feature flags in this GDK yet.
|
|
75
|
-
|
|
76
|
-
= render "dialog"
|
|
102
|
+
%input.grow{":value": "`${featuresUrl}/${selectedName}`", "type": "text", "readonly": "readonly"}
|
|
103
|
+
%button(x-on:click="navigator.clipboard.writeText(`${featuresUrl}/${selectedName}`)") Copy
|
data/lib/toogle/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gdk-toogle
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thomas Hutterer
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -94,8 +93,6 @@ files:
|
|
|
94
93
|
- app/assets/config/toogle_manifest.js
|
|
95
94
|
- app/assets/javascript/toogle/application.js
|
|
96
95
|
- app/assets/stylesheets/toogle/application.css
|
|
97
|
-
- app/assets/stylesheets/toogle/components/card.css
|
|
98
|
-
- app/assets/stylesheets/toogle/components/scrollbox.css
|
|
99
96
|
- app/assets/stylesheets/toogle/components/toggle.css
|
|
100
97
|
- app/assets/stylesheets/toogle/elements.css
|
|
101
98
|
- app/assets/stylesheets/toogle/layout.css
|
|
@@ -107,9 +104,7 @@ files:
|
|
|
107
104
|
- app/models/toogle/definition.rb
|
|
108
105
|
- app/models/toogle/feature.rb
|
|
109
106
|
- app/views/layouts/toogle/application.html.haml
|
|
110
|
-
- app/views/toogle/
|
|
111
|
-
- app/views/toogle/features/_dialog.html.haml
|
|
112
|
-
- app/views/toogle/features/_toggle.html.haml
|
|
107
|
+
- app/views/toogle/features/_details_button.html.haml
|
|
113
108
|
- app/views/toogle/features/index.html.haml
|
|
114
109
|
- config/routes.rb
|
|
115
110
|
- lib/toogle.rb
|
|
@@ -123,7 +118,6 @@ metadata:
|
|
|
123
118
|
homepage_uri: https://gitlab.com/thutterer/toogle
|
|
124
119
|
source_code_uri: https://gitlab.com/thutterer/toogle
|
|
125
120
|
changelog_uri: https://gitlab.com/thutterer/toogle
|
|
126
|
-
post_install_message:
|
|
127
121
|
rdoc_options: []
|
|
128
122
|
require_paths:
|
|
129
123
|
- lib
|
|
@@ -138,8 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
138
132
|
- !ruby/object:Gem::Version
|
|
139
133
|
version: '0'
|
|
140
134
|
requirements: []
|
|
141
|
-
rubygems_version:
|
|
142
|
-
signing_key:
|
|
135
|
+
rubygems_version: 4.0.12
|
|
143
136
|
specification_version: 4
|
|
144
137
|
summary: Toogle toggles feature flags
|
|
145
138
|
test_files: []
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
.scrollbox {
|
|
2
|
-
max-height: 20ch;
|
|
3
|
-
overflow: auto;
|
|
4
|
-
padding: 1rem 0;
|
|
5
|
-
word-break: break-all;
|
|
6
|
-
|
|
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));
|
|
14
|
-
|
|
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
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
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}, '#{features_url}')")
|
|
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
|
|
@@ -1,18 +0,0 @@
|
|
|
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, '#{features_url}')")
|
|
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.
|
|
@@ -1,8 +0,0 @@
|
|
|
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.'")
|