Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion src/components/gui/export/export-result-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import OptimizeTableState, {
} from "../table-optimized/optimize-table-state";

export type ExportTarget = "clipboard" | "file";
type ExportFormat = "csv" | "delimited" | "json" | "sql" | "xlsx";
export type ExportFormat = "csv" | "delimited" | "json" | "sql" | "xlsx";
export type ExportSelection =
| "complete"
| "selected_row"
Expand Down
56 changes: 56 additions & 0 deletions src/components/gui/schema-sidebar-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { OpenContextMenuList } from "@/core/channel-builtin";
import { scc } from "@/core/command";
import { DatabaseSchemaItem } from "@/drivers/base-driver";
import { triggerEditorExtensionTab } from "@/extensions/trigger-editor";
import { ExportFormat, exportTableData } from "@/lib/export-helper";
import { Table } from "@phosphor-icons/react";
import { LucideCog, LucideDatabase, LucideView } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
Expand Down Expand Up @@ -122,6 +123,31 @@ function flattenSchemaGroup(
return schemaGroup;
}

// Copy of export-result-button.tsx
async function downloadExportTable(
format: string,
handler: Promise<string | Blob>
) {
try {
if (!format) return;
const content = await handler;
if (!content) return;
// TODO: more mimeTypes support
const blob =
content instanceof Blob
? content
: new Blob([content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `export.${format === "delimited" ? "csv" : format}`;
a.click();
URL.revokeObjectURL(url);
} catch (error) {
console.error(`Failed to download exported ${format} file:`, error);
}
}

export default function SchemaList({ search }: Readonly<SchemaListProps>) {
const { databaseDriver, extensions } = useStudioContext();
const [selected, setSelected] = useState("");
Expand All @@ -136,10 +162,18 @@ export default function SchemaList({ search }: Readonly<SchemaListProps>) {
setSelected("");
}, [setSelected, search]);

const exportFormats = [
{ title: "Export as CSV", format: "csv" },
{ title: "Export as Excel", format: "xlsx" },
{ title: "Export as JSON", format: "json" },
{ title: "Export as SQL INSERT", format: "sql" },
];

const prepareContextMenu = useCallback(
(item?: DatabaseSchemaItem) => {
const selectedName = item?.name;
const isTable = item?.type === "table";
const schemaName = item?.schemaName ?? currentSchemaName;

const createMenuSection = {
title: "Create",
Expand Down Expand Up @@ -173,6 +207,26 @@ export default function SchemaList({ search }: Readonly<SchemaListProps>) {
].filter(Boolean)
: [];

const exportSection =
isTable && selectedName
? {
title: "Export Table",
sub: exportFormats.map(({ title, format }) => ({
title,
onClick: async () => {
const handler = exportTableData(
databaseDriver,
schemaName,
selectedName,
format as ExportFormat,
"file"
);
downloadExportTable(format, handler);
},
})),
}
: undefined;

return [
createMenuSection,
{
Expand All @@ -184,6 +238,8 @@ export default function SchemaList({ search }: Readonly<SchemaListProps>) {
},
{ separator: true },

// Export Section
exportSection,
// Modification Section
...modificationSection,
modificationSection.length > 0 ? { separator: true } : undefined,
Expand Down
5 changes: 3 additions & 2 deletions src/components/gui/tabs/table-data-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ export default function TableDataWindow({
onChange={(e) => setLimit(e.currentTarget.value)}
onBlur={(e) => {
try {
const finalValue = parseInt(e.currentTarget.value);
const finalValue = Math.max(0, parseInt(e.currentTarget.value));
if (finalValue !== finalLimit) {
setFinalLimit(finalValue);
}
Expand All @@ -375,10 +375,11 @@ export default function TableDataWindow({
onChange={(e) => setOffset(e.currentTarget.value)}
onBlur={(e) => {
try {
const finalValue = parseInt(e.currentTarget.value);
const finalValue = Math.max(0, parseInt(e.currentTarget.value));
if (finalValue !== finalOffset) {
setFinalOffset(finalValue);
}
setOffset(finalValue.toString());
} catch (e) {
setOffset(finalOffset.toString());
}
Expand Down
47 changes: 47 additions & 0 deletions src/lib/export-helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ExportFormat,
ExportOptions,
ExportSelection,
ExportTarget,
Expand Down Expand Up @@ -245,3 +246,49 @@ export function convertExcelStringToArray(data: string): string[][] {
const lines = data.split("\r\n");
return lines.map((line) => line.split("\t"));
}

export async function exportTableData(
databaseDriver: any,
schemaName: string,
tableName: string,
format: ExportFormat,
exportTarget: ExportTarget,
options?: ExportOptions
): Promise<string | Blob> {
console.log("Exporting", schemaName, tableName, format, exportTarget, options);
const result = await databaseDriver.query(
`SELECT * FROM ${databaseDriver.escapeId(schemaName)}.${databaseDriver.escapeId(tableName)}`
);
console.log("QueryResults", result);
if (!result.rows || result.rows.length === 0) {
return "";
}

const headers = Object.keys(result.rows[0]);
const records = result.rows.map((row: { [x: string]: string; }) => headers.map(header => row[header]));

const formatHandlers = {
csv: () => exportDataAsDelimitedText(headers, records, ",", "\n", '"', exportTarget),
json: () => exportRowsToJson(headers, records, exportTarget),
sql: () => exportRowsToSqlInsert(tableName, headers, records, exportTarget),
xlsx: () => exportToExcel(records, headers, tableName, exportTarget),
delimited: () =>
exportDataAsDelimitedText(
headers,
records,
options?.fieldSeparator || ",",
options?.lineTerminator || "\n",
options?.encloser || '"',
exportTarget
),
};

const handler = formatHandlers[format];
if (handler) {
return handler();
} else {
throw new Error(`Unsupported export format: ${format}`);
}
}
// TODO: maybe we should move export related types here
export type { ExportFormat };
Loading