refactor: migration export

This commit is contained in:
tglide
2023-07-17 13:17:41 +01:00
parent 09a9be3e3d
commit 23cb17135f
20 changed files with 538 additions and 799 deletions
+3 -1
View File
@@ -17,7 +17,8 @@ export type CommandGroup =
| 'storage'
| 'domains'
| 'webhooks'
| 'integrations';
| 'integrations'
| 'migrations';
type BaseCommand = {
callback: () => void;
@@ -264,6 +265,7 @@ export const commandGroupRanks = derived(groupRankTransformations, ($groupRankTr
functions: 0,
storage: 0,
integrations: 0,
migrations: 0,
help: -1
};
-2
View File
@@ -15,12 +15,10 @@
});
const examples = [
'How can I integrate Appwrite with my frontend application?',
'How to add platform in the console?',
'How can I manage users, permissions, and access control in Appwrite?',
'How can I set up database collections and documents in Appwrite?',
'How do I configure and manage server-side functions in Appwrite?',
'How can I access the audit logs in the console?',
'How to add custom domain in the console?'
];
+84 -3
View File
@@ -1,8 +1,5 @@
import { excludeArray } from '$lib/helpers/array';
import { writable } from 'svelte/store';
export type Provider = 'appwrite' | 'nhost' | 'supabase' | 'firebase';
const initialFormData = {
users: {
root: false,
@@ -103,3 +100,87 @@ export const migrationFormToResources = (formData: MigrationFormData): Resource[
return resources;
};
import {
PUBLIC_NHOST_TEST_DATABASE,
PUBLIC_NHOST_TEST_PASSWORD,
PUBLIC_NHOST_TEST_REGION,
PUBLIC_NHOST_TEST_SECRET,
PUBLIC_NHOST_TEST_SUBDOMAIN,
PUBLIC_NHOST_TEST_USERNAME
} from '$env/static/public';
type AppwriteInput = {
provider: 'appwrite';
endpoint?: string;
projectID?: string;
apiKey?: string;
};
type FirebaseInput = {
provider: 'firebase';
serviceAccount?: string;
};
type SupabaseInput = {
provider: 'supabase';
host?: string;
username?: string;
password?: string;
endpoint?: string;
apiKey?: string;
port?: number;
};
type NhostInput = {
provider: 'nhost';
region?: string;
subdomain?: string;
database?: string;
username?: string;
password?: string;
adminSecret?: string;
};
export type ProviderInput = AppwriteInput | NhostInput | SupabaseInput | FirebaseInput;
export type Provider = ProviderInput['provider'];
// const mockProvider: ProviderInput = {
// provider: 'appwrite',
// endpoint: PUBLIC_MOCK_ENDPOINT,
// apiKey: PUBLIC_MOCK_APIKEY,
// projectID: PUBLIC_MOCK_PROJECTID
// };
// const mockProvider: ProviderInput = {
// provider: 'supabase',
// endpoint: PUBLIC_SUPABASE_TEST_ENDPOINT,
// apiKey: PUBLIC_SUPABASE_TEST_KEY,
// host: PUBLIC_SUPABASE_TEST_HOST,
// port: Number(PUBLIC_SUPABASE_TEST_PORT),
// username: PUBLIC_SUPABASE_TEST_USERNAME,
// password: PUBLIC_SUPABASE_TEST_PASSWORD
// };
const mockProvider: ProviderInput = {
provider: 'nhost',
subdomain: PUBLIC_NHOST_TEST_SUBDOMAIN,
region: PUBLIC_NHOST_TEST_REGION,
adminSecret: PUBLIC_NHOST_TEST_SECRET,
database: PUBLIC_NHOST_TEST_DATABASE,
username: PUBLIC_NHOST_TEST_USERNAME,
password: PUBLIC_NHOST_TEST_PASSWORD
};
const initialProvider: ProviderInput = { provider: 'appwrite' };
export const createMigrationProviderStore = () => {
const store = writable<ProviderInput>({ ...mockProvider });
const changeProvider = (provider: Provider) => {
const newProvider: ProviderInput = { provider };
store.set(newProvider);
};
return {
...store,
changeProvider
};
};
-60
View File
@@ -1,60 +0,0 @@
import { writable } from 'svelte/store';
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const randRange = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
type Status = 'idle' | 'running' | 'error' | 'success';
type State = {
items: {
databases?: Status;
documents?: Status;
users?: Status;
storage?: Status;
};
status: Status;
};
function createMigrator() {
const { subscribe, update, set } = writable<State>({
items: { databases: 'idle', documents: 'running', users: 'error', storage: 'success' },
status: 'running'
});
const startMigration = async () => {
set({
items: {
databases: 'idle',
documents: 'idle',
users: 'idle',
storage: 'idle'
},
status: 'running'
});
const keys = ['databases', 'documents', 'users', 'storage'];
for (const key of keys) {
sleep(randRange(1000, 5000)).then(() => {
update((state) => ({
...state,
items: { ...state.items, [key]: 'running' }
}));
sleep(randRange(1000, 5000)).then(() => {
update((state) => ({
...state,
items: { ...state.items, [key]: 'success' }
}));
});
});
}
};
return {
subscribe,
startMigration
};
}
export const migrator = createMigrator();
+2 -3
View File
@@ -10,13 +10,12 @@
import { user } from '$lib/stores/user';
import { ENV, isCloud } from '$lib/system';
import * as Sentry from '@sentry/svelte';
import LogRocket from 'logrocket';
import { BrowserTracing } from '@sentry/tracing';
import LogRocket from 'logrocket';
import { onMount } from 'svelte';
import { onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals';
import Loading from './loading.svelte';
import { loading, requestedMigration } from './store';
import { openMigrationWizard } from './console/organization-[organization]/(migration-wizard)';
import { loading } from './store';
if (browser) {
window.VERCEL_ANALYTICS_ID = import.meta.env.VERCEL_ANALYTICS_ID?.toString() ?? false;
+3 -1
View File
@@ -7,6 +7,7 @@ import { redirect } from '@sveltejs/kit';
import { Dependencies } from '$lib/constants';
import type { LayoutLoad } from './$types';
import { requestedMigration } from './store';
import { parseIfString } from '$lib/helpers/object';
export const ssr = false;
@@ -14,7 +15,8 @@ export const load: LayoutLoad = async ({ depends, url }) => {
depends(Dependencies.ACCOUNT);
if (url.searchParams.has('migrate')) {
requestedMigration.set(url.searchParams.get('migrate'));
const migrateData = url.searchParams.get('migrate');
requestedMigration.set(parseIfString(migrateData));
}
try {
@@ -0,0 +1,23 @@
import { createMigrationFormStore, createMigrationProviderStore } from '$lib/stores/migration';
import { wizard } from '$lib/stores/wizard';
import { requestedMigration } from '$routes/store';
import { get, writable } from 'svelte/store';
import Wizard from './wizard.svelte';
export const formData = createMigrationFormStore();
export function openMigrationWizard() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
wizard.start(Wizard as any);
const migData = get(requestedMigration);
provider.set({
provider: 'appwrite',
apiKey: migData?.apiKey,
endpoint: migData?.endpoint,
projectID: migData?.projectId
});
}
export const selectedProject = writable(null);
export const provider = createMigrationProviderStore();
@@ -0,0 +1,338 @@
<script lang="ts">
import { EyebrowHeading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { deepMap } from '$lib/helpers/object';
import type { DeepKeys, WritableValue } from '$lib/helpers/types';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import {
createMigrationFormStore,
createMigrationProviderStore,
providerResources
} from '$lib/stores/migration';
import { wizard } from '$lib/stores/wizard';
export let formData: ReturnType<typeof createMigrationFormStore>;
export let provider: ReturnType<typeof createMigrationProviderStore>;
type FormData = WritableValue<typeof formData>;
type FormKeys = DeepKeys<FormData>;
/**
*
* @param field {string} The field to update.
* Representes the path to the field in the formData object e.g. 'users.root'
*/
function handleInputChange(field: FormKeys) {
return (event: Event) => {
const checked = (event.target as HTMLInputElement).checked;
// For each entry in formData, if the root is changed, change all children to the same value
// otherwise, if a child is changed to true, change the root to true
const [parent, child] = field.split('.');
if (child === 'root') {
formData.update((data) => {
data[parent].root = checked;
for (const key in data[parent]) {
if (key !== 'root') {
data[parent][key] = checked;
}
}
return data;
});
} else if (checked) {
formData.update((data) => {
data[parent].root = checked;
return data;
});
}
};
}
function deselectAll() {
$formData = deepMap($formData, (v) => {
if (typeof v === 'boolean') {
return false;
}
return v;
});
}
function selectAll() {
$formData = deepMap($formData, (v) => {
if (typeof v === 'boolean') {
return true;
}
return v;
});
}
let report: any;
onMount(async () => {
switch ($provider.provider) {
case 'appwrite': {
const res = await sdk.forProject.migrations.generateAppwriteReport(
providerResources.appwrite,
$provider.endpoint,
$provider.projectID,
$provider.apiKey
);
report = res;
break;
}
case 'supabase': {
const res = await sdk.forProject.migrations.generateSupabaseReport(
providerResources.supabase,
$provider.endpoint,
$provider.apiKey,
$provider.host,
$provider.username,
$provider.password,
$provider.port
);
report = res;
break;
}
case 'nhost': {
const res = await sdk.forProject.migrations.generateNHostReport(
providerResources.nhost,
$provider.subdomain,
$provider.region,
$provider.adminSecret,
$provider.database,
$provider.username,
$provider.password
);
report = res;
}
}
});
$: {
wizard.setNextDisabled(!report);
}
$: resources = providerResources[$provider.provider];
</script>
<div class="box" style:border-radius="0.5rem">
<div class="u-flex u-flex-vertical u-gap-16">
<EyebrowHeading class="eyebrow" tag="h3" size={3}>Good to know</EyebrowHeading>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-cog" />
</div>
<div>
<p class="u-bold">Project settings are not imported</p>
<p>You will need to set service and project settings manually</p>
</div>
</div>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-trending-up" />
</div>
<div>
<p class="u-bold">Keep your project plan limits in mind</p>
<p>Make sure to have enough storage in your project plan when importing files</p>
</div>
</div>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-currency-dollar" />
</div>
<div>
<p class="u-bold">Transfer is free of charge</p>
<p>You won't be charged for Appwrite bandwidth usage for importing data</p>
</div>
</div>
</div>
</div>
<ul class="buttons-list u-margin-block-start-32 u-main-end">
<li class="buttons-list-item">
<Button text on:click={deselectAll}>Deselect all</Button>
</li>
<li class="buttons-list-item">
<Button text on:click={selectAll}>Select all</Button>
</li>
</ul>
<ul class="u-flex u-flex-vertical u-gap-32 u-margin-block-start-16">
{#if resources.includes('user')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.users.root}
on:change={handleInputChange('users.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Users</span>
<span class="inline-tag">{report?.user ?? '...'}</span>
</div>
<div />
<span>Import all users</span>
{#if resources.includes('team')}
<ul>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.users.teams}
on:change={handleInputChange('users.teams')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include teams</span>
<span class="inline-tag">{report?.team ?? '...'}</span>
</div>
<div />
<span>Import all teams and the team memberships of your users</span>
</li>
</ul>
{/if}
</li>
{/if}
{#if resources.includes('database')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.databases.root}
on:change={handleInputChange('databases.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Databases</span>
<span class="inline-tag">{report?.database ?? '...'}</span>
</div>
<div />
<span>Import all databases, including collections, indexes and attributes</span>
{#if resources.includes('document')}
<ul>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.databases.documents}
on:change={handleInputChange('databases.documents')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include documents</span>
<span class="inline-tag">{report?.document ?? '...'}</span>
</div>
<div />
<span>Import all of your documents</span>
</li>
</ul>
{/if}
</li>
{/if}
{#if resources.includes('function')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.root}
on:change={handleInputChange('functions.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Functions</span>
<span class="inline-tag">{report?.function ?? '...'}</span>
</div>
<div />
<span>Import all functions and their active deployment</span>
<ul>
{#if resources.includes('envVar')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.env}
on:change={handleInputChange('functions.env')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include environment variables</span>
</div>
<div />
<span>Import all environment variables</span>
</li>
{/if}
{#if resources.includes('deployment')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.inactive}
on:change={handleInputChange('functions.inactive')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include inactive deployments</span>
</div>
<div />
<span>Import all deployments that are not currently active</span>
</li>
{/if}
</ul>
</li>
{/if}
{#if resources.includes('bucket') && resources.includes('file')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.storage.root}
on:change={handleInputChange('storage.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Storage</span>
<span class="inline-tag">
{report?.size ? `${report.size.toFixed(2)}MB` : '...'}
</span>
</div>
<div />
<span>
Import all buckets <span class="inline-tag">{report?.bucket ?? '...'}</span> and
files
<span class="inline-tag">{report?.file ?? '...'}</span>
</span>
</li>
{/if}
</ul>
<style lang="scss">
.box :global(.eyebrow) {
font-weight: 500;
color: hsl(var(--color-neutral-70));
}
.circled {
width: 1.5rem;
height: 1.5rem;
flex-shrink: 0;
border-radius: 100%;
border: 1px solid hsl(var(--color-border));
position: relative;
i {
position: absolute;
left: 50%;
top: 50%;
translate: -50% -50%;
font-size: 1rem;
}
}
.buttons-list {
border-block-end: 1px solid hsl(var(--color-border));
padding-block-end: 0.625rem;
}
.checkbox-field {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.25rem 1rem;
align-items: center;
ul {
grid-column-start: 2;
padding-block-end: 2rem;
li {
margin-block-start: 1rem;
}
}
}
</style>
@@ -0,0 +1,11 @@
<script lang="ts">
import { WizardStep } from '$lib/layout';
import { formData, provider } from '.';
import ResourceForm from './resource-form.svelte';
</script>
<WizardStep>
<svelte:fragment slot="title">Select data</svelte:fragment>
<ResourceForm {provider} {formData} />
</WizardStep>
@@ -4,9 +4,12 @@
import Wizard from '$lib/layout/wizard.svelte';
import { migrationFormToResources } from '$lib/stores/migration';
import { onDestroy } from 'svelte';
import { formData } from '.';
import { formData, provider } from '.';
import Step1 from './step1.svelte';
import Step2 from './step2.svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
const onExit = () => {
formData.reset();
@@ -14,8 +17,16 @@
const onFinish = async () => {
const resources = migrationFormToResources($formData);
console.log('resources', resources);
// wizard.hide();
if ($provider.provider !== 'appwrite') return;
const res = await sdk.forProject.migrations.migrateAppwrite(
resources,
$provider.endpoint,
$provider.projectID,
$provider.apiKey
);
console.log('appwrite', res);
invalidate(Dependencies.MIGRATIONS);
};
onDestroy(onExit);
+5
View File
@@ -21,6 +21,7 @@
import { CommandCenter, registerCommands } from '$lib/commandCenter';
import { AI, Organizations } from '$lib/commandCenter/panels';
import { addSubPanel } from '$lib/commandCenter/subPanels';
import { openMigrationWizard } from './(migration-wizard)';
$: $registerCommands([
{
@@ -108,6 +109,10 @@
$log.data = null;
$log.func = null;
}
$: if ($requestedMigration) {
openMigrationWizard();
}
</script>
<CommandCenter />
@@ -1,13 +0,0 @@
import { createMigrationFormStore } from '$lib/stores/migration';
import { wizard } from '$lib/stores/wizard';
import { writable } from 'svelte/store';
import Wizard from './wizard.svelte';
export const formData = createMigrationFormStore();
export function openMigrationWizard() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
wizard.start(Wizard as any);
}
export const selectedProject = writable(null);
@@ -1,296 +0,0 @@
<script lang="ts">
import { EyebrowHeading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { deepMap } from '$lib/helpers/object';
import type { DeepKeys, WritableValue } from '$lib/helpers/types';
import { WizardStep } from '$lib/layout';
import { onMount } from 'svelte';
import { migrationFormToResources } from '$lib/stores/migration';
import { formData } from '.';
type FormData = WritableValue<typeof formData>;
type FormKeys = DeepKeys<FormData>;
/**
*
* @param field {string} The field to update.
* Representes the path to the field in the formData object e.g. 'users.root'
*/
function handleInputChange(field: FormKeys) {
return (event: Event) => {
const checked = (event.target as HTMLInputElement).checked;
// For each entry in formData, if the root is changed, change all children to the same value
// otherwise, if a child is changed to true, change the root to true
const [parent, child] = field.split('.');
if (child === 'root') {
formData.update((data) => {
data[parent].root = checked;
for (const key in data[parent]) {
if (key !== 'root') {
data[parent][key] = checked;
}
}
return data;
});
} else if (checked) {
formData.update((data) => {
data[parent].root = checked;
return data;
});
}
};
}
function deselectAll() {
$formData = deepMap($formData, (v) => {
if (typeof v === 'boolean') {
return false;
}
return v;
});
}
function selectAll() {
$formData = deepMap($formData, (v) => {
if (typeof v === 'boolean') {
return true;
}
return v;
});
}
let report: any;
onMount(async () => {
const resources = migrationFormToResources($formData);
// switch ($provider.provider) {
// case 'appwrite': {
// const res = await sdk.forProject.migrations.generateAppwriteReport(
// resources,
// $provider.endpoint,
// $provider.projectID,
// $provider.apiKey
// );
// report = res;
// }
// }
});
</script>
<WizardStep>
<svelte:fragment slot="title">Select data</svelte:fragment>
<div class="box" style:border-radius="0.5rem">
<div class="u-flex u-flex-vertical u-gap-16">
<EyebrowHeading class="eyebrow" tag="h3" size={3}>Good to know</EyebrowHeading>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-cog" />
</div>
<div>
<p class="u-bold">Project settings are not imported</p>
<p>You will need to set service and project settings manually</p>
</div>
</div>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-trending-up" />
</div>
<div>
<p class="u-bold">Keep your project plan limits in mind</p>
<p>
Make sure to have enough storage in your project plan when importing files
</p>
</div>
</div>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-currency-dollar" />
</div>
<div>
<p class="u-bold">Transfer is free of charge</p>
<p>You won’t be charged for Appwrite bandwidth usage for importing data</p>
</div>
</div>
</div>
</div>
<ul class="buttons-list u-margin-block-start-32 u-main-end">
<li class="buttons-list-item">
<Button text on:click={deselectAll}>Deselect all</Button>
</li>
<li class="buttons-list-item">
<Button text on:click={selectAll}>Select all</Button>
</li>
</ul>
<ul class="u-flex u-flex-vertical u-gap-32 u-margin-block-start-16">
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.users.root}
on:change={handleInputChange('users.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Users</span>
{#if report}
<span class="inline-tag">{report.user}</span>
{/if}
</div>
<div />
<span>Import all users</span>
<ul>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.users.teams}
on:change={handleInputChange('users.teams')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include teams</span>
{#if report}
<span class="inline-tag">{report.team}</span>
{/if}
</div>
<div />
<span>Import all teams and the team memberships of your users</span>
</li>
</ul>
</li>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.databases.root}
on:change={handleInputChange('databases.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Databases</span>
{#if report}
<span class="inline-tag">{report.database}</span>
{/if}
</div>
<div />
<span>Import all databases, including collections, indexes and attributes</span>
<ul>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.databases.documents}
on:change={handleInputChange('databases.documents')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include documents</span>
{#if report}
<span class="inline-tag">{report.document}</span>
{/if}
</div>
<div />
<span>Import all of your documents</span>
</li>
</ul>
</li>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.root}
on:change={handleInputChange('functions.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Functions</span>
{#if report}
<span class="inline-tag">{report.function}</span>
{/if}
</div>
<div />
<span>Import all functions and their active deployment</span>
<ul>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.env}
on:change={handleInputChange('functions.env')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include environment variables</span>
</div>
<div />
<span>Import all environment variables</span>
</li>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.inactive}
on:change={handleInputChange('functions.inactive')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include inactive deployments</span>
</div>
<div />
<span>Import all deployments that are not currently active</span>
</li>
</ul>
</li>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.storage.root}
on:change={handleInputChange('storage.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Storage</span>
{#if report}
<span class="inline-tag">{report.size.toFixed(2)}MB</span>
{/if}
</div>
<div />
{#if report}
<span>Import all buckets ({report.bucket}) and files ({report.file})</span>
{:else}
<span>Import all buckets and files</span>
{/if}
</li>
</ul>
</WizardStep>
<style lang="scss">
.box :global(.eyebrow) {
font-weight: 500;
color: hsl(var(--color-neutral-70));
}
.circled {
width: 1.5rem;
height: 1.5rem;
flex-shrink: 0;
border-radius: 100%;
border: 1px solid hsl(var(--color-border));
position: relative;
i {
position: absolute;
left: 50%;
top: 50%;
translate: -50% -50%;
font-size: 1rem;
}
}
.buttons-list {
border-block-end: 1px solid hsl(var(--color-border));
padding-block-end: 0.625rem;
}
.checkbox-field {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.25rem 1rem;
align-items: center;
ul {
grid-column-start: 2;
padding-block-end: 2rem;
li {
margin-block-start: 1rem;
}
}
}
</style>
@@ -2,7 +2,7 @@
import { newOrgModal, newMemberModal } from '$lib/stores/organization';
import CreateMember from './createMember.svelte';
import Create from '../createOrganization.svelte';
import { openMigrationWizard } from './(migration-wizard)';
import { openMigrationWizard } from '../(migration-wizard)';
import { requestedMigration } from '$routes/store';
$: if ($requestedMigration) {
@@ -1,91 +1,13 @@
import {
PUBLIC_MOCK_APIKEY,
PUBLIC_MOCK_ENDPOINT,
PUBLIC_MOCK_PROJECTID,
PUBLIC_SUPABASE_TEST_ENDPOINT,
PUBLIC_SUPABASE_TEST_HOST,
PUBLIC_SUPABASE_TEST_KEY,
PUBLIC_SUPABASE_TEST_PASSWORD,
PUBLIC_SUPABASE_TEST_USERNAME,
PUBLIC_SUPABASE_TEST_PORT,
PUBLIC_NHOST_TEST_SUBDOMAIN,
PUBLIC_NHOST_TEST_REGION,
PUBLIC_NHOST_TEST_SECRET,
PUBLIC_NHOST_TEST_DATABASE,
PUBLIC_NHOST_TEST_USERNAME,
PUBLIC_NHOST_TEST_PASSWORD
} from '$env/static/public';
import { createMigrationFormStore } from '$lib/stores/migration';
import { createMigrationFormStore, createMigrationProviderStore } from '$lib/stores/migration';
import { wizard } from '$lib/stores/wizard';
import { writable } from 'svelte/store';
import Wizard from './wizard.svelte';
type AppwriteInput = {
provider: 'appwrite';
endpoint?: string;
projectID?: string;
apiKey?: string;
};
type FirebaseInput = {
provider: 'firebase';
serviceAccount?: string;
};
type SupabaseInput = {
provider: 'supabase';
host?: string;
username?: string;
password?: string;
endpoint?: string;
apiKey?: string;
port?: number;
};
type NhostInput = {
provider: 'nhost';
region?: string;
subdomain?: string;
database?: string;
username?: string;
password?: string;
adminSecret?: string;
};
type ProviderInput = AppwriteInput | NhostInput | SupabaseInput | FirebaseInput;
const initialProvider: ProviderInput = { provider: 'appwrite' };
// const mockProvider: ProviderInput = {
// provider: 'appwrite',
// endpoint: PUBLIC_MOCK_ENDPOINT,
// apiKey: PUBLIC_MOCK_APIKEY,
// projectID: PUBLIC_MOCK_PROJECTID
// };
// const mockProvider: ProviderInput = {
// provider: 'supabase',
// endpoint: PUBLIC_SUPABASE_TEST_ENDPOINT,
// apiKey: PUBLIC_SUPABASE_TEST_KEY,
// host: PUBLIC_SUPABASE_TEST_HOST,
// port: Number(PUBLIC_SUPABASE_TEST_PORT),
// username: PUBLIC_SUPABASE_TEST_USERNAME,
// password: PUBLIC_SUPABASE_TEST_PASSWORD
// };
const mockProvider: ProviderInput = {
provider: 'nhost',
subdomain: PUBLIC_NHOST_TEST_SUBDOMAIN,
region: PUBLIC_NHOST_TEST_REGION,
adminSecret: PUBLIC_NHOST_TEST_SECRET,
database: PUBLIC_NHOST_TEST_DATABASE,
username: PUBLIC_NHOST_TEST_USERNAME,
password: PUBLIC_NHOST_TEST_PASSWORD
};
export const provider = writable<ProviderInput>({ ...mockProvider });
export const provider = createMigrationProviderStore();
export const formData = createMigrationFormStore();
export const resetImportStores = () => {
provider.set({ ...mockProvider });
provider.changeProvider('appwrite');
formData.reset();
};
@@ -1,333 +1,11 @@
<script lang="ts">
import { EyebrowHeading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { deepMap } from '$lib/helpers/object';
import type { DeepKeys, WritableValue } from '$lib/helpers/types';
import { sdk } from '$lib/stores/sdk';
import { WizardStep } from '$lib/layout';
import { onMount } from 'svelte';
import { providerResources } from '$lib/stores/migration';
import ResourceForm from '$routes/console/(migration-wizard)/resource-form.svelte';
import { formData, provider } from '.';
type FormData = WritableValue<typeof formData>;
type FormKeys = DeepKeys<FormData>;
/**
*
* @param field {string} The field to update.
* Representes the path to the field in the formData object e.g. 'users.root'
*/
function handleInputChange(field: FormKeys) {
return (event: Event) => {
const checked = (event.target as HTMLInputElement).checked;
// For each entry in formData, if the root is changed, change all children to the same value
// otherwise, if a child is changed to true, change the root to true
const [parent, child] = field.split('.');
if (child === 'root') {
formData.update((data) => {
data[parent].root = checked;
for (const key in data[parent]) {
if (key !== 'root') {
data[parent][key] = checked;
}
}
return data;
});
} else if (checked) {
formData.update((data) => {
data[parent].root = checked;
return data;
});
}
};
}
function deselectAll() {
$formData = deepMap($formData, (v) => {
if (typeof v === 'boolean') {
return false;
}
return v;
});
}
function selectAll() {
$formData = deepMap($formData, (v) => {
if (typeof v === 'boolean') {
return true;
}
return v;
});
}
let report: any;
onMount(async () => {
switch ($provider.provider) {
case 'appwrite': {
const res = await sdk.forProject.migrations.generateAppwriteReport(
providerResources.appwrite,
$provider.endpoint,
$provider.projectID,
$provider.apiKey
);
report = res;
break;
}
case 'supabase': {
const res = await sdk.forProject.migrations.generateSupabaseReport(
providerResources.supabase,
$provider.endpoint,
$provider.apiKey,
$provider.host,
$provider.username,
$provider.password,
$provider.port
);
report = res;
break;
}
case 'nhost': {
const res = await sdk.forProject.migrations.generateNHostReport(
providerResources.nhost,
$provider.subdomain,
$provider.region,
$provider.adminSecret,
$provider.database,
$provider.username,
$provider.password
);
report = res;
}
}
});
$: resources = providerResources[$provider.provider];
</script>
<WizardStep nextDisabled={!report}>
<WizardStep>
<svelte:fragment slot="title">Select data</svelte:fragment>
<div class="box" style:border-radius="0.5rem">
<div class="u-flex u-flex-vertical u-gap-16">
<EyebrowHeading class="eyebrow" tag="h3" size={3}>Good to know</EyebrowHeading>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-cog" />
</div>
<div>
<p class="u-bold">Project settings are not imported</p>
<p>You will need to set service and project settings manually</p>
</div>
</div>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-trending-up" />
</div>
<div>
<p class="u-bold">Keep your project plan limits in mind</p>
<p>
Make sure to have enough storage in your project plan when importing files
</p>
</div>
</div>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-currency-dollar" />
</div>
<div>
<p class="u-bold">Transfer is free of charge</p>
<p>You won't be charged for Appwrite bandwidth usage for importing data</p>
</div>
</div>
</div>
</div>
<ul class="buttons-list u-margin-block-start-32 u-main-end">
<li class="buttons-list-item">
<Button text on:click={deselectAll}>Deselect all</Button>
</li>
<li class="buttons-list-item">
<Button text on:click={selectAll}>Select all</Button>
</li>
</ul>
<ul class="u-flex u-flex-vertical u-gap-32 u-margin-block-start-16">
{#if resources.includes('user')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.users.root}
on:change={handleInputChange('users.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Users</span>
<span class="inline-tag">{report?.user ?? '...'}</span>
</div>
<div />
<span>Import all users</span>
{#if resources.includes('team')}
<ul>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.users.teams}
on:change={handleInputChange('users.teams')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include teams</span>
<span class="inline-tag">{report?.team ?? '...'}</span>
</div>
<div />
<span>Import all teams and the team memberships of your users</span>
</li>
</ul>
{/if}
</li>
{/if}
{#if resources.includes('database')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.databases.root}
on:change={handleInputChange('databases.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Databases</span>
<span class="inline-tag">{report?.database ?? '...'}</span>
</div>
<div />
<span>Import all databases, including collections, indexes and attributes</span>
{#if resources.includes('document')}
<ul>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.databases.documents}
on:change={handleInputChange('databases.documents')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include documents</span>
<span class="inline-tag">{report?.document ?? '...'}</span>
</div>
<div />
<span>Import all of your documents</span>
</li>
</ul>
{/if}
</li>
{/if}
{#if resources.includes('function')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.root}
on:change={handleInputChange('functions.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Functions</span>
<span class="inline-tag">{report?.function ?? '...'}</span>
</div>
<div />
<span>Import all functions and their active deployment</span>
<ul>
{#if resources.includes('envVar')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.env}
on:change={handleInputChange('functions.env')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include environment variables</span>
</div>
<div />
<span>Import all environment variables</span>
</li>
{/if}
{#if resources.includes('deployment')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.inactive}
on:change={handleInputChange('functions.inactive')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include inactive deployments</span>
</div>
<div />
<span>Import all deployments that are not currently active</span>
</li>
{/if}
</ul>
</li>
{/if}
{#if resources.includes('bucket') && resources.includes('file')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.storage.root}
on:change={handleInputChange('storage.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Storage</span>
<span class="inline-tag">
{report?.size ? `${report.size.toFixed(2)}MB` : '...'}
</span>
</div>
<div />
<span>
Import all buckets <span class="inline-tag">{report?.bucket ?? '...'}</span> and
files
<span class="inline-tag">{report?.file ?? '...'}</span>
</span>
</li>
{/if}
</ul>
<ResourceForm {formData} {provider} />
</WizardStep>
<style lang="scss">
.box :global(.eyebrow) {
font-weight: 500;
color: hsl(var(--color-neutral-70));
}
.circled {
width: 1.5rem;
height: 1.5rem;
flex-shrink: 0;
border-radius: 100%;
border: 1px solid hsl(var(--color-border));
position: relative;
i {
position: absolute;
left: 50%;
top: 50%;
translate: -50% -50%;
font-size: 1rem;
}
}
.buttons-list {
border-block-end: 1px solid hsl(var(--color-border));
padding-block-end: 0.625rem;
}
.checkbox-field {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.25rem 1rem;
align-items: center;
ul {
grid-column-start: 2;
padding-block-end: 2rem;
li {
margin-block-start: 1rem;
}
}
}
</style>
@@ -21,7 +21,7 @@
import { openImportWizard } from './(import)';
import Details from './details.svelte';
import ExportModal from './exportModal.svelte';
import { registerCommands } from '$lib/commandCenter';
import { registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
export let data;
let details: (typeof data.migrations)[number] | null = null;
@@ -48,17 +48,24 @@
$: $registerCommands([
{
label: 'Import data',
icon: 'upload',
icon: 'download',
keys: ['i', 'd'],
callback: openImportWizard
callback: openImportWizard,
group: 'migrations'
},
{
label: 'Export data',
icon: 'download',
icon: 'upload',
keys: ['e', 'd'],
callback: () => (showExport = true)
callback: () => (showExport = true),
group: 'migrations'
}
]);
$: $updateCommandGroupRanks((prev) => ({
...prev,
migrations: 100
}));
</script>
<Container>
@@ -2,6 +2,8 @@
import { Alert, Modal } from '$lib/components';
import { Button, InputText, InputTextarea } from '$lib/elements/forms';
import { getFormData } from '$lib/helpers/form';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
export let show = false;
let submitted = false;
@@ -39,7 +41,7 @@
return endpoint;
};
const onSubmit = (e: SubmitEvent) => {
const onSubmit = async (e: SubmitEvent) => {
e.preventDefault();
submitted = true;
@@ -56,10 +58,39 @@
}
const currEndpoint = getCurrentEndpoint();
// URI encode the current endpoint, so that it can be passed as a query string
const encodedCurrEndpoint = encodeURIComponent(currEndpoint);
// Create API key
const { secret } = await sdk.forConsole.projects.createKey(
$project.$id,
`[AUTO-GENERATED] Migration ${new Date().toISOString()}`,
const dest = `${removeTrailingSlash(endpoint)}/?migrate=${encodedCurrEndpoint}`;
[
'users.read',
'teams.read',
'databases.read',
'collections.read',
'attributes.read',
'indexes.read',
'documents.read',
'files.read',
'buckets.read',
'functions.read',
'execution.read',
'locale.read',
'avatars.read',
'health.read'
],
undefined
);
const migrationData = {
endpoint: currEndpoint,
projectId: $project.$id,
apiKey: secret
};
const dest = `${removeTrailingSlash(endpoint)}/?migrate=${encodeURIComponent(
JSON.stringify(migrationData)
)}`;
window.location.href = dest;
};
</script>
+1 -1
View File
@@ -7,4 +7,4 @@ export const loading = writable(true);
export const organizations = derived(page, ($page) => {
return $page.data.organizations as Models.TeamList<Models.Preferences>;
});
export const requestedMigration = writable<string | null>(null);
export const requestedMigration = writable<Record<string, any> | null>(null);