Skip to content
Open
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
31 changes: 24 additions & 7 deletions src/components/modals/ExportProgressModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { X, FileText, Loader2, CheckCircle, AlertCircle } from "lucide-react";
import {
X,
FileText,
Loader2,
CheckCircle,
AlertCircle,
AlertTriangle,
} from "lucide-react";
import { useTranslation } from "react-i18next";
import { Modal } from "../ui/Modal";

Expand All @@ -10,6 +17,7 @@ interface ExportProgressModalProps {
rowsProcessed: number;
fileName: string;
errorMessage?: string;
warningMessage?: string;
onCancel: () => void;
onClose: () => void;
}
Expand All @@ -22,6 +30,7 @@ export const ExportProgressModal = ({
onClose,
fileName,
errorMessage,
warningMessage,
}: ExportProgressModalProps) => {
const { t } = useTranslation();

Expand Down Expand Up @@ -62,12 +71,20 @@ export const ExportProgressModal = ({
{errorMessage}
</p>
) : (
<p className="text-secondary text-sm">
{t("editor.rowsProcessed")}:{" "}
<span className="text-white font-mono font-bold">
{rowsProcessed.toLocaleString()}
</span>
</p>
<>
<p className="text-secondary text-sm">
{t("editor.rowsProcessed")}:{" "}
<span className="text-white font-mono font-bold">
{rowsProcessed.toLocaleString()}
</span>
</p>
{warningMessage && (
<div className="mt-3 flex items-start gap-2 rounded border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-left text-xs text-amber-200">
<AlertTriangle size={14} className="mt-0.5 shrink-0" />
<span>{warningMessage}</span>
</div>
)}
</>
)}
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,7 @@
"executingQuery": "Abfrage wird ausgeführt...",
"exporting": "Wird exportiert...",
"rowsProcessed": "Verarbeitete Zeilen",
"exportLoadedRowsWarning": "Es wurden nur {{loaded}} geladene Zeilen von insgesamt {{total}} Zeilen exportiert. Blättere durch das Ergebnis oder führe eine eingeschränktere Abfrage aus, wenn du mehr Zeilen exportieren musst.",
"queryParameters": "Abfrageparameter",
"formatSql": "SQL formatieren",
"convertToConsole": "In Konsole umwandeln",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,7 @@
"executingQuery": "Executing query...",
"exporting": "Exporting...",
"rowsProcessed": "Rows Processed",
"exportLoadedRowsWarning": "Only {{loaded}} loaded rows were exported out of {{total}} total rows. Page through the result or run a narrower query before exporting if you need more rows.",
"queryParameters": "Query Parameters",
"formatSql": "Format SQL",
"convertToConsole": "Convert to Console",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,7 @@
"executingQuery": "Ejecutando consulta...",
"exporting": "Exportando...",
"rowsProcessed": "Filas Procesadas",
"exportLoadedRowsWarning": "Solo se exportaron {{loaded}} filas cargadas de {{total}} filas totales. Recorre las páginas del resultado o ejecuta una consulta más específica antes de exportar si necesitas más filas.",
"queryParameters": "Parámetros de Consulta",
"formatSql": "Formatear SQL",
"convertToConsole": "Convertir a Consola",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,7 @@
"executingQuery": "Exécution de la requête...",
"exporting": "Exportation...",
"rowsProcessed": "Lignes traitées",
"exportLoadedRowsWarning": "Seules {{loaded}} lignes chargées sur {{total}} lignes au total ont été exportées. Parcourez les pages du résultat ou exécutez une requête plus restrictive avant d'exporter si vous avez besoin de plus de lignes.",
"queryParameters": "Paramètres de requête",
"formatSql": "Formater SQL",
"convertToConsole": "Convertir en console",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,7 @@
"executingQuery": "Esecuzione query...",
"exporting": "Esportazione in corso...",
"rowsProcessed": "Righe processate",
"exportLoadedRowsWarning": "Sono state esportate solo {{loaded}} righe caricate su {{total}} righe totali. Scorri le pagine del risultato o esegui una query più mirata prima di esportare se ti servono più righe.",
"queryParameters": "Parametri Query",
"formatSql": "Formatta SQL",
"convertToConsole": "Converti in Console",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,7 @@
"executingQuery": "クエリを実行中...",
"exporting": "エクスポート中...",
"rowsProcessed": "処理行数",
"exportLoadedRowsWarning": "合計 {{total}} 行のうち、読み込み済みの {{loaded}} 行のみをエクスポートしました。さらに多くの行が必要な場合は、結果をページ送りするか、より絞り込んだクエリを実行してからエクスポートしてください。",
"queryParameters": "クエリパラメータ",
"formatSql": "SQLをフォーマット",
"convertToConsole": "コンソールに変換",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,7 @@
"executingQuery": "쿼리를 실행하는 중...",
"exporting": "내보내는 중...",
"rowsProcessed": "처리된 행",
"exportLoadedRowsWarning": "총 {{total}}개 행 중 로드된 {{loaded}}개 행만 내보냈습니다. 더 많은 행이 필요하면 결과 페이지를 넘기거나 더 좁은 쿼리를 실행한 뒤 내보내세요.",
"queryParameters": "쿼리 매개변수",
"formatSql": "SQL 포맷",
"convertToConsole": "콘솔로 변환",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,7 @@
"executingQuery": "Executando consulta...",
"exporting": "Exportando...",
"rowsProcessed": "Linhas Processadas",
"exportLoadedRowsWarning": "Apenas {{loaded}} linhas carregadas de {{total}} linhas totais foram exportadas. Navegue pelas páginas do resultado ou execute uma consulta mais restrita antes de exportar se precisar de mais linhas.",
"queryParameters": "Parâmetros da Consulta",
"formatSql": "Formatar SQL",
"convertToConsole": "Converter para Console",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,7 @@
"executingQuery": "Выполнение запроса...",
"exporting": "Экспорт...",
"rowsProcessed": "Обработано строк",
"exportLoadedRowsWarning": "Экспортированы только {{loaded}} загруженных строк из {{total}} строк. Если нужны дополнительные строки, пролистайте результат или выполните более узкий запрос перед экспортом.",
"queryParameters": "Параметры запроса",
"formatSql": "Форматировать SQL",
"convertToConsole": "Преобразовать в консоль",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/tl.json
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,7 @@
"executingQuery": "Isinasakatuparan ang query...",
"exporting": "Nag-e-export...",
"rowsProcessed": "Mga Row na Naproseso",
"exportLoadedRowsWarning": "{{loaded}} lang na na-load na row ang na-export mula sa kabuuang {{total}} row. Mag-page sa resulta o magpatakbo ng mas makitid na query bago mag-export kung kailangan mo ng mas maraming row.",
"queryParameters": "Mga Parameter ng Query",
"formatSql": "I-format ang SQL",
"convertToConsole": "I-convert sa Console",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,7 @@
"executingQuery": "执行查询中...",
"exporting": "导出中...",
"rowsProcessed": "已处理行数",
"exportLoadedRowsWarning": "仅导出了总计 {{total}} 行中的 {{loaded}} 行已加载数据。如果需要更多行,请先翻页加载结果或运行范围更窄的查询后再导出。",
"queryParameters": "查询参数",
"formatSql": "格式化 SQL",
"convertToConsole": "转换为控制台",
Expand Down
86 changes: 83 additions & 3 deletions src/pages/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { useTranslation } from "react-i18next";
import { reconstructTableQuery } from "../utils/editor";
import { shouldShowStatementSuccess } from "../utils/resultPresentation";
import { formatRowsForCopy, copyTextToClipboard } from "../utils/clipboard";
import {
formatResultForExport,
getLoadedRowsExportLimit,
} from "../utils/resultExport";
import { serializePkKey, buildPkMap } from "../utils/dataGrid";
import {
buildKeylessUpdatePlan,
Expand Down Expand Up @@ -70,6 +74,7 @@ import {
} from "lucide-react";
import { invoke } from "@tauri-apps/api/core";
import { listen, emit } from "@tauri-apps/api/event";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { TableToolbar } from "../components/ui/TableToolbar";
import { DataGrid } from "../components/ui/DataGrid";
import { MultiResultPanel } from "../components/ui/MultiResultPanel";
Expand Down Expand Up @@ -98,6 +103,7 @@ import {
removeOtherEntries,
removeEntriesToRight,
removeEntriesToLeft,
findActiveEntry,
} from "../utils/multiResult";
import {
extractQueryParams,
Expand Down Expand Up @@ -264,6 +270,7 @@ export const Editor = ({ commandScopeId }: EditorProps) => {
rowsProcessed: number;
fileName: string;
errorMessage?: string;
warningMessage?: string;
}>({
isOpen: false,
status: "exporting",
Expand Down Expand Up @@ -468,6 +475,16 @@ export const Editor = ({ commandScopeId }: EditorProps) => {
const isMultiDb = usesMultiDatabaseLayout(activeCapabilities, selectedDatabases);
const isEditorOpen =
!isTableTab && (activeTab?.isEditorOpen ?? activeTab?.type !== "table");
const activeResultEntry = useMemo(
() =>
activeTab?.results
? findActiveEntry(activeTab.results, activeTab.activeResultId)
: undefined,
[activeTab?.activeResultId, activeTab?.results],
);
const activeExportResult = activeResultEntry?.result ?? activeTab?.result;
const canExportActiveResult =
!!activeExportResult && activeExportResult.rows.length > 0;

const handleCloseTab = useCallback(
(tabId: string) => {
Expand Down Expand Up @@ -1346,7 +1363,15 @@ export const Editor = ({ commandScopeId }: EditorProps) => {
applied.add(idx);
applyStatement(idx, item);
});
updateTab(targetTabId, { isLoading: false });
const firstResultEntry = batchResults.findIndex(
(item) => (item.result?.rows.length ?? 0) > 0,
);
updateTab(targetTabId, {
isLoading: false,
...(firstResultEntry >= 0
? { activeResultId: entries[firstResultEntry].id }
: {}),
});
},
[
activeConnectionId,
Expand Down Expand Up @@ -3283,6 +3308,59 @@ export const Editor = ({ commandScopeId }: EditorProps) => {
const handleExportCommon = async (format: "csv" | "json" | "markdown") => {
if (!activeTab || !activeConnectionId) return;

const extension = format === "markdown" ? "md" : format;
const multiResult = activeResultEntry?.result;
if (multiResult?.rows.length) {
try {
const loadedRowsLimit = getLoadedRowsExportLimit(multiResult);
const warningMessage = loadedRowsLimit
? t("editor.exportLoadedRowsWarning", {
loaded: loadedRowsLimit.loadedRows.toLocaleString(),
total: loadedRowsLimit.totalRows.toLocaleString(),
})
: undefined;

const filePath = await save({
filters: [
{
name: format === "markdown" ? "Markdown" : format.toUpperCase(),
extensions: [extension],
},
],
defaultPath: `result_${Date.now()}.${extension}`,
});

if (!filePath) return;

setExportState({
isOpen: true,
status: "exporting",
rowsProcessed: multiResult.rows.length,
fileName: filePath.split(/[/\\]/).pop() || filePath,
errorMessage: undefined,
warningMessage,
});
setExportMenuOpen(false);

await writeTextFile(
filePath,
formatResultForExport(multiResult, format, csvDelimiter),
);

setExportState((prev) => ({
...prev,
status: "completed",
}));
} catch (e) {
setExportState((prev) => ({
...prev,
status: "error",
errorMessage: String(e),
}));
}
return;
}

const effectiveSchema =
activeCapabilities?.schemas === true ? activeTab.schema : undefined;
const tabForQuery = { ...activeTab, schema: effectiveSchema };
Expand All @@ -3294,7 +3372,6 @@ export const Editor = ({ commandScopeId }: EditorProps) => {
if (!query || !query.trim()) return;

try {
const extension = format === "markdown" ? "md" : format;
const filePath = await save({
filters: [
{
Expand All @@ -3312,6 +3389,8 @@ export const Editor = ({ commandScopeId }: EditorProps) => {
status: "exporting",
rowsProcessed: 0,
fileName: filePath.split(/[/\\]/).pop() || filePath, // Show only filename
errorMessage: undefined,
warningMessage: undefined,
});
setExportMenuOpen(false);

Expand Down Expand Up @@ -3823,7 +3902,7 @@ export const Editor = ({ commandScopeId }: EditorProps) => {
<div ref={exportMenuRef} className="relative ml-auto shrink-0">
<button
onClick={() => setExportMenuOpen(!exportMenuOpen)}
disabled={!activeTab.result || activeTab.result.rows.length === 0}
disabled={!canExportActiveResult}
aria-haspopup="menu"
aria-expanded={exportMenuOpen}
title={t("editor.export")}
Expand Down Expand Up @@ -4814,6 +4893,7 @@ export const Editor = ({ commandScopeId }: EditorProps) => {
rowsProcessed={exportState.rowsProcessed}
fileName={exportState.fileName}
errorMessage={exportState.errorMessage}
warningMessage={exportState.warningMessage}
onCancel={cancelExport}
onClose={closeExportModal}
/>
Expand Down
65 changes: 65 additions & 0 deletions src/utils/resultExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { QueryResult } from "../types/editor";
import { rowsToMarkdown } from "./clipboard";

export type ResultExportFormat = "csv" | "json" | "markdown";

export function getLoadedRowsExportLimit(
result: QueryResult,
): { loadedRows: number; totalRows: number } | null {
const totalRows = result.pagination?.total_rows;

if (typeof totalRows !== "number" || !Number.isFinite(totalRows)) {
return null;
}

const loadedRows = result.rows.length;
return loadedRows < totalRows ? { loadedRows, totalRows } : null;
}

function csvValue(value: unknown, delimiter: string): string {
const text =
value === null || value === undefined
? ""
: typeof value === "object"
? JSON.stringify(value)
: String(value);

if (text.includes(delimiter) || text.includes('"') || /\r?\n/.test(text)) {
return `"${text.replace(/"/g, '""')}"`;
}

return text;
}

function resultToCsv(result: QueryResult, delimiter: string): string {
const header = result.columns
.map((column) => csvValue(column, delimiter))
.join(delimiter);
const rows = result.rows.map((row) =>
row.map((value) => csvValue(value, delimiter)).join(delimiter),
);
return [header, ...rows].join("\n");
}

export function formatResultForExport(
result: QueryResult,
format: ResultExportFormat,
csvDelimiter = ",",
): string {
if (format === "json") {
const rows = result.rows.map((row) => {
const obj: Record<string, unknown> = {};
result.columns.forEach((column, index) => {
obj[column] = row[index] ?? null;
});
return obj;
});
return JSON.stringify(rows, null, 2);
}

if (format === "markdown") {
return rowsToMarkdown(result.rows, result.columns);
}

return resultToCsv(result, csvDelimiter);
}
Loading