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

likyh 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 4c07a8113 feat(config-ui): support plugin azure devops (#4504)
4c07a8113 is described below

commit 4c07a81131bd63de7053e3c461fa938be583ff14
Author: 青湛 <[email protected]>
AuthorDate: Fri Mar 31 18:32:38 2023 +0800

    feat(config-ui): support plugin azure devops (#4504)
    
    * feat(config-ui): support plugin azure devops
    
    * fix: fix some error to run azure devops in config-ui
    
    * fix(config-ui): adjust the tips for azuredevops token
    
    * fix(config-ui): updated transformationId to transformationName
    
    * fix: fix for linter
    
    * fix(config-ui): token style for azuredevops connection
    
    ---------
    
    Co-authored-by: linyh <[email protected]>
---
 .../plugins/azuredevops/azuredevops/models.py      |   2 +-
 backend/server/services/remote/bridge/cmd.go       |   2 +-
 backend/server/services/remote/models/models.go    |  15 +-
 .../services/remote/plugin/connection_api.go       |   2 +-
 .../server/services/remote/plugin/default_api.go   |   3 +-
 backend/server/services/remote/plugin/scope_api.go |  51 +++++-
 .../remote/plugin/transformation_rule_api.go       |  31 +++-
 .../plugins/components/data-scope-form/index.tsx   |   5 +
 .../components/transformation-form/index.tsx       |   5 +
 .../plugins/components/transformation/index.tsx    |   2 +-
 config-ui/src/plugins/register/azure/config.tsx    |  71 ++++++++
 .../{config.ts => connection-fields/base-url.tsx}  |  24 ++-
 .../azure/{ => connection-fields}/index.ts         |   2 +-
 .../{config.ts => connection-fields/styled.ts}     |  20 ++-
 .../src/plugins/register/azure/data-scope.tsx      |  49 ++++++
 config-ui/src/plugins/register/azure/index.ts      |   2 +
 config-ui/src/plugins/register/azure/styled.ts     |  98 +++++++++++
 .../src/plugins/register/azure/transformation.tsx  | 183 +++++++++++++++++++++
 .../plugins/register/azure/{index.ts => types.ts}  |   6 +-
 19 files changed, 532 insertions(+), 41 deletions(-)

diff --git a/backend/python/plugins/azuredevops/azuredevops/models.py 
b/backend/python/plugins/azuredevops/azuredevops/models.py
index b19782a86..82b2dc134 100644
--- a/backend/python/plugins/azuredevops/azuredevops/models.py
+++ b/backend/python/plugins/azuredevops/azuredevops/models.py
@@ -31,7 +31,7 @@ class AzureDevOpsConnection(Connection):
 
 
 class AzureDevOpsTransformationRule(TransformationRule):
-    refdiff_options: Optional[RefDiffOptions]
+    refdiff: Optional[RefDiffOptions]
     deployment_pattern: Optional[re.Pattern]
     production_pattern: Optional[re.Pattern]
 
diff --git a/backend/server/services/remote/bridge/cmd.go 
b/backend/server/services/remote/bridge/cmd.go
index edcb5ccd6..4dab3c075 100644
--- a/backend/server/services/remote/bridge/cmd.go
+++ b/backend/server/services/remote/bridge/cmd.go
@@ -71,7 +71,7 @@ func (c *CmdInvoker) Call(methodName string, ctx 
plugin.ExecContext, args ...any
        err = response.GetError()
        if err != nil {
                return &CallResult{
-                       Err: errors.Default.Wrap(err, fmt.Sprintf("failed to 
invoke remote function \"%s\"", methodName)),
+                       Err: errors.Default.Wrap(err, fmt.Sprintf("get error 
when invoking remote function %s", methodName)),
                }
        }
        return NewCallResult(response.GetFdOut(), nil)
diff --git a/backend/server/services/remote/models/models.go 
b/backend/server/services/remote/models/models.go
index 1ed047844..de12dbd82 100644
--- a/backend/server/services/remote/models/models.go
+++ b/backend/server/services/remote/models/models.go
@@ -61,18 +61,19 @@ func (d DynamicModelInfo) LoadDynamicTabler(encrypt bool, 
parentModel any) (*mod
 }
 
 type ScopeModel struct {
-       common.NoPKModel
+       common.NoPKModel     `json:"-"`
        Id                   string `gorm:"primarykey;type:varchar(255)" 
json:"id"`
-       ConnectionId         uint64 `gorm:"primaryKey" json:"connection_id"`
+       ConnectionId         uint64 `gorm:"primaryKey" json:"connectionId"`
        Name                 string `json:"name" validate:"required"`
-       TransformationRuleId uint64 `json:"transformation_rule_id"`
+       TransformationRuleId uint64 `json:"transformationRuleId"`
 }
 
 type TransformationModel struct {
-       Id        uint64    `gorm:"primaryKey" json:"id"`
-       Name      string    `json:"name"`
-       CreatedAt time.Time `json:"createdAt"`
-       UpdatedAt time.Time `json:"updatedAt"`
+       ConnectionId uint64    `gorm:"primaryKey" json:"connectionId"`
+       Id           uint64    `gorm:"primaryKey" json:"id"`
+       Name         string    `json:"name"`
+       CreatedAt    time.Time `json:"createdAt"`
+       UpdatedAt    time.Time `json:"updatedAt"`
 }
 
 type SubtaskMeta struct {
diff --git a/backend/server/services/remote/plugin/connection_api.go 
b/backend/server/services/remote/plugin/connection_api.go
index 1c324d292..fc21fe206 100644
--- a/backend/server/services/remote/plugin/connection_api.go
+++ b/backend/server/services/remote/plugin/connection_api.go
@@ -33,7 +33,7 @@ func (pa *pluginAPI) TestConnection(input 
*plugin.ApiResourceInput) (*plugin.Api
                        Success: false,
                        Message: err.Error(),
                }
-               return &plugin.ApiResourceOutput{Body: body, Status: 401}, nil
+               return &plugin.ApiResourceOutput{Body: body, Status: 500}, nil
        } else {
                body := shared.ApiBody{Success: true}
                return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
diff --git a/backend/server/services/remote/plugin/default_api.go 
b/backend/server/services/remote/plugin/default_api.go
index 568ec85da..d5044b251 100644
--- a/backend/server/services/remote/plugin/default_api.go
+++ b/backend/server/services/remote/plugin/default_api.go
@@ -37,7 +37,8 @@ func GetDefaultAPI(
        connType *models.DynamicTabler,
        txRuleType *models.DynamicTabler,
        scopeType *models.DynamicTabler,
-       helper *api.ConnectionApiHelper) 
map[string]map[string]plugin.ApiResourceHandler {
+       helper *api.ConnectionApiHelper,
+) map[string]map[string]plugin.ApiResourceHandler {
        papi := &pluginAPI{
                invoker:    invoker,
                connType:   connType,
diff --git a/backend/server/services/remote/plugin/scope_api.go 
b/backend/server/services/remote/plugin/scope_api.go
index 7477f2aa3..44ea12d6c 100644
--- a/backend/server/services/remote/plugin/scope_api.go
+++ b/backend/server/services/remote/plugin/scope_api.go
@@ -18,6 +18,7 @@ limitations under the License.
 package plugin
 
 import (
+       "encoding/json"
        "net/http"
        "strconv"
 
@@ -33,8 +34,45 @@ import (
 
 // DTO that includes the transformation rule name
 type apiScopeResponse struct {
-       Scope                  any
-       TransformationRuleName string `json:"transformationRuleId,omitempty"`
+       Scope                  any    `json:"-"`
+       TransformationRuleName string `json:"transformationRuleName,omitempty"`
+}
+
+// MarshalJSON make Scope display inline
+func (r apiScopeResponse) MarshalJSON() ([]byte, error) {
+       // encode scope to map
+       scopeBytes, err := json.Marshal(r.Scope)
+       if err != nil {
+               return nil, err
+       }
+       var scopeMap map[string]interface{}
+       err = json.Unmarshal(scopeBytes, &scopeMap)
+       if err != nil {
+               return nil, err
+       }
+
+       // encode other column (transformationRuleName) to map
+       otherBytes, err := json.Marshal(struct {
+               TransformationRuleName string 
`json:"transformationRuleName,omitempty"`
+       }{
+               TransformationRuleName: r.TransformationRuleName,
+       })
+       if err != nil {
+               return nil, err
+       }
+
+       // merge the two maps
+       var merged map[string]interface{}
+       err = json.Unmarshal(otherBytes, &merged)
+       if err != nil {
+               return nil, err
+       }
+       for k, v := range scopeMap {
+               merged[k] = v
+       }
+
+       // encode the merged map to JSON
+       return json.Marshal(merged)
 }
 
 type request struct {
@@ -175,7 +213,10 @@ func (pa *pluginAPI) ListScopes(input 
*plugin.ApiResourceInput) (*plugin.ApiReso
 func (pa *pluginAPI) GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        connectionId, scopeId := extractParam(input.Params)
        if connectionId == 0 {
-               return nil, errors.BadInput.New("invalid path params")
+               return nil, errors.BadInput.New("invalid connectionId")
+       }
+       if scopeId == `` {
+               return nil, errors.BadInput.New("invalid scopeId")
        }
        rawScope := pa.scopeType.New()
        db := basicRes.GetDal()
@@ -195,7 +236,7 @@ func (pa *pluginAPI) GetScope(input 
*plugin.ApiResourceInput) (*plugin.ApiResour
        if scope.TransformationRuleId > 0 {
                err = api.CallDB(db.First, &rule, 
dal.From(pa.txRuleType.TableName()), dal.Where("id = ?", 
scope.TransformationRuleId))
                if err != nil {
-                       return nil, err
+                       return nil, errors.Default.Wrap(err, `no related 
transformationRule for scope`)
                }
        }
        return &plugin.ApiResourceOutput{Body: 
apiScopeResponse{rawScope.Unwrap(), rule.Name}, Status: http.StatusOK}, nil
@@ -208,7 +249,7 @@ func extractParam(params map[string]string) (uint64, 
string) {
 }
 
 func verifyScope(scope map[string]any) errors.Error {
-       if scope["connection_id"].(float64) == 0 {
+       if connectionId, ok := scope["connectionId"]; !ok || 
connectionId.(float64) == 0 {
                return errors.BadInput.New("invalid connectionId")
        }
 
diff --git a/backend/server/services/remote/plugin/transformation_rule_api.go 
b/backend/server/services/remote/plugin/transformation_rule_api.go
index faf2d44c0..8a84beda1 100644
--- a/backend/server/services/remote/plugin/transformation_rule_api.go
+++ b/backend/server/services/remote/plugin/transformation_rule_api.go
@@ -28,7 +28,12 @@ import (
 )
 
 func (pa *pluginAPI) PostTransformationRules(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connectionId, _ := strconv.ParseUint(input.Params["connectionId"], 10, 
64)
+       if connectionId == 0 {
+               return nil, errors.BadInput.New("invalid connectionId")
+       }
        txRule := pa.txRuleType.New()
+       input.Body[`connectionId`] = connectionId
        err := api.Decode(input.Body, txRule, vld)
        if err != nil {
                return nil, errors.BadInput.Wrap(err, "error in decoding 
transformation rule")
@@ -42,18 +47,19 @@ func (pa *pluginAPI) PostTransformationRules(input 
*plugin.ApiResourceInput) (*p
 }
 
 func (pa *pluginAPI) PatchTransformationRule(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       id, err := strconv.ParseUint(input.Params["id"], 10, 64)
+       connectionId, trId, err := extractTrParam(input.Params)
        if err != nil {
-               return nil, errors.Default.Wrap(err, "id should be an integer")
+               return nil, err
        }
 
        txRule := pa.txRuleType.New()
        db := basicRes.GetDal()
-       err = api.CallDB(db.First, txRule, dal.Where("id = ?", id))
+       err = api.CallDB(db.First, txRule, dal.Where("connection_id = ? AND id 
= ?", connectionId, trId))
        if err != nil {
                return nil, errors.Default.Wrap(err, "no transformation rule 
with given id")
        }
 
+       input.Body[`connectionId`] = connectionId
        err = api.Decode(input.Body, txRule, vld)
        if err != nil {
                return nil, errors.Default.Wrap(err, "decoding error")
@@ -65,7 +71,11 @@ func (pa *pluginAPI) PatchTransformationRule(input 
*plugin.ApiResourceInput) (*p
 func (pa *pluginAPI) GetTransformationRule(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        txRule := pa.txRuleType.New()
        db := basicRes.GetDal()
-       err := api.CallDB(db.First, txRule, dal.Where("id = ?", input.Params))
+       connectionId, trId, err := extractTrParam(input.Params)
+       if err != nil {
+               return nil, err
+       }
+       err = api.CallDB(db.First, txRule, dal.Where("connection_id = ? AND id 
= ?", connectionId, trId))
        if err != nil {
                return nil, errors.Default.Wrap(err, "no transformation rule 
with given id")
        }
@@ -87,3 +97,16 @@ func (pa *pluginAPI) ListTransformationRules(input 
*plugin.ApiResourceInput) (*p
        }
        return &plugin.ApiResourceOutput{Body: txRules.Unwrap()}, nil
 }
+
+func extractTrParam(params map[string]string) (connectionId uint64, 
transformationId uint64, err errors.Error) {
+       connectionId, _ = strconv.ParseUint(params["connectionId"], 10, 64)
+       transformationId, _ = strconv.ParseUint(params["id"], 10, 64)
+       if connectionId == 0 {
+               return 0, 0, errors.BadInput.New("invalid connectionId")
+       }
+       if transformationId == 0 {
+               return 0, 0, errors.BadInput.New("invalid transformationId")
+       }
+
+       return connectionId, transformationId, nil
+}
diff --git a/config-ui/src/plugins/components/data-scope-form/index.tsx 
b/config-ui/src/plugins/components/data-scope-form/index.tsx
index adf630099..96cb3a002 100644
--- a/config-ui/src/plugins/components/data-scope-form/index.tsx
+++ b/config-ui/src/plugins/components/data-scope-form/index.tsx
@@ -29,6 +29,7 @@ import { JiraDataScope } from '@/plugins/register/jira';
 import { GitLabDataScope } from '@/plugins/register/gitlab';
 import { JenkinsDataScope } from '@/plugins/register/jenkins';
 import { BitbucketDataScope } from '@/plugins/register/bitbucket';
+import { AzureDataScope } from '@/plugins/register/azure';
 import { SonarQubeDataScope } from '@/plugins/register/sonarqube';
 import { PagerDutyDataScope } from '@/plugins/register/pagerduty';
 import { ZentaoDataScope } from '@/plugins/register/zentao';
@@ -144,6 +145,10 @@ export const DataScopeForm = ({
             <BitbucketDataScope connectionId={connectionId} 
selectedItems={scope} onChangeItems={setScope} />
           )}
 
+          {plugin === 'azuredevops' && (
+            <AzureDataScope connectionId={connectionId} selectedItems={scope} 
onChangeItems={setScope} />
+          )}
+
           {plugin === 'sonarqube' && (
             <SonarQubeDataScope connectionId={connectionId} 
selectedItems={scope} onChangeItems={setScope} />
           )}
diff --git a/config-ui/src/plugins/components/transformation-form/index.tsx 
b/config-ui/src/plugins/components/transformation-form/index.tsx
index 37972cabc..fe26788dc 100644
--- a/config-ui/src/plugins/components/transformation-form/index.tsx
+++ b/config-ui/src/plugins/components/transformation-form/index.tsx
@@ -28,6 +28,7 @@ import { JiraTransformation } from '@/plugins/register/jira';
 import { GitLabTransformation } from '@/plugins/register/gitlab';
 import { JenkinsTransformation } from '@/plugins/register/jenkins';
 import { BitbucketTransformation } from '@/plugins/register/bitbucket';
+import { AzureTransformation } from '@/plugins/register/azure';
 
 import { TIPS_MAP } from './misc';
 import * as API from './api';
@@ -113,6 +114,10 @@ export const TransformationForm = ({ plugin, connectionId, 
scopeId, id, onCancel
           <BitbucketTransformation transformation={transformation} 
setTransformation={setTransformation} />
         )}
 
+        {plugin === 'azuredevops' && (
+          <AzureTransformation transformation={transformation} 
setTransformation={setTransformation} />
+        )}
+
         {plugin === 'tapd' && (
           <TapdTransformation
             connectionId={connectionId}
diff --git a/config-ui/src/plugins/components/transformation/index.tsx 
b/config-ui/src/plugins/components/transformation/index.tsx
index 118d86ccd..535004c08 100644
--- a/config-ui/src/plugins/components/transformation/index.tsx
+++ b/config-ui/src/plugins/components/transformation/index.tsx
@@ -120,7 +120,7 @@ export const Transformation = ({
               { title: 'Data Scope', dataIndex: 'name', key: 'name' },
               {
                 title: 'Transformation',
-                dataIndex: 'transformationRuleId',
+                dataIndex: 'transformationRuleName',
                 key: 'transformation',
                 align: 'center',
                 render: (val, row) => (
diff --git a/config-ui/src/plugins/register/azure/config.tsx 
b/config-ui/src/plugins/register/azure/config.tsx
new file mode 100644
index 000000000..571bf4fbb
--- /dev/null
+++ b/config-ui/src/plugins/register/azure/config.tsx
@@ -0,0 +1,71 @@
+/*
+ * 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 React from 'react';
+
+import { ExternalLink } from '@/components';
+import type { PluginConfigType } from '@/plugins';
+import { PluginType } from '@/plugins';
+
+import Icon from './assets/icon.svg';
+import { BaseURL } from './connection-fields';
+
+export const AzureConfig: PluginConfigType = {
+  type: PluginType.Connection,
+  plugin: 'azuredevops',
+  name: 'Azure DevOps',
+  icon: Icon,
+  sort: 6,
+  connection: {
+    docLink: 'https://devlake.apache.org/docs/Configuration/AzureDevOps',
+    fields: [
+      'name',
+      () => <BaseURL key="base-url" />,
+      {
+        key: 'token',
+        label: 'Personal Access Token',
+        subLabel: (
+          <span>
+            <ExternalLink 
link="https://devlake.apache.org/docs/Configuration/AzureDevOps#auth-tokens";>
+              Learn about how to create a PAT
+            </ExternalLink>{' '}
+            Please select ALL ACCESSIBLE ORGANIZATIONS for the Organization 
field when you create the PAT.
+          </span>
+        ),
+      },
+      'proxy',
+      {
+        key: 'rateLimitPerHour',
+        subLabel:
+          'By default, DevLake uses 18,000 requests/hour for data collection 
for Azure DevOps. But you can adjust the collection speed by setting up your 
desirable rate limit.',
+        learnMore: 
'https://devlake.apache.org/docs/Configuration/AzureDevOps/#custom-rate-limit-optional',
+        externalInfo: 'Azure DevOps does not specify a maximum value of rate 
limit.',
+        maximum: 18000,
+      },
+    ],
+  },
+  entities: ['CODE', 'CODEREVIEW', 'CROSS', 'CICD'],
+  transformation: {
+    deploymentPattern: '(deploy|push-image)',
+    productionPattern: 'production',
+    refdiff: {
+      tagsOrder: 10,
+      tagsPattern: '/v\\d+\\.\\d+(\\.\\d+(-rc)*\\d*)*$/',
+    },
+  },
+};
diff --git a/config-ui/src/plugins/register/azure/config.ts 
b/config-ui/src/plugins/register/azure/connection-fields/base-url.tsx
similarity index 55%
copy from config-ui/src/plugins/register/azure/config.ts
copy to config-ui/src/plugins/register/azure/connection-fields/base-url.tsx
index ebbcbf2e8..d33ca3a09 100644
--- a/config-ui/src/plugins/register/azure/config.ts
+++ b/config-ui/src/plugins/register/azure/connection-fields/base-url.tsx
@@ -16,15 +16,21 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import React from 'react';
+import { FormGroup, RadioGroup, Radio } from '@blueprintjs/core';
 
-import { BasePipelineConfig } from '../base';
+import * as S from './styled';
 
-import Icon from './assets/icon.svg';
-
-export const AzureConfig: PluginConfigType = {
-  ...BasePipelineConfig,
-  plugin: 'azure',
-  name: 'Azure',
-  icon: Icon,
+export const BaseURL = () => {
+  return (
+    <FormGroup label={<S.Label>Azure DevOps Version</S.Label>} 
labelInfo={<S.LabelInfo>*</S.LabelInfo>}>
+      <RadioGroup inline selectedValue="cloud" onChange={() => {}}>
+        <Radio value="cloud">Azure DevOps Cloud</Radio>
+        <Radio value="server" disabled>
+          Azure DevOps Server (not supported)
+        </Radio>
+      </RadioGroup>
+      <p style={{ margin: 0 }}>If you are using Azure DevOps Cloud, you do not 
need to enter the endpoint URL.</p>
+    </FormGroup>
+  );
 };
diff --git a/config-ui/src/plugins/register/azure/index.ts 
b/config-ui/src/plugins/register/azure/connection-fields/index.ts
similarity index 96%
copy from config-ui/src/plugins/register/azure/index.ts
copy to config-ui/src/plugins/register/azure/connection-fields/index.ts
index de415db39..de415ebac 100644
--- a/config-ui/src/plugins/register/azure/index.ts
+++ b/config-ui/src/plugins/register/azure/connection-fields/index.ts
@@ -16,4 +16,4 @@
  *
  */
 
-export * from './config';
+export * from './base-url';
diff --git a/config-ui/src/plugins/register/azure/config.ts 
b/config-ui/src/plugins/register/azure/connection-fields/styled.ts
similarity index 75%
rename from config-ui/src/plugins/register/azure/config.ts
rename to config-ui/src/plugins/register/azure/connection-fields/styled.ts
index ebbcbf2e8..11e47a3d0 100644
--- a/config-ui/src/plugins/register/azure/config.ts
+++ b/config-ui/src/plugins/register/azure/connection-fields/styled.ts
@@ -16,15 +16,17 @@
  *
  */
 
-import type { PluginConfigType } from '@/plugins';
+import styled from 'styled-components';
 
-import { BasePipelineConfig } from '../base';
+export const Label = styled.label`
+  font-size: 16px;
+  font-weight: 600;
+`;
 
-import Icon from './assets/icon.svg';
+export const LabelInfo = styled.i`
+  color: #ff8b8b;
+`;
 
-export const AzureConfig: PluginConfigType = {
-  ...BasePipelineConfig,
-  plugin: 'azure',
-  name: 'Azure',
-  icon: Icon,
-};
+export const LabelDescription = styled.p`
+  margin: 0;
+`;
diff --git a/config-ui/src/plugins/register/azure/data-scope.tsx 
b/config-ui/src/plugins/register/azure/data-scope.tsx
new file mode 100644
index 000000000..41393db42
--- /dev/null
+++ b/config-ui/src/plugins/register/azure/data-scope.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 React, { useMemo } from 'react';
+
+import { DataScopeMillerColumns } from '@/plugins';
+
+import type { AzureScopeType } from './types';
+
+interface Props {
+  connectionId: ID;
+  selectedItems: AzureScopeType[];
+  onChangeItems: (selectedItems: AzureScopeType[]) => void;
+}
+
+export const AzureDataScope = ({ connectionId, onChangeItems, ...props }: 
Props) => {
+  const selectedItems = useMemo(
+    () => props.selectedItems.map((it) => ({ id: `${it.id}`, name: it.name, 
data: it })),
+    [props.selectedItems],
+  );
+
+  return (
+    <>
+      <h4>Add Repositories by Selecting from the Directory</h4>
+      <p>The following directory lists out all repositories in your 
organizations.</p>
+      <DataScopeMillerColumns
+        plugin="azuredevops"
+        connectionId={connectionId}
+        selectedItems={selectedItems}
+        onChangeItems={onChangeItems}
+      />
+    </>
+  );
+};
diff --git a/config-ui/src/plugins/register/azure/index.ts 
b/config-ui/src/plugins/register/azure/index.ts
index de415db39..14fdb67cd 100644
--- a/config-ui/src/plugins/register/azure/index.ts
+++ b/config-ui/src/plugins/register/azure/index.ts
@@ -17,3 +17,5 @@
  */
 
 export * from './config';
+export * from './data-scope';
+export * from './transformation';
diff --git a/config-ui/src/plugins/register/azure/styled.ts 
b/config-ui/src/plugins/register/azure/styled.ts
new file mode 100644
index 000000000..932aa9bc8
--- /dev/null
+++ b/config-ui/src/plugins/register/azure/styled.ts
@@ -0,0 +1,98 @@
+/*
+ * 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 styled from 'styled-components';
+
+export const Transfromation = styled.div`
+  .ci-cd {
+    h3 {
+      margin-top: 16px;
+
+      .bp4-tag {
+        margin-left: 4px;
+      }
+    }
+
+    .radio {
+      padding-left: 20px;
+      margin-bottom: 16px;
+
+      .input {
+        display: flex;
+        align-items: center;
+
+        & + .input {
+          margin-top: 8px;
+        }
+
+        p {
+          color: #292b3f;
+        }
+
+        .bp4-input-group {
+          margin: 0 4px;
+        }
+      }
+    }
+  }
+
+  .additional-settings {
+    h2 {
+      display: flex;
+      align-items: center;
+      cursor: pointer;
+    }
+
+    .radio {
+      display: flex;
+      align-items: center;
+      margin: 8px 0 16px;
+
+      p {
+        margin: 0;
+      }
+
+      .bp4-control {
+        margin: 0;
+      }
+    }
+
+    .refdiff {
+      display: flex;
+      align-items: center;
+      padding-left: 20px;
+
+      .bp4-input-group {
+        margin: 0 8px;
+      }
+    }
+  }
+
+  .bp4-form-group {
+    display: flex;
+    align-items: center;
+
+    .bp4-label {
+      flex: 0 0 140px;
+    }
+
+    .bp4-form-content {
+      flex: auto;
+    }
+  }
+`;
diff --git a/config-ui/src/plugins/register/azure/transformation.tsx 
b/config-ui/src/plugins/register/azure/transformation.tsx
new file mode 100644
index 000000000..60cdcb1fe
--- /dev/null
+++ b/config-ui/src/plugins/register/azure/transformation.tsx
@@ -0,0 +1,183 @@
+/*
+ * 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 React, { useState, useEffect } from 'react';
+import { Tag, RadioGroup, Radio, InputGroup, Icon, Collapse, Intent } from 
'@blueprintjs/core';
+
+import { ExternalLink, HelpTooltip } from '@/components';
+
+import * as S from './styled';
+
+interface Props {
+  transformation: any;
+  setTransformation: React.Dispatch<React.SetStateAction<any>>;
+}
+
+export const AzureTransformation = ({ transformation, setTransformation }: 
Props) => {
+  const [enableCICD, setEnableCICD] = useState(1);
+  const [openAdditionalSettings, setOpenAdditionalSettings] = useState(false);
+
+  useEffect(() => {
+    if (transformation.refdiff) {
+      setOpenAdditionalSettings(true);
+    }
+  }, [transformation]);
+
+  const handleChangeCICDEnable = (e: number) => {
+    if (e === 0) {
+      setTransformation({
+        ...transformation,
+        deploymentPattern: undefined,
+        productionPattern: undefined,
+      });
+    } else {
+      setTransformation({
+        ...transformation,
+        deploymentPattern: '',
+        productionPattern: '',
+      });
+    }
+    setEnableCICD(e);
+  };
+
+  const handleChangeAdditionalSettingsOpen = () => {
+    setOpenAdditionalSettings(!openAdditionalSettings);
+    if (!openAdditionalSettings) {
+      setTransformation({
+        ...transformation,
+        refdiff: null,
+      });
+    }
+  };
+
+  return (
+    <S.Transfromation>
+      <div className="ci-cd">
+        <h2>CI/CD</h2>
+        <h3>
+          <span>Deployment</span>
+          <Tag minimal intent={Intent.PRIMARY} style={{ marginLeft: 4, 
fontWeight: 400 }}>
+            DORA
+          </Tag>
+        </h3>
+        <p>Tell DevLake what CI builds are Deployments.</p>
+        <RadioGroup
+          selectedValue={enableCICD}
+          onChange={(e) => handleChangeCICDEnable(+(e.target as 
HTMLInputElement).value)}
+        >
+          <Radio label="Detect Deployment from Builds in Azure Pipelines" 
value={1} />
+          {enableCICD === 1 && (
+            <div className="radio">
+              <p>
+                Please fill in the following RegEx, as DevLake ONLY accounts 
for deployments in the production
+                environment for DORA metrics. Not sure what an Azure Build is?
+                <ExternalLink 
link="https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/what-is-azure-pipelines?view=azure-devops#continuous-testing";>
+                  See it here
+                </ExternalLink>
+              </p>
+              <div className="input">
+                <p>The Build name that matches</p>
+                <InputGroup
+                  placeholder="(?i)deploy"
+                  value={transformation.deploymentPattern}
+                  onChange={(e) =>
+                    setTransformation({
+                      ...transformation,
+                      deploymentPattern: e.target.value,
+                    })
+                  }
+                />
+                <p>
+                  will be registered as a `Deployment` in DevLake. <span 
style={{ color: '#E34040' }}>*</span>
+                </p>
+              </div>
+              <div className="input">
+                <p>The Build name that matches</p>
+                <InputGroup
+                  placeholder="(?i)production"
+                  value={transformation.productionPattern}
+                  onChange={(e) =>
+                    setTransformation({
+                      ...transformation,
+                      productionPattern: e.target.value,
+                    })
+                  }
+                />
+                <p>
+                  will be registered as a `Deployment` to the Production 
environment in DevLake.
+                  <HelpTooltip content="If you leave this field empty, all 
data will be tagged as in the Production environment. " />
+                </p>
+              </div>
+            </div>
+          )}
+          <Radio label="Not using Builds in Azure Pipelines as Deployments" 
value={0} />
+        </RadioGroup>
+      </div>
+      {/* Additional Settings */}
+      <div className="additional-settings">
+        <h2 onClick={handleChangeAdditionalSettingsOpen}>
+          <Icon icon={!openAdditionalSettings ? 'chevron-up' : 'chevron-down'} 
size={18} />
+          <span>Additional Settings</span>
+        </h2>
+        <Collapse isOpen={openAdditionalSettings}>
+          <div className="radio">
+            <Radio defaultChecked />
+            <p>
+              Enable the <ExternalLink 
link="https://devlake.apache.org/docs/Plugins/refdiff";>RefDiff</ExternalLink>{' 
'}
+              plugin to pre-calculate version-based metrics
+              <HelpTooltip content="Calculate the commits diff between two 
consecutive tags that match the following RegEx. Issues closed by PRs which 
contain these commits will also be calculated. The result will be shown in 
table.refs_commits_diffs and table.refs_issues_diffs." />
+            </p>
+          </div>
+          <div className="refdiff">
+            Compare the last
+            <InputGroup
+              style={{ width: 60 }}
+              placeholder="10"
+              value={transformation.refdiff?.tagsOrder}
+              onChange={(e) =>
+                setTransformation({
+                  ...transformation,
+                  refdiff: {
+                    ...transformation?.refdiff,
+                    tagsOrder: e.target.value,
+                  },
+                })
+              }
+            />
+            tags that match the
+            <InputGroup
+              style={{ width: 200 }}
+              placeholder="v\d+\.\d+(\.\d+(-rc)*\d*)*$"
+              value={transformation.refdiff?.tagsPattern}
+              onChange={(e) =>
+                setTransformation({
+                  ...transformation,
+                  refdiff: {
+                    ...transformation?.refdiff,
+                    tagsPattern: e.target.value,
+                  },
+                })
+              }
+            />
+            for calculation
+          </div>
+        </Collapse>
+      </div>
+    </S.Transfromation>
+  );
+};
diff --git a/config-ui/src/plugins/register/azure/index.ts 
b/config-ui/src/plugins/register/azure/types.ts
similarity index 90%
copy from config-ui/src/plugins/register/azure/index.ts
copy to config-ui/src/plugins/register/azure/types.ts
index de415db39..075e59287 100644
--- a/config-ui/src/plugins/register/azure/index.ts
+++ b/config-ui/src/plugins/register/azure/types.ts
@@ -16,4 +16,8 @@
  *
  */
 
-export * from './config';
+export type AzureScopeType = {
+  connectionId: ID;
+  id: ID;
+  name: string;
+};

Reply via email to