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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +156 -25
  3. data/app/assets/stylesheets/fuik/application.css +152 -88
  4. data/app/controllers/concerns/fuik/event_type.rb +32 -7
  5. data/app/controllers/fuik/events_controller.rb +6 -1
  6. data/app/controllers/fuik/retries_controller.rb +12 -0
  7. data/app/controllers/fuik/webhooks_controller.rb +19 -15
  8. data/app/jobs/fuik/webhook_processing_job.rb +14 -0
  9. data/app/models/fuik/event.rb +36 -0
  10. data/app/models/fuik/webhook_event/filterable.rb +3 -4
  11. data/app/models/fuik/webhook_event.rb +32 -1
  12. data/app/views/fuik/events/index.html.erb +5 -1
  13. data/app/views/fuik/events/show.html.erb +43 -20
  14. data/app/views/layouts/fuik/application.html.erb +11 -1
  15. data/config/routes.rb +4 -1
  16. data/lib/fuik/configuration.rb +29 -0
  17. data/lib/fuik/engine.rb +0 -4
  18. data/lib/fuik/errors.rb +5 -0
  19. data/lib/fuik/notifications.rb +48 -0
  20. data/lib/fuik/routing/provider_constraint.rb +4 -4
  21. data/lib/fuik/test_helpers.rb +33 -0
  22. data/lib/fuik/version.rb +1 -1
  23. data/lib/fuik.rb +9 -0
  24. data/lib/generators/fuik/provider/templates/base.rb.tt +11 -2
  25. data/lib/generators/fuik/provider/templates/github/base.rb.tt +3 -0
  26. data/lib/generators/fuik/provider/templates/loops/base.rb.tt +2 -0
  27. data/lib/generators/fuik/provider/templates/postmark/base.rb.tt +1 -0
  28. data/lib/generators/fuik/provider/templates/spinal/base.rb.tt +16 -0
  29. data/lib/generators/fuik/provider/templates/spinal/collection_synced.rb.tt +14 -0
  30. data/lib/generators/fuik/provider/templates/spinal/resource_created.rb.tt +14 -0
  31. data/lib/generators/fuik/provider/templates/spinal/resource_pushed.rb.tt +13 -0
  32. data/lib/generators/fuik/provider/templates/spinal/resource_scheduled.rb.tt +13 -0
  33. data/lib/generators/fuik/provider/templates/spinal/resource_unscheduled.rb.tt +12 -0
  34. metadata +14 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a653582dac2c822bb4496a89d0dbd12cd9b96a082bdeed11c209793df27de21
4
- data.tar.gz: 44070a1620d3d867cc5a400e7f61bbd58bebec6e713407d6da6a65d8d2d2037b
3
+ metadata.gz: 7e63ae360a189d38134db206274a5733ad73ea37ae2b338686c952df7ac3c109
4
+ data.tar.gz: c59410136f9c4143690b96b496c3e83fdf0419d806cc308a5ab4e73c1c1b21fb
5
5
  SHA512:
6
- metadata.gz: c870f47e09396993506e96851c491d0e715ddf87d964554aba212e83927a2e9370c75b601cce24f8b4a0653b1b8cfe16f3825fb916048fa9828c05e95ab8d49a
7
- data.tar.gz: 9ce68f8cb2d9aa7af1aa3cfb6a394a1a26cc165ed6dc58320fbfc0b266d219e839d0a29a352164fadce614ff75579dcb8041077d553cdbd45cfe6424d709125e
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. Easiest is to set `Fuik::Engine.config.events_controller_parent` to a controller that requires authentication.
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
- * **Click any key to get the Ruby accessor path**: click `product_id` (as seen in the screenshot above) and get `payload["line_items"][0]["product_id"]` (say what? 🤯)
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::Engine.config.providers_allowed`:
248
+ Configure with `Fuik.configuration.providers_allowed`:
140
249
  ```ruby
141
250
  # Allow all (including production)
142
- Fuik::Engine.config.providers_allowed = :all
251
+ Fuik.configuration.providers_allowed = :all
143
252
 
144
253
  # Explicit allowlist (overrides directory scan)
145
- Fuik::Engine.config.providers_allowed = %w[stripe github shopify]
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 config (if exists);
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 config (if exists);
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 via config
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
- Create `app/webhooks/provider_name/config.yml`:
176
- ```yaml
177
- event_type:
178
- source: header
179
- key: X-Custom-Event
304
+ ## Monitoring
180
305
 
181
- event_id:
182
- source: payload
183
- key: custom_id
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
- The options for `event_type`'s source are:
313
+ Available events:
187
314
 
188
- - header
189
- - payload
190
- - static; for cases when no event type is present in header or payload
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
- --color-text: oklch(30% 0.02 250);
6
- --color-text-muted: oklch(55% 0.02 250);
7
- --color-border: oklch(90% 0.01 250);
8
- --color-bg-hover: oklch(97% 0.01 250);
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(--color-text);
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(--color-bg-hover);
52
- border-radius: .5rem;
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
- dl {
57
- display: grid;
58
- grid-template-columns: auto 1fr;
59
- gap: 1rem 2rem;
60
- overflow: hidden;
61
-
62
- dt {
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
- dd .copy-button .success-icon {
88
- display: none;
62
+ .divider {
63
+ font-weight: 400;
64
+ color: light-dark(var(--gray-600), var(--gray-300));
89
65
  }
90
66
 
91
- dd:has([data-copy-success="true"]) .copy-button .copy-icon {
92
- display: none;
93
- }
67
+ a {
68
+ color: light-dark(var(--gray-500), var(--gray-400));
94
69
 
95
- dd:has([data-copy-success="true"]) .copy-button .success-icon {
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(--color-bg-hover); }
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(--color-border);
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: 2px solid oklch(from currentColor 1 c h / 85%);
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(--color-text-muted);
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(--color-bg);
189
- border: 1px solid var(--color-border);
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(--color-border);
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(--color-bg-hover);
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(--color-bg);
217
- border: 1px solid var(--color-border);
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(--color-bg);
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: oklch(from var(--color-text) l c h);
272
+ color: light-dark(var(--gray-800), var(--gray-400));
235
273
  transition: transform ease-in-out 300ms;
236
- transform: scale(1.10);
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
- &:hover {
246
- background: oklch(90% 0.02 250);
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% 0.05 250);
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) ? YAML.load_file(config_path) : nil
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::Engine.config.events_controller_parent.constantize
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