diff --git a/bun.lock b/bun.lock index 798c00a88a..ea221f77a8 100644 --- a/bun.lock +++ b/bun.lock @@ -1,12 +1,11 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@ed09983", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@56957ff", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", @@ -130,7 +129,7 @@ "@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="], - "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@ed09983", { "dependencies": { "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@56957ff", { "dependencies": { "json-bigint": "1.0.0" } }], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], diff --git a/package.json b/package.json index fd0ebc590d..e54f0e19d7 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@ed09983", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@56957ff", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", diff --git a/src/lib/components/git/IconOrigin.svelte b/src/lib/components/git/IconOrigin.svelte new file mode 100644 index 0000000000..184073d564 --- /dev/null +++ b/src/lib/components/git/IconOrigin.svelte @@ -0,0 +1,13 @@ + + + + diff --git a/src/lib/components/git/connectGit.svelte b/src/lib/components/git/connectGit.svelte index 6473f5f6d1..f9b0abe917 100644 --- a/src/lib/components/git/connectGit.svelte +++ b/src/lib/components/git/connectGit.svelte @@ -1,20 +1,14 @@ @@ -41,23 +35,16 @@ title="No installation was added to the project yet" description="Add an installation to connect repositories"> - - - {#if isGiteaEnabled} + + {#each providers as provider (provider.id)} - {/if} + {/each} diff --git a/src/lib/components/git/connectRepoModal.svelte b/src/lib/components/git/connectRepoModal.svelte index faca63d917..7247151062 100644 --- a/src/lib/components/git/connectRepoModal.svelte +++ b/src/lib/components/git/connectRepoModal.svelte @@ -13,7 +13,9 @@ import { Click, trackEvent } from '$lib/actions/analytics'; import RepositoryBehaviour from '$lib/components/git/repositoryBehaviour.svelte'; import { page } from '$app/state'; - import { connectGitHub } from '$lib/stores/git'; + + import { connectVcsProvider, supportsRepositoryCreation } from '$lib/stores/git'; + import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store'; let { show = $bindable(false), @@ -37,6 +39,11 @@ let selectedInstallationId = $state(''); let selectedRepository = $state(''); let installations = $state({ installations: [], total: 0 }); + const canCreateRepository = $derived( + installations.installations.some((entry) => + supportsRepositoryCreation(entry.provider, $regionalConsoleVariables) + ) + ); let error = $state(''); onMount(async () => { @@ -96,7 +103,7 @@ {#if !!installations?.total} - {#if !onlyExisting} + {#if !onlyExisting && canCreateRepository} {/if} {#if repositoryBehaviour === 'new'} @@ -143,12 +150,26 @@ {#if repositoryBehaviour === 'existing'} - - - Missing a repository? check your permissions - - + {#if $installation?.provider === 'origin'} + + + Missing a repository? check your repository access + + + {:else} + + + Missing a repository? check your permissions + + + {/if} {:else if repositoryBehaviour === 'new'} diff --git a/src/lib/components/git/deploymentSource.svelte b/src/lib/components/git/deploymentSource.svelte index 9b3220aaae..cc223e8b1d 100644 --- a/src/lib/components/git/deploymentSource.svelte +++ b/src/lib/components/git/deploymentSource.svelte @@ -20,6 +20,7 @@ Typography } from '@appwrite.io/pink-svelte'; import Button from '$lib/elements/forms/button.svelte'; + import IconOrigin from './IconOrigin.svelte'; let { deployment, @@ -35,6 +36,12 @@ let repository = $state(null); + // The deployment's server-supplied URLs carry the provider; the model has + // no provider field of its own. + const isOrigin = $derived( + (deployment?.providerRepositoryUrl ?? '').toLowerCase().includes('cursor.com') + ); + async function loadRepository() { if (!resource?.installationId || !resource?.providerRepositoryId || !region || !project) { return; @@ -66,7 +73,11 @@ toggle(e); }}> - GitHub + {#if isOrigin} + Origin + {:else} + GitHub + {/if} {#if repository?.authorized === false} @@ -82,8 +93,10 @@ To enable, add the repository to the installation settings on - GitHub + href={isOrigin + ? 'https://cursor.com/codebase/settings/apps' + : `https://github.com/settings/installations/${repository.providerInstallationId}`}> + {isOrigin ? 'Origin' : 'GitHub'} . @@ -97,7 +110,7 @@ + leadingIcon={isOrigin ? IconOrigin : IconGithub}> {deployment.providerRepositoryOwner}/{deployment.providerRepositoryName} import { InputCheckbox, InputSelect, InputText } from '$lib/elements/forms'; import { installation } from '$lib/stores/vcs'; + import { supportsPublicRepositories, supportsRepositoryCreation } from '$lib/stores/git'; + import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store'; import type { Models } from '@appwrite.io/console'; import { Layout } from '@appwrite.io/pink-svelte'; - export let selectedInstallationId: string; - export let installations: Models.InstallationList; - export let repositoryName: string; - export let repositoryPrivate = true; - export let disableFields = false; + let { + selectedInstallationId = $bindable(), + installations, + repositoryName = $bindable(), + repositoryPrivate = $bindable(true), + disableFields = false + }: { + selectedInstallationId: string; + installations: Models.InstallationList; + repositoryName: string; + repositoryPrivate?: boolean; + disableFields?: boolean; + } = $props(); + + // Only offer organizations whose provider can create repositories. + const creatableInstallations = $derived( + installations.installations.filter((entry) => + supportsRepositoryCreation(entry.provider, $regionalConsoleVariables) + ) + ); + + const supportsPublic = $derived( + supportsPublicRepositories( + creatableInstallations.find((entry) => entry.$id === selectedInstallationId)?.provider, + $regionalConsoleVariables + ) + ); + + $effect(() => { + if ( + creatableInstallations.length && + !creatableInstallations.some((entry) => entry.$id === selectedInstallationId) + ) { + selectedInstallationId = creatableInstallations[0].$id; + $installation = creatableInstallations[0]; + } + }); + + $effect(() => { + // Without public repository support every repository is private. + if (!supportsPublic) { + repositoryPrivate = true; + } + }); @@ -18,14 +59,14 @@ id="installation" label="Git organization" disabled={disableFields} - options={installations.installations.map((entry) => { + options={creatableInstallations.map((entry) => { return { label: entry.organization, value: entry.$id }; })} on:change={() => { - $installation = installations.installations.find( + $installation = creatableInstallations.find( (entry) => entry.$id === selectedInstallationId ); }} @@ -38,9 +79,11 @@ disabled={disableFields} bind:value={repositoryName} /> - + {#if supportsPublic} + + {/if} diff --git a/src/lib/components/git/repositories.svelte b/src/lib/components/git/repositories.svelte index df2c800501..619e9ee953 100644 --- a/src/lib/components/git/repositories.svelte +++ b/src/lib/components/git/repositories.svelte @@ -19,7 +19,8 @@ import SvgIcon from '../svgIcon.svelte'; import { Query, VCSDetectionType, type Models } from '@appwrite.io/console'; import { getFrameworkIcon } from '$lib/stores/sites'; - import { connectGitHub } from '$lib/stores/git'; + import { connectVcsProvider, enabledVcsProviders, getVcsProvider } from '$lib/stores/git'; + import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store'; import { addNotification } from '$lib/stores/notifications'; import { page } from '$app/state'; import Card from '../card.svelte'; @@ -27,6 +28,8 @@ import { onMount, untrack, onDestroy } from 'svelte'; import { debounce } from '$lib/helpers/debounce'; + const vcsProviders = enabledVcsProviders($regionalConsoleVariables?._APP_VCS_PROVIDERS); + let { action = $bindable('select'), selectedRepository = $bindable(undefined), @@ -176,18 +179,27 @@ ...installationsMap.map((entry) => { return { label: entry.organization, + leadingIcon: getVcsProvider(entry.provider).icon, value: entry.$id }; }), - { - label: 'Add installation', - leadingIcon: IconPlus, - value: 'new' - } + ...vcsProviders.map((provider) => { + return { + label: + vcsProviders.length > 1 + ? `Add ${provider.label} installation` + : 'Add installation', + leadingIcon: IconPlus, + value: `new-${provider.id}` + }; + }) ]} on:change={() => { - if (selectedInstallation === 'new') { - window.location.href = connectGitHub(callbackState).toString(); + if (selectedInstallation?.startsWith('new-')) { + window.location.href = connectVcsProvider( + selectedInstallation.slice('new-'.length), + callbackState + ).toString(); } search = ''; installation.set( diff --git a/src/lib/components/git/repositoryCard.svelte b/src/lib/components/git/repositoryCard.svelte index 0c64432c55..c898dc539b 100644 --- a/src/lib/components/git/repositoryCard.svelte +++ b/src/lib/components/git/repositoryCard.svelte @@ -6,10 +6,18 @@ import { IconGithub } from '@appwrite.io/pink-icons-svelte'; import { Card, Icon, Layout, Typography } from '@appwrite.io/pink-svelte'; import { createEventDispatcher } from 'svelte'; + import { installation } from '$lib/stores/vcs'; + import { getProviderOrganizationUrl } from '$lib/stores/git'; + import IconOrigin from './IconOrigin.svelte'; export let repository: Models.ProviderRepository; const dispatch = createEventDispatcher(); + + // ProviderRepository carries no provider, so read it off the owning installation. + $: provider = $installation?.provider ?? 'github'; + // '' when the provider's host is unknown client-side (e.g. Gitea). + $: organizationUrl = getProviderOrganizationUrl(provider, repository.organization); @@ -24,7 +32,9 @@ alignItems="flex-start" gap="s" style="min-width: 0; flex: 1 1 16rem;"> - + {repository.name} @@ -38,13 +48,15 @@ - - {repository.organization} - + {#if organizationUrl} + + {repository.organization} + + {:else} + + {repository.organization} + + {/if} diff --git a/src/lib/stores/git.ts b/src/lib/stores/git.ts index 8181f20cce..f55f3e7975 100644 --- a/src/lib/stores/git.ts +++ b/src/lib/stores/git.ts @@ -1,7 +1,104 @@ import { page } from '$app/state'; +import type { Models } from '@appwrite.io/console'; +import type { ComponentType } from 'svelte'; +import { IconGithub } from '@appwrite.io/pink-icons-svelte'; +import IconGitea from '$lib/components/git/IconGitea.svelte'; +import IconOrigin from '$lib/components/git/IconOrigin.svelte'; +import { isSelfHosted } from '$lib/system'; import { getApiEndpoint } from './sdk'; -function connectVcsProvider(provider: string, callbackState: Record = null) { +export type VcsProviderId = 'github' | 'origin' | 'gitea'; + +export interface VcsProviderMeta { + id: VcsProviderId; + label: string; + icon: ComponentType; + /** Web URL of an owner/organization on the provider, or '' when the host is not knowable client-side. */ + organizationUrl: (organization: string) => string; + /** Where the user manages the installation's repository access, or null when the provider has no such page. */ + installationSettingsUrl: (providerInstallationId: string) => string | null; + /** GitHub predates the _APP_VCS_PROVIDERS variable, so it stays on unconditionally. */ + alwaysEnabled?: boolean; + /** Gitea is a self-hosted-only feature, not offered on Appwrite Cloud. */ + selfHostedOnly?: boolean; +} + +/** + * Single source of truth for the console's git providers - icons, labels, + * provider URLs and the authorize redirects. Key order is display order: + * GitHub first, Origin second, then the rest. + */ +export const VCS_PROVIDERS: Record = { + github: { + id: 'github', + label: 'GitHub', + icon: IconGithub, + organizationUrl: (organization) => `https://github.com/${organization}`, + installationSettingsUrl: (providerInstallationId) => + `https://github.com/settings/installations/${providerInstallationId}`, + alwaysEnabled: true + }, + origin: { + id: 'origin', + label: 'Origin', + icon: IconOrigin as unknown as ComponentType, + // Origin repositories are browsed in the Codebase section of cursor.com + organizationUrl: () => 'https://cursor.com/codebase', + installationSettingsUrl: () => 'https://cursor.com/codebase/settings/apps' + }, + gitea: { + id: 'gitea', + label: 'Gitea', + icon: IconGitea as unknown as ComponentType, + // The Gitea host is per-deployment server configuration the client does not know + organizationUrl: () => '', + installationSettingsUrl: () => null, + selfHostedOnly: true + } +}; + +/** Provider metadata with a GitHub fallback for unknown ids (cosmetic use only). */ +export function getVcsProvider(provider?: string): VcsProviderMeta { + return VCS_PROVIDERS[provider as VcsProviderId] ?? VCS_PROVIDERS.github; +} + +/** + * Providers the connect UI should offer, honoring the server's + * _APP_VCS_PROVIDERS list and each provider's own gates. + */ +export function enabledVcsProviders(vcsProviders: string[] | undefined): VcsProviderMeta[] { + return Object.values(VCS_PROVIDERS).filter( + (provider) => + provider.alwaysEnabled || + ((vcsProviders?.includes(provider.id) ?? false) && + (!provider.selfHostedOnly || isSelfHosted)) + ); +} + +/** + * Whether the console can create repositories on the provider. Servers that + * predate _APP_VCS_PROVIDERS_WITH_REPOSITORY_CREATION omit the list; keep the + * console's historical behavior (creation offered) for them. + */ +export function supportsRepositoryCreation( + provider: string, + variables: Models.ConsoleVariables +): boolean { + return variables?._APP_VCS_PROVIDERS_WITH_REPOSITORY_CREATION?.includes(provider) ?? true; +} + +/** + * Whether the provider can host public repositories. When it cannot, every + * repository is private and the public/private choice is meaningless. + */ +export function supportsPublicRepositories( + provider: string, + variables: Models.ConsoleVariables +): boolean { + return variables?._APP_VCS_PROVIDERS_WITH_PUBLIC_REPOSITORIES?.includes(provider) ?? true; +} + +export function connectVcsProvider(provider: string, callbackState: Record = null) { const redirect = new URL(page.url); if (callbackState) { Object.keys(callbackState).forEach((key) => { @@ -24,6 +121,28 @@ export function connectGitea(callbackState: Record = null) { return connectVcsProvider('gitea', callbackState); } +export function connectOrigin(callbackState: Record = null) { + return connectVcsProvider('origin', callbackState); +} + +/** + * Browser URL of an owner/organization on the provider. Unknown providers + * fall back to GitHub, matching the console's historical behavior. + */ +export function getProviderOrganizationUrl(provider: string, organization: string): string { + return getVcsProvider(provider).organizationUrl(organization); +} + +/** + * Browser URL of a repository on the provider, or '' when the provider's host + * is not knowable client-side. Callers must not render a link for ''; an empty + * or relative href would resolve against the console's own origin. + */ +export function getProviderRepositoryUrl(provider: string, organization: string, name: string) { + const organizationUrl = getProviderOrganizationUrl(provider, organization); + return organizationUrl ? `${organizationUrl}/${name}` : ''; +} + export function deploymentStatusConverter(status: string) { // Status component possible values - status: 'waiting' | 'ready' | 'processing' | 'pending' | 'failed' | 'complete'; switch (status) { diff --git a/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/aside.svelte b/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/aside.svelte index 4acf3d41a7..2b473c61a7 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/aside.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/aside.svelte @@ -1,7 +1,9 @@