@@ -83,5 +83,54 @@
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# Notification preferences #}
|
||||
<div class="mb-12">
|
||||
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-4">Notification preferences</h2>
|
||||
<p class="text-sm text-gray-500 mb-4">Choose which events trigger notifications on each channel.</p>
|
||||
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 border-b border-gray-200">
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Event</th>
|
||||
<th class="text-center px-4 py-3 font-medium text-gray-700 w-24">CLI</th>
|
||||
<th class="text-center px-4 py-3 font-medium text-gray-700 w-24">Slack</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
{% set event_types = [
|
||||
{"key": "NOTIFICATION_TYPE_RELEASE_ANNOTATED", "label": "Release annotated"},
|
||||
{"key": "NOTIFICATION_TYPE_RELEASE_STARTED", "label": "Release started"},
|
||||
{"key": "NOTIFICATION_TYPE_RELEASE_SUCCEEDED", "label": "Release succeeded"},
|
||||
{"key": "NOTIFICATION_TYPE_RELEASE_FAILED", "label": "Release failed"},
|
||||
] %}
|
||||
{% set channels = [
|
||||
{"key": "NOTIFICATION_CHANNEL_CLI", "label": "CLI"},
|
||||
{"key": "NOTIFICATION_CHANNEL_SLACK", "label": "Slack"},
|
||||
] %}
|
||||
{% for event in event_types %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 text-gray-700">{{ event.label }}</td>
|
||||
{% for ch in channels %}
|
||||
{% set pref_key = event.key ~ "|" ~ ch.key %}
|
||||
{% set is_enabled = pref_key in enabled_prefs %}
|
||||
<td class="text-center px-4 py-3">
|
||||
<form method="POST" action="/settings/account/notifications" class="inline">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="notification_type" value="{{ event.key }}">
|
||||
<input type="hidden" name="channel" value="{{ ch.key }}">
|
||||
<input type="hidden" name="enabled" value="{{ 'false' if is_enabled else 'true' }}">
|
||||
<button type="submit" class="w-8 h-5 rounded-full relative transition-colors {{ 'bg-gray-900' if is_enabled else 'bg-gray-300' }}" title="{{ 'Disable' if is_enabled else 'Enable' }} {{ event.label | lower }} via {{ ch.label }}">
|
||||
<span class="block w-3.5 h-3.5 rounded-full bg-white shadow-sm absolute top-0.5 transition-all {{ 'left-[calc(100%-1.125rem)]' if is_enabled else 'left-0.5' }}"></span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -227,6 +227,14 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Spec (forest.cue) ──────────────────────────────────────── #}
|
||||
{% if artifact_spec %}
|
||||
<div class="mb-8">
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-3">Spec</h2>
|
||||
<spec-viewer content="{{ artifact_spec | e }}" filename="forest.cue"></spec-viewer>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Details ───────────────────────────────────────────────── #}
|
||||
{% if artifact.commit_message or artifact.repo_url or artifact.source_email or artifact.run_url %}
|
||||
<div class="mb-8">
|
||||
|
||||
105
templates/pages/install_slack.html.jinja
Normal file
105
templates/pages/install_slack.html.jinja
Normal file
@@ -0,0 +1,105 @@
|
||||
{% extends "base.html.jinja" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="max-w-2xl mx-auto px-4 py-12">
|
||||
<div class="mb-8">
|
||||
<a href="/orgs/{{ current_org }}/settings/integrations" class="text-sm text-gray-500 hover:text-gray-700">← All integrations</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4 mb-8">
|
||||
<div class="w-14 h-14 rounded-lg border border-gray-200 flex items-center justify-center bg-white shrink-0">
|
||||
<svg class="w-7 h-7" viewBox="0 0 24 24" fill="#4A154B">
|
||||
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zm10.122 2.521a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.268 0a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zm-2.523 10.122a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zm0-1.268a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">Install Slack</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Post deployment notifications directly to Slack channels from <strong>{{ current_org }}</strong>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if error is defined and error %}
|
||||
<div class="mb-6 px-4 py-3 text-sm text-red-700 bg-red-50 border border-red-200 rounded-lg">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── How it works ──────────────────────────────────────────── #}
|
||||
<div class="mb-8 p-4 bg-gray-50 border border-gray-200 rounded-lg">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">How it works</h3>
|
||||
<ul class="text-sm text-gray-600 space-y-1.5">
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-gray-400 mt-0.5">1.</span>
|
||||
<span>Rich Block Kit messages with release metadata, status badges, and color-coded sidebars</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-gray-400 mt-0.5">2.</span>
|
||||
<span>Notifications include organisation, project, destination, commit, branch, and author</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-gray-400 mt-0.5">3.</span>
|
||||
<span>Configure which events trigger notifications (releases started, succeeded, failed, annotated)</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-gray-400 mt-0.5">4.</span>
|
||||
<span>Failed deliveries are retried up to 3 times with exponential backoff</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% if has_slack_oauth %}
|
||||
{# ── OAuth "Add to Slack" flow ─────────────────────────────── #}
|
||||
<div class="mb-8">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-3">Connect with Slack</h3>
|
||||
<p class="text-sm text-gray-500 mb-4">Click the button below to authorize Forage to post to a Slack channel. You'll choose which channel during the Slack authorization flow.</p>
|
||||
<a href="{{ slack_oauth_url }}" class="inline-flex items-center gap-3 px-5 py-3 bg-[#4A154B] text-white rounded-lg hover:bg-[#3e1240] transition-colors font-medium text-sm">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zm10.122 2.521a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.268 0a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zm-2.523 10.122a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zm0-1.268a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/>
|
||||
</svg>
|
||||
Add to Slack
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="relative my-8">
|
||||
<div class="absolute inset-0 flex items-center"><div class="w-full border-t border-gray-200"></div></div>
|
||||
<div class="relative flex justify-center text-sm"><span class="px-3 bg-white text-gray-400">or use a webhook URL</span></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Manual webhook URL form ───────────────────────────────── #}
|
||||
<form method="POST" action="/orgs/{{ current_org }}/settings/integrations/slack" class="space-y-5">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
|
||||
{% if has_slack_oauth %}
|
||||
<p class="text-sm text-gray-500 mb-2">Alternatively, paste a Slack Incoming Webhook URL directly. Create one in your <a href="https://api.slack.com/apps" target="_blank" rel="noopener" class="text-gray-700 underline hover:text-gray-900">Slack App settings</a>.</p>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500 mb-2">Paste a Slack Incoming Webhook URL. Create one in your <a href="https://api.slack.com/apps" target="_blank" rel="noopener" class="text-gray-700 underline hover:text-gray-900">Slack App settings</a> under <strong>Incoming Webhooks</strong>.</p>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
||||
<input type="text" id="name" name="name" placeholder="e.g. #deploys" required
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent">
|
||||
<p class="text-xs text-gray-400 mt-1">A friendly name to identify this integration</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="webhook_url" class="block text-sm font-medium text-gray-700 mb-1">Webhook URL</label>
|
||||
<input type="url" id="webhook_url" name="webhook_url" placeholder="https://hooks.slack.com/services/T.../B.../..." required
|
||||
class="w-full px-3 py-2 text-sm font-mono border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent">
|
||||
<p class="text-xs text-gray-400 mt-1">Must be a <code class="bg-gray-200 px-1 py-0.5 rounded">https://hooks.slack.com/</code> URL</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="channel_name" class="block text-sm font-medium text-gray-700 mb-1">Channel name <span class="font-normal text-gray-400">(optional)</span></label>
|
||||
<input type="text" id="channel_name" name="channel_name" placeholder="#deploys"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent">
|
||||
<p class="text-xs text-gray-400 mt-1">For display purposes only (defaults to #general)</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
<button type="submit" class="w-full px-4 py-2.5 text-sm font-medium bg-gray-900 text-white rounded-md hover:bg-gray-800 transition-colors">
|
||||
Install Slack integration
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
80
templates/pages/install_webhook.html.jinja
Normal file
80
templates/pages/install_webhook.html.jinja
Normal file
@@ -0,0 +1,80 @@
|
||||
{% extends "base.html.jinja" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="max-w-2xl mx-auto px-4 py-12">
|
||||
<div class="mb-8">
|
||||
<a href="/orgs/{{ current_org }}/settings/integrations" class="text-sm text-gray-500 hover:text-gray-700">← All integrations</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4 mb-8">
|
||||
<div class="w-14 h-14 rounded-lg border border-gray-200 flex items-center justify-center bg-white shrink-0">
|
||||
<svg class="w-7 h-7 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">Install Webhook</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Send HTTP POST requests to your endpoint when deployment events occur in <strong>{{ current_org }}</strong>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if error is defined and error %}
|
||||
<div class="mb-6 px-4 py-3 text-sm text-red-700 bg-red-50 border border-red-200 rounded-lg">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── How it works ──────────────────────────────────────────── #}
|
||||
<div class="mb-8 p-4 bg-gray-50 border border-gray-200 rounded-lg">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">How it works</h3>
|
||||
<ul class="text-sm text-gray-600 space-y-1.5">
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-gray-400 mt-0.5">1.</span>
|
||||
<span>Forage sends a <code class="text-xs bg-gray-200 px-1 py-0.5 rounded">POST</code> request with a JSON payload to your URL</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-gray-400 mt-0.5">2.</span>
|
||||
<span>Payloads include event type, release metadata, project, and organisation</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-gray-400 mt-0.5">3.</span>
|
||||
<span>Optional HMAC-SHA256 signing via <code class="text-xs bg-gray-200 px-1 py-0.5 rounded">X-Forage-Signature</code> header</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-gray-400 mt-0.5">4.</span>
|
||||
<span>Failed deliveries are retried up to 3 times with exponential backoff</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{# ── Setup form ────────────────────────────────────────────── #}
|
||||
<form method="POST" action="/orgs/{{ current_org }}/settings/integrations/webhook" class="space-y-5">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
||||
<input type="text" id="name" name="name" placeholder="e.g. Production alerts" required
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent">
|
||||
<p class="text-xs text-gray-400 mt-1">A friendly name to identify this webhook</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="url" class="block text-sm font-medium text-gray-700 mb-1">Payload URL</label>
|
||||
<input type="url" id="url" name="url" placeholder="https://example.com/webhooks/forage" required
|
||||
class="w-full px-3 py-2 text-sm font-mono border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent">
|
||||
<p class="text-xs text-gray-400 mt-1">Must use HTTPS (HTTP allowed for localhost only)</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="secret" class="block text-sm font-medium text-gray-700 mb-1">Secret <span class="font-normal text-gray-400">(optional)</span></label>
|
||||
<input type="text" id="secret" name="secret" placeholder="whsec_..."
|
||||
class="w-full px-3 py-2 text-sm font-mono border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent">
|
||||
<p class="text-xs text-gray-400 mt-1">Used to compute <code class="bg-gray-200 px-1 py-0.5 rounded">X-Forage-Signature</code> (HMAC-SHA256) so you can verify payloads are from Forage</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
<button type="submit" class="w-full px-4 py-2.5 text-sm font-medium bg-gray-900 text-white rounded-md hover:bg-gray-800 transition-colors">
|
||||
Install webhook
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
157
templates/pages/integration_detail.html.jinja
Normal file
157
templates/pages/integration_detail.html.jinja
Normal file
@@ -0,0 +1,157 @@
|
||||
{% extends "base.html.jinja" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="max-w-3xl mx-auto px-4 py-12">
|
||||
<div class="mb-8">
|
||||
<a href="/orgs/{{ current_org }}/settings/integrations" class="text-sm text-gray-500 hover:text-gray-700">← All integrations</a>
|
||||
</div>
|
||||
|
||||
{# ── Header ───────────────────────────────────────────────── #}
|
||||
<div class="flex items-start justify-between mb-8">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-lg border border-gray-200 flex items-center justify-center bg-white shrink-0">
|
||||
{% if integration.integration_type == "webhook" %}
|
||||
<svg class="w-6 h-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg>
|
||||
{% elif integration.integration_type == "slack" %}
|
||||
<svg class="w-6 h-6 text-gray-600" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">{{ integration.name }}</h1>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="text-sm text-gray-500">{{ integration.type_display }}</span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {{ 'bg-green-50 text-green-700' if integration.enabled else 'bg-gray-100 text-gray-500' }}">
|
||||
{{ "Active" if integration.enabled else "Paused" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<form method="POST" action="/orgs/{{ current_org }}/settings/integrations/{{ integration.id }}/toggle" class="inline">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="enabled" value="{{ 'false' if integration.enabled else 'true' }}">
|
||||
<button type="submit" class="px-3 py-1.5 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
|
||||
{{ "Pause" if integration.enabled else "Resume" }}
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/orgs/{{ current_org }}/settings/integrations/{{ integration.id }}/delete" class="inline" onsubmit="return confirm('Uninstall this integration? This cannot be undone.')">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<button type="submit" class="px-3 py-1.5 text-sm text-red-600 border border-red-200 rounded-md hover:bg-red-50 transition-colors">
|
||||
Uninstall
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if test_sent is defined and test_sent %}
|
||||
<div class="mb-6 px-4 py-3 text-sm text-green-700 bg-green-50 border border-green-200 rounded-lg">
|
||||
Test notification sent. Check your endpoint for delivery.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Configuration ────────────────────────────────────────── #}
|
||||
<div class="mb-8">
|
||||
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">Configuration</h2>
|
||||
<div class="border border-gray-200 rounded-lg divide-y divide-gray-100">
|
||||
{% if config.type_name == "Webhook" %}
|
||||
<div class="px-4 py-3 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-500">Payload URL</span>
|
||||
<span class="text-sm text-gray-900 font-mono">{{ config.detail }}</span>
|
||||
</div>
|
||||
{% if config.has_secret is defined and config.has_secret %}
|
||||
<div class="px-4 py-3 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-500">Signing</span>
|
||||
<span class="text-sm text-gray-900">HMAC-SHA256 enabled</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="px-4 py-3">
|
||||
<span class="text-sm text-gray-900">{{ config.detail }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Events ───────────────────────────────────────────────── #}
|
||||
<div class="mb-8">
|
||||
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">Events</h2>
|
||||
<p class="text-sm text-gray-500 mb-3">Choose which deployment events trigger this integration.</p>
|
||||
<div class="border border-gray-200 rounded-lg divide-y divide-gray-100">
|
||||
{% for rule in rules %}
|
||||
<div class="flex items-center justify-between px-4 py-3">
|
||||
<div>
|
||||
<span class="text-sm text-gray-700">{{ rule.label }}</span>
|
||||
</div>
|
||||
<form method="POST" action="/orgs/{{ current_org }}/settings/integrations/{{ integration.id }}/rules" class="inline">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="notification_type" value="{{ rule.notification_type }}">
|
||||
<input type="hidden" name="enabled" value="{{ 'false' if rule.enabled else 'true' }}">
|
||||
<button type="submit" class="w-9 h-5 rounded-full relative transition-colors {{ 'bg-gray-900' if rule.enabled else 'bg-gray-300' }}" title="{{ 'Disable' if rule.enabled else 'Enable' }} {{ rule.label | lower }}">
|
||||
<span class="block w-3.5 h-3.5 rounded-full bg-white shadow-sm absolute top-[3px] transition-all {{ 'left-[calc(100%-1.125rem)]' if rule.enabled else 'left-[3px]' }}"></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Recent deliveries ────────────────────────────────────── #}
|
||||
<div class="mb-8">
|
||||
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">Recent deliveries</h2>
|
||||
{% if deliveries | length > 0 %}
|
||||
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Notification</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Time</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-100">
|
||||
{% for d in deliveries %}
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap">
|
||||
{% if d.status == "delivered" %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-50 text-green-700">Delivered</span>
|
||||
{% elif d.status == "failed" %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-50 text-red-700">Failed</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-50 text-yellow-700">Pending</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-700 font-mono truncate max-w-[200px]" title="{{ d.notification_id }}">{{ d.notification_id[:12] }}{% if d.notification_id | length > 12 %}…{% endif %}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-500 whitespace-nowrap">{{ d.attempted_at[:19] | replace("T", " ") }} UTC</td>
|
||||
<td class="px-4 py-2 text-sm text-red-600 truncate max-w-[250px]" title="{{ d.error_message }}">{{ d.error_message | default("—", true) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="border border-gray-200 rounded-lg p-6 text-center">
|
||||
<p class="text-sm text-gray-500">No deliveries yet. Send a test event or wait for a deployment notification.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ── Test ─────────────────────────────────────────────────── #}
|
||||
<div class="mb-8">
|
||||
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">Testing</h2>
|
||||
<div class="border border-gray-200 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-500 mb-3">Send a test <code class="text-xs bg-gray-100 px-1 py-0.5 rounded">release_succeeded</code> event to verify your endpoint is receiving payloads correctly.</p>
|
||||
<form method="POST" action="/orgs/{{ current_org }}/settings/integrations/{{ integration.id }}/test">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<button type="submit" class="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
|
||||
Send test event
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
75
templates/pages/integration_installed.html.jinja
Normal file
75
templates/pages/integration_installed.html.jinja
Normal file
@@ -0,0 +1,75 @@
|
||||
{% extends "base.html.jinja" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="max-w-2xl mx-auto px-4 py-12">
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-50 mb-4">
|
||||
<svg class="w-8 h-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold">{{ integration.type_display }} installed</h1>
|
||||
<p class="text-sm text-gray-500 mt-1"><strong>{{ integration.name }}</strong> is now active in {{ current_org }}.</p>
|
||||
</div>
|
||||
|
||||
{# ── API Token (shown once) ───────────────────────────────── #}
|
||||
{% if api_token %}
|
||||
<div class="mb-8 p-5 bg-amber-50 border border-amber-200 rounded-lg">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-amber-600 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="text-sm font-semibold text-amber-800 mb-1">API Token</h3>
|
||||
<p class="text-sm text-amber-700 mb-3">This token allows the integration to query the Forage API. Copy it now — it won't be shown again.</p>
|
||||
<div class="relative">
|
||||
<code id="api-token" class="block w-full px-3 py-2 text-sm font-mono bg-white border border-amber-300 rounded-md text-gray-900 break-all select-all">{{ api_token }}</code>
|
||||
<button type="button" onclick="copyToken()" id="copy-btn" class="absolute top-1.5 right-1.5 px-2 py-1 text-xs bg-amber-100 text-amber-700 rounded hover:bg-amber-200 transition-colors">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── What's next ──────────────────────────────────────────── #}
|
||||
<div class="mb-8 p-4 bg-gray-50 border border-gray-200 rounded-lg">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">What's next</h3>
|
||||
<ul class="text-sm text-gray-600 space-y-1.5">
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-gray-400 mt-0.5">1.</span>
|
||||
<span>Configure which events trigger notifications on the integration settings page</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-gray-400 mt-0.5">2.</span>
|
||||
<span>Use the API token to query releases, projects, and notifications from your service</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-gray-400 mt-0.5">3.</span>
|
||||
<span>Send a test event to verify your endpoint receives payloads correctly</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<a href="/orgs/{{ current_org }}/settings/integrations/{{ integration.id }}" class="flex-1 text-center px-4 py-2.5 text-sm font-medium bg-gray-900 text-white rounded-md hover:bg-gray-800 transition-colors">
|
||||
Configure integration
|
||||
</a>
|
||||
<a href="/orgs/{{ current_org }}/settings/integrations" class="flex-1 text-center px-4 py-2.5 text-sm font-medium border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
|
||||
Back to integrations
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function copyToken() {
|
||||
var token = document.getElementById('api-token').textContent;
|
||||
navigator.clipboard.writeText(token).then(function() {
|
||||
var btn = document.getElementById('copy-btn');
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
140
templates/pages/integrations.html.jinja
Normal file
140
templates/pages/integrations.html.jinja
Normal file
@@ -0,0 +1,140 @@
|
||||
{% extends "base.html.jinja" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="max-w-4xl mx-auto px-4 py-12">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold">Integrations</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Connect tools and services to receive deployment notifications from {{ current_org }}.</p>
|
||||
</div>
|
||||
|
||||
{% if error is defined and error %}
|
||||
<div class="mb-6 px-4 py-3 text-sm text-red-700 bg-red-50 border border-red-200 rounded-lg">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Installed integrations ─────────────────────────────────── #}
|
||||
{% if integrations | length > 0 %}
|
||||
<div class="mb-12">
|
||||
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-4">Installed</h2>
|
||||
<div class="border border-gray-200 rounded-lg divide-y divide-gray-100">
|
||||
{% for integ in integrations %}
|
||||
<a href="/orgs/{{ current_org }}/settings/integrations/{{ integ.id }}" class="flex items-center justify-between px-5 py-4 hover:bg-gray-50 transition-colors first:rounded-t-lg last:rounded-b-lg">
|
||||
<div class="flex items-center gap-4">
|
||||
{# Icon #}
|
||||
<div class="w-10 h-10 rounded-lg border border-gray-200 flex items-center justify-center bg-white shrink-0">
|
||||
{% if integ.integration_type == "webhook" %}
|
||||
<svg class="w-5 h-5 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg>
|
||||
{% elif integ.integration_type == "slack" %}
|
||||
<svg class="w-5 h-5 text-gray-600" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zm10.122 2.521a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.268 0a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zm-2.523 10.122a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zm0-1.268a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-gray-900">{{ integ.name }}</span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {{ 'bg-green-50 text-green-700' if integ.enabled else 'bg-gray-100 text-gray-500' }}">
|
||||
{{ "Active" if integ.enabled else "Paused" }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 mt-0.5">
|
||||
{{ integ.type_display }}
|
||||
·
|
||||
{% if integ.enabled_rules == integ.total_rules %}
|
||||
All events
|
||||
{% elif integ.enabled_rules == 0 %}
|
||||
No events
|
||||
{% else %}
|
||||
{{ integ.enabled_rules }}/{{ integ.total_rules }} events
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-gray-400">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/></svg>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Available integrations (marketplace) ─────────────────── #}
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-4">Available integrations</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{# Webhook #}
|
||||
<a href="/orgs/{{ current_org }}/settings/integrations/install/webhook" class="group border border-gray-200 rounded-lg p-5 hover:border-gray-300 hover:shadow-sm transition-all">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-lg border border-gray-200 flex items-center justify-center bg-white shrink-0 group-hover:border-gray-300">
|
||||
<svg class="w-6 h-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-gray-900">Webhook</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-1">Send HTTP POST notifications to any URL when deployments happen. Supports HMAC-SHA256 payload signing and custom headers.</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{# Slack #}
|
||||
<div class="border border-gray-200 rounded-lg p-5 opacity-60">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-lg border border-gray-200 flex items-center justify-center bg-white shrink-0">
|
||||
<svg class="w-6 h-6 text-gray-400" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zm10.122 2.521a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.268 0a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zm-2.523 10.122a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zm0-1.268a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-gray-500">Slack</span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500">Coming soon</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 mt-1">Post deployment notifications directly to Slack channels. Rich formatting with release details, status, and quick links.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Discord #}
|
||||
<div class="border border-gray-200 rounded-lg p-5 opacity-60">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-lg border border-gray-200 flex items-center justify-center bg-white shrink-0">
|
||||
<svg class="w-6 h-6 text-gray-400" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-gray-500">Discord</span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500">Coming soon</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 mt-1">Send deployment updates to Discord channels via webhook. Includes embeds with release metadata and status.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Email #}
|
||||
<div class="border border-gray-200 rounded-lg p-5 opacity-60">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-lg border border-gray-200 flex items-center justify-center bg-white shrink-0">
|
||||
<svg class="w-6 h-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-gray-500">Email</span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500">Coming soon</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 mt-1">Email notifications for deployment events. Configure recipients and digest frequency per project.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user