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
This commit is contained in:
2026-02-13 13:21:52 -06:00
parent 421b184829
commit da6111bd1f
17 changed files with 1629 additions and 1370 deletions

View File

@@ -0,0 +1,3 @@
export { useDatabaseApi } from './useDatabaseApi'
export { useDataTable, type UseDataTableOptions, type UseDataTableReturn } from './useDataTable'
export { useQueryExecutor } from './useQueryExecutor'

View File

@@ -0,0 +1,158 @@
import { ref, computed, type Ref, type ComputedRef } from 'vue'
export interface UseDataTableOptions {
data: Ref<any[]>
}
export interface UseDataTableReturn {
filter: Ref<string>
hiddenColumns: Ref<Set<string>>
selectedRows: Ref<Set<number>>
copiedCell: Ref<string | null>
copiedAll: Ref<boolean>
filteredData: ComputedRef<any[]>
visibleColumns: ComputedRef<string[]>
allColumns: ComputedRef<string[]>
allRowsSelected: ComputedRef<boolean>
toggleColumn: (column: string) => void
toggleRow: (idx: number) => void
toggleAllRows: () => void
copyCell: (value: any, cellId: string) => Promise<void>
copyAll: () => Promise<void>
reset: () => void
formatValue: (value: any) => string
}
export function useDataTable(options: UseDataTableOptions): UseDataTableReturn {
const { data } = options
const filter = ref('')
const hiddenColumns = ref<Set<string>>(new Set())
const selectedRows = ref<Set<number>>(new Set())
const copiedCell = ref<string | null>(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<string, any> = {}
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
}
}

View File

@@ -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<TableInfo[]>([])
const tableSchema = ref<TableSchema[]>([])
const tableData = ref<any[]>([])
const dbStats = ref<DbStats | null>(null)
const loading = ref(false)
const error = ref<string | null>(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
}
}

View File

@@ -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<any[] | null>(null)
const queryError = ref<string | null>(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
}
}