Merge remote-tracking branch 'origin/main' into feat/project-premium-geo-db-addon

This commit is contained in:
Damodar Lohani
2026-05-12 05:59:18 +00:00
9 changed files with 262 additions and 158 deletions
+16 -12
View File
@@ -8,7 +8,7 @@
import { database, checkForDatabaseBackupPolicies } from '$lib/stores/database';
import { newOrgModal, organization } from '$lib/stores/organization';
import { wizard } from '$lib/stores/wizard';
import { afterUpdate, onMount } from 'svelte';
import { onMount } from 'svelte';
import { loading } from '$routes/store';
import { requestedMigration } from '../store';
import Create from './createOrganization.svelte';
@@ -281,10 +281,12 @@
}
}
database.subscribe(async (database) => {
if (!database || !page.params.region || !page.params.project) return;
// the component checks `isCloud` internally.
await checkForDatabaseBackupPolicies(page.params.region, page.params.project, database);
onMount(() => {
return database.subscribe(async (database) => {
if (!database || !page.params.region || !page.params.project) return;
// the component checks `isCloud` internally.
await checkForDatabaseBackupPolicies(page.params.region, page.params.project, database);
});
});
let currentOrganizationId = null;
@@ -294,19 +296,23 @@
if (isCloud) {
currentOrganizationId = org.$id;
checkForEnterpriseTrial(org);
await checkForUsageLimit(org);
checkForMarkedForDeletion(org);
checkForUpgradingStatus(org);
await checkForNewDevUpgradePro(org);
if (org?.billingPlanDetails.requiresPaymentMethod) {
await paymentExpired(org);
await checkPaymentAuthorizationRequired(org);
const billingChecks: Promise<unknown>[] = [
checkForUsageLimit(org),
checkForNewDevUpgradePro(org)
];
if (org?.billingPlanDetails.requiresPaymentMethod) {
billingChecks.push(paymentExpired(org), checkPaymentAuthorizationRequired(org));
if (org?.billingTrialDays) {
calculateTrialDay(org);
}
}
await Promise.all(billingChecks);
$activeHeaderAlert = headerAlert.getExcluding('impersonation');
}
}
@@ -319,9 +325,7 @@
$registerSearchers(orgSearcher, projectsSearcher);
afterUpdate(() => {
$activeHeaderAlert = headerAlert.getExcluding('impersonation');
});
$: (void $headerAlert, ($activeHeaderAlert = headerAlert.getExcluding('impersonation')));
</script>
<CommandCenter />
+1 -25
View File
@@ -3,7 +3,7 @@ import { isCloud } from '$lib/system';
import type { LayoutLoad } from './$types';
import type { Account } from '$lib/stores/user';
import { Dependencies } from '$lib/constants';
import { Platform, Query, type Models } from '@appwrite.io/console';
import { Platform, type Models } from '@appwrite.io/console';
import { makePlansMap } from '$lib/helpers/billing';
import { plansInfo as plansInfoStore } from '$lib/stores/billing';
import { normalizeConsoleVariables } from '$lib/helpers/domains';
@@ -54,7 +54,6 @@ export const load: LayoutLoad = async ({ depends, parent, url }) => {
currentOrgId: undefined,
organizations,
consoleVariables,
allProjectsCount: 0,
plansInfo: plansInfo ?? null,
version: versionData?.version ?? null
};
@@ -107,28 +106,6 @@ export const load: LayoutLoad = async ({ depends, parent, url }) => {
preferences.organization ??
(organizations.teams.length > 0 ? organizations.teams[0].$id : undefined);
// Load projects for the current organization if one is selected
let projectsCount = 0;
if (currentOrgId) {
try {
projectsCount = (
await sdk.forConsole.projects.list({
queries: [
Query.equal('teamId', currentOrgId),
Query.limit(1),
Query.select(['$id'])
]
})
).total;
} catch (e) {
if (shouldRedirectToVerifyEmail(e)) {
redirect(303, verifyEmailUrl);
}
projectsCount = 0;
}
}
// just in case!
plansInfoStore.set(fallbackPlansInfoArray);
@@ -139,7 +116,6 @@ export const load: LayoutLoad = async ({ depends, parent, url }) => {
currentOrgId,
organizations,
consoleVariables,
allProjectsCount: projectsCount,
plansInfo: fallbackPlansInfoArray,
version: versionData?.version ?? null
};
@@ -99,11 +99,22 @@
return 'processing';
case 'failed':
return 'failed';
// pink-svelte's Status union has no 'skipped' — fall back to the
// neutral 'waiting' visual and override the label below.
case 'skipped':
return 'waiting';
default:
return 'waiting';
}
}
function getBackupStatusLabel(backup: Models.BackupArchive): string {
if (backup.status === 'skipped') {
return 'Skipped';
}
return capitalize(getBackupStatus(backup));
}
async function deleteSingleBackup(archiveId: string) {
try {
await sdk
@@ -231,7 +242,7 @@
</Table.Cell>
<Table.Cell column="status" {root}>
{@const backupStatus = getBackupStatus(backup)}
<Status status={backupStatus} label={capitalize(backupStatus)} />
<Status status={backupStatus} label={getBackupStatusLabel(backup)} />
<!--{#if backup.status === 'Failed'}-->
<!-- <span class="u-underline">Get support</span>-->
<!--{/if}-->
@@ -5,7 +5,15 @@
import type { Column, ColumnType } from '$lib/helpers/types';
import { Container } from '$lib/layout';
import { preferences } from '$lib/stores/preferences';
import { Icon, Layout, Divider, Tooltip } from '@appwrite.io/pink-svelte';
import {
Icon,
Layout,
Divider,
Tooltip,
Selector,
Typography,
Dialog
} from '@appwrite.io/pink-svelte';
import type { PageProps } from './$types';
import FilePicker from '$lib/components/filePicker.svelte';
import { page } from '$app/state';
@@ -21,7 +29,7 @@
IconUpload,
IconDownload
} from '@appwrite.io/pink-icons-svelte';
import { type Models } from '@appwrite.io/console';
import { OnDuplicate, type Models } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
@@ -49,7 +57,11 @@
let isRefreshing = $state(false);
let showImportJson = $state(false);
let showImportOptions = $state(false);
let showCustomColumnsModal = $state(false);
let importOnDuplicate: OnDuplicate = $state(OnDuplicate.Fail);
let pendingFile: Models.File | null = $state(null);
let pendingLocalFile = $state(false);
let columnsError: string = $state(null);
let spreadsheet: SpreadSheet | null = $state(null);
@@ -74,17 +86,28 @@
return queryParam ? `${url}?query=${encodeURIComponent(queryParam)}` : url;
}
async function onSelect(file: Models.File, localFile = false) {
function onSelect(file: Models.File, localFile = false) {
pendingFile = file;
pendingLocalFile = localFile;
importOnDuplicate = OnDuplicate.Fail;
showImportOptions = true;
}
async function startImport() {
if (!pendingFile) return;
showImportOptions = false;
$isCollectionsJsonImportInProgress = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.migrations.createJSONImport({
bucketId: file.bucketId,
fileId: file.$id,
bucketId: pendingFile.bucketId,
fileId: pendingFile.$id,
resourceId: `${page.params.database}:${page.params.collection}`,
internalFile: localFile
internalFile: pendingLocalFile,
onDuplicate: importOnDuplicate
});
addNotification({
@@ -101,6 +124,7 @@
});
} finally {
$isCollectionsJsonImportInProgress = false;
pendingFile = null;
}
}
@@ -343,6 +367,52 @@
}} />
{/if}
<Dialog title="Import options" bind:open={showImportOptions}>
<Layout.Stack gap="l">
<Typography.Text variant="m-400">
Choose how to handle documents that already exist in this collection.
</Typography.Text>
<Layout.Stack gap="m">
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Fail}
label="Fail on duplicate (default)">
<svelte:fragment slot="description">
Import aborts on the first document with a matching ID.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Skip}
label="Skip existing documents">
<svelte:fragment slot="description">
Documents with matching IDs will be silently skipped.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Overwrite}
label="Overwrite existing documents">
<svelte:fragment slot="description">
Documents with matching IDs will be updated with the imported data.
</svelte:fragment>
</Selector.Radio>
</Layout.Stack>
</Layout.Stack>
<svelte:fragment slot="footer">
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
<Button text on:click={() => (showImportOptions = false)}>Cancel</Button>
<Button on:click={startImport}>Start import</Button>
</Layout.Stack>
</svelte:fragment>
</Dialog>
<Modal
title="Custom columns"
bind:error={columnsError}
@@ -8,7 +8,16 @@
import { Container } from '$lib/layout';
import { preferences } from '$lib/stores/preferences';
import { canWriteTables, canWriteRows } from '$lib/stores/roles';
import { Icon, Layout, Divider, Tooltip, Typography, Link } from '@appwrite.io/pink-svelte';
import {
Dialog,
Icon,
Layout,
Divider,
Selector,
Tooltip,
Typography,
Link
} from '@appwrite.io/pink-svelte';
import type { PageData } from './$types';
import {
tableColumns,
@@ -34,7 +43,7 @@
IconUpload,
IconDownload
} from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
import { OnDuplicate, type Models } from '@appwrite.io/console';
import CreateRow from '$database/table-[table]/rows/create.svelte';
import { onDestroy } from 'svelte';
import { isCloud } from '$lib/system';
@@ -56,6 +65,10 @@
let isRefreshing = false;
let showImportCSV = false;
let showImportOptions = false;
let importOnDuplicate: OnDuplicate = OnDuplicate.Fail;
let pendingFile: Models.File | null = null;
let pendingLocalFile = false;
// todo: might need a type fix here.
const filterColumns = writable<Column[]>([]);
@@ -107,17 +120,28 @@
$: disableButton = canShowSuggestionsSheet;
async function onSelect(file: Models.File, localFile = false) {
function onSelect(file: Models.File, localFile = false) {
pendingFile = file;
pendingLocalFile = localFile;
importOnDuplicate = OnDuplicate.Fail;
showImportOptions = true;
}
async function startImport() {
if (!pendingFile) return;
showImportOptions = false;
$isTablesCsvImportInProgress = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.migrations.createCSVImport({
bucketId: file.bucketId,
fileId: file.$id,
bucketId: pendingFile.bucketId,
fileId: pendingFile.$id,
resourceId: `${page.params.database}:${page.params.table}`,
internalFile: localFile
internalFile: pendingLocalFile,
onDuplicate: importOnDuplicate
});
addNotification({
@@ -134,6 +158,7 @@
});
} finally {
$isTablesCsvImportInProgress = false;
pendingFile = null;
}
}
@@ -434,6 +459,52 @@
}} />
{/if}
<Dialog title="Import options" bind:open={showImportOptions}>
<Layout.Stack gap="l">
<Typography.Text variant="m-400">
Choose how to handle documents that already exist in this table.
</Typography.Text>
<Layout.Stack gap="m">
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Fail}
label="Fail on duplicate (default)">
<svelte:fragment slot="description">
Migration aborts on the first row with a matching ID.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Skip}
label="Skip existing documents">
<svelte:fragment slot="description">
Documents with matching IDs will be silently skipped.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Overwrite}
label="Overwrite existing documents">
<svelte:fragment slot="description">
Documents with matching IDs will be updated with the imported data.
</svelte:fragment>
</Selector.Radio>
</Layout.Stack>
</Layout.Stack>
<svelte:fragment slot="footer">
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
<Button text on:click={() => (showImportOptions = false)}>Cancel</Button>
<Button on:click={startImport}>Start import</Button>
</Layout.Stack>
</svelte:fragment>
</Dialog>
<CreateRow
{table}
bind:showSheet={$showRowCreateSheet.show}
@@ -11,6 +11,7 @@
AppwriteMigrationResource,
FirebaseMigrationResource,
NHostMigrationResource,
OnDuplicate,
SupabaseMigrationResource
} from '@appwrite.io/console';
import { started } from '../stores';
@@ -23,6 +24,7 @@
Fieldset,
Icon,
Layout,
Selector,
Typography
} from '@appwrite.io/pink-svelte';
import { Link } from '$lib/elements';
@@ -38,6 +40,8 @@
import { capitalize } from '$lib/helpers/string';
import { page } from '$app/state';
let importOnDuplicate: OnDuplicate = OnDuplicate.Fail;
const onExit = () => {
resetImportStores();
};
@@ -46,6 +50,15 @@
try {
const resources = migrationFormToResources($formData, $provider.provider);
// Gate onDuplicate to Fail when databases isn't selected. The radios
// are only shown when databases.root is checked, but the local value
// persists across toggles — without this gate, deselecting databases
// after picking Overwrite/Skip would silently apply that mode to
// other resource types (users, teams, functions, etc.) on submit.
const importOptions = {
onDuplicate: $formData.databases.root ? importOnDuplicate : OnDuplicate.Fail
};
switch ($provider.provider) {
case 'appwrite': {
await sdk
@@ -54,7 +67,8 @@
resources: resources as AppwriteMigrationResource[],
endpoint: $provider.endpoint,
projectId: $provider.projectID,
apiKey: $provider.apiKey
apiKey: $provider.apiKey,
...importOptions
});
await invalidate(Dependencies.MIGRATIONS);
@@ -70,7 +84,8 @@
databaseHost: $provider.host,
username: $provider.username || 'postgres',
password: $provider.password,
port: $provider.port || 5432
port: $provider.port || 5432,
...importOptions
});
await invalidate(Dependencies.MIGRATIONS);
break;
@@ -80,7 +95,8 @@
.forProject(page.params.region, page.params.project)
.migrations.createFirebaseMigration({
resources: resources as FirebaseMigrationResource[],
serviceAccount: $provider.serviceAccount
serviceAccount: $provider.serviceAccount,
...importOptions
});
await invalidate(Dependencies.MIGRATIONS);
break;
@@ -95,7 +111,8 @@
adminSecret: $provider.adminSecret,
database: $provider.database || $provider.subdomain,
username: $provider.username || 'postgres',
password: $provider.password
password: $provider.password,
...importOptions
});
await invalidate(Dependencies.MIGRATIONS);
@@ -196,6 +213,46 @@
projectSdk={sdk.forProject(page.params.region, page.params.project)} />
</Layout.Stack>
</Fieldset>
{#if $formData.databases.root}
<Fieldset legend="Import options">
<Layout.Stack gap="m">
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Fail}
label="Fail on duplicate (default)">
<svelte:fragment slot="description">
Migration aborts on the first existing resource (database,
table, column, index, or row).
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Skip}
label="Skip existing resources">
<svelte:fragment slot="description">
Existing resources are left untouched. Only resources missing on
the destination are created.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Overwrite}
label="Overwrite existing resources">
<svelte:fragment slot="description">
Existing resources are updated to match the source. Schema drift
and row data are both reconciled.
</svelte:fragment>
</Selector.Radio>
</Layout.Stack>
</Fieldset>
{/if}
{/if}
</Layout.Stack>
</Layout.Stack>
@@ -100,13 +100,18 @@
}
return {
total,
total: variables.length,
variables
};
}
async function ensureAllVariablesLoaded() {
if (fullVariableList && fullVariableList.total === variableList.total) return;
if (
fullVariableList &&
fullVariableList.variables.length >= variableList.variables.length
) {
return;
}
fullVariableList = await loadAllVariables();
}
@@ -338,7 +343,7 @@
previousVariableList = variableList;
}
$: if (fullVariableList && fullVariableList.total !== variableList.total) {
$: if (fullVariableList && fullVariableList.variables.length < variableList.variables.length) {
fullVariableList = undefined;
}
@@ -346,6 +351,7 @@
$: displayedVariables = backendPagination
? variableList.variables
: variableList.variables.slice(offset, offset + limit);
$: variableCount = backendPagination ? variableList.total : variableList.variables.length;
$: hasConflictOnPage = globalVariableList
? displayedVariables.filter((variable) => {
@@ -422,7 +428,7 @@
<Icon slot="start" icon={IconUpload} /> Import .env
</Button>
</Layout.Stack>
{#if variableList.total}
{#if variableCount}
<Button
secondary
{disabled}
@@ -436,7 +442,7 @@
{/if}
</Layout.Stack>
</Layout.Stack>
{@const sum = variableList.total}
{@const sum = variableCount}
{#if sum}
<Layout.Stack gap="l">
{#if conflictVariables.length > 0}
+3 -91
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { Wizard } from '$lib/layout';
import { Icon, Input, Layout, Tag, Typography, Card, Upload } from '@appwrite.io/pink-svelte';
import { Icon, Input, Layout, Typography, Card, Upload } from '@appwrite.io/pink-svelte';
import { supportData, isSupportOnline } from './wizard/support/store';
import { onMount, onDestroy } from 'svelte';
import { sdk } from '$lib/stores/sdk';
@@ -24,46 +24,6 @@
let projectOptions = $state<Array<{ value: string; label: string }>>([]);
let files = $state<FileList | null>(null);
// Category options with display names
const categories = [
{ value: 'general', label: 'General' },
{ value: 'billing', label: 'Billing' },
{ value: 'technical', label: 'Technical' }
];
// Topic options based on category
const topicsByCategory = {
general: [
'Security',
'Compliance',
'Performance',
'Account',
'Project',
'Regions',
'Other'
],
billing: ['Invoice', 'Plans', 'Payment methods', 'Downgrade', 'Refund', 'Usage', 'Other'],
technical: [
'Auth',
'Databases',
'Storage',
'Functions',
'Realtime',
'Messaging',
'Migrations',
'Webhooks',
'SDKs',
'Console',
'Backups',
'Blocked project',
'Domains',
'Outage',
'Platforms',
'Sites',
'Other'
]
};
onMount(async () => {
// Filter projects by organization ID using server-side queries
const projectList = await sdk.forConsole.projects.list({
@@ -82,39 +42,20 @@
$supportData = {
message: null,
subject: null,
category: 'technical',
topic: undefined,
file: null
};
});
// Update topic options when category changes
const topicOptions = $derived(
($supportData.category ? topicsByCategory[$supportData.category] || [] : []).map(
(topic) => ({
value: topic.toLowerCase().trim().replace(/\s+/g, '-'),
label: topic
})
)
);
async function handleSubmit() {
// Create category-topic tag
const categoryTopicTag = $supportData.topic
? `${$supportData.category}-${$supportData.topic}`.toLowerCase()
: $supportData.category.toLowerCase();
const formData = new FormData();
formData.append('email', $user.email);
formData.append('subject', $supportData.subject);
formData.append('subject', $supportData.subject ?? '');
formData.append('firstName', ($user?.name || 'Unknown').slice(0, 40));
formData.append('message', $supportData.message);
formData.append('message', $supportData.message ?? '');
formData.append('tags[]', 'cloud');
formData.append('tags[]', categoryTopicTag);
formData.append(
'metaFields',
JSON.stringify({
category: $supportData.category,
orgId: $organization?.$id ?? '',
projectId: $supportData?.project ?? '',
billingPlan: $organization?.billingPlanId ?? ''
@@ -151,8 +92,6 @@
$supportData = {
message: null,
subject: null,
category: 'technical',
topic: undefined,
file: null,
project: null
};
@@ -190,33 +129,6 @@
>Please describe your request in detail. If applicable, include steps for
reproduction of any in-app issues.</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="s">
<Typography.Text color="--fgcolor-neutral-secondary"
>Choose a category</Typography.Text>
<Layout.Stack gap="s" direction="row">
{#each categories as category}
<Tag
on:click={() => {
if ($supportData.category !== category.value) {
$supportData.topic = undefined;
}
$supportData.category = category.value;
}}
selected={$supportData.category === category.value}
>{category.label}</Tag>
{/each}
</Layout.Stack>
</Layout.Stack>
{#if topicOptions.length > 0}
{#key $supportData.category}
<Input.ComboBox
id="topic"
label="Choose a topic"
placeholder="Select topic"
bind:value={$supportData.topic}
options={topicOptions} />
{/key}
{/if}
<Input.ComboBox
id="project"
label="Choose a project"
+5 -8
View File
@@ -1,18 +1,15 @@
import { writable } from 'svelte/store';
export type SupportData = {
message: string;
subject: string;
category: string;
topic?: string;
message: string | null;
subject: string | null;
file?: File | null;
project?: string;
project?: string | null;
};
export const supportData = writable<SupportData>({
message: '',
subject: '',
category: 'technical',
message: null,
subject: null,
file: null
});