This is an automated email from the ASF dual-hosted git repository.
klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
The following commit(s) were added to refs/heads/main by this push:
new 691b14f01 feat: support teambition plugin (#8466)
691b14f01 is described below
commit 691b14f01750246db097b3c0466c9a34d470148a
Author: zfan <[email protected]>
AuthorDate: Fri Jun 20 12:12:41 2025 +0800
feat: support teambition plugin (#8466)
* feat: support teambition plugin
* fix: run CI
* fix: typo
---
backend/plugins/teambition/api/blueprint_v200.go | 4 +-
backend/plugins/teambition/api/init.go | 11 +-
backend/plugins/teambition/api/remote_api.go | 170 +++++++++++++++++++++
backend/plugins/teambition/api/scope_api.go | 107 +++++++++++++
backend/plugins/teambition/api/scope_config_api.go | 111 ++++++++++++++
backend/plugins/teambition/impl/impl.go | 29 +++-
.../migrationscripts/20250529_add_app_id_back.go | 70 +++++++++
.../teambition/models/migrationscripts/register.go | 1 +
backend/plugins/teambition/models/project.go | 14 +-
backend/plugins/teambition/models/scope_config.go | 40 +++++
.../plugins/teambition/tasks/project_convertor.go | 15 +-
backend/plugins/teambition/tasks/shared.go | 21 ---
backend/plugins/teambition/tasks/task_collector.go | 5 +-
backend/plugins/teambition/tasks/task_converter.go | 43 +++---
backend/plugins/teambition/tasks/task_extractor.go | 3 +-
.../teambition/tasks/task_scenario_collector.go | 7 +-
.../plugins/teambition/tasks/task_tag_extractor.go | 3 +-
config-ui/src/plugins/register/index.ts | 2 +
.../plugins/register/teambition/assets/icon.svg | 5 +
.../src/plugins/register/teambition/config.tsx | 97 ++++++++++++
.../register/teambition/connection-fields/index.ts | 20 +++
.../teambition/connection-fields/tenant-id.tsx | 66 ++++++++
.../teambition/connection-fields/tenant-type.tsx | 66 ++++++++
config-ui/src/plugins/register/teambition/index.ts | 19 +++
24 files changed, 863 insertions(+), 66 deletions(-)
diff --git a/backend/plugins/teambition/api/blueprint_v200.go
b/backend/plugins/teambition/api/blueprint_v200.go
index c71d5b644..e85a3dd46 100644
--- a/backend/plugins/teambition/api/blueprint_v200.go
+++ b/backend/plugins/teambition/api/blueprint_v200.go
@@ -53,7 +53,7 @@ func MakeDataSourcePipelinePlanV200(
func makePipelinePlanV200(
subtaskMetas []plugin.SubTaskMeta,
- scopeDetails []*srvhelper.ScopeDetail[models.TeambitionProject,
srvhelper.NoScopeConfig],
+ scopeDetails []*srvhelper.ScopeDetail[models.TeambitionProject,
models.TeambitionScopeConfig],
connection *models.TeambitionConnection,
) (coreModels.PipelinePlan, errors.Error) {
plan := make(coreModels.PipelinePlan, len(scopeDetails))
@@ -85,7 +85,7 @@ func makePipelinePlanV200(
}
func makeScopesV200(
- scopeDetails []*srvhelper.ScopeDetail[models.TeambitionProject,
srvhelper.NoScopeConfig],
+ scopeDetails []*srvhelper.ScopeDetail[models.TeambitionProject,
models.TeambitionScopeConfig],
connection *models.TeambitionConnection,
) ([]plugin.Scope, errors.Error) {
scopes := make([]plugin.Scope, 0, len(scopeDetails))
diff --git a/backend/plugins/teambition/api/init.go
b/backend/plugins/teambition/api/init.go
index f1e3e0c8b..d429405af 100644
--- a/backend/plugins/teambition/api/init.go
+++ b/backend/plugins/teambition/api/init.go
@@ -21,14 +21,16 @@ import (
"github.com/apache/incubator-devlake/core/context"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
- "github.com/apache/incubator-devlake/helpers/srvhelper"
"github.com/apache/incubator-devlake/plugins/teambition/models"
"github.com/go-playground/validator/v10"
)
var vld *validator.Validate
-var dsHelper *api.DsHelper[models.TeambitionConnection,
models.TeambitionProject, srvhelper.NoScopeConfig]
+var dsHelper *api.DsHelper[models.TeambitionConnection,
models.TeambitionProject, models.TeambitionScopeConfig]
var basicRes context.BasicRes
+var raProxy *api.DsRemoteApiProxyHelper[models.TeambitionConnection]
+var raScopeList *api.DsRemoteApiScopeListHelper[models.TeambitionConnection,
models.TeambitionProject, TeambitionPagination]
+var raScopeSearch
*api.DsRemoteApiScopeSearchHelper[models.TeambitionConnection,
models.TeambitionProject]
func Init(br context.BasicRes, p plugin.PluginMeta) {
basicRes = br
@@ -36,7 +38,7 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
dsHelper = api.NewDataSourceHelper[
models.TeambitionConnection,
models.TeambitionProject,
- srvhelper.NoScopeConfig,
+ models.TeambitionScopeConfig,
](
br,
p.Name(),
@@ -47,4 +49,7 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
nil,
nil,
)
+ raProxy = api.NewDsRemoteApiProxyHelper(dsHelper.ConnApi.ModelApiHelper)
+ raScopeList = api.NewDsRemoteApiScopeListHelper(raProxy,
listTeambitionRemoteScopes)
+ raScopeSearch = api.NewDsRemoteApiScopeSearchHelper(raProxy,
searchTeambitionRemoteProjects)
}
diff --git a/backend/plugins/teambition/api/remote_api.go
b/backend/plugins/teambition/api/remote_api.go
new file mode 100644
index 000000000..97ced7352
--- /dev/null
+++ b/backend/plugins/teambition/api/remote_api.go
@@ -0,0 +1,170 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+ "fmt"
+ "net/url"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ dsmodels
"github.com/apache/incubator-devlake/helpers/pluginhelper/api/models"
+ "github.com/apache/incubator-devlake/plugins/teambition/models"
+)
+
+type TeambitionPagination struct {
+ PageToken string `json:"pageToken"`
+ PageSize int `json:"pageSize"`
+}
+
+func queryTeambitionProjects(
+ apiClient plugin.ApiClient,
+ keyword string,
+ page TeambitionPagination,
+) (
+ children []dsmodels.DsRemoteApiScopeListEntry[models.TeambitionProject],
+ nextPage *TeambitionPagination,
+ err errors.Error,
+) {
+ if page.PageSize == 0 {
+ page.PageSize = 50
+ }
+ res, err := apiClient.Get("v3/project/query", url.Values{
+ "name": {keyword},
+ "pageSize": {fmt.Sprintf("%v", page.PageSize)},
+ "pageToken": {page.PageToken},
+ }, nil)
+ if err != nil {
+ return
+ }
+ resBody := struct {
+ Result []models.TeambitionProject `json:"result"`
+ NextPageToken string `json:"nextPageToken"`
+ }{}
+ err = api.UnmarshalResponse(res, &resBody)
+ if err != nil {
+ return
+ }
+ for _, project := range resBody.Result {
+ children = append(children,
dsmodels.DsRemoteApiScopeListEntry[models.TeambitionProject]{
+ Type: api.RAS_ENTRY_TYPE_SCOPE,
+ Id: fmt.Sprintf("%v", project.Id),
+ ParentId: nil,
+ Name: project.Name,
+ FullName: project.Name,
+ Data: &project,
+ })
+ }
+ if resBody.NextPageToken != "" {
+ nextPage = &TeambitionPagination{
+ PageToken: resBody.NextPageToken,
+ PageSize: page.PageSize,
+ }
+ }
+ return
+}
+
+func listTeambitionRemoteScopes(
+ connection *models.TeambitionConnection,
+ apiClient plugin.ApiClient,
+ groupId string,
+ page TeambitionPagination,
+) (
+ children []dsmodels.DsRemoteApiScopeListEntry[models.TeambitionProject],
+ nextPage *TeambitionPagination,
+ err errors.Error,
+) {
+ // construct the query and request
+ return queryTeambitionProjects(apiClient, "", page)
+}
+
+func searchTeambitionRemoteProjects(
+ apiClient plugin.ApiClient,
+ params *dsmodels.DsRemoteApiScopeSearchParams,
+) (
+ children []dsmodels.DsRemoteApiScopeListEntry[models.TeambitionProject],
+ err errors.Error,
+) {
+ if params.Page == 0 {
+ params.Page = 1
+ }
+ page := TeambitionPagination{
+ PageSize: params.PageSize,
+ }
+ children, _, err = queryTeambitionProjects(apiClient, params.Search,
page)
+ return
+}
+
+type Entry = dsmodels.DsRemoteApiScopeListEntry[models.TeambitionProject]
+type Node struct {
+ entry *Entry
+}
+type Children []*Node
+
+func (a Children) Len() int { return len(a) }
+func (a Children) Less(i, j int) bool {
+ if a[i].entry.Type != a[j].entry.Type {
+ return a[i].entry.Type < a[j].entry.Type
+ }
+ return a[i].entry.Name < a[j].entry.Name
+}
+func (a Children) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+
+// RemoteScopes list all available scope for users
+// @Summary list all available scope for users
+// @Description list all available scope for users
+// @Tags plugins/tapd
+// @Accept application/json
+// @Param connectionId path int false "connection ID"
+// @Param groupId query string false "group ID"
+// @Param pageToken query string false "page Token"
+// @Success 200 {object} dsmodels.DsRemoteApiScopeList[models.TapdWorkspace]
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/teambition/connections/{connectionId}/remote-scopes [GET]
+func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
errors.Error) {
+ return raScopeList.Get(input)
+}
+
+// SearchRemoteScopes searches scopes on the remote server
+// @Summary searches scopes on the remote server
+// @Description searches scopes on the remote server
+// @Accept application/json
+// @Param connectionId path int false "connection ID"
+// @Param search query string false "search"
+// @Param page query int false "page number"
+// @Param pageSize query int false "page size per page"
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Success 200 {object}
dsmodels.DsRemoteApiScopeList[models.SonarqubeProject] "the parentIds are
always null"
+// @Tags plugins/sonarqube
+// @Router /plugins/sonarqube/connections/{connectionId}/search-remote-scopes
[GET]
+func SearchRemoteScopes(input *plugin.ApiResourceInput)
(*plugin.ApiResourceOutput, errors.Error) {
+ return raScopeSearch.Get(input)
+}
+
+// @Summary Remote server API proxy
+// @Description Forward API requests to the specified remote server
+// @Param connectionId path int true "connection ID"
+// @Param path path string true "path to a API endpoint"
+// @Tags plugins/github
+// @Router /plugins/teambition/connections/{connectionId}/proxy/{path} [GET]
+func Proxy(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
errors.Error) {
+ return raProxy.Proxy(input)
+}
diff --git a/backend/plugins/teambition/api/scope_api.go
b/backend/plugins/teambition/api/scope_api.go
new file mode 100644
index 000000000..918297269
--- /dev/null
+++ b/backend/plugins/teambition/api/scope_api.go
@@ -0,0 +1,107 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+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/teambition/models"
+)
+
+type PutScopesReqBody api.PutScopesReqBody[models.TeambitionProject]
+type ScopeDetail api.ScopeDetail[models.TeambitionProject,
models.TeambitionScopeConfig]
+
+// PutScopes create or update Azure DevOps repo
+// @Summary create or update Azure DevOps repo
+// @Description Create or update Azure DevOps repo
+// @Tags plugins/teambition
+// @Accept application/json
+// @Param connectionId path int true "connection ID"
+// @Param scope body PutScopesReqBody true "json"
+// @Success 200 {object} []models.teambitionRepo
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/teambition/connections/{connectionId}/scopes [PUT]
+func PutScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
errors.Error) {
+ return dsHelper.ScopeApi.PutMultiple(input)
+}
+
+// PatchScope patch to Azure DevOps repo
+// @Summary patch to Azure DevOps repo
+// @Description patch to Azure DevOps repo
+// @Tags plugins/teambition
+// @Accept application/json
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @Param scope body models.teambitionRepo true "json"
+// @Success 200 {object} models.teambitionRepo
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/teambition/connections/{connectionId}/scopes/{scopeId}
[PATCH]
+func PatchScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
errors.Error) {
+ return dsHelper.ScopeApi.Patch(input)
+}
+
+// GetScopes get Azure DevOps repos
+// @Summary get Azure DevOps repos
+// @Description get Azure DevOps repos
+// @Tags plugins/teambition
+// @Param connectionId path int true "connection ID"
+// @Param searchTerm query string false "search term for scope name"
+// @Param pageSize query int false "page size, default 50"
+// @Param page query int false "page size, default 1"
+// @Param blueprints query bool false "also return blueprints using these
scopes as part of the payload"
+// @Success 200 {object} []ScopeDetail
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/teambition/connections/{connectionId}/scopes [GET]
+func GetScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
errors.Error) {
+ return dsHelper.ScopeApi.GetPage(input)
+}
+
+// GetScope get one Azure DevOps repo
+// @Summary get one Azure DevOps repo
+// @Description get one Azure DevOps repo
+// @Tags plugins/teambition
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @Param blueprints query bool false "also return blueprints using these
scopes as part of the payload"
+// @Success 200 {object} ScopeDetail
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/teambition/connections/{connectionId}/scopes/{scopeId}
[GET]
+func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
errors.Error) {
+ return dsHelper.ScopeApi.GetScopeDetail(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the
scope itself
+// @Summary delete plugin data associated with the scope and optionally the
scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/teambition
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @Param delete_data_only query bool false "Only delete the scope data, not
the scope itself"
+// @Success 200 {object} models.teambitionRepo
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 409 {object} srvhelper.DsRefs "References exist to this scope"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/teambition/connections/{connectionId}/scopes/{scopeId}
[DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
errors.Error) {
+ return dsHelper.ScopeApi.Delete(input)
+}
diff --git a/backend/plugins/teambition/api/scope_config_api.go
b/backend/plugins/teambition/api/scope_config_api.go
new file mode 100644
index 000000000..6294cb5c7
--- /dev/null
+++ b/backend/plugins/teambition/api/scope_config_api.go
@@ -0,0 +1,111 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+)
+
+// PostScopeConfig create scope config for Azure DevOps
+// @Summary create scope config for Azure DevOps
+// @Description create scope config for Azure DevOps
+// @Tags plugins/azuredevops
+// @Accept application/json
+// @Param connectionId path int true "connectionId"
+// @Param scopeConfig body models.AzuredevopsScopeConfig true "scope config"
+// @Success 200 {object} models.AzuredevopsScopeConfig
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/azuredevops/connections/{connectionId}/scope-configs [POST]
+func PostScopeConfig(input *plugin.ApiResourceInput)
(*plugin.ApiResourceOutput, errors.Error) {
+ return dsHelper.ScopeConfigApi.Post(input)
+}
+
+// PatchScopeConfig update scope config for Azure DevOps
+// @Summary update scope config for Azure DevOps
+// @Description update scope config for Azure DevOps
+// @Tags plugins/azuredevops
+// @Accept application/json
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Param scopeConfig body models.AzuredevopsScopeConfig true "scope config"
+// @Success 200 {object} models.AzuredevopsScopeConfig
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/azuredevops/connections/{connectionId}/scope-configs/{id}
[PATCH]
+func PatchScopeConfig(input *plugin.ApiResourceInput)
(*plugin.ApiResourceOutput, errors.Error) {
+ return dsHelper.ScopeConfigApi.Patch(input)
+}
+
+// GetScopeConfig return one scope config
+// @Summary return one scope config
+// @Description return one scope config
+// @Tags plugins/azuredevops
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200 {object} models.AzuredevopsScopeConfig
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/azuredevops/connections/{connectionId}/scope-configs/{id}
[GET]
+func GetScopeConfig(input *plugin.ApiResourceInput)
(*plugin.ApiResourceOutput, errors.Error) {
+ return dsHelper.ScopeConfigApi.GetDetail(input)
+}
+
+// GetScopeConfigList return all scope configs
+// @Summary return all scope configs
+// @Description return all scope configs
+// @Tags plugins/azuredevops
+// @Param pageSize query int false "page size, default 50"
+// @Param page query int false "page size, default 1"
+// @Param connectionId path int true "connectionId"
+// @Success 200 {object} []models.AzuredevopsScopeConfig
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/azuredevops/connections/{connectionId}/scope-configs [GET]
+func GetScopeConfigList(input *plugin.ApiResourceInput)
(*plugin.ApiResourceOutput, errors.Error) {
+ return dsHelper.ScopeConfigApi.GetAll(input)
+}
+
+// GetProjectsByScopeConfig return projects details related by scope config
+// @Summary return all related projects
+// @Description return all related projects
+// @Tags plugins/azuredevops
+// @Param id path int true "id"
+// @Param scopeConfigId path int true "scopeConfigId"
+// @Success 200 {object} models.ProjectScopeOutput
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/azuredevops/scope-config/{scopeConfigId}/projects [GET]
+func GetProjectsByScopeConfig(input *plugin.ApiResourceInput)
(*plugin.ApiResourceOutput, errors.Error) {
+ return dsHelper.ScopeConfigApi.GetProjectsByScopeConfig(input)
+}
+
+// DeleteScopeConfig delete a scope config
+// @Summary delete a scope config
+// @Description delete a scope config
+// @Tags plugins/azuredevops
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/azuredevops/connections/{connectionId}/scope-configs/{id}
[DELETE]
+func DeleteScopeConfig(input *plugin.ApiResourceInput)
(*plugin.ApiResourceOutput, errors.Error) {
+ return dsHelper.ScopeConfigApi.Delete(input)
+}
diff --git a/backend/plugins/teambition/impl/impl.go
b/backend/plugins/teambition/impl/impl.go
index 2140dfe6d..a9aa53a72 100644
--- a/backend/plugins/teambition/impl/impl.go
+++ b/backend/plugins/teambition/impl/impl.go
@@ -39,8 +39,10 @@ var _ interface {
plugin.PluginInit
plugin.PluginTask
plugin.PluginApi
- plugin.CloseablePluginTask
+ plugin.PluginModel
plugin.PluginSource
+ plugin.DataSourcePluginBlueprintV200
+ plugin.CloseablePluginTask
} = (*Teambition)(nil)
type Teambition struct{}
@@ -72,6 +74,7 @@ func (p Teambition) GetTablesInfo() []dal.Tabler {
&models.TeambitionProject{},
&models.TeambitionTaskFlowStatus{},
&models.TeambitionTaskScenario{},
+ &models.TeambitionScopeConfig{},
}
}
@@ -180,6 +183,30 @@ func (p Teambition) ApiResources()
map[string]map[string]plugin.ApiResourceHandl
"connections/:connectionId/test": {
"POST": api.TestExistingConnection,
},
+ "connections/:connectionId/remote-scopes": {
+ "GET": api.RemoteScopes,
+ },
+ "connections/:connectionId/search-remote-scopes": {
+ "GET": api.SearchRemoteScopes,
+ },
+ "connections/:connectionId/scopes": {
+ "GET": api.GetScopes,
+ "PUT": api.PutScopes,
+ },
+ "connections/:connectionId/scope-configs/:scopeConfigId": {
+ "PATCH": api.PatchScopeConfig,
+ "GET": api.GetScopeConfig,
+ "DELETE": api.DeleteScopeConfig,
+ },
+ "connections/:connectionId/scope-configs": {
+ "POST": api.PostScopeConfig,
+ "GET": api.GetScopeConfigList,
+ },
+ "connections/:connectionId/scopes/:scopeId": {
+ "GET": api.GetScope,
+ "PATCH": api.PatchScope,
+ "DELETE": api.DeleteScope,
+ },
}
}
diff --git
a/backend/plugins/teambition/models/migrationscripts/20250529_add_app_id_back.go
b/backend/plugins/teambition/models/migrationscripts/20250529_add_app_id_back.go
new file mode 100644
index 000000000..855bab0a3
--- /dev/null
+++
b/backend/plugins/teambition/models/migrationscripts/20250529_add_app_id_back.go
@@ -0,0 +1,70 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+ "github.com/apache/incubator-devlake/core/context"
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ "github.com/apache/incubator-devlake/helpers/migrationhelper"
+)
+
+var _ plugin.MigrationScript = (*addAppIdBack)(nil)
+
+type teambitionConnection20250529 struct {
+ AppId string
+ SecretKey string `gorm:"serializer:encdec"`
+}
+
+func (teambitionConnection20250529) TableName() string {
+ return "_tool_teambition_connections"
+}
+
+type teambitionScopeConfig20250529 struct {
+ Entities []string `gorm:"type:json;serializer:json"
json:"entities" mapstructure:"entities"`
+ ConnectionId uint64 `json:"connectionId" gorm:"index"
validate:"required" mapstructure:"connectionId,omitempty"`
+ Name string `mapstructure:"name" json:"name"
gorm:"type:varchar(255);uniqueIndex" validate:"required"`
+ TypeMappings map[string]string
`mapstructure:"typeMappings,omitempty" json:"typeMappings"
gorm:"serializer:json"`
+ StatusMappings map[string]string
`mapstructure:"statusMappings,omitempty" json:"statusMappings"
gorm:"serializer:json"`
+ BugDueDateField string
`mapstructure:"bugDueDateField,omitempty" json:"bugDueDateField"
gorm:"column:bug_due_date_field"`
+ TaskDueDateField string
`mapstructure:"taskDueDateField,omitempty" json:"taskDueDateField"
gorm:"column:task_due_date_field"`
+ StoryDueDateField string
`mapstructure:"storyDueDateField,omitempty" json:"storyDueDateField"
gorm:"column:story_due_date_field"`
+}
+
+func (t teambitionScopeConfig20250529) TableName() string {
+ return "_tool_teambition_scope_configs"
+}
+
+type addAppIdBack struct{}
+
+func (*addAppIdBack) Up(basicRes context.BasicRes) errors.Error {
+ basicRes.GetLogger().Warn(nil, "*********")
+ err := migrationhelper.AutoMigrateTables(basicRes,
&teambitionConnection20250529{})
+ basicRes.GetLogger().Warn(err, "err connection")
+ err = migrationhelper.AutoMigrateTables(basicRes,
&teambitionScopeConfig20250529{})
+ basicRes.GetLogger().Warn(err, "err scope")
+ return err
+}
+
+func (*addAppIdBack) Version() uint64 {
+ return 20250529165745
+}
+
+func (*addAppIdBack) Name() string {
+ return "add app id back to teambition_connections"
+}
diff --git a/backend/plugins/teambition/models/migrationscripts/register.go
b/backend/plugins/teambition/models/migrationscripts/register.go
index 0f5567d5b..f9914e7a1 100644
--- a/backend/plugins/teambition/models/migrationscripts/register.go
+++ b/backend/plugins/teambition/models/migrationscripts/register.go
@@ -25,5 +25,6 @@ func All() []plugin.MigrationScript {
new(addInitTables),
new(reCreateTeambitionConnections),
new(addScopeConfigId),
+ new(addAppIdBack),
}
}
diff --git a/backend/plugins/teambition/models/project.go
b/backend/plugins/teambition/models/project.go
index 0d50e8f0e..59531c34f 100644
--- a/backend/plugins/teambition/models/project.go
+++ b/backend/plugins/teambition/models/project.go
@@ -22,10 +22,12 @@ import (
"github.com/apache/incubator-devlake/core/plugin"
)
-var _ plugin.ToolLayerScope = (*TeambitionProject)(nil)
+func (t TeambitionProject) ConvertApiScope() plugin.ToolLayerScope {
+ return t
+}
type TeambitionProject struct {
- common.Scope
+ common.Scope `mapstructure:",squash"`
Id string
`gorm:"primaryKey;type:varchar(100)" json:"id"`
Name string `gorm:"type:varchar(255)"
json:"name"`
Logo string `gorm:"type:varchar(255)"
json:"logo"`
@@ -88,3 +90,11 @@ type TeambitionApiParams struct {
OrganizationId string
ProjectId string
}
+
+type TeambitionProjectsResponse struct {
+ Status int `json:"status"`
+ Data []struct {
+ TeambitionProject `json:"Workspace"`
+ } `json:"data"`
+ Info string `json:"info"`
+}
diff --git a/backend/plugins/teambition/models/scope_config.go
b/backend/plugins/teambition/models/scope_config.go
new file mode 100644
index 000000000..d97585cdc
--- /dev/null
+++ b/backend/plugins/teambition/models/scope_config.go
@@ -0,0 +1,40 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+ "github.com/apache/incubator-devlake/core/models/common"
+)
+
+type TeambitionScopeConfig struct {
+ common.ScopeConfig `mapstructure:",squash" json:",inline"
gorm:"embedded"`
+ TypeMappings map[string]string
`mapstructure:"typeMappings,omitempty" json:"typeMappings"
gorm:"serializer:json"`
+ StatusMappings map[string]string
`mapstructure:"statusMappings,omitempty" json:"statusMappings"
gorm:"serializer:json"`
+ BugDueDateField string
`mapstructure:"bugDueDateField,omitempty" json:"bugDueDateField"
gorm:"column:bug_due_date_field"`
+ TaskDueDateField string
`mapstructure:"taskDueDateField,omitempty" json:"taskDueDateField"
gorm:"column:task_due_date_field"`
+ StoryDueDateField string
`mapstructure:"storyDueDateField,omitempty" json:"storyDueDateField"
gorm:"column:story_due_date_field"`
+}
+
+func (t TeambitionScopeConfig) TableName() string {
+ return "_tool_teambition_scope_configs"
+}
+
+func (t *TeambitionScopeConfig) SetConnectionId(c *TeambitionScopeConfig,
connectionId uint64) {
+ c.ConnectionId = connectionId
+ c.ScopeConfig.ConnectionId = connectionId
+}
diff --git a/backend/plugins/teambition/tasks/project_convertor.go
b/backend/plugins/teambition/tasks/project_convertor.go
index 53d6fe94c..abeddf245 100644
--- a/backend/plugins/teambition/tasks/project_convertor.go
+++ b/backend/plugins/teambition/tasks/project_convertor.go
@@ -19,6 +19,8 @@ package tasks
import (
"fmt"
+ "reflect"
+
"github.com/apache/incubator-devlake/core/dal"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/models/domainlayer"
@@ -26,11 +28,10 @@ import (
"github.com/apache/incubator-devlake/core/plugin"
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
"github.com/apache/incubator-devlake/plugins/teambition/models"
- "reflect"
)
var ConvertProjectsMeta = plugin.SubTaskMeta{
- Name: "convertAccounts",
+ Name: "convertProjects",
EntryPoint: ConvertProjects,
EnabledByDefault: true,
Description: "convert teambition projects",
@@ -56,7 +57,7 @@ func ConvertProjects(taskCtx plugin.SubTaskContext)
errors.Error {
Input: cursor,
Convert: func(inputRow interface{}) ([]interface{},
errors.Error) {
userTool := inputRow.(*models.TeambitionProject)
- account := &ticket.Board{
+ board := &ticket.Board{
DomainEntity: domainlayer.DomainEntity{
Id:
getProjectIdGen().Generate(data.Options.ConnectionId, userTool.Id),
},
@@ -65,9 +66,12 @@ func ConvertProjects(taskCtx plugin.SubTaskContext)
errors.Error {
Url:
fmt.Sprintf("https://www.teambition.com/project/%s", userTool.Id),
CreatedDate: userTool.Created.ToNullableTime(),
}
-
+ err := db.CreateOrUpdate(board)
+ if err != nil {
+ return nil, err
+ }
return []interface{}{
- account,
+ board,
}, nil
},
})
@@ -75,6 +79,5 @@ func ConvertProjects(taskCtx plugin.SubTaskContext)
errors.Error {
if err != nil {
return err
}
-
return converter.Execute()
}
diff --git a/backend/plugins/teambition/tasks/shared.go
b/backend/plugins/teambition/tasks/shared.go
index 9831c69b0..7fbf56efe 100644
--- a/backend/plugins/teambition/tasks/shared.go
+++ b/backend/plugins/teambition/tasks/shared.go
@@ -18,8 +18,6 @@ limitations under the License.
package tasks
import (
- "strings"
-
"github.com/apache/incubator-devlake/core/dal"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
@@ -103,25 +101,6 @@ func CreateRawDataSubTaskArgs(taskCtx
plugin.SubTaskContext, rawTable string) (*
return rawDataSubTaskArgs, &filteredData
}
-func getStdTypeMappings(data *TeambitionTaskData) map[string]string {
- stdTypeMappings := make(map[string]string)
- for userType, stdType := range
data.Options.TransformationRules.TypeMappings {
- stdTypeMappings[userType] =
strings.ToUpper(stdType.StandardType)
- }
- return stdTypeMappings
-}
-
-func getStatusMapping(data *TeambitionTaskData) map[string]string {
- statusMapping := make(map[string]string)
- mapping := data.Options.TransformationRules.StatusMappings
- for std, orig := range mapping {
- for _, v := range orig {
- statusMapping[v] = std
- }
- }
- return statusMapping
-}
-
func FindAccountById(db dal.Dal, accountId string) (*models.TeambitionAccount,
errors.Error) {
if accountId == "" {
return nil, errors.Default.New("account id must not empty")
diff --git a/backend/plugins/teambition/tasks/task_collector.go
b/backend/plugins/teambition/tasks/task_collector.go
index 14ab16557..429674990 100644
--- a/backend/plugins/teambition/tasks/task_collector.go
+++ b/backend/plugins/teambition/tasks/task_collector.go
@@ -20,11 +20,12 @@ package tasks
import (
"encoding/json"
"fmt"
+ "net/http"
+ "net/url"
+
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
- "net/http"
- "net/url"
)
const RAW_TASK_TABLE = "teambition_api_tasks"
diff --git a/backend/plugins/teambition/tasks/task_converter.go
b/backend/plugins/teambition/tasks/task_converter.go
index fd212d379..aa2071d9c 100644
--- a/backend/plugins/teambition/tasks/task_converter.go
+++ b/backend/plugins/teambition/tasks/task_converter.go
@@ -21,6 +21,7 @@ import (
"fmt"
"reflect"
"strconv"
+ "strings"
"time"
"github.com/apache/incubator-devlake/core/dal"
@@ -97,36 +98,30 @@ func ConvertTasks(taskCtx plugin.SubTaskContext)
errors.Error {
issue.OriginalProject = p.Name
}
- stdStatusMappings := getStatusMapping(data)
if taskflowstatus, err := FindTaskFlowStatusById(db,
userTool.TfsId); err == nil {
issue.OriginalStatus = taskflowstatus.Name
- if v, ok :=
stdStatusMappings[taskflowstatus.Name]; ok {
- issue.Status = v
- } else {
- switch taskflowstatus.Kind {
- case "start":
- issue.Status = ticket.TODO
- case "unset":
- issue.Status =
ticket.IN_PROGRESS
- case "end":
- issue.Status = ticket.DONE
- }
+ switch strings.ToUpper(taskflowstatus.Kind) {
+ case "START":
+ issue.Status = ticket.TODO
+ case "UNSET":
+ issue.Status = ticket.IN_PROGRESS
+ case "END":
+ issue.Status = ticket.DONE
+ }
+ if issue.Status == "" {
+ issue.Status =
strings.ToUpper(taskflowstatus.Kind)
}
}
- stdTypeMappings := getStdTypeMappings(data)
+
if scenario, err := FindTaskScenarioById(db,
userTool.SfcId); err == nil {
issue.OriginalType = scenario.Name
- if v, ok := stdTypeMappings[scenario.Name]; ok {
- issue.Type = v
- } else {
- switch scenario.Source {
- case "application.bug":
- issue.Type = ticket.BUG
- case "application.story":
- issue.Type = ticket.REQUIREMENT
- case "application.risk":
- issue.Type = ticket.INCIDENT
- }
+ switch scenario.Source {
+ case "application.bug":
+ issue.Type = ticket.BUG
+ case "application.story":
+ issue.Type = ticket.REQUIREMENT
+ case "application.risk":
+ issue.Type = ticket.INCIDENT
}
}
diff --git a/backend/plugins/teambition/tasks/task_extractor.go
b/backend/plugins/teambition/tasks/task_extractor.go
index a2c5a721b..abe14653c 100644
--- a/backend/plugins/teambition/tasks/task_extractor.go
+++ b/backend/plugins/teambition/tasks/task_extractor.go
@@ -19,6 +19,7 @@ package tasks
import (
"encoding/json"
+
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -54,7 +55,7 @@ func ExtractTasks(taskCtx plugin.SubTaskContext) errors.Error
{
ConnectionId: data.Options.ConnectionId,
TaskId: userRes.Id,
TaskTagId: tagId,
- ProjectId: userRes.ProjectId,
+ ProjectId: data.Options.ProjectId,
}
results = append(results, taskTag)
}
diff --git a/backend/plugins/teambition/tasks/task_scenario_collector.go
b/backend/plugins/teambition/tasks/task_scenario_collector.go
index 5f89b1343..79348c54f 100644
--- a/backend/plugins/teambition/tasks/task_scenario_collector.go
+++ b/backend/plugins/teambition/tasks/task_scenario_collector.go
@@ -20,11 +20,12 @@ package tasks
import (
"encoding/json"
"fmt"
+ "net/http"
+ "net/url"
+
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
- "net/http"
- "net/url"
)
const RAW_TASK_SCENARIOS_TABLE = "teambition_api_task_scenarios"
@@ -32,7 +33,7 @@ const RAW_TASK_SCENARIOS_TABLE =
"teambition_api_task_scenarios"
var _ plugin.SubTaskEntryPoint = CollectTaskScenarios
var CollectTaskScenariosMeta = plugin.SubTaskMeta{
- Name: "collect task flow status",
+ Name: "collect task scenario",
EntryPoint: CollectTaskScenarios,
EnabledByDefault: true,
Description: "collect teambition task flow scenarios",
diff --git a/backend/plugins/teambition/tasks/task_tag_extractor.go
b/backend/plugins/teambition/tasks/task_tag_extractor.go
index 423d7432a..b7fdfbdcc 100644
--- a/backend/plugins/teambition/tasks/task_tag_extractor.go
+++ b/backend/plugins/teambition/tasks/task_tag_extractor.go
@@ -19,6 +19,7 @@ package tasks
import (
"encoding/json"
+
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -49,7 +50,7 @@ func ExtractTaskTags(taskCtx plugin.SubTaskContext)
errors.Error {
ConnectionId: data.Options.ConnectionId,
Id: userRes.Id,
Name: userRes.Name,
- ProjectId: userRes.ProjectId,
+ ProjectId: data.Options.ProjectId,
IsArchived: userRes.IsArchived,
Updated: userRes.Updated,
Created: userRes.Created,
diff --git a/config-ui/src/plugins/register/index.ts
b/config-ui/src/plugins/register/index.ts
index ca06b5d1d..c97d72993 100644
--- a/config-ui/src/plugins/register/index.ts
+++ b/config-ui/src/plugins/register/index.ts
@@ -33,6 +33,7 @@ import { TAPDConfig } from './tapd';
import { WebhookConfig } from './webhook';
import { ZenTaoConfig } from './zentao';
import { OpsgenieConfig } from './opsgenie';
+import { TeambitionConfig } from './teambition';
export const pluginConfigs: IPluginConfig[] = [
AzureConfig,
@@ -51,4 +52,5 @@ export const pluginConfigs: IPluginConfig[] = [
ZenTaoConfig,
WebhookConfig,
OpsgenieConfig,
+ TeambitionConfig,
].sort((a, b) => a.sort - b.sort);
diff --git a/config-ui/src/plugins/register/teambition/assets/icon.svg
b/config-ui/src/plugins/register/teambition/assets/icon.svg
new file mode 100644
index 000000000..93d227f72
--- /dev/null
+++ b/config-ui/src/plugins/register/teambition/assets/icon.svg
@@ -0,0 +1,5 @@
+<svg width="36" height="36" viewBox="0 0 36 36" fill="none"
xmlns="http://www.w3.org/2000/svg">
+<path d="M24.8601 7.75227C24.8606 6.58118 25.3105 5.45494 26.1169
4.60578C26.9234 3.75663 28.025 3.24932 29.1945 3.18845C26.7937 3.06965 24.1226
3 21.345 3C10.6606 3 2 3.98733 2 5.20407C2 6.42082 10.6606 7.40814 21.345
7.40814V12.3243H28.9487C28.7076 12.3059 28.4704 12.2535 28.244 12.1686C27.2724
11.9079 26.414 11.3336 25.8021 10.5351C25.1902 9.73651 24.8591 8.7583 24.8601
7.75227Z" fill="#BDCEFB"/>
+<path d="M21.345 7.40329C10.6606 7.40329 2 6.41596 2 5.19922V13.1757C2 14.0892
6.86288 14.8717 13.7987 15.2241V30.9066L21.4024 32.3609L21.345 7.40329Z"
fill="#7497F7"/>
+<path d="M29.428 12.3237C31.953 12.3237 34 10.2768 34 7.7517C34 5.22665 31.953
3.17969 29.428 3.17969C26.9029 3.17969 24.856 5.22665 24.856 7.7517C24.856
10.2768 26.9029 12.3237 29.428 12.3237Z" fill="#7497F7"/>
+</svg>
diff --git a/config-ui/src/plugins/register/teambition/config.tsx
b/config-ui/src/plugins/register/teambition/config.tsx
new file mode 100644
index 000000000..9653441cd
--- /dev/null
+++ b/config-ui/src/plugins/register/teambition/config.tsx
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { DOC_URL } from '@/release';
+
+import { IPluginConfig } from '@/types';
+
+import Icon from './assets/icon.svg?react';
+import { ConnectionTenantId, ConnectionTenantType } from './connection-fields';
+
+export const TeambitionConfig: IPluginConfig = {
+ plugin: 'teambition',
+ name: 'Teambition',
+ icon: ({ color }) => <Icon fill={color} />,
+ isBeta: true,
+ sort: 100,
+ connection: {
+ docLink: DOC_URL.PLUGIN.TEAMBITION.BASIS,
+ initialValues: {
+ endpoint: 'https://open.teambition.com/api/',
+ tenantType: 'organization',
+ },
+ fields: [
+ 'name',
+ {
+ key: 'endpoint',
+ subLabel: 'Your Teambition endpoint URL.',
+ },
+ {
+ key: 'appId',
+ label: 'Application App Id',
+ subLabel: 'Your teambition application App Id.',
+ },
+ {
+ key: 'secretKey',
+ label: 'Application Secret Key',
+ subLabel: 'Your teambition application App Secret.',
+ },
+ ({ initialValues, values, errors, setValues, setErrors }: any) => (
+ <ConnectionTenantId
+ key="tenantId"
+ initialValue={initialValues.tenantId}
+ value={values.tenantId}
+ error={errors.tenantId}
+ setValue={(value) => setValues({ tenantId: value })}
+ setError={(error) => setErrors({ tenantId: error })}
+ />
+ ),
+ ({ initialValues, values, errors, setValues, setErrors }: any) => (
+ <ConnectionTenantType
+ key="tenantType"
+ initialValue={initialValues.tenantType}
+ value={values.tenantType}
+ error={errors.tenantType}
+ setValue={(value) => setValues({ tenantType: value })}
+ setError={(error) => setErrors({ tenantType: error })}
+ />
+ ),
+ 'proxy',
+ {
+ key: 'rateLimitPerHour',
+ subLabel:
+ 'By default, DevLake uses dynamic rate limit for optimized data
collection for Teambition. But you can adjust the collection speed by entering
a fixed value. Please note: the rate limit setting applies to all tokens you
have entered above.',
+ learnMore: DOC_URL.PLUGIN.TEAMBITION.RATE_LIMIT,
+ externalInfo: 'Teambition specifies a maximum QPS of 40.',
+ defaultValue: 5000,
+ },
+ ],
+ },
+ dataScope: {
+ searchPlaceholder: 'Please enter at least 3 characters to search',
+ title: 'Projects',
+ millerColumn: {
+ columnCount: 2.5,
+ firstColumnTitle: 'Subgroups/Projects',
+ },
+ },
+ scopeConfig: {
+ entities: ['TICKET'],
+ transformation: {},
+ },
+};
diff --git
a/config-ui/src/plugins/register/teambition/connection-fields/index.ts
b/config-ui/src/plugins/register/teambition/connection-fields/index.ts
new file mode 100644
index 000000000..86a33adbd
--- /dev/null
+++ b/config-ui/src/plugins/register/teambition/connection-fields/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './tenant-type';
+export * from './tenant-id';
diff --git
a/config-ui/src/plugins/register/teambition/connection-fields/tenant-id.tsx
b/config-ui/src/plugins/register/teambition/connection-fields/tenant-id.tsx
new file mode 100644
index 000000000..0455f3883
--- /dev/null
+++ b/config-ui/src/plugins/register/teambition/connection-fields/tenant-id.tsx
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useEffect } from 'react';
+import { Input } from 'antd';
+import { Block } from '@/components';
+
+interface Props {
+ initialValue: string;
+ value: string;
+ error: string;
+ setValue: (value: string) => void;
+ setError: (value: string) => void;
+}
+
+export const ConnectionTenantId = ({ initialValue, value, setValue, setError
}: Props) => {
+ useEffect(() => {
+ setValue(initialValue);
+ }, [initialValue]);
+
+ useEffect(() => {
+ setError(value ? '' : 'TenantId is required');
+ }, [value]);
+
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ setValue(e.target.value);
+ };
+
+ return (
+ <Block title="Tenant Id" description="" required>
+ <Input style={{ width: 386 }} placeholder="Tenant Id" value={value}
onChange={handleChange} />
+ </Block>
+ );
+};
diff --git
a/config-ui/src/plugins/register/teambition/connection-fields/tenant-type.tsx
b/config-ui/src/plugins/register/teambition/connection-fields/tenant-type.tsx
new file mode 100644
index 000000000..42088d375
--- /dev/null
+++
b/config-ui/src/plugins/register/teambition/connection-fields/tenant-type.tsx
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useEffect } from 'react';
+import { Input } from 'antd';
+import { Block } from '@/components';
+
+interface Props {
+ initialValue: string;
+ value: string;
+ error: string;
+ setValue: (value: string) => void;
+ setError: (value: string) => void;
+}
+
+export const ConnectionTenantType = ({ initialValue, value, setValue, setError
}: Props) => {
+ useEffect(() => {
+ setValue(initialValue);
+ }, [initialValue]);
+
+ useEffect(() => {
+ setError(value ? '' : 'TenantType is required');
+ }, [value]);
+
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ setValue(e.target.value);
+ };
+
+ return (
+ <Block title="Tenant type" description="" required>
+ <Input style={{ width: 386 }} placeholder="Tenant type" value={value}
onChange={handleChange} />
+ </Block>
+ );
+};
diff --git a/config-ui/src/plugins/register/teambition/index.ts
b/config-ui/src/plugins/register/teambition/index.ts
new file mode 100644
index 000000000..de415db39
--- /dev/null
+++ b/config-ui/src/plugins/register/teambition/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './config';