mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
(feat): Add dedicated database settings components
This commit is contained in:
+137
-4
@@ -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>
|
||||
|
||||
+66
@@ -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 />
|
||||
+90
@@ -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>
|
||||
+92
@@ -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>
|
||||
+277
@@ -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}
|
||||
+100
@@ -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>
|
||||
+227
@@ -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}
|
||||
+291
@@ -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}
|
||||
• Standby: {crossRegionStatus.standbyRegion}
|
||||
</span>
|
||||
<span class="text u-x-small">
|
||||
Lag: {crossRegionStatus.lagSeconds}s
|
||||
• 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}
|
||||
+185
@@ -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}
|
||||
+251
@@ -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}
|
||||
+102
@@ -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>
|
||||
+63
@@ -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>
|
||||
+88
@@ -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>
|
||||
+130
@@ -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}
|
||||
+246
@@ -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} → {replica.targetRegion}
|
||||
• Lag: {replica.lagSeconds}s
|
||||
• {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}
|
||||
+131
@@ -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>
|
||||
+146
@@ -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>
|
||||
+68
@@ -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>
|
||||
+91
@@ -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>
|
||||
+121
@@ -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}
|
||||
Reference in New Issue
Block a user