mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
feat(export): add HTML export for query result grids
This commit is contained in:
@@ -19,3 +19,7 @@ LIBSQLITE3_FLAGS = "SQLITE_ENABLE_MATH_FUNCTIONS"
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
linker = "rust-lld"
|
||||
# Increase main-thread stack from the Windows default of 1 MiB to 8 MiB.
|
||||
# Large Rust projects in debug mode overflow the 1 MiB stack when DB query
|
||||
# paths pull in deep serde/driver call stacks. This embeds the size in the PE.
|
||||
rustflags = ["-C", "link-arg=/STACK:8388608"]
|
||||
|
||||
@@ -5565,6 +5565,12 @@ function exportSelectedRowsMarkdown() {
|
||||
return exportMarkdown(rowIds);
|
||||
}
|
||||
|
||||
function exportSelectedRowsHtml() {
|
||||
const rowIds = affectedRowIds();
|
||||
if (rowIds.length === 0) return;
|
||||
return exportHtml(rowIds);
|
||||
}
|
||||
|
||||
function exportSelectedRowsSql() {
|
||||
const rowIds = affectedRowIds();
|
||||
if (rowIds.length === 0) return;
|
||||
@@ -7467,6 +7473,8 @@ const {
|
||||
exportCurrentPageJson,
|
||||
exportMarkdown,
|
||||
exportCurrentPageMarkdown,
|
||||
exportHtml,
|
||||
exportCurrentPageHtml,
|
||||
exportXlsx,
|
||||
exportXlsxWithSql,
|
||||
exportCurrentPageXlsx,
|
||||
@@ -7655,6 +7663,10 @@ const exportMenuItems = computed(() => {
|
||||
value: "selected-markdown",
|
||||
label: t("grid.exportSelectedRowsMarkdown"),
|
||||
},
|
||||
{
|
||||
value: "selected-html",
|
||||
label: t("grid.exportSelectedRowsHtml"),
|
||||
},
|
||||
{ value: "selected-sql", label: t("grid.exportSelectedRowsSql") },
|
||||
{ value: "selected-txt", label: t("grid.exportSelectedRowsTxt") },
|
||||
]
|
||||
@@ -7667,6 +7679,7 @@ const exportMenuItems = computed(() => {
|
||||
...(canIncludeSql ? [{ value: "xlsx-with-sql", label: t("grid.exportXlsxWithSql") }] : []),
|
||||
{ value: "json", label: t("grid.exportJson") },
|
||||
{ value: "markdown", label: t("grid.exportMarkdown") },
|
||||
{ value: "html", label: t("grid.exportHtml") },
|
||||
{ value: "sql", label: t("grid.exportSql") },
|
||||
{ value: "txt", label: t("grid.exportTxt") },
|
||||
...allResultItems,
|
||||
@@ -7687,6 +7700,7 @@ const exportMenuItems = computed(() => {
|
||||
: []),
|
||||
{ value: "page-json", label: t("grid.exportCurrentPageJson") },
|
||||
{ value: "page-markdown", label: t("grid.exportCurrentPageMarkdown") },
|
||||
{ value: "page-html", label: t("grid.exportCurrentPageHtml") },
|
||||
{ value: "page-sql", label: t("grid.exportCurrentPageSql") },
|
||||
{ value: "page-txt", label: t("grid.exportCurrentPageTxt") },
|
||||
{
|
||||
@@ -7705,6 +7719,7 @@ const exportMenuItems = computed(() => {
|
||||
: []),
|
||||
{ value: "json", label: t("grid.exportCurrentResultJson") },
|
||||
{ value: "markdown", label: t("grid.exportCurrentResultMarkdown") },
|
||||
{ value: "html", label: t("grid.exportCurrentResultHtml") },
|
||||
{ value: "sql", label: t("grid.exportCurrentResultSql") },
|
||||
{ value: "txt", label: t("grid.exportCurrentResultTxt") },
|
||||
...allResultItems,
|
||||
@@ -7723,6 +7738,7 @@ function selectExportMenuItem(value: string) {
|
||||
"page-xlsx-with-sql": exportCurrentPageXlsxWithSql,
|
||||
"page-json": exportCurrentPageJson,
|
||||
"page-markdown": exportCurrentPageMarkdown,
|
||||
"page-html": exportCurrentPageHtml,
|
||||
"page-sql": exportCurrentPageSql,
|
||||
"page-txt": exportCurrentPageTxt,
|
||||
csv: exportCsv,
|
||||
@@ -7732,6 +7748,7 @@ function selectExportMenuItem(value: string) {
|
||||
"all-results-xlsx-with-sql": exportAllResultsXlsxWithSql,
|
||||
json: exportJson,
|
||||
markdown: exportMarkdown,
|
||||
html: exportHtml,
|
||||
sql: exportSql,
|
||||
txt: exportTxt,
|
||||
"selected-csv": exportSelectedRowsCsv,
|
||||
@@ -7739,6 +7756,7 @@ function selectExportMenuItem(value: string) {
|
||||
"selected-xlsx-with-sql": exportSelectedRowsXlsxWithSql,
|
||||
"selected-json": exportSelectedRowsJson,
|
||||
"selected-markdown": exportSelectedRowsMarkdown,
|
||||
"selected-html": exportSelectedRowsHtml,
|
||||
"selected-sql": exportSelectedRowsSql,
|
||||
"selected-txt": exportSelectedRowsTxt,
|
||||
};
|
||||
@@ -11415,6 +11433,7 @@ function exportSubmenu(): ContextMenuItem {
|
||||
{ label: t("grid.exportXlsx"), action: exportXlsx },
|
||||
{ label: t("grid.exportJson"), action: exportJson },
|
||||
{ label: t("grid.exportMarkdown"), action: exportMarkdown },
|
||||
{ label: t("grid.exportHtml"), action: exportHtml },
|
||||
{ label: t("grid.exportSql"), action: exportSql },
|
||||
{ label: t("grid.exportTxt"), action: exportTxt },
|
||||
];
|
||||
@@ -11448,6 +11467,10 @@ function exportSubmenu(): ContextMenuItem {
|
||||
label: t("grid.exportSelectedRowsMarkdown"),
|
||||
action: exportSelectedRowsMarkdown,
|
||||
},
|
||||
{
|
||||
label: t("grid.exportSelectedRowsHtml"),
|
||||
action: exportSelectedRowsHtml,
|
||||
},
|
||||
{ label: t("grid.exportSelectedRowsSql"), action: exportSelectedRowsSql },
|
||||
{ label: t("grid.exportSelectedRowsTxt"), action: exportSelectedRowsTxt },
|
||||
);
|
||||
|
||||
@@ -48,6 +48,7 @@ const FORMAT_META: Record<string, { ext: string; label: string }> = {
|
||||
xlsx: { ext: "xlsx", label: "Excel" },
|
||||
json: { ext: "json", label: "JSON" },
|
||||
markdown: { ext: "md", label: "Markdown" },
|
||||
html: { ext: "html", label: "HTML" },
|
||||
sql: { ext: "sql", label: "SQL" },
|
||||
txt: { ext: "txt", label: "Text" },
|
||||
};
|
||||
@@ -453,6 +454,10 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
return currentResultLabel?.value || tableMeta.value?.tableName || "Export";
|
||||
}
|
||||
|
||||
function currentExportTitle(): string {
|
||||
return tableMeta.value?.tableName || currentResultLabel?.value || "Query Result";
|
||||
}
|
||||
|
||||
function currentExportSql(): string | undefined {
|
||||
return resultExportSql?.value || sql.value;
|
||||
}
|
||||
@@ -985,6 +990,50 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
async function exportHtml(rowIds?: number[]) {
|
||||
await runExclusiveExport(async () => {
|
||||
try {
|
||||
let outputPath = exportFileName("export", "html");
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const path = await save({
|
||||
defaultPath: outputPath,
|
||||
filters: [{ name: "HTML", extensions: ["html"] }],
|
||||
});
|
||||
if (!path) return;
|
||||
outputPath = path as string;
|
||||
}
|
||||
const result = await resultToExport(rowIds);
|
||||
await api.exportQueryResultHtml(outputPath, currentExportTitle(), result.columns, result.rows);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function exportCurrentPageHtml() {
|
||||
await runExclusiveExport(async () => {
|
||||
try {
|
||||
let outputPath = exportFileName("export-page", "html", { page: true });
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const path = await save({
|
||||
defaultPath: outputPath,
|
||||
filters: [{ name: "HTML", extensions: ["html"] }],
|
||||
});
|
||||
if (!path) return;
|
||||
outputPath = path as string;
|
||||
}
|
||||
const result = await resultToExport(undefined, undefined, false);
|
||||
await api.exportQueryResultHtml(outputPath, currentExportTitle(), result.columns, result.rows);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function exportCurrentPageTxt() {
|
||||
await runExclusiveExport(async () => {
|
||||
try {
|
||||
@@ -1599,6 +1648,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
exportCurrentPageJson,
|
||||
exportMarkdown,
|
||||
exportCurrentPageMarkdown,
|
||||
exportHtml,
|
||||
exportCurrentPageHtml,
|
||||
exportTxt,
|
||||
exportCurrentPageTxt,
|
||||
exportXlsx,
|
||||
|
||||
@@ -1797,6 +1797,7 @@ export default withEnglishFallback({
|
||||
exportCurrentPageXlsxWithSql: "Cari səhifəni SQL ilə XLSX kimi ixrac et",
|
||||
exportCurrentPageJson: "Cari səhifəni JSON kimi ixrac et",
|
||||
exportCurrentPageMarkdown: "Cari səhifəni Markdown kimi ixrac et",
|
||||
exportCurrentPageHtml: "Cari səhifəni HTML kimi ixrac et",
|
||||
exportCurrentPageSql: "Cari səhifəni SQL INSERT kimi ixrac et",
|
||||
exportCurrentPageTxt: "Cari səhifəni TXT kimi ixrac et",
|
||||
exportCurrentResultCsv: "Cari nəticə dəstinin bütün verilənlərini CSV kimi ixrac et",
|
||||
@@ -1804,12 +1805,14 @@ export default withEnglishFallback({
|
||||
exportCurrentResultXlsxWithSql: "Cari nəticə dəstinin bütün verilənlərini SQL ilə XLSX kimi ixrac et",
|
||||
exportCurrentResultJson: "Cari nəticə dəstinin bütün verilənlərini JSON kimi ixrac et",
|
||||
exportCurrentResultMarkdown: "Cari nəticə dəstinin bütün verilənlərini Markdown kimi ixrac et",
|
||||
exportCurrentResultHtml: "Cari nəticə dəstinin bütün verilənlərini HTML kimi ixrac et",
|
||||
exportCurrentResultSql: "Cari nəticə dəstinin bütün verilənlərini SQL INSERT kimi ixrac et",
|
||||
exportCurrentResultTxt: "Cari nəticə dəstinin bütün verilənlərini TXT kimi ixrac et",
|
||||
exportAllResultsXlsx: "Bu icranın bütün nəticə dəstlərini XLSX kimi ixrac et",
|
||||
exportAllResultsXlsxWithSql: "Bu icranın bütün nəticə dəstlərini SQL ilə XLSX kimi ixrac et",
|
||||
exportJson: "JSON-a ixrac et",
|
||||
exportMarkdown: "Markdown-a ixrac et",
|
||||
exportHtml: "HTML-ə ixrac et",
|
||||
exportSql: "SQL INSERT kimi ixrac et",
|
||||
sqlInsertModeTitle: "SQL INSERT ixrac rejimi",
|
||||
sqlInsertModePrompt: "Sətirlərin SQL INSERT faylına necə yazılacağını seçin:",
|
||||
@@ -1823,6 +1826,7 @@ export default withEnglishFallback({
|
||||
exportSelectedRowsXlsxWithSql: "Seçilmiş sətirləri SQL ilə XLSX kimi ixrac et",
|
||||
exportSelectedRowsJson: "Seçilmiş sətirləri JSON kimi ixrac et",
|
||||
exportSelectedRowsMarkdown: "Seçilmiş sətirləri Markdown kimi ixrac et",
|
||||
exportSelectedRowsHtml: "Seçilmiş sətirləri HTML kimi ixrac et",
|
||||
exportSelectedRowsSql: "Seçilmiş sətirləri SQL INSERT kimi ixrac et",
|
||||
exportSelectedRowsTxt: "Seçilmiş sətirləri TXT kimi ixrac et",
|
||||
exported: "İxrac edildi",
|
||||
|
||||
@@ -1931,6 +1931,7 @@ export default {
|
||||
exportCurrentPageXlsxWithSql: "Export Current Page as XLSX with SQL",
|
||||
exportCurrentPageJson: "Export Current Page as JSON",
|
||||
exportCurrentPageMarkdown: "Export Current Page as Markdown",
|
||||
exportCurrentPageHtml: "Export Current Page as HTML",
|
||||
exportCurrentPageSql: "Export Current Page as SQL INSERT",
|
||||
exportCurrentPageTxt: "Export Current Page as TXT",
|
||||
exportCurrentResultCsv: "Export Current Result Set All Data as CSV",
|
||||
@@ -1938,12 +1939,14 @@ export default {
|
||||
exportCurrentResultXlsxWithSql: "Export Current Result Set All Data as XLSX with SQL",
|
||||
exportCurrentResultJson: "Export Current Result Set All Data as JSON",
|
||||
exportCurrentResultMarkdown: "Export Current Result Set All Data as Markdown",
|
||||
exportCurrentResultHtml: "Export Current Result Set All Data as HTML",
|
||||
exportCurrentResultSql: "Export Current Result Set All Data as SQL INSERT",
|
||||
exportCurrentResultTxt: "Export Current Result Set All Data as TXT",
|
||||
exportAllResultsXlsx: "Export This Run's All Result Sets as XLSX",
|
||||
exportAllResultsXlsxWithSql: "Export This Run's All Result Sets as XLSX with SQL",
|
||||
exportJson: "Export JSON",
|
||||
exportMarkdown: "Export Markdown",
|
||||
exportHtml: "Export HTML",
|
||||
exportSql: "Export SQL INSERT",
|
||||
sqlInsertModeTitle: "SQL INSERT Export Mode",
|
||||
sqlInsertModePrompt: "Choose how rows are written to the SQL INSERT file:",
|
||||
@@ -1957,6 +1960,7 @@ export default {
|
||||
exportSelectedRowsXlsxWithSql: "Export Selected Rows as XLSX with SQL",
|
||||
exportSelectedRowsJson: "Export Selected Rows as JSON",
|
||||
exportSelectedRowsMarkdown: "Export Selected Rows as Markdown",
|
||||
exportSelectedRowsHtml: "Export Selected Rows as HTML",
|
||||
exportSelectedRowsSql: "Export Selected Rows as SQL INSERT",
|
||||
exportSelectedRowsTxt: "Export Selected Rows as TXT",
|
||||
exported: "Exported",
|
||||
|
||||
@@ -1804,6 +1804,7 @@ export default withEnglishFallback({
|
||||
exportCurrentPageXlsxWithSql: "Exportar página actual como XLSX con SQL",
|
||||
exportCurrentPageJson: "Exportar página actual como JSON",
|
||||
exportCurrentPageMarkdown: "Exportar página actual como Markdown",
|
||||
exportCurrentPageHtml: "Exportar página actual como HTML",
|
||||
exportCurrentPageSql: "Exportar página actual como SQL INSERT",
|
||||
exportCurrentPageTxt: "Exportar página actual como TXT",
|
||||
exportCurrentResultCsv: "Exportar todos los datos del resultado actual como CSV",
|
||||
@@ -1811,12 +1812,14 @@ export default withEnglishFallback({
|
||||
exportCurrentResultXlsxWithSql: "Exportar todos los datos del resultado actual como XLSX con SQL",
|
||||
exportCurrentResultJson: "Exportar todos los datos del resultado actual como JSON",
|
||||
exportCurrentResultMarkdown: "Exportar todos los datos del resultado actual como Markdown",
|
||||
exportCurrentResultHtml: "Exportar todos los datos del resultado actual como HTML",
|
||||
exportCurrentResultSql: "Exportar todos los datos del resultado actual como SQL INSERT",
|
||||
exportCurrentResultTxt: "Exportar todos los datos del resultado actual como TXT",
|
||||
exportAllResultsXlsx: "Exportar todos los resultados de esta ejecución como XLSX",
|
||||
exportAllResultsXlsxWithSql: "Exportar todos los resultados de esta ejecución como XLSX con SQL",
|
||||
exportJson: "Exportar JSON",
|
||||
exportMarkdown: "Exportar Markdown",
|
||||
exportHtml: "Exportar HTML",
|
||||
exportSql: "Exportar SQL INSERT",
|
||||
sqlInsertModeTitle: "Modo de exportación SQL INSERT",
|
||||
sqlInsertModePrompt: "Elige cómo se escribirán las filas en el archivo SQL INSERT:",
|
||||
@@ -1830,6 +1833,7 @@ export default withEnglishFallback({
|
||||
exportSelectedRowsXlsxWithSql: "Exportar filas seleccionadas como XLSX con SQL",
|
||||
exportSelectedRowsJson: "Exportar filas seleccionadas como JSON",
|
||||
exportSelectedRowsMarkdown: "Exportar filas seleccionadas como Markdown",
|
||||
exportSelectedRowsHtml: "Exportar filas seleccionadas como HTML",
|
||||
exportSelectedRowsSql: "Exportar filas seleccionadas como SQL INSERT",
|
||||
exportSelectedRowsTxt: "Exportar filas seleccionadas como TXT",
|
||||
exported: "Exportado",
|
||||
|
||||
@@ -1802,6 +1802,7 @@ export default withEnglishFallback({
|
||||
exportCurrentPageXlsxWithSql: "Esporta pagina corrente come XLSX con SQL",
|
||||
exportCurrentPageJson: "Esporta pagina corrente come JSON",
|
||||
exportCurrentPageMarkdown: "Esporta pagina corrente come Markdown",
|
||||
exportCurrentPageHtml: "Esporta pagina corrente come HTML",
|
||||
exportCurrentPageSql: "Esporta pagina corrente come SQL INSERT",
|
||||
exportCurrentPageTxt: "Esporta pagina corrente come TXT",
|
||||
exportCurrentResultCsv: "Esporta tutti i dati del risultato corrente come CSV",
|
||||
@@ -1809,12 +1810,14 @@ export default withEnglishFallback({
|
||||
exportCurrentResultXlsxWithSql: "Esporta tutti i dati del risultato corrente come XLSX con SQL",
|
||||
exportCurrentResultJson: "Esporta tutti i dati del risultato corrente come JSON",
|
||||
exportCurrentResultMarkdown: "Esporta tutti i dati del risultato corrente come Markdown",
|
||||
exportCurrentResultHtml: "Esporta tutti i dati del risultato corrente come HTML",
|
||||
exportCurrentResultSql: "Esporta tutti i dati del risultato corrente come SQL INSERT",
|
||||
exportCurrentResultTxt: "Esporta tutti i dati del risultato corrente come TXT",
|
||||
exportAllResultsXlsx: "Esporta tutti i set di risultati di questa esecuzione come XLSX",
|
||||
exportAllResultsXlsxWithSql: "Esporta tutti i set di risultati di questa esecuzione come XLSX con SQL",
|
||||
exportJson: "Esporta JSON",
|
||||
exportMarkdown: "Esporta Markdown",
|
||||
exportHtml: "Esporta HTML",
|
||||
exportSql: "Esporta SQL INSERT",
|
||||
sqlInsertModeTitle: "Modalità di esportazione SQL INSERT",
|
||||
sqlInsertModePrompt: "Scegli come scrivere le righe nel file SQL INSERT:",
|
||||
@@ -1828,6 +1831,7 @@ export default withEnglishFallback({
|
||||
exportSelectedRowsXlsxWithSql: "Esporta Righe Selezionate come XLSX con SQL",
|
||||
exportSelectedRowsJson: "Esporta Righe Selezionate come JSON",
|
||||
exportSelectedRowsMarkdown: "Esporta Righe Selezionate come Markdown",
|
||||
exportSelectedRowsHtml: "Esporta Righe Selezionate come HTML",
|
||||
exportSelectedRowsSql: "Esporta Righe Selezionate come SQL INSERT",
|
||||
exportSelectedRowsTxt: "Esporta Righe Selezionate come TXT",
|
||||
exported: "Esportato",
|
||||
|
||||
@@ -1816,6 +1816,7 @@ export default withEnglishFallback({
|
||||
exportCurrentPageXlsxWithSql: "現在のページをSQL付きXLSXでエクスポート",
|
||||
exportCurrentPageJson: "現在のページをJSONでエクスポート",
|
||||
exportCurrentPageMarkdown: "現在のページをMarkdownでエクスポート",
|
||||
exportCurrentPageHtml: "現在のページをHTMLでエクスポート",
|
||||
exportCurrentPageSql: "現在のページをSQL INSERTでエクスポート",
|
||||
exportCurrentPageTxt: "現在のページをTXTでエクスポート",
|
||||
exportCurrentResultCsv: "現在の結果セット全データをCSVでエクスポート",
|
||||
@@ -1823,12 +1824,14 @@ export default withEnglishFallback({
|
||||
exportCurrentResultXlsxWithSql: "現在の結果セット全データをSQL付きXLSXでエクスポート",
|
||||
exportCurrentResultJson: "現在の結果セット全データをJSONでエクスポート",
|
||||
exportCurrentResultMarkdown: "現在の結果セット全データをMarkdownでエクスポート",
|
||||
exportCurrentResultHtml: "現在の結果セット全データをHTMLでエクスポート",
|
||||
exportCurrentResultSql: "現在の結果セット全データをSQL INSERTでエクスポート",
|
||||
exportCurrentResultTxt: "現在の結果セット全データをTXTでエクスポート",
|
||||
exportAllResultsXlsx: "今回実行の全結果セットをXLSXでエクスポート",
|
||||
exportAllResultsXlsxWithSql: "今回実行の全結果セットをSQL付きXLSXでエクスポート",
|
||||
exportJson: "JSONエクスポート",
|
||||
exportMarkdown: "Markdownエクスポート",
|
||||
exportHtml: "HTMLエクスポート",
|
||||
exportSql: "SQL INSERTエクスポート",
|
||||
sqlInsertModeTitle: "SQL INSERT エクスポートモード",
|
||||
sqlInsertModePrompt: "SQL INSERT ファイルへの行の書き出し方法を選択してください:",
|
||||
@@ -1842,6 +1845,7 @@ export default withEnglishFallback({
|
||||
exportSelectedRowsXlsxWithSql: "選択行をSQL付きXLSXでエクスポート",
|
||||
exportSelectedRowsJson: "選択行をJSONでエクスポート",
|
||||
exportSelectedRowsMarkdown: "選択行をMarkdownでエクスポート",
|
||||
exportSelectedRowsHtml: "選択行をHTMLでエクスポート",
|
||||
exportSelectedRowsSql: "選択行をSQL INSERTでエクスポート",
|
||||
exportSelectedRowsTxt: "選択行をTXTでエクスポート",
|
||||
exported: "エクスポートしました",
|
||||
|
||||
@@ -1803,6 +1803,7 @@ export default withEnglishFallback({
|
||||
exportCurrentPageXlsxWithSql: "현재 페이지를 SQL 포함 XLSX로 내보내기",
|
||||
exportCurrentPageJson: "현재 페이지를 JSON으로 내보내기",
|
||||
exportCurrentPageMarkdown: "현재 페이지를 Markdown으로 내보내기",
|
||||
exportCurrentPageHtml: "현재 페이지를 HTML로 내보내기",
|
||||
exportCurrentPageSql: "현재 페이지를 SQL INSERT로 내보내기",
|
||||
exportCurrentPageTxt: "현재 페이지를 TXT로 내보내기",
|
||||
exportCurrentResultCsv: "현재 결과 집합 전체를 CSV로 내보내기",
|
||||
@@ -1810,12 +1811,14 @@ export default withEnglishFallback({
|
||||
exportCurrentResultXlsxWithSql: "현재 결과 집합 전체를 SQL 포함 XLSX로 내보내기",
|
||||
exportCurrentResultJson: "현재 결과 집합 전체를 JSON으로 내보내기",
|
||||
exportCurrentResultMarkdown: "현재 결과 집합 전체를 Markdown으로 내보내기",
|
||||
exportCurrentResultHtml: "현재 결과 집합 전체를 HTML로 내보내기",
|
||||
exportCurrentResultSql: "현재 결과 집합 전체를 SQL INSERT로 내보내기",
|
||||
exportCurrentResultTxt: "현재 결과 집합 전체를 TXT로 내보내기",
|
||||
exportAllResultsXlsx: "이번 실행의 모든 결과 집합을 XLSX로 내보내기",
|
||||
exportAllResultsXlsxWithSql: "이번 실행의 모든 결과 집합을 SQL 포함 XLSX로 내보내기",
|
||||
exportJson: "JSON 내보내기",
|
||||
exportMarkdown: "Markdown 내보내기",
|
||||
exportHtml: "HTML 내보내기",
|
||||
exportSql: "SQL INSERT 내보내기",
|
||||
sqlInsertModeTitle: "SQL INSERT 내보내기 모드",
|
||||
sqlInsertModePrompt: "SQL INSERT 파일에 행을 기록할 방식을 선택하세요:",
|
||||
@@ -1829,6 +1832,7 @@ export default withEnglishFallback({
|
||||
exportSelectedRowsXlsxWithSql: "선택한 행을 SQL 포함 XLSX로 내보내기",
|
||||
exportSelectedRowsJson: "선택한 행을 JSON으로 내보내기",
|
||||
exportSelectedRowsMarkdown: "선택한 행을 Markdown으로 내보내기",
|
||||
exportSelectedRowsHtml: "선택한 행을 HTML로 내보내기",
|
||||
exportSelectedRowsSql: "선택한 행을 SQL INSERT로 내보내기",
|
||||
exportSelectedRowsTxt: "선택한 행을 TXT로 내보내기",
|
||||
exported: "내보냈습니다",
|
||||
|
||||
@@ -1804,6 +1804,7 @@ export default withEnglishFallback({
|
||||
exportCurrentPageXlsxWithSql: "Exportar página atual como XLSX com SQL",
|
||||
exportCurrentPageJson: "Exportar página atual como JSON",
|
||||
exportCurrentPageMarkdown: "Exportar página atual como Markdown",
|
||||
exportCurrentPageHtml: "Exportar página atual como HTML",
|
||||
exportCurrentPageSql: "Exportar página atual como SQL INSERT",
|
||||
exportCurrentPageTxt: "Exportar página atual como TXT",
|
||||
exportCurrentResultCsv: "Exportar todos os dados do resultado atual como CSV",
|
||||
@@ -1811,12 +1812,14 @@ export default withEnglishFallback({
|
||||
exportCurrentResultXlsxWithSql: "Exportar todos os dados do resultado atual como XLSX com SQL",
|
||||
exportCurrentResultJson: "Exportar todos os dados do resultado atual como JSON",
|
||||
exportCurrentResultMarkdown: "Exportar todos os dados do resultado atual como Markdown",
|
||||
exportCurrentResultHtml: "Exportar todos os dados do resultado atual como HTML",
|
||||
exportCurrentResultSql: "Exportar todos os dados do resultado atual como SQL INSERT",
|
||||
exportCurrentResultTxt: "Exportar todos os dados do resultado atual como TXT",
|
||||
exportAllResultsXlsx: "Exportar todos os conjuntos de resultados desta execução como XLSX",
|
||||
exportAllResultsXlsxWithSql: "Exportar todos os conjuntos de resultados desta execução como XLSX com SQL",
|
||||
exportJson: "Exportar JSON",
|
||||
exportMarkdown: "Exportar Markdown",
|
||||
exportHtml: "Exportar HTML",
|
||||
exportSql: "Exportar SQL INSERT",
|
||||
sqlInsertModeTitle: "Modo de exportação SQL INSERT",
|
||||
sqlInsertModePrompt: "Escolha como as linhas serão gravadas no arquivo SQL INSERT:",
|
||||
@@ -1830,6 +1833,7 @@ export default withEnglishFallback({
|
||||
exportSelectedRowsXlsxWithSql: "Exportar Linhas Selecionadas como XLSX com SQL",
|
||||
exportSelectedRowsJson: "Exportar Linhas Selecionadas como JSON",
|
||||
exportSelectedRowsMarkdown: "Exportar Linhas Selecionadas como Markdown",
|
||||
exportSelectedRowsHtml: "Exportar Linhas Selecionadas como HTML",
|
||||
exportSelectedRowsSql: "Exportar Linhas Selecionadas como SQL INSERT",
|
||||
exportSelectedRowsTxt: "Exportar Linhas Selecionadas como TXT",
|
||||
exported: "Exportado",
|
||||
|
||||
@@ -1798,6 +1798,7 @@ export default withEnglishFallback({
|
||||
exportCurrentPageXlsxWithSql: "Geçerli Sayfayı SQL ile XLSX Olarak Dışa Aktar",
|
||||
exportCurrentPageJson: "Geçerli Sayfayı JSON Olarak Dışa Aktar",
|
||||
exportCurrentPageMarkdown: "Geçerli Sayfayı Markdown Olarak Dışa Aktar",
|
||||
exportCurrentPageHtml: "Geçerli Sayfayı HTML Olarak Dışa Aktar",
|
||||
exportCurrentPageSql: "Geçerli Sayfayı SQL INSERT Olarak Dışa Aktar",
|
||||
exportCurrentPageTxt: "Geçerli Sayfayı TXT Olarak Dışa Aktar",
|
||||
exportCurrentResultCsv: "Geçerli Sonuç Kümesinin Tüm Verisini CSV Olarak Dışa Aktar",
|
||||
@@ -1805,12 +1806,14 @@ export default withEnglishFallback({
|
||||
exportCurrentResultXlsxWithSql: "Geçerli Sonuç Kümesinin Tüm Verisini SQL ile XLSX Olarak Dışa Aktar",
|
||||
exportCurrentResultJson: "Geçerli Sonuç Kümesinin Tüm Verisini JSON Olarak Dışa Aktar",
|
||||
exportCurrentResultMarkdown: "Geçerli Sonuç Kümesinin Tüm Verisini Markdown Olarak Dışa Aktar",
|
||||
exportCurrentResultHtml: "Geçerli Sonuç Kümesinin Tüm Verisini HTML Olarak Dışa Aktar",
|
||||
exportCurrentResultSql: "Geçerli Sonuç Kümesinin Tüm Verisini SQL INSERT Olarak Dışa Aktar",
|
||||
exportCurrentResultTxt: "Geçerli Sonuç Kümesinin Tüm Verisini TXT Olarak Dışa Aktar",
|
||||
exportAllResultsXlsx: "Bu Çalıştırmanın Tüm Sonuç Kümelerini XLSX Olarak Dışa Aktar",
|
||||
exportAllResultsXlsxWithSql: "Bu Çalıştırmanın Tüm Sonuç Kümelerini SQL ile XLSX Olarak Dışa Aktar",
|
||||
exportJson: "JSON Dışa Aktar",
|
||||
exportMarkdown: "Markdown Dışa Aktar",
|
||||
exportHtml: "HTML Dışa Aktar",
|
||||
exportSql: "SQL INSERT Dışa Aktar",
|
||||
exportTxt: "TXT Dışa Aktar",
|
||||
exportSelectedRowsCsv: "Seçili Satırları CSV Olarak Dışa Aktar",
|
||||
@@ -1818,6 +1821,7 @@ export default withEnglishFallback({
|
||||
exportSelectedRowsXlsxWithSql: "Seçili Satırları SQL ile XLSX Olarak Dışa Aktar",
|
||||
exportSelectedRowsJson: "Seçili Satırları JSON Olarak Dışa Aktar",
|
||||
exportSelectedRowsMarkdown: "Seçili Satırları Markdown Olarak Dışa Aktar",
|
||||
exportSelectedRowsHtml: "Seçili Satırları HTML Olarak Dışa Aktar",
|
||||
exportSelectedRowsSql: "Seçili Satırları SQL INSERT Olarak Dışa Aktar",
|
||||
exportSelectedRowsTxt: "Seçili Satırları TXT Olarak Dışa Aktar",
|
||||
exported: "Dışa aktarıldı",
|
||||
|
||||
@@ -1845,6 +1845,7 @@ export default withEnglishFallback({
|
||||
exportCurrentPageXlsxWithSql: "导出当前页 XLSX(含 SQL)",
|
||||
exportCurrentPageJson: "导出当前页 JSON",
|
||||
exportCurrentPageMarkdown: "导出当前页 Markdown",
|
||||
exportCurrentPageHtml: "导出当前页 HTML",
|
||||
exportCurrentPageSql: "导出当前页 SQL INSERT",
|
||||
exportCurrentPageTxt: "导出当前页 TXT",
|
||||
exportCurrentResultCsv: "导出当前结果集全部数据 CSV",
|
||||
@@ -1852,12 +1853,14 @@ export default withEnglishFallback({
|
||||
exportCurrentResultXlsxWithSql: "导出当前结果集全部数据 XLSX(含 SQL)",
|
||||
exportCurrentResultJson: "导出当前结果集全部数据 JSON",
|
||||
exportCurrentResultMarkdown: "导出当前结果集全部数据 Markdown",
|
||||
exportCurrentResultHtml: "导出当前结果集全部数据 HTML",
|
||||
exportCurrentResultSql: "导出当前结果集全部数据 SQL INSERT",
|
||||
exportCurrentResultTxt: "导出当前结果集全部数据 TXT",
|
||||
exportAllResultsXlsx: "导出本次执行全部结果集为 XLSX",
|
||||
exportAllResultsXlsxWithSql: "导出本次执行全部结果集为 XLSX(含 SQL)",
|
||||
exportJson: "导出 JSON",
|
||||
exportMarkdown: "导出 Markdown",
|
||||
exportHtml: "导出 HTML",
|
||||
exportSql: "导出 SQL INSERT",
|
||||
sqlInsertModeTitle: "SQL INSERT 导出模式",
|
||||
sqlInsertModePrompt: "请选择 SQL INSERT 文件的写出模式:",
|
||||
@@ -1871,6 +1874,7 @@ export default withEnglishFallback({
|
||||
exportSelectedRowsXlsxWithSql: "导出选中行为 XLSX(含 SQL)",
|
||||
exportSelectedRowsJson: "导出选中行为 JSON",
|
||||
exportSelectedRowsMarkdown: "导出选中行为 Markdown",
|
||||
exportSelectedRowsHtml: "导出选中行为 HTML",
|
||||
exportSelectedRowsSql: "导出选中行为 SQL INSERT",
|
||||
exportSelectedRowsTxt: "导出选中行为 TXT",
|
||||
exported: "导出完成",
|
||||
|
||||
@@ -1802,6 +1802,7 @@ export default withEnglishFallback({
|
||||
exportCurrentPageXlsxWithSql: "匯出目前頁 XLSX(含 SQL)",
|
||||
exportCurrentPageJson: "匯出目前頁 JSON",
|
||||
exportCurrentPageMarkdown: "匯出目前頁 Markdown",
|
||||
exportCurrentPageHtml: "匯出目前頁 HTML",
|
||||
exportCurrentPageSql: "匯出目前頁 SQL INSERT",
|
||||
exportCurrentPageTxt: "匯出目前頁 TXT",
|
||||
exportCurrentResultCsv: "匯出目前結果集全部資料 CSV",
|
||||
@@ -1809,12 +1810,14 @@ export default withEnglishFallback({
|
||||
exportCurrentResultXlsxWithSql: "匯出目前結果集全部資料 XLSX(含 SQL)",
|
||||
exportCurrentResultJson: "匯出目前結果集全部資料 JSON",
|
||||
exportCurrentResultMarkdown: "匯出目前結果集全部資料 Markdown",
|
||||
exportCurrentResultHtml: "匯出目前結果集全部資料 HTML",
|
||||
exportCurrentResultSql: "匯出目前結果集全部資料 SQL INSERT",
|
||||
exportCurrentResultTxt: "匯出目前結果集全部資料 TXT",
|
||||
exportAllResultsXlsx: "匯出本次執行全部結果集為 XLSX",
|
||||
exportAllResultsXlsxWithSql: "匯出本次執行全部結果集為 XLSX(含 SQL)",
|
||||
exportJson: "匯出 JSON",
|
||||
exportMarkdown: "匯出 Markdown",
|
||||
exportHtml: "匯出 HTML",
|
||||
exportSql: "匯出 SQL INSERT",
|
||||
sqlInsertModeTitle: "SQL INSERT 匯出模式",
|
||||
sqlInsertModePrompt: "請選擇 SQL INSERT 檔案的資料列寫入方式:",
|
||||
@@ -1828,6 +1831,7 @@ export default withEnglishFallback({
|
||||
exportSelectedRowsXlsxWithSql: "匯出選取資料列為 XLSX(含 SQL)",
|
||||
exportSelectedRowsJson: "匯出選取資料列為 JSON",
|
||||
exportSelectedRowsMarkdown: "匯出選取資料列為 Markdown",
|
||||
exportSelectedRowsHtml: "匯出選取資料列為 HTML",
|
||||
exportSelectedRowsSql: "匯出選取資料列為 SQL INSERT",
|
||||
exportSelectedRowsTxt: "匯出選取資料列為 TXT",
|
||||
exported: "匯出完成",
|
||||
|
||||
@@ -516,6 +516,7 @@ export const exportQueryResultXlsx = forward("exportQueryResultXlsx");
|
||||
export const exportQueryResultsXlsx = forward("exportQueryResultsXlsx");
|
||||
export const exportQueryResultJson = forward("exportQueryResultJson");
|
||||
export const exportQueryResultMarkdown = forward("exportQueryResultMarkdown");
|
||||
export const exportQueryResultHtml = forward("exportQueryResultHtml");
|
||||
export const startTableExport = forward("startTableExport");
|
||||
export const cancelTableExport = forward("cancelTableExport");
|
||||
export const startQueryResultExport = forward("startQueryResultExport");
|
||||
|
||||
@@ -3212,6 +3212,11 @@ export async function exportQueryResultMarkdown(filePath: string, columns: strin
|
||||
downloadTextFile(filePath, "export.md", result.content, "text/markdown;charset=utf-8");
|
||||
}
|
||||
|
||||
export async function exportQueryResultHtml(filePath: string, title: string | undefined, columns: string[], rows: readonly (readonly XlsxCellValue[])[]): Promise<void> {
|
||||
const result = await post<{ content: string }>("/api/export/query-result-html", { title, columns, rows });
|
||||
downloadTextFile(filePath, "export.html", result.content, "text/html;charset=utf-8");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Redis
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -5650,6 +5650,17 @@ export async function exportQueryResultMarkdown(filePath: string, columns: strin
|
||||
});
|
||||
}
|
||||
|
||||
export async function exportQueryResultHtml(filePath: string, title: string | undefined, columns: string[], rows: readonly (readonly XlsxCellValue[])[]): Promise<void> {
|
||||
return invoke("export_query_result_html", {
|
||||
request: {
|
||||
filePath,
|
||||
title,
|
||||
columns,
|
||||
rows,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export * from "@/lib/backend/mq-tauri";
|
||||
export * from "@/lib/backend/mqtt-tauri";
|
||||
export * from "@/lib/backend/nacos-tauri";
|
||||
|
||||
@@ -4,6 +4,8 @@ use serde_json::{Map, Value};
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryResultTextExportData {
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
pub columns: Vec<String>,
|
||||
pub rows: Vec<Vec<Value>>,
|
||||
}
|
||||
@@ -94,15 +96,193 @@ fn pad(value: &str, width: usize) -> String {
|
||||
format!("{value}{}", " ".repeat(width - current))
|
||||
}
|
||||
|
||||
pub fn format_html(data: &QueryResultTextExportData) -> String {
|
||||
let now = chrono_local_now();
|
||||
let heading = data.title.as_deref().unwrap_or("Query Result");
|
||||
let row_count = data.rows.len();
|
||||
let col_count = data.columns.len();
|
||||
|
||||
let mut html = String::with_capacity(4096 + col_count * 64 + row_count * col_count * 48);
|
||||
|
||||
html.push_str("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n");
|
||||
html.push_str(" <meta charset=\"UTF-8\">\n");
|
||||
html.push_str(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n");
|
||||
html.push_str(&format!(" <title>{}</title>\n", html_escape(heading)));
|
||||
html.push_str(
|
||||
r#" <style>
|
||||
:root {
|
||||
--surface: #ffffff; --bg: #f6f8fa;
|
||||
--text: #1f2328; --text-2: #59636e; --muted: #818b98;
|
||||
--border: #d1d9e0; --border-2: #e7ecf0;
|
||||
--hover: #eef2f6; --stripe: #f9fbfc;
|
||||
--th-bg: #f0f3f6;
|
||||
--ok: #1a7f37; --danger: #cf222e;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--surface: #1c2128; --bg: #0d1117;
|
||||
--text: #e6edf3; --text-2: #9198a1; --muted: #6e7681;
|
||||
--border: #3d444d; --border-2: #2a3038;
|
||||
--hover: #202830; --stripe: #161b22;
|
||||
--th-bg: #22272e;
|
||||
--ok: #3fb950; --danger: #f85149;
|
||||
}
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC",
|
||||
"Microsoft YaHei", sans-serif;
|
||||
background: var(--bg); color: var(--text);
|
||||
padding: 16px; line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
height: 100vh; overflow: hidden;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.page {
|
||||
flex: 1; min-height: 0; width: 100%;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.hdr { flex-shrink: 0; margin-bottom: 12px; }
|
||||
.hdr h1 { font-size: 16px; font-weight: 600; }
|
||||
.hdr .meta { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||||
.tbl-wrap {
|
||||
flex: 1; min-height: 0;
|
||||
display: flex; flex-direction: column;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.tbl-scroll {
|
||||
flex: 1; min-height: 0;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--muted) transparent;
|
||||
}
|
||||
.tbl-scroll::-webkit-scrollbar { height: 10px; width: 10px; }
|
||||
.tbl-scroll::-webkit-scrollbar-thumb { background: var(--muted); border-radius: 8px; }
|
||||
.tbl-scroll::-webkit-scrollbar-thumb:hover { background: var(--text-2); }
|
||||
.tbl-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||
table { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 13px; }
|
||||
thead th {
|
||||
position: sticky; top: 0; z-index: 1;
|
||||
padding: 8px 12px;
|
||||
background: var(--th-bg); color: var(--text);
|
||||
font-weight: 600; font-size: 12px; text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
tbody td {
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid var(--border-2);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 380px;
|
||||
color: var(--text); vertical-align: middle;
|
||||
}
|
||||
tbody tr:last-child td { border-bottom: none; }
|
||||
tbody tr:nth-child(even) td { background: var(--stripe); }
|
||||
tbody tr:hover td { background: var(--hover); }
|
||||
td.null { color: var(--muted); font-style: italic; }
|
||||
td.number { text-align: right; font-variant-numeric: tabular-nums; font-family: ui-monospace, Consolas, "Liberation Mono", monospace; }
|
||||
td.boolean { text-align: center; }
|
||||
td.boolean.true { color: var(--ok); }
|
||||
td.boolean.false { color: var(--danger); }
|
||||
.ftr { flex-shrink: 0; margin-top: 12px; font-size: 11px; color: var(--muted); text-align: center; }
|
||||
@media print {
|
||||
html, body { height: auto; overflow: visible; }
|
||||
body { display: block; background: #fff; padding: 0; }
|
||||
.page, .tbl-wrap { display: block; }
|
||||
.tbl-scroll { max-height: none; overflow: visible; }
|
||||
thead th { position: static; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
"#
|
||||
);
|
||||
|
||||
// Header
|
||||
html.push_str(" <div class=\"hdr\">\n");
|
||||
html.push_str(&format!(" <h1>{}</h1>\n", html_escape(heading)));
|
||||
html.push_str(&format!(
|
||||
" <div class=\"meta\">{} rows · {} columns · {} · DBX</div>\n",
|
||||
row_count,
|
||||
col_count,
|
||||
html_escape(&now)
|
||||
));
|
||||
html.push_str(" </div>\n");
|
||||
|
||||
// Table card
|
||||
html.push_str(" <div class=\"tbl-wrap\">\n");
|
||||
html.push_str(" <div class=\"tbl-scroll\">\n");
|
||||
html.push_str(" <table>\n <thead>\n <tr>\n");
|
||||
for col in &data.columns {
|
||||
html.push_str(&format!(" <th>{}</th>\n", html_escape(col)));
|
||||
}
|
||||
html.push_str(" </tr>\n </thead>\n <tbody>\n");
|
||||
|
||||
for row in &data.rows {
|
||||
html.push_str(" <tr>\n");
|
||||
for cell in row {
|
||||
let (text, css_class) = html_cell_value(cell);
|
||||
if css_class.is_empty() {
|
||||
html.push_str(&format!(" <td>{}</td>\n", html_escape(&text)));
|
||||
} else {
|
||||
html.push_str(&format!(" <td class=\"{}\">{}</td>\n", css_class, html_escape(&text)));
|
||||
}
|
||||
}
|
||||
html.push_str(" </tr>\n");
|
||||
}
|
||||
|
||||
html.push_str(" </tbody>\n </table>\n </div>\n </div>\n");
|
||||
html.push_str(" <div class=\"ftr\">Exported by DBX</div>\n");
|
||||
html.push_str(" </div>\n</body>\n</html>\n");
|
||||
html
|
||||
}
|
||||
|
||||
fn html_escape(value: &str) -> String {
|
||||
let mut out = String::with_capacity(value.len());
|
||||
for ch in value.chars() {
|
||||
match ch {
|
||||
'<' => out.push_str("<"),
|
||||
'>' => out.push_str(">"),
|
||||
'&' => out.push_str("&"),
|
||||
'"' => out.push_str("""),
|
||||
'\'' => out.push_str("'"),
|
||||
'\n' => out.push_str("<br>"),
|
||||
'\r' => {}
|
||||
_ => out.push(ch),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn html_cell_value(value: &Value) -> (String, &'static str) {
|
||||
match value {
|
||||
Value::Null => ("NULL".to_string(), "null"),
|
||||
Value::Bool(true) => ("true".to_string(), "boolean true"),
|
||||
Value::Bool(false) => ("false".to_string(), "boolean false"),
|
||||
Value::Number(n) => (n.to_string(), "number"),
|
||||
Value::String(s) => (s.clone(), ""),
|
||||
other => (other.to_string(), ""),
|
||||
}
|
||||
}
|
||||
|
||||
fn chrono_local_now() -> String {
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::{format_json, format_markdown, QueryResultTextExportData};
|
||||
use super::{format_html, format_json, format_markdown, QueryResultTextExportData};
|
||||
|
||||
#[test]
|
||||
fn formats_json_rows_as_objects() {
|
||||
let out = format_json(&QueryResultTextExportData {
|
||||
title: None,
|
||||
columns: vec!["id".to_string(), "name".to_string(), "active".to_string(), "note".to_string()],
|
||||
rows: vec![vec![json!(1), json!("Ada"), json!(true), Value::Null]],
|
||||
})
|
||||
@@ -124,6 +304,7 @@ mod tests {
|
||||
#[test]
|
||||
fn formats_markdown_with_escaped_pipes_and_newlines() {
|
||||
let out = format_markdown(&QueryResultTextExportData {
|
||||
title: None,
|
||||
columns: vec!["id".to_string(), "payload|kind".to_string()],
|
||||
rows: vec![vec![json!(1), json!("a|b")], vec![json!(2), json!("line one\nline two")]],
|
||||
});
|
||||
@@ -140,4 +321,101 @@ mod tests {
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_html_document_structure() {
|
||||
let out = format_html(&QueryResultTextExportData {
|
||||
title: None,
|
||||
columns: vec!["id".to_string(), "name".to_string()],
|
||||
rows: vec![vec![json!(1), json!("Ada")]],
|
||||
});
|
||||
|
||||
assert!(out.starts_with("<!DOCTYPE html>"), "must start with doctype");
|
||||
assert!(out.ends_with("</html>\n"), "must end with closing html tag");
|
||||
assert!(out.contains("<meta charset=\"UTF-8\">"));
|
||||
assert!(out.contains("<style>"), "CSS must be embedded");
|
||||
assert!(out.contains("<h1>Query Result</h1>"), "default heading when no title");
|
||||
assert!(out.contains("<th>id</th>"), "column headers must be rendered");
|
||||
assert!(out.contains("<th>name</th>"));
|
||||
assert!(out.contains("1 rows · 2 columns"), "row/column counts in meta line");
|
||||
assert!(out.contains("Exported by DBX"), "footer watermark");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_html_uses_custom_title() {
|
||||
let out = format_html(&QueryResultTextExportData {
|
||||
title: Some("public.users".to_string()),
|
||||
columns: vec!["id".to_string()],
|
||||
rows: vec![vec![json!(1)]],
|
||||
});
|
||||
|
||||
assert!(out.contains("<title>public.users</title>"), "document title must use table name");
|
||||
assert!(out.contains("<h1>public.users</h1>"), "heading must use table name");
|
||||
assert!(!out.contains("<h1>Query Result</h1>"), "default heading must not appear");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_html_escapes_title() {
|
||||
let out = format_html(&QueryResultTextExportData {
|
||||
title: Some("<b>users & more</b>".to_string()),
|
||||
columns: vec!["id".to_string()],
|
||||
rows: vec![],
|
||||
});
|
||||
|
||||
assert!(out.contains("<b>users & more</b>"), "title must be escaped");
|
||||
assert!(!out.contains("<b>users"), "raw markup must never appear in heading");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_html_with_typed_cell_classes() {
|
||||
let out = format_html(&QueryResultTextExportData {
|
||||
title: None,
|
||||
columns: vec!["n".to_string(), "flag".to_string(), "off".to_string(), "note".to_string()],
|
||||
rows: vec![vec![json!(42), json!(true), json!(false), Value::Null]],
|
||||
});
|
||||
|
||||
assert!(out.contains("<td class=\"number\">42</td>"));
|
||||
assert!(out.contains("<td class=\"boolean true\">true</td>"));
|
||||
assert!(out.contains("<td class=\"boolean false\">false</td>"));
|
||||
assert!(out.contains("<td class=\"null\">NULL</td>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_html_escapes_special_characters() {
|
||||
let out = format_html(&QueryResultTextExportData {
|
||||
title: None,
|
||||
columns: vec!["col<a>".to_string()],
|
||||
rows: vec![vec![json!("<script>alert('x');</script> & \"quoted\"")]],
|
||||
});
|
||||
|
||||
assert!(out.contains("<th>col<a></th>"), "header must be escaped");
|
||||
assert!(!out.contains("<script>alert"), "raw script tag must never appear");
|
||||
assert!(out.contains("<script>"));
|
||||
assert!(out.contains("& "quoted""));
|
||||
assert!(out.contains("'x'"), "single quotes must be escaped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_html_empty_result_set() {
|
||||
let out =
|
||||
format_html(&QueryResultTextExportData { title: None, columns: vec!["id".to_string()], rows: vec![] });
|
||||
|
||||
assert!(out.contains("0 rows · 1 columns"));
|
||||
assert!(out.contains("<tbody>\n </tbody>"), "empty tbody");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_now_matches_expected_format() {
|
||||
let stamp = super::chrono_local_now();
|
||||
// e.g. 2026-09-17 14:30:59
|
||||
let parts: Vec<&str> = stamp.split(['-', ' ', ':']).collect();
|
||||
assert_eq!(parts.len(), 6, "timestamp must have 6 parts: {stamp}");
|
||||
assert_eq!(stamp.len(), 19, "timestamp must be 19 chars: {stamp}");
|
||||
let year: i32 = parts[0].parse().unwrap();
|
||||
let month: u32 = parts[1].parse().unwrap();
|
||||
let day: u32 = parts[2].parse().unwrap();
|
||||
assert!((2020..=2100).contains(&year), "year out of range: {year}");
|
||||
assert!((1..=12).contains(&month), "month out of range: {month}");
|
||||
assert!((1..=31).contains(&day), "day out of range: {day}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,6 +661,7 @@ async fn main() {
|
||||
.route("/query/close-client-session", post(routes::query::close_client_connection_session))
|
||||
.route("/export/query-result-json", post(routes::text_export::export_query_result_json))
|
||||
.route("/export/query-result-markdown", post(routes::text_export::export_query_result_markdown))
|
||||
.route("/export/query-result-html", post(routes::text_export::export_query_result_html))
|
||||
// Redis
|
||||
.route("/redis/list-databases", post(routes::redis::list_databases))
|
||||
.route("/redis/scan-keys", post(routes::redis::scan_keys))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::Json;
|
||||
use dbx_core::text_export::{format_json, format_markdown, QueryResultTextExportData};
|
||||
use dbx_core::text_export::{format_html, format_json, format_markdown, QueryResultTextExportData};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -8,13 +8,15 @@ use crate::error::AppError;
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryResultTextExportRequest {
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
pub columns: Vec<String>,
|
||||
pub rows: Vec<Vec<Value>>,
|
||||
}
|
||||
|
||||
impl QueryResultTextExportRequest {
|
||||
fn data(self) -> QueryResultTextExportData {
|
||||
QueryResultTextExportData { columns: self.columns, rows: self.rows }
|
||||
QueryResultTextExportData { title: self.title, columns: self.columns, rows: self.rows }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,3 +38,9 @@ pub async fn export_query_result_markdown(
|
||||
) -> Json<QueryResultTextExportResponse> {
|
||||
Json(QueryResultTextExportResponse { content: format_markdown(&req.data()) })
|
||||
}
|
||||
|
||||
pub async fn export_query_result_html(
|
||||
Json(req): Json<QueryResultTextExportRequest>,
|
||||
) -> Json<QueryResultTextExportResponse> {
|
||||
Json(QueryResultTextExportResponse { content: format_html(&req.data()) })
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use dbx_core::text_export::{format_json, format_markdown, QueryResultTextExportData};
|
||||
use dbx_core::text_export::{format_html, format_json, format_markdown, QueryResultTextExportData};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -6,13 +6,15 @@ use serde_json::Value;
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryResultTextExportRequest {
|
||||
pub file_path: String,
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
pub columns: Vec<String>,
|
||||
pub rows: Vec<Vec<Value>>,
|
||||
}
|
||||
|
||||
impl QueryResultTextExportRequest {
|
||||
fn into_data(self) -> QueryResultTextExportData {
|
||||
QueryResultTextExportData { columns: self.columns, rows: self.rows }
|
||||
QueryResultTextExportData { title: self.title, columns: self.columns, rows: self.rows }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +42,17 @@ pub async fn export_query_result_markdown(request: QueryResultTextExportRequest)
|
||||
.map_err(|err| err.to_string())?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn export_query_result_html(request: QueryResultTextExportRequest) -> Result<(), String> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let file_path = request.file_path.clone();
|
||||
let content = format_html(&request.into_data());
|
||||
std::fs::write(file_path, content).map_err(|err| err.to_string())
|
||||
})
|
||||
.await
|
||||
.map_err(|err| err.to_string())?
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::{json, Value};
|
||||
@@ -51,6 +64,7 @@ mod tests {
|
||||
let path = std::env::temp_dir().join(format!("dbx-json-export-{}.json", uuid::Uuid::new_v4()));
|
||||
write_query_result_json(QueryResultTextExportRequest {
|
||||
file_path: path.to_string_lossy().into_owned(),
|
||||
title: None,
|
||||
columns: vec!["id".to_string(), "name".to_string()],
|
||||
rows: vec![vec![json!(1), json!("Ada")]],
|
||||
})
|
||||
|
||||
@@ -2580,6 +2580,7 @@ pub fn run() {
|
||||
commands::xlsx_export::export_query_results_xlsx,
|
||||
commands::text_export::export_query_result_json,
|
||||
commands::text_export::export_query_result_markdown,
|
||||
commands::text_export::export_query_result_html,
|
||||
commands::agents::list_installed_agents,
|
||||
commands::agents::list_installed_agents_local,
|
||||
commands::agents::is_agent_installed,
|
||||
|
||||
Reference in New Issue
Block a user