(feat): Add dedicated database settings components

This commit is contained in:
Jake Barnby
2026-03-12 21:23:28 +13:00
parent da0cc2b500
commit d7f6efe87e
20 changed files with 2902 additions and 4 deletions
@@ -13,16 +13,48 @@
import Delete from '../delete.svelte';
import { Query } from '@appwrite.io/console';
import { Layout, Skeleton } from '@appwrite.io/pink-svelte';
import type { PageProps } from './$types';
import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases';
import { getTerminologies } from '$database/(entity)';
import UpdateName from './updateName.svelte';
import UpdateTier from './updateTier.svelte';
import UpdateStorage from './updateStorage.svelte';
import UpdateNetwork from './updateNetwork.svelte';
import UpdateMaintenance from './updateMaintenance.svelte';
import UpdateBackups from './updateBackups.svelte';
import UpdateAutoscaling from './updateAutoscaling.svelte';
import UpdatePooler from './updatePooler.svelte';
import UpdateExtensions from './updateExtensions.svelte';
import UpdateConnections from './updateConnections.svelte';
import RotateCredentials from './rotateCredentials.svelte';
import UpgradeVersion from './upgradeVersion.svelte';
import UpdateReadReplicas from './updateReadReplicas.svelte';
import UpdateCrossRegion from './updateCrossRegion.svelte';
import UpdateHAStatus from './updateHAStatus.svelte';
import UpdateBackupStorage from './updateBackupStorage.svelte';
import UpdateSecurity from './updateSecurity.svelte';
import UpdateSqlApi from './updateSqlApi.svelte';
import DangerZone from './dangerZone.svelte';
const { data }: PageProps = $props();
const data = page.data;
const database = $derived(data.database);
const dedicatedDatabase = $derived(data.dedicatedDatabase as DedicatedDatabase | null);
const isDedicatedType = $derived(
dedicatedDatabase !== null &&
(database.type === 'prisma' ||
database.type === 'dedicated' ||
database.type === 'shared')
);
const isDedicated = $derived(dedicatedDatabase?.type === 'dedicated');
const isShared = $derived(dedicatedDatabase?.type === 'shared');
const isPrisma = $derived(dedicatedDatabase?.backend === 'prisma');
const isPostgres = $derived(dedicatedDatabase?.engine === 'postgres');
// Legacy database fallback state
let showDelete = $state(false);
let databaseName: string | null = $state(null);
let errorMessage: string = $state('Something went wrong');
let errorType: 'error' | 'warning' | 'success' = $state('error');
let showError: false | 'name' | 'email' | 'password' = $state(false);
@@ -70,7 +102,108 @@
}
</script>
{#if database}
{#if isDedicatedType && dedicatedDatabase}
<!-- Dedicated / Shared / Prisma database settings -->
<Container>
<CardGrid>
<svelte:fragment slot="title">{dedicatedDatabase.name}</svelte:fragment>
<svelte:fragment slot="aside">
<div class="grid-1-2-col-2">
<p>Created: {toLocaleDateTime(dedicatedDatabase.$createdAt)}</p>
<p>Last updated: {toLocaleDateTime(dedicatedDatabase.$updatedAt)}</p>
</div>
</svelte:fragment>
</CardGrid>
<!-- 1. Rename Database - all types -->
<UpdateName database={dedicatedDatabase} />
<!-- 2. Resource Scaling - dedicated only -->
{#if isDedicated}
<UpdateTier database={dedicatedDatabase} />
{/if}
<!-- 3. Storage Resize - dedicated and shared -->
{#if isDedicated || isShared}
<UpdateStorage database={dedicatedDatabase} />
{/if}
<!-- 4. Network Settings - not for Prisma -->
{#if !isPrisma}
<UpdateNetwork database={dedicatedDatabase} />
{/if}
<!-- 5. Maintenance Window - dedicated and shared -->
{#if isDedicated || isShared}
<UpdateMaintenance database={dedicatedDatabase} />
{/if}
<!-- 6. Backup Settings - all types -->
<UpdateBackups database={dedicatedDatabase} />
<!-- 7. Storage Autoscaling - dedicated and shared -->
{#if isDedicated || isShared}
<UpdateAutoscaling database={dedicatedDatabase} />
{/if}
<!-- 8. Connection Pooler - PostgreSQL only, not Prisma -->
{#if isPostgres && !isPrisma}
<UpdatePooler database={dedicatedDatabase} />
{/if}
<!-- 9. Extensions - PostgreSQL only, not Prisma -->
{#if isPostgres && !isPrisma}
<UpdateExtensions database={dedicatedDatabase} />
{/if}
<!-- 10. Database Users - not for Prisma -->
{#if !isPrisma}
<UpdateConnections database={dedicatedDatabase} />
{/if}
<!-- 11. Credential Rotation - not for Prisma -->
{#if !isPrisma}
<RotateCredentials database={dedicatedDatabase} />
{/if}
<!-- 12. Version Upgrade - dedicated and shared -->
{#if isDedicated || isShared}
<UpgradeVersion database={dedicatedDatabase} />
{/if}
<!-- 13. Read Replicas - dedicated only -->
{#if isDedicated}
<UpdateReadReplicas database={dedicatedDatabase} />
{/if}
<!-- 14. Cross-Region Failover - dedicated only -->
{#if isDedicated}
<UpdateCrossRegion database={dedicatedDatabase} />
{/if}
<!-- 15. High Availability - not for Prisma -->
{#if !isPrisma}
<UpdateHAStatus database={dedicatedDatabase} />
{/if}
<!-- 16. Backup Storage - dedicated only -->
{#if isDedicated}
<UpdateBackupStorage database={dedicatedDatabase} />
{/if}
<!-- 17. Security - all types -->
<UpdateSecurity database={dedicatedDatabase} />
<!-- 18. SQL API - not for Prisma -->
{#if !isPrisma}
<UpdateSqlApi database={dedicatedDatabase} />
{/if}
<!-- 19. Delete Database - all types -->
<DangerZone database={dedicatedDatabase} />
</Container>
{:else if database}
<!-- Legacy / tablesdb / documentsdb settings -->
<Container databasesMainScreen>
<CardGrid>
<svelte:fragment slot="title">{database.name}</svelte:fragment>
@@ -0,0 +1,66 @@
<script lang="ts">
import { BoxAvatar, CardGrid } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Click, trackEvent } from '$lib/actions/analytics';
import Delete from '../delete.svelte';
import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases';
import { Layout, Typography } from '@appwrite.io/pink-svelte';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
let showDelete = $state(false);
function getEngineDisplayName(engine: string): string {
switch (engine) {
case 'postgres':
return 'PostgreSQL';
case 'mysql':
return 'MySQL';
case 'mariadb':
return 'MariaDB';
case 'mongodb':
return 'MongoDB';
default:
return engine;
}
}
</script>
<CardGrid>
<svelte:fragment slot="title">Delete database</svelte:fragment>
The database will be permanently deleted, including all data and backups. This action is
irreversible.
<svelte:fragment slot="aside">
<BoxAvatar>
<svelte:fragment slot="title">
<Layout.Stack direction="column" gap="xxs">
<h6 class="u-bold u-trim-1">{database.name}</h6>
<Layout.Stack direction="row" gap="s">
<Typography.Caption variant="400" color="--fgcolor-neutral-tertiary">
{getEngineDisplayName(database.engine)} {database.version}
</Typography.Caption>
</Layout.Stack>
</Layout.Stack>
</svelte:fragment>
<p>Last updated: {toLocaleDateTime(database.$updatedAt)}</p>
</BoxAvatar>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
secondary
on:click={() => {
showDelete = true;
trackEvent(Click.DatabaseDatabaseDelete);
}}>
Delete
</Button>
</svelte:fragment>
</CardGrid>
<Delete bind:showDelete />
@@ -0,0 +1,90 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases';
import { Alert } from '@appwrite.io/pink-svelte';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
let showConfirm = $state(false);
let isRotating = $state(false);
async function rotateCredentials() {
isRotating = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.rotateCredentials(database.$id);
await invalidate(Dependencies.DATABASE);
showConfirm = false;
addNotification({
message:
'Credentials have been rotated. Update your application with the new credentials.',
type: 'success'
});
trackEvent(Submit.DatabaseRotateCredentials);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseRotateCredentials);
} finally {
isRotating = false;
}
}
</script>
<CardGrid>
<svelte:fragment slot="title">Credential rotation</svelte:fragment>
Generate new database credentials. Existing connections using the old credentials will be
terminated.
<svelte:fragment slot="aside">
<Alert.Inline status="warning" title="Warning">
Rotating credentials will invalidate the current username and password. All active
connections will be dropped. Make sure to update your application configuration
immediately after rotation.
</Alert.Inline>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
secondary
on:click={() => {
showConfirm = true;
trackEvent('click_database_rotate_credentials');
}}>
Rotate credentials
</Button>
</svelte:fragment>
</CardGrid>
<Modal
title="Rotate credentials"
bind:show={showConfirm}
onSubmit={rotateCredentials}>
<p class="text">
Are you sure you want to rotate the credentials for <b>{database.name}</b>? This will
generate a new username and password, and all existing connections will be terminated.
</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showConfirm = false)}>Cancel</Button>
<Button danger submit disabled={isRotating}>
{isRotating ? 'Rotating...' : 'Rotate'}
</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,92 @@
<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, InputSwitch, InputNumber } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
let autoscaling: boolean = $state(database.storageAutoscaling);
let thresholdPercent: number = $state(database.storageAutoscalingThresholdPercent);
let maxGb: number = $state(database.storageAutoscalingMaxGb);
const hasChanges = $derived(
autoscaling !== database.storageAutoscaling ||
thresholdPercent !== database.storageAutoscalingThresholdPercent ||
maxGb !== database.storageAutoscalingMaxGb
);
async function updateAutoscaling() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.update(database.$id, {
storageAutoscaling: autoscaling,
storageAutoscalingThresholdPercent: autoscaling
? thresholdPercent
: undefined,
storageAutoscalingMaxGb: autoscaling ? maxGb : undefined
});
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Storage autoscaling settings have been updated',
type: 'success'
});
trackEvent(Submit.DatabaseUpdateAutoscaling);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseUpdateAutoscaling);
}
}
</script>
<Form onSubmit={updateAutoscaling}>
<CardGrid>
<svelte:fragment slot="title">Storage autoscaling</svelte:fragment>
Automatically increase storage when disk usage reaches a threshold. Storage will never
exceed the configured maximum.
<svelte:fragment slot="aside">
<ul>
<InputSwitch
id="autoscaling"
label="Enable storage autoscaling"
bind:value={autoscaling} />
{#if autoscaling}
<InputNumber
id="threshold"
label="Usage threshold (%)"
min={50}
max={95}
bind:value={thresholdPercent}
required />
<InputNumber
id="maxGb"
label="Maximum storage (GB)"
min={database.storage}
max={10000}
bind:value={maxGb}
required />
{/if}
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!hasChanges} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,277 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputSelect, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import type {
DedicatedDatabase,
BackupStorageConfig,
BackupStorageProvider
} from '$lib/sdk/dedicatedDatabases';
import { Layout } from '@appwrite.io/pink-svelte';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
const providerOptions: { value: BackupStorageProvider; label: string }[] = [
{ value: 's3', label: 'Amazon S3' },
{ value: 'gcs', label: 'Google Cloud Storage' },
{ value: 'azure', label: 'Azure Blob Storage' }
];
let config: BackupStorageConfig | null = $state(null);
let isLoading = $state(true);
let isConfigured = $state(false);
let provider: BackupStorageProvider = $state('s3');
let bucket: string = $state('');
let region: string = $state('');
let accessKeyId: string = $state('');
let secretAccessKey: string = $state('');
let prefix: string = $state('');
let endpoint: string = $state('');
let isSubmitting = $state(false);
let showRemoveConfirm = $state(false);
let isRemoving = $state(false);
onMount(async () => {
try {
config = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.getBackupStorageConfig(database.$id);
isConfigured = true;
} catch {
// 404 means not configured
isConfigured = false;
config = null;
} finally {
isLoading = false;
}
});
async function configureStorage() {
isSubmitting = true;
try {
config = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.configureBackupStorage(database.$id, {
provider,
bucket,
region,
accessKeyId,
secretAccessKey,
prefix: prefix || undefined,
endpoint: endpoint || undefined
});
isConfigured = true;
// Reset sensitive fields
accessKeyId = '';
secretAccessKey = '';
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Backup storage has been configured',
type: 'success'
});
trackEvent(Submit.DatabaseConfigureBackupStorage);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseConfigureBackupStorage);
} finally {
isSubmitting = false;
}
}
async function removeStorage() {
isRemoving = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.deleteBackupStorageConfig(database.$id);
isConfigured = false;
config = null;
showRemoveConfirm = false;
// Reset form
provider = 's3';
bucket = '';
region = '';
accessKeyId = '';
secretAccessKey = '';
prefix = '';
endpoint = '';
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Backup storage configuration has been removed',
type: 'success'
});
trackEvent(Submit.DatabaseDeleteBackupStorage);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseDeleteBackupStorage);
} finally {
isRemoving = false;
}
}
</script>
{#if !isLoading}
{#if isConfigured && config}
<CardGrid>
<svelte:fragment slot="title">Backup storage</svelte:fragment>
Your database backups are stored on an external storage provider for added durability
and disaster recovery.
<svelte:fragment slot="aside">
<ul>
<li class="u-margin-block-end-16">
<div class="box">
<Layout.Stack direction="column" gap="xs">
<Layout.Stack direction="row" gap="s">
<span class="u-bold">Provider:</span>
<span>
{config.provider === 's3'
? 'Amazon S3'
: config.provider === 'gcs'
? 'Google Cloud Storage'
: 'Azure Blob Storage'}
</span>
</Layout.Stack>
<Layout.Stack direction="row" gap="s">
<span class="u-bold">Bucket:</span>
<span>{config.bucket}</span>
</Layout.Stack>
<Layout.Stack direction="row" gap="s">
<span class="u-bold">Region:</span>
<span>{config.region}</span>
</Layout.Stack>
{#if config.prefix}
<Layout.Stack direction="row" gap="s">
<span class="u-bold">Prefix:</span>
<span>{config.prefix}</span>
</Layout.Stack>
{/if}
{#if config.endpoint}
<Layout.Stack direction="row" gap="s">
<span class="u-bold">Endpoint:</span>
<span>{config.endpoint}</span>
</Layout.Stack>
{/if}
</Layout.Stack>
</div>
</li>
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
secondary
on:click={() => {
showRemoveConfirm = true;
}}>
Remove
</Button>
</svelte:fragment>
</CardGrid>
{:else}
<Form onSubmit={configureStorage}>
<CardGrid>
<svelte:fragment slot="title">Backup storage</svelte:fragment>
Configure off-cluster backup storage to store backups on an external cloud provider
for added durability and disaster recovery.
<svelte:fragment slot="aside">
<ul>
<InputSelect
id="provider"
label="Provider"
bind:value={provider}
options={providerOptions} />
<InputText
id="bucket"
label="Bucket"
placeholder="my-backup-bucket"
bind:value={bucket}
required />
<InputText
id="storageRegion"
label="Region"
placeholder="us-east-1"
bind:value={region}
required />
<InputText
id="accessKeyId"
label="Access key ID"
placeholder="Enter access key ID"
bind:value={accessKeyId}
required />
<InputText
id="secretAccessKey"
label="Secret access key"
placeholder="Enter secret access key"
bind:value={secretAccessKey}
required />
<InputText
id="prefix"
label="Prefix (optional)"
placeholder="backups/"
bind:value={prefix} />
<InputText
id="endpoint"
label="Endpoint (optional)"
placeholder="https://s3.amazonaws.com"
bind:value={endpoint} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!bucket || !region || !accessKeyId || !secretAccessKey || isSubmitting} submit>
{isSubmitting ? 'Configuring...' : 'Configure'}
</Button>
</svelte:fragment>
</CardGrid>
</Form>
{/if}
<Modal
title="Remove backup storage"
bind:show={showRemoveConfirm}
onSubmit={removeStorage}>
<p class="text">
Are you sure you want to remove the off-cluster backup storage configuration for
<b>{database.name}</b>? Existing backups in the external storage will not be deleted,
but new backups will no longer be stored externally.
</p>
<svelte:fragment slot="footer">
<Button
text
on:click={() => {
showRemoveConfirm = false;
}}>Cancel</Button>
<Button danger submit disabled={isRemoving}>
{isRemoving ? 'Removing...' : 'Remove'}
</Button>
</svelte:fragment>
</Modal>
{/if}
@@ -0,0 +1,100 @@
<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,
InputSwitch,
InputCron,
InputNumber
} from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
let backupEnabled: boolean = $state(database.backupEnabled);
let backupPitr: boolean = $state(database.backupPitr);
let backupCron: string = $state(database.backupCron ?? '0 2 * * *');
let backupRetentionDays: number = $state(database.backupRetentionDays);
const hasChanges = $derived(
backupEnabled !== database.backupEnabled ||
backupPitr !== database.backupPitr ||
backupCron !== (database.backupCron ?? '0 2 * * *') ||
backupRetentionDays !== database.backupRetentionDays
);
async function updateBackups() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.update(database.$id, {
backupEnabled,
backupPitr: backupEnabled ? backupPitr : false,
backupCron: backupEnabled ? backupCron : undefined,
backupRetentionDays: backupEnabled ? backupRetentionDays : undefined
});
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Backup settings have been updated',
type: 'success'
});
trackEvent(Submit.DatabaseUpdateBackups);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseUpdateBackups);
}
}
</script>
<Form onSubmit={updateBackups}>
<CardGrid>
<svelte:fragment slot="title">Backups</svelte:fragment>
Configure automatic backups and point-in-time recovery for your database.
<svelte:fragment slot="aside">
<ul>
<InputSwitch
id="backupEnabled"
label="Enable automatic backups"
bind:value={backupEnabled} />
{#if backupEnabled}
<InputSwitch
id="backupPitr"
label="Enable point-in-time recovery (PITR)"
bind:value={backupPitr} />
<InputCron
id="backupCron"
label="Backup schedule"
bind:value={backupCron}
required />
<InputNumber
id="backupRetention"
label="Retention (days)"
min={1}
max={365}
bind:value={backupRetentionDays}
required />
{/if}
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!hasChanges} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,227 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Box, CardGrid, Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputText, InputSelect } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import type {
DedicatedDatabase,
DatabaseConnection,
ConnectionRole
} from '$lib/sdk/dedicatedDatabases';
import { Badge, Layout, Typography } from '@appwrite.io/pink-svelte';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
const roleOptions: { value: ConnectionRole; label: string }[] = [
{ value: 'readwrite', label: 'Read / Write' },
{ value: 'readonly', label: 'Read only' }
];
let connections: DatabaseConnection[] = $state([]);
let isLoading = $state(true);
let username: string = $state('');
let role: ConnectionRole = $state('readwrite');
let isCreating = $state(false);
let showDeleteConfirm = $state(false);
let connectionToDelete: DatabaseConnection | null = $state(null);
let isDeleting = $state(false);
onMount(async () => {
try {
const result = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.listConnections(database.$id);
connections = result.connections;
} catch {
connections = [];
} finally {
isLoading = false;
}
});
async function createConnection() {
if (!username) return;
isCreating = true;
try {
const connection = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.createConnection(database.$id, username, role);
connections = [...connections, connection];
username = '';
role = 'readwrite';
await invalidate(Dependencies.DATABASE);
addNotification({
message: `Database user "${connection.username}" has been created`,
type: 'success'
});
trackEvent(Submit.DatabaseCreateConnection);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseCreateConnection);
} finally {
isCreating = false;
}
}
async function deleteConnection() {
if (!connectionToDelete) return;
isDeleting = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.deleteConnection(database.$id, connectionToDelete.$id);
connections = connections.filter((c) => c.$id !== connectionToDelete.$id);
showDeleteConfirm = false;
connectionToDelete = null;
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Database user has been deleted',
type: 'success'
});
trackEvent(Submit.DatabaseDeleteConnection);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseDeleteConnection);
} finally {
isDeleting = false;
}
}
function getRoleLabel(r: ConnectionRole): string {
return r === 'readwrite' ? 'Read / Write' : 'Read only';
}
</script>
{#if !isLoading}
<Form onSubmit={createConnection}>
<CardGrid>
<svelte:fragment slot="title">Database users</svelte:fragment>
Create and manage database users with specific roles. Each user receives unique credentials
for connecting to the database.
<svelte:fragment slot="aside">
<ul>
{#if connections.length > 0}
<li class="u-margin-block-end-16">
<label class="label u-margin-block-end-8">Existing users</label>
<Layout.Stack direction="column" gap="s">
{#each connections as connection}
<Box>
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center">
<Layout.Stack direction="column" gap="xxs">
<h6 class="u-bold u-trim-1">
{connection.username}
</h6>
<Layout.Stack direction="row" gap="s">
<Typography.Caption
variant="400"
color="--fgcolor-neutral-tertiary">
{connection.database}
</Typography.Caption>
<Badge
variant="secondary"
content={getRoleLabel(connection.role)} />
</Layout.Stack>
<Typography.Caption
variant="400"
color="--fgcolor-neutral-tertiary">
Created: {toLocaleDateTime(
connection.$createdAt
)}
</Typography.Caption>
</Layout.Stack>
<Button
text
round
ariaLabel="Delete user {connection.username}"
on:click={() => {
connectionToDelete = connection;
showDeleteConfirm = true;
}}>
<span class="icon-trash" aria-hidden="true"></span>
</Button>
</Layout.Stack>
</Box>
{/each}
</Layout.Stack>
</li>
{:else}
<li class="u-margin-block-end-16">
<p class="text">No database users created.</p>
</li>
{/if}
<InputText
id="connectionUsername"
label="Username"
placeholder="Enter username"
autocomplete={false}
bind:value={username}
required />
<InputSelect
id="connectionRole"
label="Role"
bind:value={role}
options={roleOptions} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!username || isCreating} submit>
{isCreating ? 'Creating...' : 'Create user'}
</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Modal
title="Delete database user"
bind:show={showDeleteConfirm}
onSubmit={deleteConnection}>
<p class="text">
Are you sure you want to delete the database user
<b>{connectionToDelete?.username}</b>? Any active connections using this user will be
terminated.
</p>
<svelte:fragment slot="footer">
<Button
text
on:click={() => {
showDeleteConfirm = false;
connectionToDelete = null;
}}>Cancel</Button>
<Button danger submit disabled={isDeleting}>
{isDeleting ? 'Deleting...' : 'Delete'}
</Button>
</svelte:fragment>
</Modal>
{/if}
@@ -0,0 +1,291 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputSelect } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import type { DedicatedDatabase, CrossRegionStatus } from '$lib/sdk/dedicatedDatabases';
import { Badge, Layout } from '@appwrite.io/pink-svelte';
import { toLocaleDateTime } from '$lib/helpers/date';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
const regionOptions: { value: string; label: string }[] = [
{ value: 'fra', label: 'Frankfurt' },
{ value: 'nyc', label: 'New York' },
{ value: 'sfo', label: 'San Francisco' },
{ value: 'blr', label: 'Bangalore' },
{ value: 'lon', label: 'London' },
{ value: 'syd', label: 'Sydney' },
{ value: 'tor', label: 'Toronto' },
{ value: 'ams', label: 'Amsterdam' },
{ value: 'sgp', label: 'Singapore' }
];
let crossRegionStatus: CrossRegionStatus | null = $state(null);
let isLoading = $state(true);
let isEnabled = $state(false);
let standbyRegion: string = $state('');
let isEnabling = $state(false);
let showDisableConfirm = $state(false);
let isDisabling = $state(false);
let showFailoverConfirm = $state(false);
let isFailingOver = $state(false);
const availableRegionOptions = $derived(
regionOptions.filter((r) => r.value !== database.region)
);
function getStandbyStatusVariant(
status: string
): 'primary' | 'secondary' | 'success' | 'warning' | 'error' {
switch (status) {
case 'healthy':
return 'success';
case 'provisioning':
return 'primary';
case 'degraded':
return 'warning';
case 'unhealthy':
return 'error';
default:
return 'secondary';
}
}
onMount(async () => {
try {
crossRegionStatus = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.getCrossRegionStatus(database.$id);
isEnabled = crossRegionStatus.enabled;
} catch {
// 404 means not enabled
isEnabled = false;
crossRegionStatus = null;
} finally {
isLoading = false;
}
});
async function enableCrossRegion() {
if (!standbyRegion) return;
isEnabling = true;
try {
crossRegionStatus = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.enableCrossRegion(database.$id, standbyRegion);
isEnabled = true;
standbyRegion = '';
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Cross-region failover has been enabled',
type: 'success'
});
trackEvent(Submit.DatabaseEnableCrossRegion);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseEnableCrossRegion);
} finally {
isEnabling = false;
}
}
async function disableCrossRegion() {
isDisabling = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.disableCrossRegion(database.$id);
isEnabled = false;
crossRegionStatus = null;
showDisableConfirm = false;
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Cross-region failover has been disabled',
type: 'success'
});
trackEvent(Submit.DatabaseDisableCrossRegion);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseDisableCrossRegion);
} finally {
isDisabling = false;
}
}
async function triggerFailover() {
isFailingOver = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.triggerCrossRegionFailover(database.$id);
showFailoverConfirm = false;
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Cross-region failover has been triggered',
type: 'success'
});
trackEvent(Submit.DatabaseTriggerCrossRegionFailover);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseTriggerCrossRegionFailover);
} finally {
isFailingOver = false;
}
}
</script>
{#if !isLoading}
{#if isEnabled && crossRegionStatus}
<CardGrid>
<svelte:fragment slot="title">Cross-region failover</svelte:fragment>
Your database has a standby replica in another region for disaster recovery. In the event
of a regional outage, you can trigger a failover to promote the standby to primary.
<svelte:fragment slot="aside">
<ul>
<li class="u-margin-block-end-16">
<Layout.Stack direction="column" gap="s">
<div class="box">
<Layout.Stack direction="column" gap="xs">
<Layout.Stack direction="row" gap="xs" alignItems="center">
<span class="u-bold">Standby status</span>
<Badge
variant={getStandbyStatusVariant(crossRegionStatus.standbyStatus)}
content={crossRegionStatus.standbyStatus} />
</Layout.Stack>
<span class="text u-x-small">
Primary: {crossRegionStatus.primaryRegion}
&bull; Standby: {crossRegionStatus.standbyRegion}
</span>
<span class="text u-x-small">
Lag: {crossRegionStatus.lagSeconds}s
&bull; Last synced: {toLocaleDateTime(crossRegionStatus.lastSyncedAt)}
</span>
</Layout.Stack>
</div>
</Layout.Stack>
</li>
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Layout.Stack direction="row" gap="s">
<Button
secondary
on:click={() => {
showDisableConfirm = true;
}}>
Disable
</Button>
<Button
danger
on:click={() => {
showFailoverConfirm = true;
}}>
Trigger failover
</Button>
</Layout.Stack>
</svelte:fragment>
</CardGrid>
{:else}
<Form onSubmit={enableCrossRegion}>
<CardGrid>
<svelte:fragment slot="title">Cross-region failover</svelte:fragment>
Enable cross-region failover to maintain a standby replica in a different region for
disaster recovery.
<svelte:fragment slot="aside">
<ul>
<InputSelect
id="standbyRegion"
label="Standby region"
placeholder="Select a region"
bind:value={standbyRegion}
options={availableRegionOptions} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!standbyRegion || isEnabling} submit>
{isEnabling ? 'Enabling...' : 'Enable'}
</Button>
</svelte:fragment>
</CardGrid>
</Form>
{/if}
<Modal
title="Disable cross-region failover"
bind:show={showDisableConfirm}
onSubmit={disableCrossRegion}>
<p class="text">
Are you sure you want to disable cross-region failover for <b>{database.name}</b>?
The standby replica will be removed and your database will no longer have
disaster recovery across regions.
</p>
<svelte:fragment slot="footer">
<Button
text
on:click={() => {
showDisableConfirm = false;
}}>Cancel</Button>
<Button danger submit disabled={isDisabling}>
{isDisabling ? 'Disabling...' : 'Disable'}
</Button>
</svelte:fragment>
</Modal>
<Modal
title="Trigger cross-region failover"
bind:show={showFailoverConfirm}
onSubmit={triggerFailover}>
<p class="text">
Are you sure you want to trigger a cross-region failover for <b>{database.name}</b>?
This will promote the standby replica in <b>{crossRegionStatus?.standbyRegion}</b>
to primary. The current primary in <b>{crossRegionStatus?.primaryRegion}</b> will
become the new standby. This operation may cause brief downtime.
</p>
<svelte:fragment slot="footer">
<Button
text
on:click={() => {
showFailoverConfirm = false;
}}>Cancel</Button>
<Button danger submit disabled={isFailingOver}>
{isFailingOver ? 'Failing over...' : 'Trigger failover'}
</Button>
</svelte:fragment>
</Modal>
{/if}
@@ -0,0 +1,185 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputSelect } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import type { DedicatedDatabase, DatabaseExtensions } from '$lib/sdk/dedicatedDatabases';
import { Badge, Layout } from '@appwrite.io/pink-svelte';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
let extensions: DatabaseExtensions | null = $state(null);
let isLoading = $state(true);
let selectedExtension: string = $state('');
let isInstalling = $state(false);
let showUninstallConfirm = $state(false);
let extensionToUninstall: string | null = $state(null);
let isUninstalling = $state(false);
const availableOptions = $derived(
extensions
? extensions.available
.filter((ext) => !extensions.installed.includes(ext))
.sort()
.map((ext) => ({ value: ext, label: ext }))
: []
);
onMount(async () => {
try {
extensions = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.listExtensions(database.$id);
} catch {
extensions = { installed: [], available: [] };
} finally {
isLoading = false;
}
});
async function installExtension() {
if (!selectedExtension) return;
isInstalling = true;
try {
extensions = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.createExtension(database.$id, selectedExtension);
selectedExtension = '';
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Extension has been installed',
type: 'success'
});
trackEvent(Submit.DatabaseInstallExtension);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseInstallExtension);
} finally {
isInstalling = false;
}
}
async function uninstallExtension() {
if (!extensionToUninstall) return;
isUninstalling = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.deleteExtension(database.$id, extensionToUninstall);
if (extensions) {
extensions = {
installed: extensions.installed.filter((e) => e !== extensionToUninstall),
available: [...extensions.available, extensionToUninstall].sort()
};
}
showUninstallConfirm = false;
extensionToUninstall = null;
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Extension has been uninstalled',
type: 'success'
});
trackEvent(Submit.DatabaseUninstallExtension);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseUninstallExtension);
} finally {
isUninstalling = false;
}
}
</script>
{#if !isLoading && extensions}
<Form onSubmit={installExtension}>
<CardGrid>
<svelte:fragment slot="title">Extensions</svelte:fragment>
Manage PostgreSQL extensions for your database. Extensions add additional functionality such
as full-text search, geospatial queries, and more.
<svelte:fragment slot="aside">
<ul>
{#if extensions.installed.length > 0}
<li class="u-margin-block-end-16">
<label class="label u-margin-block-end-8">Installed extensions</label>
<Layout.Stack direction="row" gap="xs" wrap>
{#each extensions.installed as ext}
<Badge
variant="secondary"
content={ext}
onClear={() => {
extensionToUninstall = ext;
showUninstallConfirm = true;
}} />
{/each}
</Layout.Stack>
</li>
{:else}
<li class="u-margin-block-end-16">
<p class="text">No extensions installed.</p>
</li>
{/if}
{#if availableOptions.length > 0}
<InputSelect
id="extensionSelect"
label="Add extension"
placeholder="Select an extension"
bind:value={selectedExtension}
options={availableOptions} />
{/if}
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!selectedExtension || isInstalling} submit>
{isInstalling ? 'Installing...' : 'Install'}
</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Modal
title="Uninstall extension"
bind:show={showUninstallConfirm}
onSubmit={uninstallExtension}>
<p class="text">
Are you sure you want to uninstall the extension <b>{extensionToUninstall}</b> from
<b>{database.name}</b>? Any database objects that depend on this extension may stop
working.
</p>
<svelte:fragment slot="footer">
<Button
text
on:click={() => {
showUninstallConfirm = false;
extensionToUninstall = null;
}}>Cancel</Button>
<Button danger submit disabled={isUninstalling}>
{isUninstalling ? 'Uninstalling...' : 'Uninstall'}
</Button>
</svelte:fragment>
</Modal>
{/if}
@@ -0,0 +1,251 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import {
Button,
Form,
InputSwitch,
InputNumber,
InputSelect
} from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import type {
DedicatedDatabase,
HAStatus,
HASyncMode
} from '$lib/sdk/dedicatedDatabases';
import { Badge, Layout } from '@appwrite.io/pink-svelte';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
const syncModeOptions: { value: HASyncMode; label: string }[] = [
{ value: 'async', label: 'Asynchronous' },
{ value: 'sync', label: 'Synchronous' },
{ value: 'quorum', label: 'Quorum' }
];
let haStatus: HAStatus | null = $state(null);
let isLoading = $state(true);
let haEnabled: boolean = $state(database.highAvailability);
let replicaCount: number = $state(database.haReplicaCount);
let syncMode: HASyncMode = $state(database.haSyncMode ?? 'async');
let initialEnabled = $state(database.highAvailability);
let initialReplicaCount = $state(database.haReplicaCount);
let initialSyncMode: HASyncMode = $state(database.haSyncMode ?? 'async');
let showFailoverConfirm = $state(false);
let isFailingOver = $state(false);
const hasChanges = $derived(
haEnabled !== initialEnabled ||
replicaCount !== initialReplicaCount ||
syncMode !== initialSyncMode
);
function getHealthVariant(
status: string
): 'primary' | 'secondary' | 'success' | 'warning' | 'error' {
switch (status) {
case 'healthy':
return 'success';
case 'degraded':
return 'warning';
case 'unhealthy':
return 'error';
default:
return 'secondary';
}
}
onMount(async () => {
try {
haStatus = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.getHAStatus(database.$id);
} catch {
haStatus = null;
} finally {
isLoading = false;
}
});
async function updateHA() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.update(database.$id, {
highAvailability: haEnabled,
haReplicaCount: replicaCount,
haSyncMode: syncMode
});
initialEnabled = haEnabled;
initialReplicaCount = replicaCount;
initialSyncMode = syncMode;
// Refresh HA status after update
try {
haStatus = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.getHAStatus(database.$id);
} catch {
// Ignore if HA was just disabled
}
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'High availability settings have been updated',
type: 'success'
});
trackEvent(Submit.DatabaseUpdateHA);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseUpdateHA);
}
}
async function manualFailover() {
isFailingOver = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.createFailover(database.$id);
showFailoverConfirm = false;
// Refresh HA status
try {
haStatus = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.getHAStatus(database.$id);
} catch {
// Ignore
}
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Manual failover has been initiated',
type: 'success'
});
trackEvent(Submit.DatabaseManualFailover);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseManualFailover);
} finally {
isFailingOver = false;
}
}
</script>
{#if !isLoading}
<Form onSubmit={updateHA}>
<CardGrid>
<svelte:fragment slot="title">High availability</svelte:fragment>
High availability maintains replicas of your database that automatically take over if the
primary instance fails, minimizing downtime.
<svelte:fragment slot="aside">
<ul>
<InputSwitch
id="haEnabled"
label="Enable high availability"
bind:value={haEnabled} />
{#if haEnabled}
<InputNumber
id="replicaCount"
label="Replica count"
min={1}
max={5}
bind:value={replicaCount}
required />
<InputSelect
id="syncMode"
label="Sync mode"
bind:value={syncMode}
options={syncModeOptions} />
{/if}
{#if haStatus && haStatus.replicas.length > 0}
<li class="u-margin-block-start-16">
<label class="label u-margin-block-end-8">Replicas</label>
<Layout.Stack direction="column" gap="s">
{#each haStatus.replicas as replica}
<div class="box">
<Layout.Stack direction="row" gap="xs" alignItems="center">
<span class="u-bold">{replica.$id}</span>
<Badge
variant={replica.role === 'primary' ? 'primary' : 'secondary'}
content={replica.role} />
<Badge
variant={getHealthVariant(replica.status)}
content={replica.status} />
<span class="text u-x-small">
Lag: {replica.lagSeconds}s
</span>
</Layout.Stack>
</div>
{/each}
</Layout.Stack>
</li>
{/if}
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Layout.Stack direction="row" gap="s">
{#if haEnabled && haStatus?.enabled}
<Button
secondary
on:click={() => {
showFailoverConfirm = true;
}}>
Manual failover
</Button>
{/if}
<Button disabled={!hasChanges} submit>Update</Button>
</Layout.Stack>
</svelte:fragment>
</CardGrid>
</Form>
<Modal
title="Manual failover"
bind:show={showFailoverConfirm}
onSubmit={manualFailover}>
<p class="text">
Are you sure you want to trigger a manual failover for <b>{database.name}</b>?
This will promote a replica to primary. The operation may cause brief downtime
while the roles are switched.
</p>
<svelte:fragment slot="footer">
<Button
text
on:click={() => {
showFailoverConfirm = false;
}}>Cancel</Button>
<Button danger submit disabled={isFailingOver}>
{isFailingOver ? 'Failing over...' : 'Trigger failover'}
</Button>
</svelte:fragment>
</Modal>
{/if}
@@ -0,0 +1,102 @@
<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, InputSelect, InputNumber } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { DedicatedDatabase, MaintenanceDay } from '$lib/sdk/dedicatedDatabases';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
const dayOptions: { value: MaintenanceDay; label: string }[] = [
{ value: 'sun', label: 'Sunday' },
{ value: 'mon', label: 'Monday' },
{ value: 'tue', label: 'Tuesday' },
{ value: 'wed', label: 'Wednesday' },
{ value: 'thu', label: 'Thursday' },
{ value: 'fri', label: 'Friday' },
{ value: 'sat', label: 'Saturday' }
];
const hourOptions = Array.from({ length: 24 }, (_, i) => ({
value: i,
label: `${String(i).padStart(2, '0')}:00 UTC`
}));
let day: MaintenanceDay = $state(database.maintenanceWindowDay);
let hourUtc: number = $state(database.maintenanceWindowHourUtc);
let durationMinutes: number = $state(database.maintenanceWindowDurationMinutes);
const hasChanges = $derived(
day !== database.maintenanceWindowDay ||
hourUtc !== database.maintenanceWindowHourUtc ||
durationMinutes !== database.maintenanceWindowDurationMinutes
);
async function updateMaintenance() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.updateMaintenance(database.$id, {
day,
hourUtc,
durationMinutes
});
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Maintenance window has been updated',
type: 'success'
});
trackEvent(Submit.DatabaseUpdateMaintenance);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseUpdateMaintenance);
}
}
</script>
<Form onSubmit={updateMaintenance}>
<CardGrid>
<svelte:fragment slot="title">Maintenance window</svelte:fragment>
Schedule a preferred time window for automatic maintenance operations such as minor
version upgrades and patches.
<svelte:fragment slot="aside">
<ul>
<InputSelect
id="maintenanceDay"
label="Day"
bind:value={day}
options={dayOptions} />
<InputSelect
id="maintenanceHour"
label="Hour (UTC)"
bind:value={hourUtc}
options={hourOptions} />
<InputNumber
id="maintenanceDuration"
label="Duration (minutes)"
min={30}
max={480}
bind:value={durationMinutes}
required />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!hasChanges} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,63 @@
<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, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
let databaseName: string = $state(database.name);
async function updateName() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.update(database.$id, { name: databaseName });
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Database name has been updated',
type: 'success'
});
trackEvent(Submit.DatabaseUpdateName);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseUpdateName);
}
}
</script>
<Form onSubmit={updateName}>
<CardGrid>
<svelte:fragment slot="title">Name</svelte:fragment>
<svelte:fragment slot="aside">
<InputText
id="name"
label="Name"
placeholder="Enter database name"
autocomplete={false}
bind:value={databaseName}
required />
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={databaseName === database.name || !databaseName} submit>
Update
</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,88 @@
<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, InputNumber, InputTags } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
let maxConnections: number = $state(database.networkMaxConnections);
let idleTimeout: number = $state(database.networkIdleTimeoutSeconds);
let ipAllowlist: string[] = $state([...(database.networkIPAllowlist ?? [])]);
const hasChanges = $derived(
maxConnections !== database.networkMaxConnections ||
idleTimeout !== database.networkIdleTimeoutSeconds ||
JSON.stringify(ipAllowlist) !== JSON.stringify(database.networkIPAllowlist ?? [])
);
async function updateNetwork() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.update(database.$id, {
networkMaxConnections: maxConnections,
networkIdleTimeoutSeconds: idleTimeout,
networkIPAllowlist: ipAllowlist
});
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Network settings have been updated',
type: 'success'
});
trackEvent(Submit.DatabaseUpdateNetwork);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseUpdateNetwork);
}
}
</script>
<Form onSubmit={updateNetwork}>
<CardGrid>
<svelte:fragment slot="title">Network</svelte:fragment>
Configure connection limits and network access controls for your database.
<svelte:fragment slot="aside">
<ul>
<InputNumber
id="maxConnections"
label="Max connections"
min={1}
max={10000}
bind:value={maxConnections}
required />
<InputNumber
id="idleTimeout"
label="Idle timeout (seconds)"
min={0}
max={86400}
bind:value={idleTimeout}
required />
<InputTags
id="ipAllowlist"
label="IP allowlist"
placeholder="Enter IP address and press Enter"
bind:tags={ipAllowlist} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!hasChanges} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,130 @@
<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, InputSelect, InputNumber, InputSwitch } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { DedicatedDatabase, PoolerConfig, PoolerMode } from '$lib/sdk/dedicatedDatabases';
import { onMount } from 'svelte';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
const modeOptions: { value: PoolerMode; label: string }[] = [
{ value: 'transaction', label: 'Transaction' },
{ value: 'session', label: 'Session' }
];
let poolerConfig: PoolerConfig | null = $state(null);
let isLoading = $state(true);
let poolerEnabled: boolean = $state(false);
let poolerMode: PoolerMode = $state('transaction');
let poolSize: number = $state(25);
let initialEnabled = $state(false);
let initialMode: PoolerMode = $state('transaction');
let initialPoolSize = $state(25);
onMount(async () => {
try {
poolerConfig = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.getPoolerConfig(database.$id);
poolerEnabled = poolerConfig.enabled;
poolerMode = poolerConfig.mode;
poolSize = poolerConfig.defaultPoolSize;
initialEnabled = poolerConfig.enabled;
initialMode = poolerConfig.mode;
initialPoolSize = poolerConfig.defaultPoolSize;
} catch {
// Pooler not configured yet
poolerEnabled = false;
} finally {
isLoading = false;
}
});
const hasChanges = $derived(
poolerEnabled !== initialEnabled ||
poolerMode !== initialMode ||
poolSize !== initialPoolSize
);
async function updatePooler() {
try {
const dedicatedSdk = sdk.forProject(
page.params.region,
page.params.project
).dedicatedDatabases;
await dedicatedSdk.updatePoolerConfig(database.$id, {
mode: poolerEnabled ? poolerMode : undefined,
defaultPoolSize: poolerEnabled ? poolSize : undefined
});
initialEnabled = poolerEnabled;
initialMode = poolerMode;
initialPoolSize = poolSize;
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Connection pooler settings have been updated',
type: 'success'
});
trackEvent(Submit.DatabaseUpdatePooler);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseUpdatePooler);
}
}
</script>
{#if !isLoading}
<Form onSubmit={updatePooler}>
<CardGrid>
<svelte:fragment slot="title">Connection pooler</svelte:fragment>
A connection pooler sits between your application and the database, reusing connections
to reduce overhead. Transaction mode is recommended for serverless workloads.
<svelte:fragment slot="aside">
<ul>
<InputSwitch
id="poolerEnabled"
label="Enable connection pooler"
bind:value={poolerEnabled} />
{#if poolerEnabled}
<InputSelect
id="poolerMode"
label="Pooler mode"
bind:value={poolerMode}
options={modeOptions} />
<InputNumber
id="poolSize"
label="Default pool size"
min={1}
max={10000}
bind:value={poolSize}
required />
{/if}
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!hasChanges} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
{/if}
@@ -0,0 +1,246 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputSelect, InputCheckbox } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import type { DedicatedDatabase, ReadReplica } from '$lib/sdk/dedicatedDatabases';
import { Badge, Layout } from '@appwrite.io/pink-svelte';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
const regionOptions: { value: string; label: string }[] = [
{ value: 'fra', label: 'Frankfurt' },
{ value: 'nyc', label: 'New York' },
{ value: 'sfo', label: 'San Francisco' },
{ value: 'blr', label: 'Bangalore' },
{ value: 'lon', label: 'London' },
{ value: 'syd', label: 'Sydney' },
{ value: 'tor', label: 'Toronto' },
{ value: 'ams', label: 'Amsterdam' },
{ value: 'sgp', label: 'Singapore' }
];
let replicas: ReadReplica[] = $state([]);
let isLoading = $state(true);
let targetRegion: string = $state('');
let crossZoneConsent: boolean = $state(false);
let isCreating = $state(false);
let showDeleteConfirm = $state(false);
let replicaToDelete: ReadReplica | null = $state(null);
let isDeleting = $state(false);
const availableRegionOptions = $derived(
regionOptions.filter(
(r) =>
r.value !== database.region &&
!replicas.some((rep) => rep.targetRegion === r.value)
)
);
onMount(async () => {
try {
const result = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.listReadReplicas(database.$id);
replicas = result.replicas ?? [];
} catch {
replicas = [];
} finally {
isLoading = false;
}
});
function getStatusVariant(
status: string
): 'primary' | 'secondary' | 'success' | 'warning' | 'error' {
switch (status) {
case 'active':
return 'success';
case 'provisioning':
return 'primary';
case 'degraded':
return 'warning';
case 'failed':
return 'error';
case 'deleting':
return 'warning';
default:
return 'secondary';
}
}
async function addReplica() {
if (!targetRegion) return;
isCreating = true;
try {
const replica = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.createReadReplica(
database.$id,
targetRegion,
crossZoneConsent
);
replicas = [...replicas, replica];
targetRegion = '';
crossZoneConsent = false;
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Read replica is being provisioned',
type: 'success'
});
trackEvent(Submit.DatabaseCreateReadReplica);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseCreateReadReplica);
} finally {
isCreating = false;
}
}
async function deleteReplica() {
if (!replicaToDelete) return;
isDeleting = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.deleteReadReplica(database.$id, replicaToDelete.$id);
replicas = replicas.filter((r) => r.$id !== replicaToDelete?.$id);
showDeleteConfirm = false;
replicaToDelete = null;
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Read replica has been deleted',
type: 'success'
});
trackEvent(Submit.DatabaseDeleteReadReplica);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseDeleteReadReplica);
} finally {
isDeleting = false;
}
}
</script>
{#if !isLoading}
<Form onSubmit={addReplica}>
<CardGrid>
<svelte:fragment slot="title">Read replicas</svelte:fragment>
Deploy read-only replicas of your database to other regions to reduce read latency for
geographically distributed workloads.
<svelte:fragment slot="aside">
<ul>
{#if replicas.length > 0}
<li class="u-margin-block-end-16">
<label class="label u-margin-block-end-8">Active replicas</label>
<Layout.Stack direction="column" gap="s">
{#each replicas as replica}
<div class="box">
<Layout.Stack
direction="row"
gap="s"
justifyContent="space-between"
alignItems="center">
<Layout.Stack direction="column" gap="xxs">
<Layout.Stack direction="row" gap="xs" alignItems="center">
<span class="u-bold">{replica.$id}</span>
<Badge
variant={getStatusVariant(replica.status)}
content={replica.status} />
</Layout.Stack>
<span class="text u-x-small">
{replica.sourceRegion} &rarr; {replica.targetRegion}
&bull; Lag: {replica.lagSeconds}s
&bull; {replica.hostname}
</span>
</Layout.Stack>
<Button
text
danger
on:click={() => {
replicaToDelete = replica;
showDeleteConfirm = true;
}}>
Delete
</Button>
</Layout.Stack>
</div>
{/each}
</Layout.Stack>
</li>
{:else}
<li class="u-margin-block-end-16">
<p class="text">No read replicas configured.</p>
</li>
{/if}
{#if availableRegionOptions.length > 0}
<InputSelect
id="targetRegion"
label="Target region"
placeholder="Select a region"
bind:value={targetRegion}
options={availableRegionOptions} />
<InputCheckbox
id="crossZoneConsent"
label="I consent to cross-zone data transfer charges"
bind:checked={crossZoneConsent} />
{/if}
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!targetRegion || isCreating} submit>
{isCreating ? 'Adding...' : 'Add replica'}
</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Modal
title="Delete read replica"
bind:show={showDeleteConfirm}
onSubmit={deleteReplica}>
<p class="text">
Are you sure you want to delete the read replica
<b>{replicaToDelete?.$id}</b> in region <b>{replicaToDelete?.targetRegion}</b>?
This action cannot be undone.
</p>
<svelte:fragment slot="footer">
<Button
text
on:click={() => {
showDeleteConfirm = false;
replicaToDelete = null;
}}>Cancel</Button>
<Button danger submit disabled={isDeleting}>
{isDeleting ? 'Deleting...' : 'Delete'}
</Button>
</svelte:fragment>
</Modal>
{/if}
@@ -0,0 +1,131 @@
<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, InputSwitch, InputNumber } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases';
import { Layout } from '@appwrite.io/pink-svelte';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
function getKeyManagementLabel(km: string): string {
switch (km) {
case 'appwriteKms':
return 'Appwrite KMS';
case 'customerManaged':
return 'Customer-managed';
default:
return km;
}
}
function getResidencyLabel(residency: string): string {
switch (residency) {
case 'eu':
return 'European Union';
case 'us':
return 'United States';
case 'apac':
return 'Asia Pacific';
case 'global':
return 'Global';
default:
return residency;
}
}
let auditLogEnabled: boolean = $state(database.securityAuditLogEnabled);
let logRetentionDays: number = $state(database.securityLogRetentionDays);
let initialAuditLogEnabled = $state(database.securityAuditLogEnabled);
let initialLogRetentionDays = $state(database.securityLogRetentionDays);
const hasChanges = $derived(
auditLogEnabled !== initialAuditLogEnabled ||
logRetentionDays !== initialLogRetentionDays
);
async function updateSecurity() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.update(database.$id, {
securityAuditLogEnabled: auditLogEnabled,
securityLogRetentionDays: logRetentionDays
});
initialAuditLogEnabled = auditLogEnabled;
initialLogRetentionDays = logRetentionDays;
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Security settings have been updated',
type: 'success'
});
trackEvent(Submit.DatabaseUpdateSecurity);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseUpdateSecurity);
}
}
</script>
<Form onSubmit={updateSecurity}>
<CardGrid>
<svelte:fragment slot="title">Security</svelte:fragment>
Manage encryption, key management, data residency, and audit logging for your database.
<svelte:fragment slot="aside">
<ul>
<li class="u-margin-block-end-16">
<div class="box">
<Layout.Stack direction="column" gap="xs">
<Layout.Stack direction="row" gap="s">
<span class="u-bold">Encryption at rest:</span>
<span>{database.securityEncryptionAtRest ? 'Enabled' : 'Disabled'}</span>
</Layout.Stack>
<Layout.Stack direction="row" gap="s">
<span class="u-bold">Key management:</span>
<span>{getKeyManagementLabel(database.securityKeyManagement)}</span>
</Layout.Stack>
<Layout.Stack direction="row" gap="s">
<span class="u-bold">Data residency:</span>
<span>{getResidencyLabel(database.securityDataResidency)}</span>
</Layout.Stack>
</Layout.Stack>
</div>
</li>
<InputSwitch
id="auditLogEnabled"
label="Enable audit log"
bind:value={auditLogEnabled} />
{#if auditLogEnabled}
<InputNumber
id="logRetentionDays"
label="Log retention (days)"
min={1}
max={365}
bind:value={logRetentionDays}
required />
{/if}
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!hasChanges} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,146 @@
<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,
InputSwitch,
InputNumber,
InputCheckbox
} from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
const allStatements = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'EXPLAIN'] as const;
let sqlApiEnabled: boolean = $state(database.sqlApiEnabled);
let maxBytes: number = $state(database.sqlApiMaxBytes);
let maxRows: number = $state(database.sqlApiMaxRows);
let timeout: number = $state(database.sqlApiTimeoutSeconds);
let allowedStatements: string[] = $state([...(database.sqlApiAllowedStatements ?? [])]);
let initialEnabled = $state(database.sqlApiEnabled);
let initialMaxBytes = $state(database.sqlApiMaxBytes);
let initialMaxRows = $state(database.sqlApiMaxRows);
let initialTimeout = $state(database.sqlApiTimeoutSeconds);
let initialAllowedStatements = $state([...(database.sqlApiAllowedStatements ?? [])]);
function isStatementAllowed(statement: string): boolean {
return allowedStatements.includes(statement);
}
function toggleStatement(statement: string) {
if (allowedStatements.includes(statement)) {
allowedStatements = allowedStatements.filter((s) => s !== statement);
} else {
allowedStatements = [...allowedStatements, statement];
}
}
const hasChanges = $derived(
sqlApiEnabled !== initialEnabled ||
maxBytes !== initialMaxBytes ||
maxRows !== initialMaxRows ||
timeout !== initialTimeout ||
JSON.stringify([...allowedStatements].sort()) !==
JSON.stringify([...initialAllowedStatements].sort())
);
async function updateSqlApi() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.update(database.$id, {
sqlApiEnabled,
sqlApiMaxBytes: maxBytes,
sqlApiMaxRows: maxRows,
sqlApiTimeoutSeconds: timeout,
sqlApiAllowedStatements: allowedStatements
});
initialEnabled = sqlApiEnabled;
initialMaxBytes = maxBytes;
initialMaxRows = maxRows;
initialTimeout = timeout;
initialAllowedStatements = [...allowedStatements];
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'SQL API settings have been updated',
type: 'success'
});
trackEvent(Submit.DatabaseUpdateSqlApi);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseUpdateSqlApi);
}
}
</script>
<Form onSubmit={updateSqlApi}>
<CardGrid>
<svelte:fragment slot="title">SQL API</svelte:fragment>
The SQL API allows direct SQL query execution against your database through the Appwrite
API. Configure which statements are permitted and set resource limits.
<svelte:fragment slot="aside">
<ul>
<InputSwitch
id="sqlApiEnabled"
label="Enable SQL API"
bind:value={sqlApiEnabled} />
{#if sqlApiEnabled}
<InputNumber
id="maxBytes"
label="Max response bytes"
min={1024}
max={104857600}
bind:value={maxBytes}
required />
<InputNumber
id="maxRows"
label="Max rows"
min={1}
max={100000}
bind:value={maxRows}
required />
<InputNumber
id="timeout"
label="Timeout (seconds)"
min={1}
max={300}
bind:value={timeout}
required />
<li class="u-margin-block-start-16">
<label class="label u-margin-block-end-8">Allowed statements</label>
{#each allStatements as statement}
<InputCheckbox
id="stmt_{statement}"
label={statement}
checked={isStatementAllowed(statement)}
on:change={() => toggleStatement(statement)} />
{/each}
</li>
{/if}
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!hasChanges} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,68 @@
<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, InputNumber, Helper } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
let storageGb: number = $state(database.storage);
const isValid = $derived(storageGb >= database.storage && storageGb !== database.storage);
async function updateStorage() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.update(database.$id, { storage: storageGb });
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Storage has been resized',
type: 'success'
});
trackEvent(Submit.DatabaseResizeStorage);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseResizeStorage);
}
}
</script>
<Form onSubmit={updateStorage}>
<CardGrid>
<svelte:fragment slot="title">Storage</svelte:fragment>
Resize the storage allocated to your database. Storage can only be increased, not
decreased.
<svelte:fragment slot="aside">
<InputNumber
id="storage"
label="Storage (GB)"
min={database.storage}
max={database.maxStorageGb || 1000}
bind:value={storageGb}
required />
{#if storageGb < database.storage}
<Helper type="warning">Storage can only be increased, not decreased.</Helper>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!isValid} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,91 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { trackError, trackEvent, Submit } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputSelect } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
const tierOptions = [
{ value: 's-1vcpu-1gb', label: 'Starter - 1 vCPU, 1 GB RAM' },
{ value: 's-2vcpu-2gb', label: 'Standard - 2 vCPU, 2 GB RAM' },
{ value: 's-2vcpu-4gb', label: 'Standard Plus - 2 vCPU, 4 GB RAM' },
{ value: 's-4vcpu-8gb', label: 'Pro - 4 vCPU, 8 GB RAM' },
{ value: 's-4vcpu-16gb', label: 'Pro Plus - 4 vCPU, 16 GB RAM' },
{ value: 's-4vcpu-32gb', label: 'Business - 4 vCPU, 32 GB RAM' },
{ value: 's-8vcpu-32gb', label: 'Business Plus - 8 vCPU, 32 GB RAM' },
{ value: 's-8vcpu-64gb', label: 'Enterprise - 8 vCPU, 64 GB RAM' }
];
const tierResources: Record<string, { cpu: number; memory: number }> = {
's-1vcpu-1gb': { cpu: 1, memory: 1024 },
's-2vcpu-2gb': { cpu: 2, memory: 2048 },
's-2vcpu-4gb': { cpu: 2, memory: 4096 },
's-4vcpu-8gb': { cpu: 4, memory: 8192 },
's-4vcpu-16gb': { cpu: 4, memory: 16384 },
's-4vcpu-32gb': { cpu: 4, memory: 32768 },
's-8vcpu-32gb': { cpu: 8, memory: 32768 },
's-8vcpu-64gb': { cpu: 8, memory: 65536 }
};
let selectedTier: string = $state(database.tier);
async function updateTier() {
try {
const resources = tierResources[selectedTier];
if (!resources) {
throw new Error('Invalid tier selected');
}
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.update(database.$id, {
cpu: resources.cpu,
memory: resources.memory
});
await invalidate(Dependencies.DATABASE);
addNotification({
message: 'Resource tier has been updated. Scaling may take a few minutes.',
type: 'success'
});
trackEvent(Submit.DatabaseUpdateTier);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseUpdateTier);
}
}
</script>
<Form onSubmit={updateTier}>
<CardGrid>
<svelte:fragment slot="title">Resource scaling</svelte:fragment>
Change the compute resources allocated to your database. Scaling may cause a brief
interruption while the database restarts.
<svelte:fragment slot="aside">
<InputSelect
id="tier"
label="Resource tier"
bind:value={selectedTier}
options={tierOptions} />
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={selectedTier === database.tier} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,121 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { DedicatedDatabase, DatabaseStatusDetail } from '$lib/sdk/dedicatedDatabases';
import { onMount } from 'svelte';
import { Typography, Layout } from '@appwrite.io/pink-svelte';
let {
database
}: {
database: DedicatedDatabase;
} = $props();
let statusDetail: DatabaseStatusDetail | null = $state(null);
let showConfirm = $state(false);
let isUpgrading = $state(false);
let targetVersion: string = $state('');
let isLoading = $state(true);
const currentVersion = $derived(statusDetail?.version ?? database.version);
onMount(async () => {
try {
statusDetail = await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.getStatus(database.$id);
} catch {
// Status not available
} finally {
isLoading = false;
}
});
async function upgradeVersion() {
isUpgrading = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
.dedicatedDatabases.upgradeVersion(database.$id, targetVersion);
await invalidate(Dependencies.DATABASE);
showConfirm = false;
addNotification({
message: `Database is upgrading to version ${targetVersion}. This may take a few minutes.`,
type: 'success'
});
trackEvent(Submit.DatabaseUpgradeVersion);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DatabaseUpgradeVersion);
} finally {
isUpgrading = false;
}
}
</script>
{#if !isLoading}
<CardGrid>
<svelte:fragment slot="title">Version</svelte:fragment>
Upgrade your database engine to a newer version. This operation may cause a brief
interruption.
<svelte:fragment slot="aside">
<Layout.Stack gap="m">
<Layout.Stack gap="xxs">
<Typography.Caption variant="400" color="--fgcolor-neutral-tertiary">
Current version
</Typography.Caption>
<Typography.Text variant="m-500">
{currentVersion}
</Typography.Text>
</Layout.Stack>
<InputText
id="targetVersion"
label="Target version"
placeholder="e.g. 16.2"
bind:value={targetVersion} />
</Layout.Stack>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
secondary
disabled={!targetVersion || targetVersion === currentVersion}
on:click={() => {
showConfirm = true;
trackEvent('click_database_upgrade_version');
}}>
Upgrade
</Button>
</svelte:fragment>
</CardGrid>
<Modal
title="Upgrade database version"
bind:show={showConfirm}
onSubmit={upgradeVersion}>
<p class="text">
Are you sure you want to upgrade <b>{database.name}</b> from version
<b>{currentVersion}</b> to <b>{targetVersion}</b>? The database may be briefly
unavailable during the upgrade.
</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showConfirm = false)}>Cancel</Button>
<Button submit disabled={isUpgrading}>
{isUpgrading ? 'Upgrading...' : 'Upgrade'}
</Button>
</svelte:fragment>
</Modal>
{/if}