前端:重构 Web UI 代码结构

- 添加 Import Map 支持 Lit 和本地模块的简洁导入
- 创建统一的 API 管理模块 (common/api.js)
- 重命名 styles/ 为 common/ 目录
- 修复分页时列选择被重置的问题
- 将 app.js 重命名为 main.js
- 所有导入路径使用 ~ 别名映射
This commit is contained in:
2025-10-09 15:53:58 +08:00
parent 8019f2d794
commit 4aade1cff1
20 changed files with 1349 additions and 421 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,276 @@
package srdb
import (
"testing"
)
// TestPickL0MergeContinuity 测试 L0 合并任务的连续性
func TestPickL0MergeContinuity(t *testing.T) {
tmpDir := t.TempDir()
manifestDir := tmpDir
versionSet, err := NewVersionSet(manifestDir)
if err != nil {
t.Fatal(err)
}
defer versionSet.Close()
picker := NewPicker()
// 创建混合大小的文件:小-大-小-小
// 这是触发 bug 的场景
edit := NewVersionEdit()
// 文件1: 29MB (小文件)
edit.AddFile(&FileMetadata{
FileNumber: 1,
Level: 0,
FileSize: 29 * 1024 * 1024,
MinKey: 1,
MaxKey: 100,
RowCount: 100,
})
// 文件2: 36MB (大文件)
edit.AddFile(&FileMetadata{
FileNumber: 2,
Level: 0,
FileSize: 36 * 1024 * 1024,
MinKey: 101,
MaxKey: 200,
RowCount: 100,
})
// 文件3: 8MB (小文件)
edit.AddFile(&FileMetadata{
FileNumber: 3,
Level: 0,
FileSize: 8 * 1024 * 1024,
MinKey: 201,
MaxKey: 300,
RowCount: 100,
})
// 文件4: 15MB (小文件)
edit.AddFile(&FileMetadata{
FileNumber: 4,
Level: 0,
FileSize: 15 * 1024 * 1024,
MinKey: 301,
MaxKey: 400,
RowCount: 100,
})
edit.SetNextFileNumber(5)
err = versionSet.LogAndApply(edit)
if err != nil {
t.Fatal(err)
}
version := versionSet.GetCurrent()
// 测试 Stage 0: L0 合并任务
t.Log("=== 测试 Stage 0: L0 合并 ===")
tasks := picker.pickL0MergeTasks(version)
if len(tasks) == 0 {
t.Fatal("Expected L0 merge tasks")
}
t.Logf("找到 %d 个合并任务", len(tasks))
// 验证任务应该只有1个任务包含文件3和文件4
// 文件1是单个小文件不合并len > 1 才合并)
// 文件2是大文件跳过
// 文件3+文件4是连续的2个小文件应该合并
if len(tasks) != 1 {
t.Errorf("Expected 1 task, got %d", len(tasks))
for i, task := range tasks {
t.Logf("Task %d: %d files", i+1, len(task.InputFiles))
for _, f := range task.InputFiles {
t.Logf(" - File %d", f.FileNumber)
}
}
}
task1 := tasks[0]
if len(task1.InputFiles) != 2 {
t.Errorf("Task 1: expected 2 files, got %d", len(task1.InputFiles))
}
if task1.InputFiles[0].FileNumber != 3 || task1.InputFiles[1].FileNumber != 4 {
t.Errorf("Task 1: expected files 3,4, got %d,%d",
task1.InputFiles[0].FileNumber, task1.InputFiles[1].FileNumber)
}
t.Logf("✓ 合并任务: 文件3+文件4 (连续的2个小文件)")
t.Logf("✓ 文件1 (单个小文件) 不合并,留给升级阶段")
// 验证 seq 范围连续性
// 任务1: seq 201-400 (文件3+文件4)
// 文件1seq 1-100, 单个小文件)留给升级阶段
// 文件2seq 101-200, 大文件)留给 Stage 1
if task1.InputFiles[0].MinKey != 201 || task1.InputFiles[1].MaxKey != 400 {
t.Errorf("Task 1 seq range incorrect: [%d, %d]",
task1.InputFiles[0].MinKey, task1.InputFiles[1].MaxKey)
}
t.Logf("✓ Seq 范围正确任务1 [201-400]")
// 测试 Stage 1: L0 升级任务
t.Log("=== 测试 Stage 1: L0 升级 ===")
upgradeTasks := picker.pickL0UpgradeTasks(version)
if len(upgradeTasks) == 0 {
t.Fatal("Expected L0 upgrade tasks")
}
// 应该有1个任务以文件2大文件为中心搭配周围的小文件
// 文件2向左收集文件1向右收集文件3和文件4
// 总共文件1 (29MB) + 文件2 (36MB) + 文件3 (8MB) + 文件4 (15MB) = 88MB
if len(upgradeTasks) != 1 {
t.Errorf("Expected 1 upgrade task, got %d", len(upgradeTasks))
}
upgradeTask := upgradeTasks[0]
// 应该包含所有4个文件
if len(upgradeTask.InputFiles) != 4 {
t.Errorf("Upgrade task: expected 4 files, got %d", len(upgradeTask.InputFiles))
for i, f := range upgradeTask.InputFiles {
t.Logf(" File %d: %d", i+1, f.FileNumber)
}
}
// 验证文件顺序1, 2, 3, 4
expectedFiles := []int64{1, 2, 3, 4}
for i, expected := range expectedFiles {
if upgradeTask.InputFiles[i].FileNumber != expected {
t.Errorf("Upgrade task file %d: expected %d, got %d",
i, expected, upgradeTask.InputFiles[i].FileNumber)
}
}
if upgradeTask.OutputLevel != 1 {
t.Errorf("Upgrade task: expected OutputLevel 1, got %d", upgradeTask.OutputLevel)
}
t.Logf("✓ 升级任务: 文件1+文件2+文件3+文件4 (以大文件为中心,搭配周围小文件) → L1")
t.Log("=== 连续性测试通过 ===")
}
// TestPickL0UpgradeContinuity 测试 L0 升级任务的连续性
func TestPickL0UpgradeContinuity(t *testing.T) {
tmpDir := t.TempDir()
manifestDir := tmpDir
versionSet, err := NewVersionSet(manifestDir)
if err != nil {
t.Fatal(err)
}
defer versionSet.Close()
picker := NewPicker()
// 创建混合大小的文件:大-小-大-大
edit := NewVersionEdit()
// 文件1: 40MB (大文件)
edit.AddFile(&FileMetadata{
FileNumber: 1,
Level: 0,
FileSize: 40 * 1024 * 1024,
MinKey: 1,
MaxKey: 100,
RowCount: 100,
})
// 文件2: 20MB (小文件)
edit.AddFile(&FileMetadata{
FileNumber: 2,
Level: 0,
FileSize: 20 * 1024 * 1024,
MinKey: 101,
MaxKey: 200,
RowCount: 100,
})
// 文件3: 50MB (大文件)
edit.AddFile(&FileMetadata{
FileNumber: 3,
Level: 0,
FileSize: 50 * 1024 * 1024,
MinKey: 201,
MaxKey: 300,
RowCount: 100,
})
// 文件4: 45MB (大文件)
edit.AddFile(&FileMetadata{
FileNumber: 4,
Level: 0,
FileSize: 45 * 1024 * 1024,
MinKey: 301,
MaxKey: 400,
RowCount: 100,
})
edit.SetNextFileNumber(5)
err = versionSet.LogAndApply(edit)
if err != nil {
t.Fatal(err)
}
version := versionSet.GetCurrent()
// 测试 L0 升级任务
t.Log("=== 测试 L0 升级任务连续性 ===")
tasks := picker.pickL0UpgradeTasks(version)
if len(tasks) == 0 {
t.Fatal("Expected L0 upgrade tasks")
}
t.Logf("找到 %d 个升级任务", len(tasks))
// 验证任务1应该包含所有4个文件以大文件为锚点搭配周围文件
// 文件1大文件作为锚点 → 向左无文件 → 向右收集文件2+文件3+文件4
// 总大小40+20+50+45 = 155MB < 256MB符合 L1 限制
task1 := tasks[0]
expectedFileCount := 4
if len(task1.InputFiles) != expectedFileCount {
t.Errorf("Task 1: expected %d files, got %d", expectedFileCount, len(task1.InputFiles))
for i, f := range task1.InputFiles {
t.Logf(" File %d: %d", i+1, f.FileNumber)
}
}
// 验证文件顺序1, 2, 3, 4
expectedFiles := []int64{1, 2, 3, 4}
for i, expected := range expectedFiles {
if i >= len(task1.InputFiles) {
break
}
if task1.InputFiles[i].FileNumber != expected {
t.Errorf("Task 1 file %d: expected %d, got %d",
i, expected, task1.InputFiles[i].FileNumber)
}
}
t.Logf("✓ Task 1: 文件1+文件2+文件3+文件4 (以大文件为锚点搭配周围文件总155MB < 256MB)")
// 只应该有1个任务所有文件都被收集了
if len(tasks) != 1 {
t.Errorf("Expected 1 task (all files collected), got %d", len(tasks))
for i, task := range tasks {
t.Logf("Task %d: %d files", i+1, len(task.InputFiles))
for _, f := range task.InputFiles {
t.Logf(" - File %d", f.FileNumber)
}
}
}
// 验证所有任务的 OutputLevel 都是 1
for i, task := range tasks {
if task.OutputLevel != 1 {
t.Errorf("Task %d: expected OutputLevel 1, got %d", i+1, task.OutputLevel)
}
}
t.Logf("✓ 所有任务都升级到 L1")
t.Log("=== 升级任务连续性测试通过 ===")
}

