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:
3
frontend/src/composables/database/index.ts
Normal file
3
frontend/src/composables/database/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { useDatabaseApi } from './useDatabaseApi'
|
||||
export { useDataTable, type UseDataTableOptions, type UseDataTableReturn } from './useDataTable'
|
||||
export { useQueryExecutor } from './useQueryExecutor'
|
||||
158
frontend/src/composables/database/useDataTable.ts
Normal file
158
frontend/src/composables/database/useDataTable.ts
Normal 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
|
||||
}
|
||||
}
|
||||
80
frontend/src/composables/database/useDatabaseApi.ts
Normal file
80
frontend/src/composables/database/useDatabaseApi.ts
Normal 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
|
||||
}
|
||||
}
|
||||
53
frontend/src/composables/database/useQueryExecutor.ts
Normal file
53
frontend/src/composables/database/useQueryExecutor.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user