feat(ui): Shadow UI redesign, domain management, and DNS autoconfigure (#11119)

Co-authored-by: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
This commit is contained in:
Andras BacsaiandShadowArcanist authored and GitHub committed 2026-08-03 23:11:54 +02:00
1 parent 0b843bb07c
commit 8ae56587d5
497 files changed
+44045 -22630

No files matched your search

@@ -1,434 +1,497 @@
<div>
<x-slot:title>
{{ $github_app->name ?: 'GitHub App' }} | Sources | Coolify
</x-slot>
@if (data_get($github_app, 'app_id'))
<form wire:submit='submit'>
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
<div class="flex items-center gap-2">
<h1>GitHub App</h1>
@if ($isConnected)
<x-status-badge status="Connected" type="success" />
@endif
</div>
<div class="flex gap-2">
@if (data_get($github_app, 'installation_id'))
<x-forms.button canGate="update" :canResource="$github_app" type="submit"
:disabled="$activeTab !== 'general'">Save</x-forms.button>
<x-forms.button canGate="view" :canResource="$github_app"
wire:click.prevent="testConnection">Test Connection</x-forms.button>
@endif
@if ($github_app)
@can('delete', $github_app)
@if ($applications->count() > 0)
<x-modal-confirmation title="Confirm GitHub App Deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete" :actions="['The selected GitHub App will be permanently deleted.']" confirmationText="{{ data_get($github_app, 'name') }}"
confirmationLabel="Please confirm the execution of the actions by entering the GitHub App Name below"
shortConfirmationLabel="GitHub App Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
@else
<x-modal-confirmation title="Confirm GitHub App Deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete" :actions="['The selected GitHub App will be permanently deleted.']"
confirmationLabel="Please confirm the execution of the actions by entering the GitHub App Name below"
shortConfirmationLabel="GitHub App Name"
confirmationText="{{ data_get($github_app, 'name') }}" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
@endif
@endcan
@endif
</div>
</div>
<div class="subtitle">Your Private GitHub App for private repositories.</div>
@if (!data_get($github_app, 'installation_id'))
<div class="mb-10 rounded-sm alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>You must complete this step before you can use this source!</span>
</div>
<a class="items-center justify-center coolbox" href="{{ getInstallationPath($github_app) }}">
Install Repositories on GitHub
</a>
@else
<div class="navbar-main">
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
<a class="{{ request()->routeIs('source.github.show') ? 'dark:text-white' : '' }}"
{{ wireNavigate() }}
href="{{ route('source.github.show', ['github_app_uuid' => $github_app->uuid]) }}">
General
</a>
<a class="{{ request()->routeIs('source.github.permissions') ? 'dark:text-white' : '' }}"
{{ wireNavigate() }}
href="{{ route('source.github.permissions', ['github_app_uuid' => $github_app->uuid]) }}">
Permissions
</a>
<a class="{{ request()->routeIs('source.github.resources') ? 'dark:text-white' : '' }}"
{{ wireNavigate() }}
href="{{ route('source.github.resources', ['github_app_uuid' => $github_app->uuid]) }}">
Resources
</a>
</nav>
</div>
@php
$githubAppRouteParameters = ['github_app_uuid' => $github_app->uuid];
$showSettingsSidebar = in_array($activeTab, ['general', 'danger'], true);
$settingsMenuItems = [
[
'label' => 'General',
'route' => 'source.github.show',
'active' => $activeTab === 'general',
'icon' => 'settings',
],
[
'label' => 'Danger Zone',
'route' => 'source.github.danger',
'active' => $activeTab === 'danger',
'icon' => 'shield-alert',
],
];
@endphp
<div class="pt-4">
<div class="flex flex-col gap-2" @if ($activeTab !== 'general') hidden @endif>
<div class="flex flex-col sm:flex-row gap-2">
<div class="flex flex-col sm:flex-row items-start sm:items-end gap-2 w-full">
<x-forms.input canGate="update" :canResource="$github_app" id="name" label="App Name" />
<x-forms.button canGate="update" :canResource="$github_app" wire:click.prevent="updateGithubAppName">
Sync Name
</x-forms.button>
@can('update', $github_app)
<a href="{{ $this->getGithubAppNameUpdatePath() }}">
<x-forms.button canGate="update" :canResource="$github_app"
class="bg-transparent border-transparent hover:bg-transparent hover:border-transparent hover:underline">
Rename
<x-external-link />
</x-forms.button>
</a>
<a href="{{ getInstallationPath($github_app) }}" class="w-fit">
<x-forms.button canGate="update" :canResource="$github_app"
class="bg-transparent border-transparent hover:bg-transparent hover:border-transparent hover:underline whitespace-nowrap">
Update Repositories
<x-external-link />
</x-forms.button>
</a>
@endcan
</div>
</div>
<x-forms.input canGate="update" :canResource="$github_app" id="organization" label="Organization"
placeholder="If empty, personal user will be used" />
@if (!isCloud())
<div class="w-48">
<x-forms.checkbox canGate="update" :canResource="$github_app" label="System Wide?"
helper="If checked, this GitHub App will be available for everyone in this Coolify instance."
instantSave id="isSystemWide" />
</div>
@if ($isSystemWide)
<x-callout type="warning" title="Not Recommended">
System-wide GitHub Apps are shared across all teams on this Coolify instance. This means any team can use this GitHub App to deploy applications from your repositories. For better security and isolation, it's recommended to create team-specific GitHub Apps instead.
</x-callout>
@endif
@endif
<div class="flex flex-col sm:flex-row gap-2">
<x-forms.input canGate="update" :canResource="$github_app" id="htmlUrl" label="HTML Url" />
<x-forms.input canGate="update" :canResource="$github_app" id="apiUrl" label="API Url" />
</div>
<div class="flex flex-col sm:flex-row gap-2">
<x-forms.input canGate="update" :canResource="$github_app" id="customUser" label="User"
required />
<x-forms.input canGate="update" :canResource="$github_app" type="number" id="customPort"
label="Port" required />
</div>
<div class="flex flex-col sm:flex-row gap-2">
<x-forms.input canGate="update" :canResource="$github_app" type="number" id="appId"
label="App Id" required />
<x-forms.input canGate="update" :canResource="$github_app" type="number"
id="installationId" label="Installation Id" required />
</div>
<div class="flex flex-col sm:flex-row gap-2">
<x-forms.input canGate="update" :canResource="$github_app" id="clientId" label="Client Id"
type="password" required />
<x-forms.input canGate="update" :canResource="$github_app" id="clientSecret"
label="Client Secret" type="password" required />
<x-forms.input canGate="update" :canResource="$github_app" id="webhookSecret"
label="Webhook Secret" type="password" required />
</div>
<div class="flex gap-2">
<x-forms.select canGate="update" :canResource="$github_app" id="privateKeyId"
label="Private Key" required>
@if (blank($github_app->private_key_id))
<option value="0" selected>Select a private key</option>
@endif
@foreach ($privateKeys as $privateKey)
<option value="{{ $privateKey->id }}">{{ $privateKey->name }}</option>
@endforeach
</x-forms.select>
</div>
</div>
<div class="flex flex-col gap-2" @if ($activeTab !== 'permissions') hidden @endif>
<div class="flex flex-col sm:flex-row items-start sm:items-end gap-2">
<h2>Permissions</h2>
@can('view', $github_app)
<x-forms.button canGate="view" :canResource="$github_app" wire:click.prevent="checkPermissions">Refetch</x-forms.button>
<a href="{{ getPermissionsPath($github_app) }}">
<x-forms.button canGate="view" :canResource="$github_app" class="bg-transparent border-transparent hover:bg-transparent hover:border-transparent hover:underline">
Update
<x-external-link />
</x-forms.button>
</a>
@endcan
</div>
<div class="flex flex-col sm:flex-row gap-2">
<x-forms.input canGate="view" :canResource="$github_app" id="contents" helper="read - mandatory." label="Content" readonly
placeholder="N/A" />
<x-forms.input canGate="view" :canResource="$github_app" id="metadata" helper="read - mandatory." label="Metadata" readonly
placeholder="N/A" />
<x-forms.input canGate="view" :canResource="$github_app" id="pullRequests"
helper="write access needed to use deployment status update in previews."
label="Pull Request" readonly placeholder="N/A" />
</div>
</div>
<div class="flex flex-col" @if ($activeTab !== 'resources') hidden @endif x-data="{ search: '' }">
@if ($applications->isEmpty())
<div class="py-4 text-sm opacity-70">
No resources are currently using this GitHub App.
</div>
@else
<x-forms.input canGate="view" :canResource="$github_app" placeholder="Search resources..." x-model="search" id="null" />
<div class="overflow-x-auto pt-4">
<div class="inline-block min-w-full">
<div class="overflow-hidden">
<table class="min-w-full">
<thead>
<tr>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Project</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Environment</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Name</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Type</th>
</tr>
</thead>
<tbody class="divide-y">
@foreach ($applications->sortBy('name', SORT_NATURAL) as $resource)
@php
$projectName = (string) data_get($resource->project(), 'name');
$environmentName = (string) data_get($resource, 'environment.name');
$resourceName = (string) $resource->name;
$resourceType = (string) str($resource->type())->headline();
@endphp
<tr class="dark:hover:bg-coolgray-300 hover:bg-neutral-100"
x-show="search === ''
|| '{{ strtolower(addslashes($projectName)) }}'.includes(search.toLowerCase())
|| '{{ strtolower(addslashes($environmentName)) }}'.includes(search.toLowerCase())
|| '{{ strtolower(addslashes($resourceName)) }}'.includes(search.toLowerCase())
|| '{{ strtolower(addslashes($resourceType)) }}'.includes(search.toLowerCase())">
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ $projectName }}
</td>
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ $environmentName }}
</td>
<td class="px-5 py-4 text-sm whitespace-nowrap">
<a {{ wireNavigate() }} href="{{ $resource->link() }}">
{{ $resourceName }}
<x-internal-link />
</a>
</td>
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ $resourceType }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endif
</div>
</div>
@endif
</form>
@else
<div class="flex flex-col sm:flex-row sm:items-center gap-2 pb-4">
<h1>GitHub App</h1>
<div class="flex gap-2">
@if ($github_app)
@can('delete', $github_app)
<x-modal-confirmation title="Confirm GitHub App Deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete" :actions="['The selected GitHub App will be permanently deleted.']" confirmationText="{{ data_get($github_app, 'name') }}"
confirmationLabel="Please confirm the execution of the actions by entering the GitHub App Name below"
shortConfirmationLabel="GitHub App Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
@endcan
<x-dashboard.navbar section="source" :parameters="$githubAppRouteParameters"
:title="$name ?: 'GitHub App'"
:subtitle="filled($organization) ? 'GitHub App for '.$organization : 'Private GitHub source'"
:titleOnDesktop="true">
<x-slot:titleMeta>
@if (data_get($github_app, 'installation_id'))
<x-status-badge label="Connected" type="success" />
@else
<x-status-badge label="Setup incomplete" type="warning" />
@endif
</x-slot:titleMeta>
</x-dashboard.navbar>
@if ($showSettingsSidebar)
<section class="application-settings-workspace mt-4 w-full max-w-[1180px] lg:mt-0">
<div class="grid min-w-0 gap-8 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
<aside class="application-settings-navigation min-w-0 xl:sticky xl:top-26 xl:self-start">
<nav aria-label="GitHub App settings"
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
<div class="nav-section hidden xl:block">Settings</div>
@foreach ($settingsMenuItems as $menuItem)
<a wire:key="github-app-settings-{{ str($menuItem['label'])->slug() }}"
@class([
'menu-item',
'menu-item-active' => $menuItem['active'],
])
{{ wireNavigate() }}
href="{{ route($menuItem['route'], $githubAppRouteParameters) }}">
<x-reicon :name="$menuItem['icon']" class="menu-item-icon" />
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
</a>
@endforeach
</nav>
</aside>
<div class="min-w-0 xl:mt-3">
@if (!data_get($github_app, 'installation_id') && $activeTab === 'general')
<div class="application-settings-form">
<x-application.settings-section title="Complete GitHub installation"
description="Choose which repositories this GitHub App can access before using it as a source.">
<div class="flex flex-col items-start gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-start gap-3">
<div
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-warning/10 text-warning">
<x-reicon name="alert-triangle" class="size-4" />
</div>
<p class="max-w-xl text-[12px] leading-5 text-neutral-500 dark:text-fg-dim">
Repository access has not been installed yet. Complete this step before attaching the
source to an application.
</p>
</div>
<a class="button shrink-0 bg-coollabs/10! text-coollabs! ring-1 ring-coollabs/25 hover:bg-coollabs/15! dark:bg-warning/15! dark:text-warning! dark:ring-warning/25 dark:hover:bg-warning/20!"
href="{{ getInstallationPath($github_app) }}">
Install repositories
<x-external-link />
</a>
</div>
</x-application.settings-section>
</div>
@elseif ($activeTab === 'general')
@php
$privateKeyOptions = collect([
blank($github_app->private_key_id)
? ['value' => 0, 'label' => 'Select a private key']
: null,
...$privateKeys->map(fn ($privateKey) => [
'value' => $privateKey->id,
'label' => $privateKey->name,
])->all(),
])->filter()->values()->all();
@endphp
<form wire:submit="submit" class="application-settings-form">
<x-unsaved-bar action="submit" />
<x-application.settings-section title="General"
description="Connection and authentication settings for this private GitHub source.">
<x-slot:actions>
<x-forms.button type="button" wire:click.prevent="updateGithubAppName">
<x-reicon name="refresh" class="size-3.5" />
Sync name
</x-forms.button>
@can('update', $github_app)
<a href="{{ $this->getGithubAppNameUpdatePath() }}" class="button">
Rename
<x-external-link />
</a>
<a href="{{ getInstallationPath($github_app) }}" class="button">
Repositories
<x-external-link />
</a>
@endcan
</x-slot:actions>
<div class="grid gap-4 lg:grid-cols-2">
<x-forms.input canGate="update" :canResource="$github_app" id="name" label="App name" />
<x-forms.input canGate="update" :canResource="$github_app" id="organization"
label="Organization" placeholder="Personal account when empty" />
@if (!isCloud())
<div class="lg:col-span-2">
<x-forms.listbox id="isSystemWide" label="Availability" :options="[
['value' => false, 'label' => 'Only this team'],
['value' => true, 'label' => 'Every team on this instance'],
]"
helper="System-wide GitHub Apps can be used by every team on this Coolify instance."
:disabled="!auth()->user()->can('update', $github_app)" />
</div>
@if ($isSystemWide)
<div class="lg:col-span-2">
<x-callout type="warning" title="Shared with every team">
Use team-specific GitHub Apps when you need repository isolation between teams.
</x-callout>
</div>
@endif
@endif
<x-forms.input canGate="update" :canResource="$github_app" id="htmlUrl"
label="HTML URL" />
<x-forms.input canGate="update" :canResource="$github_app" id="apiUrl"
label="API URL" />
<x-forms.input canGate="update" :canResource="$github_app" id="customUser"
label="User" required />
<x-forms.input canGate="update" :canResource="$github_app" type="number"
id="customPort" label="Port" required />
<x-forms.input canGate="update" :canResource="$github_app" type="number" id="appId"
label="App ID" required />
<x-forms.input canGate="update" :canResource="$github_app" type="number"
id="installationId" label="Installation ID" required />
<x-forms.input canGate="update" :canResource="$github_app" id="clientId"
label="Client ID" type="password" required />
<x-forms.input canGate="update" :canResource="$github_app" id="clientSecret"
label="Client secret" type="password" required />
<x-forms.input canGate="update" :canResource="$github_app" id="webhookSecret"
label="Webhook secret" type="password" required />
<x-forms.listbox id="privateKeyId" label="Private key" required
:options="$privateKeyOptions" :disabled="!auth()->user()->can('update', $github_app)" />
</div>
</x-application.settings-section>
</form>
@elseif ($activeTab === 'danger')
<div class="application-settings-form">
<x-application.settings-section id="github-app-danger-section" title="Danger zone"
helper="Destructive actions for this GitHub App source cannot be undone.">
<div
class="rounded-lg border border-red-300 bg-red-50 p-4 ring-1 ring-inset ring-red-200/60 dark:border-error/30 dark:bg-error/[0.08] dark:ring-error/10">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<h4 class="text-sm font-semibold text-red-700 dark:text-error">Delete GitHub App</h4>
<x-status-badge status="Permanent" type="error" />
</div>
<p class="mt-2 max-w-2xl text-[13px] leading-5 text-neutral-600 dark:text-fg-dim">
Permanently delete
<strong class="font-semibold text-black dark:text-fg">{{ $name ?: 'this GitHub App' }}</strong>
from Coolify. Applications using this source will need another Git provider configured.
</p>
<ul class="mt-3 space-y-1 text-xs text-neutral-500 dark:text-fg-dim">
<li>• The App registration on GitHub is not removed automatically.</li>
<li>• Linked applications keep their Git settings until you change them.</li>
<li>• This source cannot be restored from Coolify after deletion.</li>
</ul>
</div>
<div class="shrink-0">
@can('delete', $github_app)
<x-modal-confirmation title="Confirm GitHub App Deletion?" isErrorButton
buttonTitle="Delete" submitAction="delete"
:actions="['The selected GitHub App will be permanently deleted.']"
confirmationText="{{ data_get($github_app, 'name') }}"
confirmationLabel="Please confirm the execution of the actions by entering the GitHub App Name below"
shortConfirmationLabel="GitHub App Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
@else
<x-forms.button disabled tooltip="You do not have permission to delete this GitHub App.">
Delete
</x-forms.button>
@endcan
</div>
</div>
</div>
@cannot('delete', $github_app)
<div class="mt-4">
<x-callout type="danger" title="Insufficient permissions">
Contact a team administrator if this GitHub App must be deleted.
</x-callout>
</div>
@endcannot
</x-application.settings-section>
</div>
@endif
</div>
</div>
</section>
@elseif ($activeTab === 'permissions')
<div class="application-settings-form">
<x-application.settings-section title="Permissions"
description="GitHub permissions currently granted to this App.">
<x-slot:actions>
@can('view', $github_app)
<x-forms.button type="button" wire:click.prevent="checkPermissions">
<x-reicon name="refresh" class="size-3.5" />
Refetch
</x-forms.button>
<a href="{{ getPermissionsPath($github_app) }}" class="button">
Update on GitHub
<x-external-link />
</a>
@endcan
</x-slot:actions>
<div class="grid gap-4 lg:grid-cols-3">
<x-forms.input canGate="view" :canResource="$github_app" id="contents"
helper="Read access is mandatory." label="Contents" readonly placeholder="N/A" />
<x-forms.input canGate="view" :canResource="$github_app" id="metadata"
helper="Read access is mandatory." label="Metadata" readonly placeholder="N/A" />
<x-forms.input canGate="view" :canResource="$github_app" id="pullRequests"
helper="Write access is needed for preview deployment status updates."
label="Pull requests" readonly placeholder="N/A" />
</div>
</x-application.settings-section>
</div>
</div>
<div class="flex items-center justify-center min-h-[calc(100vh-12rem)]">
<div class="mx-auto grid w-full max-w-5xl grid-cols-1 gap-4 lg:grid-cols-2">
@can('create', $github_app)
<section class="box-without-bg flex-col gap-4 p-6 h-full transition-all duration-200"
@else
<div x-data="{ search: '' }" class="application-settings-form">
<x-application.settings-section title="Resources"
description="Applications currently using this GitHub App." flush>
@if ($applications->isEmpty())
<x-empty title="No resources use this source"
description="Applications will appear here after this GitHub App is selected as their source."
icon-name="sources" size="sm" />
@else
<div class="border-b border-neutral-200 p-3 dark:border-white/[0.08]">
<div class="relative w-full max-w-sm">
<x-reicon name="search"
class="pointer-events-none absolute top-1/2 left-2.5 z-10 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-fg-faint" />
<input x-model.debounce.150ms="search" type="search"
placeholder="Search resources"
class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-3! pl-8! text-[12px]! shadow-none! placeholder:text-neutral-400 focus:border-accent! focus:ring-0! dark:border-white/[0.08]! dark:bg-white/[0.035]! dark:text-fg! dark:placeholder:text-fg-faint">
</div>
</div>
<div class="overflow-x-auto">
<div
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
<div>Project</div>
<div>Environment</div>
<div>Resource</div>
<div>Type</div>
</div>
@foreach ($applications->sortBy('name', SORT_NATURAL) as $resource)
@php
$projectName = (string) data_get($resource->project(), 'name');
$environmentName = (string) data_get($resource, 'environment.name');
$resourceName = (string) $resource->name;
$resourceType = (string) str($resource->type())->headline();
$searchValue = strtolower(
$projectName.' '.$environmentName.' '.$resourceName.' '.$resourceType,
);
@endphp
<a {{ wireNavigate() }} href="{{ $resource->link() }}"
x-show="search === '' || '{{ addslashes($searchValue) }}'.includes(search.toLowerCase())"
class="grid min-h-13 min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] transition-colors last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
<span class="truncate text-neutral-500 dark:text-fg-dim">{{ $projectName }}</span>
<span class="truncate text-neutral-500 dark:text-fg-dim">{{ $environmentName }}</span>
<span class="truncate font-medium text-black dark:text-fg">{{ $resourceName }}</span>
<span class="text-neutral-500 dark:text-fg-dim">{{ $resourceType }}</span>
</a>
@endforeach
</div>
@endif
</x-application.settings-section>
</div>
@endif
@else
<header class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0">
<h1 class="truncate text-[24px]! leading-7! font-semibold! tracking-tight!">
{{ $name ?: 'GitHub App' }}
</h1>
<p class="mt-1 text-[13px] text-neutral-500 dark:text-fg-dim">
Finish registering this GitHub App before using it as a source
</p>
</div>
@can('delete', $github_app)
<div class="shrink-0">
<x-modal-confirmation title="Confirm GitHub App Deletion?" isErrorButton
buttonTitle="Delete" submitAction="delete"
:actions="['The selected GitHub App will be permanently deleted.']"
confirmationText="{{ data_get($github_app, 'name') }}"
confirmationLabel="Please confirm the execution of the actions by entering the GitHub App Name below"
shortConfirmationLabel="GitHub App Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
</div>
@endcan
</header>
@can('create', $github_app)
@php
$endpointOptions = collect([
$fqdn ? ['value' => $fqdn, 'label' => 'Use '.$fqdn] : null,
$ipv4 ? ['value' => $ipv4, 'label' => 'Use '.$ipv4] : null,
$ipv6 ? ['value' => $ipv6, 'label' => 'Use '.$ipv6] : null,
config('app.url')
? ['value' => config('app.url'), 'label' => 'Use '.config('app.url')]
: null,
])->filter()->values()->all();
@endphp
<div class="grid gap-4 lg:grid-cols-2">
<div class="application-settings-form"
x-data="{
webhookEndpoint: $wire.entangle('webhook_endpoint').live,
useCustomWebhookEndpoint: $wire.entangle('use_custom_webhook_endpoint').live,
customWebhookEndpoint: $wire.entangle('custom_webhook_endpoint').live,
}">
<div class="flex flex-col gap-4 text-left h-full">
<div class="flex items-center justify-between">
<svg class="size-10" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
</svg>
<span
class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:bg-warning/20 text-coollabs dark:text-warning rounded">
Recommended
</span>
</div>
<div>
<h3 class="text-xl font-bold mb-2">Automated Installation</h3>
<p class="text-sm dark:text-neutral-400">
Register a GitHub App via GitHub's manifest flow. Permissions and webhooks are pre-configured.
</p>
</div>
<div class="flex flex-col gap-3 pt-4 border-t border-neutral-200 dark:border-coolgray-400">
<x-application.settings-section title="Automated installation"
description="Register through GitHub's manifest flow with permissions and webhooks preconfigured.">
<x-slot:actions>
<x-status-badge label="Recommended" type="success" />
</x-slot:actions>
<div class="flex min-h-[24rem] flex-col gap-4">
@if (!isCloud() || isDev())
<x-forms.checkbox canGate="create" :canResource="$github_app"
x-model="useCustomWebhookEndpoint" id="use_custom_webhook_endpoint"
label="Use custom webhook endpoint"
helper="Enable this when the public URL GitHub should call differs from Coolify's configured URL, for example behind Cloudflare Tunnel." />
<x-forms.listbox id="use_custom_webhook_endpoint" label="Webhook endpoint"
:live="true" :options="[
['value' => false, 'label' => 'Use an instance endpoint'],
['value' => true, 'label' => 'Use a custom endpoint'],
]"
x-model="useCustomWebhookEndpoint"
helper="Use a custom public URL when Coolify is behind a tunnel or reverse proxy." />
<div x-show="!useCustomWebhookEndpoint">
<x-forms.select canGate="create" :canResource="$github_app"
wire:model.live='webhook_endpoint' x-model="webhookEndpoint"
label="Selected endpoint"
helper="GitHub will use this endpoint unless custom mode is enabled.">
@if ($fqdn)
<option value="{{ $fqdn }}">Use {{ $fqdn }}</option>
@endif
@if ($ipv4)
<option value="{{ $ipv4 }}">Use {{ $ipv4 }}</option>
@endif
@if ($ipv6)
<option value="{{ $ipv6 }}">Use {{ $ipv6 }}</option>
@endif
@if (config('app.url'))
<option value="{{ config('app.url') }}">Use {{ config('app.url') }}</option>
@endif
</x-forms.select>
<x-forms.listbox id="webhook_endpoint" label="Instance endpoint"
:options="$endpointOptions" x-model="webhookEndpoint" />
</div>
<div x-cloak x-show="useCustomWebhookEndpoint">
<x-forms.input canGate="create" :canResource="$github_app"
x-model="customWebhookEndpoint" id="custom_webhook_endpoint" type="url"
label="Custom endpoint" placeholder="https://coolify.example.com"
helper="GitHub will use this custom public URL. Do not include /webhooks." />
helper="Do not include /webhooks." />
</div>
@else
<div class="text-sm dark:text-neutral-400">You need to register a GitHub App before using this source.</div>
<p class="text-[12px] leading-5 text-neutral-500 dark:text-fg-dim">
Register the GitHub App before using this source.
</p>
@endif
<div class="flex w-full flex-col gap-2">
<x-forms.checkbox canGate="create" :canResource="$github_app" disabled id="default_permissions" label="Mandatory"
helper="Contents: read<br>Metadata: read<br>Email: read" />
<x-forms.checkbox canGate="create" :canResource="$github_app" id="preview_deployment_permissions" label="Preview Deployments"
helper="Necessary for updating pull requests with useful comments (deployment status, links, etc.)<br><br>Pull Request: read & write" />
<div
class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[12px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
<p class="font-medium text-black dark:text-fg">Mandatory permissions</p>
<p class="mt-1">Contents: read · Metadata: read · Email: read</p>
</div>
</div>
<div class="mt-auto pt-2">
<x-forms.button canGate="create" :canResource="$github_app" class="w-full justify-center" isHighlighted
<x-forms.listbox id="preview_deployment_permissions"
label="Preview deployment access" :options="[
['value' => false, 'label' => 'Do not update pull requests'],
['value' => true, 'label' => 'Read and update pull requests'],
]"
helper="Write access lets Coolify post deployment status and links on pull requests." />
<button type="button"
class="button mt-auto w-full justify-center bg-coollabs/10! text-coollabs! ring-1 ring-coollabs/25 hover:bg-coollabs/15! dark:bg-warning/15! dark:text-warning! dark:ring-warning/25 dark:hover:bg-warning/20!"
x-on:click.prevent="createGithubApp(webhookEndpoint, useCustomWebhookEndpoint, customWebhookEndpoint, {{ Illuminate\Support\Js::from($preview_deployment_permissions) }}, {{ Illuminate\Support\Js::from($administration) }})">
Register Now
</x-forms.button>
Register with GitHub
</button>
</div>
</div>
</section>
<section class="box-without-bg flex-col gap-4 p-6 h-full transition-all duration-200">
<div class="flex flex-col gap-4 text-left h-full">
<div class="flex items-center justify-between">
<svg class="size-10" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z" />
</svg>
<span
class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-neutral-100 dark:bg-coolgray-300 dark:text-neutral-400 rounded">
Advanced
</span>
</div>
<div>
<h3 class="text-xl font-bold mb-2">Manual Installation</h3>
<p class="text-sm dark:text-neutral-400">
Fill the GitHub App form manually. For self-hosted GitHub Enterprise or custom permission setups.
</p>
</div>
<div class="mt-auto pt-2">
<x-forms.button canGate="create" :canResource="$github_app" class="w-full justify-center" wire:click.prevent="createGithubAppManually">
Continue
</x-forms.button>
</div>
</div>
</section>
@else
<div class="pb-10">
<x-callout type="danger" title="Insufficient Permissions">
You don't have permission to create new GitHub Apps. Please contact your team administrator.
</x-callout>
</x-application.settings-section>
</div>
@endcan
</div>
</div>
<script>
function createGithubApp(webhook_endpoint, use_custom_webhook_endpoint, custom_webhook_endpoint, preview_deployment_permissions, administration) {
const {
organization,
html_url
} = @js($github_app->only(['organization', 'html_url']));
const selectedEndpoint = webhook_endpoint ? webhook_endpoint.trim() : '';
const customEndpoint = custom_webhook_endpoint ? custom_webhook_endpoint.trim() : '';
if (use_custom_webhook_endpoint && !customEndpoint) {
alert('Please enter a custom webhook endpoint.');
return;
}
if (!use_custom_webhook_endpoint && !selectedEndpoint) {
alert('Please enter a webhook endpoint.');
return;
}
let baseUrl = (use_custom_webhook_endpoint ? customEndpoint : selectedEndpoint).replace(/\/+$/, '');
const name = @js($name);
const manifestState = @js($manifestState);
const isDev = @js(config('app.env')) ===
'local';
const devWebhook = @js(config('constants.webhooks.dev_webhook'));
if (isDev && devWebhook) {
baseUrl = devWebhook;
}
const webhookBaseUrl = `${baseUrl}/webhooks`;
const organizationPath = organization ? encodeURIComponent(organization.replace(/^\/+|\/+$/g, '')) : '';
const path = organizationPath ? `organizations/${organizationPath}/settings/apps/new` : 'settings/apps/new';
const default_permissions = {
contents: 'read',
metadata: 'read',
emails: 'read',
administration: 'read'
};
const default_events = ['push'];
if (preview_deployment_permissions) {
default_permissions.pull_requests = 'write';
default_events.push('pull_request');
}
if (administration) {
default_permissions.administration = 'write';
}
const data = {
name,
url: baseUrl,
hook_attributes: {
url: `${webhookBaseUrl}/source/github/events`,
active: true,
},
redirect_url: `${webhookBaseUrl}/source/github/redirect`,
callback_urls: [`${baseUrl}/login/github/app`],
public: false,
request_oauth_on_install: false,
setup_url: `${webhookBaseUrl}/source/github/install`,
setup_on_update: true,
default_permissions,
default_events
};
const form = document.createElement('form');
form.setAttribute('method', 'post');
form.setAttribute('action', `${html_url}/${path}?state=${manifestState}`);
const input = document.createElement('input');
input.setAttribute('id', 'manifest');
input.setAttribute('name', 'manifest');
input.setAttribute('type', 'hidden');
input.setAttribute('value', JSON.stringify(data));
form.appendChild(input);
document.getElementsByTagName('body')[0].appendChild(form);
form.submit();
<div class="application-settings-form">
<x-application.settings-section title="Manual installation"
description="Enter GitHub App credentials manually for GitHub Enterprise or custom permission sets.">
<x-slot:actions>
<x-status-badge label="Advanced" type="neutral" />
</x-slot:actions>
<div class="flex min-h-[24rem] flex-col">
<div
class="flex size-10 items-center justify-center rounded-xl border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
<x-reicon name="settings" class="size-5" />
</div>
<p class="mt-4 max-w-md text-[12px] leading-5 text-neutral-500 dark:text-fg-dim">
Use this path when the automated GitHub manifest flow is unavailable or you need
complete control over the App configuration.
</p>
<button type="button" class="button mt-auto w-fit"
wire:click.prevent="createGithubAppManually">
Continue manually
<x-reicon name="arrow-right" class="size-3.5" />
</button>
</div>
</x-application.settings-section>
</div>
</div>
@else
<x-callout type="danger" title="Insufficient permissions">
You do not have permission to create GitHub Apps. Contact your team administrator.
</x-callout>
@endcan
<script>
function createGithubApp(webhook_endpoint, use_custom_webhook_endpoint, custom_webhook_endpoint,
preview_deployment_permissions, administration) {
const {
organization,
html_url
} = @js($github_app->only(['organization', 'html_url']));
const selectedEndpoint = webhook_endpoint ? webhook_endpoint.trim() : '';
const customEndpoint = custom_webhook_endpoint ? custom_webhook_endpoint.trim() : '';
if (use_custom_webhook_endpoint && !customEndpoint) {
alert('Please enter a custom webhook endpoint.');
return;
}
</script>
if (!use_custom_webhook_endpoint && !selectedEndpoint) {
alert('Please enter a webhook endpoint.');
return;
}
let baseUrl = (use_custom_webhook_endpoint ? customEndpoint : selectedEndpoint).replace(/\/+$/, '');
const name = @js($name);
const manifestState = @js($manifestState);
const isDev = @js(config('app.env')) === 'local';
const devWebhook = @js(config('constants.webhooks.dev_webhook'));
if (isDev && devWebhook) {
baseUrl = devWebhook;
}
const webhookBaseUrl = `${baseUrl}/webhooks`;
const organizationPath = organization ? encodeURIComponent(organization.replace(/^\/+|\/+$/g, '')) : '';
const path = organizationPath ? `organizations/${organizationPath}/settings/apps/new` : 'settings/apps/new';
const default_permissions = {
contents: 'read',
metadata: 'read',
emails: 'read',
administration: 'read'
};
const default_events = ['push'];
if (preview_deployment_permissions) {
default_permissions.pull_requests = 'write';
default_events.push('pull_request');
}
if (administration) {
default_permissions.administration = 'write';
}
const data = {
name,
url: baseUrl,
hook_attributes: {
url: `${webhookBaseUrl}/source/github/events`,
active: true,
},
redirect_url: `${webhookBaseUrl}/source/github/redirect`,
callback_urls: [`${baseUrl}/login/github/app`],
public: false,
request_oauth_on_install: false,
setup_url: `${webhookBaseUrl}/source/github/install`,
setup_on_update: true,
default_permissions,
default_events
};
const form = document.createElement('form');
form.setAttribute('method', 'post');
form.setAttribute('action', `${html_url}/${path}?state=${manifestState}`);
const input = document.createElement('input');
input.setAttribute('id', 'manifest');
input.setAttribute('name', 'manifest');
input.setAttribute('type', 'hidden');
input.setAttribute('value', JSON.stringify(data));
form.appendChild(input);
document.getElementsByTagName('body')[0].appendChild(form);
form.submit();
}
</script>
@endif
</div>
@@ -1,69 +1,59 @@
@can('createAnyResource')
<form wire:submit='createGitHubApp' class="flex flex-col w-full gap-2">
<div class="pb-2">This is required, if you would like to get full integration (commit / pull request
deployments, etc)
with GitHub.</div>
<div class="flex gap-2">
<form wire:submit="createGitHubApp" class="flex w-full flex-col gap-4">
<p class="text-[12px] leading-5 text-neutral-500 dark:text-fg-dim">
Connect a GitHub App for private repositories, webhooks, and commit / pull request deployments.
</p>
<div class="grid gap-4 sm:grid-cols-2">
<x-forms.input id="name" label="Name" required />
<x-forms.input helper="If empty, your GitHub user will be used."
placeholder="If empty, your GitHub user will be used." id="organization" label="Organization (on GitHub)" />
<x-forms.input id="organization" label="Organization"
helper="If empty, your GitHub user will be used."
placeholder="Personal account when empty" />
</div>
@if (!isCloud())
@if (! isCloud())
<div x-data="{ showWarning: @entangle('is_system_wide') }">
<div class="w-48">
<x-forms.checkbox id="is_system_wide" label="System Wide"
<div class="max-w-xs">
<x-forms.checkbox id="is_system_wide" label="System wide"
helper="If checked, this GitHub App will be available for everyone in this Coolify instance." />
</div>
<div x-show="showWarning" x-transition x-cloak class="w-full max-w-2xl mx-auto pt-2">
<x-callout type="warning" title="Not Recommended">
<div class="whitespace-normal break-words">
System-wide GitHub Apps are shared across all teams on this Coolify instance. This means any team
can use this GitHub App to deploy applications from your repositories. For better security and
isolation, it's recommended to create team-specific GitHub Apps instead.
</div>
<div x-cloak x-show="showWarning" x-transition class="mt-3">
<x-callout type="warning" title="Shared with every team">
System-wide GitHub Apps are available to every team on this instance. Prefer team-specific apps when you need repository isolation.
</x-callout>
</div>
</div>
@endif
<div x-data="{
activeAccordion: '',
setActiveAccordion(id) {
this.activeAccordion = (this.activeAccordion == id) ? '' : id
}
}" class="relative w-full py-2 mx-auto overflow-hidden text-sm font-normal rounded-md">
<div x-data="{ id: $id('accordion') }" class="cursor-pointer">
<button @click="setActiveAccordion(id)"
class="flex items-center justify-between w-full px-1 py-2 text-left select-none dark:hover:text-white hover:bg-white/5"
type="button">
<h4>Self-hosted / Enterprise GitHub</h4>
<svg class="w-4 h-4 duration-200 ease-out" :class="{ 'rotate-180': activeAccordion == id }"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div x-show="activeAccordion==id" x-collapse x-cloak class="px-2">
<div class="flex flex-col gap-2 pt-0 opacity-70">
<div class="flex gap-2">
<x-forms.input id="html_url" label="HTML Url" required />
<x-forms.input id="api_url" label="API Url" required />
</div>
<div class="flex gap-2">
<x-forms.input id="custom_user" label="Custom Git User" required />
<x-forms.input id="custom_port" type="number" label="Custom Git Port" required />
</div>
</div>
open: false,
}" class="rounded-lg border border-neutral-200 dark:border-white/[0.08]">
<button type="button" @click="open = !open"
class="flex w-full items-center justify-between px-3 py-2.5 text-left text-[12px] font-medium text-neutral-700 transition-colors hover:bg-neutral-50 dark:text-fg-dim dark:hover:bg-white/[0.03]">
Self-hosted / Enterprise GitHub
<svg class="size-3.5 transition-transform" :class="{ 'rotate-180': open }" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div x-cloak x-show="open" x-collapse class="border-t border-neutral-200 px-3 py-3 dark:border-white/[0.08]">
<div class="grid gap-4 sm:grid-cols-2">
<x-forms.input id="html_url" label="HTML URL" required
helper="For GitHub Enterprise, enter your instance URL (e.g. https://github.example.com)." />
<x-forms.input id="api_url" label="API URL" required
helper="Usually https://api.github.com or your Enterprise API base URL." />
<x-forms.input id="custom_user" label="Custom Git user" required />
<x-forms.input id="custom_port" type="number" label="Custom Git port" required />
</div>
</div>
</div>
<x-forms.button class="mt-4" type="submit">
<x-forms.button class="mt-1 w-full justify-center" type="submit">
Continue
</x-forms.button>
</form>
@else
<x-callout type="danger" title="Insufficient Permissions">
You don't have permission to create new GitHub Apps. Please contact your team administrator for access.
<x-callout type="danger" title="Insufficient permissions">
You don't have permission to create new GitHub Apps. Contact your team administrator.
</x-callout>
@endcan
@endcan