This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch kw-5519-remoteapi-dshelper
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git

commit b2b13ca90d97cef873374b73013c0acf3c9f5cee
Merge: df4a07510 f7f808742
Author: Klesh Wong <[email protected]>
AuthorDate: Tue May 14 16:49:49 2024 +0800

    Merge remote-tracking branch 'origin/main' into kw-5519-remoteapi-dshelper

 README.md                                          |  43 +++-
 backend/Makefile                                   |   2 +-
 backend/core/config/config_viper.go                |   1 +
 backend/core/models/common/string_int64.go         |  78 ++++++
 .../20240424_add_subtask_states.go                 |  59 +++++
 backend/core/models/migrationscripts/register.go   |   1 +
 backend/core/models/project.go                     |  15 ++
 backend/core/models/subtask_state.go               |  40 +++
 backend/core/utils/json.go                         |   9 +
 backend/helpers/e2ehelper/data_flow_tester.go      |   5 +-
 backend/helpers/pluginhelper/api/api_client.go     |  66 ++++-
 .../helpers/pluginhelper/api/api_client_test.go    |  65 +++++
 backend/helpers/pluginhelper/api/api_extractor.go  |   2 +-
 ...{api_extractor.go => api_extractor_stateful.go} |  94 ++++---
 .../helpers/pluginhelper/api/batch_save_divider.go |  42 +--
 .../pluginhelper/api/data_convertor_stateful.go    | 129 ++++++++++
 backend/helpers/pluginhelper/api/ds_helper.go      |   2 +-
 .../pluginhelper/api/ds_scope_config_api_helper.go |  11 +
 .../helpers/pluginhelper/api/graphql_collector.go  |   2 +-
 backend/helpers/pluginhelper/api/pipeline_plan.go  |  24 ++
 .../pluginhelper/api/subtask_state_manager.go      | 155 +++++++++++
 .../pluginhelper/api/subtask_state_manager_test.go | 189 ++++++++++++++
 .../helpers/srvhelper/any_model_service_helper.go  |   2 +-
 .../srvhelper/any_scope_config_service_helper.go   |  72 ++++++
 backend/impls/context/default_subtask_context.go   |   6 +-
 backend/impls/dalgorm/dalgorm.go                   |  14 +
 .../azuredevops_go/api/azuredevops/client.go       | 282 +++++++++++++++++++++
 .../client_test.go}                                |   6 +-
 .../azuredevops_go/api/azuredevops/models.go       | 132 ++++++++++
 .../api/{ => azuredevops}/testdata/test.txt        |   0
 .../plugins/azuredevops_go/api/blueprint_v200.go   |  62 ++++-
 .../azuredevops_go/api/blueprint_v200_test.go      |  75 +++++-
 .../plugins/azuredevops_go/api/connection_api.go   |   9 +-
 backend/plugins/azuredevops_go/api/remote_data.go  |  65 -----
 .../plugins/azuredevops_go/api/remote_helper.go    | 223 +++++++++++-----
 .../plugins/azuredevops_go/api/scope_config_api.go |  14 +
 backend/plugins/azuredevops_go/api/vs_client.go    | 137 ----------
 backend/plugins/azuredevops_go/e2e/build_test.go   |   1 +
 .../raw_tables/_raw_azuredevops_go_api_builds.csv  |   5 +-
 .../_raw_azuredevops_go_api_timeline_records.csv   |   6 +-
 .../_tool_azuredevops_go_builds.csv                |   5 +-
 .../_tool_azuredevops_go_timeline_records.csv      |   4 +
 .../e2e/snapshot_tables/cicd_pipeline_commits.csv  |   5 +-
 .../e2e/snapshot_tables/cicd_pipelines.csv         |   5 +-
 .../e2e/snapshot_tables/cicd_tasks.csv             |   4 +
 backend/plugins/azuredevops_go/impl/impl.go        |   6 +-
 backend/plugins/azuredevops_go/models/base.go      |   5 +
 backend/plugins/azuredevops_go/models/build.go     |  16 +-
 .../20240413_add_remote_repo_support.go            |  68 +++++
 .../models/migrationscripts/register.go            |   1 +
 backend/plugins/azuredevops_go/models/repo.go      |  13 +-
 .../azuredevops_go/tasks/ci_cd_build_collector.go  |  12 +-
 .../azuredevops_go/tasks/ci_cd_build_converter.go  |   7 +-
 .../azuredevops_go/tasks/ci_cd_build_extractor.go  |   6 +-
 backend/plugins/azuredevops_go/tasks/task_data.go  |   4 +-
 backend/plugins/bamboo/api/scope_config_api.go     |  14 +
 backend/plugins/bamboo/impl/impl.go                |   3 +
 backend/plugins/bitbucket/api/scope_config_api.go  |  14 +
 backend/plugins/bitbucket/impl/impl.go             |   3 +
 backend/plugins/bitbucket_server/api/remote_api.go |  29 +--
 .../bitbucket_server/api/scope_config_api.go       |  14 +
 backend/plugins/bitbucket_server/impl/impl.go      |   3 +
 backend/plugins/circleci/api/connection_api.go     |   6 +-
 backend/plugins/circleci/api/scope_config_api.go   |  14 +
 backend/plugins/circleci/impl/impl.go              |   5 +-
 .../dora/e2e/change_lead_time/commits_diffs.csv    |  13 +-
 .../e2e/change_lead_time/project_pr_metrics.csv    |   1 +
 .../e2e/change_lead_time/pull_request_comments.csv |  27 +-
 .../e2e/change_lead_time/pull_request_commits.csv  |  27 +-
 .../dora/e2e/change_lead_time/pull_requests.csv    |  15 +-
 .../dora/tasks/change_lead_time_calculator.go      |   1 +
 backend/plugins/gitextractor/gitextractor.go       |   2 +-
 backend/plugins/gitextractor/impl/impl.go          |   2 +-
 .../plugins/gitextractor/parser/clone_gitcli.go    |  62 +++--
 backend/plugins/gitextractor/parser/taskdata.go    |  29 ++-
 backend/plugins/gitextractor/tasks/repo_cloner.go  |   3 +
 backend/plugins/github/api/scope_config_api.go     |  14 +
 backend/plugins/github/impl/impl.go                |   3 +
 backend/plugins/gitlab/api/scope_config_api.go     |  14 +
 backend/plugins/gitlab/impl/impl.go                |   3 +
 backend/plugins/jenkins/api/remote_api.go          |  13 +-
 backend/plugins/jenkins/api/scope_config_api.go    |  14 +
 backend/plugins/jenkins/impl/impl.go               |   3 +
 .../plugins/jenkins/tasks/build_cicd_convertor.go  |   4 +
 backend/plugins/jenkins/tasks/stage_convertor.go   |   4 +
 backend/plugins/jira/api/scope_config_api.go       |  14 +
 backend/plugins/jira/impl/impl.go                  |   3 +
 .../jira/tasks/board_filter_begin_collector.go     |   3 +
 .../jira/tasks/board_filter_end_collector.go       |   3 +-
 .../jira/tasks/issue_changelog_collector.go        |   2 +-
 .../jira/tasks/issue_changelog_convertor.go        |  76 +++---
 .../jira/tasks/issue_changelog_extractor.go        |  15 +-
 backend/plugins/jira/tasks/issue_convertor.go      |  61 ++---
 backend/plugins/jira/tasks/issue_extractor.go      |  24 +-
 .../20240508_modify_commit_character_type.go       |  54 ++++
 .../sonarqube/models/migrationscripts/register.go  |   1 +
 backend/plugins/tapd/api/scope_config_api.go       |  14 +
 backend/plugins/tapd/impl/impl.go                  |   3 +
 .../plugins/tapd/tasks/bug_changelog_collector.go  |   2 +-
 .../plugins/tapd/tasks/bug_changelog_converter.go  |   2 +-
 .../plugins/tapd/tasks/bug_changelog_extractor.go  |   3 +-
 backend/plugins/trello/api/scope_config_api.go     |  14 +
 backend/plugins/trello/impl/impl.go                |   3 +
 backend/plugins/zentao/api/scope_api.go            |   8 +-
 backend/plugins/zentao/api/scope_config_api.go     |  14 +
 backend/plugins/zentao/impl/impl.go                |   3 +
 backend/plugins/zentao/models/task.go              |   2 +-
 .../zentao/tasks/bug_repo_commits_collector.go     |   2 +-
 .../zentao/tasks/bug_repo_commits_convertor.go     |   2 +-
 .../zentao/tasks/bug_repo_commits_extractor.go     |   2 +-
 .../zentao/tasks/story_repo_commits_collector.go   |   2 +-
 .../zentao/tasks/story_repo_commits_convertor.go   |   5 +-
 .../zentao/tasks/story_repo_commits_extractor.go   |   3 +-
 .../zentao/tasks/task_repo_commits_collector.go    |   2 +-
 .../zentao/tasks/task_repo_commits_convertor.go    |   5 +-
 .../zentao/tasks/task_repo_commits_extractor.go    |   3 +-
 .../python/plugins/azuredevops/azuredevops/main.py |   2 +-
 .../plugins/azuredevops/azuredevops/models.py      |   4 +-
 .../python/pydevlake/pydevlake/pipeline_tasks.py   |   5 +-
 backend/server/api/api.go                          |  37 +--
 backend/server/api/project/project.go              |   4 +-
 backend/server/api/router.go                       |   4 +-
 backend/server/services/project.go                 |  32 +++
 .../server/services/remote/plugin/default_api.go   |   3 +
 .../services/remote/plugin/scope_config_api.go     |   4 +
 backend/server/services/task.go                    |   9 +-
 backend/test/e2e/manual/azuredevops/models.go      |   1 +
 backend/test/e2e/remote/helper.go                  |   7 +-
 backend/test/helper/api.go                         |  37 ++-
 backend/test/helper/models.go                      |   1 +
 config-ui/env.example                              |   1 +
 config-ui/index.html                               |   2 +-
 config-ui/nginx.conf                               |   6 +-
 config-ui/package.json                             |   4 +-
 config-ui/src/api/blueprint/index.ts               |   9 +-
 config-ui/src/api/project/index.ts                 |   5 +-
 config-ui/src/api/scope-config/index.ts            |   5 +
 .../tips/index.ts => api/scope-config/types.ts}    |   7 +-
 config-ui/src/app/store.ts                         |   3 +-
 .../action/icon-button/index.tsx}                  |  29 +--
 config-ui/src/components/action/index.ts           |   1 +
 config-ui/src/features/index.ts                    |   1 -
 config-ui/src/features/tips/slice.ts               |  58 -----
 config-ui/src/plugins/components/index.ts          |   2 +
 .../components/plugin-name/index.tsx}              |  45 +++-
 .../plugins/components/scope-config-form/index.tsx |  16 +-
 .../components/scope-config-select/index.tsx       |  31 +--
 .../src/plugins/components/scope-config/index.tsx  | 204 +++++++++++++++
 config-ui/src/plugins/register/github/config.tsx   |   1 -
 .../routes/blueprint/connection-detail/index.tsx   | 138 +++++-----
 .../routes/blueprint/connection-detail/table.tsx   | 102 ++++++++
 .../blueprint/detail/blueprint-detail-page.tsx     |   8 +
 .../components/add-connection-dialog/index.tsx     |  50 ++--
 config-ui/src/routes/blueprint/home/index.tsx      |  15 +-
 config-ui/src/routes/connection/connection.tsx     | 153 +++++++----
 config-ui/src/routes/layout/layout.tsx             |  88 ++-----
 config-ui/src/routes/onboard/components/card.tsx   |   6 +-
 config-ui/src/routes/onboard/index.tsx             |   8 +-
 config-ui/src/routes/onboard/step-0.tsx            |  40 ++-
 config-ui/src/routes/onboard/step-3.tsx            |  20 +-
 config-ui/src/routes/pipeline/components/table.tsx |  10 +-
 config-ui/src/routes/pipeline/components/task.tsx  |   3 +
 config-ui/src/routes/project/detail/index.tsx      |   8 +
 config-ui/src/routes/project/home/index.tsx        |  29 +--
 config-ui/src/vite-env.d.ts                        |   1 +
 config-ui/yarn.lock                                |  73 +++---
 env.example                                        |  12 +-
 grafana/dashboards/DORA.json                       |  27 +-
 grafana/dashboards/EngineeringOverview.json        |   2 +-
 169 files changed, 3300 insertions(+), 1088 deletions(-)

