diff --git a/api/handler/user.go b/api/handler/user.go index 83d731004..e72a7a878 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -702,6 +702,7 @@ func (h *UserHandler) GetFinetuneInstances(ctx *gin.Context) { // @Param per query int false "per" default(50) // @Param page query int false "page index" default(1) // @Param current_user query string false "current user" +// @Param search query string false "search by path or deployname" // @Success 200 {object} types.ResponseWithTotal{data=[]types.DeployRepo,total=int} "OK" // @Failure 400 {object} types.APIBadRequest "Bad request" // @Failure 500 {object} types.APIInternalServerError "Internal server error" @@ -722,6 +723,7 @@ func (h *UserHandler) GetRunServerless(ctx *gin.Context) { req.PageSize = per req.RepoType = types.ModelRepo req.DeployType = types.ServerlessType + req.Query = ctx.Query("search") ds, total, err := h.user.ListServerless(ctx.Request.Context(), req) if err != nil { slog.ErrorContext(ctx.Request.Context(), "Failed to get serverless list", slog.Any("error", err), slog.Any("req", req)) diff --git a/builder/store/database/deploy_task.go b/builder/store/database/deploy_task.go index 677a2af18..90dfba698 100644 --- a/builder/store/database/deploy_task.go +++ b/builder/store/database/deploy_task.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log/slog" + "strings" "time" "github.com/uptrace/bun" @@ -368,13 +369,21 @@ func (s *deployTaskStoreImpl) ListServerless(ctx context.Context, req types.Depl var result []Deploy query := s.db.Operator.Core.NewSelect().Model(&result).Where("type = ?", req.DeployType) query = query.Where("status != ?", common.Deleted) - query = query.Limit(req.PageSize).Offset((req.Page - 1) * req.PageSize) - _, err := query.Exec(ctx, &result) + + searchQuery := strings.TrimSpace(req.Query) + if searchQuery != "" { + searchPattern := "%" + strings.ToLower(searchQuery) + "%" + query = query.Where("LOWER(deploy_name) LIKE ? OR LOWER(git_path) LIKE ?", searchPattern, searchPattern) + } + + total, err := query.Count(ctx) if err != nil { err = errorx.HandleDBError(err, nil) return nil, 0, err } - total, err := query.Count(ctx) + + query = query.Limit(req.PageSize).Offset((req.Page - 1) * req.PageSize) + _, err = query.Exec(ctx, &result) if err != nil { err = errorx.HandleDBError(err, nil) return nil, 0, err diff --git a/builder/store/database/deploy_task_test.go b/builder/store/database/deploy_task_test.go index d3258cdd7..89113a27e 100644 --- a/builder/store/database/deploy_task_test.go +++ b/builder/store/database/deploy_task_test.go @@ -653,3 +653,306 @@ func TestDeployTaskStore_DeleteDeployByID(t *testing.T) { err = store.DeleteDeployByID(ctx, 100, 999999) require.NotNil(t, err) } + +func TestDeployTaskStore_GetLatestDeploysBySpaceIDs(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + store := database.NewDeployTaskStoreWithDB(db) + + // Test with empty spaceIDs + result, err := store.GetLatestDeploysBySpaceIDs(ctx, []int64{}) + require.Nil(t, err) + require.NotNil(t, result) + require.Equal(t, 0, len(result)) + + // Create test data: multiple deploys for different space IDs + // Space 100: 3 deploys (should return the latest) + // Space 200: 2 deploys (should return the latest) + // Space 300: 1 deploy (should return that one) + // Space 400: no deploys (should not appear in result) + + now := time.Now().UTC() + space100Deploys := []database.Deploy{ + {SpaceID: 100, DeployName: "space100-old", SvcName: "svc100-1", UserID: 1, RepoID: 1, GitPath: "test", GitBranch: "main", Template: "test", Hardware: "test"}, + {SpaceID: 100, DeployName: "space100-middle", SvcName: "svc100-2", UserID: 1, RepoID: 1, GitPath: "test", GitBranch: "main", Template: "test", Hardware: "test"}, + {SpaceID: 100, DeployName: "space100-latest", SvcName: "svc100-3", UserID: 1, RepoID: 1, GitPath: "test", GitBranch: "main", Template: "test", Hardware: "test"}, + } + + space200Deploys := []database.Deploy{ + {SpaceID: 200, DeployName: "space200-old", SvcName: "svc200-1", UserID: 1, RepoID: 2, GitPath: "test", GitBranch: "main", Template: "test", Hardware: "test"}, + {SpaceID: 200, DeployName: "space200-latest", SvcName: "svc200-2", UserID: 1, RepoID: 2, GitPath: "test", GitBranch: "main", Template: "test", Hardware: "test"}, + } + + space300Deploy := database.Deploy{ + SpaceID: 300, DeployName: "space300-single", SvcName: "svc300-1", UserID: 1, RepoID: 3, GitPath: "test", GitBranch: "main", Template: "test", Hardware: "test", + } + + // Create deploys with different timestamps + for i, dp := range space100Deploys { + err := store.CreateDeploy(ctx, &dp) + require.Nil(t, err) + // Set created_at to different times (oldest first) + _, err = db.BunDB.ExecContext(ctx, "UPDATE deploys SET created_at = ?, updated_at = ? WHERE id = ?", + now.Add(-time.Duration(3-i)*time.Hour), now.Add(-time.Duration(3-i)*time.Hour), dp.ID) + require.NoError(t, err) + } + + for i, dp := range space200Deploys { + err := store.CreateDeploy(ctx, &dp) + require.Nil(t, err) + // Set created_at to different times (oldest first) + _, err = db.BunDB.ExecContext(ctx, "UPDATE deploys SET created_at = ?, updated_at = ? WHERE id = ?", + now.Add(-time.Duration(2-i)*time.Hour), now.Add(-time.Duration(2-i)*time.Hour), dp.ID) + require.NoError(t, err) + } + + err = store.CreateDeploy(ctx, &space300Deploy) + require.Nil(t, err) + + // Test: Get latest deploys for space 100, 200, 300, 400 + spaceIDs := []int64{100, 200, 300, 400} + result, err = store.GetLatestDeploysBySpaceIDs(ctx, spaceIDs) + require.Nil(t, err) + require.NotNil(t, result) + + // Should have 3 results (space 400 has no deploys, so won't appear) + require.Equal(t, 3, len(result)) + + // Verify space 100 has the latest deploy + deploy100, exists := result[100] + require.True(t, exists) + require.NotNil(t, deploy100) + require.Equal(t, "space100-latest", deploy100.DeployName) + require.Equal(t, "svc100-3", deploy100.SvcName) + + // Verify space 200 has the latest deploy + deploy200, exists := result[200] + require.True(t, exists) + require.NotNil(t, deploy200) + require.Equal(t, "space200-latest", deploy200.DeployName) + require.Equal(t, "svc200-2", deploy200.SvcName) + + // Verify space 300 has its deploy + deploy300, exists := result[300] + require.True(t, exists) + require.NotNil(t, deploy300) + require.Equal(t, "space300-single", deploy300.DeployName) + require.Equal(t, "svc300-1", deploy300.SvcName) + + // Verify space 400 is not in the result (no deploys) + _, exists = result[400] + require.False(t, exists) + + // Test with only space IDs that don't exist + result, err = store.GetLatestDeploysBySpaceIDs(ctx, []int64{999, 998}) + require.Nil(t, err) + require.NotNil(t, result) + require.Equal(t, 0, len(result)) +} + +func TestDeployTaskStore_ListServerless_Search(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + + store := database.NewDeployTaskStoreWithDB(db) + + // Create test serverless deploys with different deploy names and git paths + deploys := []database.Deploy{ + { + DeployName: "qwen-model-deploy", + GitPath: "models_namespace1/qwen-model", + GitBranch: "main", + Template: "test", + Hardware: "test", + Type: types.ServerlessType, + Status: common.Running, + RepoID: 1, + UserID: 1, + SpaceID: 0, + SvcName: "svc1", + }, + { + DeployName: "test-deploy", + GitPath: "models_namespace2/test-model", + GitBranch: "main", + Template: "test", + Hardware: "test", + Type: types.ServerlessType, + Status: common.Running, + RepoID: 2, + UserID: 1, + SpaceID: 0, + SvcName: "svc2", + }, + { + DeployName: "QWEN-Deploy-Upper", + GitPath: "models_namespace3/another-model", + GitBranch: "main", + Template: "test", + Hardware: "test", + Type: types.ServerlessType, + Status: common.Running, + RepoID: 3, + UserID: 1, + SpaceID: 0, + SvcName: "svc3", + }, + { + DeployName: "other-deploy", + GitPath: "models_namespace4/qwen-other", + GitBranch: "main", + Template: "test", + Hardware: "test", + Type: types.ServerlessType, + Status: common.Running, + RepoID: 4, + UserID: 1, + SpaceID: 0, + SvcName: "svc4", + }, + { + DeployName: "deleted-deploy", + GitPath: "models_namespace5/qwen-deleted", + GitBranch: "main", + Template: "test", + Hardware: "test", + Type: types.ServerlessType, + Status: common.Deleted, + RepoID: 5, + UserID: 1, + SpaceID: 0, + SvcName: "svc5", + }, + { + DeployName: "non-serverless", + GitPath: "models_namespace6/qwen-non-serverless", + GitBranch: "main", + Template: "test", + Hardware: "test", + Type: types.InferenceType, + Status: common.Running, + RepoID: 6, + UserID: 1, + SpaceID: 0, + SvcName: "svc6", + }, + } + + for _, dp := range deploys { + err := store.CreateDeploy(ctx, &dp) + require.Nil(t, err) + } + + // Test 1: List all serverless (no search) + dps, total, err := store.ListServerless(ctx, types.DeployReq{ + DeployType: types.ServerlessType, + PageOpts: types.PageOpts{ + Page: 1, + PageSize: 10, + }, + }) + require.Nil(t, err) + require.Equal(t, 4, total) // 4 serverless deploys (excluding deleted and non-serverless) + require.Equal(t, 4, len(dps)) + + // Test 2: Search by deploy_name (case-insensitive) + dps, total, err = store.ListServerless(ctx, types.DeployReq{ + DeployType: types.ServerlessType, + Query: "qwen", + PageOpts: types.PageOpts{ + Page: 1, + PageSize: 10, + }, + }) + require.Nil(t, err) + require.Equal(t, 3, total) // qwen-model-deploy, QWEN-Deploy-Upper, qwen-other (in git_path) + require.Equal(t, 3, len(dps)) + deployNames := []string{} + for _, dp := range dps { + deployNames = append(deployNames, dp.DeployName) + } + require.Contains(t, deployNames, "qwen-model-deploy") + require.Contains(t, deployNames, "QWEN-Deploy-Upper") + require.Contains(t, deployNames, "other-deploy") // matches git_path + + // Test 3: Search by git_path + dps, total, err = store.ListServerless(ctx, types.DeployReq{ + DeployType: types.ServerlessType, + Query: "namespace2", + PageOpts: types.PageOpts{ + Page: 1, + PageSize: 10, + }, + }) + require.Nil(t, err) + require.Equal(t, 1, total) + require.Equal(t, 1, len(dps)) + require.Equal(t, "test-deploy", dps[0].DeployName) + + // Test 4: Search with uppercase (case-insensitive) + dps, total, err = store.ListServerless(ctx, types.DeployReq{ + DeployType: types.ServerlessType, + Query: "QWEN", + PageOpts: types.PageOpts{ + Page: 1, + PageSize: 10, + }, + }) + require.Nil(t, err) + require.Equal(t, 3, total) // Should match lowercase and uppercase + require.Equal(t, 3, len(dps)) + + // Test 5: Search with empty string (should return all) + dps, total, err = store.ListServerless(ctx, types.DeployReq{ + DeployType: types.ServerlessType, + Query: "", + PageOpts: types.PageOpts{ + Page: 1, + PageSize: 10, + }, + }) + require.Nil(t, err) + require.Equal(t, 4, total) + require.Equal(t, 4, len(dps)) + + // Test 6: Search with whitespace (should be trimmed and return all) + dps, total, err = store.ListServerless(ctx, types.DeployReq{ + DeployType: types.ServerlessType, + Query: " ", + PageOpts: types.PageOpts{ + Page: 1, + PageSize: 10, + }, + }) + require.Nil(t, err) + require.Equal(t, 4, total) + require.Equal(t, 4, len(dps)) + + // Test 7: Search with no matches + dps, total, err = store.ListServerless(ctx, types.DeployReq{ + DeployType: types.ServerlessType, + Query: "nonexistent", + PageOpts: types.PageOpts{ + Page: 1, + PageSize: 10, + }, + }) + require.Nil(t, err) + require.Equal(t, 0, total) + require.Equal(t, 0, len(dps)) + + // Test 8: Search with pagination + dps, total, err = store.ListServerless(ctx, types.DeployReq{ + DeployType: types.ServerlessType, + Query: "qwen", + PageOpts: types.PageOpts{ + Page: 1, + PageSize: 2, + }, + }) + require.Nil(t, err) + require.Equal(t, 3, total) // Total should be 3 + require.Equal(t, 2, len(dps)) // But only 2 per page +}