feat(query): sql workspace conveniences per opsxdb patterns

- generic ContextMenu component (viewport flip, esc/outside close)
- SchemaSidebar table right-click menu: preview data, generate
  SELECT/INSERT/UPDATE/DELETE templates with pk-aware WHERE, copy table
  name; double-click table runs preview SELECT; column right-click
  copy/insert; dialect-aware quoting; preview SELECT carries no LIMIT
- SqlEditor selection-first execution (Mod-Enter runs selection else
  statement at cursor via quote/comment-aware splitter); pagination
  replays last executed statement
- query history persisted server-side: drawer fetches query-histories
  with keyword/mine/favorites filters, star toggle (new PUT favorite +
  mine filter endpoints), insert and insert-and-run
- custom templates in localStorage alongside built-ins
- unit tests: genSql + statement splitter (28 tests green)
This commit is contained in:
Logan.Li committed 2026-10-26 18:53:02 +08:00
1 parent ff539b47e0
commit f6d636fa44
16 files changed
+1392 -457

No files matched your search

+29 -3
View File
@@ -9,6 +9,7 @@ import (
"sql-platform/internal/ghost"
"sql-platform/internal/handler/resp"
"sql-platform/internal/middleware"
"sql-platform/internal/model"
"github.com/gin-gonic/gin"
@@ -445,11 +446,36 @@ func ListAnnouncements(c *gin.Context) {
// GET /api/v1/query-histories
func ListQueryHistories(c *gin.Context) {
_, _, _ = c.GetInt("user_id"), c.GetString("username"), c.GetString("role")
userID, _, _ := middleware.GetCurrentUser(c)
q := model.DB.Order("id DESC").Limit(100)
if c.Query("mine") == "1" {
q = q.Where("user_id = ?", userID)
}
var histories []model.QueryHistory
model.DB.Order("id DESC").Limit(100).Find(&histories)
c.JSON(http.StatusOK, histories)
q.Find(&histories)
resp.OK(c, histories)
}
// PUT /api/v1/query-histories/:id/favorite — 收藏/取消收藏
func ToggleQueryHistoryFavorite(c *gin.Context) {
userID, _, _ := middleware.GetCurrentUser(c)
id := c.Param("id")
var history model.QueryHistory
if err := model.DB.First(&history, id).Error; err != nil {
resp.NotFound(c, "History not found")
return
}
if history.UserID != userID {
resp.Forbidden(c, "只能收藏自己的查询历史")
return
}
if err := model.DB.Model(&history).Update("is_favorite", !history.IsFavorite).Error; err != nil {
resp.InternalErr(c, err)
return
}
resp.OK(c, gin.H{"id": history.ID, "is_favorite": !history.IsFavorite})
}
// GET /api/v1/settings — returns settings grouped by `group` field
+1
View File
@@ -199,6 +199,7 @@ func Setup(r *gin.Engine) {
}
auth.GET("/announcements", handler.ListAnnouncements)
auth.GET("/query-histories", handler.ListQueryHistories)
auth.PUT("/query-histories/:id/favorite", handler.ToggleQueryHistoryFavorite)
// Notifications CRUD (replaces old channels/templates endpoints)
auth.GET("/notifications", handler.ListNotifications)
auth.POST("/notifications", handler.CreateNotification)