diff --cc backend/helpers/pluginhelper/api/ds_helper.go
index 0a0718bc0,0b14356e3..74f065b6b
--- a/backend/helpers/pluginhelper/api/ds_helper.go
+++ b/backend/helpers/pluginhelper/api/ds_helper.go
@@@ -25,55 -25,8 +25,55 @@@ import 
        "github.com/apache/incubator-devlake/helpers/srvhelper"
  )
  
 +// DsAnyHelper is a helper struct for implementing APIs for data source 
plugin like github/gitlab, etc.
 +// Normally, the plugin will have a connection model, a scope model, and a 
scope config model.
 +//   - connection: holds the data source url and credential etc
 +//   - scope: a scope is a collection of data source objects, like a github 
repo, a gitlab project, etc
 +//   - scope config: configuration of what to collect, how to transform, etc
 +//
 +// The helper provides APIs for CRUD operations on connection, scope, and 
scope config without type information
 +type DsAnyHelper struct {
 +      ConnSrv        *srvhelper.AnyConnectionSrvHelper
 +      ConnApi        *DsAnyConnectionApiHelper
 +      ScopeSrv       *srvhelper.AnyScopeSrvHelper
 +      ScopeApi       *DsAnyScopeApiHelper
 +      ScopeConfigSrv *srvhelper.AnyScopeConfigSrvHelper
 +      ScopeConfigApi *DsAnyScopeConfigApiHelper
 +}
 +
 +// NewDataSourceAnyHelper creates a new DsAnyHelper
 +func NewDataSourceAnyHelper(
 +      basicRes context.BasicRes,
 +      pluginName string,
 +      scopeSearchColumns []string,
 +      connectionSterilizer func(c any) any,
 +      connModelInfo srvhelper.ConnectionModelInfo,
 +      scopeModelInfo srvhelper.ScopeModelInfo,
 +      scopeConfigModelInfo srvhelper.ScopeConfigModelInfo,
 +) *DsAnyHelper {
 +      connSrv := srvhelper.NewAnyConnectionSrvHelper(basicRes, connModelInfo, 
scopeModelInfo, scopeConfigModelInfo, pluginName)
 +      connApi := NewDsAnyConnectionApiHelper(basicRes, connSrv, 
connectionSterilizer)
 +      scopeSrv := srvhelper.NewAnyScopeSrvHelper(basicRes, scopeModelInfo, 
scopeConfigModelInfo, pluginName, scopeSearchColumns)
 +      scopeApi := NewDsAnyScopeApiHelper(basicRes, scopeSrv)
 +      var scopeConfigSrv *srvhelper.AnyScopeConfigSrvHelper
 +      var scopeConfigApi *DsAnyScopeConfigApiHelper
 +      if scopeConfigModelInfo != nil {
-               scopeConfigSrv = srvhelper.NewAnyScopeConfigSrvHelper(basicRes, 
scopeConfigModelInfo, scopeModelInfo)
++              scopeConfigSrv = srvhelper.NewAnyScopeConfigSrvHelper(basicRes, 
scopeConfigModelInfo, scopeModelInfo, pluginName)
 +              scopeConfigApi = NewDsAnyScopeConfigApiHelper(basicRes, 
scopeConfigSrv)
 +      }
 +      return &DsAnyHelper{
 +              ConnSrv:        connSrv,
 +              ConnApi:        connApi,
 +              ScopeSrv:       scopeSrv,
 +              ScopeApi:       scopeApi,
 +              ScopeConfigSrv: scopeConfigSrv,
 +              ScopeConfigApi: scopeConfigApi,
 +      }
 +}
 +
  var noScopeConfig = reflect.TypeOf(new(srvhelper.NoScopeConfig))
  
 +// DsHelper is the Typed version of DsAnyHelper
  type DsHelper[
        C plugin.ToolLayerConnection,
        S plugin.ToolLayerScope,
diff --cc backend/helpers/pluginhelper/api/ds_scope_config_api_helper.go
index af6616391,c93144e45..a58105a7e
--- a/backend/helpers/pluginhelper/api/ds_scope_config_api_helper.go
+++ b/backend/helpers/pluginhelper/api/ds_scope_config_api_helper.go
@@@ -53,7 -57,19 +53,18 @@@ func (scopeConfigApi *DsAnyScopeConfigA
        }, nil
  }
  
 -func (connApi *DsScopeConfigApiHelper[C, S, SC]) 
