feat(analytics): maximize drawer and export SQL results (#4871)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-07-28 11:33:19 -07:00
committed by GitHub
parent 03011eea97
commit 3ca08f781b
19 changed files with 195 additions and 9 deletions
+3
View File
@@ -105,6 +105,8 @@ declare module 'vue' {
'Mdi:alertCircleOutline': typeof import('~icons/mdi/alert-circle-outline')['default']
'Mdi:alertOutline': typeof import('~icons/mdi/alert-outline')['default']
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
'Mdi:arrowCollapse': typeof import('~icons/mdi/arrow-collapse')['default']
'Mdi:arrowExpand': typeof import('~icons/mdi/arrow-expand')['default']
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
'Mdi:beer': typeof import('~icons/mdi/beer')['default']
'Mdi:bell': typeof import('~icons/mdi/bell')['default']
@@ -169,6 +171,7 @@ declare module 'vue' {
'Ph:controlBold': typeof import('~icons/ph/control-bold')['default']
'Ph:database': typeof import('~icons/ph/database')['default']
'Ph:dotsThreeVerticalBold': typeof import('~icons/ph/dots-three-vertical-bold')['default']
'Ph:downloadSimple': typeof import('~icons/ph/download-simple')['default']
'Ph:fileSql': typeof import('~icons/ph/file-sql')['default']
'Ph:globeSimple': typeof import('~icons/ph/globe-simple')['default']
'Ph:stack': typeof import('~icons/ph/stack')['default']
@@ -48,6 +48,25 @@
}}</template>
</span>
</div>
<div class="dropdown dropdown-end shrink-0" v-if="canExport">
<div tabindex="0" role="button" class="btn btn-xs btn-ghost cursor-pointer gap-1">
<ph:download-simple class="size-4" />
{{ $t("analytics.export") }}
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-200 rounded-box z-30 w-44 p-2 shadow-sm">
<li>
<a class="cursor-pointer whitespace-nowrap" @click="exportResults('csv')">{{
$t("analytics.export_csv")
}}</a>
</li>
<li>
<a class="cursor-pointer whitespace-nowrap" @click="exportResults('json')">{{
$t("analytics.export_json")
}}</a>
</li>
</ul>
</div>
</div>
</section>
@@ -238,5 +257,54 @@ whenever(evaluating, () => {
const page = computed(() =>
results.value.numRows > pageLimit ? results.value.slice(0, pageLimit) : results.value,
) as unknown as ComputedRef<Table<Record<string, any>>>;
const canExport = computed(() => state.value === "ready" && !evaluating.value && results.value.numRows > 0);
function stringify(value: unknown): string {
if (value === null || value === undefined) return "";
if (typeof value === "bigint") return value.toString();
if (typeof value === "object") return JSON.stringify(value, (_, v) => (typeof v === "bigint" ? v.toString() : v));
return String(value);
}
function toCSV(table: Table<Record<string, any>>, columns: string[]): string {
const escape = (value: string) => (/[",\n\r]/.test(value) ? `"${value.replaceAll('"', '""')}"` : value);
const lines = [columns.map(escape).join(",")];
for (const row of table) {
lines.push(columns.map((column) => escape(stringify((row as Record<string, any>)[column]))).join(","));
}
return lines.join("\n");
}
function toJSON(table: Table<Record<string, any>>, columns: string[]): string {
const rows = [];
for (const row of table) {
const record: Record<string, unknown> = {};
for (const column of columns) {
const value = (row as Record<string, any>)[column];
record[column] = typeof value === "bigint" ? value.toString() : value;
}
rows.push(record);
}
return JSON.stringify(rows, null, 2);
}
function exportResults(format: "csv" | "json") {
const table = results.value as unknown as Table<Record<string, any>>;
if (table.numRows === 0) return;
const columns = Object.keys(table.get(0) as Record<string, any>);
const content = format === "csv" ? toCSV(table, columns) : toJSON(table, columns);
const type = format === "csv" ? "text/csv;charset=utf-8" : "application/json";
const name = container.name.replace(/[^\w.-]+/g, "-");
const url = URL.createObjectURL(new Blob([content], { type }));
const link = document.createElement("a");
link.href = url;
link.download = `${name}-query.${format}`;
link.click();
URL.revokeObjectURL(url);
(document.activeElement as HTMLElement | null)?.blur();
}
</script>
<style scoped></style>
+23 -4
View File
@@ -1,16 +1,29 @@
<template>
<dialog ref="panel" class="modal-right modal items-start outline-hidden backdrop:bg-none">
<div class="modal-box" :width="width">
<div class="modal-box" :width="maximized ? 'full' : width">
<div class="pt-safe relative">
<form method="dialog" class="absolute right-0">
<button v-if="isMobile">
<div class="absolute right-0 flex items-center gap-3">
<button
v-if="!isMobile"
class="hover:text-base-content/60 cursor-pointer outline-hidden"
type="button"
:title="maximized ? $t('drawer.restore') : $t('drawer.maximize')"
:aria-label="maximized ? $t('drawer.restore') : $t('drawer.maximize')"
@click="maximized = !maximized"
>
<mdi:arrow-collapse v-if="maximized" />
<mdi:arrow-expand v-else />
</button>
<form method="dialog">
<button v-if="isMobile" class="cursor-pointer">
<mdi:close />
</button>
<button v-else class="swap hover:swap-active outline-hidden">
<button v-else class="swap hover:swap-active cursor-pointer outline-hidden">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
</div>
<slot v-if="open" :close="close"></slot>
</div>
</div>
@@ -24,6 +37,7 @@ import { type DrawerWidth } from "@/composable/drawer";
const panel = useTemplateRef<HTMLDialogElement>("panel");
const open = ref(false);
const maximized = ref(false);
const { width } = defineProps<{
width: DrawerWidth;
}>();
@@ -35,6 +49,7 @@ function close() {
defineExpose({
open: () => {
open.value = true;
maximized.value = false;
panel.value?.showModal();
},
close,
@@ -55,6 +70,10 @@ useEventListener(panel, "close", () => (open.value = false));
&[width="lg"] {
@apply max-w-5xl;
}
&[width="full"] {
@apply w-full max-w-full;
}
}
.modal-right[open] .modal-box {
+6
View File
@@ -213,6 +213,12 @@ analytics:
example_all: Alle logfiler
example_count: Antal rækker
example_group: Antal efter {column}
export: Eksporter
export_csv: Download CSV
export_json: Download JSON
drawer:
maximize: Maksimer
restore: Gendan
notifications:
title: Notifikationer
description: Konfigurer hvor og hvornår du vil modtage alarmer
+6
View File
@@ -213,6 +213,12 @@ analytics:
example_all: Alle Logs
example_count: Anzahl Zeilen
example_group: Anzahl nach {column}
export: Exportieren
export_csv: CSV herunterladen
export_json: JSON herunterladen
drawer:
maximize: Maximieren
restore: Wiederherstellen
notifications:
title: Benachrichtigungen
description: Konfigurieren Sie, wo und wann Sie Alarme erhalten möchten
+6
View File
@@ -263,6 +263,12 @@ analytics:
example_all: All logs
example_count: Row count
example_group: Count by {column}
export: Export
export_csv: Download CSV
export_json: Download JSON
drawer:
maximize: Maximize
restore: Restore
notifications:
title: Notifications
description: Configure where and when to receive alerts
+6
View File
@@ -257,6 +257,12 @@ analytics:
example_all: Todos los registros
example_count: Número de filas
example_group: Conteo por {column}
export: Exportar
export_csv: Descargar CSV
export_json: Descargar JSON
drawer:
maximize: Maximizar
restore: Restaurar
notifications:
title: Notificaciones
description: Configure dónde y cuándo recibir alertas
+6
View File
@@ -213,6 +213,12 @@ analytics:
example_all: Tous les journaux
example_count: Nombre de lignes
example_group: Nombre par {column}
export: Exporter
export_csv: Télécharger le CSV
export_json: Télécharger le JSON
drawer:
maximize: Agrandir
restore: Restaurer
notifications:
title: Notifications
description: Configurez où et quand recevoir des alertes
+6
View File
@@ -225,6 +225,12 @@ analytics:
example_all: Semua log
example_count: Jumlah baris
example_group: Hitung berdasarkan {column}
export: Ekspor
export_csv: Unduh CSV
export_json: Unduh JSON
drawer:
maximize: Maksimalkan
restore: Pulihkan
notifications:
title: Notifikasi
description: Konfigurasikan di mana dan kapan menerima peringatan
+6
View File
@@ -213,6 +213,12 @@ analytics:
example_all: Tutti i log
example_count: Numero di righe
example_group: Conteggio per {column}
export: Esporta
export_csv: Scarica CSV
export_json: Scarica JSON
drawer:
maximize: Massimizza
restore: Ripristina
notifications:
title: Notifiche
description: Configura dove e quando ricevere avvisi
+6
View File
@@ -217,6 +217,12 @@ analytics:
example_all: 모든 로그
example_count: 행 수
example_group: "{column}별 개수"
export: 내보내기
export_csv: CSV 다운로드
export_json: JSON 다운로드
drawer:
maximize: 최대화
restore: 복원
notifications:
title: 알림
description: 알림을 받을 위치와 시간을 설정합니다
+6
View File
@@ -214,6 +214,12 @@ analytics:
example_all: Alle logs
example_count: Aantal rijen
example_group: Aantal per {column}
export: Exporteren
export_csv: CSV downloaden
export_json: JSON downloaden
drawer:
maximize: Maximaliseren
restore: Herstellen
notifications:
title: Meldingen
description: Configureer waar en wanneer je waarschuwingen ontvangt
+6
View File
@@ -216,6 +216,12 @@ analytics:
example_all: Wszystkie logi
example_count: Liczba wierszy
example_group: Liczba według {column}
export: Eksportuj
export_csv: Pobierz CSV
export_json: Pobierz JSON
drawer:
maximize: Maksymalizuj
restore: Przywróć
notifications:
title: Powiadomienia
description: Skonfiguruj gdzie i kiedy otrzymywać alerty
+6
View File
@@ -213,6 +213,12 @@ analytics:
example_all: Todos os logs
example_count: Número de linhas
example_group: Contagem por {column}
export: Exportar
export_csv: Baixar CSV
export_json: Baixar JSON
drawer:
maximize: Maximizar
restore: Restaurar
notifications:
title: Notificações
description: Configure onde e quando receber alertas
+6
View File
@@ -213,6 +213,12 @@ analytics:
example_all: Все логи
example_count: Количество строк
example_group: Количество по {column}
export: Экспорт
export_csv: Скачать CSV
export_json: Скачать JSON
drawer:
maximize: Развернуть
restore: Восстановить
notifications:
title: Уведомления
description: Настройте где и когда получать оповещения
+6
View File
@@ -219,6 +219,12 @@ analytics:
example_all: Vsi dnevniki
example_count: Število vrstic
example_group: Štetje po {column}
export: Izvozi
export_csv: Prenesi CSV
export_json: Prenesi JSON
drawer:
maximize: Maksimiraj
restore: Obnovi
notifications:
title: Obvestila
description: Nastavite kje in kdaj prejemati opozorila
+6
View File
@@ -217,6 +217,12 @@ analytics:
example_all: Tüm günlükler
example_count: Satır sayısı
example_group: "{column} bazında sayı"
export: Dışa aktar
export_csv: CSV indir
export_json: JSON indir
drawer:
maximize: Büyüt
restore: Geri yükle
notifications:
title: Bildirimler
description: Uyarıları nerede ve ne zaman alacağınızı yapılandırın
+6
View File
@@ -216,6 +216,12 @@ analytics:
example_all: 所有日誌
example_count: 資料列數
example_group: 依 {column} 計數
export: 匯出
export_csv: 下載 CSV
export_json: 下載 JSON
drawer:
maximize: 最大化
restore: 還原
notifications:
title: 通知
description: 設定接收警報的位置和時間
+6
View File
@@ -213,6 +213,12 @@ analytics:
example_all: 所有日志
example_count: 行数
example_group: 按 {column} 计数
export: 导出
export_csv: 下载 CSV
export_json: 下载 JSON
drawer:
maximize: 最大化
restore: 还原
notifications:
title: 通知
description: 配置接收警报的位置和时间