From a5eb3d403ce996bb1232ca97c794c28c923c6589 Mon Sep 17 00:00:00 2001 From: Alexander Petric Date: Wed, 26 Mar 2025 23:41:10 -0400 Subject: [PATCH] gh app import export from other instance + ui improvements (#5518) * updating hub paths for git sync scripts supporting github app * removing accidentally committed file * gh app import export from other instance + ui improvements * update repo ref * sqlx + ellipsis --- ...f2062c36ee5dab3d4894ad4b2aa8b2f5a0db9.json | 23 --- backend/ee-repo-ref.txt | 2 +- backend/windmill-api/openapi.yaml | 57 ++++++ .../src/lib/components/ApiConnectForm.svelte | 184 +++++++++++++++--- 4 files changed, 215 insertions(+), 51 deletions(-) delete mode 100644 backend/.sqlx/query-dee32ce9c4010ae407b4d1ba2ecf2062c36ee5dab3d4894ad4b2aa8b2f5a0db9.json diff --git a/backend/.sqlx/query-dee32ce9c4010ae407b4d1ba2ecf2062c36ee5dab3d4894ad4b2aa8b2f5a0db9.json b/backend/.sqlx/query-dee32ce9c4010ae407b4d1ba2ecf2062c36ee5dab3d4894ad4b2aa8b2f5a0db9.json deleted file mode 100644 index bb8d339e55..0000000000 --- a/backend/.sqlx/query-dee32ce9c4010ae407b4d1ba2ecf2062c36ee5dab3d4894ad4b2aa8b2f5a0db9.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT EXISTS(SELECT 1 FROM log_file WHERE hostname = $1 AND log_ts = $2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Timestamp" - ] - }, - "nullable": [ - null - ] - }, - "hash": "dee32ce9c4010ae407b4d1ba2ecf2062c36ee5dab3d4894ad4b2aa8b2f5a0db9" -} diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 11f1ee4339..933b24a492 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -6406a96fb6d0ef473915529300a757057ee19306 +94cbac33756c1fba56da037456f44a4d294475b1 \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 960b07462a..96756fc9fd 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1381,6 +1381,63 @@ paths: '200': description: Installation successfully deleted + /w/{workspace}/github_app/export/{installationId}: + get: + summary: Export GitHub installation JWT token + description: Exports the JWT token for a specific GitHub installation in the workspace + operationId: exportInstallation + tags: + - Git Sync + parameters: + - name: workspace + in: path + required: true + schema: + type: string + - name: installationId + in: path + required: true + schema: + type: integer + responses: + '200': + description: Successfully exported the JWT token + content: + application/json: + schema: + type: object + properties: + jwt_token: + type: string + + /w/{workspace}/github_app/import: + post: + summary: Import GitHub installation from JWT token + description: Imports a GitHub installation from a JWT token exported from another instance + operationId: importInstallation + tags: + - Git Sync + parameters: + - name: workspace + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - jwt_token + properties: + jwt_token: + type: string + responses: + '200': + description: Successfully imported the installation + /users/accept_invite: post: summary: accept invite to workspace diff --git a/frontend/src/lib/components/ApiConnectForm.svelte b/frontend/src/lib/components/ApiConnectForm.svelte index f577e30202..32ee3e3cba 100644 --- a/frontend/src/lib/components/ApiConnectForm.svelte +++ b/frontend/src/lib/components/ApiConnectForm.svelte @@ -5,7 +5,7 @@ type ResourceType, type GetGlobalConnectedRepositoriesResponse } from '$lib/gen' - import { workspaceStore, enterpriseLicense, userWorkspaces } from '$lib/stores' + import { workspaceStore, enterpriseLicense, userWorkspaces, userStore } from '$lib/stores' import { base } from '$lib/base' import { emptySchema, emptyString } from '$lib/utils' import SchemaForm from './SchemaForm.svelte' @@ -16,7 +16,8 @@ import { sendUserToast } from '$lib/toast' import Popover from './meltComponents/Popover.svelte' import Button from './common/button/Button.svelte' - import { Loader2, Github, RotateCw, Plus, Minus } from 'lucide-svelte' + import { Loader2, Github, RotateCw, Plus, Minus, Download } from 'lucide-svelte' + import { onDestroy } from 'svelte' export let resourceType: string export let resourceTypeInfo: ResourceType | undefined @@ -36,6 +37,10 @@ let selectedGHAppAccountId: string | undefined = undefined let selectedGHAppRepository: string | undefined = undefined let githubInstallationUrl: string | undefined = undefined + let installationCheckInterval: number | undefined = undefined + let isCheckingInstallation = false + let importJwt = '' + let githubAppPopover: { open: () => void; close: () => void } | null = null async function loadGithubInstallations() { if (!$enterpriseLicense) return @@ -70,6 +75,37 @@ } } + function startInstallationCheck() { + isCheckingInstallation = true + installationCheckInterval = window.setInterval(async () => { + const installations = await GitSyncService.getGlobalConnectedRepositories() + if (installations.length > 0) { + stopInstallationCheck() + githubInstallations = installations + workspaceGithubInstallations = githubInstallations.filter( + (_) => _.workspace_id === $workspaceStore + ) + // Open the popover with a small delay as otherwise it doesn't open + setTimeout(() => { + githubAppPopover?.open() + }, 100) + } + }, 2000) + } + + function stopInstallationCheck() { + if (installationCheckInterval) { + clearInterval(installationCheckInterval) + installationCheckInterval = undefined + } + isCheckingInstallation = false + } + + // Clean up interval when component is destroyed + onDestroy(() => { + stopInstallationCheck() + }) + function getRepositories(accountId: string) { return githubInstallations.find((_) => _.account_id === accountId)?.repositories || [] } @@ -153,7 +189,7 @@ } $: resourceType == 'postgresql' && isSupabaseAvailable() - $: resourceType == 'git_repository' && loadGithubInstallations() + $: resourceType == 'git_repository' && $userStore?.is_admin && loadGithubInstallations() let connectionString = '' let validConnectionString = true @@ -235,6 +271,61 @@ sendUserToast('Failed to delete installation', true) } } + + async function exportInstallation(installationId: number) { + if (!$workspaceStore) { + sendUserToast('Failed to export installation', true) + return + } + try { + const response = await GitSyncService.exportInstallation({ + workspace: $workspaceStore, + installationId: installationId + }) + if (!response.jwt_token) { + sendUserToast('Failed to export installation', true) + return + } + // Copy to clipboard + await navigator.clipboard.writeText(response.jwt_token) + sendUserToast( + 'JWT token copied to clipboard. This token is sensitive and should be kept secret!', + false, + undefined, + undefined, + 10000 + ) + } catch (error) { + console.error(error) + sendUserToast('Failed to export installation', true) + } + } + + async function importInstallation(jwt: string) { + if (!$workspaceStore) { + sendUserToast('Failed to import installation', true) + return + } + try { + await GitSyncService.importInstallation({ + workspace: $workspaceStore, + requestBody: { jwt_token: jwt } + }) + importJwt = '' + sendUserToast('Installation imported successfully', false) + await loadGithubInstallations() + } catch (error) { + sendUserToast('Failed to import installation', true) + } + } + + function handleInstallClick() { + if (githubInstallations.length === 0) { + if (!isCheckingInstallation) { + startInstallationCheck() + } + } + } {#if !notFound} @@ -303,7 +394,7 @@
Connect Supabase
{/if} - {#if resourceType == 'git_repository' && $workspaceStore} + {#if resourceType == 'git_repository' && $workspaceStore && $userStore?.is_admin} {#if !loadingGithubInstallations} + + {/each} @@ -472,9 +577,9 @@ Org - Workspace - Repos - + Workspace + Repos + @@ -502,13 +607,12 @@ {installation.repositories.length} repos - + + + @@ -538,13 +663,18 @@ size="xs" disabled={!$enterpriseLicense || loadingGithubInstallations} startIcon={{ - icon: loadingGithubInstallations ? Loader2 : Github, - classes: loadingGithubInstallations ? 'animate-spin' : '' + icon: loadingGithubInstallations || isCheckingInstallation ? Loader2 : Github, + classes: loadingGithubInstallations || isCheckingInstallation ? 'animate-spin' : '' }} href={githubInstallationUrl} target="_blank" + on:click={handleInstallClick} > - {$enterpriseLicense ? 'Install GitHub App' : 'GitHub App (ee only)'} + {$enterpriseLicense + ? isCheckingInstallation + ? 'Waiting for installation...' + : 'Install GitHub App' + : 'GitHub App (ee only)'} {/if} {/if}