mirror of
https://github.com/appwrite/console.git
synced 2026-04-07 19:17:46 +00:00
feat: basic export data modal functionality
This commit is contained in:
@@ -13,3 +13,5 @@ node_modules
|
||||
node_modules/
|
||||
dist/
|
||||
.vercel
|
||||
|
||||
*.swp
|
||||
@@ -80,7 +80,7 @@
|
||||
<div class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
|
||||
<span>{percentage}%</span>
|
||||
</div>
|
||||
<div class="progress-bar-container" style="--graph-size:50%" />
|
||||
<div class="progress-bar-container" style="--graph-size:{percentage}%" />
|
||||
<span>Importing users...</span>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
export let error: string = null;
|
||||
export let closable = true;
|
||||
export let headerDivider = true;
|
||||
export let onSubmit: () => Promise<void> | void = function () {
|
||||
export let onSubmit: (e: SubmitEvent) => Promise<void> | void = function () {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
export let noMargin = false;
|
||||
export let noStyle = false;
|
||||
export let isModal = false;
|
||||
export let onSubmit: () => Promise<void> | void;
|
||||
export let onSubmit: (e: SubmitEvent) => Promise<void> | void;
|
||||
|
||||
const { isSubmitting } = setContext<FormContext>('form', {
|
||||
isSubmitting: writable(false)
|
||||
});
|
||||
|
||||
async function submit() {
|
||||
async function submit(e: SubmitEvent) {
|
||||
isSubmitting.set(true);
|
||||
await onSubmit();
|
||||
await onSubmit(e);
|
||||
isSubmitting.set(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let id: string;
|
||||
export let name: string = id;
|
||||
export let value = '';
|
||||
export let placeholder = '';
|
||||
export let required = false;
|
||||
@@ -29,6 +30,7 @@
|
||||
});
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
console.log('Invalid');
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.valueMissing) {
|
||||
@@ -39,8 +41,11 @@
|
||||
error = element.validationMessage;
|
||||
};
|
||||
|
||||
$: if (value) {
|
||||
error = null;
|
||||
$: {
|
||||
value;
|
||||
if (element?.validity?.valid) {
|
||||
error = null;
|
||||
}
|
||||
}
|
||||
|
||||
let prevValue = '';
|
||||
@@ -56,6 +61,10 @@
|
||||
|
||||
$: showTextCounter = !!maxlength;
|
||||
$: showNullCheckbox = nullable && !required;
|
||||
|
||||
type $$Events = {
|
||||
input: Event & { target: HTMLInputElement };
|
||||
};
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
@@ -66,6 +75,7 @@
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
{name}
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{readonly}
|
||||
@@ -77,7 +87,8 @@
|
||||
bind:value
|
||||
class:u-padding-inline-end-56={typeof maxlength === 'number'}
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
on:invalid={handleInvalid}
|
||||
on:input />
|
||||
{#if showTextCounter || showNullCheckbox}
|
||||
<ul
|
||||
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
export let id: string;
|
||||
export let name: string = id;
|
||||
export let value = '';
|
||||
export let placeholder = '';
|
||||
export let required = false;
|
||||
@@ -63,6 +64,7 @@
|
||||
<div class="input-text-wrapper">
|
||||
<textarea
|
||||
{id}
|
||||
{name}
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{readonly}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export function getFormData<T extends Record<string, unknown> = Record<string, unknown>>(
|
||||
e: SubmitEvent
|
||||
): T {
|
||||
const form = e.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
const data = {};
|
||||
|
||||
formData.forEach((value, key) => {
|
||||
data[key] = value;
|
||||
});
|
||||
|
||||
// TODO: Add validation? Maybe with zod
|
||||
return data as T;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export const load: LayoutLoad = async ({ depends, url }) => {
|
||||
depends(Dependencies.ACCOUNT);
|
||||
|
||||
if (url.searchParams.has('migrate')) {
|
||||
requestedMigration.set(true);
|
||||
requestedMigration.set(url.searchParams.get('migrate'));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Arrow, CardGrid, Heading } from '$lib/components';
|
||||
import Alert from '$lib/components/alert.svelte';
|
||||
import Modal from '$lib/components/modal.svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import InputText from '$lib/elements/forms/inputText.svelte';
|
||||
import InputTextarea from '$lib/elements/forms/inputTextarea.svelte';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -14,20 +12,20 @@
|
||||
} from '$lib/elements/table';
|
||||
import Table from '$lib/elements/table/table.svelte';
|
||||
import { isSameDay, toLocaleDate } from '$lib/helpers/date';
|
||||
import { parseIfString } from '$lib/helpers/object';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import { Container } from '$lib/layout';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { isSelfHosted } from '$lib/system';
|
||||
import { onMount } from 'svelte';
|
||||
import { openImportWizard } from './(import)';
|
||||
import Details from './details.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { parseIfString } from '$lib/helpers/object';
|
||||
import ExportModal from './exportModal.svelte';
|
||||
import { registerCommands } from '$lib/commandCenter';
|
||||
|
||||
export let data;
|
||||
let details: (typeof data.migrations)[number] | null = null;
|
||||
let showCloudExport = false;
|
||||
let showExport = false;
|
||||
|
||||
const getStatus = (status: string) => {
|
||||
if (status === 'failed') {
|
||||
@@ -46,6 +44,21 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$: $registerCommands([
|
||||
{
|
||||
label: 'Import data',
|
||||
icon: 'upload',
|
||||
keys: ['i', 'd'],
|
||||
callback: openImportWizard
|
||||
},
|
||||
{
|
||||
label: 'Export data',
|
||||
icon: 'download',
|
||||
keys: ['e', 'd'],
|
||||
callback: () => (showExport = true)
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -173,52 +186,14 @@
|
||||
<Button
|
||||
class="u-margin-block-start-48"
|
||||
secondary
|
||||
on:click={() => (showCloudExport = true)}>Export data</Button>
|
||||
on:click={() => (showExport = true)}>Export data</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<Modal bind:show={showCloudExport}>
|
||||
<svelte:fragment slot="header">Export to self-hosted instance</svelte:fragment>
|
||||
<div class="modal-contents">
|
||||
<Alert standalone>
|
||||
<svelte:fragment slot="title">API key creation</svelte:fragment>
|
||||
By initiating the transfer, an API key will be automatically generated in the background,
|
||||
which you can delete after completion
|
||||
</Alert>
|
||||
|
||||
<div class="u-margin-block-start-24">
|
||||
<InputText
|
||||
label="Endpoint self-hosted instance"
|
||||
required
|
||||
id="endpoint"
|
||||
placeholder="https://[YOUR_APPWRITE_HOSTNAME]" />
|
||||
</div>
|
||||
|
||||
<div class="box u-margin-block-start-24">
|
||||
<p class="u-bold">
|
||||
Share your feedback: why our self-hosted solution works better for you
|
||||
</p>
|
||||
<p class="u-margin-block-start-8">
|
||||
We appreciate your continued support and we understand that our self-hosted solution
|
||||
might better fit your needs. To help us improve our Cloud solution, please share why
|
||||
it works better for you. Your feedback is important to us and we'll use it to make
|
||||
our services better.
|
||||
</p>
|
||||
<div class="u-margin-block-start-24">
|
||||
<InputTextarea id="feedback" label="Your feedback" placeholder="Type here..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="u-flex u-gap-16 u-cross-center" slot="footer">
|
||||
<span> You will be redirected to your self-hosted instance </span>
|
||||
|
||||
<Button on:click={() => (showCloudExport = false)}>Continue TODO: SUBMIT</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<ExportModal bind:show={showExport} />
|
||||
|
||||
<Details bind:details />
|
||||
|
||||
@@ -228,15 +203,10 @@
|
||||
place-items: center;
|
||||
|
||||
border: 1px dashed hsl(var(--color-border));
|
||||
/* border-color: red; */
|
||||
border-radius: 1rem;
|
||||
|
||||
height: 100%;
|
||||
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-contents :global(.alert) {
|
||||
grid-area: initial !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,9 +4,15 @@ import { sdk } from '$lib/stores/sdk';
|
||||
export async function load({ depends }) {
|
||||
depends(Dependencies.MIGRATIONS);
|
||||
|
||||
const { migrations } = await sdk.forProject.migrations.list();
|
||||
try {
|
||||
const { migrations } = await sdk.forProject.migrations.list();
|
||||
|
||||
return {
|
||||
migrations
|
||||
};
|
||||
return {
|
||||
migrations
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
migrations: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import { Alert, Modal } from '$lib/components';
|
||||
import { Button, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
import { getFormData } from '$lib/helpers/form';
|
||||
|
||||
export let show = false;
|
||||
let submitted = false;
|
||||
|
||||
const isValidEndpoint = (endpoint: string) => {
|
||||
try {
|
||||
// Endpoint should be a valid URL. It may not have a path, nor a query string
|
||||
const url = new URL(endpoint);
|
||||
|
||||
return (
|
||||
url.protocol &&
|
||||
url.hostname &&
|
||||
(url.pathname === '/' || !url.pathname) &&
|
||||
!url.search
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentEndpoint = () => {
|
||||
// Remove subpaths and query strings from the current URL. Add a /v1 suffix
|
||||
const url = new URL(window.location.href);
|
||||
url.pathname = '';
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
url.pathname = '/v1';
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
const removeTrailingSlash = (endpoint: string) => {
|
||||
if (endpoint.endsWith('/')) {
|
||||
return endpoint.slice(0, -1);
|
||||
}
|
||||
return endpoint;
|
||||
};
|
||||
|
||||
const onSubmit = (e: SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
submitted = true;
|
||||
|
||||
const formData = getFormData<{ endpoint: string; feedback: string }>(e);
|
||||
|
||||
// TODO: Send feedback somewhere
|
||||
const { endpoint, feedback: _feedback } = formData;
|
||||
|
||||
if (!isValidEndpoint(endpoint)) {
|
||||
const endpointInput = document.getElementById('endpoint') as HTMLInputElement;
|
||||
endpointInput.setCustomValidity('Please enter a valid endpoint');
|
||||
endpointInput.reportValidity();
|
||||
return;
|
||||
}
|
||||
|
||||
const currEndpoint = getCurrentEndpoint();
|
||||
// URI encode the current endpoint, so that it can be passed as a query string
|
||||
const encodedCurrEndpoint = encodeURIComponent(currEndpoint);
|
||||
|
||||
const dest = `${removeTrailingSlash(endpoint)}/?migrate=${encodedCurrEndpoint}`;
|
||||
window.location.href = dest;
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal bind:show {onSubmit}>
|
||||
<svelte:fragment slot="header">Export to self-hosted instance</svelte:fragment>
|
||||
<div class="modal-contents">
|
||||
<Alert standalone>
|
||||
<svelte:fragment slot="title">API key creation</svelte:fragment>
|
||||
By initiating the transfer, an API key will be automatically generated in the background,
|
||||
which you can delete after completion
|
||||
</Alert>
|
||||
|
||||
<div class="u-margin-block-start-24">
|
||||
<InputText
|
||||
label="Endpoint self-hosted instance"
|
||||
required
|
||||
id="endpoint"
|
||||
placeholder="https://[YOUR_APPWRITE_HOSTNAME]"
|
||||
autofocus
|
||||
value="http://localhost:3000/"
|
||||
on:input={(e) => {
|
||||
if (!submitted) return;
|
||||
const input = e.target;
|
||||
const value = input.value;
|
||||
|
||||
if (!isValidEndpoint(value)) {
|
||||
input.setCustomValidity('Please enter a valid endpoint');
|
||||
} else {
|
||||
input.setCustomValidity('');
|
||||
}
|
||||
input.reportValidity();
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<div class="box u-margin-block-start-24">
|
||||
<p class="u-bold">
|
||||
Share your feedback: why our self-hosted solution works better for you
|
||||
</p>
|
||||
<p class="u-margin-block-start-8">
|
||||
We appreciate your continued support and we understand that our self-hosted solution
|
||||
might better fit your needs. To help us improve our Cloud solution, please share why
|
||||
it works better for you. Your feedback is important to us and we'll use it to make
|
||||
our services better.
|
||||
</p>
|
||||
<div class="u-margin-block-start-24">
|
||||
<InputTextarea id="feedback" label="Your feedback" placeholder="Type here..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="u-flex u-gap-16 u-cross-center" slot="footer">
|
||||
<span> You will be redirected to your self-hosted instance </span>
|
||||
|
||||
<Button submit>Continue</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
+1
-1
@@ -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(false);
|
||||
export const requestedMigration = writable<string | null>(null);
|
||||
|
||||
Reference in New Issue
Block a user