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-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:
+4
-1
@@ -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
@@ -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" />
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
+2
-2
@@ -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}
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+84
@@ -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>
|
||||
+13
-6
@@ -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(() =>
|
||||
|
||||
+2
-2
@@ -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
|
||||
},
|
||||
{
|
||||
|
||||
+7
-4
@@ -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();
|
||||
|
||||
+13
-4
@@ -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));
|
||||
|
||||
|
||||
+5
-2
@@ -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={() => {
|
||||
|
||||
+10
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
+16
-5
@@ -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,
|
||||
|
||||
+1
-1
@@ -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] : [];
|
||||
|
||||
+161
-117
@@ -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>
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@
|
||||
commands: installCommand || undefined,
|
||||
scopes: selectedScopes?.length ? selectedScopes : undefined,
|
||||
providerSilentMode: false,
|
||||
specification: specification || undefined
|
||||
buildSpecification: specification || undefined
|
||||
});
|
||||
|
||||
// Add domain
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@
|
||||
enabled: true,
|
||||
entrypoint,
|
||||
commands: buildCommand,
|
||||
specification: specification || undefined
|
||||
buildSpecification: specification || undefined
|
||||
});
|
||||
|
||||
// Add domain
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@
|
||||
providerBranch: branch,
|
||||
providerSilentMode: silentMode,
|
||||
providerRootDirectory: rootDir,
|
||||
specification: specification || undefined
|
||||
buildSpecification: specification || undefined
|
||||
});
|
||||
|
||||
// Add domain
|
||||
|
||||
+1
-1
@@ -154,7 +154,7 @@
|
||||
providerBranch: branch,
|
||||
providerSilentMode: silentMode,
|
||||
providerRootDirectory: rootDir,
|
||||
specification: specification || undefined
|
||||
buildSpecification: specification || undefined
|
||||
});
|
||||
|
||||
// Add domain
|
||||
|
||||
+5
-1
@@ -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>
|
||||
|
||||
+11
-2
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+1
-1
@@ -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({
|
||||
|
||||
+1
-1
@@ -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({
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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({
|
||||
|
||||
+52
-16
@@ -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>
|
||||
|
||||
+1
-1
@@ -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({
|
||||
|
||||
+1
-1
@@ -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({
|
||||
|
||||
+1
-1
@@ -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({
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+30
-20
@@ -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>
|
||||
|
||||
+10
-4
@@ -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()
|
||||
};
|
||||
};
|
||||
|
||||
+1
-2
@@ -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
|
||||
})
|
||||
};
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+1
-2
@@ -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);
|
||||
|
||||
+1
-2
@@ -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),
|
||||
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+13
-10
@@ -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
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+18
-8
@@ -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()
|
||||
|
||||
+1
-1
@@ -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({
|
||||
|
||||
+1
-1
@@ -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({
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+56
-18
@@ -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>
|
||||
|
||||
+3
-3
@@ -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"
|
||||
|
||||
+1
-1
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user