220
compaction_stage_test.go Normal file
View File

@@ -0,0 +1,220 @@
package srdb
import (
"testing"
)
// TestPickerStageRotation 测试 Picker 的阶段轮换机制
func TestPickerStageRotation(t *testing.T) {
// 创建临时目录
tmpDir := t.TempDir()
manifestDir := tmpDir
// 创建 VersionSet
versionSet, err := NewVersionSet(manifestDir)
if err != nil {
t.Fatal(err)
}
defer versionSet.Close()
// 创建 Picker
picker := NewPicker()
// 初始阶段应该是 L0
if stage := picker.GetCurrentStage(); stage != 0 {
t.Errorf("Initial stage should be 0 (L0), got %d", stage)
}
// 添加 L0 文件(触发 L0 compaction
edit := NewVersionEdit()
for i := 0; i < 10; i++ {
edit.AddFile(&FileMetadata{
FileNumber: int64(i + 1),
Level: 0,
FileSize: 10 * 1024 * 1024, // 10MB each
MinKey: int64(i * 100),
MaxKey: int64((i+1)*100 - 1),
RowCount: 100,
})
}
edit.SetNextFileNumber(11)
err = versionSet.LogAndApply(edit)
if err != nil {
t.Fatal(err)
}
version := versionSet.GetCurrent()
// 第1次调用应该返回 L0 任务,然后推进到 L1
t.Log("=== 第1次调用 PickCompaction ===")
tasks1 := picker.PickCompaction(version)
if len(tasks1) == 0 {
t.Error("Expected L0 tasks on first call")
}
for _, task := range tasks1 {
if task.Level != 0 {
t.Errorf("Expected L0 task, got L%d", task.Level)
}
}
if stage := picker.GetCurrentStage(); stage != 1 {
t.Errorf("After L0 tasks, stage should be 1 (L1), got %d", stage)
}
t.Logf("✓ Returned %d L0 tasks, stage advanced to L1", len(tasks1))
// 第2次调用应该尝试 Stage 1 (L0-upgrade没有大文件)
t.Log("=== 第2次调用 PickCompaction ===")
tasks2 := picker.PickCompaction(version)
if len(tasks2) == 0 {
t.Log("✓ Stage 1 (L0-upgrade) has no tasks")
}
// 此时 stage 应该已经循环(尝试了 Stage 1→2→3→0...
if stage := picker.GetCurrentStage(); stage >= 0 {
t.Logf("After trying, current stage is %d", stage)
}
// 现在添加 L1 文件
edit2 := NewVersionEdit()
for i := 0; i < 20; i++ {
edit2.AddFile(&FileMetadata{
FileNumber: int64(100 + i + 1),
Level: 1,
FileSize: 20 * 1024 * 1024, // 20MB each
MinKey: int64(i * 200),
MaxKey: int64((i+1)*200 - 1),
RowCount: 200,
})
}
edit2.SetNextFileNumber(121)
err = versionSet.LogAndApply(edit2)
if err != nil {
t.Fatal(err)
}
version2 := versionSet.GetCurrent()
// 现在可能需要多次调用才能到达 Stage 2 (L1-upgrade)
// 因为要经过 Stage 1 (L0-upgrade) 和 Stage 0 (L0-merge)
t.Log("=== 多次调用 PickCompaction 直到找到 L1 任务 ===")
var tasks3 []*CompactionTask
for i := 0; i < 8; i++ { // 最多尝试两轮4个阶段×2
tasks3 = picker.PickCompaction(version2)
if len(tasks3) > 0 && tasks3[0].Level == 1 {
t.Logf("✓ Found %d L1 tasks after %d attempts", len(tasks3), i+1)
break
}
}
if len(tasks3) == 0 || tasks3[0].Level != 1 {
t.Error("Expected to find L1 tasks within 8 attempts")
}
t.Log("=== Stage rotation test passed ===")
}
// TestPickerStageWithMultipleLevels 测试多层级同时有任务时的阶段轮换
func TestPickerStageWithMultipleLevels(t *testing.T) {
tmpDir := t.TempDir()
manifestDir := tmpDir
versionSet, err := NewVersionSet(manifestDir)
if err != nil {
t.Fatal(err)
}
defer versionSet.Close()
picker := NewPicker()
// 同时添加 L0、L1、L2 文件
edit := NewVersionEdit()
// L0 小文件: 5 files × 10MB = 50MB (应该触发 Stage 0: L0-merge)
for i := 0; i < 5; i++ {
edit.AddFile(&FileMetadata{
FileNumber: int64(i + 1),
Level: 0,
FileSize: 10 * 1024 * 1024,
MinKey: int64(i * 100),
MaxKey: int64((i+1)*100 - 1),
RowCount: 100,
})
}
// L0 大文件: 5 files × 40MB = 200MB (应该触发 Stage 1: L0-upgrade)
for i := 0; i < 5; i++ {
edit.AddFile(&FileMetadata{
FileNumber: int64(10 + i + 1),
Level: 0,
FileSize: 40 * 1024 * 1024,
MinKey: int64((i+5) * 100),
MaxKey: int64((i+6)*100 - 1),
RowCount: 100,
})
}
// L1: 20 files × 20MB = 400MB (应该触发 Stage 2: L1-upgrade256MB阈值)
for i := 0; i < 20; i++ {
edit.AddFile(&FileMetadata{
FileNumber: int64(100 + i + 1),
Level: 1,
FileSize: 20 * 1024 * 1024,
MinKey: int64(i * 200),
MaxKey: int64((i+1)*200 - 1),
RowCount: 200,
})
}
// L2: 10 files × 150MB = 1500MB (应该触发 Stage 3: L2-upgrade1GB阈值)
for i := 0; i < 10; i++ {
edit.AddFile(&FileMetadata{
FileNumber: int64(200 + i + 1),
Level: 2,
FileSize: 150 * 1024 * 1024,
MinKey: int64(i * 300),
MaxKey: int64((i+1)*300 - 1),
RowCount: 300,
})
}
edit.SetNextFileNumber(301)
err = versionSet.LogAndApply(edit)
if err != nil {
t.Fatal(err)
}
version := versionSet.GetCurrent()
// 验证阶段按顺序执行Stage 0→1→2→3→0→1→2→3
expectedStages := []struct {
stage int
name string
level int
}{
{0, "L0-merge", 0},
{1, "L0-upgrade", 0},
{2, "L1-upgrade", 1},
{3, "L2-upgrade", 2},
{0, "L0-merge", 0},
{1, "L0-upgrade", 0},
{2, "L1-upgrade", 1},
{3, "L2-upgrade", 2},
}
for i, expected := range expectedStages {
t.Logf("=== 第%d次调用 PickCompaction (期望 Stage %d: %s) ===", i+1, expected.stage, expected.name)
tasks := picker.PickCompaction(version)
if len(tasks) == 0 {
t.Errorf("Call %d: Expected tasks from Stage %d (%s), got no tasks", i+1, expected.stage, expected.name)
continue
}
actualLevel := tasks[0].Level
if actualLevel != expected.level {
t.Errorf("Call %d: Expected L%d tasks, got L%d tasks", i+1, expected.level, actualLevel)
} else {
t.Logf("✓ Call %d: Got %d tasks from L%d (Stage %d: %s) as expected",
i+1, len(tasks), actualLevel, expected.stage, expected.name)
}
}
t.Log("=== Multi-level stage rotation test passed ===")
}

