Merge remote-tracking branch 'origin/main' into feat-dedicated-db

# Conflicts:
#	bun.lock
#	package.json
#	src/lib/stores/sdk.ts
#	src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte
#	src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/store.ts
#	src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/createColumn.svelte
#	src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte
This commit is contained in:
Jake Barnby
2026-03-25 15:26:01 +13:00
89 changed files with 1269 additions and 795 deletions
+166 -254
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -49,6 +49,7 @@
"dayjs": "^1.11.13",
"deep-equal": "^2.2.3",
"echarts": "^5.6.0",
"flatted": "^3.4.2",
"ignore": "^6.0.2",
"json5": "^2.2.3",
"nanoid": "^5.1.5",
@@ -102,6 +103,8 @@
},
"overrides": {
"vite": "npm:rolldown-vite@latest",
"minimatch": "10.2.3"
"minimatch": "10.2.3",
"immutable": "^5.1.5",
"flatted": "^3.4.2"
}
}
+2 -2
View File
@@ -26,8 +26,8 @@
sizes="120x120"
href="%sveltekit.assets%/logos/apple-touch-icon-120x120.png" />
<!-- apple web app meta tags -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- web app / standalone display meta tags -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Appwrite Console" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
+3
View File
@@ -235,6 +235,7 @@ export enum Submit {
UserUpdatePassword = 'submit_user_update_password',
UserUpdatePhone = 'submit_user_update_phone',
UserUpdateMfa = 'submit_user_update_mfa',
UserUpdateImpersonation = 'submit_user_update_impersonation',
UserUpdatePreferences = 'submit_user_update_preferences',
UserUpdateStatus = 'submit_user_update_status',
UserUpdateVerificationEmail = 'submit_user_update_verification_email',
@@ -356,6 +357,7 @@ export enum Submit {
FunctionUpdateSchedule = 'submit_function_update_schedule',
FunctionUpdateConfiguration = 'submit_function_update_configuration',
FunctionUpdateLogging = 'submit_function_update_logging',
FunctionUpdateResourceLimits = 'submit_function_update_resource_limits',
FunctionUpdateTimeout = 'submit_function_update_timeout',
FunctionUpdateEvents = 'submit_function_update_events',
FunctionUpdateScopes = 'submit_function_key_update_scopes',
@@ -480,6 +482,7 @@ export enum Submit {
SiteUpdateEvents = 'submit_site_update_events',
SiteUpdateScopes = 'submit_site_key_update_scopes',
SiteUpdateBuildSettings = 'submit_site_update_build_settings',
SiteUpdateResourceLimits = 'submit_site_update_resource_limits',
SiteUpdateSinglePageApplication = 'submit_site_update_single_page_application',
SiteConnectRepo = 'submit_site_connect_repo',
SiteRedeploy = 'submit_site_redeploy',
@@ -1,11 +1,12 @@
<script lang="ts">
import { initCreateColumn } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte';
import { columnOptions } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/store';
import { getSupportedColumns } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/store';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import Template from './template.svelte';
let search = '';
let options = columnOptions.map((option) => {
$: options = getSupportedColumns($regionalConsoleVariables).map((option) => {
return {
label: option.name,
icon: option.icon,
@@ -1,7 +1,8 @@
<script lang="ts">
import { invalidate, goto } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Button, InputEmail, InputPassword } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { get } from 'svelte/store';
@@ -28,6 +29,17 @@
let resendTimer = $state(0);
let timerInterval: ReturnType<typeof setInterval> | null = null;
let showUpdateEmail = $state(false);
let newEmail = $state('');
let newPassword = $state('');
let updating = $state(false);
$effect(() => {
if (showUpdateEmail) {
newEmail = email || get(user)?.email || '';
}
});
async function logout() {
error = null;
try {
@@ -114,6 +126,32 @@
}
}
async function updateEmail() {
error = null;
updating = true;
try {
await sdk.forConsole.account.updateEmail({
email: newEmail,
password: newPassword
});
await invalidate(Dependencies.ACCOUNT);
trackEvent(Submit.AccountUpdateEmail);
resetUpdateEmailForm();
} catch (err) {
error = err.message;
trackError(err, Submit.AccountUpdateEmail);
} finally {
updating = false;
}
}
function resetUpdateEmailForm() {
showUpdateEmail = false;
newEmail = '';
newPassword = '';
error = null;
}
onMount(restoreTimerState);
onDestroy(() => {
@@ -129,48 +167,87 @@
</script>
<div class="email-verification-scrim">
<Modal
bind:show
bind:error
title="Verify your email address"
{onSubmit}
dismissible={false}
autoClose={false}
backdrop={false}>
<Card.Base variant="secondary" padding="s">
<Layout.Stack gap="xxs">
<Typography.Text gap="m">
To continue using Appwrite Cloud, please verify your email address. An email
will be sent to <Typography.Text
variant="m-600"
color="neutral-secondary"
style="display: inline;">{email || get(user)?.email}</Typography.Text>
</Typography.Text>
<Link variant="default" on:click={() => logout()}>Switch account</Link>
{#if emailSent && resendTimer > 0}
<div transition:slide={{ duration: 150 }}>
<Typography.Text
{#if !showUpdateEmail}
<Modal
bind:show
bind:error
title="Verify your email address"
{onSubmit}
dismissible={false}
autoClose={false}
backdrop={false}>
<Card.Base variant="secondary" padding="s">
<Layout.Stack gap="xxs">
<Typography.Text gap="m">
To continue using Appwrite Cloud, please verify your email address. An email
will be sent to <Typography.Text
variant="m-600"
color="neutral-secondary"
style="margin-block-start: var(--gap-L, 16px);">
Didn't get the email? Try again in {resendTimer}s
</Typography.Text>
</div>
{/if}
</Layout.Stack>
</Card.Base>
style="display: inline;">{email || get(user)?.email}</Typography.Text>
</Typography.Text>
<svelte:fragment slot="footer">
<Button
submit
submissionLoader
forceShowLoader={creating}
disabled={creating || resendTimer > 0}>
{emailSent ? 'Resend email' : 'Send email'}
</Button>
</svelte:fragment>
</Modal>
<Typography.Text>
Wrong email? <Link
variant="default"
on:click={() => {
showUpdateEmail = true;
error = null;
}}>Update email address</Link> or <Link
variant="default"
on:click={() => logout()}>Switch account</Link>
</Typography.Text>
{#if emailSent && resendTimer > 0}
<div transition:slide={{ duration: 150 }}>
<Typography.Text
color="neutral-secondary"
style="margin-block-start: var(--gap-L, 16px);">
Didn't get the email? Try again in {resendTimer}s
</Typography.Text>
</div>
{/if}
</Layout.Stack>
</Card.Base>
<svelte:fragment slot="footer">
<Button
submit
submissionLoader
forceShowLoader={creating}
disabled={creating || resendTimer > 0}>
{emailSent ? 'Resend email' : 'Send email'}
</Button>
</svelte:fragment>
</Modal>
{:else}
<Modal
bind:show={showUpdateEmail}
bind:error
title="Update email address"
onSubmit={updateEmail}
autoClose={false}
backdrop={false}>
<InputEmail
id="new-email"
label="Email"
placeholder="Enter email"
bind:value={newEmail}
required
helper="You'll need access to this email to verify your account" />
<InputPassword
id="update-password"
label="Password"
placeholder="Enter password"
required
bind:value={newPassword} />
<svelte:fragment slot="footer">
<Button text on:click={resetUpdateEmailForm}>Cancel</Button>
<Button submit submissionLoader forceShowLoader={updating} disabled={updating}
>Update</Button>
</svelte:fragment>
</Modal>
{/if}
</div>
<style>
@@ -26,7 +26,10 @@
function shouldShowTooltip(plan: Models.BillingPlan) {
if (plan.group !== BillingPlanGroup.Starter) return true;
else return !anyOrgFree;
if (!anyOrgFree) return true;
// Hide only when upgrading from Free (current org on Free, user selected Pro)
if ($organization?.billingPlanId === plan.$id && selectedPlan !== plan.$id) return true;
return false;
}
function shouldDisable(plan: Models.BillingPlan) {
@@ -45,7 +48,6 @@
name="plan"
bind:group={selectedPlan}
disabled={!selfService || shouldDisable(plan)}
tooltipShow={shouldDisable(plan)}
value={plan.$id}
title={plan.name}>
<svelte:fragment slot="action">
@@ -140,16 +140,14 @@
{#if variant === 'cname' && !subdomain}
{#if isCloud}
<Alert.Inline>
Since <Badge variant="secondary" size="s" content={domain} /> is an apex domain,
CNAME record is only supported by certain providers. If yours doesn't, please verify
using
Since <Badge variant="secondary" size="s" content={domain} /> is an apex domain, CNAME
record is only supported by certain providers. If yours doesn't, please verify using
<Link variant="muted" on:click={onNavigateToNameservers}>nameservers</Link> instead.
</Alert.Inline>
{:else if aTabVisible || aaaaTabVisible}
<Alert.Inline>
Since <Badge variant="secondary" size="s" content={domain} /> is an apex domain,
CNAME record is only supported by certain providers. If yours doesn't, please verify
using
Since <Badge variant="secondary" size="s" content={domain} /> is an apex domain, CNAME
record is only supported by certain providers. If yours doesn't, please verify using
{#if aTabVisible}
<Link variant="muted" on:click={onNavigateToA}>A record</Link>
{#if aaaaTabVisible}
+71 -10
View File
@@ -1,35 +1,96 @@
<script lang="ts">
import { Trim } from '$lib/components';
import { Link } from '$lib/elements';
import type { Models } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { type Models } from '@appwrite.io/console';
import {
IconCode,
IconExclamation,
IconGitBranch,
IconGitCommit,
IconGithub,
IconTerminal
} from '@appwrite.io/pink-icons-svelte';
import { ActionMenu, Layout, Popover, Icon } from '@appwrite.io/pink-svelte';
import {
ActionMenu,
Layout,
Popover,
Icon,
Skeleton,
Typography
} from '@appwrite.io/pink-svelte';
import Button from '$lib/elements/forms/button.svelte';
let {
deployment
deployment,
resource,
region,
project
}: {
deployment: Models.Deployment;
resource?: Models.Site | Models.Function;
region?: string;
project?: string;
} = $props();
let repository = $state<Models.ProviderRepository | null>(null);
async function loadRepository() {
if (!resource?.installationId || !resource?.providerRepositoryId || !region || !project) {
return;
}
try {
repository = await sdk.forProject(region, project).vcs.getRepository({
installationId: resource.installationId,
providerRepositoryId: resource.providerRepositoryId
});
} catch (err) {
console.warn(err);
}
}
</script>
{#if deployment.type === 'vcs'}
<Popover padding="none" let:toggle>
<div>
<Link
on:click={(e) => {
e.preventDefault();
toggle(e);
}}>
{#await loadRepository()}
<Layout.Stack direction="row" gap="xs" alignItems="center">
<Icon icon={IconGithub} size="s" /> GitHub
<Skeleton variant="line" width={100} height={20} />
</Layout.Stack>
</Link>
{:then}
<Layout.Stack direction="row" gap="xs" alignItems="center">
<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>
{#if repository?.authorized === false}
<Popover let:toggle placement="bottom-start">
<Button extraCompact on:click={toggle}>
<Icon icon={IconExclamation} size="s" color="--bgcolor-warning" />
</Button>
<svelte:fragment slot="tooltip">
<Typography.Text
variant="m-400"
color="--fgcolor-neutral-secondary">
Integration not authorized for auto deployments.<br />
To enable, add the repository to the installation settings on <Link
variant="muted"
external
href={`https://github.com/settings/installations/${repository.providerInstallationId}`}>
GitHub
</Link>.
</Typography.Text>
</svelte:fragment>
</Popover>
{/if}
</Layout.Stack>
{/await}
</div>
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
+1 -1
View File
@@ -30,7 +30,7 @@
$: upgradeURL = `${base}/organization-${freeOrganization?.$id}/change-plan`;
$: supportTimings = `${utcHourToLocaleHour('16:00')} - ${utcHourToLocaleHour('00:00')} ${localeShortTimezoneName()}`;
$: supportTimings = `${utcHourToLocaleHour('04:00')} - ${utcHourToLocaleHour('17:00')} ${localeShortTimezoneName()}`;
type SupportOption = {
cta?: string;
+2 -1
View File
@@ -13,6 +13,7 @@
export let readonly = false;
export let autofocus = false;
export let autocomplete = false;
export let helper: string = undefined;
export let leadingIcon: ComponentType | undefined = undefined;
let error: string;
@@ -49,7 +50,7 @@
on:input
on:invalid={handleInvalid}
type="email"
helper={error}
helper={error || helper}
state={error ? 'error' : 'default'}
autofocus={autofocus || undefined}
autocomplete={autocomplete ? 'on' : 'off'}
+1
View File
@@ -62,6 +62,7 @@ export type Column = PinkColumn & {
draggable?: boolean;
resizedWidth?: number;
isAction?: boolean;
sticky?: { side: 'left' | 'right'; offset?: number };
};
export function isValueOfStringEnum<T extends Record<string, string>>(
+121 -56
View File
@@ -1,7 +1,29 @@
import { writable } from 'svelte/store';
import { Resources } from '@appwrite.io/console';
import {
AppwriteMigrationResource,
FirebaseMigrationResource,
NHostMigrationResource,
SupabaseMigrationResource
} from '@appwrite.io/console';
import { includesAll } from '$lib/helpers/array';
export type MigrationResource =
| AppwriteMigrationResource
| FirebaseMigrationResource
| NHostMigrationResource
| SupabaseMigrationResource;
// Appwrite enum is the superset of all provider resources — used as a
// provider-agnostic reference. The addResource guard filters by provider.
export const ResourceType = AppwriteMigrationResource;
type ProviderResourceMap = {
appwrite: AppwriteMigrationResource[];
supabase: SupabaseMigrationResource[];
nhost: NHostMigrationResource[];
firebase: FirebaseMigrationResource[];
};
const initialFormData = {
users: {
root: false,
@@ -13,11 +35,18 @@ const initialFormData = {
},
functions: {
root: false,
env: false,
inactive: false
deploymentInactive: false
},
storage: {
root: false
},
sites: {
root: false,
deploymentInactive: false
},
messaging: {
root: false,
messages: false
}
};
@@ -48,72 +77,79 @@ export const ResourcesFriendly = {
table: { singular: 'Table', plural: 'Tables' },
index: { singular: 'Index', plural: 'Indexes' },
column: { singular: 'Column', plural: 'Columns' },
row: { singular: 'Row', plural: 'Rows' }
row: { singular: 'Row', plural: 'Rows' },
site: { singular: 'Site', plural: 'Sites' },
'site-deployment': { singular: 'Site Deployment', plural: 'Site Deployments' },
'site-variable': { singular: 'Site Variable', plural: 'Site Variables' },
provider: { singular: 'Provider', plural: 'Providers' },
topic: { singular: 'Topic', plural: 'Topics' },
subscriber: { singular: 'Subscriber', plural: 'Subscribers' },
message: { singular: 'Message', plural: 'Messages' }
};
// @todo: @itznotabug - check if other resources are correct and work fine!
export const providerResources: Record<Provider, Resources[]> = {
appwrite: Object.values(Resources),
supabase: [
Resources.User,
Resources.Database,
Resources.Collection,
Resources.Attribute,
Resources.Index,
Resources.Document,
Resources.Bucket,
Resources.File
],
nhost: [
Resources.User,
Resources.Database,
Resources.Collection,
Resources.Attribute,
Resources.Index,
Resources.Document,
Resources.Bucket,
Resources.File
],
firebase: [
Resources.User,
Resources.Database,
Resources.Collection,
Resources.Attribute,
Resources.Document,
Resources.Bucket,
Resources.File
]
export const providerResources: ProviderResourceMap = {
appwrite: Object.values(AppwriteMigrationResource),
supabase: Object.values(SupabaseMigrationResource),
nhost: Object.values(NHostMigrationResource),
firebase: Object.values(FirebaseMigrationResource)
};
export const migrationFormToResources = (
export const migrationFormToResources = <P extends Provider>(
formData: MigrationFormData,
provider: Provider
): Resources[] => {
const resources: Resources[] = [];
const addResource = (resource: Resources) => {
if (providerResources[provider].includes(resource)) {
provider: P
): ProviderResourceMap[P] => {
const resources: MigrationResource[] = [];
const providerValues = providerResources[provider] as MigrationResource[];
const addResource = (resource: MigrationResource) => {
if (providerValues.includes(resource)) {
resources.push(resource);
}
};
if (formData.users.root) {
addResource(Resources.User);
addResource(ResourceType.User);
if (formData.users.teams) {
addResource(ResourceType.Team);
addResource(ResourceType.Membership);
}
}
if (formData.databases.root) {
addResource(Resources.Database);
addResource(Resources.Table);
addResource(Resources.Column);
addResource(Resources.Index);
addResource(ResourceType.Database);
addResource(ResourceType.Table);
addResource(ResourceType.Column);
addResource(ResourceType.Index);
}
if (formData.databases.rows) {
addResource(Resources.Row);
addResource(ResourceType.Row);
}
if (formData.storage.root) {
addResource(Resources.Bucket);
addResource(Resources.File);
addResource(ResourceType.Bucket);
addResource(ResourceType.File);
}
if (formData.functions.root) {
addResource(ResourceType.Function);
addResource(ResourceType.Environmentvariable);
if (formData.functions.deploymentInactive) {
addResource(ResourceType.Deployment);
}
}
if (formData.sites.root) {
addResource(ResourceType.Site);
addResource(ResourceType.Sitevariable);
if (formData.sites.deploymentInactive) {
addResource(ResourceType.Sitedeployment);
}
}
if (formData.messaging.root) {
addResource(ResourceType.Provider);
addResource(ResourceType.Topic);
addResource(ResourceType.Subscriber);
}
if (formData.messaging.messages) {
addResource(ResourceType.Message);
}
return resources;
return resources as ProviderResourceMap[P];
};
const compareVersions = (a: string, b: string) => {
@@ -137,20 +173,49 @@ export const isVersionAtLeast = (version: string, atLeast: string) => {
return compareVersions(version, atLeast) >= 0;
};
export const resourcesToMigrationForm = (resources: Resources[]): MigrationFormData => {
export const resourcesToMigrationForm = (resources: MigrationResource[]): MigrationFormData => {
const formData = { ...initialFormData };
if (resources.includes(Resources.User)) {
if (resources.includes(ResourceType.User)) {
formData.users.root = true;
}
if (resources.includes(Resources.Database)) {
if (
includesAll(resources, [ResourceType.Team, ResourceType.Membership] as MigrationResource[])
) {
formData.users.teams = true;
}
if (resources.includes(ResourceType.Database)) {
formData.databases.root = true;
}
if (includesAll(resources, [Resources.Table, Resources.Column, Resources.Row] as Resources[])) {
if (
includesAll(resources, [
ResourceType.Table,
ResourceType.Column,
ResourceType.Row
] as MigrationResource[])
) {
formData.databases.rows = true;
}
if (includesAll(resources, [Resources.Bucket, Resources.File] as Resources[])) {
if (includesAll(resources, [ResourceType.Bucket, ResourceType.File] as MigrationResource[])) {
formData.storage.root = true;
}
if (resources.includes(ResourceType.Function)) {
formData.functions.root = true;
}
if (resources.includes(ResourceType.Deployment)) {
formData.functions.deploymentInactive = true;
}
if (resources.includes(ResourceType.Site)) {
formData.sites.root = true;
}
if (resources.includes(ResourceType.Sitedeployment)) {
formData.sites.deploymentInactive = true;
}
if (resources.includes(ResourceType.Provider)) {
formData.messaging.root = true;
}
if (resources.includes(ResourceType.Message)) {
formData.messaging.messages = true;
}
return formData;
};
+4 -2
View File
@@ -23,7 +23,8 @@ import {
Tokens,
TablesDB,
Domains,
/*DocumentsDB,*/
Webhooks,
DocumentsDB,
Realtime,
Organizations
} from '@appwrite.io/console';
@@ -139,7 +140,8 @@ const sdkForProject = {
tablesDB: new TablesDB(clientProject),
documentsDB: new DocumentsDB(clientProject),
dedicatedDatabases: new DedicatedDatabases(clientProject),
console: new Console(clientProject) // for suggestions API
console: new Console(clientProject), // for suggestions API
webhooks: new Webhooks(clientProject)
};
export const realtime = {
@@ -6,12 +6,14 @@
createMigrationFormStore,
createMigrationProviderStore,
type MigrationFormData,
type MigrationResource,
providerResources,
resourcesToMigrationForm
resourcesToMigrationForm,
ResourceType
} from '$lib/stores/migration';
import { Button } from '$lib/elements/forms';
import { wizard } from '$lib/stores/wizard';
import { Resources, type Models } from '@appwrite.io/console';
import { type Models } from '@appwrite.io/console';
import type { sdk } from '$lib/stores/sdk';
import ImportReport from '$routes/(console)/project-[region]-[project]/settings/migrations/(import)/importReport.svelte';
@@ -91,22 +93,28 @@
$: errorInResources = error;
$: wizard.setNextDisabled(!report);
$: resources = providerResources[$provider.provider];
$: resources = providerResources[$provider.provider] as MigrationResource[];
const shouldRenderGroup = (groupKey: string): boolean => {
if (groupKey === 'functions') {
// Functions not in SDK Resources enum, skip
return false;
}
if (groupKey === 'storage') {
return resources.includes(Resources.Bucket) && resources.includes(Resources.File);
return resources.includes(ResourceType.Bucket) && resources.includes(ResourceType.File);
}
// Map groupKey to Resources enum
const groupToResource: Record<string, Resources> = {
users: Resources.User,
databases: Resources.Database
if (groupKey === 'functions') {
return resources.includes(ResourceType.Function);
}
if (groupKey === 'sites') {
return resources.includes(ResourceType.Site);
}
if (groupKey === 'messaging') {
return resources.includes(ResourceType.Provider);
}
const groupToResource: Record<string, MigrationResource> = {
users: ResourceType.User,
databases: ResourceType.Database
};
const resource = groupToResource[groupKey];
return resource ? resources.includes(resource) : false;
@@ -122,7 +130,9 @@
users: 'user',
databases: 'database',
functions: 'function',
storage: 'bucket'
storage: 'bucket',
sites: 'site',
messaging: 'provider'
};
return map[groupKey] || groupKey;
};
@@ -49,8 +49,8 @@
<Form onSubmit={updateBudget}>
<CardGrid>
<svelte:fragment slot="title">Budget cap</svelte:fragment>
Restrict your resource usage by setting a budget cap. Cap usage is reset at the beginning of
each billing cycle.
Restrict your resource usage by setting a budget cap. Cap usage is reset at the beginning of each
billing cycle.
<svelte:fragment slot="aside">
{#if !currentPlan.budgeting}
<Alert.Inline status="info" title="Budget caps are a Pro plan feature">
@@ -478,7 +478,7 @@
{@const isStarter = selectedPlan.group === BillingPlanGroup.Starter}
{@const isSelfService = data.organization.billingPlanDetails.selfService}
{@const isSameGroup = data.organization.billingPlanDetails.group === selectedPlan.group}
{#if !isStarter && !isSameGroup && !isSelfService}
{#if !isStarter && !isSameGroup && isSelfService}
<EstimatedTotalBox
{collaborators}
{isDowngrade}
@@ -486,7 +486,7 @@
bind:couponData={selectedCoupon}
bind:billingBudget
organizationId={data.organization.$id} />
{:else if !data.organization.billingPlanDetails.selfService}
{:else if isSelfService}
<PlanComparisonBox downgrade={data.hasFreeOrgs ? false : isDowngrade} />
{/if}
</svelte:fragment>
@@ -81,8 +81,8 @@
<CardGrid>
<svelte:fragment slot="title">Delete organization</svelte:fragment>
The organization will be permanently deleted, including all projects and data associated
with this organization. This action is irreversible.
The organization will be permanently deleted, including all projects and data associated with
this organization. This action is irreversible.
<svelte:fragment slot="aside">
<BoxAvatar>
<svelte:fragment slot="image">
@@ -16,8 +16,8 @@
<CardGrid>
<svelte:fragment slot="title">DPA</svelte:fragment>
After downloading, have the DPA signed by your organization's compliance authority, such as your
CEO or Compliance Manager, and submit it to
After downloading, have the DPA signed by your organization's compliance authority, such as your CEO
or Compliance Manager, and submit it to
<a class="link" href="mailto:privacy@appwrite.io">privacy@appwrite.io</a>.
<svelte:fragment slot="aside">
<Box>
@@ -44,8 +44,8 @@
<Modal {error} onSubmit={update} bind:show on:close title={`${provider.name} OAuth2 settings`}>
<p slot="description">
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
To use {provider.name} authentication in your application, first fill in this form. For more info
you can
<Link.Anchor
class="link"
href={oAuthProvider?.docs}
@@ -45,8 +45,8 @@
<Modal {error} bind:show onSubmit={update} on:close title={`${provider.name} OAuth2 settings`}>
<svelte:fragment slot="description">
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
To use {provider.name} authentication in your application, first fill in this form. For more info
you can
<Link.Anchor href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
>visit the docs.</Link.Anchor>
</svelte:fragment>
@@ -46,8 +46,8 @@
<Modal {error} bind:show onSubmit={update} on:close title={`${provider.name} OAuth2 settings`}>
<p slot="description">
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
To use {provider.name} authentication in your application, first fill in this form. For more info
you can
<Link.Anchor
class="link"
href={oAuthProvider?.docs}
@@ -44,8 +44,8 @@
<Modal {error} bind:show onSubmit={update} on:close title={`${provider.name} OAuth2 settings`}>
<p slot="description">
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
To use {provider.name} authentication in your application, first fill in this form. For more info
you can
<Link.Anchor href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
>visit the docs.</Link.Anchor>
</p>
@@ -43,8 +43,8 @@
<svelte:fragment slot="title">{provider.name} OAuth2 settings</svelte:fragment>
<p slot="description">
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
To use {provider.name} authentication in your application, first fill in this form. For more info
you can
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
>visit the docs.</a>
</p>
@@ -41,8 +41,8 @@
<Modal {error} bind:show onSubmit={update} on:close title={`${provider.name} OAuth2 settings`}>
<p slot="description">
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
To use {provider.name} authentication in your application, first fill in this form. For more info
you can
<Link.Anchor
class="link"
href={oAuthProvider?.docs}
@@ -45,8 +45,8 @@
<Modal {error} onSubmit={update} bind:show on:close title={`${provider.name} OAuth2 settings`}>
<p slot="description">
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
To use {provider.name} authentication in your application, first fill in this form. For more info
you can
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer">
visit the docs.
</a>
@@ -67,8 +67,8 @@
<Modal {error} onSubmit={update} title="{provider.name} OAuth2 settings" bind:show on:close>
<p slot="description">
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
To use {provider.name} authentication in your application, first fill in this form. For more info
you can
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
>visit the docs.</a>
</p>
@@ -55,8 +55,8 @@
on:close
title={`${provider.name} OAuth2 settings`}>
<p slot="description">
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
To use {provider.name} authentication in your application, first fill in this form. For more info
you can
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
>visit the docs.</a>
</p>
@@ -7,6 +7,7 @@
import UpdateName from './updateName.svelte';
import UpdatePassword from './updatePassword.svelte';
import UpdatePhone from './updatePhone.svelte';
import UpdateImpersonation from './updateImpersonation.svelte';
import UpdatePrefs from './updatePrefs.svelte';
import UpdateStatus from './updateStatus.svelte';
import type { PageData } from './$types';
@@ -22,6 +23,7 @@
<UpdatePassword />
<UpdateLabels />
<UpdatePrefs />
<UpdateImpersonation />
<UpdateMfa />
<DangerZone project={data.project} />
</Container>
@@ -43,8 +43,8 @@
More info
</Tag>
<p slot="tooltip">
User identities are the user's connected OAuth accounts. <br /> The user can
sign in using these identities.
User identities are the user's connected OAuth accounts. <br /> The user can sign
in using these identities.
</p>
</Popover>
</div>
@@ -25,8 +25,8 @@
</Tag>
<p slot="tooltip">
User targets include emails, phone numbers, and devices with your app
installed.<br /> These targets can subscribe to a topic and receive messages
published to it.
installed.<br /> These targets can subscribe to a topic and receive messages published
to it.
</p>
</Popover>
</div>
@@ -0,0 +1,84 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Alert, Layout, Link, Selector, Typography } from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
import { user } from './store';
let impersonator = false;
$: currentImpersonator = Boolean($user?.impersonator);
onMount(() => {
impersonator = currentImpersonator;
});
async function updateImpersonator() {
try {
await sdk.forProject(page.params.region, page.params.project).users.updateImpersonator({
userId: $user.$id,
impersonator
});
await invalidate(Dependencies.USER);
addNotification({
message: `Impersonation capability has been ${impersonator ? 'enabled' : 'disabled'}`,
type: 'success'
});
trackEvent(Submit.UserUpdateImpersonation);
} catch (error) {
impersonator = currentImpersonator;
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.UserUpdateImpersonation);
}
}
</script>
<Form onSubmit={updateImpersonator}>
<CardGrid>
<svelte:fragment slot="title">User impersonation</svelte:fragment>
<svelte:fragment slot="aside">
<Layout.Stack gap="m">
<Selector.Switch
id="user-impersonation"
bind:checked={impersonator}
label="User impersonation">
<svelte:fragment slot="description">
<div class="u-margin-block-start-8">
<Typography.Text variant="m-400">
Enabling this option allows this user to impersonate other users
through your app using the Appwrite Client SDK.
<Link.Anchor
target="_blank"
rel="noopener noreferrer"
href="https://appwrite.io/docs/products/auth">
Learn more.
</Link.Anchor>
</Typography.Text>
</div>
</svelte:fragment>
</Selector.Switch>
<Alert.Inline status="info">
Only grant this permission to trusted operator or support accounts. Actions are
logged under the account that initiated impersonation.
</Alert.Inline>
</Layout.Stack>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={impersonator === currentImpersonator} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -24,6 +24,7 @@
import { type Entity, getTerminologies } from '$database/(entity)';
import { resolveRoute, withPath } from '$lib/stores/navigation';
import { columnOptions as baseColumnOptions } from '$database/table-[table]/columns/store';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
let {
entity,
@@ -66,12 +67,18 @@
length: number | null;
}> = $state([{ value: '', order: null, length: null }]);
const types = [
{ value: IndexType.Key, label: 'Key' },
{ value: IndexType.Unique, label: 'Unique' },
{ value: IndexType.Fulltext, label: 'Fulltext' },
{ value: IndexType.Spatial, label: 'Spatial' }
];
const types = $derived(
[
{ value: IndexType.Key, label: 'Key' },
{ value: IndexType.Unique, label: 'Unique' },
{ value: IndexType.Fulltext, label: 'Fulltext' },
{ value: IndexType.Spatial, label: 'Spatial' }
].filter((type) => {
if (type.value === IndexType.Spatial && !$regionalConsoleVariables?.supportForSpatials)
return false;
return true;
})
);
// order options derived from selected type
let orderOptions = $derived.by(() =>
@@ -74,8 +74,8 @@
const spreadsheetColumns = $derived([
{
id: 'key',
width: getColumnWidth('key', $isSmallViewport ? 250 : 200),
minimumWidth: $isSmallViewport ? 250 : 200,
width: getColumnWidth('key', $isSmallViewport ? 250 : 280),
minimumWidth: $isSmallViewport ? 250 : 280,
resizable: true
},
{
@@ -38,7 +38,8 @@
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { isWithinSafeRange } from '$lib/helpers/numbers';
import { columnOptions } from '../table-[table]/columns/store';
import type { Columns } from '../table-[table]/store';
import { columnOptions, getSupportedColumns } from '../table-[table]/columns/store';
import Options from './options.svelte';
import { InputSelect, InputText } from '$lib/elements/forms';
import { isCloud, VARS } from '$lib/system';
@@ -46,6 +47,7 @@
import IconAINotification from './icon/aiNotification.svelte';
import type { Models } from '@appwrite.io/console';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
let {
userColumns = [],
@@ -56,6 +58,7 @@
} = $props();
const tableId = page.params.table;
const supportedColumns = $derived(getSupportedColumns($regionalConsoleVariables));
const minimumUserColumnWidth = 168;
function getUserColumnWidth(
@@ -1359,7 +1362,7 @@
</Spreadsheet.Header.Cell>
{:else}
{@const columnObj = getColumn(column.id)}
{@const columnIcon = basicColumnOptions.find(
{@const columnIcon = columnOptions.find(
(col) => col.type === columnObj?.type
)?.icon}
{@const columnIconColor = !columnObj?.type
@@ -1516,7 +1519,7 @@
});
}
}}
options={basicColumnOptions.map((col) => {
options={supportedColumns.map((col) => {
return {
label: col.name,
value: col.name,
@@ -1835,7 +1838,7 @@
gap="none"
direction="column"
class="filter-modal-actions-menu variant">
{#each basicColumnOptions as option}
{#each supportedColumns as option}
<ActionMenu.Item.Button
on:click={() => {
toggle();
@@ -10,6 +10,7 @@
import { capitalize } from '$lib/helpers/string';
import type { Columns } from '$database/store';
import { isRelationship } from '../table-[table]/rows/store';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import { VARS } from '$lib/system';
import { sleep } from '$lib/helpers/promises';
import { sdk } from '$lib/stores/sdk';
@@ -291,10 +292,18 @@
return false; // close the sheet!
}
const typeOptions = Object.values(IndexType).map((type) => ({
label: capitalize(type),
value: type
}));
const typeOptions = $derived(
Object.values(IndexType)
.filter((type) => {
if (type === IndexType.Spatial && !$regionalConsoleVariables?.supportForSpatials)
return false;
return true;
})
.map((type) => ({
label: capitalize(type),
value: type
}))
);
onMount(() => showIndexesSuggestions.set(false));
@@ -1,13 +1,16 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { ActionMenu, Icon, Popover } from '@appwrite.io/pink-svelte';
import { columnOptions, type Option } from './store';
import { getSupportedColumns, type Option } from './store';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { isTablesCsvImportInProgress } from '../store';
import { CsvDisabled } from '$database/(entity)';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
export let showCreate = false;
export let selectedOption: Option['name'] = null;
$: options = getSupportedColumns($regionalConsoleVariables);
</script>
{#if $isTablesCsvImportInProgress}
@@ -26,7 +29,7 @@
</Button>
</slot>
<ActionMenu.Root slot="tooltip">
{#each columnOptions as column}
{#each options as column}
<ActionMenu.Item.Button
leadingIcon={column.icon}
on:click={() => {
@@ -17,6 +17,7 @@ import Point, { submitPoint, updatePoint } from './point.svelte';
import Line, { submitLine, updateLine } from './line.svelte';
import Polygon, { submitPolygon, updatePolygon } from './polygon.svelte';
import type { Columns } from '$database/store';
import type { Models } from '@appwrite.io/console';
import Relationship, { submitRelationship, updateRelationship } from './relationship.svelte';
import {
IconCalendar,
@@ -248,3 +249,12 @@ export const columnOptions: Option[] = [
];
export const option = writable<Option>();
export function getSupportedColumns(consoleVariables: Models.ConsoleVariables): Option[] {
const spatialTypes: Set<Option['type']> = new Set(['point', 'linestring', 'polygon']);
return columnOptions.filter((col) => {
if (col.type === 'relationship' && !consoleVariables?.supportForRelationships) return false;
if (spatialTypes.has(col.type) && !consoleVariables?.supportForSpatials) return false;
return true;
});
}
@@ -6,8 +6,9 @@
import { InputSelect, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { option, columnOptions, type Option } from '$database/table-[table]/columns/store';
import { option, getSupportedColumns, type Option } from '$database/table-[table]/columns/store';
import type { Column } from '$lib/helpers/types';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import { preferences } from '$lib/stores/preferences';
import { onMount } from 'svelte';
@@ -52,8 +53,10 @@
...column
} as Partial<Columns>);
let availableOptions = $derived(getSupportedColumns($regionalConsoleVariables));
let ColumnComponent = $derived(
columnOptions.find((option) => option.name === selectedOption).component
(availableOptions.find((option) => option.name === selectedOption) ?? availableOptions[0])
.component
);
function init() {
@@ -68,7 +71,7 @@
/* default to text */
selectedOption = 'Text';
$option = columnOptions[0];
$option = availableOptions[0];
}
function insertColumnInOrder() {
@@ -184,7 +187,15 @@
// correct view
if (selectedOption) {
$option = columnOptions.find((option) => option.name === selectedOption);
const resolved =
availableOptions.find((option) => option.name === selectedOption) ??
availableOptions[0];
$option = resolved;
if (resolved && resolved.name !== selectedOption) {
selectedOption = resolved.name;
}
}
});
</script>
@@ -221,7 +232,7 @@
id="type"
label="Type"
bind:value={selectedOption}
options={columnOptions.map((attr) => {
options={availableOptions.map((attr) => {
return {
label: attr.name,
value: attr.name,
@@ -23,7 +23,7 @@
onUpdateFormValues?: (formValues: object) => void;
} = $props();
let formStore = writable(formValues);
let formStore = writable(formValues ?? {});
function removeArrayItem(key: string, index: number) {
const currentArray = Array.isArray($formStore[key]) ? $formStore[key] : [];
@@ -8,6 +8,7 @@
import { addNotification } from '$lib/stores/notifications';
import { preferences } from '$lib/stores/preferences';
import { sdk } from '$lib/stores/sdk';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import { type Models, Query } from '@appwrite.io/console';
import { type ComponentType, onDestroy, onMount } from 'svelte';
import type { PageData } from './$types';
@@ -109,6 +110,8 @@
};
$: table = data.table;
/** Keep sort in sync after client navigations (e.g. filters); onMount alone misses SPA loads. */
$: sortState.set(data.currentSort as SortState);
$: rows = writable(data.rows);
$: if ($rows) {
paginatedRows.clear();
@@ -119,7 +122,9 @@
}
// create index map for O(1) row lookups, reactive!
$: rowIndexMap = new Map($paginatedRows.items.map((row, index) => [row.$id, index]));
$: rowIndexMap = new Map(
$paginatedRows.items.flatMap((r, index) => (r ? [[r.$id, index]] : []))
);
const tableId = page.params.table;
const databaseId = page.params.database;
@@ -135,7 +140,6 @@
: 24;
let selectedRows = [];
let showExpandIconForId: number | null = null;
let spreadsheetContainer: SpreadsheetContainer;
let previouslyFocusedElement: Element | null = null;
@@ -213,6 +217,7 @@
width: getColumnWidth('$id', 250),
minimumWidth: 250,
draggable: false,
sticky: $isSmallViewport ? undefined : { side: 'left', offset: 40 },
type: 'string',
icon: IconFingerPrint,
isEditable: false,
@@ -795,7 +800,7 @@
$: canShowDatetimePopover = true;
$: if (table.fields) {
$: if (table.fields && typeof $isSmallViewport === 'boolean') {
makeTableColumns();
}
@@ -809,6 +814,24 @@
previouslyFocusedElement = null;
});
}
function getSpreadsheetCellProps(
rowId: string | undefined,
columnId: string | undefined,
state
) {
if (columnId !== '$id' || !rowId || state?.isHeader || state?.isEmptyRow) {
return undefined;
}
return {
style: `
--row-expand-opacity: ${state?.hovered ? '1' : '0'};
--row-expand-pointer-events: ${state?.hovered ? 'auto' : 'none'};
--row-expand-transform: ${state?.hovered ? 'translateX(0)' : 'translateX(4px)'};
`
};
}
</script>
<SpreadsheetContainer bind:this={spreadsheetContainer}>
@@ -818,9 +841,11 @@
allowSelection
useVirtualizer
keyboardNavigation
showScrollbars
bind:selectedRows
selection={rowSelection}
bind:columns={$tableColumns}
getCellProps={getSpreadsheetCellProps}
loading={$spreadsheetLoading}
emptyCells={emptyCellsCount}
rowCount={$paginatedRows.virtualLength}
@@ -903,7 +928,8 @@
<svelte:fragment slot="rows" let:root let:item let:index>
{@const row = $paginatedRows.getItemAtVirtualIndex(index)}
{#if row === null}
<!-- !row: virtualizer can briefly yield null/undefined while row data reloads (e.g. after filters). -->
{#if !row}
<Spreadsheet.Row.Base
{root}
{index}
@@ -927,87 +953,83 @@
select={rowSelection}
hoverEffect
showSelectOnHover
valueWithoutHover={row.$sequence}>
valueWithoutHover={$regionalConsoleVariables?.supportForIntegerIds
? row?.$sequence
: undefined}>
{#each $tableColumns as { id: columnId, isEditable, hide } (columnId)}
{@const rowColumn = $columns.find((col) => col.key === columnId)}
{#if columnId === '$id' && !hide}
<button
on:mouseenter={() => {
showExpandIconForId = index;
}}
on:mouseleave={() => {
showExpandIconForId = null;
}}>
<Spreadsheet.Cell {root} {isEditable} column={columnId}>
<Layout.Stack
gap="none"
direction="row"
alignItems="center"
alignContent="center"
justifyContent="space-between">
<Id value={row.$id} tooltipPortal tooltipDelay={200}
>{row.$id}</Id>
<Spreadsheet.Cell {root} {isEditable} column={columnId}>
<Layout.Stack
class="row-id-cell-content"
gap="none"
direction="row"
alignItems="center"
alignContent="center"
justifyContent="space-between">
<Id value={row?.$id} tooltipPortal tooltipDelay={200}>
{row?.$id}
</Id>
<Popover let:show let:hide portal padding="none">
{@const opacityValue =
showExpandIconForId === index ? '1' : '0'}
<button
on:mouseenter={show}
on:mouseleave={hide}
style:opacity={opacityValue}
style:transition="opacity 225ms ease-in-out">
<Button.Button
size="xs"
icon
variant="secondary"
on:click={() => {
hide();
previouslyFocusedElement =
document.activeElement;
$databaseRowSheetOptions.autoFocus = false;
onSelectSheetOption(
'update',
null,
null,
'row',
row
);
}}>
<Icon icon={IconArrowExpand} size="s" />
</Button.Button>
</button>
<Popover let:show let:hide portal padding="none">
<div
class="row-expand-trigger"
on:mouseenter={show}
on:mouseleave={hide}>
<Button.Button
size="xs"
icon
variant="secondary"
on:click={() => {
hide();
previouslyFocusedElement =
document.activeElement;
$databaseRowSheetOptions.autoFocus = false;
onSelectSheetOption(
'update',
null,
null,
'row',
row
);
}}>
<Icon icon={IconArrowExpand} size="s" />
</Button.Button>
</div>
<svelte:fragment slot="tooltip">
<Layout.Stack
inline
gap="xxs"
direction="row"
alignItems="center"
alignContent="center"
style="padding: var(--gap-XS, 6px) var(--gap-S, 8px);">
Expand row
<svelte:fragment slot="tooltip">
<Layout.Stack
inline
gap="xxs"
gap="xxxs"
direction="row"
alignItems="center"
alignContent="center"
style="padding: var(--gap-XS, 6px) var(--gap-S, 8px);">
Expand row
<Layout.Stack
inline
gap="xxxs"
direction="row"
alignItems="center"
alignContent="center">
<Keyboard key="⌘" />
<Keyboard key={'Enter'} autoWidth />
</Layout.Stack>
alignContent="center">
<Keyboard size="s" key="⌘" />
<Keyboard
key={'Enter'}
autoWidth
size="s" />
</Layout.Stack>
</svelte:fragment>
</Popover>
</Layout.Stack>
</Spreadsheet.Cell>
</button>
</Layout.Stack>
</svelte:fragment>
</Popover>
</Layout.Stack>
</Spreadsheet.Cell>
{:else}
<Spreadsheet.Cell {root} {isEditable} column={columnId}>
{#if columnId === '$createdAt' || columnId === '$updatedAt'}
<DualTimeView
showDatetime
time={row[columnId]}
time={row?.[columnId]}
canShowPopover={canShowDatetimePopover} />
{:else if columnId === 'actions'}
<SpreadsheetOptions
@@ -1031,12 +1053,12 @@
</Button.Button>
{/snippet}
</SpreadsheetOptions>
{:else if isRelationship(rowColumn)}
{@const args = getDisplayNamesForTable(row[columnId])}
{:else if rowColumn && isRelationship(rowColumn)}
{@const args = getDisplayNamesForTable(row?.[columnId])}
{#if !isRelationshipToMany(rowColumn)}
{#if row[columnId]}
{#if row?.[columnId]}
{@const displayValue = args
.map((arg) => row[columnId]?.[arg])
.map((arg) => row?.[columnId]?.[arg])
.filter(Boolean)
.join(' | ')}
@@ -1045,9 +1067,9 @@
variant="muted"
on:click={() => {
$databaseRelatedRowSheetOptions.tableId =
row[columnId]?.['$tableId'];
row?.[columnId]?.['$tableId'];
$databaseRelatedRowSheetOptions.rows =
row[columnId]?.['$id'];
row?.[columnId]?.['$id'];
$databaseRelatedRowSheetOptions.show = true;
}}>
{displayValue}
@@ -1065,19 +1087,23 @@
size="xs" />
{/if}
{:else}
{@const itemsNum = row[columnId]?.length}
{@const itemsNum = row?.[columnId]?.length}
Items <Badge
content={itemsNum}
variant="secondary"
size="s" />
{/if}
{:else if isSpatialType(rowColumn) && row[columnId] !== null}
{:else if rowColumn && isSpatialType(rowColumn) && row?.[columnId] !== null}
<Typography.Text truncate>
{JSON.stringify(row[columnId])}
{JSON.stringify(row?.[columnId])}
</Typography.Text>
{:else if !rowColumn}
<Typography.Text truncate>
{formatColumn(row?.[columnId])}
</Typography.Text>
{:else}
{@const value = row[columnId]}
{@const formatted = formatColumn(row[columnId])}
{@const value = row?.[columnId]}
{@const formatted = formatColumn(row?.[columnId])}
{@const isEmptyArray = formatted === 'Empty'}
{@const isDatetimeAttribute = rowColumn.type === 'datetime'}
{@const isEncryptedAttribute =
@@ -1124,44 +1150,47 @@
{/if}
<svelte:fragment slot="cell-editor" let:close>
{@const isRelatedToMany = isRelationshipToMany(rowColumn)}
{@const hasItems = isRelatedToMany
? row[columnId]?.length
: false}
{#if rowColumn}
{@const isRelatedToMany =
isRelationshipToMany(rowColumn)}
{@const hasItems = isRelatedToMany
? row?.[columnId]?.length
: false}
<EditRowCell
{row}
column={rowColumn}
onRowStructureUpdate={async (row) => {
const success = await updateRowContents(row);
if (success) {
// database update succeeded!
paginatedRows.update(index, row);
}
return success;
}}
noInlineEdit={isRelatedToMany && hasItems}
onChange={(row) => paginatedRows.update(index, row)}
onRevert={(row) => paginatedRows.update(index, row)}
openSideSheet={() => {
close(); /* closes the editor */
<EditRowCell
{row}
column={rowColumn}
onRowStructureUpdate={async (row) => {
const success = await updateRowContents(row);
if (success) {
// database update succeeded!
paginatedRows.update(index, row);
}
return success;
}}
noInlineEdit={isRelatedToMany && hasItems}
onChange={(row) => paginatedRows.update(index, row)}
onRevert={(row) => paginatedRows.update(index, row)}
openSideSheet={() => {
close(); /* closes the editor */
if (isRelationshipToMany(rowColumn)) {
openSideSheetForRelationsToMany(
row[columnId],
rowColumn
);
} else {
$databaseRowSheetOptions.autoFocus = true;
onSelectSheetOption(
'update',
null,
null,
'row',
row
);
}
}} />
if (isRelationshipToMany(rowColumn)) {
openSideSheetForRelationsToMany(
row?.[columnId],
rowColumn
);
} else {
$databaseRowSheetOptions.autoFocus = true;
onSelectSheetOption(
'update',
null,
null,
'row',
row
);
}
}} />
{/if}
</svelte:fragment>
</Spreadsheet.Cell>
{/if}
@@ -1392,4 +1421,19 @@
top: 0 !important;
}
}
.row-id-cell-content {
min-width: 0;
}
.row-expand-trigger {
display: flex;
flex: 0 0 auto;
opacity: var(--row-expand-opacity, 0);
pointer-events: var(--row-expand-pointer-events, none);
transition:
opacity 225ms ease-in-out,
transform 225ms ease-in-out;
transform: var(--row-expand-transform, translateX(4px));
}
</style>
@@ -105,7 +105,7 @@
commands: installCommand || undefined,
scopes: selectedScopes?.length ? selectedScopes : undefined,
providerSilentMode: false,
specification: specification || undefined
buildSpecification: specification || undefined
});
// Add domain
@@ -73,7 +73,7 @@
enabled: true,
entrypoint,
commands: buildCommand,
specification: specification || undefined
buildSpecification: specification || undefined
});
// Add domain
@@ -119,7 +119,7 @@
providerBranch: branch,
providerSilentMode: silentMode,
providerRootDirectory: rootDir,
specification: specification || undefined
buildSpecification: specification || undefined
});
// Add domain
@@ -154,7 +154,7 @@
providerBranch: branch,
providerSilentMode: silentMode,
providerRootDirectory: rootDir,
specification: specification || undefined
buildSpecification: specification || undefined
});
// Add domain
@@ -187,7 +187,11 @@
{@render titleSnippet('Source')}
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
<div>
<DeploymentSource {deployment} />
<DeploymentSource
{deployment}
resource={$func}
region={page.params.region}
project={page.params.project} />
</div>
</Typography.Text>
</Layout.Stack>
@@ -5,7 +5,15 @@ export const load = async ({ params, depends, parent }) => {
depends(Dependencies.VARIABLES);
depends(Dependencies.FUNCTION);
const { runtimesList, specificationsList } = await parent();
const { runtimesList, specificationsList, function: fn } = await parent();
const enabledSpecs = specificationsList?.specifications?.filter((s) => s.enabled) ?? [];
if (!enabledSpecs.some((s) => s.slug === fn.buildSpecification)) {
fn.buildSpecification = enabledSpecs[0]?.slug;
}
if (!enabledSpecs.some((s) => s.slug === fn.runtimeSpecification)) {
fn.runtimeSpecification = enabledSpecs[0]?.slug;
}
const [globalVariables, variables] = await Promise.all([
sdk.forProject(params.region, params.project).projectApi.listVariables(),
@@ -36,6 +44,7 @@ export const load = async ({ params, depends, parent }) => {
variables,
globalVariables,
runtimesList,
specificationsList
specificationsList,
function: fn
};
};
@@ -36,7 +36,7 @@
providerBranch: func.providerBranch || undefined,
providerSilentMode: func.providerSilentMode || undefined,
providerRootDirectory: func.providerRootDirectory || undefined,
specification: func.specification || undefined
buildSpecification: func.buildSpecification || undefined
});
await invalidate(Dependencies.FUNCTION);
@@ -47,7 +47,7 @@
providerBranch: $func.providerBranch || undefined,
providerSilentMode: $func.providerSilentMode || undefined,
providerRootDirectory: $func.providerRootDirectory || undefined,
specification: $func.specification || undefined
buildSpecification: $func.buildSpecification || undefined
});
await invalidate(Dependencies.FUNCTION);
addNotification({
@@ -37,7 +37,7 @@
providerBranch: func.providerBranch || undefined,
providerSilentMode: func.providerSilentMode || undefined,
providerRootDirectory: func.providerRootDirectory || undefined,
specification: func.specification || undefined
buildSpecification: func.buildSpecification || undefined
});
await invalidate(Dependencies.FUNCTION);
addNotification({
@@ -42,7 +42,7 @@
providerBranch: $func.providerBranch || undefined,
providerSilentMode: $func.providerSilentMode || undefined,
providerRootDirectory: $func.providerRootDirectory || undefined,
specification: $func.specification || undefined
buildSpecification: $func.buildSpecification || undefined
});
await invalidate(Dependencies.FUNCTION);
addNotification({
@@ -47,7 +47,7 @@
providerBranch: $func.providerBranch || undefined,
providerSilentMode: $func.providerSilentMode || undefined,
providerRootDirectory: $func.providerRootDirectory || undefined,
specification: $func.specification || undefined
buildSpecification: $func.buildSpecification || undefined
});
await invalidate(Dependencies.FUNCTION);
addNotification({
@@ -9,7 +9,8 @@
import { isValueOfStringEnum } from '$lib/helpers/types';
import { Runtime, type Models, type Scopes } from '@appwrite.io/console';
import Link from '$lib/elements/link.svelte';
import { Alert } from '@appwrite.io/pink-svelte';
import { Alert, Icon, Tooltip } from '@appwrite.io/pink-svelte';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { isStarterPlan, getChangePlanUrl } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
@@ -18,11 +19,23 @@
export let func: Models.Function;
export let specs: Models.SpecificationList;
let specification = func.specification;
let originalSpecification = func.specification;
$: originalSpecification = func.specification;
let buildSpecification = func.buildSpecification;
let runtimeSpecification = func.runtimeSpecification;
let originalBuild = func.buildSpecification;
let originalRuntime = func.runtimeSpecification;
async function updateLogging() {
let functionId: string | undefined = undefined;
$: if (func.$id !== functionId) {
functionId = func.$id;
buildSpecification = func.buildSpecification;
runtimeSpecification = func.runtimeSpecification;
}
$: {
originalBuild = func.buildSpecification;
originalRuntime = func.runtimeSpecification;
}
async function updateResourceLimits() {
try {
if (!isValueOfStringEnum(Runtime, func.runtime)) {
throw new Error(`Invalid runtime: ${func.runtime}`);
@@ -45,24 +58,23 @@
providerBranch: func.providerBranch || undefined,
providerSilentMode: func.providerSilentMode || undefined,
providerRootDirectory: func.providerRootDirectory || undefined,
specification: specification || undefined
buildSpecification: buildSpecification || undefined,
runtimeSpecification: runtimeSpecification || undefined
});
await invalidate(Dependencies.FUNCTION);
originalSpecification = specification;
addNotification({
type: 'success',
message: 'Resource limits have been updated'
});
trackEvent(Submit.FunctionUpdateLogging);
trackEvent(Submit.FunctionUpdateResourceLimits);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.FunctionUpdateLogging);
trackError(error, Submit.FunctionUpdateResourceLimits);
}
}
@@ -73,7 +85,7 @@
}));
</script>
<Form onSubmit={updateLogging}>
<Form onSubmit={updateResourceLimits}>
<CardGrid>
<svelte:fragment slot="title">Resource limits</svelte:fragment>
Define your function's compute specifications, including CPU and memory, to optimize performance
@@ -84,12 +96,33 @@
</Link>.
<svelte:fragment slot="aside">
<InputSelect
label="CPU and memory"
id="resources"
label="Build specification"
id="build-specification"
required
disabled={options.length < 1}
bind:value={specification}
{options} />
bind:value={buildSpecification}
{options}>
<Tooltip slot="info">
<Icon icon={IconInfo} size="s" />
<span slot="tooltip">
CPU and memory used when building and packaging your function deployment.
</span>
</Tooltip>
</InputSelect>
<InputSelect
label="Runtime specification"
id="runtime-specification"
required
disabled={options.length < 1}
bind:value={runtimeSpecification}
{options}>
<Tooltip slot="info">
<Icon icon={IconInfo} size="s" />
<span slot="tooltip">
CPU and memory available to each function execution at runtime.
</span>
</Tooltip>
</InputSelect>
<!-- always show upgrade on starters -->
{@const isStarter = isStarterPlan($organization.billingPlanId)}
@@ -104,7 +137,10 @@
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={originalSpecification === specification} submit>Update</Button>
<Button
disabled={originalBuild === buildSpecification &&
originalRuntime === runtimeSpecification}
submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -46,7 +46,7 @@
providerBranch: $func.providerBranch || undefined,
providerSilentMode: $func.providerSilentMode || undefined,
providerRootDirectory: $func.providerRootDirectory || undefined,
specification: $func.specification || undefined
buildSpecification: $func.buildSpecification || undefined
});
await invalidate(Dependencies.FUNCTION);
addNotification({
@@ -47,7 +47,7 @@
providerBranch: $func.providerBranch || undefined,
providerSilentMode: $func.providerSilentMode || undefined,
providerRootDirectory: $func.providerRootDirectory || undefined,
specification: $func.specification || undefined
buildSpecification: $func.buildSpecification || undefined
});
await invalidate(Dependencies.FUNCTION);
addNotification({
@@ -45,7 +45,7 @@
providerBranch: $func.providerBranch || undefined,
providerSilentMode: $func.providerSilentMode || undefined,
providerRootDirectory: $func.providerRootDirectory || undefined,
specification: $func.specification || undefined
buildSpecification: $func.buildSpecification || undefined
});
await invalidate(Dependencies.FUNCTION);
addNotification({
@@ -42,7 +42,7 @@
providerBranch: $func.providerBranch || undefined,
providerSilentMode: $func.providerSilentMode || undefined,
providerRootDirectory: $func.providerRootDirectory || undefined,
specification: $func.specification || undefined
buildSpecification: $func.buildSpecification || undefined
});
await invalidate(Dependencies.FUNCTION);
addNotification({
@@ -1,4 +1,4 @@
<script lang="ts" context="module">
<script lang="ts" module>
import { total } from '$lib/helpers/array';
import { clampMin } from '$lib/helpers/numbers';
import type { Metric } from '$lib/sdk/usage';
@@ -16,6 +16,7 @@
import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
import { PlatformsPanel } from '$lib/commandCenter/panels';
import { Tab, Tabs } from '$lib/components';
import { isTabSelected } from '$lib/helpers/load';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { Container, type UsagePeriods } from '$lib/layout';
import { onMount, setContext, type Component } from 'svelte';
@@ -30,8 +31,8 @@
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { isSmallViewport } from '$lib/stores/viewport';
let period: UsagePeriods = '30d';
$: path = `${base}/project-${page.params.region}-${page.params.project}/overview`;
let period: UsagePeriods = $state('30d');
const path = $derived(`${base}/project-${page.params.region}-${page.params.project}/overview`);
onMount(handle);
afterNavigate(handle);
@@ -52,45 +53,64 @@
return usage.load(dates.start, dates.end, dates.period);
}
function isTabSelected(key: string) {
return page.url.pathname.endsWith(`/${key}`);
}
$: $registerCommands([
const integrationTabs = $derived([
{
label: 'Add platform',
keys: ['a', 'p'],
callback() {
addSubPanel(PlatformsPanel);
},
icon: IconPlus,
group: 'integrations',
disabled: !$canWriteProjects
href: `${path}/platforms`,
title: 'Platforms',
event: 'platforms',
hasChildren: true
},
{
label: 'Create API Key',
icon: IconPlus,
callback() {
goto(`${path}/api-keys/create`);
},
keys: ['c', 'k'],
group: 'integrations',
disabled: !$canWriteProjects
href: `${path}/api-keys`,
title: 'API keys',
event: 'api-keys',
hasChildren: true
},
{
label: 'Create Dev Key',
icon: IconPlus,
callback() {
goto(`${path}/dev-keys/create`);
},
keys: ['c', 'd', 'k'],
group: 'integrations',
disabled: !$canWriteProjects
href: `${path}/dev-keys`,
title: 'Dev keys',
event: 'dev-keys',
hasChildren: true
}
]);
$: $updateCommandGroupRanks({
integrations: 10
$effect(() => {
const unregister = $registerCommands([
{
label: 'Add platform',
keys: ['a', 'p'],
callback() {
addSubPanel(PlatformsPanel);
},
icon: IconPlus,
group: 'integrations',
disabled: !$canWriteProjects
},
{
label: 'Create API Key',
icon: IconPlus,
callback() {
goto(`${path}/api-keys/create`);
},
keys: ['c', 'k'],
group: 'integrations',
disabled: !$canWriteProjects
},
{
label: 'Create Dev Key',
icon: IconPlus,
callback() {
goto(`${path}/dev-keys/create`);
},
keys: ['c', 'd', 'k'],
group: 'integrations',
disabled: !$canWriteProjects
}
]);
$updateCommandGroupRanks({
integrations: 10
});
return unregister;
});
</script>
@@ -223,21 +243,14 @@
<Typography.Title>Integrations</Typography.Title>
<Layout.Stack gap="xl" direction="row" justifyContent="space-between">
<Tabs>
<Tab
noscroll
event="platforms"
href={`${path}/platforms`}
selected={isTabSelected('platforms')}>Platforms</Tab>
<Tab
noscroll
event="api-keys"
href={`${path}/api-keys`}
selected={isTabSelected('api-keys')}>API keys</Tab>
<Tab
noscroll
event="dev-keys"
href={`${path}/dev-keys`}
selected={isTabSelected('dev-keys')}>Dev keys</Tab>
{#each integrationTabs as tab}
<Tab
noscroll
href={tab.href}
event={tab.event}
selected={isTabSelected(tab, page.url.pathname, path, integrationTabs)}
>{tab.title}</Tab>
{/each}
</Tabs>
{#if $action}
<svelte:component this={$action} />
@@ -57,13 +57,30 @@
loading = false;
}
}
function handleUpgrade() {
goto(
resolve('/(console)/organization-[organization]/change-plan', {
organization: teamId
})
);
}
function handleBackToOrganization() {
goto(
resolve('/(console)/organization-[organization]', {
organization: teamId
})
);
}
</script>
<Modal title="Project paused" bind:open={show} size="s" dismissible={false}>
<Modal title="Project paused" bind:open={show} size="l" dismissible={false}>
<Layout.Stack gap="m">
<Typography.Text>This project has been paused due to inactivity.</Typography.Text>
<Typography.Text>
Your data is safe and will remain intact. Resume the project to continue using it.
Your data is safe and will remain intact. Upgrade your plan to keep your projects
active, or restore the project to continue using it.
</Typography.Text>
{#if error}
@@ -74,25 +91,20 @@
</Layout.Stack>
<svelte:fragment slot="footer">
<Layout.Stack direction="row" justifyContent="flex-end">
<Button
text
disabled={loading}
on:click={() =>
goto(
resolve('/(console)/organization-[organization]', {
organization: teamId
})
)}>
Cancel
</Button>
<Button disabled={loading} on:click={handleResume}>
{#if loading}
Resuming...
{:else}
Resume project
{/if}
<Layout.Stack direction="row" justifyContent="space-between">
<Button text disabled={loading} on:click={handleBackToOrganization}>
Back to organization
</Button>
<Layout.Stack direction="row" justifyContent="flex-end">
<Button secondary disabled={loading} on:click={handleResume}>
{#if loading}
Restoring...
{:else}
Restore project
{/if}
</Button>
<Button disabled={loading} on:click={handleUpgrade}>Upgrade</Button>
</Layout.Stack>
</Layout.Stack>
</svelte:fragment>
</Modal>
@@ -23,10 +23,17 @@
databases: { root: 'Databases', rows: 'Include rows' },
functions: {
root: 'Functions',
env: 'Include environment variables',
inactive: 'Include inactive deployments'
deploymentInactive: 'Include inactive deployments'
},
storage: { root: 'Storage' }
storage: { root: 'Storage' },
sites: {
root: 'Sites',
deploymentInactive: 'Include inactive deployments'
},
messaging: {
root: 'Messaging',
messages: 'Include messages'
}
};
const descriptionMap = {
@@ -40,8 +47,15 @@
},
functions: {
root: 'Import all functions and their active deployment',
env: 'Import all environment variables',
inactive: 'Import all deployments that are not currently active'
deploymentInactive: 'Import all deployments that are not currently active'
},
sites: {
root: 'Import all sites and their active deployment',
deploymentInactive: 'Import all deployments that are not currently active'
},
messaging: {
root: 'Import all messaging providers, topics and subscribers',
messages: 'Import all messages'
}
};
@@ -49,24 +63,16 @@
let parentState: boolean | 'indeterminate';
$: {
const total = formGroupChildren.length;
const checked = formGroupChildren.filter((key) => formGroup[key]).length;
if (checked === 0) {
parentState = false;
} else if (checked === total) {
parentState = true;
} else {
parentState = 'indeterminate';
}
}
$: parentState = formGroup.root;
function onParentChange(event: CustomEvent<boolean | 'indeterminate'>) {
if (event.detail === 'indeterminate') return;
const updated = { ...formGroup };
for (const key of Object.keys(formGroup)) {
updated[key] = event.detail;
updated.root = event.detail;
if (!event.detail) {
for (const key of formGroupChildren) {
updated[key] = false;
}
}
dispatch('updateFormGroup', updated);
@@ -114,7 +120,11 @@
<Selector.Checkbox
size="s"
id={key}
bind:checked={formGroup[key]}
checked={formGroup[key]}
on:change={(event) => {
const updated = { ...formGroup, [key]: event.detail };
dispatch('updateFormGroup', updated);
}}
label={labelMap[groupKey]?.[key] ?? key}
description={descriptionMap[groupKey]?.[key]} />
</div>
@@ -7,6 +7,12 @@
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { migrationFormToResources, type Provider } from '$lib/stores/migration';
import {
AppwriteMigrationResource,
FirebaseMigrationResource,
NHostMigrationResource,
SupabaseMigrationResource
} from '@appwrite.io/console';
import { started } from '../stores';
import { showMigrationBox } from '$lib/components/migrationBox.svelte';
import { addNotification } from '$lib/stores/notifications';
@@ -45,7 +51,7 @@
await sdk
.forProject(page.params.region, page.params.project)
.migrations.createAppwriteMigration({
resources,
resources: resources as AppwriteMigrationResource[],
endpoint: $provider.endpoint,
projectId: $provider.projectID,
apiKey: $provider.apiKey
@@ -58,7 +64,7 @@
await sdk
.forProject(page.params.region, page.params.project)
.migrations.createSupabaseMigration({
resources,
resources: resources as SupabaseMigrationResource[],
endpoint: $provider.endpoint,
apiKey: $provider.apiKey,
databaseHost: $provider.host,
@@ -73,7 +79,7 @@
await sdk
.forProject(page.params.region, page.params.project)
.migrations.createFirebaseMigration({
resources,
resources: resources as FirebaseMigrationResource[],
serviceAccount: $provider.serviceAccount
});
await invalidate(Dependencies.MIGRATIONS);
@@ -83,7 +89,7 @@
await sdk
.forProject(page.params.region, page.params.project)
.migrations.createNHostMigration({
resources,
resources: resources as NHostMigrationResource[],
subdomain: $provider.subdomain,
region: $provider.region,
adminSecret: $provider.adminSecret,
@@ -13,7 +13,8 @@ export async function load({ depends, params }) {
Query.or([
Query.equal('destination', ['Appwrite', 'Firebase', 'NHost', 'Supabase']),
Query.isNull('destination')
])
]),
Query.orderDesc('')
]
});
@@ -130,8 +130,8 @@
<div>
<span class="u-capitalize"
>{total(Object.values(entityCounter)) > 1
? ResourcesFriendly[entity].plural
: ResourcesFriendly[entity].singular}</span>
? (ResourcesFriendly[entity]?.plural ?? entity)
: (ResourcesFriendly[entity]?.singular ?? entity)}</span>
<Tag size="xs" selected>{totalItems(entityCounter)}</Tag>
</div>
@@ -106,6 +106,11 @@
Scopes.BucketsRead,
Scopes.FunctionsRead,
Scopes.ExecutionRead,
Scopes.SitesRead,
Scopes.ProvidersRead,
Scopes.TopicsRead,
Scopes.SubscribersRead,
Scopes.MessagesRead,
Scopes.LocaleRead,
Scopes.AvatarsRead,
Scopes.HealthRead
@@ -10,6 +10,6 @@ export const load: PageLoad = async ({ params, url, depends }) => {
return {
page,
webhooks: await sdk.forConsole.projects.listWebhooks({ projectId: params.project })
webhooks: await sdk.forProject(params.region, params.project).webhooks.list()
};
};
@@ -10,8 +10,7 @@ export const load: LayoutLoad = async ({ params, depends }) => {
return {
header: Header,
breadcrumbs: Breadcrumbs,
webhook: await sdk.forConsole.projects.getWebhook({
projectId: params.project,
webhook: await sdk.forProject(params.region, params.project).webhooks.get({
webhookId: params.webhook
})
};
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { page } from '$app/state';
import { base } from '$app/paths';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import Confirm from '$lib/components/confirm.svelte';
@@ -14,8 +15,7 @@
let error: string;
async function handleDelete() {
try {
await sdk.forConsole.projects.deleteWebhook({
projectId: $project.$id,
await sdk.forProject(page.params.region, $project.$id).webhooks.delete({
webhookId: $webhook.$id
});
await invalidate(Dependencies.WEBHOOKS);
@@ -25,7 +25,7 @@
message: `${$webhook.name} has been deleted`
});
trackEvent(Submit.WebhookDelete);
await goto(`${base}/project-${$project.region}-${$project.$id}/settings/webhooks`);
await goto(`${base}/project-${page.params.region}-${$project.$id}/settings/webhooks`);
} catch (e) {
error = e.message;
trackError(e, Submit.WebhookDelete);
@@ -25,8 +25,7 @@
async function setEnabled() {
try {
await sdk.forConsole.projects.updateWebhook({
projectId,
await sdk.forProject(page.params.region, projectId).webhooks.update({
webhookId: $webhook.$id,
name: $webhook.name,
events: $webhook.events,
@@ -13,8 +13,7 @@
async function regenerate() {
try {
await sdk.forConsole.projects.updateWebhookSignature({
projectId,
await sdk.forProject(page.params.region, projectId).webhooks.updateSignature({
webhookId: $webhook.$id
});
await invalidate(Dependencies.WEBHOOK);
@@ -26,8 +26,7 @@
async function updateEvents() {
try {
await sdk.forConsole.projects.updateWebhook({
projectId,
await sdk.forProject(page.params.region, projectId).webhooks.update({
webhookId: $webhook.$id,
name: $webhook.name,
events: Array.from($eventSet),
@@ -19,8 +19,7 @@
async function updateName() {
try {
await sdk.forConsole.projects.updateWebhook({
projectId,
await sdk.forProject(page.params.region, projectId).webhooks.update({
webhookId: $webhook.$id,
name,
events: $webhook.events,
@@ -24,8 +24,7 @@
async function updateSecurity() {
try {
await sdk.forConsole.projects.updateWebhook({
projectId,
await sdk.forProject(page.params.region, projectId).webhooks.update({
webhookId: $webhook.$id,
name: $webhook.name,
events: $webhook.events,
@@ -19,8 +19,7 @@
async function updateUrl() {
try {
await sdk.forConsole.projects.updateWebhook({
projectId,
await sdk.forProject(page.params.region, projectId).webhooks.update({
webhookId: $webhook.$id,
name: $webhook.name,
events: $webhook.events,
@@ -5,6 +5,7 @@
import { Layout } from '@appwrite.io/pink-svelte';
import Form from '$lib/elements/forms/form.svelte';
import { goto } from '$app/navigation';
import { ID } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
@@ -25,16 +26,18 @@
export let data;
async function create() {
try {
const webhook = await sdk.forConsole.projects.createWebhook({
projectId: data.project.$id,
name,
events,
url,
security,
enabled: true,
httpUser: httpUser || undefined,
httpPass: httpPass || undefined
});
const webhook = await sdk
.forProject(page.params.region, data.project.$id)
.webhooks.create({
webhookId: ID.unique(),
name,
events,
url,
security,
enabled: true,
httpUser: httpUser || undefined,
httpPass: httpPass || undefined
});
addNotification({
message: 'Webhook has been created',
type: 'success'
@@ -29,12 +29,14 @@
import type { Snippet } from 'svelte';
let {
site,
deployment,
proxyRuleList,
hideQRCode = false,
variant = 'primary',
footer
}: {
site?: Models.Site;
deployment: Models.Deployment;
proxyRuleList: Models.ProxyRuleList;
hideQRCode?: boolean;
@@ -208,7 +210,11 @@
Source
</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
<DeploymentSource {deployment} />
<DeploymentSource
{deployment}
resource={site}
region={page.params.region}
project={page.params.project} />
</Typography.Text>
</Layout.Stack>
</Layout.Stack>
@@ -32,7 +32,10 @@
<Container>
<Layout.Stack gap="xxxl">
{#if data?.deployment && data.deployment.status === 'ready'}
<SiteCard deployment={data.deployment} proxyRuleList={data.proxyRuleList}>
<SiteCard
site={data.site}
deployment={data.deployment}
proxyRuleList={data.proxyRuleList}>
{#snippet footer()}
{#if data.proxyRuleList.total}
<Button
@@ -77,7 +77,7 @@
providerBranch: nextBranch,
providerSilentMode: data.site?.providerSilentMode ?? undefined,
providerRootDirectory: data.site?.providerRootDirectory ?? undefined,
specification: data.site?.specification || undefined
buildSpecification: data.site?.buildSpecification || undefined
});
invalidate(Dependencies.SITE);
}
@@ -39,8 +39,11 @@ export const load = async ({ params, depends, parent }) => {
});
const enabledSpecs = specificationsList?.specifications?.filter((s) => s.enabled) ?? [];
if (!enabledSpecs.some((s) => s.slug === site.specification)) {
site.specification = enabledSpecs[0]?.slug;
if (!enabledSpecs.some((s) => s.slug === site.buildSpecification)) {
site.buildSpecification = enabledSpecs[0]?.slug;
}
if (!enabledSpecs.some((s) => s.slug === site.runtimeSpecification)) {
site.runtimeSpecification = enabledSpecs[0]?.slug;
}
return {
@@ -112,8 +112,13 @@
site.adapter = adapter;
}
if (specs && specs.specifications?.length) {
if (!specs.specifications.some((s) => s.slug === site.specification)) {
site.specification = specs.specifications[0].slug;
const enabledSpecs = specs.specifications.filter((s) => s.enabled);
const fallbackSlug = enabledSpecs[0]?.slug ?? specs.specifications[0]?.slug;
if (!enabledSpecs.some((s) => s.slug === site.buildSpecification)) {
site.buildSpecification = fallbackSlug;
}
if (!enabledSpecs.some((s) => s.slug === site.runtimeSpecification)) {
site.runtimeSpecification = fallbackSlug;
}
}
}
@@ -128,10 +133,12 @@
}
// only allow enabled specsification for it
const enabledSpecs = specs?.specifications?.filter((s) => s.enabled) ?? [];
let specToSend = enabledSpecs.some((s) => s.slug === site.specification)
? site.specification
const specToSend = enabledSpecs.some((s) => s.slug === site.buildSpecification)
? site.buildSpecification
: enabledSpecs[0]?.slug;
const runtimeSpecToSend = enabledSpecs.some((s) => s.slug === site.runtimeSpecification)
? site.runtimeSpecification
: enabledSpecs[0]?.slug;
site.specification = specToSend;
try {
await sdk.forProject(page.params.region, page.params.project).sites.update({
siteId: site.$id,
@@ -151,8 +158,11 @@
providerBranch: site.providerBranch || undefined,
providerSilentMode: site.providerSilentMode || undefined,
providerRootDirectory: site.providerRootDirectory || undefined,
specification: specToSend || undefined
buildSpecification: specToSend || undefined,
runtimeSpecification: runtimeSpecToSend || undefined
});
site.buildSpecification = specToSend;
site.runtimeSpecification = runtimeSpecToSend;
await invalidate(Dependencies.SITE);
addNotification({
message: 'Build settings have been updated',
@@ -184,8 +194,8 @@
<Form onSubmit={update}>
<CardGrid>
<svelte:fragment slot="title">Build settings</svelte:fragment>
Default build settings are configured based on your framework, ensuring optimal performance.
Adjust the settings here if needed.
Default build settings are configured based on your framework, ensuring optimal performance. Adjust
the settings here if needed.
<svelte:fragment slot="aside">
{@const adapterData = adapterDataList.find(
(adapterData) => adapterData.framework === frameworkKey.toLowerCase()
@@ -33,7 +33,7 @@
providerBranch: site?.providerBranch || undefined,
providerSilentMode: site?.providerSilentMode || undefined,
providerRootDirectory: site?.providerRootDirectory || undefined,
specification: site?.specification || undefined
buildSpecification: site?.buildSpecification || undefined
});
await invalidate(Dependencies.SITE);
addNotification({
@@ -37,7 +37,7 @@
providerBranch: site?.providerBranch || undefined,
providerSilentMode: site?.providerSilentMode || undefined,
providerRootDirectory: site?.providerRootDirectory || undefined,
specification: site?.specification || undefined
buildSpecification: site?.buildSpecification || undefined
});
await invalidate(Dependencies.SITE);
addNotification({
@@ -86,7 +86,7 @@
providerBranch: selectedBranch || undefined,
providerSilentMode: silentMode || undefined,
providerRootDirectory: selectedDir || undefined,
specification: site?.specification || undefined
buildSpecification: site?.buildSpecification || undefined
});
await invalidate(Dependencies.SITE);
addNotification({
@@ -152,7 +152,7 @@
providerBranch: nextBranch,
providerSilentMode: site?.providerSilentMode ?? undefined,
providerRootDirectory: site?.providerRootDirectory ?? undefined,
specification: site?.specification || undefined
buildSpecification: site?.buildSpecification || undefined
});
invalidate(Dependencies.SITE);
@@ -8,7 +8,8 @@
import { sdk } from '$lib/stores/sdk';
import { Adapter, BuildRuntime, Framework, type Models } from '@appwrite.io/console';
import Link from '$lib/elements/link.svelte';
import { Alert } from '@appwrite.io/pink-svelte';
import { Alert, Icon, Tooltip } from '@appwrite.io/pink-svelte';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { getChangePlanUrl, isStarterPlan } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
@@ -17,10 +18,23 @@
export let site: Models.Site;
export let specs: Models.SpecificationList;
let specification = site.specification;
let originalSpecification = site.specification;
let buildSpecification = site.buildSpecification;
let runtimeSpecification = site.runtimeSpecification;
let originalBuild = site.buildSpecification;
let originalRuntime = site.runtimeSpecification;
async function updateLogging() {
let siteId: string | undefined = undefined;
$: if (site.$id !== siteId) {
siteId = site.$id;
buildSpecification = site.buildSpecification;
runtimeSpecification = site.runtimeSpecification;
}
$: {
originalBuild = site.buildSpecification;
originalRuntime = site.runtimeSpecification;
}
async function updateResourceLimits() {
try {
await sdk.forProject(page.params.region, page.params.project).sites.update({
siteId: site.$id,
@@ -40,22 +54,22 @@
providerBranch: site?.providerBranch || undefined,
providerSilentMode: site?.providerSilentMode || undefined,
providerRootDirectory: site?.providerRootDirectory || undefined,
specification: specification || undefined
buildSpecification: buildSpecification || undefined,
runtimeSpecification: runtimeSpecification || undefined
});
await invalidate(Dependencies.SITE);
originalSpecification = specification;
addNotification({
type: 'success',
message: 'Resource limits have been updated'
});
trackEvent(Submit.FunctionUpdateLogging);
trackEvent(Submit.SiteUpdateResourceLimits);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.FunctionUpdateLogging);
trackError(error, Submit.SiteUpdateResourceLimits);
}
}
@@ -66,23 +80,44 @@
}));
</script>
<Form onSubmit={updateLogging}>
<Form onSubmit={updateResourceLimits}>
<CardGrid>
<svelte:fragment slot="title">Resource limits</svelte:fragment>
Define your sites's compute specifications, including CPU and memory, to optimize performance
for your workloads. <Link
href="https://appwrite.io/docs/advanced/platform/compute"
external>
Define your site's compute specifications, including CPU and memory, to optimize performance for
your workloads. <Link href="https://appwrite.io/docs/advanced/platform/compute" external>
Learn more
</Link>.
<svelte:fragment slot="aside">
<InputSelect
label="CPU and memory"
id="resources"
label="Build specification"
id="build-specification"
required
disabled={options.length < 1}
bind:value={specification}
{options} />
bind:value={buildSpecification}
{options}>
<Tooltip slot="info">
<Icon icon={IconInfo} size="s" />
<span slot="tooltip">
CPU and memory used when installing dependencies and building your site for
deployment.
</span>
</Tooltip>
</InputSelect>
<InputSelect
label="Runtime specification"
id="runtime-specification"
required
disabled={options.length < 1}
bind:value={runtimeSpecification}
{options}>
<Tooltip slot="info">
<Icon icon={IconInfo} size="s" />
<span slot="tooltip">
CPU and memory used when your site serves traffic, including server-side
rendering (SSR).
</span>
</Tooltip>
</InputSelect>
<!-- always show upgrade on starters -->
{@const isStarter = isStarterPlan($organization.billingPlanId)}
@@ -97,7 +132,10 @@
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={originalSpecification === specification} submit>Update</Button>
<Button
disabled={originalBuild === buildSpecification &&
originalRuntime === runtimeSpecification}
submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -43,7 +43,7 @@
providerBranch: site?.providerBranch || undefined,
providerSilentMode: site?.providerSilentMode || undefined,
providerRootDirectory: site?.providerRootDirectory || undefined,
specification: site?.specification || undefined
buildSpecification: site?.buildSpecification || undefined
});
await invalidate(Dependencies.SITE);
addNotification({
@@ -64,8 +64,8 @@
<Form onSubmit={updateRuntime}>
<CardGrid>
<svelte:fragment slot="title">Runtime settings</svelte:fragment>
Select the runtime for building and serving your site. Version changes apply on redeploy and
can be updated here.
Select the runtime for building and serving your site. Version changes apply on redeploy and can
be updated here.
<svelte:fragment slot="aside">
<InputSelect
label="Build runtime"
@@ -36,7 +36,7 @@
providerBranch: site?.providerBranch || undefined,
providerSilentMode: site?.providerSilentMode || undefined,
providerRootDirectory: site?.providerRootDirectory || undefined,
specification: site?.specification || undefined
buildSpecification: site?.buildSpecification || undefined
});
await invalidate(Dependencies.SITE);
addNotification({
+2 -2
View File
@@ -195,8 +195,8 @@
}
const workTimings = {
start: '16:00',
end: '00:00',
start: '04:00',
end: '17:00',
startDay: 'Monday' as WeekDay,
endDay: 'Friday' as WeekDay
};
@@ -16,35 +16,35 @@ describe('isSupportOnline', () => {
};
describe('weekday support hours (Monday-Friday)', () => {
it('should be online at 16:00 UTC (Monday)', () => {
mockDate(16, 1); // Monday, 16:00 UTC
it('should be online at 04:00 UTC (Monday)', () => {
mockDate(4, 1); // Monday, 04:00 UTC
expect(isSupportOnline()).toBe(true);
});
it('should be online at 20:00 UTC (Tuesday)', () => {
mockDate(20, 2); // Tuesday, 20:00 UTC
it('should be online at 10:00 UTC (Tuesday)', () => {
mockDate(10, 2); // Tuesday, 10:00 UTC
expect(isSupportOnline()).toBe(true);
});
it('should be online at 23:59 UTC (Wednesday)', () => {
mockDate(23, 3); // Wednesday, 23:59 UTC
it('should be online at 16:59 UTC (Wednesday)', () => {
mockDate(16, 3); // Wednesday, 16:59 UTC
expect(isSupportOnline()).toBe(true);
});
it('should be online at 23:00 UTC (Thursday)', () => {
mockDate(23, 4); // Thursday, 23:00 UTC
it('should be online at 12:00 UTC (Thursday)', () => {
mockDate(12, 4); // Thursday, 12:00 UTC
expect(isSupportOnline()).toBe(true);
});
it('should be online at 22:00 UTC (Friday)', () => {
mockDate(22, 5); // Friday, 22:00 UTC
it('should be online at 08:00 UTC (Friday)', () => {
mockDate(8, 5); // Friday, 08:00 UTC
expect(isSupportOnline()).toBe(true);
});
});
describe('outside support hours on weekdays', () => {
it('should be offline at 15:59 UTC (Monday)', () => {
mockDate(15, 1); // Monday, 15:59 UTC
it('should be offline at 03:59 UTC (Monday)', () => {
mockDate(3, 1); // Monday, 03:59 UTC
expect(isSupportOnline()).toBe(false);
});
@@ -53,20 +53,20 @@ describe('isSupportOnline', () => {
expect(isSupportOnline()).toBe(false);
});
it('should be offline at 10:00 UTC (Wednesday)', () => {
mockDate(10, 3); // Wednesday, 10:00 UTC
it('should be offline at 17:00 UTC (Wednesday)', () => {
mockDate(17, 3); // Wednesday, 17:00 UTC
expect(isSupportOnline()).toBe(false);
});
it('should be offline at 14:00 UTC (Friday)', () => {
mockDate(14, 5); // Friday, 14:00 UTC
it('should be offline at 23:00 UTC (Friday)', () => {
mockDate(23, 5); // Friday, 23:00 UTC
expect(isSupportOnline()).toBe(false);
});
});
describe('weekends', () => {
it('should be offline on Saturday regardless of hour', () => {
mockDate(18, 6); // Saturday, 18:00 UTC (in support hours for weekdays)
mockDate(10, 6); // Saturday, 10:00 UTC (in support hours for weekdays)
expect(isSupportOnline()).toBe(false);
});
@@ -80,25 +80,30 @@ describe('isSupportOnline', () => {
expect(isSupportOnline()).toBe(false);
});
it('should be offline on Sunday at 16:00', () => {
mockDate(16, 0); // Sunday, 16:00 UTC
it('should be offline on Sunday at 10:00', () => {
mockDate(10, 0); // Sunday, 10:00 UTC
expect(isSupportOnline()).toBe(false);
});
});
describe('edge cases', () => {
it('should handle boundary at 15:59 (just before support hours)', () => {
mockDate(15, 1); // Monday, 15:59 UTC
it('should handle boundary at 03:59 (just before support hours)', () => {
mockDate(3, 1); // Monday, 03:59 UTC
expect(isSupportOnline()).toBe(false);
});
it('should handle boundary at 16:00 (start of support hours)', () => {
mockDate(16, 1); // Monday, 16:00 UTC
it('should handle boundary at 04:00 (start of support hours)', () => {
mockDate(4, 1); // Monday, 04:00 UTC
expect(isSupportOnline()).toBe(true);
});
it('should handle late night hours on Friday', () => {
mockDate(23, 5); // Friday, 23:00 UTC
it('should handle boundary at 17:00 (end of support hours)', () => {
mockDate(17, 1); // Monday, 17:00 UTC
expect(isSupportOnline()).toBe(false);
});
it('should handle boundary at 16:59 (just before end of support hours)', () => {
mockDate(16, 5); // Friday, 16:59 UTC
expect(isSupportOnline()).toBe(true);
});
});
+2 -3
View File
@@ -28,9 +28,8 @@ export function isSupportOnline() {
return false;
}
// Support hours are 16:00 UTC to 23:59 UTC on weekdays
// This roughly covers 09:00 PT to 17:00 PT (depending on PST/PDT)
if (hour >= 16) {
// Support hours are 04:00 UTC to 17:00 UTC on weekdays
if (hour >= 4 && hour < 17) {
return true;
}
+16 -7
View File
@@ -23,22 +23,31 @@
});
async function recover() {
let showGenericSuccessNotification = true;
try {
await sdk.forConsole.account.createRecovery({
email,
url: window.location.toString()
});
addNotification({
type: 'success',
message: 'We have sent you an email with a password reset link'
});
trackEvent(Submit.AccountRecover);
} catch (error) {
// Do not show error for 403 Forbidden or 404 Not Found to prevent email enumeration
if (error.code !== 403 && error.code !== 404) {
showGenericSuccessNotification = false;
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.AccountRecover);
}
}
if (showGenericSuccessNotification) {
addNotification({
type: 'error',
message: error.message
type: 'success',
message:
'If an account exists for this email, you will receive a password reset link shortly'
});
trackError(error, Submit.AccountRecover);
}
}