From f2b5c31cb41e1a1ea45c6275b01635ef428f90d5 Mon Sep 17 00:00:00 2001 From: Daniel Togey Date: Sun, 25 Jan 2026 16:41:00 +0300 Subject: [PATCH] feat(github): Update GitHub installation handling and enhance UI for repository selection - Corrected the GitHub Apps installation URL format in the API handler. - Improved post-installation logic to dynamically determine the web URL, enhancing reliability. - Added a new button handler for connecting GitHub instances, including error handling and redirection. - Refactored repository picker to support multiple providers with a searchable select interface. - Enhanced user experience by providing loading states and clearer messaging in the UI. --- internal/api/handlers/github.go | 31 +++++- web/assets/js/ui-polish.js | 182 ++++++++++++++++++++++++-------- web/pages/apps/detail.templ | 22 +++- 3 files changed, 185 insertions(+), 50 deletions(-) diff --git a/internal/api/handlers/github.go b/internal/api/handlers/github.go index f0785c7..16cf190 100644 --- a/internal/api/handlers/github.go +++ b/internal/api/handlers/github.go @@ -256,7 +256,8 @@ func (h *GitHubHandler) ManifestCallback(w http.ResponseWriter, r *http.Request) } // Seamlessly redirect to the installation page on GitHub - installURL := fmt.Sprintf("https://github.com/settings/apps/%s/installations/new", *config.Slug) + // Use the correct GitHub Apps installation URL format + installURL := fmt.Sprintf("https://github.com/apps/%s/installations/new", *config.Slug) http.Redirect(w, r, installURL, http.StatusFound) } @@ -401,7 +402,8 @@ func (h *GitHubHandler) AppInstall(w http.ResponseWriter, r *http.Request) { return } - installURL := fmt.Sprintf("https://github.com/settings/apps/%s/installations/new", *config.Slug) + // Use the correct GitHub Apps installation URL format + installURL := fmt.Sprintf("https://github.com/apps/%s/installations/new", *config.Slug) WriteJSON(w, http.StatusOK, map[string]string{"url": installURL}) } @@ -414,10 +416,31 @@ func (h *GitHubHandler) PostInstallation(w http.ResponseWriter, r *http.Request) h.logger.Info("GitHub post-installation callback", "installation_id", installationIDStr, "setup_action", setupAction, + "host", r.Host, + "x-forwarded-host", r.Header.Get("X-Forwarded-Host"), ) + // Get the correct web URL from environment or settings + // Don't rely on request headers as GitHub is calling this endpoint + ctx := r.Context() + webURL := os.Getenv("PUBLIC_WEB_URL") + if webURL == "" { + // Try database settings + if dbDomain, _ := h.store.Settings().Get(ctx, "server_domain"); dbDomain != "" && dbDomain != "localhost" { + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + webURL = fmt.Sprintf("%s://%s:8090", scheme, dbDomain) + } else { + // Fallback - try to detect from request but filter internal hostnames + webURL, _ = h.getBaseURLs(r) + } + } + + h.logger.Info("redirecting to web URL", "url", webURL) + if installationIDStr == "" { - webURL, _ := h.getBaseURLs(r) http.Redirect(w, r, webURL+"/git?error=missing_installation_id", http.StatusFound) return } @@ -434,7 +457,6 @@ func (h *GitHubHandler) PostInstallation(w http.ResponseWriter, r *http.Request) h.logger.Info("no user context in post-installation, storing orphaned installation", "installation_id", installationID) // We could store this in a temporary table or skip for now // For now, just redirect to login - webURL, _ := h.getBaseURLs(r) http.Redirect(w, r, webURL+"/git?success=GitHub+App+installed&installation_id="+installationIDStr, http.StatusFound) return } @@ -450,7 +472,6 @@ func (h *GitHubHandler) PostInstallation(w http.ResponseWriter, r *http.Request) h.logger.Error("failed to create installation", "error", err) } - webURL, _ := h.getBaseURLs(r) http.Redirect(w, r, webURL+"/git?success=GitHub+App+installed", http.StatusFound) } diff --git a/web/assets/js/ui-polish.js b/web/assets/js/ui-polish.js index e7ff220..577f127 100644 --- a/web/assets/js/ui-polish.js +++ b/web/assets/js/ui-polish.js @@ -63,11 +63,8 @@ async function initGitIntegration() { const gitSection = document.getElementById('git-connection-section'); if (!gitSection) { - // Check if we are in the detail page with a repo picker - const repoPicker = document.getElementById('github-repo-picker'); - if (repoPicker) { - loadGithubRepos(); - } + // Check if we are in the detail page with repo pickers + initRepoPickers(); return; } @@ -211,6 +208,39 @@ } // Reconfigure logic + // Add Installation button handler + const btnAddInstallation = document.getElementById('btn-connect-github-instance'); + if (btnAddInstallation) { + btnAddInstallation.addEventListener('click', async () => { + btnAddInstallation.disabled = true; + const originalHTML = btnAddInstallation.innerHTML; + btnAddInstallation.innerHTML = ' Loading...'; + + try { + const response = await fetch('/api/github/install'); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + const data = await response.json(); + + if (data.url) { + // Redirect to GitHub installation page + window.location.href = data.url; + } else if (data.error) { + throw new Error(data.error); + } else { + throw new Error('No installation URL returned'); + } + } catch (err) { + console.error('Add installation failed:', err); + alert('Error: ' + err.message); + btnAddInstallation.disabled = false; + btnAddInstallation.innerHTML = originalHTML; + } + }); + } + + // Reconfigure button handler const btnReconfigure = document.getElementById('btn-reconfigure-github'); if (btnReconfigure) { btnReconfigure.addEventListener('click', async () => { @@ -238,68 +268,134 @@ }); } - async function loadGithubRepos() { - const repoPicker = document.getElementById('github-repo-picker'); - const manualInput = document.getElementById('manual-repo-input'); - const hiddenRepo = document.getElementById('selected-github-repo'); - if (!repoPicker) return; + // Initialize repository pickers for service creation dialogs + function initRepoPickers() { + const prefixes = ['web', 'static', 'worker']; + + prefixes.forEach(prefix => { + const repoPicker = document.getElementById(`${prefix}-repo-picker`); + if (repoPicker) { + loadReposForPicker(prefix); + } + }); + + // Listen for provider tab switches + document.querySelectorAll('[data-provider-tab]').forEach(tab => { + tab.addEventListener('click', function() { + const prefix = this.getAttribute('data-provider-tab').split('-')[0]; + const provider = this.getAttribute('data-provider'); + + // Update active tab styling + document.querySelectorAll(`[data-provider-tab^="${prefix}-"]`).forEach(t => { + t.classList.remove('bg-zinc-800', 'text-white'); + t.classList.add('text-muted-foreground'); + }); + this.classList.add('bg-zinc-800', 'text-white'); + this.classList.remove('text-muted-foreground'); + + // Load repos for the selected provider + loadReposForPicker(prefix, provider); + }); + }); + } + + async function loadReposForPicker(prefix, provider = 'github') { + const selectId = `${prefix}_repo_select`; + const contentZone = document.querySelector(`#${selectId} [data-tui-selectbox-content]`); + const hiddenInput = document.getElementById(`${prefix}_selected_repo`); + const repoInput = document.getElementById(`${prefix}_svc_repo`); + + if (!contentZone) return; + + // Show loading state + contentZone.innerHTML = '
Loading repositories...
'; try { - const resp = await fetch('/api/github/repos'); + const resp = await fetch(`/api/${provider}/repos`); if (!resp.ok) { - const contentZone = document.querySelector('#github_repo_select [data-tui-selectbox-content]'); - if (contentZone) contentZone.innerHTML = '
Failed to load repositories
'; - return; + throw new Error(`Failed to load repositories: ${resp.statusText}`); } const repos = await resp.json(); - const contentZone = document.querySelector('#github_repo_select [data-tui-selectbox-content]'); - if (!contentZone) return; - if (repos && repos.length > 0) { - repoPicker.classList.remove('hidden'); - if (manualInput) manualInput.classList.add('opacity-50'); - contentZone.innerHTML = ''; + + // Add search input + const searchWrapper = document.createElement('div'); + searchWrapper.className = 'p-2 border-b border-zinc-800'; + searchWrapper.innerHTML = ` + + `; + contentZone.appendChild(searchWrapper); + + // Add scrollable container + const scrollContainer = document.createElement('div'); + scrollContainer.className = 'max-h-[300px] overflow-y-auto'; + scrollContainer.setAttribute('data-repo-list', prefix); + repos.forEach(repo => { const item = document.createElement('div'); - item.className = 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'; + item.className = 'relative flex w-full cursor-default select-none items-center rounded-sm py-2 pl-8 pr-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'; item.setAttribute('data-tui-selectbox-item', 'true'); - item.setAttribute('data-value', repo.full_name); + item.setAttribute('data-value', repo.clone_url || repo.html_url); + item.setAttribute('data-repo-name', repo.full_name.toLowerCase()); item.innerHTML = ` - + ${repo.full_name} - ${repo.description || 'No description'} + ${repo.description ? `${repo.description}` : ''} `; - contentZone.appendChild(item); + scrollContainer.appendChild(item); + }); + + contentZone.appendChild(scrollContainer); + + // Setup search functionality + const searchInput = searchWrapper.querySelector('input'); + searchInput.addEventListener('input', function() { + const query = this.value.toLowerCase(); + const items = scrollContainer.querySelectorAll('[data-repo-name]'); + items.forEach(item => { + const name = item.getAttribute('data-repo-name'); + if (name.includes(query)) { + item.style.display = ''; + } else { + item.style.display = 'none'; + } + }); + }); + + // Setup selection handler + scrollContainer.addEventListener('click', function(e) { + const item = e.target.closest('[data-tui-selectbox-item]'); + if (item) { + const value = item.getAttribute('data-value'); + if (hiddenInput) hiddenInput.value = value; + if (repoInput) repoInput.value = value; + + // Update trigger text + const trigger = document.querySelector(`#${selectId} [data-tui-selectbox-value]`); + if (trigger) { + trigger.textContent = item.querySelector('.font-medium').textContent; + } + } }); } else { - contentZone.innerHTML = '
No repositories found
'; + contentZone.innerHTML = '
No repositories found. Connect a Git provider first.
'; } } catch (err) { - console.error('Error loading GitHub repos:', err); - const contentZone = document.querySelector('#github_repo_select [data-tui-selectbox-content]'); - if (contentZone) contentZone.innerHTML = '
Error loading repositories
'; + console.error('Error loading repos:', err); + contentZone.innerHTML = `
Error: ${err.message}
`; } } - - // Listen for repo selection - document.addEventListener('change', function (e) { - const target = e.target; - if (target && target.hasAttribute('data-tui-selectbox-hidden-input')) { - const selectContainer = target.closest('#github_repo_select'); - const hiddenRepo = document.getElementById('selected-github-repo'); - if (selectContainer && hiddenRepo) { - hiddenRepo.value = target.value; - const manualInputEl = document.getElementById('svc_repo'); - if (manualInputEl) manualInputEl.value = target.value; - } - } - }); } // Simple confirmation dialogs for destructive actions diff --git a/web/pages/apps/detail.templ b/web/pages/apps/detail.templ index 1cea2db..9f7cef2 100644 --- a/web/pages/apps/detail.templ +++ b/web/pages/apps/detail.templ @@ -600,9 +600,27 @@ templ gitProviderSelector(data DetailData, prefix string) { } - // Repository picker placeholder (populated via JS) + // Repository picker (searchable select)
-

Loading repositories...

+ @label.Label(label.Props{For: prefix + "_repo_select", Class: "text-sm"}) { Repository } + @selectbox.SelectBox(selectbox.Props{ID: prefix + "_repo_select", Class: "w-full"}) { + @selectbox.Trigger() { + @selectbox.Value(selectbox.ValueProps{Placeholder: "Loading repositories..."}) + } + @selectbox.Content() { +
Loading...
+ } + } + +

+ Select a repository from your connected GitHub account +