fuik 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +156 -25
- data/app/assets/stylesheets/fuik/application.css +152 -88
- data/app/controllers/concerns/fuik/event_type.rb +32 -7
- data/app/controllers/fuik/events_controller.rb +6 -1
- data/app/controllers/fuik/retries_controller.rb +12 -0
- data/app/controllers/fuik/webhooks_controller.rb +19 -15
- data/app/jobs/fuik/webhook_processing_job.rb +14 -0
- data/app/models/fuik/event.rb +36 -0
- data/app/models/fuik/webhook_event/filterable.rb +3 -4
- data/app/models/fuik/webhook_event.rb +32 -1
- data/app/views/fuik/events/index.html.erb +5 -1
- data/app/views/fuik/events/show.html.erb +43 -20
- data/app/views/layouts/fuik/application.html.erb +11 -1
- data/config/routes.rb +4 -1
- data/lib/fuik/configuration.rb +29 -0
- data/lib/fuik/engine.rb +0 -4
- data/lib/fuik/errors.rb +5 -0
- data/lib/fuik/notifications.rb +48 -0
- data/lib/fuik/routing/provider_constraint.rb +4 -4
- data/lib/fuik/test_helpers.rb +33 -0
- data/lib/fuik/version.rb +1 -1
- data/lib/fuik.rb +9 -0
- data/lib/generators/fuik/provider/templates/base.rb.tt +11 -2
- data/lib/generators/fuik/provider/templates/github/base.rb.tt +3 -0
- data/lib/generators/fuik/provider/templates/loops/base.rb.tt +2 -0
- data/lib/generators/fuik/provider/templates/postmark/base.rb.tt +1 -0
- data/lib/generators/fuik/provider/templates/spinal/base.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/spinal/collection_synced.rb.tt +14 -0
- data/lib/generators/fuik/provider/templates/spinal/resource_created.rb.tt +14 -0
- data/lib/generators/fuik/provider/templates/spinal/resource_pushed.rb.tt +13 -0
- data/lib/generators/fuik/provider/templates/spinal/resource_scheduled.rb.tt +13 -0
- data/lib/generators/fuik/provider/templates/spinal/resource_unscheduled.rb.tt +12 -0
- metadata +14 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7e63ae360a189d38134db206274a5733ad73ea37ae2b338686c952df7ac3c109
|
|
4
|
+
data.tar.gz: c59410136f9c4143690b96b496c3e83fdf0419d806cc308a5ab4e73c1c1b21fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 07d53ebe3d570ec39d8e2b669492433972789437100dd811b795e5abad902e5d921fc93cf6714c3fef5f5a484cbcdedab04280becea9ec02fb2e65e762517beb
|
|
7
|
+
data.tar.gz: 604fb6bc76e23853ca980b1f25861eab8ee45bfd2f0d66182d805db8b133fe411d35e485b44042937114236a040e5deb9563f248a703f245cedcc86d58b40cdc
|
data/README.md
CHANGED
|
@@ -57,14 +57,17 @@ Visit `/webhooks` to see all received webhooks. Click any event to view all the
|
|
|
57
57
|
|
|
58
58
|
<img alt="Fuik event detail interface" src="https://raw.githubusercontent.com/Rails-Designer/fuik/HEAD/.github/docs/event-detail.jpg" style="max-width: 100%;">
|
|
59
59
|
|
|
60
|
-
⚠️ The `/webhooks` path is by default not protected.
|
|
60
|
+
⚠️ The `/webhooks` path is by default not protected. Set `Fuik.configuration.events_controller_parent` to a controller that requires authentication.
|
|
61
|
+
|
|
61
62
|
|
|
62
63
|
### Dashboard features
|
|
63
64
|
|
|
64
65
|
* **Copy payload as JSON**: click a button, payload is in your clipboard
|
|
65
66
|
* **Download payload as JSON file**: keep it for testing, debugging or throw it at your LLM agent, bot or colleague
|
|
66
67
|
* **Add `.json` to any URL**: get the raw payload without the UI
|
|
67
|
-
*
|
|
68
|
+
* **`GET /events.json`**: index as JSON with filter support
|
|
69
|
+
* **Click any key to get the Ruby accessor path**: click any key and get, e.g. `payload["line_items"][0]["product_id"]` (say what? 🤯)
|
|
70
|
+
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
### Add business logic
|
|
@@ -130,64 +133,192 @@ end
|
|
|
130
133
|
If `Provider::Base.verify!` exists, Fuik calls it automatically. Invalid signatures return 401 without storing the webhook.
|
|
131
134
|
|
|
132
135
|
|
|
136
|
+
### Configuration
|
|
137
|
+
|
|
138
|
+
Configure Fuik in `config/initializers/fuik.rb`:
|
|
139
|
+
```ruby
|
|
140
|
+
Fuik.configure do |config|
|
|
141
|
+
# config.events_controller_parent = "AdministrationController"
|
|
142
|
+
# config.providers_allowed = %w[stripe github postmark]
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
### Custom job class
|
|
148
|
+
|
|
149
|
+
Webhook events are processed asynchronously by `Fuik::WebhookProcessingJob`. When processing raises, the event is marked as `failed` so you can retry it from the dashboard.
|
|
150
|
+
|
|
151
|
+
Define a custom job class, then configure Fuik to use it:
|
|
152
|
+
```ruby
|
|
153
|
+
# app/jobs/webhook_processing_job.rb
|
|
154
|
+
class WebhookProcessingJob < ApplicationJob
|
|
155
|
+
queue_as :webhooks
|
|
156
|
+
|
|
157
|
+
retry_on MyApp::WebhookError, attempts: 5
|
|
158
|
+
discard_on ActiveJob::DeserializationError
|
|
159
|
+
|
|
160
|
+
def perform(event_class_name, webhook_event)
|
|
161
|
+
event_class_name.safe_constantize.new(webhook_event).process!
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
# config/initializers/fuik.rb
|
|
168
|
+
Fuik.configure do |config|
|
|
169
|
+
config.webhook_processing_job_class = "WebhookProcessingJob"
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The `perform` method receives two arguments:
|
|
174
|
+
- `event_class_name`: a string like `"Stripe::CheckoutSessionCompleted"` that resolves to your event class
|
|
175
|
+
- `webhook_event`: the `Fuik::WebhookEvent` record with the payload, headers and status
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
### Testing event classes
|
|
179
|
+
|
|
180
|
+
Unit test your `process!` methods without a database using the built-in test helper:
|
|
181
|
+
```ruby
|
|
182
|
+
# test/test_helper.rb
|
|
183
|
+
require "fuik/test_helpers"
|
|
184
|
+
|
|
185
|
+
class ActiveSupport::TestCase
|
|
186
|
+
include Fuik::TestHelpers
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
# test/webhooks/stripe/checkout_session_completed_test.rb
|
|
192
|
+
module Stripe
|
|
193
|
+
class CheckoutSessionCompletedTest < ActiveSupport::TestCase
|
|
194
|
+
test "processes a completed checkout" do
|
|
195
|
+
event = build_webhook_event(
|
|
196
|
+
provider: "stripe",
|
|
197
|
+
event_type: "checkout.session.completed",
|
|
198
|
+
payload: { "client_reference_id" => "user_123" }
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
CheckoutSessionCompleted.new(event).process!
|
|
202
|
+
|
|
203
|
+
assert_equal "processed", event.status
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
test "fails when customer is not found" do
|
|
207
|
+
event = build_webhook_event(
|
|
208
|
+
provider: "stripe",
|
|
209
|
+
event_type: "checkout.session.completed",
|
|
210
|
+
payload: {}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
CheckoutSessionCompleted.new(event).process!
|
|
214
|
+
|
|
215
|
+
assert_equal "failed", event.status
|
|
216
|
+
assert_equal "Customer not found", event.error
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Your `process!` calls `@webhook_event.processed!` on success and `@webhook_event.failed!(error)` on failure:
|
|
223
|
+
```ruby
|
|
224
|
+
module Stripe
|
|
225
|
+
class CheckoutSessionCompleted < Base
|
|
226
|
+
def process!
|
|
227
|
+
user = User.find_by(id: payload.client_reference_id)
|
|
228
|
+
|
|
229
|
+
if user.present?
|
|
230
|
+
user.activate_subscription!
|
|
231
|
+
|
|
232
|
+
@webhook_event.processed!
|
|
233
|
+
else
|
|
234
|
+
@webhook_event.failed!("Customer not found")
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
|
|
133
242
|
### Provider allowlist
|
|
134
243
|
|
|
135
244
|
By default:
|
|
136
245
|
- **Development/test**: all providers are allowed
|
|
137
246
|
- **Production/staging**: only providers in `app/webhooks/` are allowed
|
|
138
247
|
|
|
139
|
-
Configure with `Fuik
|
|
248
|
+
Configure with `Fuik.configuration.providers_allowed`:
|
|
140
249
|
```ruby
|
|
141
250
|
# Allow all (including production)
|
|
142
|
-
Fuik
|
|
251
|
+
Fuik.configuration.providers_allowed = :all
|
|
143
252
|
|
|
144
253
|
# Explicit allowlist (overrides directory scan)
|
|
145
|
-
Fuik
|
|
254
|
+
Fuik.configuration.providers_allowed = %w[stripe github postmark]
|
|
146
255
|
```
|
|
147
256
|
|
|
148
257
|
Unknown providers return `404 Not Found`.
|
|
149
258
|
|
|
150
259
|
|
|
151
|
-
### Pre-packaged providers
|
|
152
|
-
|
|
153
|
-
Fuik includes ready-to-use [templates for common providers](https://github.com/Rails-Designer/fuik/tree/main/lib/generators/fuik/provider/templates).
|
|
154
|
-
|
|
155
|
-
|
|
156
260
|
### Event type & ID lookup
|
|
157
261
|
|
|
158
262
|
Fuik automatically extracts event types and IDs from common locations:
|
|
159
263
|
|
|
160
264
|
**Event Type:**
|
|
161
|
-
1. provider
|
|
265
|
+
1. provider Base class (if configured);
|
|
162
266
|
2. common headers (`X-Github-Event`, `X-Event-Type`, etc.);
|
|
163
267
|
3. payload (`type`, `event`, `event_type`);
|
|
164
268
|
4. falls back to `"unknown"`.
|
|
165
269
|
|
|
166
270
|
**Event ID:**
|
|
167
|
-
1. provider
|
|
271
|
+
1. provider Base class (if configured);
|
|
168
272
|
2. common headers (`X-GitHub-Delivery`, `X-Event-Id`, etc.);
|
|
169
273
|
3. payload (`id`).
|
|
170
274
|
4. falls back to MD5 hash of request body.
|
|
171
275
|
|
|
172
276
|
|
|
173
|
-
#### Custom lookup
|
|
277
|
+
#### Custom lookup
|
|
278
|
+
|
|
279
|
+
Define `event_type` and/or `event_id` on your provider's Base class if Fuik can't find them on its own:
|
|
280
|
+
```ruby
|
|
281
|
+
module CustomProvider
|
|
282
|
+
class Base < Fuik::Event
|
|
283
|
+
event_type header: "X-Custom-Event"
|
|
284
|
+
event_id header: "X-Custom-Id"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
You can also read from the payload body or use a static value:
|
|
290
|
+
```ruby
|
|
291
|
+
event_type payload: "event_type" # payload["event_type"]
|
|
292
|
+
event_type payload: "data.type" # nested via dot-notation (payload["data"]["type"])
|
|
293
|
+
event_type "always_this_value" # static/literal
|
|
294
|
+
|
|
295
|
+
event_id payload: "id"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
### Pre-packaged providers
|
|
300
|
+
|
|
301
|
+
Fuik includes ready-to-use [templates for common providers](https://github.com/Rails-Designer/fuik/tree/main/lib/generators/fuik/provider/templates).
|
|
302
|
+
|
|
174
303
|
|
|
175
|
-
|
|
176
|
-
```yaml
|
|
177
|
-
event_type:
|
|
178
|
-
source: header
|
|
179
|
-
key: X-Custom-Event
|
|
304
|
+
## Monitoring
|
|
180
305
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
306
|
+
Fuik publishes lifecycle events via `ActiveSupport::Notifications`. Subscribe to track webhook activity:
|
|
307
|
+
```ruby
|
|
308
|
+
ActiveSupport::Notifications.subscribe("webhook_received.fuik") do |event|
|
|
309
|
+
Rails.logger.info("[Fuik] Received #{event.payload[:provider]}:#{event.payload[:event_type]}")
|
|
310
|
+
end
|
|
184
311
|
```
|
|
185
312
|
|
|
186
|
-
|
|
313
|
+
Available events:
|
|
187
314
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
315
|
+
| Event | When it fires |
|
|
316
|
+
|---|---|
|
|
317
|
+
| `webhook_received.fuik` | Event record created |
|
|
318
|
+
| `webhook_processed.fuik` | `process!` completed |
|
|
319
|
+
| `webhook_failed.fuik` | `process!` raised |
|
|
320
|
+
| `webhook_signature_invalid.fuik` | Signature verification failed (401) |
|
|
321
|
+
| `webhook_receive_error.fuik` | Unexpected error during receive (500) |
|
|
191
322
|
|
|
192
323
|
|
|
193
324
|
## Add your custom provider
|
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
@layer base {
|
|
4
4
|
:root {
|
|
5
|
-
--
|
|
6
|
-
--
|
|
7
|
-
--
|
|
8
|
-
--
|
|
5
|
+
--gray-50: oklch(98.7% .002 197.1);
|
|
6
|
+
--gray-100: oklch(96.3% .002 197.1);
|
|
7
|
+
--gray-200: oklch(92.5% .005 214.3);
|
|
8
|
+
--gray-300: oklch(87.2% .007 219.6);
|
|
9
|
+
--gray-400: oklch(72.3% .014 214.4);
|
|
10
|
+
--gray-500: oklch(56% .021 213.5);
|
|
11
|
+
--gray-600: oklch(45% .017 213.2);
|
|
12
|
+
--gray-700: oklch(37.8% .015 216);
|
|
13
|
+
--gray-800: oklch(27.5% .011 216.9);
|
|
14
|
+
--gray-900: oklch(21.8% .008 223.9);
|
|
15
|
+
--gray-950: oklch(14.8% .004 228.8);
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -13,7 +20,8 @@
|
|
|
13
20
|
body {
|
|
14
21
|
font-family: system-ui, -apple-system, sans-serif;
|
|
15
22
|
line-height: 1.5;
|
|
16
|
-
color: var(--
|
|
23
|
+
color: light-dark(var(--gray-800), var(--gray-200));
|
|
24
|
+
background-color: light-dark(white, var(--gray-950));
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
main {
|
|
@@ -22,19 +30,6 @@
|
|
|
22
30
|
margin-inline: auto;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
h1 {
|
|
26
|
-
margin-block-end: 1rem;
|
|
27
|
-
font-size: 1.875rem;
|
|
28
|
-
font-weight: 700;
|
|
29
|
-
letter-spacing: -.025em;
|
|
30
|
-
|
|
31
|
-
a {
|
|
32
|
-
color: var(--color-text-muted);
|
|
33
|
-
|
|
34
|
-
&:hover { color: var(--color-text); }
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
33
|
a {
|
|
39
34
|
color: inherit;
|
|
40
35
|
text-decoration: none;
|
|
@@ -47,58 +42,35 @@
|
|
|
47
42
|
padding-block: .5rem;
|
|
48
43
|
padding-inline: 1rem;
|
|
49
44
|
max-height: calc(20lh + 1rem);
|
|
45
|
+
width: 100%;
|
|
50
46
|
font-size: .875rem;
|
|
51
|
-
background: var(--
|
|
52
|
-
border
|
|
47
|
+
background: light-dark(var(--gray-50), var(--gray-800));
|
|
48
|
+
border: 1px solid light-dark(transparent, var(--gray-700));
|
|
49
|
+
border-radius: .375rem;
|
|
53
50
|
overflow-x: auto;
|
|
54
51
|
}
|
|
52
|
+
}
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
font-weight: 600;
|
|
64
|
-
color: var(--color-text);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
dd {
|
|
68
|
-
position: relative;
|
|
69
|
-
overflow-x: auto;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.actions {
|
|
73
|
-
display: flex;
|
|
74
|
-
align-items: center;
|
|
75
|
-
gap: .5rem;
|
|
76
|
-
position: absolute;
|
|
77
|
-
top: .5rem; right: .5rem;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
svg {
|
|
81
|
-
width: 1rem;
|
|
82
|
-
aspect-ratio: 1 / 1;
|
|
83
|
-
color: oklch(from var(--color-text) l c h / 80%);
|
|
84
|
-
background: var(--color-bg);
|
|
85
|
-
}
|
|
54
|
+
@layer components {
|
|
55
|
+
.heading {
|
|
56
|
+
margin-block-end: 1rem;
|
|
57
|
+
font-size: 1.875rem;
|
|
58
|
+
font-weight: 550;
|
|
59
|
+
letter-spacing: -.0125em;
|
|
60
|
+
color: light-dark(var(--gray-700), var(--gray-100));
|
|
86
61
|
|
|
87
|
-
|
|
88
|
-
|
|
62
|
+
.divider {
|
|
63
|
+
font-weight: 400;
|
|
64
|
+
color: light-dark(var(--gray-600), var(--gray-300));
|
|
89
65
|
}
|
|
90
66
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
67
|
+
a {
|
|
68
|
+
color: light-dark(var(--gray-500), var(--gray-400));
|
|
94
69
|
|
|
95
|
-
|
|
96
|
-
display: block;
|
|
70
|
+
&:hover { color: light-dark(var(--gray-800), var(--gray-200)); }
|
|
97
71
|
}
|
|
98
72
|
}
|
|
99
|
-
}
|
|
100
73
|
|
|
101
|
-
@layer components {
|
|
102
74
|
.events {
|
|
103
75
|
list-style: none;
|
|
104
76
|
margin-block-start: 1rem;
|
|
@@ -109,7 +81,7 @@
|
|
|
109
81
|
padding-block: .5rem;
|
|
110
82
|
padding-inline: .75rem;
|
|
111
83
|
|
|
112
|
-
&:hover { background: var(--
|
|
84
|
+
&:hover { background: light-dark(var(--gray-50), var(--gray-800)); }
|
|
113
85
|
|
|
114
86
|
a {
|
|
115
87
|
display: grid;
|
|
@@ -119,6 +91,12 @@
|
|
|
119
91
|
&:hover { text-decoration: none; }
|
|
120
92
|
}
|
|
121
93
|
|
|
94
|
+
.icon {
|
|
95
|
+
width: var(--icon-width);
|
|
96
|
+
aspect-ratio: 1/1;
|
|
97
|
+
border-radius: .15rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
122
100
|
.name {
|
|
123
101
|
display: grid;
|
|
124
102
|
grid-template-columns: var(--icon-width) 1fr;
|
|
@@ -126,12 +104,6 @@
|
|
|
126
104
|
gap: .5rem;
|
|
127
105
|
}
|
|
128
106
|
|
|
129
|
-
.icon {
|
|
130
|
-
width: var(--icon-width);
|
|
131
|
-
aspect-ratio: 1/1;
|
|
132
|
-
border-radius: .125rem;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
107
|
code {
|
|
136
108
|
display: flex;
|
|
137
109
|
align-items: center;
|
|
@@ -139,7 +111,16 @@
|
|
|
139
111
|
}
|
|
140
112
|
|
|
141
113
|
li:not(:first-child) {
|
|
142
|
-
border-top: 1px solid var(--
|
|
114
|
+
border-top: 1px solid light-dark(var(--gray-100), var(--gray-800));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.empty-state {
|
|
118
|
+
display: none;
|
|
119
|
+
font-size: .875rem;
|
|
120
|
+
font-weight: 300;
|
|
121
|
+
color: light-dark(var(--gray-400), var(--gray-500));
|
|
122
|
+
|
|
123
|
+
&:only-child { display: block; }
|
|
143
124
|
}
|
|
144
125
|
}
|
|
145
126
|
|
|
@@ -153,7 +134,7 @@
|
|
|
153
134
|
width: .5rem;
|
|
154
135
|
aspect-ratio: 1/1;
|
|
155
136
|
background-color: currentColor;
|
|
156
|
-
border:
|
|
137
|
+
border: 1.75px solid oklch(from currentColor .9 c h / .8);
|
|
157
138
|
border-radius: .5rem;
|
|
158
139
|
}
|
|
159
140
|
|
|
@@ -175,7 +156,7 @@
|
|
|
175
156
|
legend {
|
|
176
157
|
font-size: .875rem;
|
|
177
158
|
font-weight: 400;
|
|
178
|
-
color: var(--
|
|
159
|
+
color: light-dark(var(--gray-500), var(--gray-400));
|
|
179
160
|
}
|
|
180
161
|
|
|
181
162
|
input[type="radio"] {
|
|
@@ -185,8 +166,8 @@
|
|
|
185
166
|
|
|
186
167
|
.pills {
|
|
187
168
|
display: inline-flex;
|
|
188
|
-
background: var(--
|
|
189
|
-
border: 1px solid var(--
|
|
169
|
+
background-color: light-dark(var(--gray-50), var(--gray-800));
|
|
170
|
+
border: 1px solid light-dark(var(--gray-200), var(--gray-700));
|
|
190
171
|
border-radius: .3rem;
|
|
191
172
|
overflow: clip;
|
|
192
173
|
|
|
@@ -196,7 +177,7 @@
|
|
|
196
177
|
padding: .25rem .75rem;
|
|
197
178
|
font-size: .875rem;
|
|
198
179
|
font-weight: 400;
|
|
199
|
-
border-inline-start: 1px solid var(--
|
|
180
|
+
border-inline-start: 1px solid light-dark(var(--gray-200), var(--gray-700));
|
|
200
181
|
cursor: pointer;
|
|
201
182
|
transition: all ease-in-out 100ms;
|
|
202
183
|
|
|
@@ -204,7 +185,7 @@
|
|
|
204
185
|
|
|
205
186
|
&:has(input:checked),
|
|
206
187
|
&:hover:not(:has(input:checked)) {
|
|
207
|
-
background-color: var(--
|
|
188
|
+
background-color: light-dark(var(--gray-100), var(--gray-700));
|
|
208
189
|
}
|
|
209
190
|
}
|
|
210
191
|
}
|
|
@@ -213,8 +194,8 @@
|
|
|
213
194
|
select {
|
|
214
195
|
padding: .4rem .75rem;
|
|
215
196
|
font-size: .875rem;
|
|
216
|
-
background: var(--
|
|
217
|
-
border: 1px solid var(--
|
|
197
|
+
background-color: light-dark(var(--gray-50), var(--gray-800));
|
|
198
|
+
border: 1px solid light-dark(var(--gray-200), var(--gray-700));
|
|
218
199
|
border-radius: .25rem;
|
|
219
200
|
cursor: pointer;
|
|
220
201
|
}
|
|
@@ -222,39 +203,122 @@
|
|
|
222
203
|
[type=submit] { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; }
|
|
223
204
|
}
|
|
224
205
|
|
|
206
|
+
.details {
|
|
207
|
+
display: grid;
|
|
208
|
+
grid-template-columns: auto 1fr;
|
|
209
|
+
gap: 1rem 2rem;
|
|
210
|
+
overflow: hidden;
|
|
211
|
+
|
|
212
|
+
.description {
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: baseline;
|
|
215
|
+
margin-block-start: .125em;
|
|
216
|
+
font-weight: 500;
|
|
217
|
+
color: light-dark(var(--gray-700), var(--gray-400));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.icon {
|
|
221
|
+
width: 1.125rem;
|
|
222
|
+
aspect-ratio: 1/1;
|
|
223
|
+
margin-right: .25rem;
|
|
224
|
+
border-radius: .15rem;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.value {
|
|
228
|
+
position: relative;
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
overflow-x: auto;
|
|
232
|
+
|
|
233
|
+
button { margin-left: .25rem; }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.actions {
|
|
237
|
+
display: flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
gap: .5rem;
|
|
240
|
+
position: absolute;
|
|
241
|
+
top: .5rem; right: .5rem;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
svg {
|
|
245
|
+
width: 1rem;
|
|
246
|
+
aspect-ratio: 1 / 1;
|
|
247
|
+
color: light-dark(var(--gray-800), var(--gray-200));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
dd .copy-button .success-icon {
|
|
251
|
+
display: none;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
dd:has([data-copy-success="true"]) .copy-button .copy-icon {
|
|
255
|
+
display: none;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
dd:has([data-copy-success="true"]) .copy-button .success-icon {
|
|
259
|
+
display: block;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
225
263
|
button,
|
|
226
264
|
.copy-button {
|
|
227
265
|
padding: .125rem;
|
|
228
|
-
background: var(--
|
|
266
|
+
background: light-dark(var(--gray-50), var(--gray-800));
|
|
229
267
|
border: 0;
|
|
230
268
|
border-radius: .25rem;
|
|
231
269
|
cursor: pointer;
|
|
232
270
|
|
|
233
271
|
&:hover {
|
|
234
|
-
color:
|
|
272
|
+
color: light-dark(var(--gray-800), var(--gray-400));
|
|
235
273
|
transition: transform ease-in-out 300ms;
|
|
236
|
-
|
|
274
|
+
|
|
275
|
+
& svg { transform: scale(1.05); }
|
|
237
276
|
}
|
|
238
277
|
}
|
|
239
278
|
|
|
279
|
+
.retry-button {
|
|
280
|
+
display: inline-flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
gap: .25rem;
|
|
283
|
+
margin-inline-start: .5rem;
|
|
284
|
+
padding: .25rem .75rem;
|
|
285
|
+
border: 1px solid light-dark(var(--gray-200), var(--gray-700));
|
|
286
|
+
}
|
|
287
|
+
|
|
240
288
|
.json-highlight {
|
|
241
289
|
.json-key {
|
|
242
|
-
color: oklch(35% .15 250);
|
|
243
|
-
cursor: pointer;
|
|
290
|
+
color: light-dark(oklch(35% .15 250), oklch(70% .2 250));
|
|
244
291
|
|
|
245
|
-
|
|
246
|
-
|
|
292
|
+
#payload & {
|
|
293
|
+
border-radius: .125rem;
|
|
294
|
+
cursor: pointer;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#payload &:hover {
|
|
298
|
+
background: light-dark(oklch(from currentColor l c h / .1), oklch(from currentColor l c h / .2));
|
|
247
299
|
}
|
|
248
300
|
}
|
|
249
301
|
|
|
250
302
|
.json-key-selected {
|
|
251
|
-
background: oklch(85%
|
|
303
|
+
background: light-dark(oklch(85% .05 250), oklch(25% .08 250));
|
|
252
304
|
}
|
|
253
305
|
|
|
254
|
-
.json-string { color: oklch(30% .18 160); }
|
|
255
|
-
.json-number { color: oklch(40% .15 130); }
|
|
256
|
-
.json-boolean { color: oklch(45% .18 260); }
|
|
257
|
-
.json-null { color: oklch(50% .05 250); }
|
|
258
|
-
.json-punctuation { color: oklch(20% .03 250); }
|
|
306
|
+
.json-string { color: light-dark(oklch(30% .18 160), oklch(72% .18 160)); user-select: all; }
|
|
307
|
+
.json-number { color: light-dark(oklch(40% .15 130), oklch(70% .18 130)); }
|
|
308
|
+
.json-boolean { color: light-dark(oklch(45% .18 260), oklch(75% .2 260)); }
|
|
309
|
+
.json-null { color: light-dark(oklch(50% .05 250), oklch(68% .08 250)); }
|
|
310
|
+
.json-punctuation { color: light-dark(oklch(20% .03 250), oklch(60% .03 250)); }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.errors {
|
|
314
|
+
padding-block: .5rem;
|
|
315
|
+
padding-inline: 1rem;
|
|
316
|
+
max-height: calc(20lh + 1rem);
|
|
317
|
+
font-size: .875rem;
|
|
318
|
+
color: light-dark(oklch(50.5% .213 27.52), oklch(88.5% .062 18.33));
|
|
319
|
+
background: light-dark(oklch(97.1% .013 17.38), oklch(25.8% .092 26.04 / .5));
|
|
320
|
+
border-radius: .5rem;
|
|
321
|
+
overflow-x: auto;
|
|
322
|
+
font-family: "Courier New", Courier, monospace;
|
|
259
323
|
}
|
|
260
324
|
}
|
|
@@ -5,23 +5,44 @@ module Fuik
|
|
|
5
5
|
private
|
|
6
6
|
|
|
7
7
|
COMMON_EVENT_TYPE_HEADERS = [
|
|
8
|
-
"X-Github-Event",
|
|
9
8
|
"X-Event-Type",
|
|
10
|
-
"X-Webhook-Event"
|
|
9
|
+
"X-Webhook-Event",
|
|
10
|
+
"Webhook-Event"
|
|
11
11
|
]
|
|
12
12
|
|
|
13
13
|
COMMON_EVENT_ID_HEADERS = [
|
|
14
|
-
"X-GitHub-Delivery",
|
|
15
14
|
"X-Event-Id",
|
|
16
|
-
"X-Webhook-Id"
|
|
15
|
+
"X-Webhook-Id",
|
|
16
|
+
"Webhook-Id",
|
|
17
|
+
"webhook-id"
|
|
17
18
|
]
|
|
18
19
|
|
|
19
20
|
def event_type
|
|
20
|
-
from_config("event_type") || from_event_type_headers || from_payload_type || "unknown"
|
|
21
|
+
from_base_class_event_type || from_config("event_type") || from_event_type_headers || from_payload_type || "unknown"
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def event_id
|
|
24
|
-
from_config("event_id") || from_event_id_headers || payload["id"] || Digest::MD5.hexdigest(request.raw_post)
|
|
25
|
+
from_base_class_event_id || from_config("event_id") || from_event_id_headers || payload["id"] || Digest::MD5.hexdigest(request.raw_post)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def from_base_class_event_type
|
|
29
|
+
config = base_class&.event_type_config
|
|
30
|
+
|
|
31
|
+
return if !config
|
|
32
|
+
return config if config.is_a?(String)
|
|
33
|
+
return request.headers[config[:header]] if config.key?(:header)
|
|
34
|
+
|
|
35
|
+
payload.dig(*config[:payload].to_s.split(".")) if config.key?(:payload)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def from_base_class_event_id
|
|
39
|
+
config = base_class&.event_id_config
|
|
40
|
+
|
|
41
|
+
return if !config
|
|
42
|
+
return config if config.is_a?(String)
|
|
43
|
+
return request.headers[config[:header]] if config.key?(:header)
|
|
44
|
+
|
|
45
|
+
payload.dig(*config[:payload].to_s.split(".")) if config.key?(:payload)
|
|
25
46
|
end
|
|
26
47
|
|
|
27
48
|
def from_config(key)
|
|
@@ -53,7 +74,11 @@ module Fuik
|
|
|
53
74
|
@config ||= begin
|
|
54
75
|
config_path = Rails.root.join("app/webhooks/#{params[:provider]}/config.yml")
|
|
55
76
|
|
|
56
|
-
File.exist?(config_path)
|
|
77
|
+
if File.exist?(config_path)
|
|
78
|
+
Fuik.deprecator.warn("Using config.yml is deprecated. Define event_type/event_id on #{params[:provider].camelize}::Base instead.")
|
|
79
|
+
|
|
80
|
+
YAML.load_file(config_path)
|
|
81
|
+
end
|
|
57
82
|
end
|
|
58
83
|
end
|
|
59
84
|
end
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Fuik
|
|
4
|
-
class EventsController < Fuik
|
|
4
|
+
class EventsController < Fuik.configuration.events_controller_parent.constantize
|
|
5
5
|
layout "fuik/application"
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
8
|
@webhook_events = WebhookEvent.filtered(params).order(created_at: :desc)
|
|
9
|
+
|
|
10
|
+
respond_to do |format|
|
|
11
|
+
format.html
|
|
12
|
+
format.json { render json: @webhook_events }
|
|
13
|
+
end
|
|
9
14
|
end
|
|
10
15
|
|
|
11
16
|
def show
|