Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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 @@ -12,14 +12,17 @@
import { table } from '../store';
import { queries, type TagValue } from '$lib/components/filters/store';
import { TagList } from '$lib/components/filters';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Click, Submit, trackEvent, trackError } from '$lib/actions/analytics';
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,139 @@
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
});

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) {
addNotification({
type: 'error',
message: error.message
Comment thread
Divyansh2992 marked this conversation as resolved.
Outdated
});
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();
}
Comment thread
Divyansh2992 marked this conversation as resolved.
}

Expand All @@ -134,8 +244,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" role="progressbar" aria-label="Export progress" aria-valuenow={exportProgress} aria-valuemin="0" aria-valuemax="100" style="background:linear-gradient(to right, #4caf50 {exportProgress}%, #e0e0e0 0%); height:1rem; border-radius:0.25rem;"></div>
<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 +288,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 Expand Up @@ -233,7 +364,12 @@
</Button>
<Button
fullWidthMobile
on:click={() => formComponent.triggerSubmit()}
on:click={() => {
trackEvent(
exportFormat === 'json' ? Click.DatabaseExportJson : Click.DatabaseExportCsv
);
formComponent.triggerSubmit();
}}
disabled={$isSubmitting || selectedColumnCount === 0}>
Export
</Button>
Expand Down