From da6111bd1f6af9fe12bb3de2e58b7dfe7dad0800 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 13 Feb 2026 13:21:52 -0600 Subject: [PATCH] refactor: Modularize database page into components and composables - Extract types to types/database.ts - Create composables: useDatabaseApi, useDataTable, useQueryExecutor - Create components: DatabaseSidebar, DataTable, FilterBar, SchemaInfo, QueryEditor, QueryColumnsBar, DatabaseStats, TablePagination - Add horizontal scroll to DataTable with sticky checkbox column - Configure @ path alias in vite and tsconfig - Reduce DatabasePage.vue from 1548 to 314 lines --- .../src/components/database/DataTable.vue | 220 +++ .../components/database/DatabaseSidebar.vue | 213 +++ .../src/components/database/DatabaseStats.vue | 184 ++ .../src/components/database/FilterBar.vue | 171 ++ .../components/database/QueryColumnsBar.vue | 83 + .../src/components/database/QueryEditor.vue | 96 ++ .../src/components/database/SchemaInfo.vue | 115 ++ .../components/database/TablePagination.vue | 76 + frontend/src/components/database/index.ts | 8 + frontend/src/composables/database/index.ts | 3 + .../src/composables/database/useDataTable.ts | 158 ++ .../composables/database/useDatabaseApi.ts | 80 + .../composables/database/useQueryExecutor.ts | 53 + frontend/src/pages/DatabasePage.vue | 1507 ++--------------- frontend/src/types/database.ts | 23 + frontend/tsconfig.app.json | 4 + frontend/vite.config.ts | 5 + 17 files changed, 1629 insertions(+), 1370 deletions(-) create mode 100644 frontend/src/components/database/DataTable.vue create mode 100644 frontend/src/components/database/DatabaseSidebar.vue create mode 100644 frontend/src/components/database/DatabaseStats.vue create mode 100644 frontend/src/components/database/FilterBar.vue create mode 100644 frontend/src/components/database/QueryColumnsBar.vue create mode 100644 frontend/src/components/database/QueryEditor.vue create mode 100644 frontend/src/components/database/SchemaInfo.vue create mode 100644 frontend/src/components/database/TablePagination.vue create mode 100644 frontend/src/components/database/index.ts create mode 100644 frontend/src/composables/database/index.ts create mode 100644 frontend/src/composables/database/useDataTable.ts create mode 100644 frontend/src/composables/database/useDatabaseApi.ts create mode 100644 frontend/src/composables/database/useQueryExecutor.ts create mode 100644 frontend/src/types/database.ts diff --git a/frontend/src/components/database/DataTable.vue b/frontend/src/components/database/DataTable.vue new file mode 100644 index 0000000..b35c631 --- /dev/null +++ b/frontend/src/components/database/DataTable.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/frontend/src/components/database/DatabaseSidebar.vue b/frontend/src/components/database/DatabaseSidebar.vue new file mode 100644 index 0000000..5b69988 --- /dev/null +++ b/frontend/src/components/database/DatabaseSidebar.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/frontend/src/components/database/DatabaseStats.vue b/frontend/src/components/database/DatabaseStats.vue new file mode 100644 index 0000000..eb84eda --- /dev/null +++ b/frontend/src/components/database/DatabaseStats.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/frontend/src/components/database/FilterBar.vue b/frontend/src/components/database/FilterBar.vue new file mode 100644 index 0000000..5b894d0 --- /dev/null +++ b/frontend/src/components/database/FilterBar.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/frontend/src/components/database/QueryColumnsBar.vue b/frontend/src/components/database/QueryColumnsBar.vue new file mode 100644 index 0000000..1111ef8 --- /dev/null +++ b/frontend/src/components/database/QueryColumnsBar.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/frontend/src/components/database/QueryEditor.vue b/frontend/src/components/database/QueryEditor.vue new file mode 100644 index 0000000..d6f3763 --- /dev/null +++ b/frontend/src/components/database/QueryEditor.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/frontend/src/components/database/SchemaInfo.vue b/frontend/src/components/database/SchemaInfo.vue new file mode 100644 index 0000000..25f6058 --- /dev/null +++ b/frontend/src/components/database/SchemaInfo.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/frontend/src/components/database/TablePagination.vue b/frontend/src/components/database/TablePagination.vue new file mode 100644 index 0000000..2c7892b --- /dev/null +++ b/frontend/src/components/database/TablePagination.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/frontend/src/components/database/index.ts b/frontend/src/components/database/index.ts new file mode 100644 index 0000000..f021bbf --- /dev/null +++ b/frontend/src/components/database/index.ts @@ -0,0 +1,8 @@ +export { default as DatabaseSidebar } from './DatabaseSidebar.vue' +export { default as DataTable } from './DataTable.vue' +export { default as FilterBar } from './FilterBar.vue' +export { default as SchemaInfo } from './SchemaInfo.vue' +export { default as QueryEditor } from './QueryEditor.vue' +export { default as QueryColumnsBar } from './QueryColumnsBar.vue' +export { default as DatabaseStats } from './DatabaseStats.vue' +export { default as TablePagination } from './TablePagination.vue' diff --git a/frontend/src/composables/database/index.ts b/frontend/src/composables/database/index.ts new file mode 100644 index 0000000..df81773 --- /dev/null +++ b/frontend/src/composables/database/index.ts @@ -0,0 +1,3 @@ +export { useDatabaseApi } from './useDatabaseApi' +export { useDataTable, type UseDataTableOptions, type UseDataTableReturn } from './useDataTable' +export { useQueryExecutor } from './useQueryExecutor' diff --git a/frontend/src/composables/database/useDataTable.ts b/frontend/src/composables/database/useDataTable.ts new file mode 100644 index 0000000..5227166 --- /dev/null +++ b/frontend/src/composables/database/useDataTable.ts @@ -0,0 +1,158 @@ +import { ref, computed, type Ref, type ComputedRef } from 'vue' + +export interface UseDataTableOptions { + data: Ref +} + +export interface UseDataTableReturn { + filter: Ref + hiddenColumns: Ref> + selectedRows: Ref> + copiedCell: Ref + copiedAll: Ref + filteredData: ComputedRef + visibleColumns: ComputedRef + allColumns: ComputedRef + allRowsSelected: ComputedRef + toggleColumn: (column: string) => void + toggleRow: (idx: number) => void + toggleAllRows: () => void + copyCell: (value: any, cellId: string) => Promise + copyAll: () => Promise + reset: () => void + formatValue: (value: any) => string +} + +export function useDataTable(options: UseDataTableOptions): UseDataTableReturn { + const { data } = options + + const filter = ref('') + const hiddenColumns = ref>(new Set()) + const selectedRows = ref>(new Set()) + const copiedCell = ref(null) + const copiedAll = ref(false) + + const allColumns = computed(() => { + if (data.value.length === 0) return [] + return Object.keys(data.value[0]) + }) + + const visibleColumns = computed(() => { + return allColumns.value.filter(col => !hiddenColumns.value.has(col)) + }) + + const filteredData = computed(() => { + if (!filter.value.trim()) return data.value + const search = filter.value.toLowerCase() + return data.value.filter(row => + Object.values(row).some(val => + String(val).toLowerCase().includes(search) + ) + ) + }) + + const allRowsSelected = computed(() => { + if (filteredData.value.length === 0) return false + return filteredData.value.every((_, idx) => selectedRows.value.has(idx)) + }) + + function toggleColumn(column: string) { + if (hiddenColumns.value.has(column)) { + hiddenColumns.value.delete(column) + } else { + hiddenColumns.value.add(column) + } + } + + function toggleRow(idx: number) { + if (selectedRows.value.has(idx)) { + selectedRows.value.delete(idx) + } else { + selectedRows.value.add(idx) + } + } + + function toggleAllRows() { + if (allRowsSelected.value) { + selectedRows.value.clear() + } else { + filteredData.value.forEach((_, idx) => selectedRows.value.add(idx)) + } + } + + function formatValue(value: any): string { + if (value === null) return 'NULL' + if (typeof value === 'object') return JSON.stringify(value) + if (typeof value === 'string' && value.length > 100) { + return value.substring(0, 100) + '...' + } + return String(value) + } + + async function copyCell(value: any, cellId: string) { + const textToCopy = value === null ? 'NULL' : + typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value) + + try { + await navigator.clipboard.writeText(textToCopy) + copiedCell.value = cellId + setTimeout(() => { + copiedCell.value = null + }, 1500) + } catch (e) { + console.error('Failed to copy:', e) + } + } + + async function copyAll() { + const rowsToUse = selectedRows.value.size > 0 + ? filteredData.value.filter((_, idx) => selectedRows.value.has(idx)) + : filteredData.value + + const dataToCopy = rowsToUse.map(row => { + const filtered: Record = {} + visibleColumns.value.forEach(col => { + filtered[col] = row[col] + }) + return filtered + }) + + if (dataToCopy.length === 0) return + + try { + const jsonText = JSON.stringify(dataToCopy, null, 2) + await navigator.clipboard.writeText(jsonText) + copiedAll.value = true + setTimeout(() => { + copiedAll.value = false + }, 2000) + } catch (e) { + console.error('Failed to copy:', e) + } + } + + function reset() { + filter.value = '' + hiddenColumns.value.clear() + selectedRows.value.clear() + } + + return { + filter, + hiddenColumns, + selectedRows, + copiedCell, + copiedAll, + filteredData, + visibleColumns, + allColumns, + allRowsSelected, + toggleColumn, + toggleRow, + toggleAllRows, + copyCell, + copyAll, + reset, + formatValue + } +} diff --git a/frontend/src/composables/database/useDatabaseApi.ts b/frontend/src/composables/database/useDatabaseApi.ts new file mode 100644 index 0000000..d54b55c --- /dev/null +++ b/frontend/src/composables/database/useDatabaseApi.ts @@ -0,0 +1,80 @@ +import { ref } from 'vue' +import type { TableInfo, TableSchema, DbStats } from '@/types/database' + +const API_BASE = 'http://localhost:4101/api/database' + +export function useDatabaseApi() { + const tables = ref([]) + const tableSchema = ref([]) + const tableData = ref([]) + const dbStats = ref(null) + const loading = ref(false) + const error = ref(null) + const totalRecords = ref(0) + + async function fetchTables() { + loading.value = true + error.value = null + try { + const res = await fetch(`${API_BASE}/tables`) + if (!res.ok) throw new Error('Failed to fetch tables') + tables.value = await res.json() + } catch (e: any) { + error.value = e.message + } finally { + loading.value = false + } + } + + async function fetchDbStats() { + try { + const res = await fetch(`${API_BASE}/stats`) + if (!res.ok) throw new Error('Failed to fetch stats') + dbStats.value = await res.json() + } catch (e: any) { + console.error('Error fetching stats:', e) + } + } + + async function fetchTableSchema(tableName: string) { + try { + const res = await fetch(`${API_BASE}/tables/${tableName}/schema`) + if (!res.ok) throw new Error('Failed to fetch schema') + tableSchema.value = await res.json() + } catch (e: any) { + console.error('Error fetching schema:', e) + tableSchema.value = [] + } + } + + async function fetchTableData(tableName: string, page: number, pageSize: number) { + loading.value = true + try { + const offset = (page - 1) * pageSize + const res = await fetch(`${API_BASE}/tables/${tableName}/data?limit=${pageSize}&offset=${offset}`) + if (!res.ok) throw new Error('Failed to fetch data') + const result = await res.json() + tableData.value = result.rows + totalRecords.value = result.total + } catch (e: any) { + console.error('Error fetching data:', e) + tableData.value = [] + } finally { + loading.value = false + } + } + + return { + tables, + tableSchema, + tableData, + dbStats, + loading, + error, + totalRecords, + fetchTables, + fetchDbStats, + fetchTableSchema, + fetchTableData + } +} diff --git a/frontend/src/composables/database/useQueryExecutor.ts b/frontend/src/composables/database/useQueryExecutor.ts new file mode 100644 index 0000000..500c8dd --- /dev/null +++ b/frontend/src/composables/database/useQueryExecutor.ts @@ -0,0 +1,53 @@ +import { ref } from 'vue' + +const API_BASE = 'http://localhost:4101/api/database' + +export function useQueryExecutor() { + const queryText = ref('') + const queryResult = ref(null) + const queryError = ref(null) + const queryLoading = ref(false) + + async function executeQuery() { + if (!queryText.value.trim()) return + + queryLoading.value = true + queryError.value = null + queryResult.value = null + + try { + const res = await fetch(`${API_BASE}/query`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: queryText.value }) + }) + + const result = await res.json() + + if (!res.ok) { + queryError.value = result.error || 'Query failed' + } else { + queryResult.value = result.rows + } + } catch (e: any) { + queryError.value = e.message + } finally { + queryLoading.value = false + } + } + + function reset() { + queryText.value = '' + queryResult.value = null + queryError.value = null + } + + return { + queryText, + queryResult, + queryError, + queryLoading, + executeQuery, + reset + } +} diff --git a/frontend/src/pages/DatabasePage.vue b/frontend/src/pages/DatabasePage.vue index 4dd040a..d2d9185 100644 --- a/frontend/src/pages/DatabasePage.vue +++ b/frontend/src/pages/DatabasePage.vue @@ -1,298 +1,70 @@