mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge remote-tracking branch 'origin/main' into feat-signup-email-security
This commit is contained in:
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
+12
-1
@@ -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}-->
|
||||
|
||||
+76
-6
@@ -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}
|
||||
|
||||
+77
-6
@@ -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
@@ -62,6 +62,12 @@
|
||||
.functions.deleteVariable({ functionId: data.function.$id, variableId });
|
||||
await Promise.all([invalidate(Dependencies.VARIABLES), invalidate(Dependencies.FUNCTION)]);
|
||||
};
|
||||
|
||||
const sdkListVariables = async (queries: string[]) =>
|
||||
sdk.forProject(page.params.region, page.params.project).functions.listVariables({
|
||||
functionId: data.function.$id,
|
||||
queries
|
||||
});
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -105,9 +111,14 @@
|
||||
{sdkCreateVariable}
|
||||
{sdkUpdateVariable}
|
||||
{sdkDeleteVariable}
|
||||
{sdkListVariables}
|
||||
isGlobal={false}
|
||||
globalVariableList={data.globalVariables}
|
||||
variableList={data.variables}
|
||||
backendPagination
|
||||
reloadPageOnPagination={false}
|
||||
variablesOffset={data.variablesOffset}
|
||||
variablesLimit={data.limit}
|
||||
project={data.project}
|
||||
analyticsSource="function_settings" />
|
||||
<UpdateBuildCommand func={data.function} />
|
||||
|
||||
+16
-5
@@ -1,10 +1,16 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
|
||||
const VARIABLES_LIMIT = 100;
|
||||
|
||||
export const load = async ({ params, depends, parent }) => {
|
||||
depends(Dependencies.VARIABLES);
|
||||
depends(Dependencies.FUNCTION);
|
||||
|
||||
const limit = PAGE_LIMIT;
|
||||
const variablesOffset = 0;
|
||||
|
||||
const { runtimesList, specificationsList, function: fn } = await parent();
|
||||
|
||||
const enabledSpecs = specificationsList?.specifications?.filter((s) => s.enabled) ?? [];
|
||||
@@ -16,10 +22,13 @@ export const load = async ({ params, depends, parent }) => {
|
||||
}
|
||||
|
||||
const [globalVariables, variables] = await Promise.all([
|
||||
sdk.forProject(params.region, params.project).projectApi.listVariables(),
|
||||
sdk
|
||||
.forProject(params.region, params.project)
|
||||
.functions.listVariables({ functionId: params.function })
|
||||
sdk.forProject(params.region, params.project).projectApi.listVariables({
|
||||
queries: [Query.limit(VARIABLES_LIMIT)]
|
||||
}),
|
||||
sdk.forProject(params.region, params.project).functions.listVariables({
|
||||
functionId: params.function,
|
||||
queries: [Query.limit(limit), Query.offset(variablesOffset)]
|
||||
})
|
||||
]);
|
||||
|
||||
// Conflicting variables first
|
||||
@@ -43,6 +52,8 @@ export const load = async ({ params, depends, parent }) => {
|
||||
return {
|
||||
variables,
|
||||
globalVariables,
|
||||
limit,
|
||||
variablesOffset,
|
||||
runtimesList,
|
||||
specificationsList,
|
||||
function: fn
|
||||
|
||||
+61
-4
@@ -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>
|
||||
|
||||
+11
@@ -51,6 +51,12 @@
|
||||
await Promise.all([invalidate(Dependencies.VARIABLES), invalidate(Dependencies.SITE)]);
|
||||
};
|
||||
|
||||
const sdkListVariables = async (queries: string[]) =>
|
||||
sdk.forProject(page.params.region, page.params.project).sites.listVariables({
|
||||
siteId: page.params.site,
|
||||
queries
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
if (
|
||||
page.url.searchParams.has('newInstallation') &&
|
||||
@@ -75,9 +81,14 @@
|
||||
{sdkCreateVariable}
|
||||
{sdkUpdateVariable}
|
||||
{sdkDeleteVariable}
|
||||
{sdkListVariables}
|
||||
isGlobal={false}
|
||||
globalVariableList={data.globalVariables}
|
||||
variableList={data.variables}
|
||||
backendPagination
|
||||
reloadPageOnPagination={false}
|
||||
variablesOffset={data.variablesOffset}
|
||||
variablesLimit={data.limit}
|
||||
project={data.project}
|
||||
product="site"
|
||||
analyticsSource="site_settings" />
|
||||
|
||||
+15
-5
@@ -1,18 +1,26 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
|
||||
const VARIABLES_LIMIT = 100;
|
||||
|
||||
export const load = async ({ params, depends, parent }) => {
|
||||
depends(Dependencies.VARIABLES);
|
||||
depends(Dependencies.SITE);
|
||||
const limit = PAGE_LIMIT;
|
||||
const variablesOffset = 0;
|
||||
const { site } = await parent();
|
||||
|
||||
const [globalVariables, variables, frameworks, installations, specificationsList] =
|
||||
await Promise.all([
|
||||
sdk.forProject(params.region, params.project).projectApi.listVariables(),
|
||||
sdk
|
||||
.forProject(params.region, params.project)
|
||||
.sites.listVariables({ siteId: params.site }),
|
||||
sdk.forProject(params.region, params.project).projectApi.listVariables({
|
||||
queries: [Query.limit(VARIABLES_LIMIT)]
|
||||
}),
|
||||
sdk.forProject(params.region, params.project).sites.listVariables({
|
||||
siteId: params.site,
|
||||
queries: [Query.limit(limit), Query.offset(variablesOffset)]
|
||||
}),
|
||||
sdk.forProject(params.region, params.project).sites.listFrameworks(),
|
||||
sdk.forProject(params.region, params.project).vcs.listInstallations(),
|
||||
isCloud
|
||||
@@ -51,6 +59,8 @@ export const load = async ({ params, depends, parent }) => {
|
||||
frameworks,
|
||||
variables,
|
||||
globalVariables,
|
||||
limit,
|
||||
variablesOffset,
|
||||
installations,
|
||||
specificationsList
|
||||
};
|
||||
|
||||
@@ -60,8 +60,13 @@
|
||||
secret?: boolean
|
||||
) => Promise<unknown>;
|
||||
export let sdkDeleteVariable: (variableId: string) => Promise<unknown>;
|
||||
export let sdkListVariables: (queries: string[]) => Promise<Models.VariableList> = (queries) =>
|
||||
sdk.forProject(page.params.region, page.params.project).projectApi.listVariables({
|
||||
queries
|
||||
});
|
||||
export let product: 'function' | 'site' = 'function';
|
||||
export let backendPagination = false;
|
||||
export let reloadPageOnPagination = true;
|
||||
export let variablesOffset = 0;
|
||||
export let variablesLimit = 10;
|
||||
export let disabled = false;
|
||||
@@ -76,20 +81,21 @@
|
||||
let showDeleteModal = false;
|
||||
let deleteError: string;
|
||||
let fullVariableList: Models.VariableList | undefined = allVariableList;
|
||||
let pagedVariableList: Models.VariableList = variableList;
|
||||
let previousVariableList = variableList;
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
|
||||
async function loadAllVariables() {
|
||||
const projectSdk = sdk.forProject(page.params.region, page.params.project);
|
||||
const variables = [...variableList.variables];
|
||||
let nextOffset = variables.length;
|
||||
const variables = backendPagination ? [] : [...variableList.variables];
|
||||
let nextOffset = backendPagination ? 0 : variables.length;
|
||||
let total = variableList.total;
|
||||
|
||||
while (nextOffset < total) {
|
||||
const response = await projectSdk.projectApi.listVariables({
|
||||
queries: [Query.limit(variablesLimit), Query.offset(nextOffset)]
|
||||
});
|
||||
const response = await sdkListVariables([
|
||||
Query.limit(variablesLimit),
|
||||
Query.offset(nextOffset)
|
||||
]);
|
||||
|
||||
total = response.total;
|
||||
|
||||
@@ -106,7 +112,11 @@
|
||||
}
|
||||
|
||||
async function ensureAllVariablesLoaded() {
|
||||
if (fullVariableList && fullVariableList.total === variableList.total) return;
|
||||
const total = backendPagination ? variableList.total : variableList.variables.length;
|
||||
|
||||
if (fullVariableList && fullVariableList.variables.length >= total) {
|
||||
return;
|
||||
}
|
||||
|
||||
fullVariableList = await loadAllVariables();
|
||||
}
|
||||
@@ -321,31 +331,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: conflictVariables = globalVariableList
|
||||
? globalVariableList.variables.filter((globalVariable) => {
|
||||
return variableList.variables.find((variable) => {
|
||||
return variable.key === globalVariable.key;
|
||||
});
|
||||
})
|
||||
: [];
|
||||
|
||||
$: if (allVariableList && fullVariableList !== allVariableList) {
|
||||
fullVariableList = allVariableList;
|
||||
}
|
||||
|
||||
$: if (variableList !== previousVariableList) {
|
||||
pagedVariableList = variableList;
|
||||
if (backendPagination && !reloadPageOnPagination) {
|
||||
variablesOffset = 0;
|
||||
}
|
||||
fullVariableList = undefined;
|
||||
previousVariableList = variableList;
|
||||
}
|
||||
|
||||
$: if (fullVariableList && fullVariableList.total !== variableList.total) {
|
||||
$: if (fullVariableList && fullVariableList.total < variableList.total) {
|
||||
fullVariableList = undefined;
|
||||
}
|
||||
|
||||
$: editorVariableList = fullVariableList ?? allVariableList ?? variableList;
|
||||
$: displayedVariables = backendPagination
|
||||
? variableList.variables
|
||||
? pagedVariableList.variables
|
||||
: variableList.variables.slice(offset, offset + limit);
|
||||
$: variableCount = backendPagination ? pagedVariableList.total : variableList.total;
|
||||
$: conflictVariables = globalVariableList
|
||||
? globalVariableList.variables.filter((globalVariable) => {
|
||||
return displayedVariables.find((variable) => {
|
||||
return variable.key === globalVariable.key;
|
||||
});
|
||||
})
|
||||
: [];
|
||||
|
||||
$: hasConflictOnPage = globalVariableList
|
||||
? displayedVariables.filter((variable) => {
|
||||
@@ -368,6 +382,14 @@
|
||||
];
|
||||
|
||||
async function handleVariablesPageChange() {
|
||||
if (!reloadPageOnPagination) {
|
||||
pagedVariableList = await sdkListVariables([
|
||||
Query.limit(variablesLimit),
|
||||
Query.offset(variablesOffset)
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextUrl = new URL(page.url);
|
||||
|
||||
nextUrl.searchParams.set('variablesOffset', String(variablesOffset));
|
||||
@@ -422,7 +444,7 @@
|
||||
<Icon slot="start" icon={IconUpload} /> Import .env
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
{#if variableList.total}
|
||||
{#if variableCount}
|
||||
<Button
|
||||
secondary
|
||||
{disabled}
|
||||
@@ -436,7 +458,7 @@
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
{@const sum = variableList.total}
|
||||
{@const sum = variableCount}
|
||||
{#if sum}
|
||||
<Layout.Stack gap="l">
|
||||
{#if conflictVariables.length > 0}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user