Targets section: one Add, then a target type and Next, then that type's fields (closes #370)
check / check (push) Successful in 3m18s
check / check (push) Successful in 3m18s
The targets section of the webhook page showed its add form open with every field, a URL included for types that use none. It now lists only its targets until "+ Add" is clicked; "+ Add" shows a choice of target type with Next and Cancel on one row, and Next shows the name and only that type's fields. The database and log types show no URL field and the server stores none for them; the slack form gains its retry field. A refused target brings the page back with the form open on its type, the values entered and the reason, and Cancel empties it. An encoding failure stays a logged 500. Target validation returns its message, so the new-webhook page can reuse it. Model: opus-5-5
This commit was merged in pull request #463.
This commit is contained in:
@@ -104,11 +104,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Targets -->
|
||||
<div class="card" x-data="collapsible">
|
||||
<!-- Targets. The data attributes carry a refused add target
|
||||
submission's type, reason and values back to the form. The
|
||||
URL is data-destination, not data-url: html/template treats
|
||||
an attribute named like a URL as a link and would rewrite
|
||||
a refused ftp: or javascript: value. -->
|
||||
<div class="card" x-data="targetForm"
|
||||
data-type="{{.TargetForm.Type}}"
|
||||
data-reason="{{.TargetError}}"
|
||||
data-name="{{.TargetForm.Name}}"
|
||||
data-destination="{{.TargetForm.URL}}"
|
||||
data-headers="{{.TargetForm.Headers}}"
|
||||
data-timeout="{{.TargetForm.Timeout}}"
|
||||
data-max-retries="{{.TargetForm.MaxRetries}}"
|
||||
data-expiry="{{.TargetForm.Expiry}}">
|
||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h2 class="text-lg font-medium text-gray-900">Targets</h2>
|
||||
<button type="button" @click="toggle" class="btn-small">
|
||||
<button type="button" @click="add" x-show="closed" class="btn-small">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
@@ -116,48 +128,85 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add target form -->
|
||||
<div x-show="open" x-cloak class="p-4 bg-gray-50 border-b border-gray-200">
|
||||
<form method="POST" action="/hook/{{.Webhook.ID}}/targets" x-data="targetForm" @pageshow.window="readType" class="space-y-3">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<div class="flex gap-2">
|
||||
<input type="text" name="name" placeholder="Target name" required class="input text-sm flex-1">
|
||||
<select name="type" @change="readType" class="input text-sm w-32">
|
||||
<option value="http">HTTP</option>
|
||||
<option value="slack">Slack</option>
|
||||
<option value="database">Database</option>
|
||||
<option value="log">Log</option>
|
||||
</select>
|
||||
</div>
|
||||
<div x-show="isHttp">
|
||||
<input type="url" name="url" placeholder="https://example.com/webhook" :disabled="notHttp" class="input text-sm">
|
||||
</div>
|
||||
<div x-show="isHttp">
|
||||
<textarea name="headers" rows="3" placeholder="Authorization: Bearer ..." :disabled="notHttp" class="input text-sm"></textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Optional request headers, one <code>Name: value</code> per line, sent with every delivery.</p>
|
||||
</div>
|
||||
<div x-show="isHttp" class="flex gap-2 items-center">
|
||||
<label class="text-sm text-gray-700">Timeout (seconds, blank = default):</label>
|
||||
<input type="number" name="timeout" min="0" max="300" :disabled="notHttp" class="input text-sm w-24">
|
||||
</div>
|
||||
<div x-show="isHttp">
|
||||
<div class="flex gap-2 items-center">
|
||||
<label class="text-sm text-gray-700">Max retries:</label>
|
||||
<input type="number" name="max_retries" value="0" min="0" max="20" class="input text-sm w-24">
|
||||
<!-- Add target form. Add shows the type choice; Next replaces
|
||||
it with the chosen type's fields. Each type's fields,
|
||||
and the hidden type field submitted with them, exist
|
||||
only while that type is chosen. A refused submission
|
||||
comes back open on its type, with the values entered;
|
||||
Cancel empties the form. The type choice's p-2, narrower
|
||||
than an input's own padding, keeps it, Next and Cancel on
|
||||
one row on a 360px-wide phone. -->
|
||||
<form method="POST" action="/hook/{{.Webhook.ID}}/targets" x-ref="form">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<div x-show="choosing" x-cloak class="p-4 bg-gray-50 border-b border-gray-200 flex flex-wrap gap-2">
|
||||
<select x-ref="type" aria-label="Target type" class="input text-sm p-2 flex-1">
|
||||
<option value="http">HTTP</option>
|
||||
<option value="slack">Slack</option>
|
||||
<option value="database">Database</option>
|
||||
<option value="log">Log</option>
|
||||
</select>
|
||||
<button type="button" @click="next" class="btn-primary text-sm">Next</button>
|
||||
<button type="button" @click="cancel" class="btn-secondary text-sm">Cancel</button>
|
||||
</div>
|
||||
<div x-show="filling" x-cloak class="p-4 bg-gray-50 border-b border-gray-200 space-y-3">
|
||||
<div x-show="reason" x-text="reason" class="alert-error"></div>
|
||||
<input type="text" name="name" :value="name" placeholder="Target name" required class="input text-sm">
|
||||
<template x-if="isHttp">
|
||||
<div class="space-y-3">
|
||||
<input type="hidden" name="type" value="http">
|
||||
<input type="url" name="url" :value="url" placeholder="https://example.com/webhook" class="input text-sm">
|
||||
<div>
|
||||
<textarea name="headers" rows="3" :value="headers" placeholder="Authorization: Bearer ..." class="input text-sm"></textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Optional request headers, one <code>Name: value</code> per line, sent with every delivery.</p>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<label class="text-sm text-gray-700">Timeout (seconds, blank = default):</label>
|
||||
<input type="number" name="timeout" :value="timeout" min="0" max="300" class="input text-sm w-24">
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<label class="text-sm text-gray-700">Max retries:</label>
|
||||
<input type="number" name="max_retries" :value="maxRetries" placeholder="0" min="0" max="20" class="input text-sm w-24">
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">This is the total number of delivery attempts, not retries on top of the first: a value of 3 makes three attempts in all. 0 means a single attempt with no retries and no circuit breaker.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">This is the total number of delivery attempts, not retries on top of the first: a value of 3 makes three attempts in all. 0 means a single attempt with no retries and no circuit breaker.</p>
|
||||
</template>
|
||||
<template x-if="isSlack">
|
||||
<div class="space-y-3">
|
||||
<input type="hidden" name="type" value="slack">
|
||||
<div>
|
||||
<input type="url" name="url" :value="url" placeholder="https://hooks.slack.com/services/..." class="input text-sm">
|
||||
<p class="text-xs text-gray-500 mt-1">Slack or Mattermost incoming webhook URL. Payloads are pretty-printed in code blocks.</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<label class="text-sm text-gray-700">Max retries:</label>
|
||||
<input type="number" name="max_retries" :value="maxRetries" placeholder="0" min="0" max="20" class="input text-sm w-24">
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">This is the total number of delivery attempts, not retries on top of the first: a value of 3 makes three attempts in all. 0 means a single attempt with no retries and no circuit breaker.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="isDatabase">
|
||||
<div>
|
||||
<input type="hidden" name="type" value="database">
|
||||
<input type="text" name="expiry" :value="expiry" placeholder="never" class="input text-sm">
|
||||
<p class="text-xs text-gray-500 mt-1">Archive expiry: "never" (default) keeps rows forever, or a duration like "720h" prunes older rows.</p>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="isLog">
|
||||
<div>
|
||||
<input type="hidden" name="type" value="log">
|
||||
<p class="text-xs text-gray-500">A log target writes each event to the application log. It has no settings beyond its name.</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="btn-primary text-sm">Save</button>
|
||||
<button type="button" @click="cancel" class="btn-secondary text-sm">Cancel</button>
|
||||
</div>
|
||||
<div x-show="isSlack">
|
||||
<input type="url" name="url" placeholder="https://hooks.slack.com/services/..." :disabled="notSlack" class="input text-sm">
|
||||
<p class="text-xs text-gray-500 mt-1">Slack or Mattermost incoming webhook URL. Payloads are pretty-printed in code blocks.</p>
|
||||
</div>
|
||||
<div x-show="isDatabase">
|
||||
<input type="text" name="expiry" placeholder="never" :disabled="notDatabase" class="input text-sm">
|
||||
<p class="text-xs text-gray-500 mt-1">Archive expiry: "never" (default) keeps rows forever, or a duration like "720h" prunes older rows.</p>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary text-sm">Add Target</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="divide-y divide-gray-100">
|
||||
{{range .Targets}}
|
||||
|
||||
Reference in New Issue
Block a user