migrations stuff

This commit is contained in:
tglide
2023-07-13 15:59:29 +01:00
parent cb20d7a028
commit 11dd905231
13 changed files with 344 additions and 176 deletions
+1 -1
View File
@@ -86,7 +86,7 @@
const handleKeydown = (e) => {
if (!$subPanels.length) {
keys = [...keys, e.key].slice(-5);
keys = [...keys, e.key].slice(-10);
resetKeys();
}
$commandCenterKeyDownHandler(e);
+65 -6
View File
@@ -1,8 +1,68 @@
<script lang="ts">
let show = true;
<script lang="ts" context="module">
import { last } from '$lib/helpers/array';
import { debounce } from '$lib/helpers/debounce';
import { parseIfString } from '$lib/helpers/object';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
export const showMigrationBox = writable(true);
</script>
{#if show}
<script lang="ts">
let migration: Models.Migration;
type Counter = {
pending: number;
error: number;
success: number;
processing: number;
skip: number;
warning: number;
};
type TotalCounter = {
done: number;
processing: number;
};
$: percentage = (function getPercentage() {
if (!migration) return 0;
const statusCounters = parseIfString(migration.statusCounters) as Record<string, Counter>;
const totalCounter: TotalCounter = Object.values(statusCounters).reduce(
(curr, acc) => {
return {
done: curr.done + acc.success + acc.error + acc.skip + acc.warning,
processing: curr.processing + acc.processing + acc.pending
};
},
{ done: 0, processing: 0 } as TotalCounter
);
console.log({ statusCounters, totalCounter });
return Math.round(
(totalCounter.done / (totalCounter.done + totalCounter.processing)) * 100
);
})();
const fetchMigrations = debounce(async () => {
const { migrations } = await sdk.forProject.migrations.list();
migration = last(migrations);
}, 1000);
fetchMigrations();
onMount(async () => {
return sdk.forConsole.client.subscribe(['project', 'console'], async (response) => {
if (response.events.includes('migrations.*')) {
fetchMigrations();
}
});
});
</script>
{#if $showMigrationBox && migration}
<section class="upload-box is-float">
<header class="upload-box-header">
<h4 class="upload-box-title">
@@ -11,15 +71,14 @@
<button
class="upload-box-button"
aria-label="close migration box"
on:click={() => (show = false)}>
on:click={() => ($showMigrationBox = false)}>
<span class="icon-x" aria-hidden="true" />
</button>
</header>
<div class="upload-box-content is-open">
<section class="progress-bar">
<div class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
<span>60%</span>
<span>2 mins left</span>
<span>{percentage}%</span>
</div>
<div class="progress-bar-container" style="--graph-size:50%" />
<span>Importing users...</span>
+7
View File
@@ -76,3 +76,10 @@ export function last<T>(array: readonly T[]): T;
export function last<T>(array: readonly T[]): T | undefined {
return at(array, -1);
}
export function excludeArray<T, E extends T>(
array: readonly T[],
exclude: readonly E[]
): Exclude<T, E>[] {
return array.filter((item) => !exclude.includes(item as E)) as Exclude<T, E>[];
}
+5 -8
View File
@@ -135,22 +135,19 @@
Skip optional steps
</Button>
{/if}
{#if currentStep === 1}
<Button secondary on:click={handleExit}>Cancel</Button>
<Button submit>Next</Button>
{:else if isLastStep}
<Button
secondary
on:click={() => currentStep--}
on:click={() => trackEvent('wizard_back')}>Back</Button>
<Button submit>{finalAction}</Button>
{:else}
<Button
secondary
on:click={() => currentStep--}
on:click={() => trackEvent('wizard_back')}>Back</Button>
<Button submit>Next</Button>
{/if}
<Button submit disabled={$wizard.nextDisabled}>
{isLastStep ? finalAction : 'Next'}
</Button>
</div>
</div>
</Form>
+8
View File
@@ -1,11 +1,19 @@
<script lang="ts">
import { Heading } from '$lib/components';
import { wizard } from '$lib/stores/wizard';
import { onDestroy } from 'svelte';
/**
* Pass callback that is run before the Form submit, can be canceled with throwing an exception.
*/
export let beforeSubmit: () => Promise<void> = null;
export let nextDisabled = false;
$: wizard.setNextDisabled(nextDisabled);
onDestroy(() => {
wizard.setNextDisabled(false);
});
if (beforeSubmit) {
wizard.setInterceptor(beforeSubmit);
+36 -14
View File
@@ -1,5 +1,8 @@
import { excludeArray } from '$lib/helpers/array';
import { writable } from 'svelte/store';
export type Provider = 'appwrite' | 'nhost' | 'supabase' | 'firebase';
const initialFormData = {
users: {
root: false,
@@ -32,20 +35,39 @@ export const createMigrationFormStore = () => {
};
};
type Resource =
| 'user'
| 'team'
| 'membership'
| 'file'
| 'bucket'
| 'function'
| 'envVar'
| 'deployment'
| 'database'
| 'collection'
| 'index'
| 'attribute'
| 'document';
const resources = [
'user',
'team',
'membership',
'file',
'bucket',
'function',
'envVar',
'deployment',
'database',
'collection',
'index',
'attribute',
'document'
] as const;
type Resource = (typeof resources)[number];
export const providerResources: Record<Provider, Resource[]> = {
appwrite: [...resources],
supabase: [
'user',
'database',
'collection',
'attribute',
'index',
'document',
'bucket',
'file'
],
nhost: ['user', 'database', 'collection', 'attribute', 'index', 'document', 'bucket', 'file'],
firebase: []
};
export const migrationFormToResources = (formData: MigrationFormData): Resource[] => {
const resources: Resource[] = [];
+9 -2
View File
@@ -7,6 +7,7 @@ export type WizardStore = {
media?: string;
component?: typeof SvelteComponent;
interceptor?: () => Promise<void>;
nextDisabled: boolean;
};
function createWizardStore() {
@@ -14,7 +15,8 @@ function createWizardStore() {
show: false,
component: null,
interceptor: null,
media: null
media: null,
nextDisabled: false
});
return {
@@ -31,7 +33,12 @@ function createWizardStore() {
setInterceptor: (callback: WizardStore['interceptor']) => {
update((n) => {
n.interceptor = callback;
return n;
});
},
setNextDisabled: (disabled: boolean) => {
update((n) => {
n.nextDisabled = disabled;
return n;
});
},
@@ -34,7 +34,7 @@
},
disabled: $page.url.pathname.endsWith('settings'),
group: 'navigation',
rank: 30
rank: 40
},
{
label: 'Go to Custom domains',
@@ -45,7 +45,7 @@
},
disabled: $page.url.pathname.includes('domains'),
group: 'navigation',
rank: 20
rank: 30
},
{
label: 'Go to Webhooks',
@@ -56,6 +56,17 @@
disabled: $page.url.pathname.includes('webhooks'),
group: 'navigation',
rank: 20
},
{
label: 'Go to Migrations',
keys: ['g', 'm'],
callback: () => {
goto(`/console/project-${$project.$id}/settings/migrations`);
},
disabled: $page.url.pathname.includes('migrations'),
group: 'navigation',
rank: 10
}
]);
@@ -1,15 +1,25 @@
import {
PUBLIC_MOCK_APIKEY,
PUBLIC_MOCK_ENDPOINT,
PUBLIC_MOCK_PROJECTID
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 { wizard } from '$lib/stores/wizard';
import { writable } from 'svelte/store';
import Wizard from './wizard.svelte';
export type Provider = 'appwrite' | 'nhost' | 'supabase' | 'firebase';
type AppwriteInput = {
provider: 'appwrite';
endpoint?: string;
@@ -25,11 +35,11 @@ type FirebaseInput = {
type SupabaseInput = {
provider: 'supabase';
host?: string;
port?: number;
username?: string;
password?: string;
endpoint?: string;
apiKey?: string;
port?: number;
};
type NhostInput = {
@@ -40,18 +50,36 @@ type NhostInput = {
username?: string;
password?: string;
adminSecret?: string;
port?: number;
};
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: 'appwrite',
endpoint: PUBLIC_MOCK_ENDPOINT,
apiKey: PUBLIC_MOCK_APIKEY,
projectID: PUBLIC_MOCK_PROJECTID
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 formData = createMigrationFormStore();
@@ -2,7 +2,8 @@
import SvgIcon from '$lib/components/svgIcon.svelte';
import { Button, FormList, InputNumber, InputText } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import { provider, type Provider } from '.';
import type { Provider } from '$lib/stores/migration';
import { provider } from '.';
const providers: Record<Provider, string> = {
appwrite: 'Appwrite',
@@ -144,12 +145,6 @@
required
placeholder="Enter database"
bind:value={$provider.database} />
<InputNumber
id="port"
label="Port"
required
placeholder="Enter port"
bind:value={$provider.port} />
<InputText
id="username"
label="Username"
@@ -2,14 +2,14 @@
import { EyebrowHeading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { deepMap } from '$lib/helpers/object';
import { sdk } from '$lib/stores/sdk';
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 { formData, provider } from '.';
import { migrationFormToResources } from '$lib/stores/migration';
type FormData = WritableValue<typeof formData>;
type FormKeys = DeepKeys<FormData>;
@@ -66,23 +66,49 @@
let report: any;
onMount(async () => {
const resources = migrationFormToResources($formData);
switch ($provider.provider) {
case 'appwrite': {
const res = await sdk.forProject.migrations.generateAppwriteReport(
resources,
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>
<WizardStep nextDisabled={!report}>
<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">
@@ -113,7 +139,7 @@
</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>
<p>You won't be charged for Appwrite bandwidth usage for importing data</p>
</div>
</div>
</div>
@@ -130,124 +156,134 @@
</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>
{#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>
<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>
<span class="inline-tag">{report?.user ?? '...'}</span>
</div>
<div />
<span>Import all users</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 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}
</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>
</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}
</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>
</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>
</WizardStep>
@@ -11,6 +11,7 @@
import { Dependencies } from '$lib/constants';
import { migrationFormToResources } from '$lib/stores/migration';
import { started } from '../stores';
import { showMigrationBox } from '$lib/components/migrationBox.svelte';
const onExit = () => {
resetImportStores();
@@ -61,8 +62,7 @@
$provider.adminSecret,
$provider.database,
$provider.username,
$provider.password,
$provider.port
$provider.password
);
console.log('nhost', res);
invalidate(Dependencies.MIGRATIONS);
@@ -72,6 +72,8 @@
resetImportStores();
wizard.hide();
started.set(performance.now());
showMigrationBox.set(true);
};
onDestroy(onExit);
@@ -1,15 +1,11 @@
import { Dependencies } from '$lib/constants.js';
import { sdk } from '$lib/stores/sdk';
import { get } from 'svelte/store';
import { started } from './stores.js';
export async function load({ depends }) {
depends(Dependencies.MIGRATIONS);
const { migrations } = await sdk.forProject.migrations.list();
console.log(migrations, (performance.now() - get(started)) / 1000);
return {
migrations
};