View File

@@ -143,12 +143,14 @@ func TestCompactionBasic(t *testing.T) {
t.Errorf("Expected L0 compaction, got L%d", task.Level)
}
if task.OutputLevel != 1 {
t.Errorf("Expected output to L1, got L%d", task.OutputLevel)
// 注意L0 compaction 任务的 OutputLevel 设为 0建议层级
// 实际层级由 determineLevel 根据合并后的文件大小决定
if task.OutputLevel != 0 {
t.Errorf("Expected output to L0 (suggested), got L%d", task.OutputLevel)
}
t.Logf("Found %d compaction tasks", len(tasks))
t.Logf("First task: L%d -> L%d, %d files", task.Level, task.OutputLevel, len(task.InputFiles))
t.Logf("First task: L%d -> L%d, %d files (determineLevel will decide actual level)", task.Level, task.OutputLevel, len(task.InputFiles))
// 清理
reader1.Close()
@@ -193,12 +195,18 @@ func TestPickerLevelScore(t *testing.T) {
// 计算 L0 的得分
score := picker.GetLevelScore(version, 0)
t.Logf("L0 score: %.2f (files: %d, limit: %d)", score, version.GetLevelFileCount(0), picker.levelFileLimits[0])
// L0 有 3 个文件,限制是 4得分应该是 0.75
expectedScore := 3.0 / 4.0
// L0 有 3 个文件,每个 1MB总共 3MB
// 下一级L1的限制是 256MB
// 得分应该是 3MB / 256MB = 0.01171875
totalSize := int64(3 * 1024 * 1024) // 3MB
expectedScore := float64(totalSize) / float64(level1SizeLimit)
t.Logf("L0 score: %.4f (files: %d, total: %d bytes, next level limit: %d)",
score, version.GetLevelFileCount(0), totalSize, level1SizeLimit)
if score != expectedScore {
t.Errorf("Expected L0 score %.2f, got %.2f", expectedScore, score)
t.Errorf("Expected L0 score %.4f, got %.4f", expectedScore, score)
}
}
@@ -544,9 +552,9 @@ func TestCompactionQueryOrder(t *testing.T) {
stats := table.GetCompactionManager().GetLevelStats()
t.Logf("Compaction 统计:")
for _, levelStat := range stats {
level := levelStat["level"].(int)
fileCount := levelStat["file_count"].(int)
totalSize := levelStat["total_size"].(int64)
level := levelStat.Level
fileCount := levelStat.FileCount
totalSize := levelStat.TotalSize
if fileCount > 0 {
t.Logf(" L%d: %d 个文件, %.2f MB", level, fileCount, float64(totalSize)/(1024*1024))
}

View File

@@ -948,10 +948,10 @@ func TestTableWithCompaction(t *testing.T) {
// 获取 Level 统计信息
levelStats := table.compactionManager.GetLevelStats()
for _, stat := range levelStats {
level := stat["level"].(int)
fileCount := stat["file_count"].(int)
totalSize := stat["total_size"].(int64)
score := stat["score"].(float64)
level := stat.Level
fileCount := stat.FileCount
totalSize := stat.TotalSize
score := stat.Score
if fileCount > 0 {
t.Logf("L%d: %d files, %d bytes, score: %.2f", level, fileCount, totalSize, score)

View File

@@ -16,7 +16,7 @@ import (
// FileMetadata SST 文件元数据
type FileMetadata struct {
FileNumber int64 // 文件编号
Level int // 所在层级 (0-6)
Level int // 所在层级 (0-3)
FileSize int64 // 文件大小
MinKey int64 // 最小 key
MaxKey int64 // 最大 key
@@ -24,12 +24,12 @@ type FileMetadata struct {
}
const (
NumLevels = 7 // L0-L6
NumLevels = 4 // L0-L3
)
// Version 数据库的一个版本快照
type Version struct {
// 分层存储 SST 文件 (L0-L6)
// 分层存储 SST 文件 (L0-L3)
Levels [NumLevels][]*FileMetadata
// 下一个文件编号

View File

@@ -11,6 +11,17 @@
rel="stylesheet"
/>
<link rel="stylesheet" href="/static/css/styles.css" />
<!-- Import Map for Lit and local modules -->
<script type="importmap">
{
"imports": {
"lit": "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js",
"lit/": "https://cdn.jsdelivr.net/gh/lit/dist@3/",
"~/": "/static/js/"
}
}
</script>
</head>
<body>
<!-- 应用容器 -->
@@ -20,6 +31,6 @@
<srdb-modal-dialog></srdb-modal-dialog>
<!-- 加载 Lit 组件 -->
<script type="module" src="/static/js/app.js"></script>
<script type="module" src="/static/js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,174 @@
/**
* API 请求管理模块
* 统一管理所有后端接口请求
*/
const API_BASE = '/api';
/**
* 通用请求处理函数
* @param {string} url - 请求 URL
* @param {RequestInit} options - fetch 选项
* @returns {Promise<any>}
*/
async function request(url, options = {}) {
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
error.response = response;
throw error;
}
return await response.json();
} catch (error) {
console.error('API request failed:', url, error);
throw error;
}
}
/**
* 表相关 API
*/
export const tableAPI = {
/**
* 获取所有表列表
* @returns {Promise<Array>}
*/
async list() {
return request(`${API_BASE}/tables`);
},
/**
* 获取表的 Schema
* @param {string} tableName - 表名
* @returns {Promise<Object>}
*/
async getSchema(tableName) {
return request(`${API_BASE}/tables/${tableName}/schema`);
},
/**
* 获取表数据(分页)
* @param {string} tableName - 表名
* @param {Object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.pageSize - 每页大小
* @param {string} params.select - 选择的列(逗号分隔)
* @returns {Promise<Object>}
*/
async getData(tableName, { page = 1, pageSize = 20, select = '' } = {}) {
const params = new URLSearchParams({
page: page.toString(),
pageSize: pageSize.toString(),
});
if (select) {
params.append('select', select);
}
return request(`${API_BASE}/tables/${tableName}/data?${params}`);
},
/**
* 获取单行数据详情
* @param {string} tableName - 表名
* @param {number} seq - 序列号
* @returns {Promise<Object>}
*/
async getRow(tableName, seq) {
return request(`${API_BASE}/tables/${tableName}/data/${seq}`);
},
/**
* 获取表的 Manifest 信息
* @param {string} tableName - 表名
* @returns {Promise<Object>}
*/
async getManifest(tableName) {
return request(`${API_BASE}/tables/${tableName}/manifest`);
},
/**
* 插入数据
* @param {string} tableName - 表名
* @param {Object} data - 数据对象
* @returns {Promise<Object>}
*/
async insert(tableName, data) {
return request(`${API_BASE}/tables/${tableName}/data`, {
method: 'POST',
body: JSON.stringify(data),
});
},
/**
* 批量插入数据
* @param {string} tableName - 表名
* @param {Array<Object>} data - 数据数组
* @returns {Promise<Object>}
*/
async batchInsert(tableName, data) {
return request(`${API_BASE}/tables/${tableName}/data/batch`, {
method: 'POST',
body: JSON.stringify(data),
});
},
/**
* 删除表
* @param {string} tableName - 表名
* @returns {Promise<Object>}
*/
async delete(tableName) {
return request(`${API_BASE}/tables/${tableName}`, {
method: 'DELETE',
});
},
/**
* 获取表统计信息
* @param {string} tableName - 表名
* @returns {Promise<Object>}
*/
async getStats(tableName) {
return request(`${API_BASE}/tables/${tableName}/stats`);
},
};
/**
* 数据库相关 API
*/
export const databaseAPI = {
/**
* 获取数据库信息
* @returns {Promise<Object>}
*/
async getInfo() {
return request(`${API_BASE}/database/info`);
},
/**
* 获取数据库统计信息
* @returns {Promise<Object>}
*/
async getStats() {
return request(`${API_BASE}/database/stats`);
},
};
/**
* 导出默认 API 对象
*/
export default {
table: tableAPI,
database: databaseAPI,
};

View File

@@ -1,4 +1,4 @@
import { css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { css } from 'lit';
// 共享的基础样式
export const sharedStyles = css`

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
export class AppContainer extends LitElement {
static properties = {

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
export class Badge extends LitElement {
static properties = {

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
export class DataView extends LitElement {
static properties = {

View File

@@ -1,10 +1,9 @@
import { LitElement, html, css, svg } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { LitElement, html, css } from 'lit';
export class FieldIcon extends LitElement {
static properties = {
indexed: { type: Boolean }
};
static styles = css`
:host {
display: inline-flex;

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
export class ManifestView extends LitElement {
static properties = {

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
export class ModalDialog extends LitElement {
static properties = {

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
export class PageHeader extends LitElement {
static properties = {
@@ -247,11 +247,11 @@ export class PageHeader extends LitElement {
>
<span>Data</span>
</button>
<button
<button
class="view-tab ${this.view === 'manifest' ? 'active' : ''}"
@click=${() => this.switchView('manifest')}
>
<span>Manifest / LSM-Tree</span>
<span>Manifest / Storage Layers</span>
</button>
<button class="refresh-btn" @click=${this.refreshView} title="Refresh current view">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">

View File

@@ -1,5 +1,6 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
import { tableAPI } from '~/common/api.js';
export class TableList extends LitElement {
static properties = {
@@ -209,9 +210,7 @@ export class TableList extends LitElement {
async loadTables() {
try {
const response = await fetch('/api/tables');
if (!response.ok) throw new Error('Failed to load tables');
this.tables = await response.json();
this.tables = await tableAPI.list();
} catch (error) {
console.error('Error loading tables:', error);
}

View File

@@ -1,5 +1,6 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
import { tableAPI } from '~/common/api.js';
export class TableView extends LitElement {
static properties = {
@@ -127,13 +128,20 @@ export class TableView extends LitElement {
try {
// Load schema
const schemaResponse = await fetch(`/api/tables/${this.tableName}/schema`);
if (!schemaResponse.ok) throw new Error('Failed to load schema');
this.schema = await schemaResponse.json();
this.schema = await tableAPI.getSchema(this.tableName);
// Initialize selected columns (all by default)
// Initialize selected columns from localStorage or all by default
if (this.schema.fields) {
this.selectedColumns = this.schema.fields.map(f => f.name);
const saved = this.loadSelectedColumns();
if (saved && saved.length > 0) {
// 验证保存的列是否仍然存在于当前 schema 中
const validColumns = saved.filter(col =>
this.schema.fields.some(field => field.name === col)
);
this.selectedColumns = validColumns.length > 0 ? validColumns : this.schema.fields.map(f => f.name);
} else {
this.selectedColumns = this.schema.fields.map(f => f.name);
}
}
if (this.view === 'data') {
@@ -149,24 +157,28 @@ export class TableView extends LitElement {
}
async loadTableData() {
const selectParam = this.selectedColumns.join(',');
const url = `/api/tables/${this.tableName}/data?page=${this.page}&pageSize=${this.pageSize}&select=${selectParam}`;
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to load table data');
this.tableData = await response.json();
this.tableData = await tableAPI.getData(this.tableName, {
page: this.page,
pageSize: this.pageSize,
select: this.selectedColumns.join(',')
});
}
async loadManifestData() {
const response = await fetch(`/api/tables/${this.tableName}/manifest`);
if (!response.ok) throw new Error('Failed to load manifest data');
this.manifestData = await response.json();
this.manifestData = await tableAPI.getManifest(this.tableName);
}
switchView(newView) {
this.view = newView;
}
loadSelectedColumns() {
if (!this.tableName) return null;
const key = `srdb_columns_${this.tableName}`;
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : null;
}
toggleColumn(columnName) {
const index = this.selectedColumns.indexOf(columnName);
if (index > -1) {

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles } from '~/common/shared-styles.js';
export class ThemeToggle extends LitElement {
static properties = {

View File

@@ -1,13 +1,14 @@
import './components/app.js';
import './components/table-list.js';
import './components/table-view.js';
import './components/modal-dialog.js';
import './components/theme-toggle.js';
import './components/badge.js';
import './components/field-icon.js';
import './components/data-view.js';
import './components/manifest-view.js';
import './components/page-header.js';
import '~/components/app.js';
import '~/components/table-list.js';
import '~/components/table-view.js';
import '~/components/modal-dialog.js';
import '~/components/theme-toggle.js';
import '~/components/badge.js';
import '~/components/field-icon.js';
import '~/components/data-view.js';
import '~/components/manifest-view.js';
import '~/components/page-header.js';
import { tableAPI } from '~/common/api.js';
class App {
constructor() {
@@ -78,10 +79,7 @@ class App {
async showRowDetail(tableName, seq) {
try {
const response = await fetch(`/api/tables/${tableName}/data/${seq}`);
if (!response.ok) throw new Error('Failed to load row detail');
const data = await response.json();
const data = await tableAPI.getRow(tableName, seq);
const content = JSON.stringify(data, null, 2);
this.modal.title = `Row Detail - Seq: ${seq}`;