GetProjectsByScopeConfig(input *plugin.ApiResourceInput) (out 
*plugin.ApiResourceOutput, err errors.Error) {
 -      var scopeConfig *SC
 -      scopeConfig, err = connApi.FindByPk(input)
++func (scopeConfigApi *DsAnyScopeConfigApiHelper) 
GetProjectsByScopeConfig(input *plugin.ApiResourceInput) (out 
*plugin.ApiResourceOutput, err errors.Error) {
++      scopeConfig, err := scopeConfigApi.FindByPkAny(input)
+       if err != nil {
+               return nil, err
+       }
 -      projectDetails := 
errors.Must1(connApi.ScopeConfigSrvHelper.GetProjectsByScopeConfig(input.Params["plugin"],
 scopeConfig))
++      projectDetails := 
errors.Must1(scopeConfigApi.AnyScopeConfigSrvHelper.GetProjectsByScopeConfig(scopeConfig))
+       return &plugin.ApiResourceOutput{
+               Body: projectDetails,
+       }, nil
+ }
+ 
 -func (connApi *DsScopeConfigApiHelper[C, S, SC]) Post(input 
*plugin.ApiResourceInput) (out *plugin.ApiResourceOutput, err errors.Error) {
 +func (scopeConfigApi *DsAnyScopeConfigApiHelper) Post(input 
*plugin.ApiResourceInput) (out *plugin.ApiResourceOutput, err errors.Error) {
        // fix connectionId
        connectionId, err := extractConnectionId(input)
        if err != nil {
diff --cc backend/helpers/srvhelper/any_scope_config_service_helper.go
index bcc2e5e79,c06354d25..bc075acfc
--- a/backend/helpers/srvhelper/any_scope_config_service_helper.go
+++ b/backend/helpers/srvhelper/any_scope_config_service_helper.go
@@@ -32,28 -32,18 +34,31 @@@ func (NoScopeConfig) TableName() strin
  func (NoScopeConfig) ScopeConfigId() uint64           { return 0 }
  func (NoScopeConfig) ScopeConfigConnectionId() uint64 { return 0 }
  
 +type ScopeConfigModelInfo interface {
 +      ModelInfo
 +      GetConnectionId(any) uint64
 +      GetScopeConfigId(any) uint64
 +}
 +
  // ScopeConfigSrvHelper
 -type ScopeConfigSrvHelper[C plugin.ToolLayerConnection, S 
plugin.ToolLayerScope, SC plugin.ToolLayerScopeConfig] struct {
 -      *ModelSrvHelper[SC]
 +type AnyScopeConfigSrvHelper struct {
 +      ScopeConfigModelInfo
 +      ScopeModelInfo
 +      *AnyModelSrvHelper
++      pluginName string
  }
  
 -func NewScopeConfigSrvHelper[
 -      C plugin.ToolLayerConnection,
 -      S plugin.ToolLayerScope,
 -      SC plugin.ToolLayerScopeConfig,
 -](basicRes context.BasicRes, searchColumns []string) *ScopeConfigSrvHelper[C, 
S, SC] {
 -      return &ScopeConfigSrvHelper[C, S, SC]{
 -              ModelSrvHelper: NewModelSrvHelper[SC](basicRes, searchColumns),
 +func NewAnyScopeConfigSrvHelper(
 +      basicRes context.BasicRes,
 +      scopeConfigModelInfo ScopeConfigModelInfo,
 +      scopeModelInfo ScopeModelInfo,
++      pluginName string,
 +) *AnyScopeConfigSrvHelper {
 +      return &AnyScopeConfigSrvHelper{
 +              ScopeConfigModelInfo: scopeConfigModelInfo,
 +              ScopeModelInfo:       scopeModelInfo,
 +              AnyModelSrvHelper:    NewAnyModelSrvHelper(basicRes, 
scopeConfigModelInfo, nil),
++              pluginName:           pluginName,
        }
  }
  
@@@ -66,17 -56,79 +71,84 @@@ func (scopeConfigSrv *AnyScopeConfigSrv
        return scopeConfigs, err
  }
  
 -func (scopeConfigSrv *ScopeConfigSrvHelper[C, S, SC]) 
GetProjectsByScopeConfig(pluginName string, scopeConfig *SC) 
(*models.ProjectScopeOutput, errors.Error) {
++func (scopeConfigSrv *AnyScopeConfigSrvHelper) 
GetProjectsByScopeConfig(scopeConfig any) (*models.ProjectScopeOutput, 
errors.Error) {
+       ps := &models.ProjectScopeOutput{}
+       projectMap := make(map[string]*models.ProjectScope)
+       // 1. get all scopes that are using the scopeConfigId
 -      var scope []*S
 -      err := scopeConfigSrv.db.All(&scope,
 -              dal.Where("scope_config_id = ?", 
(*scopeConfig).ScopeConfigId()),
++      scopes := scopeConfigSrv.ScopeModelInfo.NewSlice()
++      sc := scopeConfig.(plugin.ToolLayerScopeConfig)
++      err := scopeConfigSrv.db.All(&scopes,
++              dal.Where("scope_config_id = ?", sc.ScopeConfigId()),
+       )
+       if err != nil {
+               return nil, err
+       }
 -      for _, s := range scope {
++      slice := reflect.ValueOf(scopes)
++      for i := 0; i < slice.Len(); i++ {
+               // 2. get blueprint id by connection id and scope id
+               bpScope := []*models.BlueprintScope{}
++              s := slice.Index(i).Interface().(plugin.ToolLayerScope)
+               err = scopeConfigSrv.db.All(&bpScope,
 -                      dal.Where("plugin_name = ? and connection_id = ? and 
scope_id = ?", pluginName, (*s).ScopeConnectionId(), (*s).ScopeId()),
++                      dal.Where("plugin_name = ? and connection_id = ? and 
scope_id = ?", scopeConfigSrv.pluginName, s.ScopeConnectionId(), s.ScopeId()),
+               )
+               if err != nil {
+                       return nil, err
+               }
+ 
+               for _, bs := range bpScope {
+                       // 3. get project details by blueprint id
+                       bp := models.Blueprint{}
+                       err = scopeConfigSrv.db.All(&bp,
+                               dal.Where("id = ?", bs.BlueprintId),
+                       )
+                       if err != nil {
+                               return nil, err
+                       }
+                       if project, exists := projectMap[bp.ProjectName]; 
exists {
+                               project.Scopes = append(project.Scopes, struct {
+                                       ScopeID   string `json:"scopeId"`
+                                       ScopeName string `json:"scopeName"`
+                               }{
+                                       ScopeID:   bs.ScopeId,
 -                                      ScopeName: (*s).ScopeName(),
++                                      ScopeName: s.ScopeName(),
+                               })
+                       } else {
+                               projectMap[bp.ProjectName] = 
&models.ProjectScope{
+                                       Name:        bp.ProjectName,
+                                       BlueprintId: bp.ID,
+                                       Scopes: []struct {
+                                               ScopeID   string 
`json:"scopeId"`
+                                               ScopeName string 
`json:"scopeName"`
+                                       }{
+                                               {
+                                                       ScopeID:   bs.ScopeId,
 -                                                      ScopeName: 
(*s).ScopeName(),
++                                                      ScopeName: 
s.ScopeName(),
+                                               },
+                                       },
+                               }
+                       }
+               }
+       }
+       // 4. combine all projects
+       for _, project := range projectMap {
+               ps.Projects = append(ps.Projects, *project)
+       }
+       ps.Count = len(ps.Projects)
+ 
+       return ps, err
+ }
+ 
 -func (scopeConfigSrv *ScopeConfigSrvHelper[C, S, SC]) 
DeleteScopeConfig(scopeConfig *SC) (refs []*S, err errors.Error) {
 -      err = scopeConfigSrv.ModelSrvHelper.NoRunningPipeline(func(tx 
dal.Transaction) errors.Error {
 +func (scopeConfigSrv *AnyScopeConfigSrvHelper) 
DeleteScopeConfigAny(scopeConfig any) (refs any, err errors.Error) {
 +      err = scopeConfigSrv.NoRunningPipeline(func(tx dal.Transaction) 
errors.Error {
                // make sure no scope is using the scopeConfig
 -              sc := *scopeConfig
 +              connectionId := 
scopeConfigSrv.ScopeConfigModelInfo.GetConnectionId(scopeConfig)
 +              scopeConfigId := 
scopeConfigSrv.ScopeConfigModelInfo.GetScopeConfigId(scopeConfig)
 +              refs = scopeConfigSrv.ScopeModelInfo.NewSlice()
                errors.Must(tx.All(
                        &refs,
 -                      dal.Where("connection_id = ? AND scope_config_id = ?", 
sc.ScopeConfigConnectionId(), sc.ScopeConfigId()),
 +                      dal.Where("connection_id = ? AND scope_config_id = ?", 
connectionId, scopeConfigId),
                ))
 -              if len(refs) > 0 {
 +              if reflect.ValueOf(refs).Len() > 0 {
                        return errors.Conflict.New("Please delete all data 
scope(s) before you delete this ScopeConfig.")
                }
                errors.Must(tx.Delete(scopeConfig))
diff --cc backend/plugins/azuredevops_go/api/connection_api.go
index 45e6be6e8,d29cac85b..9386519ec
--- a/backend/plugins/azuredevops_go/api/connection_api.go
+++ b/backend/plugins/azuredevops_go/api/connection_api.go
@@@ -23,8 -21,10 +23,9 @@@ import 
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
        "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       
"github.com/apache/incubator-devlake/plugins/azuredevops_go/api/azuredevops"
        "github.com/apache/incubator-devlake/plugins/azuredevops_go/models"
        "github.com/apache/incubator-devlake/server/api/shared"
 -      "net/http"
  )
  
  type AzuredevopsTestConnResponse struct {
diff --cc backend/server/services/remote/plugin/default_api.go
index 09246ec32,83ead1327..826ae99ef
--- a/backend/server/services/remote/plugin/default_api.go
+++ b/backend/server/services/remote/plugin/default_api.go
@@@ -81,6 -93,28 +81,9 @@@ func GetDefaultAPI
                "connections/:connectionId/search-remote-scopes": {
                        "GET": papi.SearchRemoteScopes,
                },
+               "scope-config/:id/projects": {
+                       "GET": papi.GetProjectsByScopeConfig,
+               },
        }
 -      papi.createScopeHelper()
        return resources
  }
 -
 -func (pa *pluginAPI) createScopeHelper() {
 -      params := &api.ReflectionParameters{
 -              ScopeIdFieldName:  "Id",
 -              ScopeIdColumnName: "id",
 -              RawScopeParamName: "ScopeId",
 -      }
 -      pa.scopeHelper = 
api.NewGenericScopeHelper[remoteModel.RemoteConnection, 
remoteModel.DynamicScopeModel, remoteModel.RemoteScopeConfig](
 -              basicRes,
 -              vld,
 -              pa.connhelper,
 -              NewScopeDatabaseHelperImpl(pa, basicRes, params),
 -              params,
 -              &api.ScopeHelperOptions{
 -                      IsRemote: true,
 -              },
 -      )
 -}
diff --cc backend/server/services/remote/plugin/scope_config_api.go
index 300ba2493,f3a54fd01..aff52938a
--- a/backend/server/services/remote/plugin/scope_config_api.go
+++ b/backend/server/services/remote/plugin/scope_config_api.go
@@@ -31,11 -76,103 +31,15 @@@ func (pa *pluginAPI) PatchScopeConfig(i
  }
  
  func (pa *pluginAPI) GetScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
 -      scopeConfig := pa.scopeConfigType.New()
 -      db := basicRes.GetDal()
 -      connectionId, configId, err := extractConfigParam(input.Params)
 -      if err != nil {
 -              return nil, err
 -      }
 -      err = api.CallDB(db.First, scopeConfig, dal.Where("connection_id = ? 
AND id = ?", connectionId, configId))
 -      if err != nil {
 -              return nil, errors.Default.Wrap(err, "no scope config with 
given id")
 -      }
 -
 -      return &plugin.ApiResourceOutput{Body: scopeConfig.Unwrap()}, nil
 +      return pa.dsHelper.ConnApi.GetDetail(input)
  }
  
+ func (pa *pluginAPI) GetProjectsByScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
 -      db := basicRes.GetDal()
 -      configId, err := strconv.ParseUint(input.Params["id"], 10, 64)
 -      if err != nil {
 -              return nil, errors.BadInput.New("invalid configId")
 -      }
 -
 -      ps := &coreModels.ProjectScopeOutput{}
 -      projectMap := make(map[string]*coreModels.ProjectScope)
 -      // 1. get all scopes that are using the scopeConfigId
 -      scopes := models.NewDynamicScopeModel(pa.scopeType.NewSlice())
 -      err = api.CallDB(db.All, scopes, dal.Where("scope_config_id = ?", 
configId))
 -      if err != nil {
 -              return nil, errors.Default.Wrap(err, "no scope config with 
given id")
 -      }
 -      for _, s := range scopes.UnwrapSlice() {
 -              // 2. get blueprint id by connection id and scope id
 -              scope := models.NewDynamicScopeModel(pa.scopeType)
 -              _ = scope.From(s)
 -              // result = append(result, scope)
 -              bpScope := []*coreModels.BlueprintScope{}
 -              err = db.All(&bpScope,
 -                      dal.Where("plugin_name = ? and connection_id = ? and 
scope_id = ?", input.Params["plugin"], scope.ConnectionId(), scope.ScopeId()),
 -              )
 -              if err != nil {
 -                      return nil, errors.Default.Wrap(err, "no blueprint 
scope with given id")
 -              }
 -              for _, bs := range bpScope {
 -                      // 3. get project details by blueprint id
 -                      bp := coreModels.Blueprint{}
 -                      err = db.All(&bp,
 -                              dal.Where("id = ?", bs.BlueprintId),
 -                      )
 -                      if err != nil {
 -                              return nil, errors.Default.Wrap(err, "no 
blueprint with given id")
 -                      }
 -                      if project, exists := projectMap[bp.ProjectName]; 
exists {
 -                              project.Scopes = append(project.Scopes, struct {
 -                                      ScopeID   string `json:"scopeId"`
 -                                      ScopeName string `json:"scopeName"`
 -                              }{
 -                                      ScopeID:   bs.ScopeId,
 -                                      ScopeName: scope.ScopeName(),
 -                              })
 -                      } else {
 -                              projectMap[bp.ProjectName] = 
&coreModels.ProjectScope{
 -                                      Name:        bp.ProjectName,
 -                                      BlueprintId: bp.ID,
 -                                      Scopes: []struct {
 -                                              ScopeID   string 
`json:"scopeId"`
 -                                              ScopeName string 
`json:"scopeName"`
 -                                      }{
 -                                              {
 -                                                      ScopeID:   bs.ScopeId,
 -                                                      ScopeName: 
scope.ScopeName(),
 -                                              },
 -                                      },
 -                              }
 -                      }
 -              }
 -      }
 -      // 4. combine all projects
 -      for _, project := range projectMap {
 -              ps.Projects = append(ps.Projects, *project)
 -      }
 -      ps.Count = len(ps.Projects)
 -
 -      return &plugin.ApiResourceOutput{Body: ps}, nil
++      return pa.dsHelper.ScopeConfigApi.GetProjectsByScopeConfig(input)
+ }
+ 
  func (pa *pluginAPI) ListScopeConfigs(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
 -      scopeConfigs := pa.scopeConfigType.NewSlice()
 -      limit, offset := api.GetLimitOffset(input.Query, "pageSize", "page")
 -      if limit > 100 {
 -              return nil, errors.BadInput.New("pageSize cannot exceed 100")
 -      }
 -
 -      db := basicRes.GetDal()
 -      err := api.CallDB(db.All, scopeConfigs, dal.Limit(limit), 
dal.Offset(offset))
 -      if err != nil {
 -              return nil, err
 -      }
 -      return &plugin.ApiResourceOutput{Body: scopeConfigs.Unwrap()}, nil
 +      return pa.dsHelper.ScopeConfigApi.GetAll(input)
  }
  
  func (pa *pluginAPI) DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {

Reply via email to