Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,6 @@
},
"overrides": {
"vite": "npm:rolldown-vite@latest",
"minimatch": "10.2.1"
"minimatch": "10.2.3"
}
}
2 changes: 2 additions & 0 deletions src/lib/actions/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export enum Click {
DatabaseDatabaseDelete = 'click_database_delete',
DatabaseImportCsv = 'click_database_import_csv',
DatabaseExportCsv = 'click_database_export_csv',
DatabaseExportJson = 'click_database_export_json',
Comment thread
Divyansh2992 marked this conversation as resolved.
Comment thread
Divyansh2992 marked this conversation as resolved.
DomainCreateClick = 'click_domain_create',
DomainDeleteClick = 'click_domain_delete',
DomainRetryDomainVerificationClick = 'click_domain_retry_domain_verification',
Expand Down Expand Up @@ -283,6 +284,7 @@ export enum Submit {
DatabaseUpdateName = 'submit_database_update_name',
DatabaseImportCsv = 'submit_database_import_csv',
DatabaseExportCsv = 'submit_database_export_csv',
DatabaseExportJson = 'submit_database_export_json',
DatabaseBackupDelete = 'submit_database_backup_delete',
DatabaseBackupPolicyCreate = 'submit_database_backup_policy_create',

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@
<Icon icon={IconDownload} size="s" />
</Button>

<svelte:fragment slot="tooltip">Export CSV</svelte:fragment>
<svelte:fragment slot="tooltip">Export</svelte:fragment>
</Tooltip>

<Tooltip disabled={isRefreshing || !data.rows?.total} placement="top">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
import { toLocalDateTimeISO } from '$lib/helpers/date';
import { writable } from 'svelte/store';
import { isSmallViewport } from '$lib/stores/viewport';
import { Query } from '@appwrite.io/console';

let showExitModal = $state(false);
let formComponent: Form;
let isSubmitting = $state(writable(false));
let abortController: AbortController | null = null;
let exportProgress = 0;

let localQueries = $state<Map<TagValue, string>>(new Map());
const localTags = $derived(Array.from(localQueries.keys()));
Expand All @@ -29,7 +32,9 @@
.split('T')
.join('_')
.slice(0, -4);
const filename = `${$table.name}_${timestamp}.csv`;

let exportFormat = $state<'csv' | 'json'>('csv');
let filename = $derived(`${$table.name}_${timestamp}.${exportFormat}`);
Comment thread
Divyansh2992 marked this conversation as resolved.

let selectedColumns = $state<Record<string, boolean>>({});
let showAllColumns = $state(false);
Expand Down Expand Up @@ -97,34 +102,148 @@
return;
}

try {
await sdk
.forProject(page.params.region, page.params.project)
.migrations.createCSVExport({
resourceId: `${page.params.database}:${page.params.table}`,
filename: filename,
columns: selectedCols,
queries: exportWithFilters ? Array.from(localQueries.values()) : [],
delimiter: delimiterMap[delimiter],
header: includeHeader,
notify: true
if (exportFormat === 'csv') {
try {
await sdk
.forProject(page.params.region, page.params.project)
.migrations.createCSVExport({
resourceId: `${page.params.database}:${page.params.table}`,
filename: filename,
columns: selectedCols,
queries: exportWithFilters ? Array.from(localQueries.values()) : [],
delimiter: delimiterMap[delimiter],
header: includeHeader,
notify: true
});

addNotification({
type: 'success',
message: 'CSV export has started'
});

addNotification({
type: 'success',
message: 'CSV export has started'
});
trackEvent(Submit.DatabaseExportCsv);
await goto(tableUrl);
} catch (error) {
addNotification({
type: 'error',
message: error.message
Comment thread
Divyansh2992 marked this conversation as resolved.
Outdated
});

trackEvent(Submit.DatabaseExportCsv);
trackError(error, Submit.DatabaseExportCsv);
}
} else {
trackEvent(Submit.DatabaseExportJson); // Track event at the start of JSON export
Comment thread
Divyansh2992 marked this conversation as resolved.
Outdated
$isSubmitting = true;
abortController = new AbortController(); // Initialize abort controller
exportProgress = 0; // Reset progress

await goto(tableUrl);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
try {
const activeQueries = exportWithFilters ? Array.from(localQueries.values()) : [];
const allRows: Record<string, unknown>[] = [];
Comment thread
Divyansh2992 marked this conversation as resolved.
const pageSize = 100;
let lastId: string | undefined = undefined;
let fetched = 0;
let total = Infinity;

// Add a warning for potentially large exports
addNotification({
type: 'info',
message: 'JSON export started. This may take a while for large datasets.',
timeout: 5000 // Keep it visible for a bit
});
Comment thread
Divyansh2992 marked this conversation as resolved.
Outdated

while (fetched < total) {
// Check for abort signal
if (abortController.signal.aborted) {
addNotification({
type: 'warning',
message: 'JSON export cancelled.'
});
break; // Exit the loop if aborted
}

const pageQueries = [Query.limit(pageSize), ...activeQueries];

if (lastId) {
pageQueries.push(Query.cursorAfter(lastId));
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

const response = await sdk
.forProject(page.params.region, page.params.project)
.tablesDB.listRows({
databaseId: page.params.database,
tableId: page.params.table,
queries: pageQueries,
signal: abortController.signal // Pass abort signal
Comment thread
Divyansh2992 marked this conversation as resolved.
Outdated
});

total = response.total;

if (response.rows.length === 0) break;
Comment thread
greptile-apps[bot] marked this conversation as resolved.

trackError(error, Submit.DatabaseExportCsv);
const filtered = response.rows.map((row) => {
const obj: Record<string, unknown> = {};
for (const col of selectedCols) {
obj[col] = row[col];
}
return obj;
});

allRows.push(...filtered);
fetched += response.rows.length;
lastId = response.rows[response.rows.length - 1].$id as string;
exportProgress = Math.min(100, Math.floor((fetched / total) * 100)); // Update progress
}

if (!abortController.signal.aborted) {
const json = JSON.stringify(allRows, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();

// Revoke the object URL after a short delay to ensure the browser has started the download
setTimeout(() => {
URL.revokeObjectURL(url);
document.body.removeChild(anchor);
}, 100);

addNotification({
type: 'success',
message: `JSON export complete — ${allRows.length} row${allRows.length !== 1 ? 's' : ''} downloaded`
});

await goto(tableUrl);
}
} catch (error) {
if (error.name === 'AbortError') {
addNotification({
type: 'warning',
message: 'JSON export cancelled by user.'
});
Comment thread
Divyansh2992 marked this conversation as resolved.
Outdated
} else {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.DatabaseExportJson);
}
} finally {
$isSubmitting = false;
exportProgress = 0; // Reset progress
abortController = null; // Clean up controller
}
}
}

// Cancel the JSON export operation
function cancelExport() {
if (abortController) {
abortController.abort();
addNotification({ type: 'info', message: 'JSON export cancellation requested.' });
}
Comment thread
Divyansh2992 marked this conversation as resolved.
}

Expand All @@ -134,8 +253,14 @@
});
</script>

<Wizard title="Export CSV" columnSize="s" href={tableUrl} bind:showExitModal confirmExit column>
<Wizard title="Export" columnSize="s" href={tableUrl} bind:showExitModal confirmExit column>
<Form bind:this={formComponent} bind:isSubmitting onSubmit={handleExport}>
{#if exportFormat === 'json' && $isSubmitting}
<div class="progress-container" style="margin-top:1rem;">
<div class="progress-bar" style="background:linear-gradient(to right, #4caf50 {exportProgress}%, #e0e0e0 0%); height:1rem; border-radius:0.25rem;" aria-valuenow={exportProgress} aria-valuemin="0" aria-valuemax="100"></div>
Comment thread
Divyansh2992 marked this conversation as resolved.
Outdated
<button type="button" class="cancel-btn" on:click={cancelExport} style="margin-left:0.5rem;">Cancel</button>
</div>
{/if}
<Layout.Stack gap="xxl">
<Fieldset legend="Columns">
<Layout.Stack gap="l">
Expand Down Expand Up @@ -172,30 +297,45 @@
<Fieldset legend="Export options">
<Layout.Stack gap="l">
<InputSelect
id="delimiter"
label="Delimiter"
bind:value={delimiter}
id="exportFormat"
label="Format"
bind:value={exportFormat}
options={[
{ value: 'Comma', label: 'Comma' },
{ value: 'Semicolon', label: 'Semicolon' },
{ value: 'Tab', label: 'Tab' },
{ value: 'Pipe', label: 'Pipe' }
]}>
<Layout.Stack direction="row" gap="none" alignItems="center" slot="info">
<Tooltip>
<Icon size="s" icon={IconInfo} />
<span slot="tooltip">
Define how to separate values in the exported file.
</span>
</Tooltip>
</Layout.Stack>
</InputSelect>

<InputCheckbox
id="includeHeader"
label="Include header row"
description="Column names will be added as the first row in the CSV"
bind:checked={includeHeader} />
{ value: 'csv', label: 'CSV' },
{ value: 'json', label: 'JSON' }
]} />

{#if exportFormat === 'csv'}
<InputSelect
id="delimiter"
label="Delimiter"
bind:value={delimiter}
options={[
{ value: 'Comma', label: 'Comma' },
{ value: 'Semicolon', label: 'Semicolon' },
{ value: 'Tab', label: 'Tab' },
{ value: 'Pipe', label: 'Pipe' }
]}>
<Layout.Stack
direction="row"
gap="none"
alignItems="center"
slot="info">
<Tooltip>
<Icon size="s" icon={IconInfo} />
<span slot="tooltip">
Define how to separate values in the exported file.
</span>
</Tooltip>
</Layout.Stack>
</InputSelect>

<InputCheckbox
id="includeHeader"
label="Include header row"
description="Column names will be added as the first row in the CSV"
bind:checked={includeHeader} />
{/if}

<Layout.Stack gap="m">
<div class:disabled-checkbox={localTags.length === 0}>
Expand Down