Merge branch 'feat-pink-v2' into 'add-date-tooltips'.

This commit is contained in:
Darshan
2025-03-17 16:08:10 +05:30
529 changed files with 17073 additions and 16654 deletions
@@ -0,0 +1,22 @@
<script lang="ts">
import { LabelCard } from '$lib/components';
import { consoleVariables } from '$routes/(console)/store';
import { Layout } from '@appwrite.io/pink-svelte';
const isVcsEnabled = $consoleVariables?._APP_VCS_ENABLED === true;
export let connectBehaviour: 'now' | 'later' = isVcsEnabled ? 'now' : 'later';
</script>
<Layout.Grid columns={2} columnsXS={1}>
<LabelCard
value="now"
bind:group={connectBehaviour}
disabled={!isVcsEnabled}
title="Connect your repository">
Clone this template into a new Git repository or link it to an existing one.
</LabelCard>
<LabelCard value="later" bind:group={connectBehaviour}>
<svelte:fragment slot="title">Connect later</svelte:fragment>
Deploy now and connect your version control later via CLI or Git integration in your settings.
</LabelCard>
</Layout.Grid>
+37 -43
View File
@@ -1,54 +1,48 @@
<script lang="ts">
import { page } from '$app/stores';
import Button from '$lib/elements/forms/button.svelte';
import { sdk } from '$lib/stores/sdk';
import { consoleVariables } from '$routes/(console)/store';
import { IconGithub } from '@appwrite.io/pink-icons-svelte';
import { Card, Empty, Icon } from '@appwrite.io/pink-svelte';
import Alert from '../alert.svelte';
import { Alert, Card, Empty, Icon, Layout } from '@appwrite.io/pink-svelte';
import { isSelfHosted } from '$lib/system';
import { connectGitHub } from '$lib/stores/git';
export let callbackState: Record<string, string> = null;
let isVcsEnabled = $consoleVariables?._APP_VCS_ENABLED === true;
function connectGitHub() {
const redirect = new URL($page.url);
if (callbackState) {
Object.keys(callbackState).forEach((key) => {
redirect.searchParams.append(key, callbackState[key]);
});
}
const target = new URL(`${sdk.forProject.client.config.endpoint}/vcs/github/authorize`);
target.searchParams.set('project', $page.params.project);
target.searchParams.set('success', redirect.toString());
target.searchParams.set('failure', redirect.toString());
target.searchParams.set('mode', 'admin');
return target;
}
</script>
{#if !isVcsEnabled && isSelfHosted}
<Alert>
<span slot="title"> Installing Git on a self-hosted instance </span>
<p>
Before installing Git in a locally hosted Appwrite project, ensure your environment
variables are configured.
</p>
<svelte:fragment slot="buttons">
<Button secondary external href="#/">Learn more</Button>
</svelte:fragment>
</Alert>
{/if}
<Card.Base padding="none" border="dashed">
<Empty
type="secondary"
title="No installation was added to the project yet"
description="Add an installation to connect repositories">
<svelte:fragment slot="actions">
<Button secondary href={connectGitHub().toString()} disabled={!isVcsEnabled}>
<Icon slot="start" icon={IconGithub} />
Connect to GitHub
</Button>
</svelte:fragment>
</Empty>
</Card.Base>
<Layout.Stack>
{#if !isVcsEnabled && isSelfHosted}
<Alert.Inline status="info" title="Installing Git on a self-hosted instance ">
<Layout.Stack>
<p>
Before installing Git in a locally hosted Appwrite project, ensure your
environment variables are configured.
</p>
<div>
<Button
compact
external
href="https://appwrite.io/docs/advanced/self-hosting/functions#git"
>Learn more</Button>
</div>
</Layout.Stack>
</Alert.Inline>
{/if}
<Card.Base padding="none" border="dashed">
<Empty
type="secondary"
title="No installation was added to the project yet"
description="Add an installation to connect repositories">
<svelte:fragment slot="actions">
<Button
secondary
href={connectGitHub(callbackState).toString()}
disabled={!isVcsEnabled}>
<Icon slot="start" icon={IconGithub} />
Connect to GitHub
</Button>
</svelte:fragment>
</Empty>
</Card.Base>
</Layout.Stack>
@@ -0,0 +1,25 @@
<script lang="ts">
import Link from '$lib/elements/link.svelte';
import { capitalize } from '$lib/helpers/string';
import type { Models } from '@appwrite.io/console';
import { Tooltip } from '@appwrite.io/pink-svelte';
import { timeFromNow, toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
export let deployment: Models.Deployment;
</script>
<p>
{#if deployment.providerCommitAuthor}
<Tooltip>
<span>
{capitalize(timeFromNow(deployment.$updatedAt))}
</span>
<span slot="tooltip">{toLocaleDateTime(deployment.$updatedAt)}</span>
</Tooltip>
by <Link href={deployment.providerCommitAuthorUrl} external
>{deployment.providerCommitAuthor}</Link>
{:else}
<DualTimeView time={deployment.$updatedAt} />
{/if}
</p>
@@ -0,0 +1,52 @@
<script lang="ts">
import { Trim } from '$lib/components';
import { Link } from '$lib/elements';
import { protocol } from '$routes/(console)/store';
import type { Models } from '@appwrite.io/console';
import { IconExternalLink } from '@appwrite.io/pink-icons-svelte';
import { ActionMenu, Icon, Layout, Popover, Tag, Typography } from '@appwrite.io/pink-svelte';
export let domains: Models.ProxyRuleList;
</script>
<Layout.Stack gap="xxs" direction="row" alignItems="center">
{#if domains?.total}
<Link external href={`${$protocol}${domains.rules[0]?.domain}`} variant="muted">
<Layout.Stack gap="xxs" direction="row" alignItems="center">
<Trim alternativeTrim>
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
{domains.rules[0]?.domain}
</Typography.Text>
</Trim>
<Icon icon={IconExternalLink} size="s" />
</Layout.Stack>
</Link>
{#if domains.rules.length > 1}
<Popover padding="none" let:toggle>
<Tag size="s" on:click={toggle}>
+{domains.rules.length - 1}
</Tag>
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
{#each domains.rules as rule, i}
{#if i !== 0}
<ActionMenu.Item.Anchor
href={`${$protocol}${rule.domain}`}
external
leadingIcon={IconExternalLink}>
<Trim alternativeTrim>
{rule.domain}
</Trim>
</ActionMenu.Item.Anchor>
{/if}
{/each}
</ActionMenu.Root>
</svelte:fragment>
</Popover>
{/if}
{:else}
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
No domains available
</Typography.Text>
{/if}
</Layout.Stack>
@@ -0,0 +1,68 @@
<script lang="ts">
import { Trim } from '$lib/components';
import { Link } from '$lib/elements';
import type { Models } from '@appwrite.io/console';
import {
IconCode,
IconGitBranch,
IconGitCommit,
IconGithub,
IconTerminal
} from '@appwrite.io/pink-icons-svelte';
import { ActionMenu, Layout, Popover, Icon } from '@appwrite.io/pink-svelte';
export let deployment: Models.Deployment;
</script>
{#if deployment.type === 'vcs'}
<Popover padding="none" let:toggle>
<div>
<Link
on:click={(e) => {
e.preventDefault();
toggle(e);
}}>
<Layout.Stack direction="row" gap="xs" alignItems="center">
<Icon icon={IconGithub} size="s" /> GitHub
</Layout.Stack>
</Link>
</div>
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
<ActionMenu.Item.Anchor
href={deployment.providerRepositoryUrl}
external
leadingIcon={IconGithub}>
{deployment.providerRepositoryOwner}/{deployment.providerRepositoryName}
</ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor
href={deployment.providerBranchUrl}
external
leadingIcon={IconGitBranch}>
{deployment.providerBranch}
</ActionMenu.Item.Anchor>
{#if deployment?.providerCommitMessage && deployment?.providerCommitHash && deployment?.providerCommitUrl}
<ActionMenu.Item.Anchor
href={deployment.providerCommitUrl}
external
leadingIcon={IconGitCommit}>
<Trim alternativeTrim>
{deployment?.providerCommitHash?.substring(0, 7)}
{deployment.providerCommitMessage.substring(0, 15)}...
</Trim>
</ActionMenu.Item.Anchor>
{/if}
</ActionMenu.Root>
</svelte:fragment>
</Popover>
{:else if deployment.type === 'manual'}
<Layout.Stack gap="s" direction="row" alignItems="center">
<Icon icon={IconCode} size="s" /> <span>Manual</span>
</Layout.Stack>
{:else if deployment.type === 'cli'}
<Layout.Stack gap="s" direction="row" alignItems="center">
<Icon icon={IconTerminal} size="s" /> <span>CLI</span>
</Layout.Stack>
{:else}
<span>N/A</span>
{/if}
+5
View File
@@ -2,3 +2,8 @@ export { default as Repositories } from './repositories.svelte';
export { default as ConnectGit } from './connectGit.svelte';
export { default as NewRepository } from './newRepository.svelte';
export { default as RepositoryBehaviour } from './repositoryBehaviour.svelte';
export { default as DeploymentCreatedBy } from './deploymentCreatedBy.svelte';
export { default as DeploymentSource } from './deploymentSource.svelte';
export { default as DeploymentDomains } from './deploymentDomains.svelte';
export { default as ConnectBehaviour } from './connectBehaviour.svelte';
export { default as ProductionBranchFieldset } from './productionBranchFieldset.svelte';
@@ -1,45 +1,92 @@
<script lang="ts">
import { Button, InputSelect, InputText } from '$lib/elements/forms';
import { Fieldset, Layout, Selector } from '@appwrite.io/pink-svelte';
import SelectRootModal from '../../../routes/(console)/project-[project]/sites/(components)/selectRootModal.svelte';
import { Fieldset, Layout, Selector, Skeleton } from '@appwrite.io/pink-svelte';
import SelectRootModal from './selectRootModal.svelte';
import { sdk } from '$lib/stores/sdk';
import { sortBranches } from '$lib/stores/vcs';
export let branch: string;
export let rootDir: string;
export let options: { value: string; label: string }[] = [];
export let silentMode: boolean;
export let installationId: string;
export let repositoryId: string;
let show = false;
async function loadBranches() {
const { branches } = await sdk.forProject.vcs.listRepositoryBranches(
installationId,
repositoryId
);
const sorted = sortBranches(branches);
branch = sorted[0]?.name ?? null;
if (!branch) {
branch = 'main';
}
return sorted;
}
</script>
<Fieldset legend="Branch">
<Layout.Stack gap="xl">
<InputSelect
required
id="branch"
label="Production branch"
placeholder="Select branch"
isSearchable
bind:value={branch}
on:select={(event) => {
branch = event.detail.value;
}}
{options} />
<Layout.Stack direction="row" gap="s" alignItems="flex-end">
<InputText
id="root"
label="Root directory"
placeholder="Select directory"
bind:value={rootDir} />
<Button secondary size="s" on:click={() => (show = true)}>Select</Button>
{#await loadBranches()}
<Layout.Stack gap="xl">
<Layout.Stack gap="xs">
<Skeleton variant="line" width={100} height={20} />
<Skeleton variant="line" width="100%" height={32} />
</Layout.Stack>
<Layout.Stack gap="xs">
<Skeleton variant="line" width={100} height={20} />
<Skeleton variant="line" width="100%" height={32} />
</Layout.Stack>
<Layout.Stack gap="xs">
<Skeleton variant="line" width={100} height={20} />
<Skeleton variant="line" width={300} height={15} />
<Skeleton variant="line" width={300} height={15} />
</Layout.Stack>
</Layout.Stack>
{:then branches}
{@const options =
branches
?.map((branch) => {
return {
value: branch.name,
label: branch.name
};
})
?.sort((a, b) => {
return a.label > b.label ? 1 : -1;
}) ?? []}
<Layout.Stack gap="xl">
<InputSelect
required
id="branch"
label="Production branch"
placeholder="Select branch"
isSearchable
bind:value={branch}
on:select={(event) => {
branch = event.detail.value;
}}
{options} />
<Layout.Stack direction="row" gap="s" alignItems="flex-end">
<InputText
id="root"
label="Root directory"
placeholder="Select directory"
bind:value={rootDir} />
<Button secondary size="s" on:click={() => (show = true)}>Select</Button>
</Layout.Stack>
<Selector.Checkbox
size="s"
id="silentMode"
label="Silent mode"
description="If selected, comments will not be created when pushing changes to this repository."
bind:checked={silentMode} />
</Layout.Stack>
<Selector.Checkbox
size="s"
id="silentMode"
label="Silent mode"
description="If selected, comments will not be created when pushing changes to this repository."
bind:checked={silentMode} />
</Layout.Stack>
{/await}
</Fieldset>
{#if show}
+101 -84
View File
@@ -1,9 +1,7 @@
<script lang="ts">
import { base } from '$app/paths';
import { EmptySearch } from '$lib/components';
import { EmptySearch, Paginator } from '$lib/components';
import { Button, InputSearch, InputSelect } from '$lib/elements/forms';
import { timeFromNow } from '$lib/helpers/date';
import { app } from '$lib/stores/app';
import { sdk } from '$lib/stores/sdk';
import { repositories } from '$routes/(console)/project-[project]/functions/function-[function]/store';
import { installation, installations, repository } from '$lib/stores/vcs';
@@ -21,6 +19,7 @@
import ConnectGit from './connectGit.svelte';
import SvgIcon from '../svgIcon.svelte';
import { getFrameworkIcon } from '$routes/(console)/project-[project]/sites/store';
import { VCSDetectionType, type Models } from '@appwrite.io/console';
const dispatch = createEventDispatcher();
@@ -31,11 +30,13 @@
export let installationList = $installations;
export let product: 'functions' | 'sites' = 'functions';
let search = '';
let selectedInstallation = null;
$: {
hasInstallations = installationList?.total > 0;
}
let selectedInstallation = null;
async function loadInstallations() {
if (installationList) {
if (installationList.installations.length) {
@@ -57,16 +58,30 @@
}
}
let search = '';
async function loadRepositories(installationId: string, search: string) {
if (
!$repositories ||
$repositories.installationId !== installationId ||
$repositories.search !== search
) {
$repositories.repositories = (
await sdk.forProject.vcs.listRepositories(installationId, search || undefined)
).providerRepositories;
//TODO: remove forced cast after backend fixes
if (product === 'functions') {
$repositories.repositories = (
(await sdk.forProject.vcs.listRepositories(
installationId,
VCSDetectionType.Runtime,
search || undefined
)) as unknown as Models.ProviderRepositoryRuntimeList
).runtimeProviderRepositories;
} else {
$repositories.repositories = (
await sdk.forProject.vcs.listRepositories(
installationId,
VCSDetectionType.Framework,
search || undefined
)
).frameworkProviderRepositories;
}
}
$repositories.search = search;
@@ -77,13 +92,7 @@
$repository = $repositories.repositories[0];
}
return $repositories.repositories.slice(0, 4);
}
async function detectFramework(repo) {
console.log(repo);
// TODO add code once backend is implemented
return '';
return $repositories.repositories;
}
</script>
@@ -145,79 +154,87 @@
</Table.Root>
{:then response}
{#if response?.length}
<Table.Root>
{#each response as repo}
<Table.Row>
<Table.Cell>
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if action === 'select'}
<input
class="is-small u-margin-inline-end-8"
type="radio"
name="repositories"
bind:group={selectedRepository}
on:change={() => repository.set(repo)}
value={repo.id} />
{/if}
{#if product === 'sites'}
{#await detectFramework(repo)}
<Avatar size="xs" alt={repo.name} empty />
{:then framework}
<Avatar
size="xs"
alt={repo.name}
empty={!framework}>
<SvgIcon name={getFrameworkIcon(framework)} />
</Avatar>
{/await}
{:else}
<Avatar
size="xs"
src={repo?.runtime
? `${base}/icons/${$app.themeInUse}/color/${
repo.runtime.split('-')[0]
}.svg`
: ''}
alt={repo.name} />
{/if}
<Layout.Stack gap="s" direction="row" alignItems="center">
<Typography.Text
truncate
color="--fgcolor-neutral-secondary">
{repo.name}
</Typography.Text>
{#if repo.private}
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
<Paginator
items={response}
let:paginatedItems
hideFooter={response?.length <= 6}
limit={6}>
<Table.Root>
{#each paginatedItems as repo}
<Table.Row>
<Table.Cell>
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if action === 'select'}
<input
class="is-small u-margin-inline-end-8"
type="radio"
name="repositories"
bind:group={selectedRepository}
on:change={() => repository.set(repo)}
value={repo.id} />
{/if}
<time datetime={repo.pushedAt}>
<Typography.Caption
variant="400"
{#if product === 'sites'}
{#if repo?.framework && repo.framework !== 'other'}
<Avatar size="xs" alt={repo.name}>
<SvgIcon
name={getFrameworkIcon(repo.framework)}
iconSize="small" />
</Avatar>
{:else}
<Avatar size="xs" alt={repo.name} empty />
{/if}
{:else}
{@const iconName = repo?.runtime
? repo.runtime.split('-')[0]
: undefined}
<Avatar size="xs" alt={repo.name} empty={!iconName}>
<SvgIcon name={iconName} iconSize="small" />
</Avatar>
{/if}
<Layout.Stack
gap="s"
direction="row"
alignItems="center">
<Typography.Text
truncate
color="--fgcolor-neutral-tertiary">
{timeFromNow(repo.pushedAt)}
</Typography.Caption>
</time>
</Layout.Stack>
{#if action === 'button'}
<Layout.Stack direction="row" justifyContent="flex-end">
<PinkButton.Button
size="xs"
variant="secondary"
on:click={() => dispatch('connect', repo)}>
Connect
</PinkButton.Button>
color="--fgcolor-neutral-secondary">
{repo.name}
</Typography.Text>
{#if repo.private}
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
{/if}
<time datetime={repo.pushedAt}>
<Typography.Caption
variant="400"
truncate
color="--fgcolor-neutral-tertiary">
{timeFromNow(repo.pushedAt)}
</Typography.Caption>
</time>
</Layout.Stack>
{/if}
</Layout.Stack>
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
{#if action === 'button'}
<Layout.Stack
direction="row"
justifyContent="flex-end">
<PinkButton.Button
size="xs"
variant="secondary"
on:click={() => dispatch('connect', repo)}>
Connect
</PinkButton.Button>
</Layout.Stack>
{/if}
</Layout.Stack>
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
</Paginator>
{:else}
<EmptySearch hidePages bind:search target="repositories">
<EmptySearch hidePages hidePagination bind:search target="repositories">
<svelte:fragment slot="actions">
{#if search}
<Button secondary on:click={() => (search = '')}>
@@ -0,0 +1,122 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { iconPath } from '$lib/stores/app';
import { sdk } from '$lib/stores/sdk';
import { installation, repository } from '$lib/stores/vcs';
import { VCSDetectionType } from '@appwrite.io/console';
import { DirectoryPicker } from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
type Directory = {
title: string;
fullPath: string;
fileCount: number;
thumbnailUrl: string;
children?: Directory[];
};
export let show = false;
export let rootDir: string;
export let product: 'sites' | 'functions';
let isLoading = true;
let directories: Directory[] = [
{
title: 'Root',
fullPath: './',
fileCount: 0,
thumbnailUrl: 'root',
children: []
}
];
let currentPath: string = './';
onMount(async () => {
try {
const content = await sdk.forProject.vcs.getRepositoryContents(
$installation.$id,
$repository.id,
currentPath
);
// console.log(content);
directories[0].fileCount = content.contents?.length ?? 0;
directories[0].children = content.contents
.filter((e) => e.isDirectory)
.map((dir) => ({
title: dir.name,
fullPath: currentPath + dir.name,
fileCount: undefined,
thumbnailUrl: dir.name
}));
// console.log(directories);
isLoading = false;
} catch (e) {
console.log(e);
}
});
async function fetchContents(e: CustomEvent) {
const path = e.detail.fullPath;
currentPath = path;
try {
const content = await sdk.forProject.vcs.getRepositoryContents(
$installation.$id,
$repository.id,
path
);
// console.log(content);
const fileCount = content.contents?.length ?? 0;
const contentDirectories = content.contents.filter((e) => e.isDirectory);
if (contentDirectories.length === 0) {
return;
}
let currentDir = directories[0];
for (let i = 1; i < path.split('/').length; i++) {
const dir = path.split('/')[i];
currentDir = currentDir.children.find((d) => d.title === dir);
}
currentDir.fileCount = fileCount;
currentDir.children = contentDirectories.map((dir) => ({
title: dir.name,
fullPath: path + '/' + dir.name,
fileCount: undefined,
thumbnailUrl: undefined
}));
const runtime = await sdk.forProject.vcs.createRepositoryDetection(
$installation.$id,
$repository.id,
VCSDetectionType.Framework, //TODO: add type: VCSDetectionType.Framework || VCSDetectionType.Runtime according to the product
path
);
//TODO: Fix runtime after passing type: runtime.framework || runtime.runtime
currentDir.children.forEach((dir) => {
dir.thumbnailUrl = $iconPath(runtime.framework, 'color');
});
directories = [...directories];
} catch (error) {
console.error(error);
}
}
function handleSubmit() {
rootDir = currentPath;
show = false;
}
// $: console.log(rootDir);
</script>
<Modal title="Root directory" bind:show onSubmit={handleSubmit}>
<span slot="description">
Select the directory where your site code is located using the menu below.
</span>
<DirectoryPicker {directories} {isLoading} on:select={fetchContents} />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button submit>Save</Button>
</svelte:fragment>
</Modal>